From 2c6320512fb2190546b6fb8feaea3e8531ea7453 Mon Sep 17 00:00:00 2001 From: nizar Date: Tue, 10 Feb 2026 18:21:30 +0100 Subject: [PATCH] up --- .editorconfig | 65 + .gitattributes | 63 + .gitignore | 660 +++++ README.md | 1192 +++++++++ WingsEmu.sln | 867 +++++++ nuget.config | 8 + properties_for_visual_studio.zip | Bin 0 -> 10279 bytes scripts/Database/add-migration.ps1 | 2 + scripts/Database/default-accounts.ps1 | 1 + scripts/Database/remove-last-migration.ps1 | 2 + scripts/Database/update-database.ps1 | 2 + scripts/Docker/MQTT.ps1 | 1 + scripts/Docker/MonboDB.ps1 | 1 + scripts/Docker/PostgreSQL.ps1 | 1 + scripts/Docker/Redis.ps1 | 1 + scripts/translations-update.ps1 | 3 + scripts/update-server-files.ps1 | 73 + srcs/BazaarServer/BazaarServer.csproj | 27 + ...eMaintenanceNotificationMessageConsumer.cs | 24 + .../BazaarServer/DockerGracefulStopService.cs | 54 + srcs/BazaarServer/Managers/BazaarManager.cs | 186 ++ .../Managers/BazaarSearchManager.cs | 719 ++++++ srcs/BazaarServer/Program.cs | 82 + .../RecurrentJobs/BazaarSystem.cs | 25 + srcs/BazaarServer/Services/BazaarService.cs | 243 ++ srcs/BazaarServer/Startup.cs | 70 + .../ServiceFlushAllMessageConsumer.cs | 29 + srcs/DatabaseServer/DatabaseServer.csproj | 26 + .../DockerGracefulStopService.cs | 53 + srcs/DatabaseServer/EnvironmentConsts.cs | 10 + .../Managers/AccountWarehouseManager.cs | 440 ++++ .../Managers/AddWarehouseItemResult.cs | 11 + .../Managers/CharacterManager.cs | 317 +++ .../Managers/IAccountWarehouseManager.cs | 16 + .../Managers/ICharacterManager.cs | 20 + .../Managers/IRankingManager.cs | 42 + .../Managers/ITimeSpaceManager.cs | 14 + .../Managers/MoveWarehouseItemResult.cs | 13 + .../DatabaseServer/Managers/RankingManager.cs | 126 + srcs/DatabaseServer/Managers/SaveRequest.cs | 10 + .../Managers/TimeSpaceManager.cs | 119 + .../Managers/WithdrawWarehouseItemResult.cs | 14 + srcs/DatabaseServer/Program.cs | 104 + .../DatabaseServer/Services/AccountService.cs | 173 ++ .../Services/AccountWarehouseService.cs | 73 + .../Services/CharacterService.cs | 171 ++ .../Services/TimeSpaceService.cs | 31 + srcs/DatabaseServer/Startup.cs | 85 + .../Chat/LogChatMessageMessageFormatter.cs | 40 + .../LogFamilyCreatedEmbedMessageFormatter.cs | 32 + .../LogFamilyCreatedMessageFormatter.cs | 17 + .../LogFamilyDisbandedMessageFormatter.cs | 17 + .../Family/LogFamilyJoinedMessageFormatter.cs | 17 + .../Family/LogFamilyKickedMessageFormatter.cs | 17 + .../Family/LogFamilyLeftMessageFormatter.cs | 17 + .../LogFamilyMessageMessageFormatter.cs | 19 + .../LogInstantBattleStartDiscordConsumer.cs | 68 + .../Item/LogItemGambledMessageFormatter.cs | 17 + .../Item/LogItemUpgradedMessageFormatter.cs | 17 + .../Maintenance/ServiceDownMessageConsumer.cs | 29 + ...eMaintenanceNotificationMessageConsumer.cs | 89 + ...ogMinigameRewardClaimedMessageFormatter.cs | 25 + .../LogMinigameScoreMessageFormatter.cs | 18 + .../LogGMCommandExecutedMessageFormatter.cs | 17 + ...ogPlayerCommandExecutedMessageFormatter.cs | 16 + .../LogPlayerLevelUpMessageFormatter.cs | 17 + .../LogStrangeBehaviorMessageFormatter.cs | 17 + .../Discord/DiscordWebhookConfiguration.cs | 8 + .../Discord/DiscordWebhookLogsService.cs | 80 + .../Discord/IDiscordWebhookLogsService.cs | 13 + srcs/DiscordNotifier/DiscordNotifier.csproj | 28 + .../DockerGracefulStopService.cs | 54 + .../Formatting/DiscordLogExtensions.cs | 25 + .../GenericDiscordEmbedLogConsumer.cs | 32 + .../Formatting/GenericDiscordLogConsumer.cs | 30 + .../Formatting/IDiscordEmbedLogFormatter.cs | 12 + .../Formatting/IDiscordLogFormatter.cs | 8 + srcs/DiscordNotifier/LogType.cs | 45 + srcs/DiscordNotifier/Managers/ItemManager.cs | 44 + srcs/DiscordNotifier/Program.cs | 75 + srcs/DiscordNotifier/Startup.cs | 127 + srcs/DiscordNotifier/StaticHardcodedCode.cs | 7 + .../FamilyAchievementIncrement.cs | 11 + .../FamilyAchievementIncrementEventHandler.cs | 19 + ...milyAchievementIncrementMessageConsumer.cs | 25 + .../Achievements/FamilyAchievementManager.cs | 361 +++ .../Achievements/FamilyMissionIncrement.cs | 10 + .../FamilyMissionIncrementMessageConsumer.cs | 27 + .../FamilyCharacterConnectMessageConsumer.cs | 24 + ...amilyCharacterDisconnectMessageConsumer.cs | 41 + ...yDeclareExperienceGainedMessageConsumer.cs | 48 + .../FamilyDeclareLogsMessageConsumer.cs | 25 + .../Consumers/FamilyHeadSexMessageConsumer.cs | 48 + .../FamilyMemberTodayMessageConsumer.cs | 43 + .../FamilyMissionsResetMessageConsumer.cs | 20 + .../Consumers/FamilyNoticeMessageConsumer.cs | 42 + .../ServiceFlushAllMessageConsumer.cs | 19 + .../FamilyServer/DockerGracefulStopService.cs | 54 + srcs/FamilyServer/EnvironmentConsts.cs | 7 + srcs/FamilyServer/FamilyServer.csproj | 29 + srcs/FamilyServer/FamilySystem.cs | 34 + srcs/FamilyServer/Logs/FamilyLogManager.cs | 138 ++ .../Managers/AddWarehouseItemResult.cs | 11 + .../Managers/ExperienceIncrementRequest.cs | 9 + .../Managers/FamilyExperienceManager.cs | 209 ++ srcs/FamilyServer/Managers/FamilyManager.cs | 68 + .../Managers/FamilyMembershipManager.cs | 94 + .../Managers/FamilyWarehouseLogManager.cs | 144 ++ .../Managers/FamilyWarehouseManager.cs | 530 ++++ .../Managers/IFamilyWarehouseManager.cs | 17 + .../Managers/MoveWarehouseItemResult.cs | 13 + .../Managers/WithdrawWarehouseItemResult.cs | 14 + srcs/FamilyServer/Program.cs | 87 + .../Services/FamilyInvitationService.cs | 64 + srcs/FamilyServer/Services/FamilyService.cs | 650 +++++ .../Services/FamilyWarehouseService.cs | 128 + srcs/FamilyServer/Startup.cs | 137 ++ .../Consumers/KickAccountConsumer.cs | 23 + .../PlayerConnectedChannelGameConsumer.cs | 46 + .../PlayerDisconnectedChannelConsumer.cs | 17 + .../Consumers/PlayerKickConsumer.cs | 37 + .../ServiceKickAllMessageConsumer.cs | 60 + ...eMaintenanceNotificationMessageConsumer.cs | 98 + .../Consumers/WorldServerShutdownConsumer.cs | 25 + .../Controllers/HealthController.cs | 96 + .../Cryptography/EncodingExtensions.cs | 33 + .../Cryptography/WorldDecrypter.cs | 245 ++ .../Cryptography/WorldEncrypter.cs | 33 + srcs/GameChannel/GameChannel.csproj | 42 + srcs/GameChannel/Network/GameSession.cs | 708 ++++++ .../GameChannel/Network/GameSessionFactory.cs | 22 + srcs/GameChannel/Network/GameTcpServer.cs | 80 + .../Network/IClientSessionFactory.cs | 13 + srcs/GameChannel/Program.cs | 209 ++ .../RecurrentJobs/WorldKeepAliveSystem.cs | 53 + .../Services/GameChannelStopService.cs | 50 + srcs/GameChannel/Startup.cs | 237 ++ srcs/GameChannel/Ticks/TickConfiguration.cs | 19 + srcs/GameChannel/Ticks/TickExtensions.cs | 25 + srcs/GameChannel/Ticks/TickWorker.cs | 159 ++ .../Ticks/WorkerDispatchTickManager.cs | 94 + srcs/GameChannel/Utils/EnvironmentConsts.cs | 34 + srcs/GameChannel/Utils/GracefulShutdown.cs | 48 + srcs/GameChannel/Utils/ISpamProtector.cs | 11 + srcs/GameChannel/Utils/MapsterMapper.cs | 35 + srcs/GameChannel/Utils/SmartSpamProtector.cs | 58 + srcs/GameChannel/WorldServerSingleton.cs | 21 + .../Auth/IClientVersionCheckingService.cs | 13 + .../Auth/RedisCachedHardwareIdService.cs | 31 + .../Auth/RedisClientVersionCheckingService.cs | 28 + srcs/LoginServer/Auth/StringHashExtensions.cs | 25 + .../Handlers/GenericLoginPacketHandlerBase.cs | 28 + .../Handlers/GlobalPacketProcessor.cs | 27 + .../Handlers/IGlobalPacketProcessor.cs | 12 + srcs/LoginServer/Handlers/IPacketHandler.cs | 11 + .../Handlers/LoginPacketsExtensions.cs | 53 + .../TypedCredentialsLoginPacketHandler.cs | 186 ++ srcs/LoginServer/LoginServer.csproj | 24 + .../LoginServer/Network/LoginClientSession.cs | 124 + srcs/LoginServer/Network/LoginServer.cs | 77 + srcs/LoginServer/NostaleLoginDecrypter.cs | 25 + srcs/LoginServer/NostaleLoginEncrypter.cs | 30 + srcs/LoginServer/Program.cs | 138 ++ srcs/LoginServer/Startup.cs | 50 + .../Utils/DockerGracefulStopService.cs | 48 + srcs/LoginServer/Utils/ISpamProtector.cs | 11 + srcs/LoginServer/Utils/SmartSpamProtector.cs | 56 + .../ServiceFlushAllMessageConsumer.cs | 20 + srcs/LogsServer/DockerGracefulStopService.cs | 54 + srcs/LogsServer/LogsServer.csproj | 26 + srcs/LogsServer/Program.cs | 78 + srcs/LogsServer/Startup.cs | 41 + .../MailCharacterConnectedMessageConsumer.cs | 52 + .../NoteCharacterConnectedMessageConsumer.cs | 34 + .../ServiceFlushAllMessageConsumer.cs | 20 + srcs/MailServer/DockerGracefulStopService.cs | 54 + srcs/MailServer/MailServer.csproj | 26 + srcs/MailServer/Managers/MailManager.cs | 238 ++ srcs/MailServer/Program.cs | 81 + srcs/MailServer/RecurrentJobs/MailSystem.cs | 30 + srcs/MailServer/Services/MailService.cs | 106 + srcs/MailServer/Services/NoteService.cs | 181 ++ srcs/MailServer/Startup.cs | 78 + ...PlayerConnectedOnChannelMessageConsumer.cs | 33 + ...layerDisconnectedChannelMessageConsumer.cs | 21 + srcs/Master/Datas/WorldServer.cs | 25 + srcs/Master/DockerGracefulStopService.cs | 54 + srcs/Master/Extensions/WorldExtensions.cs | 48 + .../Managers/ClusterCharacterManager.cs | 53 + srcs/Master/Managers/WorldServerManager.cs | 59 + srcs/Master/Master.csproj | 27 + srcs/Master/Program.cs | 75 + srcs/Master/Proxies/ServerApiService.cs | 189 ++ .../GameChannelHeartbeatService.cs | 66 + .../Services/ClusterCharacterService.cs | 70 + .../Maintenance/GrpcClusterStatusService.cs | 290 +++ .../Services/Maintenance/IStatusManager.cs | 11 + .../Services/Maintenance/ServiceStatus.cs | 12 + .../Services/Maintenance/StatusManager.cs | 77 + .../StatusRefreshMessageConsumer.cs | 26 + .../Services/Sessions/EncryptionKeyFactory.cs | 13 + .../Services/Sessions/ISessionManager.cs | 14 + .../Services/Sessions/RedisSessionManager.cs | 105 + .../Services/Sessions/SessionService.cs | 439 ++++ srcs/Master/Startup.cs | 123 + .../DependencyInjectionExtensions.cs | 14 + srcs/PhoenixLib.Auth.JWT/HashingHelper.cs | 15 + srcs/PhoenixLib.Auth.JWT/IJwtTokenFactory.cs | 13 + srcs/PhoenixLib.Auth.JWT/JwtExtensions.cs | 60 + srcs/PhoenixLib.Auth.JWT/JwtTokenFactory.cs | 78 + .../PhoenixLib.Auth.JWT.csproj | 14 + srcs/PhoenixLib.Caching/ICachedRepository.cs | 32 + srcs/PhoenixLib.Caching/IKeyValueCache.cs | 6 + .../ILongKeyCachedRepository.cs | 6 + .../IUuidKeyCachedRepository.cs | 8 + .../InMemoryCacheRepository.cs | 100 + .../InMemoryKeyValueCache.cs | 107 + .../InMemoryUuidCacheRepository.cs | 90 + srcs/PhoenixLib.Caching/MemoryCacheHandle.cs | 399 +++ .../PhoenixLib.Caching.csproj | 12 + .../RuntimeCachingBuilderExtensions.cs | 48 + .../ConfigurationHelperConfig.cs | 7 + .../ConfigurationPathProvider.cs | 11 + .../DependencyInjectionExtensions.cs | 164 ++ .../IConfigurationHelper.cs | 13 + .../IConfigurationPathProvider.cs | 7 + .../PhoenixLib.Configuration.csproj | 18 + .../StringExtensions.cs | 12 + .../YamlConfigurationHelper.cs | 84 + .../BaseUuidDto.cs | 13 + srcs/PhoenixLib.DAL.Abstractions/IDto.cs | 13 + .../IGenericAsyncIntRepository.cs | 12 + .../IGenericAsyncLongRepository.cs | 16 + .../IGenericAsyncRepository.cs | 69 + .../IGenericAsyncUuidRepository.cs | 12 + srcs/PhoenixLib.DAL.Abstractions/IIntDto.cs | 9 + .../IKeyValueAsyncStorage.cs | 70 + srcs/PhoenixLib.DAL.Abstractions/ILongDto.cs | 13 + srcs/PhoenixLib.DAL.Abstractions/IMapper.cs | 27 + srcs/PhoenixLib.DAL.Abstractions/IUuidDto.cs | 13 + .../PhoenixLib.DAL.Abstractions.csproj | 14 + .../Extensions/DatabaseClearExtensions.cs | 23 + .../Extensions/ServiceCollectionExtensions.cs | 91 + .../GenericIntRepository.cs | 177 ++ .../GenericLongRepository.cs | 167 ++ .../GenericMappedIntRepository.cs | 44 + .../GenericMappedLongRepository.cs | 44 + .../GenericMappedUuidRepository.cs | 45 + .../GenericOldMappedIntRepository.cs | 156 ++ .../GenericOldMappedLongRepository.cs | 158 ++ .../GenericOldMappedUuidRepository.cs | 169 ++ .../GenericStringRepository.cs | 158 ++ .../GenericUuidRepository.cs | 177 ++ .../IContextFactory.cs | 17 + .../IGenericStringRepository.cs | 7 + .../PhoenixLib.DAL.EFCore.PGSQL/IIntEntity.cs | 6 + .../ILongEntity.cs | 13 + .../IStringKeyEntity.cs | 7 + .../IUuidEntity.cs | 9 + .../PgSqlDatabaseConfiguration.cs | 43 + .../PhoenixLib.DAL.EFCore.PGSQL.csproj | 18 + .../MappedMongoRepository.cs | 86 + .../MongoConfiguration.cs | 16 + .../MongoConfigurationBuilder.cs | 39 + .../PhoenixLib.DAL.MongoDB.csproj | 18 + .../SynchronizedMongoRepository.cs | 63 + .../DependencyInjectionExtensions.cs | 69 + .../Locks/IExpirableLockService.cs | 46 + .../Locks/ILockService.cs | 14 + .../PhoenixLib.DAL.Redis/Locks/IScopedLock.cs | 9 + .../Locks/RedisCheckableLock.cs | 68 + .../Locks/RedisLockService.cs | 20 + .../Locks/RedisScopedLock.cs | 29 + .../PhoenixLib.DAL.Redis.csproj | 20 + .../RedisConfiguration.cs | 29 + .../RedisGenericKeyValueAsyncStorage.cs | 88 + srcs/PhoenixLib.Events/AsyncEventPipeline.cs | 42 + .../ConvertedEventForwarder.cs | 29 + srcs/PhoenixLib.Events/DependencyInjection.cs | 41 + srcs/PhoenixLib.Events/EventPipeline.cs | 41 + srcs/PhoenixLib.Events/IAsyncEvent.cs | 13 + srcs/PhoenixLib.Events/IAsyncEventPipeline.cs | 22 + .../PhoenixLib.Events/IAsyncEventProcessor.cs | 18 + srcs/PhoenixLib.Events/IConverter.cs | 7 + srcs/PhoenixLib.Events/IEvent.cs | 6 + srcs/PhoenixLib.Events/IEventProcessor.cs | 8 + .../IEventProcessorPipeline.cs | 8 + .../Internal/AssemblyExtensions.cs | 105 + .../PhoenixLib.Events.csproj | 15 + .../ServiceProviderServiceExtensions.cs | 76 + .../AssemblyExtensions.cs | 105 + .../ByteArrayExtensions.cs | 19 + .../PhoenixLib.Extensions.csproj | 9 + .../PhoenixLib.Extensions/StringExtensions.cs | 19 + srcs/PhoenixLib.Logging/ILogger.cs | 28 + srcs/PhoenixLib.Logging/Log.cs | 70 + srcs/PhoenixLib.Logging/LoggingExtensions.cs | 28 + .../PhoenixLib.Logging.csproj | 16 + srcs/PhoenixLib.Logging/SerilogLogger.cs | 128 + .../DependencyInjectionExtensions.cs | 69 + .../GenericSubscribedMessage.cs | 10 + srcs/PhoenixLib.Messaging/IMessage.cs | 10 + srcs/PhoenixLib.Messaging/IMessageConsumer.cs | 10 + .../PhoenixLib.Messaging/IMessagePublisher.cs | 17 + .../PhoenixLib.Messaging/IMessagingService.cs | 18 + .../Internal/GenericMessagePublisher.cs | 23 + .../Internal/IServiceBusInstance.cs | 9 + .../MQTT/CloudEventsJsonMessageSerializer.cs | 73 + .../Internal/MQTT/IMessageSerializer.cs | 11 + .../Internal/MQTT/MqttConfiguration.cs | 16 + .../Internal/MQTT/MqttMessagingService.cs | 182 ++ .../Internal/Routing/IMessageRouter.cs | 28 + .../Internal/Routing/IRoutingInformation.cs | 11 + .../Routing/IRoutingInformationFactory.cs | 9 + .../Internal/Routing/ISubscribedMessage.cs | 13 + .../Internal/Routing/MessageExtensions.cs | 24 + .../Internal/Routing/MessageRouter.cs | 59 + .../Internal/Routing/MessageTypeAttribute.cs | 11 + .../Internal/Routing/RoutingInformation.cs | 18 + .../Routing/RoutingInformationFactory.cs | 9 + .../Internal/ServiceBusInstance.cs | 10 + .../PhoenixLib.Messaging.csproj | 22 + .../DependencyInjectionExtensions.cs | 20 + .../GenericMultilanguageService.cs | 104 + .../IEnumBasedLanguageService.cs | 16 + .../ILanguageService.cs | 56 + .../IStringBasedLanguageService.cs | 10 + .../PhoenixLib.Multilanguage.csproj | 19 + .../RegionLanguageType.cs | 19 + .../FuncTaskWorkerDisposable.cs | 35 + .../ObservableCron.cs | 12 + .../ObservableScheduler.cs | 58 + .../PhoenixLib.Scheduler.ReactiveX.csproj | 19 + .../SchedulerServiceCollectionExtensions.cs | 17 + .../TaskWorkerDisposable.cs | 35 + srcs/PhoenixLib.Scheduler/ICron.cs | 12 + srcs/PhoenixLib.Scheduler/ICronJobPool.cs | 9 + srcs/PhoenixLib.Scheduler/IGenericCronPool.cs | 64 + srcs/PhoenixLib.Scheduler/IGenericJobPool.cs | 122 + srcs/PhoenixLib.Scheduler/IScheduledJob.cs | 9 + srcs/PhoenixLib.Scheduler/IScheduler.cs | 12 + srcs/PhoenixLib.Scheduler/Interval.cs | 55 + .../PhoenixLib.Scheduler.csproj | 8 + srcs/PhoenixLib.Scheduler/TimePoint.cs | 54 + .../Command/RainbowBattleCommandModule.cs | 60 + .../RainbowBattleCaptureFlagEventHandler.cs | 205 ++ .../RainbowBattleDestroyEventHandler.cs | 39 + .../RainbowBattleEndEventHandler.cs | 274 +++ .../RainbowBattleFreezeEventHandler.cs | 127 + .../RainbowBattleLeaveEventHandler.cs | 88 + ...owBattleLeaverBusterRefreshEventHandler.cs | 33 + ...BattleProcessActivityPointsEventHandler.cs | 45 + ...nbowBattleProcessFlagPointsEventHandler.cs | 50 + .../RainbowBattleProcessLifeEventHandler.cs | 31 + .../RainbowBattleRefreshScoreEventHandler.cs | 46 + .../RainbowBattleStartEventHandler.cs | 137 ++ ...tleStartProcessRegistrationEventHandler.cs | 193 ++ .../RainbowBattleStartRegisterEventHandler.cs | 40 + .../RainbowBattleUnfreezeEventHandler.cs | 167 ++ ...ainbowBattleUnfreezeProcessEventHandler.cs | 41 + .../Managers/IRainbowFactory.cs | 92 + ...wBattleLeaverBusterResetMessageConsumer.cs | 30 + .../RainbowBattleStartMessageConsumer.cs | 21 + .../Plugin.RainbowBattle.csproj | 17 + .../RainbowBattlePluginCore.cs | 48 + .../RecurrentJob/RainbowBattleSystem.cs | 294 +++ ...RelationCharacterConnectMessageConsumer.cs | 41 + ...ationCharacterDisconnectMessageConsumer.cs | 24 + .../DockerGracefulStopService.cs | 54 + srcs/RelationServer/Program.cs | 77 + srcs/RelationServer/RelationServer.csproj | 24 + .../Services/RelationService.cs | 226 ++ srcs/RelationServer/Startup.cs | 61 + .../InstantBattleStartFileConfiguration.cs | 11 + srcs/Scheduler/DockerGracefulStopService.cs | 54 + srcs/Scheduler/HangfireJobActivator.cs | 14 + srcs/Scheduler/Program.cs | 72 + srcs/Scheduler/Ranking/RankingRefreshEvent.cs | 8 + .../Ranking/RankingRefreshEventHandler.cs | 53 + srcs/Scheduler/Scheduler.csproj | 32 + .../ComplimentsMonthlyRefreshCronScheduler.cs | 23 + .../FamilyMissionsResetCronScheduler.cs | 23 + .../Service/InstantBattleCronScheduler.cs | 23 + .../MinigameProductionRefreshCronScheduler.cs | 23 + .../Service/QuestDailyRefreshCronScheduler.cs | 26 + .../RaidRestrictionRefreshCronScheduler.cs | 23 + .../Service/RainbowBattleCronScheduler.cs | 23 + .../RainbowBattleLeaverBusterCronScheduler.cs | 26 + .../Service/RankingRefreshCronScheduler.cs | 23 + .../SpecialistPointsRefreshCronScheduler.cs | 23 + srcs/Scheduler/Startup.cs | 111 + srcs/Scheduler/Utility/RandomGenerator.cs | 26 + .../CheckTranslationsCommandHandler.cs | 101 + .../CreateAccountCommandHandler.cs | 93 + .../GenerateTranslationsCommandHandler.cs | 91 + .../Commands/CheckTranslationsCommand.cs | 13 + srcs/Toolkit/Commands/CreateAccountCommand.cs | 10 + .../Commands/GenerateTranslationsCommand.cs | 16 + .../RegionLanguageTypeExtensions.cs | 35 + srcs/Toolkit/Program.cs | 33 + srcs/Toolkit/Toolkit.csproj | 27 + .../DockerGracefulStopService.cs | 54 + .../Loader/BannedNamesConfiguration.cs | 9 + .../Loader/GenericTranslationFileLoader.cs | 91 + .../Loader/TranslationsFileLoaderOptions.cs | 7 + srcs/TranslationsServer/Program.cs | 89 + .../Services/GrpcGameLanguageService.cs | 40 + srcs/TranslationsServer/Startup.cs | 58 + .../TranslationsServer.csproj | 25 + .../Checks/RequireAuthorityAttribute.cs | 42 + .../CommandGlobalExecutorWrapper.cs | 28 + srcs/WingsAPI.Commands/CommandHandler.cs | 263 ++ .../Entities/SaltyCommandResult.cs | 20 + .../Entities/SaltyModuleBase.cs | 12 + .../Entities/WingsEmuIngameCommandContext.cs | 36 + .../Interfaces/ICommandContainer.cs | 88 + .../TypeParsers/ItemTypeParser.cs | 28 + .../TypeParsers/MapInstanceTypeParser.cs | 29 + .../TypeParsers/PlayerEntityTypeParser.cs | 27 + .../WingsAPI.Commands.csproj | 17 + .../Auth/AuthorizedClientVersionDto.cs | 19 + .../Auth/BlacklistedHwidDto.cs | 20 + .../Auth/IHardwareIdService.cs | 13 + .../BasicRpcResponse.cs | 11 + .../Bazaar/BazaarAddItemRequest.cs | 24 + .../Bazaar/BazaarBuyItemRequest.cs | 23 + .../Bazaar/BazaarChangeItemPriceRequest.cs | 27 + .../Bazaar/BazaarGetItemByIdRequest.cs | 11 + .../Bazaar/BazaarGetItemsByCharIdRequest.cs | 11 + .../Bazaar/BazaarGetItemsByCharIdResponse.cs | 16 + .../Bazaar/BazaarItemResponse.cs | 15 + .../Bazaar/BazaarRemoveItemRequest.cs | 18 + .../BazaarRemoveItemsByCharIdRequest.cs | 11 + .../BazaarRemoveItemsByCharIdResponse.cs | 11 + .../Bazaar/BazaarSearchBazaarItemsRequest.cs | 11 + .../Bazaar/BazaarSearchBazaarItemsResponse.cs | 16 + .../Bazaar/BazaarSearchContext.cs | 37 + .../Bazaar/IBazaarService.cs | 62 + .../ComplimentsMonthlyRefreshMessage.cs | 11 + .../AccountService/AccountBanGetRequest.cs | 11 + .../AccountService/AccountBanGetResponse.cs | 15 + .../AccountService/AccountBanSaveRequest.cs | 12 + .../AccountService/AccountBanSaveResponse.cs | 15 + .../AccountService/AccountLoadByIdRequest.cs | 11 + .../AccountLoadByNameRequest.cs | 11 + .../AccountService/AccountLoadResponse.cs | 15 + .../AccountPenaltyGetAllResponse.cs | 16 + .../AccountPenaltyGetRequest.cs | 11 + .../AccountPenaltyMultiSaveRequest.cs | 13 + .../AccountPenaltyMultiSaveResponse.cs | 16 + .../AccountService/AccountSaveRequest.cs | 12 + .../AccountService/AccountSaveResponse.cs | 15 + .../AccountService/IAccountService.cs | 39 + .../CharacterGetTopResponse.cs | 16 + .../CharacterRefreshRankingResponse.cs | 22 + .../DbServerDeleteCharacterRequest.cs | 12 + .../DbServerDeleteCharacterResponse.cs | 11 + .../DbServerFlushCharacterSavesRequest.cs | 9 + .../DbServerFlushCharacterSavesResponse.cs | 11 + .../DbServerGetCharacterByIdRequest.cs | 11 + .../DbServerGetCharacterFromSlotRequest.cs | 14 + .../DbServerGetCharacterRequestByName.cs | 11 + .../DbServerGetCharacterResponse.cs | 15 + .../DbServerGetCharactersRequest.cs | 11 + .../DbServerGetCharactersResponse.cs | 16 + .../DbServerSaveCharacterRequest.cs | 15 + .../DbServerSaveCharacterResponse.cs | 15 + .../DbServerSaveCharactersRequest.cs | 17 + .../DbServerSaveCharactersResponse.cs | 11 + .../CharacterService/ICharacterService.cs | 57 + .../TimeSpaceService/ITimeSpaceService.cs | 18 + .../TimeSpaceIsNewRecordRequest.cs | 14 + .../TimeSpaceIsNewRecordResponse.cs | 11 + .../TimeSpaceNewRecordRequest.cs | 12 + .../TimeSpaceRecordRequest.cs | 11 + .../TimeSpaceRecordResponse.cs | 12 + .../AccountWarehouseAddItemRequest.cs | 12 + .../AccountWarehouseAddItemResponse.cs | 15 + .../AccountWarehouseGetItemRequest.cs | 14 + .../AccountWarehouseGetItemResponse.cs | 15 + .../AccountWarehouseGetItemsRequest.cs | 11 + .../AccountWarehouseGetItemsResponse.cs | 16 + .../AccountWarehouseMoveItemRequest.cs | 18 + .../AccountWarehouseMoveItemResponse.cs | 18 + .../AccountWarehouseWithdrawItemRequest.cs | 15 + .../AccountWarehouseWithdrawItemResponse.cs | 19 + .../IAccountWarehouseService.cs | 24 + srcs/WingsAPI.Communication/EmptyResponse.cs | 9 + .../WingsAPI.Communication/EmptyRpcRequest.cs | 9 + .../Families/FamilyAddMemberRequest.cs | 18 + .../Families/FamilyAddMemberResponse.cs | 12 + .../Families/FamilyChangeAuthorityRequest.cs | 12 + .../Families/FamilyChangeContainer.cs | 15 + .../Families/FamilyChangeFactionRequest.cs | 15 + .../Families/FamilyChangeFactionResponse.cs | 11 + .../FamilyChangeFactionResponseType.cs | 10 + .../Families/FamilyChangeTitleRequest.cs | 15 + .../Families/FamilyCreateRequest.cs | 25 + .../Families/FamilyCreateResponse.cs | 15 + .../Families/FamilyCreateResponseType.cs | 14 + .../Families/FamilyDisbandRequest.cs | 11 + .../Families/FamilyIdRequest.cs | 11 + .../Families/FamilyIdResponse.cs | 19 + .../Families/FamilyInvitation.cs | 17 + .../FamilyInvitationContainsResponse.cs | 11 + .../Families/FamilyInvitationGetResponse.cs | 11 + .../Families/FamilyInvitationRemoveRequest.cs | 11 + .../Families/FamilyInvitationRequest.cs | 14 + .../Families/FamilyInvitationSaveRequest.cs | 11 + .../Families/FamilyListMembersResponse.cs | 13 + .../FamilyMemberDisconnectedRequest.cs | 15 + .../Families/FamilyMissionsResetMessage.cs | 10 + .../FamilyRemoveMemberByCharIdRequest.cs | 11 + .../Families/FamilyRemoveMemberRequest.cs | 14 + .../Families/FamilySettingsRequest.cs | 22 + .../Families/FamilyUpgradeAddResponseType.cs | 11 + .../Families/FamilyUpgradeRequest.cs | 21 + .../Families/FamilyUpgradeResponse.cs | 11 + .../Families/IFamilyInvitationService.cs | 21 + .../Families/IFamilyService.cs | 65 + .../Families/MembershipRequest.cs | 11 + .../Families/MembershipResponse.cs | 12 + .../Families/MembershipTodayRequest.cs | 14 + .../Families/MembershipTodayResponse.cs | 11 + .../FamilyWarehouseAddItemRequest.cs | 18 + .../FamilyWarehouseAddItemResponse.cs | 15 + .../FamilyWarehouseGetItemRequest.cs | 17 + .../FamilyWarehouseGetItemResponse.cs | 15 + .../FamilyWarehouseGetItemsRequest.cs | 14 + .../FamilyWarehouseGetItemsResponse.cs | 16 + .../FamilyWarehouseGetLogsRequest.cs | 14 + .../FamilyWarehouseGetLogsResponse.cs | 16 + .../FamilyWarehouseMoveItemRequest.cs | 21 + .../FamilyWarehouseMoveItemResponse.cs | 18 + .../FamilyWarehouseWithdrawItemRequest.cs | 21 + .../FamilyWarehouseWithdrawItemResponse.cs | 19 + .../Warehouse/IFamilyWarehouseService.cs | 27 + .../InstantBattleStartMessage.cs | 15 + .../Mail/CreateMailBatchRequest.cs | 16 + .../Mail/CreateMailBatchResponse.cs | 16 + .../Mail/CreateMailRequest.cs | 22 + .../Mail/CreateMailResponse.cs | 15 + .../Mail/CreateNoteRequest.cs | 43 + .../Mail/CreateNoteResponse.cs | 18 + .../Mail/GetMailsRequest.cs | 11 + .../Mail/GetMailsResponse.cs | 13 + .../Mail/IMailService.cs | 21 + .../Mail/INoteService.cs | 18 + .../Mail/OpenNoteRequest.cs | 11 + .../Mail/RemoveMailRequest.cs | 14 + .../Mail/RemoveNoteRequest.cs | 11 + .../MinigameRefreshProductionPointsMessage.cs | 15 + .../ClusterCharacterByChannelIdRequest.cs | 11 + .../Player/ClusterCharacterByIdRequest.cs | 11 + .../Player/ClusterCharacterByNameRequest.cs | 11 + .../ClusterCharacterGetMultipleResponse.cs | 15 + .../ClusterCharacterGetSortedResponse.cs | 15 + .../Player/ClusterCharacterInfo.cs | 40 + .../Player/ClusterCharacterResponse.cs | 14 + .../Player/IClusterCharacterService.cs | 24 + .../Player/RankingRefreshMessage.cs | 15 + .../Player/SpecialistPointsRefreshMessage.cs | 11 + .../Punishment/PlayerKickMessage.cs | 12 + .../Quests/QuestDailyRefreshMessage.cs | 11 + .../Raid/RaidRestrictionRefreshMessage.cs | 10 + .../RainbowBattleLeaverBusterResetMessage.cs | 11 + .../RainbowBattleStartMessage.cs | 10 + .../Relation/IRelationService.cs | 18 + .../Relation/RelationAddRequest.cs | 21 + .../Relation/RelationAddResponse.cs | 18 + .../Relation/RelationGetAllRequest.cs | 11 + .../Relation/RelationGetAllResponse.cs | 16 + .../Relation/RelationRemoveRequest.cs | 18 + .../WingsAPI.Communication/RpcResponseType.cs | 13 + .../ServerApi/IServerApiService.cs | 40 + .../ServerApi/Protocol/GameChannelType.cs | 8 + .../Protocol/GetAct4ChannelInfoRequest.cs | 11 + .../Protocol/GetChannelInfoRequest.cs | 14 + .../Protocol/GetChannelInfoResponse.cs | 14 + .../Protocol/PulseWorldServerRequest.cs | 18 + .../Protocol/RegisterLoginResponse.cs | 16 + .../Protocol/RegisterWorldRequest.cs | 6 + .../Protocol/RegisterWorldResponse.cs | 6 + .../Protocol/RegisterWorldServerRequest.cs | 11 + .../RetrieveRegisteredWorldServersRequest.cs | 12 + .../RetrieveRegisteredWorldServersResponse.cs | 12 + .../Protocol/SerializableWorldServer.cs | 41 + .../Protocol/SetMaintenanceRequest.cs | 11 + .../SetWorldServerVisibilityRequest.cs | 21 + .../ServerApi/Protocol/ShutdownRequest.cs | 11 + .../Protocol/UnregisterWorldServerRequest.cs | 11 + .../ServerApi/WorldServerShutdownMessage.cs | 11 + .../Services/IClusterStatusService.cs | 35 + .../Services/Messages/ServiceDownMessage.cs | 13 + .../Messages/ServiceFlushAllMessage.cs | 13 + .../Messages/ServiceKickAllMessage.cs | 13 + .../ServiceMaintenanceNotificationMessage.cs | 24 + .../Services/Requests/ServiceBasicRequest.cs | 11 + ...ecuteGeneralEmergencyMaintenanceRequest.cs | 11 + ...erviceScheduleGeneralMaintenanceRequest.cs | 15 + .../Responses/ServiceGetAllResponse.cs | 16 + .../ServiceGetStatusByNameResponse.cs | 14 + .../Services/Service.cs | 18 + .../Services/ServiceHealthStatus.cs | 9 + .../Sessions/ISessionService.cs | 38 + .../Sessions/Model/Session.cs | 55 + .../Sessions/Model/SessionState.cs | 11 + ...tivateCrossChannelAuthenticationRequest.cs | 14 + .../Request/ConnectCharacterRequest.cs | 17 + .../Request/ConnectToLoginServerRequest.cs | 17 + .../Request/ConnectToWorldServerRequest.cs | 17 + .../Sessions/Request/CreateSessionRequest.cs | 21 + .../Request/DisconnectSessionRequest.cs | 17 + .../Request/GetSessionByAccountIdRequest.cs | 11 + .../Request/GetSessionByAccountNameRequest.cs | 11 + .../Sessions/Request/GetSessionByIdRequest.cs | 11 + .../Sessions/Request/PulseRequest.cs | 11 + .../Sessions/Response/SessionResponse.cs | 15 + .../Translations/TranslationService.cs | 37 + .../TranslationsRefreshMessage.cs | 11 + .../WingsAPI.Communication.csproj | 20 + srcs/WingsAPI.Data/Account/AccountBanDto.cs | 33 + srcs/WingsAPI.Data/Account/AccountDTO.cs | 42 + srcs/WingsAPI.Data/Account/AccountLanguage.cs | 13 + .../Account/AccountPenaltyDto.cs | 37 + .../Account/AccountWarehouseItemDto.cs | 17 + srcs/WingsAPI.Data/Account/AuthorityType.cs | 33 + srcs/WingsAPI.Data/Account/IAccountBanDao.cs | 11 + srcs/WingsAPI.Data/Account/IAccountDAO.cs | 18 + .../Account/IAccountPenaltyDao.cs | 10 + srcs/WingsAPI.Data/ActDesc/ActDescDTO.cs | 15 + srcs/WingsAPI.Data/BCards/BCardDTO.cs | 47 + .../BCards/BCardNpcMonsterTriggerType.cs | 8 + .../BCards/BCardNpcTriggerType.cs | 7 + srcs/WingsAPI.Data/Bazaar/BazaarItemDTO.cs | 51 + srcs/WingsAPI.Data/Bazaar/IBazaarItemDAO.cs | 16 + .../Bonus/CharacterStaticBonusDto.cs | 21 + srcs/WingsAPI.Data/Bonus/StaticBonusType.cs | 16 + srcs/WingsAPI.Data/Buffs/CardDTO.cs | 41 + .../Buffs/CharacterStaticBuffDto.cs | 20 + srcs/WingsAPI.Data/Character/CharacterDTO.cs | 278 +++ .../Character/CharacterLifetimeStatsDto.cs | 107 + .../Character/CharacterRaidRestrictionDto.cs | 13 + srcs/WingsAPI.Data/Character/ICharacterDAO.cs | 45 + .../Character/RainbowBattleLeaverBusterDto.cs | 19 + srcs/WingsAPI.Data/Drops/DropDTO.cs | 26 + srcs/WingsAPI.Data/Enums/DeleteResult.cs | 12 + .../Exchanges/LogPlayerExchangeItemInfo.cs | 10 + .../FamilyAchievementCompletionDto.cs | 14 + .../Families/FamilyAchievementProgressDto.cs | 13 + .../Families/FamilyAchievementsDto.cs | 38 + srcs/WingsAPI.Data/Families/FamilyDTO.cs | 72 + srcs/WingsAPI.Data/Families/FamilyLogDTO.cs | 38 + .../Families/FamilyMembershipDto.cs | 46 + .../Families/FamilyUpgradeDto.cs | 14 + .../Families/FamilyUpgradeState.cs | 8 + .../Families/FamilyUpgradeType.cs | 17 + .../Families/FamilyWarehouseItemDto.cs | 17 + .../Families/FamilyWarehouseLogEntryDto.cs | 32 + srcs/WingsAPI.Data/Families/IFamilyDAO.cs | 13 + srcs/WingsAPI.Data/Families/IFamilyLogDAO.cs | 19 + .../Families/IFamilyMembershipDao.cs | 16 + .../Families/IFamilyWarehouseItemDAO.cs | 11 + .../Families/IFamilyWarehouseLogDao.cs | 10 + .../WingsAPI.Data/GameData/IResourceLoader.cs | 33 + .../Inventory/CharacterInventoryItemDto.cs | 32 + .../CharacterPartnerInventoryItemDto.cs | 17 + .../WingsAPI.Data/Items/EquipmentOptionDTO.cs | 29 + .../Items/EquipmentOptionType.cs | 10 + srcs/WingsAPI.Data/Items/ItemDTO.cs | 192 ++ srcs/WingsAPI.Data/Items/ItemInstanceDTO.cs | 181 ++ srcs/WingsAPI.Data/Items/ItemInstanceType.cs | 9 + srcs/WingsAPI.Data/Mails/AccountMailDto.cs | 21 + srcs/WingsAPI.Data/Mails/CharacterMailDTO.cs | 36 + srcs/WingsAPI.Data/Mails/CharacterNoteDto.cs | 60 + srcs/WingsAPI.Data/Mails/IAccountMailDao.cs | 10 + srcs/WingsAPI.Data/Mails/ICharacterMailDao.cs | 14 + srcs/WingsAPI.Data/Mails/ICharacterNoteDao.cs | 10 + srcs/WingsAPI.Data/Mails/MailGiftType.cs | 7 + srcs/WingsAPI.Data/Maps/MapDataDTO.cs | 16 + srcs/WingsAPI.Data/Maps/MapFlags.cs | 42 + srcs/WingsAPI.Data/Maps/MapMonsterDTO.cs | 27 + srcs/WingsAPI.Data/Maps/MapNpcDTO.cs | 32 + srcs/WingsAPI.Data/Maps/ServerMapDto.cs | 23 + srcs/WingsAPI.Data/Mates/MateDTO.cs | 82 + .../Miniland/CharacterMinilandObjectDto.cs | 39 + .../WingsAPI.Data/NpcMonster/NpcMonsterDTO.cs | 210 ++ .../Quests/CharacterQuestCompletedDto.cs | 9 + .../WingsAPI.Data/Quests/CharacterQuestDto.cs | 21 + .../Quests/CharacterQuestGeneratedReward.cs | 7 + .../Quests/CharacterQuestObjectiveDto.cs | 13 + .../Quests/CompletedScriptsDto.cs | 17 + srcs/WingsAPI.Data/Quests/ITutorialDao.cs | 7 + srcs/WingsAPI.Data/Quests/QuestDto.cs | 34 + srcs/WingsAPI.Data/Quests/QuestNpcDto.cs | 20 + .../WingsAPI.Data/Quests/QuestObjectiveDto.cs | 19 + srcs/WingsAPI.Data/Quests/QuestPrizeDto.cs | 24 + srcs/WingsAPI.Data/Quests/QuestSlotType.cs | 8 + .../Quests/TutorialActionType.cs | 16 + srcs/WingsAPI.Data/Quests/TutorialDto.cs | 20 + .../Quicklist/CharacterQuicklistEntryDto.cs | 45 + srcs/WingsAPI.Data/Recipes/RecipeDTO.cs | 24 + srcs/WingsAPI.Data/Recipes/RecipeItemDTO.cs | 14 + .../Relations/CharacterRelationDTO.cs | 24 + .../Relations/ICharacterRelationDAO.cs | 16 + .../Respawns/CharacterReturnDto.cs | 16 + srcs/WingsAPI.Data/ServerDatas/ItemBoxDto.cs | 28 + .../ServerDatas/ItemBoxItemDto.cs | 18 + srcs/WingsAPI.Data/ServerDatas/ItemBoxType.cs | 7 + srcs/WingsAPI.Data/ServerDatas/PortalDTO.cs | 36 + .../ServerDatas/TeleporterDTO.cs | 29 + srcs/WingsAPI.Data/Shops/ShopDTO.cs | 22 + srcs/WingsAPI.Data/Shops/ShopItemDTO.cs | 22 + srcs/WingsAPI.Data/Shops/ShopSkillDTO.cs | 14 + .../WingsAPI.Data/Skills/CharacterSkillDTO.cs | 14 + srcs/WingsAPI.Data/Skills/ComboDTO.cs | 21 + .../Skills/NpcMonsterSkillDTO.cs | 29 + srcs/WingsAPI.Data/Skills/PartnerSkillDTO.cs | 16 + srcs/WingsAPI.Data/Skills/SkillDTO.cs | 84 + srcs/WingsAPI.Data/Skills/SkillType.cs | 11 + .../Skills/TargetAffectedEntities.cs | 9 + srcs/WingsAPI.Data/Skills/TargetType.cs | 9 + .../TimeSpace/ITimeSpaceRecordDao.cs | 11 + .../TimeSpace/TimeSpaceRecordDto.cs | 24 + .../WingsAPI.Data/Titles/CharacterTitleDto.cs | 23 + srcs/WingsAPI.Data/Titles/TitleDto.cs | 22 + .../Translations/GameDataType.cs | 12 + .../Warehouse/IAccountWarehouseItemDao.cs | 16 + .../Warehouse/PartnerWarehouseItemDto.cs | 14 + srcs/WingsAPI.Data/WingsAPI.Data.csproj | 22 + .../Arena/ArenaExtensions.cs | 7 + .../Bazaar/BazaarExtensions.cs | 45 + .../CharacterExtensions/FriendsExtensions.cs | 13 + .../CharacterExtensions/ManagerExtensions.cs | 18 + .../PlayerEntityExtensions.cs | 24 + .../Families/FamilyEventExtensions.cs | 48 + .../Families/FamilyPacketExtensions.cs | 622 +++++ .../Families/FamilyWarehouseExtensions.cs | 56 + .../FamilyWarehousePacketExtension.cs | 73 + .../Groups/GroupPacketExtensions.cs | 193 ++ .../Inventory/InventoryPacketExtensions.cs | 10 + .../ItemExtension/BuyExtension.cs | 50 + .../ItemExtension/ExchangeExtension.cs | 25 + .../ItemExtension/InventoryExtension.cs | 37 + .../ItemExtension/ItemExtension.cs | 27 + .../ManagerExtensions.cs | 18 + .../MinilandExtensions/MinigameExtensions.cs | 45 + .../MinigamePacketExtensions.cs | 85 + .../MinigameUtilitiesExtensions.cs | 50 + .../MinilandPacketExtensions.cs | 34 + .../MinilandUtilitiesExtensions.cs | 14 + .../DelayPacketsExtensions.cs | 13 + .../PacketGeneration/InfoPacketsExtensions.cs | 17 + .../LoginPacketsExtensions.cs | 14 + .../PacketGeneration/MailPacketsExtensions.cs | 91 + .../PacketGeneration/MapExtensions.cs | 76 + .../ModalPacketsExtensions.cs | 19 + .../NosBazaarPacketsExtensions.cs | 127 + .../PacketGeneration/NpcPacketsExtensions.cs | 24 + .../Quests/QuestExtensions.cs | 551 +++++ .../Quests/QuestScriptExtensions.cs | 37 + .../Quicklist/QuicklistPacketExtensions.cs | 55 + .../RelationsExtensions.cs | 28 + .../Skills/LearningSkillsExtensions.cs | 134 + .../Warehouse/WarehousePacketExtensions.cs | 133 + .../WingsAPI.Game.Extensions.csproj | 15 + srcs/WingsAPI.Game/Account.cs | 59 + .../Act4/Configuration/Act4Configuration.cs | 47 + .../Act4DungeonsConfiguration.cs | 32 + srcs/WingsAPI.Game/Act4/DungeonInstance.cs | 48 + .../Act4/DungeonInstanceWrapper.cs | 6 + srcs/WingsAPI.Game/Act4/DungeonSubInstance.cs | 93 + srcs/WingsAPI.Game/Act4/DungeonType.cs | 9 + .../Act4/Entities/CalvinasDragon.cs | 16 + .../WingsAPI.Game/Act4/Entities/HatusHeads.cs | 37 + .../Event/Act4DungeonBossMapCleanUpEvent.cs | 9 + .../Act4DungeonBroadcastBossClosedEvent.cs | 8 + .../Act4DungeonBroadcastBossOpenEvent.cs | 8 + .../Event/Act4DungeonBroadcastPacketEvent.cs | 8 + .../Act4/Event/Act4DungeonEnterEvent.cs | 8 + .../Act4/Event/Act4DungeonRewardEvent.cs | 17 + .../Act4/Event/Act4DungeonStartedEvent.cs | 10 + .../Act4/Event/Act4DungeonStopEvent.cs | 8 + .../Act4/Event/Act4DungeonSystemStartEvent.cs | 6 + .../Act4/Event/Act4DungeonSystemStopEvent.cs | 5 + .../Event/Act4FactionPointsGenerationEvent.cs | 7 + .../Event/Act4FactionPointsIncreaseEvent.cs | 20 + .../Act4/Event/Act4FamilyDungeonWonEvent.cs | 12 + .../Act4/Event/Act4MukrajuDeathEvent.cs | 11 + .../Act4/Event/Act4MukrajuDespawnEvent.cs | 7 + .../Act4/Event/Act4MukrajuSpawnEvent.cs | 11 + .../Act4/Event/Act4PutFlagEvent.cs | 9 + .../Act4/Event/Act4SystemFcBroadcastEvent.cs | 5 + .../WingsAPI.Game/Act4/IAct4DungeonManager.cs | 26 + srcs/WingsAPI.Game/Act4/IAct4FlagManager.cs | 108 + srcs/WingsAPI.Game/Act4/IDungeonFactory.cs | 6 + srcs/WingsAPI.Game/Act4/IDungeonManager.cs | 69 + .../WingsAPI.Game/Act5/Act5OpenNpcRunEvent.cs | 10 + .../Events/GenerateExperienceEvent.cs | 19 + .../IBattleEntityAlgorithmService.cs | 116 + .../Algorithm/ICellonGenerationAlgorithm.cs | 12 + .../Algorithm/ICharacterAlgorithm.cs | 26 + .../Algorithm/IDamageAlgorithm.cs | 9 + .../Algorithm/IShellGenerationAlgorithm.cs | 15 + srcs/WingsAPI.Game/Arena/IArenaManager.cs | 11 + srcs/WingsAPI.Game/BCard/BCardComponent.cs | 481 ++++ srcs/WingsAPI.Game/BCard/BCardExtension.cs | 60 + srcs/WingsAPI.Game/BCard/BCardTriggerType.cs | 7 + srcs/WingsAPI.Game/BCard/BCardType.cs | 130 + srcs/WingsAPI.Game/BCard/IBCardComponent.cs | 43 + .../BCard/IBCardEffectAsyncHandler.cs | 11 + .../BCard/IBCardEffectContext.cs | 18 + .../BCard/IBCardEffectHandlerContainer.cs | 16 + .../BCard/IBCardEventContextFactory.cs | 11 + .../BCard/StaticBCardEffectHandlerService.cs | 11 + .../Battle/BattleEntityDumpExtensions.cs | 393 +++ .../Battle/BattleEntitySharedExtensions.cs | 199 ++ .../Battle/BattleExecuteSkillEvent.cs | 24 + srcs/WingsAPI.Game/Battle/BuffProcessable.cs | 20 + srcs/WingsAPI.Game/Battle/BuffRequest.cs | 23 + srcs/WingsAPI.Game/Battle/CastingComponent.cs | 52 + srcs/WingsAPI.Game/Battle/ComboState.cs | 13 + srcs/WingsAPI.Game/Battle/ElementType.cs | 11 + .../Battle/Event/ApplyHitEvent.cs | 19 + .../Battle/Event/EntityDamageEvent.cs | 13 + .../Battle/Event/ProcessBuffEvent.cs | 21 + .../Battle/Event/ProcessHitEvent.cs | 23 + srcs/WingsAPI.Game/Battle/HitInformation.cs | 21 + srcs/WingsAPI.Game/Battle/HitProcessable.cs | 20 + srcs/WingsAPI.Game/Battle/HitRequest.cs | 19 + srcs/WingsAPI.Game/Battle/HostilityType.cs | 17 + .../WingsAPI.Game/Battle/IBattleEntityDump.cs | 62 + srcs/WingsAPI.Game/Battle/MTListHitTarget.cs | 29 + .../Managers/BattleEntityDumpFactory.cs | 20 + .../Battle/Managers/ChargeManager.cs | 27 + .../Managers/IBattleEntityDumpFactory.cs | 13 + .../Battle/Managers/IChargeManager.cs | 12 + .../Battle/Managers/IMeditationManager.cs | 15 + .../Managers/IPhantomPositionManager.cs | 31 + .../Battle/Managers/ISacrificeManager.cs | 11 + .../Battle/Managers/ISkillExecutor.cs | 14 + .../Battle/Managers/ISkillUsageManager.cs | 14 + .../Battle/Managers/ITeleportManager.cs | 20 + .../Battle/Managers/MeditationManager.cs | 46 + .../Battle/Managers/SkillExecutor.cs | 126 + .../Battle/Managers/SkillUsageManager.cs | 43 + .../Battle/Managers/StaticSkillExecutor.cs | 16 + .../Battle/Managers/TeleportManager.cs | 23 + .../Battle/MateBattleEntityDump.cs | 209 ++ .../Battle/NpcMonsterEntityDump.cs | 256 ++ .../Battle/PlayerBattleEntityDump.cs | 372 +++ srcs/WingsAPI.Game/Battle/SacrificeManager.cs | 30 + srcs/WingsAPI.Game/Battle/SkillInfo.cs | 39 + srcs/WingsAPI.Game/Bazaar/BazaarItem.cs | 31 + .../Configuration/BazaarConfiguration.cs | 14 + .../Events/BazaarGetListedItemsEvent.cs | 16 + .../Bazaar/Events/BazaarItemAddEvent.cs | 24 + .../Bazaar/Events/BazaarItemAddedEvent.cs | 15 + .../Bazaar/Events/BazaarItemBoughtEvent.cs | 14 + .../Bazaar/Events/BazaarItemBuyEvent.cs | 19 + .../Events/BazaarItemChangePriceEvent.cs | 19 + .../Bazaar/Events/BazaarItemExpiredEvent.cs | 12 + .../Bazaar/Events/BazaarItemInsertedEvent.cs | 13 + .../Bazaar/Events/BazaarItemRemoveEvent.cs | 10 + .../Bazaar/Events/BazaarItemRemovedEvent.cs | 11 + .../Bazaar/Events/BazaarItemWithdrawnEvent.cs | 13 + .../Bazaar/Events/BazaarOpenUiEvent.cs | 10 + .../Bazaar/Events/BazaarSearchItemsEvent.cs | 37 + srcs/WingsAPI.Game/Bazaar/IBazaarManager.cs | 17 + srcs/WingsAPI.Game/Buffs/Buff.cs | 112 + srcs/WingsAPI.Game/Buffs/BuffComponent.cs | 158 ++ srcs/WingsAPI.Game/Buffs/BuffFactory.cs | 100 + .../Buffs/BuffFactoryExtensions.cs | 11 + .../Buffs/BuffsDurationConfiguration.cs | 29 + srcs/WingsAPI.Game/Buffs/Card.cs | 14 + .../Buffs/EquipmentOptionContainer.cs | 143 ++ .../AngelSpecialistElementalBuffEvent.cs | 9 + .../Buffs/Events/BuffAddEvent.cs | 17 + .../Buffs/Events/BuffPartnerCheckEvent.cs | 7 + .../Buffs/Events/BuffRemoveEvent.cs | 13 + srcs/WingsAPI.Game/Buffs/IBuffComponent.cs | 21 + srcs/WingsAPI.Game/Buffs/IBuffFactory.cs | 18 + .../Buffs/IEquipmentOptionContainer.cs | 25 + srcs/WingsAPI.Game/Buffs/StaticBuffFactory.cs | 11 + .../Buffs/StaticMeditationManager.cs | 13 + .../Characters/BasicPlayerDump.cs | 17 + .../Characters/Events/AddExpEvent.cs | 16 + .../Characters/Events/AddStaticBonusEvent.cs | 11 + .../Characters/Events/BankOpenEvent.cs | 10 + .../Events/BattleEntityHealEvent.cs | 11 + .../Characters/Events/BoxOpenedEvent.cs | 11 + .../Characters/Events/CellonUpgradeEvent.cs | 17 + .../Characters/Events/CellonUpgradedEvent.cs | 11 + .../Characters/Events/ChangeClassEvent.cs | 15 + .../Characters/Events/ChangeFactionEvent.cs | 9 + .../Events/CharacterBonusExpiredEvent.cs | 7 + .../Characters/Events/CharacterLoadEvent.cs | 54 + .../Events/CharacterRemoveManagersEvent.cs | 7 + .../Characters/Events/GamblingEvent.cs | 21 + .../Characters/Events/GenerateGoldEvent.cs | 19 + .../Events/GenerateReputationEvent.cs | 9 + .../Characters/Events/GetDefaultMorphEvent.cs | 7 + .../Characters/Events/InvitationEvent.cs | 17 + .../Events/InviteJoinMinilandEvent.cs | 17 + .../Characters/Events/ItemGambledEvent.cs | 15 + .../Characters/Events/ItemSummedEvent.cs | 12 + .../Characters/Events/ItemUpgradedEvent.cs | 17 + .../Characters/Events/KillBonusEvent.cs | 9 + .../Characters/Events/LevelUpEvent.cs | 10 + .../Characters/Events/LevelUpMateEvent.cs | 14 + .../Events/MinilandSignPostJoinEvent.cs | 8 + .../Characters/Events/MonsterCaptureEvent.cs | 19 + .../Characters/Events/NormalChatEvent.cs | 8 + .../Events/PlayerChangeChannelAct4Event.cs | 7 + .../Events/PlayerChangeChannelEvent.cs | 24 + .../Characters/Events/PlayerDeathEvent.cs | 9 + .../Characters/Events/PlayerRestEvent.cs | 9 + .../Events/PlayerReturnFromAct4Event.cs | 7 + .../Events/RemoveAdditionalHpMpEvent.cs | 9 + .../Characters/Events/RemoveItemTimeEvent.cs | 7 + .../Characters/Events/RollItemBoxEvent.cs | 9 + .../Characters/Events/ShellIdentifiedEvent.cs | 9 + .../Characters/Events/SpPerfectedEvent.cs | 11 + .../Characters/Events/SpTransformEvent.cs | 9 + .../Characters/Events/SpUntransformEvent.cs | 7 + .../Characters/Events/SpUpgradedEvent.cs | 15 + .../Events/SpecialistRefreshEvent.cs | 8 + .../Characters/Events/SpeedBoosterEvent.cs | 7 + .../Characters/Events/StrangeBehaviorEvent.cs | 29 + .../Events/StrangeBehaviorSeverity.cs | 9 + .../Characters/Events/UpgradeItemEvent.cs | 13 + .../Events/VehicleCheckMapSpeedEvent.cs | 7 + .../Characters/Events/VehicleRemoveEvent.cs | 10 + .../WingsAPI.Game/Characters/IPlayerEntity.cs | 250 ++ .../Characters/IPlayerEntityFactory.cs | 9 + srcs/WingsAPI.Game/Characters/LastWalk.cs | 13 + .../Characters/PlayerEntity.Family.cs | 39 + .../Characters/PlayerEntity.Revival.cs | 46 + .../Characters/PlayerEntity.Skills.cs | 108 + .../Characters/PlayerEntity.Stats.cs | 377 +++ srcs/WingsAPI.Game/Characters/PlayerEntity.cs | 856 +++++++ srcs/WingsAPI.Game/Chat/ChatGenericEvent.cs | 15 + .../Chat/ChatMessageReceivedEvent.cs | 21 + .../Chat/ChatSendFriendMessageEvent.cs | 9 + srcs/WingsAPI.Game/Chat/ChatSpeakerEvent.cs | 19 + srcs/WingsAPI.Game/Chat/ChatType.cs | 13 + .../Chat/FamilyChatReceivedEvent.cs | 10 + .../Chat/FriendChatReceivedEvent.cs | 10 + .../Chat/GlobalPlayerChatReceivedEvent.cs | 9 + srcs/WingsAPI.Game/Cheats/CheatComponent.cs | 10 + srcs/WingsAPI.Game/Cheats/ICheatComponent.cs | 13 + .../Commands/IGlobalCommandExecutor.cs | 25 + .../ComplimentsMonthlyRefreshEvent.cs | 8 + .../Compliments/IComplimentsManager.cs | 9 + .../Act5NpcRunCraftItemConfig.cs | 37 + .../BankReputationConfiguration.cs | 54 + .../BuffsToRemoveConfiguration.cs | 37 + .../ChestDropItemConfiguration.cs | 37 + .../Configurations/GameMinMaxConfiguration.cs | 64 + .../Configurations/GameRateConfiguration.cs | 56 + .../GameRevivalConfiguration.cs | 13 + .../GeneralQuestsConfiguration.cs | 8 + .../Configurations/GibberishConfiguration.cs | 31 + .../MateRevivalConfiguration.cs | 46 + .../Miniland/AntiExploitConfiguration.cs | 16 + .../Configurations/Miniland/ForcedPlacing.cs | 10 + .../Miniland/GlobalMinigameConfiguration.cs | 32 + .../Configurations/Miniland/Minigame.cs | 35 + .../Miniland/MinigameConfiguration.cs | 18 + .../Configurations/Miniland/MinigameReward.cs | 8 + .../Miniland/MinigameRewards.cs | 12 + .../Miniland/MinigameScoresHolder.cs | 13 + .../Configurations/Miniland/MinigameType.cs | 11 + .../Configurations/Miniland/Miniland.cs | 18 + .../Miniland/MinilandConfiguration.cs | 7 + .../Configurations/Miniland/RestrictedZone.cs | 10 + .../Miniland/RestrictionType.cs | 10 + .../Configurations/Miniland/RewardLevel.cs | 11 + .../Configurations/Miniland/ScoreHolder.cs | 13 + .../Miniland/SerializablePosition.cs | 8 + .../MonsterTalkingConfiguration.cs | 42 + .../NpcRunTypeQuestsConfiguration.cs | 36 + .../PartnerSpecialistBasicConfiguration.cs | 30 + .../PerfUpgradeConfiguration.cs | 23 + .../PeriodicQuestsConfiguration.cs | 28 + .../PlayerRevivalConfiguration.cs | 23 + .../PlayerRevivalPenalization.cs | 29 + .../QuestTeleportDialogConfiguration.cs | 16 + .../QuestsRatesConfiguration.cs | 13 + .../RainbowBattleConfiguration.cs | 47 + .../Configurations/RelictConfiguration.cs | 26 + .../Configurations/ReputationConfiguration.cs | 34 + .../ReturnDefaultConfiguration.cs | 52 + .../Configurations/SpPerfStats.cs | 13 + .../SpPerfectionConfiguration.cs | 41 + .../Configurations/SpStoneLink.cs | 14 + .../Configurations/SpUpgradeConfiguration.cs | 9 + .../Configurations/SpecialItem.cs | 17 + .../Configurations/SubActsConfiguration.cs | 51 + .../Configurations/TimeSpaceConfiguration.cs | 78 + .../TimeSpaceNpcRunConfiguration.cs | 38 + .../Configurations/UpgradeConfiguration.cs | 39 + .../UpgradeItemConfiguration.cs | 9 + .../Configurations/UpgradeItemStats.cs | 12 + .../UpgradeNormalItemConfiguration.cs | 8 + .../UpgradePhenomenalItemConfiguration.cs | 8 + srcs/WingsAPI.Game/Core/ECS/IMapSystem.cs | 18 + srcs/WingsAPI.Game/Core/ECS/ITickManager.cs | 15 + .../Core/ECS/ITickProcessable.cs | 14 + .../Core/ECS/Systems/IBattleSystem.cs | 15 + .../Core/ECS/Systems/ICharacterSystem.cs | 22 + .../Core/ECS/Systems/IDropSystem.cs | 13 + .../Core/ECS/Systems/IMateSystem.cs | 19 + .../Core/ECS/Systems/IMonsterSystem.cs | 40 + .../Core/ECS/Systems/INpcSystem.cs | 20 + .../Core/GuriHandling/Event/GuriEvent.cs | 15 + .../Core/GuriHandling/IGuriHandler.cs | 12 + .../GuriHandling/IGuriHandlerContainer.cs | 16 + .../ICostumeScrollConfiguration.cs | 36 + .../Configuration/ISpPartnerConfiguration.cs | 28 + .../Configuration/ISpWingConfiguration.cs | 61 + .../Event/InventoryUseItemEvent.cs | 13 + .../Event/InventoryUsedItemEvent.cs | 9 + .../Core/ItemHandling/IItemHandler.cs | 15 + .../ItemHandling/IItemHandlerContainer.cs | 18 + .../ItemHandling/IItemUsageByVnumHandler.cs | 12 + .../Core/Multilanguage/GameDialogKey.cs | 928 +++++++ .../GameLanguageServiceExtensions.cs | 19 + .../Multilanguage/IGameDataLanguageService.cs | 24 + .../Multilanguage/IGameLanguageService.cs | 39 + .../Multilanguage/IUserLanguageService.cs | 35 + .../StaticGameLanguageService.cs | 11 + .../NpcDialogHandling/Event/NpcDialogEvent.cs | 17 + .../INpcDialogAsyncHandler.cs | 13 + .../INpcDialogHandlerContainer.cs | 15 + .../ICharacterScreenPacketHandler.cs | 5 + .../Core/PacketHandling/IGamePacketHandler.cs | 5 + .../Core/PacketHandling/IPacketHandler.cs | 14 + .../PacketHandling/IPacketHandlerContainer.cs | 45 + .../PacketHandling/IUnauthedPacketHandler.cs | 5 + .../Core/PacketHandling/PlayerEvent.cs | 13 + .../Event/GenerateEntityDeathEvent.cs | 9 + .../Event/MapJoinMonsterEntityEvent.cs | 15 + .../Entities/Event/MapJoinNpcEntityEvent.cs | 13 + .../Event/MapLeaveMonsterEntityEvent.cs | 8 + .../Entities/Event/MapLeaveNpcEntityEvent.cs | 8 + .../Extensions/VisualEntitiesExtensions.cs | 67 + srcs/WingsAPI.Game/Entities/IAiEntity.cs | 6 + srcs/WingsAPI.Game/Entities/IBattleEntity.cs | 51 + .../Entities/IBattleEntityEvent.cs | 8 + .../Entities/IBattleEntityEventEmitter.cs | 5 + srcs/WingsAPI.Game/Entities/IEntity.cs | 15 + .../Entities/IGenericEventEmitter.cs | 9 + .../Entities/IMonsterAdditionalData.cs | 17 + srcs/WingsAPI.Game/Entities/IMonsterData.cs | 113 + srcs/WingsAPI.Game/Entities/IMonsterEntity.cs | 25 + .../Entities/IMonsterEntityFactory.cs | 64 + .../WingsAPI.Game/Entities/IMoveableEntity.cs | 16 + .../Entities/INpcAdditionalData.cs | 25 + srcs/WingsAPI.Game/Entities/INpcEntity.cs | 34 + .../Entities/INpcEntityFactory.cs | 15 + .../Entities/INpcMonsterEntity.cs | 44 + srcs/WingsAPI.Game/Entities/IShopFactory.cs | 9 + .../Entities/MonsterEntityEvent.cs | 9 + .../Entities/NpcAdditionalData.cs | 25 + srcs/WingsAPI.Game/Entities/NpcEntityEvent.cs | 9 + .../IMateStatisticsComponent.cs | 23 + .../IPlayerStatisticsComponent.cs | 29 + .../MateStatisticsComponent.cs | 376 +++ .../PlayerStatisticsComponent.cs | 613 +++++ .../EntityStatistics/Statistics.cs | 43 + .../Exchange/Event/ExchangeCloseEvent.cs | 9 + .../Exchange/Event/ExchangeJoinEvent.cs | 9 + .../Exchange/Event/ExchangeRegisterEvent.cs | 16 + .../Event/ExchangeTransferItemsEvent.cs | 41 + .../Exchange/Event/TradeRequestedEvent.cs | 8 + .../Exchange/ExchangeComponent.cs | 29 + .../Exchange/IExchangeComponent.cs | 10 + srcs/WingsAPI.Game/Exchange/PlayerExchange.cs | 27 + .../Extensions/AlgorithmExtension.cs | 852 +++++++ .../Extensions/BattleEntityExtension.cs | 1513 ++++++++++++ .../Extensions/BroadcasterExtensions.cs | 56 + .../WingsAPI.Game/Extensions/BuffExtension.cs | 267 ++ .../Extensions/CharacterExtension.cs | 1039 ++++++++ .../Extensions/CharacterPacketExtension.cs | 982 ++++++++ .../Extensions/CollectionExtension.cs | 18 + .../Extensions/ConcurrentBagExtension.cs | 36 + .../Extensions/DamageExtension.cs | 1879 ++++++++++++++ .../Extensions/DateTimeExtension.cs | 8 + .../Extensions/DictionaryExtension.cs | 20 + .../Extensions/EncodingExtensions.cs | 32 + .../Extensions/EquipmentOptionExtensions.cs | 97 + srcs/WingsAPI.Game/Extensions/Lists.cs | 32 + .../Extensions/MapItemExtension.cs | 18 + .../Extensions/Mates/MateExtensions.cs | 266 ++ .../Extensions/Mates/MatePacketExtensions.cs | 368 +++ .../Mates/MateStatisticsExtensions.cs | 24 + .../Extensions/NpcMonsterExtension.cs | 233 ++ .../Extensions/RarifyExtension.cs | 79 + .../Extensions/SkillExtension.cs | 409 +++ .../Extensions/SpecialistExtension.cs | 93 + .../Extensions/StringExtensions.cs | 21 + .../Extensions/UiPacketExtension.cs | 532 ++++ .../Extensions/WearableInstanceExtensions.cs | 731 ++++++ .../Configuration/FamilyConfiguration.cs | 73 + .../FamilyUpgradesConfiguration.cs | 10 + .../Families/Configuration/LevelExperience.cs | 10 + .../Families/Enum/FamXpObtainedFromType.cs | 9 + .../Event/FamilyAddExperienceEvent.cs | 16 + .../Families/Event/FamilyAddLogEvent.cs | 15 + .../Families/Event/FamilyAddMemberEvent.cs | 20 + .../Event/FamilyChangeAuthorityEvent.cs | 23 + .../Families/Event/FamilyChangeDeputyEvent.cs | 15 + .../Event/FamilyChangeFactionEvent.cs | 8 + .../Event/FamilyChangeSettingsEvent.cs | 12 + .../Families/Event/FamilyChangeSexEvent.cs | 10 + .../Families/Event/FamilyChangeTitleEvent.cs | 17 + .../Families/Event/FamilyCreateEvent.cs | 12 + .../Families/Event/FamilyCreatedEvent.cs | 7 + .../Families/Event/FamilyDisbandEvent.cs | 7 + .../Families/Event/FamilyDisbandedEvent.cs | 8 + .../Event/FamilyInviteResponseEvent.cs | 17 + .../Families/Event/FamilyInvitedEvent.cs | 8 + .../Families/Event/FamilyJoinedEvent.cs | 9 + .../Families/Event/FamilyKickedMemberEvent.cs | 8 + .../Families/Event/FamilyLeaveEvent.cs | 7 + .../Families/Event/FamilyLeftEvent.cs | 8 + .../Families/Event/FamilyListMembersEvent.cs | 7 + .../Families/Event/FamilyMessageSentEvent.cs | 9 + .../Event/FamilyNoticeMessageEvent.cs | 15 + .../Event/FamilyReceiveInviteEvent.cs | 19 + .../Families/Event/FamilyRemoveMemberEvent.cs | 10 + .../Families/Event/FamilySendInviteEvent.cs | 10 + .../Families/Event/FamilyShoutEvent.cs | 10 + .../Families/Event/FamilyTodayEvent.cs | 10 + .../Event/FamilyUpgradeBoughtEvent.cs | 12 + .../Event/FamilyWarehouseAddItemEvent.cs | 11 + .../Event/FamilyWarehouseCloseEvent.cs | 7 + .../Event/FamilyWarehouseItemPlacedEvent.cs | 11 + .../FamilyWarehouseItemWithdrawnEvent.cs | 11 + .../Event/FamilyWarehouseLogsOpenEvent.cs | 8 + .../Event/FamilyWarehouseMoveItemEvent.cs | 12 + .../Event/FamilyWarehouseOpenEvent.cs | 7 + .../Event/FamilyWarehouseShowItemEvent.cs | 8 + .../Event/FamilyWarehouseWithdrawItemEvent.cs | 9 + .../Families/ExperienceGainedSubMessage.cs | 23 + srcs/WingsAPI.Game/Families/Family.cs | 148 ++ .../Families/FamilyAchievementsVnum.cs | 75 + .../WingsAPI.Game/Families/FamilyComponent.cs | 47 + .../Families/FamilyMembership.cs | 33 + .../Families/FamilyMessageType.cs | 8 + .../Families/FamilyMissionVnums.cs | 22 + srcs/WingsAPI.Game/Families/FamilyUpgrade.cs | 9 + .../Families/FamilyUpgradeBuyFromShopEvent.cs | 9 + .../Families/FamilyUpgradeBuyableState.cs | 8 + srcs/WingsAPI.Game/Families/IFamily.cs | 36 + .../Families/IFamilyComponent.cs | 21 + srcs/WingsAPI.Game/Families/IFamilyManager.cs | 26 + .../Managers/FamilyAchievementManager.cs | 115 + .../Managers/IFamilyAchievementManager.cs | 7 + .../Managers/IFamilyMissionManager.cs | 8 + .../FamilyAchievementIncrementMessage.cs | 12 + .../Messages/FamilyMissionIncrementMessage.cs | 13 + .../Families/StaticFamilyManager.cs | 11 + srcs/WingsAPI.Game/Features/GameFeature.cs | 8 + .../Features/IGameFeatureToggleManager.cs | 10 + .../Features/RedisGameFeatureToggleManager.cs | 21 + .../Configuration/IGameEventConfiguration.cs | 12 + .../IGlobalGameEventConfiguration.cs | 10 + .../Event/GameEventInstanceProcessEvent.cs | 11 + .../Event/GameEventInstanceStartEvent.cs | 19 + srcs/WingsAPI.Game/GameEvent/GameEventType.cs | 6 + .../GameEvent/IGameEventInstance.cs | 16 + .../GameEvent/IGameEventInstanceManager.cs | 10 + .../IGameEventRegistrationManager.cs | 26 + .../InstantBattle/InstantBattleWonEvent.cs | 11 + .../Matchmaking/Filter/FilterResult.cs | 16 + .../Matchmaking/Filter/IMatchmakingFilter.cs | 8 + .../GameEvent/Matchmaking/IMatchmaking.cs | 12 + .../Matchmaking/Matchmaker/IMatchmaker.cs | 10 + .../Matchmaking/Result/IMatchmakingResult.cs | 14 + .../Generics/ThreadSafeHashSet.cs | 145 ++ srcs/WingsAPI.Game/Generics/ThreadSafeList.cs | 666 +++++ .../Generics/ThreadSafeSortedList.cs | 786 ++++++ srcs/WingsAPI.Game/GmCommandEvent.cs | 11 + .../Groups/Events/GroupActionEvent.cs | 10 + .../Groups/Events/GroupAddMemberEvent.cs | 11 + .../Groups/Events/GroupInvitedEvent.cs | 8 + .../Groups/Events/GroupJoinEvent.cs | 10 + .../Groups/Events/GroupLeaveEvent.cs | 7 + .../Groups/Events/GroupRemoveMemberEvent.cs | 11 + .../Groups/Events/GroupWeedingEvent.cs | 9 + srcs/WingsAPI.Game/Groups/GroupComponent.cs | 39 + srcs/WingsAPI.Game/Groups/GroupFactory.cs | 19 + srcs/WingsAPI.Game/Groups/IGroupComponent.cs | 19 + srcs/WingsAPI.Game/Groups/IGroupFactory.cs | 12 + srcs/WingsAPI.Game/Groups/IGroupManager.cs | 18 + srcs/WingsAPI.Game/Groups/PlayerGroup.cs | 111 + .../Calculation/CalculationBasicStatistics.cs | 29 + .../Damages/Calculation/CalculationDefense.cs | 23 + .../Calculation/CalculationElementDamage.cs | 8 + .../Calculation/CalculationPhysicalDamage.cs | 57 + .../Damages/Calculation/CalculationResult.cs | 15 + .../Helpers/Damages/DamageAlgorithmResult.cs | 24 + .../WingsAPI.Game/Helpers/Damages/Position.cs | 32 + .../Helpers/Damages/PositionExtensions.cs | 144 ++ srcs/WingsAPI.Game/Helpers/Location.cs | 15 + srcs/WingsAPI.Game/Helpers/MapLocation.cs | 10 + .../Helpers/PathfindingHelper.cs | 182 ++ srcs/WingsAPI.Game/IRandomGenerator.cs | 35 + .../InterChannel/ChatShoutAdminEvent.cs | 8 + .../InterChannel/FamilyChatMessageEvent.cs | 10 + .../InterChannelChatMessageBroadcastEvent.cs | 21 + .../InterChannelReceiveWhisperEvent.cs | 26 + .../InterChannelSendChatMsgByCharIdEvent.cs | 21 + .../InterChannelSendChatMsgByNicknameEvent.cs | 21 + .../InterChannelSendInfoByCharIdEvent.cs | 17 + .../InterChannelSendInfoByNicknameEvent.cs | 17 + .../InterChannelSendWhisperEvent.cs | 16 + .../Inventory/Event/DropMapItemEvent.cs | 50 + .../Inventory/Event/InventoryAddItemEvent.cs | 36 + .../Inventory/Event/InventoryDropItemEvent.cs | 18 + .../Event/InventoryEquipItemEvent.cs | 20 + .../Event/InventoryItemDeletedEvent.cs | 10 + .../Inventory/Event/InventoryItemUsedEvent.cs | 8 + .../Inventory/Event/InventoryMoveItemEvent.cs | 24 + .../Event/InventoryPickUpItemEvent.cs | 18 + .../Event/InventoryPickedUpItemEvent.cs | 11 + .../Event/InventoryPickedUpPlayerItemEvent.cs | 12 + .../Event/InventoryRemoveItemEvent.cs | 21 + .../Inventory/Event/InventorySortItemEvent.cs | 10 + .../Event/InventoryTakeOffItemEvent.cs | 12 + .../Inventory/Event/ItemSumEvent.cs | 15 + .../Event/PartnerInventoryEquipItemEvent.cs | 18 + .../Event/PartnerInventoryTakeOffItemEvent.cs | 15 + .../Event/PartnerSpecialistSkillEvent.cs | 10 + .../Event/PlayerItemToPartnerItemEvent.cs | 16 + .../Inventory/IInventoryComponent.cs | 61 + .../Inventory/IPartnerInventoryComponent.cs | 25 + .../Inventory/InventoryComponent.cs | 432 ++++ .../Inventory/InventoryException.cs | 17 + srcs/WingsAPI.Game/Inventory/InventoryItem.cs | 9 + .../Inventory/PartnerInventoryComponent.cs | 184 ++ .../Inventory/PartnerInventoryItem.cs | 9 + srcs/WingsAPI.Game/Items/CustomItem.cs | 7 + srcs/WingsAPI.Game/Items/GameItem.cs | 90 + srcs/WingsAPI.Game/Items/GameItemInstance.cs | 68 + .../Items/IDropRarityConfigurationProvider.cs | 8 + srcs/WingsAPI.Game/Items/IGameItem.cs | 97 + .../Items/IGameItemInstanceFactory.cs | 19 + srcs/WingsAPI.Game/Items/IItemBoxManager.cs | 9 + srcs/WingsAPI.Game/Logs/IPlayerActionLog.cs | 12 + .../Logs/IPlayerActionManager.cs | 6 + srcs/WingsAPI.Game/Mails/CharacterMail.cs | 22 + srcs/WingsAPI.Game/Mails/CharacterNote.cs | 29 + .../Mails/Events/MailClaimedEvent.cs | 11 + .../Mails/Events/MailCreateEvent.cs | 24 + .../Mails/Events/MailOpenEvent.cs | 10 + .../Mails/Events/MailRemoveEvent.cs | 10 + .../Mails/Events/MailRemovedEvent.cs | 11 + .../Mails/Events/NoteCreateEvent.cs | 19 + .../Mails/Events/NoteOpenEvent.cs | 15 + .../Mails/Events/NoteRemoveEvent.cs | 15 + .../Mails/Events/NoteSentEvent.cs | 11 + .../WingsAPI.Game/Mails/IMailNoteComponent.cs | 63 + .../WingsAPI.Game/Managers/BubbleComponent.cs | 14 + srcs/WingsAPI.Game/Managers/IAct4Manager.cs | 30 + srcs/WingsAPI.Game/Managers/IBazaarManager.cs | 1 + .../Managers/IBubbleComponent.cs | 9 + srcs/WingsAPI.Game/Managers/IDelayManager.cs | 77 + .../Managers/IForbiddenNamesManager.cs | 13 + .../Managers/IItemUsageManager.cs | 24 + .../WingsAPI.Game/Managers/IRankingManager.cs | 15 + .../WingsAPI.Game/Managers/IRevivalManager.cs | 14 + .../Managers/IScriptedInstanceManager.cs | 15 + srcs/WingsAPI.Game/Managers/IServerManager.cs | 64 + .../WingsAPI.Game/Managers/ISessionManager.cs | 76 + .../Managers/ServerData/IDropManager.cs | 29 + .../Managers/ServerData/IMapMonsterManager.cs | 28 + .../Managers/ServerData/IMapNpcManager.cs | 28 + .../Managers/ServerData/IRecipeManager.cs | 17 + .../Managers/ServerData/IShopManager.cs | 14 + .../Managers/StaticData/ICardsManager.cs | 37 + .../Managers/StaticData/IItemsManager.cs | 52 + .../Managers/StaticData/INpcMonsterManager.cs | 26 + .../Managers/StaticData/ISkillsManager.cs | 23 + .../Managers/TransportFactory.cs | 32 + srcs/WingsAPI.Game/Maps/CharacterMapItem.cs | 18 + .../Maps/Event/DisposeMapEvent.cs | 10 + .../Maps/Event/JoinMapEndEvent.cs | 10 + srcs/WingsAPI.Game/Maps/Event/JoinMapEvent.cs | 42 + .../WingsAPI.Game/Maps/Event/LeaveMapEvent.cs | 10 + .../Maps/Event/MapActivatedEvent.cs | 10 + .../Maps/Event/MapDeactivatedEvent.cs | 10 + .../Maps/Event/PortalRemoveEvent.cs | 8 + .../Maps/Event/PortalTriggerEvent.cs | 10 + .../Maps/Event/SpawnPortalEvent.cs | 15 + srcs/WingsAPI.Game/Maps/IMapAttribute.cs | 6 + srcs/WingsAPI.Game/Maps/IMapInstance.cs | 79 + .../WingsAPI.Game/Maps/IMapInstanceFactory.cs | 6 + srcs/WingsAPI.Game/Maps/IMapManager.cs | 73 + srcs/WingsAPI.Game/Maps/Map.cs | 20 + srcs/WingsAPI.Game/Maps/MapExtensions.cs | 119 + .../Maps/MapInstancePortalHandler.cs | 31 + srcs/WingsAPI.Game/Maps/MapInstanceState.cs | 7 + srcs/WingsAPI.Game/Maps/MapInstanceType.cs | 20 + srcs/WingsAPI.Game/Maps/MapItem.cs | 68 + srcs/WingsAPI.Game/Maps/TimeSpaceMapItem.cs | 35 + .../Mates/Events/MateBackToMinilandEvent.cs | 17 + .../Mates/Events/MateDeathEvent.cs | 17 + .../Mates/Events/MateHealEvent.cs | 10 + .../Mates/Events/MateInitializeEvent.cs | 9 + .../Events/MateJoinInsideMinilandEvent.cs | 8 + .../Mates/Events/MateJoinTeamEvent.cs | 12 + .../Mates/Events/MateLeaveTeamEvent.cs | 8 + .../Events/MateProcessExperienceEvent.cs | 15 + .../Mates/Events/MateRemoveEvent.cs | 8 + .../Mates/Events/MateRestEvent.cs | 10 + .../Mates/Events/MateReviveEvent.cs | 26 + .../Mates/Events/MateSpTransformEvent.cs | 8 + .../Mates/Events/MateSpUntransformEvent.cs | 8 + .../Events/MateStayInsideMinilandEvent.cs | 9 + .../Mates/Events/MateSummonEvent.cs | 8 + srcs/WingsAPI.Game/Mates/IMateComponent.cs | 104 + srcs/WingsAPI.Game/Mates/IMateEntity.cs | 62 + .../WingsAPI.Game/Mates/IMateEntityFactory.cs | 17 + .../Mates/IMateTransportFactory.cs | 6 + .../Mates/MateEntity.MonsterData.cs | 107 + srcs/WingsAPI.Game/Mates/MateEntity.Stats.cs | 108 + srcs/WingsAPI.Game/Mates/MateEntity.cs | 490 ++++ srcs/WingsAPI.Game/Mates/MateEntityFactory.cs | 103 + .../Mates/StaticMateTransportFactory.cs | 11 + .../Events/AddObjMinilandEndLogicEvent.cs | 23 + .../Miniland/Events/AddObjMinilandEvent.cs | 19 + .../Events/MinigameDurabilityCouponEvent.cs | 10 + .../Events/MinigameDurabilityInfoEvent.cs | 10 + .../Events/MinigameGetYieldInfoEvent.cs | 10 + .../Events/MinigameGetYieldRewardEvent.cs | 17 + .../Miniland/Events/MinigamePlayEvent.cs | 16 + .../Events/MinigameRepairDurabilityEvent.cs | 16 + .../Events/MinigameRewardClaimedEvent.cs | 15 + .../Miniland/Events/MinigameRewardEvent.cs | 20 + .../Miniland/Events/MinigameScoreEvent.cs | 19 + .../Miniland/Events/MinigameScoreLogEvent.cs | 15 + .../Miniland/Events/MinigameStopEvent.cs | 10 + .../Miniland/Events/MinilandIntroEvent.cs | 10 + .../Miniland/Events/MinilandStateEvent.cs | 11 + .../Miniland/Events/RmvObjMinilandEvent.cs | 10 + .../Miniland/Events/UseObjMinilandEvent.cs | 16 + .../Miniland/IMinigameManager.cs | 20 + .../Miniland/IMinilandManager.cs | 33 + .../Miniland/InteractionInformationHolder.cs | 31 + .../WingsAPI.Game/Miniland/MapDesignObject.cs | 14 + .../Miniland/MinigameInteraction.cs | 16 + .../MinigameRefreshProductionEvent.cs | 14 + .../Miniland/StaticMinilandManager.cs | 11 + .../Monster/Event/MonsterDeathEvent.cs | 15 + .../Monster/Event/MonsterRespawnedEvent.cs | 9 + .../Monster/Event/MonsterSummonEvent.cs | 29 + srcs/WingsAPI.Game/Monster/Waypoint.cs | 8 + srcs/WingsAPI.Game/MonsterMapItem.cs | 29 + .../Networking/BroadcastPacket.cs | 44 + .../Broadcasting/EmoticonsBroadcast.cs | 6 + .../Broadcasting/ExceptGroupBroadcast.cs | 12 + .../Broadcasting/ExceptRaidBroadcast.cs | 12 + .../Broadcasting/ExceptSessionBroadcast.cs | 10 + .../ExpectBlockedPlayerBroadcast.cs | 9 + .../ExpectRainbowEnemyTeamBroadcast.cs | 23 + .../Broadcasting/FactionBroadcast.cs | 31 + .../Broadcasting/FamilyBroadcast.cs | 10 + .../Networking/Broadcasting/GroupBroadcast.cs | 16 + .../Networking/Broadcasting/IBroadcastRule.cs | 15 + .../Broadcasting/InBaseMapBroadcast.cs | 8 + .../Broadcasting/InRaidBroadcast.cs | 13 + .../Networking/Broadcasting/LevelBroadcast.cs | 10 + .../Broadcasting/NotMutedBroadcast.cs | 8 + .../Broadcasting/OnlyGameMasters.cs | 8 + .../Broadcasting/OnlyPlayersBroadcast.cs | 12 + .../Networking/Broadcasting/RaidBroadcast.cs | 12 + .../Networking/Broadcasting/RangeBroadcast.cs | 19 + .../Broadcasting/SpeakerHeroBroadcast.cs | 6 + .../Broadcasting/TimeSpaceBroadcast.cs | 18 + srcs/WingsAPI.Game/Networking/IBroadcaster.cs | 30 + .../Networking/IClientSession.cs | 47 + .../Networking/SessionsContainer.cs | 303 +++ .../Npcs/Event/ItemProducedEvent.cs | 10 + .../Npcs/Event/MapNpcGenerateDeathEvent.cs | 17 + .../Npcs/Event/NpcSummonEvent.cs | 13 + srcs/WingsAPI.Game/Npcs/MonsterData.cs | 147 ++ srcs/WingsAPI.Game/PlayerCommandEvent.cs | 8 + srcs/WingsAPI.Game/Portals/IPortalEntity.cs | 21 + srcs/WingsAPI.Game/Portals/IPortalFactory.cs | 17 + .../Portals/ITimeSpacePortalEntity.cs | 24 + .../Portals/ITimeSpacePortalFactory.cs | 10 + .../Portals/PortalPacketExtensions.cs | 17 + .../Quests/BasicQuestContainer.cs | 167 ++ srcs/WingsAPI.Game/Quests/CharacterQuest.cs | 102 + .../SoundFlowerConfiguration.cs | 10 + .../Quests/Event/AddQuestEvent.cs | 16 + .../Quests/Event/AddSoundFlowerQuestEvent.cs | 9 + .../Quests/Event/QuestAbandonedEvent.cs | 10 + .../Quests/Event/QuestAddedEvent.cs | 10 + .../Quests/Event/QuestCompletedEvent.cs | 21 + .../Quests/Event/QuestCompletedLogEvent.cs | 10 + .../Quests/Event/QuestDailyRefreshEvent.cs | 8 + .../Quests/Event/QuestHarvestEvent.cs | 9 + .../Quests/Event/QuestItemPickUpEvent.cs | 10 + .../Quests/Event/QuestMonsterDeathEvent.cs | 9 + .../Quests/Event/QuestNpcTalkEvent.cs | 18 + .../Event/QuestObjectiveUpdatedEvent.cs | 8 + .../Quests/Event/QuestRemoveEvent.cs | 15 + .../Quests/Event/QuestRewardEvent.cs | 9 + .../Quests/Event/RunScriptEvent.cs | 8 + srcs/WingsAPI.Game/Quests/IQuestContainer.cs | 40 + srcs/WingsAPI.Game/Quests/IQuestFactory.cs | 8 + srcs/WingsAPI.Game/Quests/IQuestManager.cs | 33 + .../WingsAPI.Game/Quests/IRunScriptHandler.cs | 12 + .../Quests/IRunScriptHandlerContainer.cs | 16 + .../Quicklist/IQuicklistComponent.cs | 14 + .../Quicklist/QuicklistAddEvent.cs | 17 + .../Quicklist/QuicklistComponent.cs | 41 + .../Quicklist/QuicklistRemoveEvent.cs | 9 + .../Quicklist/QuicklistSwapEvent.cs | 10 + srcs/WingsAPI.Game/Raids/ButtonMapItem.cs | 66 + .../Raids/Configuration/RaidConfiguration.cs | 14 + srcs/WingsAPI.Game/Raids/DropChance.cs | 15 + .../Raids/Enum/RaidFinishType.cs | 9 + srcs/WingsAPI.Game/Raids/Enum/RaidJoinType.cs | 7 + .../Raids/Events/RaidAbandonedEvent.cs | 9 + .../Raids/Events/RaidCreatedEvent.cs | 7 + .../Raids/Events/RaidDiedEvent.cs | 7 + .../Raids/Events/RaidGiveRewardsEvent.cs | 18 + .../Raids/Events/RaidInstanceDestroyEvent.cs | 10 + .../Raids/Events/RaidInstanceFinishEvent.cs | 17 + .../Events/RaidInstanceLivesIncDecEvent.cs | 10 + .../Events/RaidInstanceRefreshInfoEvent.cs | 10 + .../Raids/Events/RaidInstanceStartEvent.cs | 12 + .../Raids/Events/RaidInvitedEvent.cs | 8 + .../Raids/Events/RaidJoinedEvent.cs | 9 + .../Raids/Events/RaidLeftEvent.cs | 9 + .../Raids/Events/RaidListJoinEvent.cs | 10 + .../Raids/Events/RaidListOpenEvent.cs | 7 + .../Raids/Events/RaidListRegisterEvent.cs | 7 + .../Raids/Events/RaidListUnregisterEvent.cs | 7 + .../Raids/Events/RaidLostEvent.cs | 7 + .../Raids/Events/RaidMonsterThrowEvent.cs | 35 + .../Events/RaidObjectiveIncreaseEvent.cs | 22 + .../Raids/Events/RaidPartyCreateEvent.cs | 20 + .../Raids/Events/RaidPartyDisbandEvent.cs | 10 + .../Events/RaidPartyInvitePlayerEvent.cs | 10 + .../Raids/Events/RaidPartyJoinEvent.cs | 15 + .../Raids/Events/RaidPartyKickPlayerEvent.cs | 10 + .../Raids/Events/RaidPartyLeaveEvent.cs | 16 + .../Events/RaidPlayerSwitchButtonEvent.cs | 10 + .../Raids/Events/RaidPortalOpenEvent.cs | 16 + .../Raids/Events/RaidResetRestrictionEvent.cs | 7 + .../Raids/Events/RaidRevivedEvent.cs | 8 + .../Raids/Events/RaidRewardReceivedEvent.cs | 8 + .../Raids/Events/RaidStartedEvent.cs | 7 + .../Events/RaidSwitchButtonToggledEvent.cs | 8 + .../Raids/Events/RaidTargetKilledEvent.cs | 8 + .../Raids/Events/RaidTeleportMemberEvent.cs | 21 + .../Raids/Events/RaidWonEvent.cs | 7 + srcs/WingsAPI.Game/Raids/IRaidComponent.cs | 16 + srcs/WingsAPI.Game/Raids/IRaidFactory.cs | 6 + srcs/WingsAPI.Game/Raids/IRaidManager.cs | 20 + srcs/WingsAPI.Game/Raids/RaidBox.cs | 15 + srcs/WingsAPI.Game/Raids/RaidBoxRarity.cs | 13 + srcs/WingsAPI.Game/Raids/RaidComponent.cs | 53 + srcs/WingsAPI.Game/Raids/RaidInstance.cs | 83 + srcs/WingsAPI.Game/Raids/RaidPacketType.cs | 11 + srcs/WingsAPI.Game/Raids/RaidParty.cs | 172 ++ srcs/WingsAPI.Game/Raids/RaidReward.cs | 15 + srcs/WingsAPI.Game/Raids/RaidSubInstance.cs | 135 + srcs/WingsAPI.Game/Raids/RaidWave.cs | 15 + srcs/WingsAPI.Game/Raids/RaidWindowType.cs | 11 + .../Raids/RaidsPacketExtensions.cs | 261 ++ .../WingsAPI.Game/Raids/RdClientPacketType.cs | 9 + .../WingsAPI.Game/Raids/RdServerPacketType.cs | 8 + .../WingsAPI.Game/Raids/RlClientPacketType.cs | 9 + .../WingsAPI.Game/Raids/RlServerPacketType.cs | 9 + .../Event/RainbowBattleCaptureFlagEvent.cs | 10 + .../Event/RainbowBattleDestroyEvent.cs | 8 + .../Event/RainbowBattleEndEvent.cs | 8 + .../Event/RainbowBattleEnterEvent.cs | 8 + .../Event/RainbowBattleFreezeEvent.cs | 9 + .../Event/RainbowBattleFrozenEvent.cs | 11 + .../Event/RainbowBattleJoinEvent.cs | 7 + .../Event/RainbowBattleLeaveEvent.cs | 10 + .../RainbowBattleLeaverBusterRefreshEvent.cs | 8 + .../Event/RainbowBattleLoseEvent.cs | 10 + ...RainbowBattleProcessActivityPointsEvent.cs | 8 + .../RainbowBattleProcessFlagPointsEvent.cs | 8 + .../Event/RainbowBattleProcessLifeEvent.cs | 8 + .../Event/RainbowBattleRefreshScoreEvent.cs | 8 + .../Event/RainbowBattleStartEvent.cs | 11 + ...nbowBattleStartProcessRegistrationEvent.cs | 7 + .../Event/RainbowBattleStartRegisterEvent.cs | 7 + .../Event/RainbowBattleTieEvent.cs | 9 + .../Event/RainbowBattleUnfreezeEvent.cs | 9 + .../RainbowBattleUnfreezeProcessEvent.cs | 8 + .../Event/RainbowBattleWonEvent.cs | 10 + .../RainbowBattle/IRainbowBattleManager.cs | 140 ++ .../RainbowBattle/RainBowFlag.cs | 11 + .../RainbowBattle/RainbowBattleComponent.cs | 52 + .../RainbowBattle/RainbowBattleExtensions.cs | 157 ++ .../RainbowBattle/RainbowBattleParty.cs | 141 ++ .../RainbowBattle/RainbowBattlePlayerDump.cs | 9 + srcs/WingsAPI.Game/RandomBag.cs | 42 + srcs/WingsAPI.Game/Range.cs | 8 + srcs/WingsAPI.Game/Recipes/IRecipeFactory.cs | 8 + srcs/WingsAPI.Game/Recipes/Recipe.cs | 38 + .../Relations/AddRelationEvent.cs | 20 + .../Relations/IInvitationManager.cs | 16 + .../Relations/IRelationComponent.cs | 19 + .../Relations/InvitationManager.cs | 107 + .../Relations/RelationBlockEvent.cs | 8 + .../Relations/RelationComponent.cs | 181 ++ .../Relations/RelationFriendEvent.cs | 10 + .../Relations/RemoveRelationEvent.cs | 16 + .../RespawnReturn/Event/RespawnChangeEvent.cs | 8 + .../RespawnReturn/Event/RespawnPlayerEvent.cs | 7 + .../RespawnReturn/Event/ReturnChangeEvent.cs | 12 + .../RespawnReturn/IHomeComponent.cs | 42 + srcs/WingsAPI.Game/Revival/Act4KillEvent.cs | 8 + .../Revival/CharacterRevivalComponent.cs | 52 + .../Revival/MateRevivalComponent.cs | 28 + srcs/WingsAPI.Game/Revival/RevivalAskEvent.cs | 18 + .../Revival/RevivalReviveEvent.cs | 38 + .../Revival/RevivalStartProcedureEvent.cs | 10 + .../IShipConfigurationProvider.cs | 8 + .../Ship/Configuration/ShipConfiguration.cs | 34 + .../Ship/Event/ShipEnterEvent.cs | 11 + .../Ship/Event/ShipLeaveEvent.cs | 7 + .../Ship/Event/ShipProcessEvent.cs | 16 + srcs/WingsAPI.Game/Ship/IShipManager.cs | 11 + srcs/WingsAPI.Game/Ship/ShipInstance.cs | 24 + .../Shops/Event/BuyItemNpcShopEvent.cs | 11 + .../Shops/Event/BuyShopItemEvent.cs | 10 + .../Shops/Event/BuyShopSkillEvent.cs | 10 + .../WingsAPI.Game/Shops/Event/CurrencyType.cs | 7 + .../Shops/Event/ShopClosedEvent.cs | 7 + .../Shops/Event/ShopNpcBoughtItemEvent.cs | 13 + .../Shops/Event/ShopNpcListItemsEvent.cs | 9 + .../Shops/Event/ShopNpcSoldItemEvent.cs | 11 + .../Shops/Event/ShopOpenedEvent.cs | 8 + .../Shops/Event/ShopPlayerBoughtItemEvent.cs | 13 + .../Shops/Event/ShopPlayerBuyItemEvent.cs | 10 + .../Shops/Event/ShopPlayerCloseEvent.cs | 7 + .../Shops/Event/ShopPlayerOpenEvent.cs | 11 + .../Shops/Event/ShopSkillBoughtEvent.cs | 8 + .../Shops/Event/ShopSkillSoldEvent.cs | 8 + srcs/WingsAPI.Game/Shops/ShopComponent.cs | 57 + srcs/WingsAPI.Game/Shops/ShopNpc.cs | 28 + srcs/WingsAPI.Game/Shops/ShopNpcMenuType.cs | 9 + srcs/WingsAPI.Game/Shops/ShopPlayerItem.cs | 18 + .../Skills/AngelElementBuffComponent.cs | 18 + srcs/WingsAPI.Game/Skills/CharacterSkill.cs | 37 + .../Skills/ComboSkillComponent.cs | 32 + .../Skills/IAngelElementBuffComponent.cs | 10 + .../Skills/IBattleEntitySkill.cs | 11 + .../Skills/IComboSkillComponent.cs | 9 + .../Skills/IEndBuffDamageComponent.cs | 68 + srcs/WingsAPI.Game/Skills/IScoutComponent.cs | 31 + srcs/WingsAPI.Game/Skills/ISkillComponent.cs | 33 + .../Skills/ISkillCooldownComponent.cs | 22 + srcs/WingsAPI.Game/Skills/ISpyOutManager.cs | 11 + .../Skills/IWildKeeperComponent.cs | 73 + srcs/WingsAPI.Game/Skills/NpcMonsterSkill.cs | 32 + srcs/WingsAPI.Game/Skills/PartnerSkill.cs | 15 + .../Skills/SkillCooldownComponent.cs | 37 + srcs/WingsAPI.Game/Skills/SpyOutManager.cs | 29 + .../SnackFood/AdditionalFoodProgress.cs | 17 + .../SnackFood/AdditionalSnackProgress.cs | 17 + .../Events/AddAdditionalHpMpEvent.cs | 12 + srcs/WingsAPI.Game/SnackFood/FoodProgress.cs | 21 + .../SnackFood/FoodSnackComponent.cs | 177 ++ .../SnackFood/FoodSnackComponentFactory.cs | 10 + .../SnackFood/IFoodSnackComponent.cs | 19 + .../SnackFood/IFoodSnackComponentFactory.cs | 6 + .../SnackFood/SnackFoodConfiguration.cs | 12 + srcs/WingsAPI.Game/SnackFood/SnackProgress.cs | 17 + .../Specialists/ISpecialistStatsComponent.cs | 40 + .../Specialists/SpecialistStatsComponent.cs | 618 +++++ .../Enums/PortalMinimapOrientation.cs | 9 + .../TimeSpaces/Enums/TimeSpaceAction.cs | 10 + .../TimeSpaces/Enums/TimeSpaceFinishType.cs | 11 + .../TimeSpaces/Enums/TimeSpaceTaskType.cs | 8 + .../Events/TimeSpaceAddTimeToTimerEvent.cs | 10 + .../Events/TimeSpaceBonusMonsterEvent.cs | 11 + .../TimeSpaceCheckForTasksCompletedEvent.cs | 10 + .../Events/TimeSpaceCheckMonsterEvent.cs | 11 + .../Events/TimeSpaceCheckObjectivesEvent.cs | 10 + .../Events/TimeSpaceClosePortalEvent.cs | 8 + .../TimeSpaces/Events/TimeSpaceDeathEvent.cs | 7 + .../Events/TimeSpaceDecreaseLiveEvent.cs | 7 + .../TimeSpaceDespawnMonstersInRoomEvent.cs | 8 + .../Events/TimeSpaceDestroyEvent.cs | 10 + .../Events/TimeSpaceGroupTryJoinEvent.cs | 10 + .../Events/TimeSpaceIncreaseScoreEvent.cs | 9 + .../Events/TimeSpaceInstanceFinishEvent.cs | 18 + .../Events/TimeSpaceInstanceStartEvent.cs | 7 + .../Events/TimeSpaceLeavePartyEvent.cs | 10 + .../Events/TimeSpacePartyCreateEvent.cs | 20 + .../Events/TimeSpacePickUpItemEvent.cs | 11 + .../Events/TimeSpacePortalOpenEvent.cs | 8 + .../TimeSpaceRefreshObjectiveProgressEvent.cs | 9 + .../Events/TimeSpaceRemoveItemsEvent.cs | 10 + .../Events/TimeSpaceSelectRewardEvent.cs | 8 + .../Events/TimeSpaceSetTimeEvent.cs | 10 + .../Events/TimeSpaceStartClockEvent.cs | 15 + .../Events/TimeSpaceStartPortalEvent.cs | 7 + .../Events/TimeSpaceStartTaskEvent.cs | 8 + .../Events/TimeSpaceTaskCheckEvent.cs | 8 + .../Events/TimeSpaceTogglePortalEvent.cs | 8 + .../Events/TimeSpaceTryFinishTaskEvent.cs | 15 + .../Events/TimeSpaceTryStartHiddenEvent.cs | 10 + .../TimeSpaces/Events/TryStartTaskEvent.cs | 8 + .../TimeSpaces/ITimeSpaceComponent.cs | 16 + .../TimeSpaces/ITimeSpaceFactory.cs | 6 + .../TimeSpaces/ITimeSpaceManager.cs | 18 + .../TimeSpaces/TimeSpaceComponent.cs | 23 + .../TimeSpaces/TimeSpaceInstance.cs | 101 + .../TimeSpaces/TimeSpaceObjective.cs | 23 + .../TimeSpaces/TimeSpacePacketExtensions.cs | 526 ++++ .../TimeSpaces/TimeSpaceParty.cs | 103 + .../TimeSpaces/TimeSpaceRewardItem.cs | 16 + .../TimeSpaces/TimeSpaceSubInstance.cs | 43 + .../WingsAPI.Game/TimeSpaces/TimeSpaceTask.cs | 25 + srcs/WingsAPI.Game/ToSummon.cs | 44 + srcs/WingsAPI.Game/Triggers/BattleTriggers.cs | 10 + .../Triggers/EventTriggerContainer.cs | 85 + .../Triggers/IEventTriggerContainer.cs | 10 + .../Upgrades/Cellons/CellonChances.cs | 7 + .../Upgrades/Cellons/CellonOption.cs | 14 + .../Upgrades/Cellons/CellonPossibilities.cs | 10 + .../Cellons/CellonSystemConfiguration.cs | 9 + .../Events/AccountWarehouseAddItemEvent.cs | 11 + .../Events/AccountWarehouseMoveEvent.cs | 17 + .../Events/AccountWarehouseOpenEvent.cs | 7 + .../Events/AccountWarehouseShowItemEvent.cs | 8 + .../AccountWarehouseWithdrawItemEvent.cs | 16 + .../Events/PartnerWarehouseAddItemEvent.cs | 9 + .../Events/PartnerWarehouseDepositEvent.cs | 20 + .../Events/PartnerWarehouseMoveEvent.cs | 17 + .../Events/PartnerWarehouseWithdrawEvent.cs | 15 + .../Events/WarehouseItemPlacedEvent.cs | 11 + .../Events/WarehouseItemWithdrawnEvent.cs | 11 + .../Warehouse/IAccountWarehouseManager.cs | 17 + srcs/WingsAPI.Game/Warehouse/IWarehouse.cs | 13 + .../Warehouse/PartnerWarehouseItem.cs | 9 + srcs/WingsAPI.Game/Warehouse/WarehouseItem.cs | 9 + srcs/WingsAPI.Game/WingsAPI.Game.csproj | 22 + srcs/WingsAPI.Game/_enum/ActRaidTypes.cs | 12 + srcs/WingsAPI.Game/_enum/ArmorItemSubType.cs | 13 + srcs/WingsAPI.Game/_enum/BankRankType.cs | 19 + srcs/WingsAPI.Game/_enum/BankType.cs | 9 + srcs/WingsAPI.Game/_enum/BuffFlag.cs | 19 + srcs/WingsAPI.Game/_enum/BuffGroup.cs | 12 + srcs/WingsAPI.Game/_enum/BuffGroupIds.cs | 7 + srcs/WingsAPI.Game/_enum/BuffVnums.cs | 80 + srcs/WingsAPI.Game/_enum/CastType.cs | 10 + srcs/WingsAPI.Game/_enum/CellonType.cs | 15 + .../WingsAPI.Game/_enum/CostumeItemSubType.cs | 12 + srcs/WingsAPI.Game/_enum/DialogVnums.cs | 15 + srcs/WingsAPI.Game/_enum/EffectType.cs | 91 + srcs/WingsAPI.Game/_enum/EtcItemType.cs | 13 + srcs/WingsAPI.Game/_enum/GroupAttackType.cs | 8 + srcs/WingsAPI.Game/_enum/ItemClassType.cs | 14 + srcs/WingsAPI.Game/_enum/ItemEffectVnums.cs | 12 + srcs/WingsAPI.Game/_enum/ItemVnums.cs | 339 +++ .../_enum/ManagerResponseType.cs | 8 + srcs/WingsAPI.Game/_enum/MapIds.cs | 31 + srcs/WingsAPI.Game/_enum/MateNrunType.cs | 12 + .../_enum/MinilandItemSubType.cs | 8 + srcs/WingsAPI.Game/_enum/MonsterRaceType.cs | 14 + srcs/WingsAPI.Game/_enum/MonsterSubRace.cs | 75 + srcs/WingsAPI.Game/_enum/MonsterVnum.cs | 78 + srcs/WingsAPI.Game/_enum/MorphIdType.cs | 38 + .../WingsAPI.Game/_enum/NpcMonsterRaceType.cs | 12 + srcs/WingsAPI.Game/_enum/NpcShopType.cs | 7 + srcs/WingsAPI.Game/_enum/QuestsVnums.cs | 16 + srcs/WingsAPI.Game/_enum/ReputationType.cs | 46 + srcs/WingsAPI.Game/_enum/ShellItemSubType.cs | 7 + srcs/WingsAPI.Game/_enum/SkillCastType.cs | 10 + srcs/WingsAPI.Game/_enum/SkillsVnums.cs | 39 + srcs/WingsAPI.Game/_enum/SoundFlowerType.cs | 7 + srcs/WingsAPI.Game/_enum/SoundType.cs | 29 + .../_enum/SpecialistItemSubType.cs | 8 + .../_enum/SpecialistPointsType.cs | 9 + srcs/WingsAPI.Game/_enum/StatisticType.cs | 29 + srcs/WingsAPI.Game/_enum/TimeType.cs | 8 + srcs/WingsAPI.Game/_enum/UsableItemSubType.cs | 12 + srcs/WingsAPI.Game/_enum/WeaponItemSubType.cs | 19 + .../GenericGamePacketHandlerBase.cs | 25 + .../PacketHandlingExtensions.cs | 35 + .../RegisteredPacketHandler.cs | 9 + .../WingsAPI.Packets.Handling.csproj | 14 + srcs/WingsAPI.Packets/ClientPacket.cs | 14 + .../ClientPacketRegistered.cs | 9 + .../ClientPackets/AddobjPacket.cs | 23 + .../ClientPackets/ArenaPacket.cs | 9 + .../ClientPackets/BIPacket.cs | 25 + .../ClientPackets/BrawlerCreatePacket.cs | 30 + .../ClientPackets/BscPacket.cs | 20 + .../ClientPackets/BtkPacket.cs | 20 + .../ClientPackets/BuyPacket.cs | 24 + .../ClientPackets/CBlistPacket.cs | 40 + .../ClientPackets/CBuyPacket.cs | 26 + .../ClientPackets/CClosePacket.cs | 14 + .../ClientPackets/CModPacket.cs | 29 + .../ClientPackets/CRegPacket.cs | 49 + .../ClientPackets/CScalcPacket.cs | 29 + .../ClientPackets/CSkillPacket.cs | 14 + .../ClientPackets/CSlistPacket.cs | 20 + .../ClientPackets/CharacterCreatePacket.cs | 34 + .../ClientPackets/CharacterDeletePacket.cs | 16 + .../ClientPackets/CharacterOptionPacket.cs | 18 + .../ClientPackets/CharacterRenamePacket.cs | 20 + .../ClientPackets/ComplimentPacket.cs | 17 + .../ClientPackets/CreateFamilyPacket.cs | 17 + .../CrossServerEntrypointPacket.cs | 12 + .../ClientPackets/CspServerPacket.cs | 20 + .../ClientPackets/CspePacket.cs | 9 + .../ClientPackets/CsprPacket.cs | 17 + .../ClientPackets/DepositPacket.cs | 31 + .../ClientPackets/DirectionPacket.cs | 25 + .../ClientPackets/EntryPointPacket.cs | 18 + .../ClientPackets/EquipmentInfoPacket.cs | 26 + .../ClientPackets/EscapePacket.cs | 11 + .../ClientPackets/ExcListPacket.cs | 17 + .../ClientPackets/ExchangeRequestPacket.cs | 18 + .../ClientPackets/FDelPacket.cs | 17 + .../ClientPackets/FDepositPacket.cs | 31 + .../ClientPackets/FInsPacket.cs | 20 + .../ClientPackets/FInsPacketType.cs | 9 + .../ClientPackets/FReposPacket.cs | 26 + .../ClientPackets/FStashEndPacket.cs | 14 + .../ClientPackets/FWithdrawPacket.cs | 23 + .../ClientPackets/FamilyChatPacket.cs | 17 + .../ClientPackets/FamilyDisbandPacket.cs | 14 + .../ClientPackets/FamilyManagementPacket.cs | 25 + .../ClientPackets/FauthPacket.cs | 25 + .../ClientPackets/FbPacket.cs | 9 + .../ClientPackets/FhistCtsPacket.cs | 16 + .../ClientPackets/FlPacket.cs | 17 + .../ClientPackets/FrankCtsPacket.cs | 17 + .../ClientPackets/FsLogCtsPacket.cs | 9 + .../ClientPackets/GLeavePacket.cs | 7 + .../ClientPackets/GListPacket.cs | 17 + .../ClientPackets/GListPacketType.cs | 8 + .../ClientPackets/GameStartPacket.cs | 11 + .../ClientPackets/GboxPacket.cs | 21 + .../ClientPackets/GetGiftPacket.cs | 20 + .../ClientPackets/GetGiftType.cs | 8 + .../ClientPackets/GetPacket.cs | 23 + .../ClientPackets/GitPacket.cs | 17 + .../ClientPackets/GroupSayPacket.cs | 17 + .../ClientPackets/GuriPacket.cs | 29 + .../ClientPackets/HeroPacket.cs | 17 + .../ClientPackets/ISortPacket.cs | 14 + .../ClientPackets/JoinFamilyPacket.cs | 20 + .../ClientPackets/LbsPacket.cs | 17 + .../ClientPackets/MJoinPacket.cs | 23 + .../ClientPackets/MLEditPacket.cs | 20 + .../ClientPackets/MShopPacket.cs | 19 + .../ClientPackets/MallPacket.cs | 17 + .../ClientPackets/MinigamePacket.cs | 29 + .../ClientPackets/MkraidPacket.cs | 20 + .../ClientPackets/MultiTargetListPacket.cs | 22 + .../ClientPackets/MultiTargetListSubPacket.cs | 18 + .../ClientPackets/MvePacket.cs | 28 + .../ClientPackets/MviPacket.cs | 28 + .../ClientPackets/MzPacket.cs | 23 + .../ClientPackets/NRunPacket.cs | 27 + .../ClientPackets/NcifPacket.cs | 20 + .../ClientPackets/Nos0575Packet.cs | 22 + .../ClientPackets/NpinfoPacket.cs | 17 + .../ClientPackets/ObaPacket.cs | 11 + .../ClientPackets/PJoinPacket.cs | 22 + .../ClientPackets/PdtClosePacket.cs | 11 + .../ClientPackets/PdtsePacket.cs | 29 + .../ClientPackets/PleavePacket.cs | 14 + .../ClientPackets/PreqPacket.cs | 17 + .../ClientPackets/PslPacket.cs | 13 + .../ClientPackets/PsopServerPacket.cs | 23 + .../ClientPackets/PstPacket.cs | 35 + .../ClientPackets/PstPacketType.cs | 9 + .../ClientPackets/PtCtlPacket.cs | 23 + .../ClientPackets/PtCtlSubPacket.cs | 19 + .../ClientPackets/PulsePacket.cs | 17 + .../ClientPackets/PutPacket.cs | 25 + .../ClientPackets/QSetPacket.cs | 25 + .../ClientPackets/QsetPacketType.cs | 10 + .../ClientPackets/QtPacket.cs | 20 + .../ClientPackets/RInfoPacket.cs | 20 + .../ClientPackets/RankSkPacket.cs | 17 + .../ClientPackets/RdPacket.cs | 23 + .../ClientPackets/Relations/BlDelPacket.cs | 17 + .../ClientPackets/Relations/BlInsPacket.cs | 17 + .../ClientPackets/RemovePacket.cs | 20 + .../ClientPackets/ReposPacket.cs | 26 + .../ClientPackets/ReqInfoPacket.cs | 23 + .../ClientPackets/RequestNpcPacket.cs | 22 + .../ClientPackets/RevivalPacket.cs | 17 + .../ClientPackets/RlPacket.cs | 20 + .../ClientPackets/RmvobjPacket.cs | 17 + .../ClientPackets/RselPacket.cs | 14 + .../ClientPackets/RstartPacket.cs | 17 + .../ClientPackets/RxitPacket.cs | 17 + .../ClientPackets/SayPPacket.cs | 20 + .../ClientPackets/SayPacket.cs | 17 + .../ClientPackets/ScpCtsPacket.cs | 14 + .../ClientPackets/ScriptPacket.cs | 16 + .../ClientPackets/SelectPacket.cs | 17 + .../ClientPackets/SellPacket.cs | 23 + .../ClientPackets/ShopClosePacket.cs | 14 + .../ClientPackets/ShoppingPacket.cs | 20 + .../ClientPackets/SitPacket.cs | 22 + .../ClientPackets/SitSubPacket.cs | 18 + .../ClientPackets/SnapPacket.cs | 14 + .../ClientPackets/SortOpenPacket.cs | 11 + .../ClientPackets/SpTransformPacket.cs | 32 + .../ClientPackets/SpecialistHolderPacket.cs | 23 + .../ClientPackets/StashEndPacket.cs | 14 + .../ClientPackets/SuctlPacket.cs | 29 + .../ClientPackets/TaCallPacket.cs | 17 + .../ClientPackets/TawPacket.cs | 17 + .../ClientPackets/TitEqPacket.cs | 14 + .../ClientPackets/TodayPacket.cs | 14 + .../ClientPackets/TreqClientPacket.cs | 25 + .../ClientPackets/UpetPacket.cs | 26 + .../ClientPackets/UpgradePacket.cs | 37 + .../ClientPackets/UpgradePacketType.cs | 21 + .../ClientPackets/UpsPacket.cs | 30 + .../ClientPackets/UseAtSkillPacket.cs | 23 + .../ClientPackets/UseItemPacket.cs | 18 + .../ClientPackets/UseSkillPacket.cs | 33 + .../ClientPackets/UseobjPacket.cs | 20 + .../ClientPackets/WalkPacket.cs | 26 + .../ClientPackets/WearPacket.cs | 23 + .../ClientPackets/WearPartnerCardPacket.cs | 12 + .../ClientPackets/WhisperPacket.cs | 17 + .../ClientPackets/WithdrawPacket.cs | 19 + .../ClientPackets/WreqPacket.cs | 16 + .../Enums/Act4/Act4FactionStateType.cs | 9 + .../Enums/Act4/DungeonEventType.cs | 10 + .../WingsAPI.Packets/Enums/Act5RespawnType.cs | 9 + .../WingsAPI.Packets/Enums/AdditionalTypes.cs | 1281 ++++++++++ srcs/WingsAPI.Packets/Enums/ArenaTeamType.cs | 12 + .../Enums/BCardScalingType.cs | 9 + srcs/WingsAPI.Packets/Enums/BankActionType.cs | 12 + .../Enums/Battle/AttackType.cs | 16 + .../Enums/Battle/CancelType.cs | 12 + .../Enums/Battle/TargetHitType.cs | 16 + .../Enums/Bazaar/BazaarListedItemType.cs | 14 + .../Bazaar/Filter/BazaarCategoryFilterType.cs | 23 + .../Bazaar/Filter/BazaarLevelFilterType.cs | 24 + .../Filter/BazaarPerfectionFilterType.cs | 17 + .../Bazaar/Filter/BazaarRarityFilterType.cs | 16 + .../Bazaar/Filter/BazaarSortFilterType.cs | 10 + .../Bazaar/Filter/BazaarUpgradeFilterType.cs | 23 + .../BazaarCategoryAccessoriesSubFilterType.cs | 12 + ...BazaarCategoryConsumerItemSubFilterType.cs | 13 + .../BazaarCategoryEquipmentSubFilterType.cs | 19 + .../BazaarCategoryMainItemSubFilterType.cs | 14 + .../BazaarCategoryPartnerSubFilterType.cs | 13 + .../BazaarCategoryPetSubFilterType.cs | 9 + .../BazaarCategoryShellSubFilterType.cs | 9 + .../BazaarCategorySpecialistSubFilterType.cs | 41 + .../BazaarCategoryStoreMountSubFilterType.cs | 9 + ...BazaarCategoryWeaponArmourSubFilterType.cs | 12 + .../Enums/BrawlerMorphType.cs | 12 + srcs/WingsAPI.Packets/Enums/BsInfoType.cs | 8 + srcs/WingsAPI.Packets/Enums/BuffCategory.cs | 10 + srcs/WingsAPI.Packets/Enums/BuyShopType.cs | 12 + .../Enums/Character/CharacterOption.cs | 27 + .../Enums/Character/CharacterState.cs | 13 + .../Enums/Character/ClassType.cs | 16 + .../Enums/Character/GenderType.cs | 13 + .../Enums/Chat/ChatMessageColorType.cs | 31 + srcs/WingsAPI.Packets/Enums/Chat/ChatType.cs | 16 + .../Enums/Chat/MsgMessageType.cs | 12 + srcs/WingsAPI.Packets/Enums/Chat/SpeakType.cs | 12 + .../Enums/Chat/SpeakerType.cs | 8 + srcs/WingsAPI.Packets/Enums/ClockType.cs | 9 + srcs/WingsAPI.Packets/Enums/EntityType.cs | 14 + srcs/WingsAPI.Packets/Enums/EquipmentType.cs | 27 + srcs/WingsAPI.Packets/Enums/ExcCloseType.cs | 8 + srcs/WingsAPI.Packets/Enums/FactionType.cs | 13 + .../Enums/Families/FamilyActionType.cs | 11 + .../Enums/Families/FamilyAuthority.cs | 14 + .../Enums/Families/FamilyJoinType.cs | 9 + .../Enums/Families/FamilyLogType.cs | 25 + .../Enums/Families/FamilySkillsType.cs | 9 + .../Enums/Families/FamilyTitle.cs | 34 + .../Families/FamilyWarehouseAuthorityType.cs | 13 + srcs/WingsAPI.Packets/Enums/FixedUpMode.cs | 12 + srcs/WingsAPI.Packets/Enums/GameType.cs | 12 + .../Enums/GroupRequestType.cs | 17 + .../Enums/GroupSharingType.cs | 12 + srcs/WingsAPI.Packets/Enums/GuriType.cs | 31 + srcs/WingsAPI.Packets/Enums/HairColorType.cs | 138 ++ srcs/WingsAPI.Packets/Enums/HairStyleType.cs | 21 + srcs/WingsAPI.Packets/Enums/InRespawnType.cs | 13 + srcs/WingsAPI.Packets/Enums/InventoryType.cs | 20 + srcs/WingsAPI.Packets/Enums/InvitationType.cs | 14 + srcs/WingsAPI.Packets/Enums/ItModeType.cs | 9 + srcs/WingsAPI.Packets/Enums/ItemType.cs | 39 + .../WingsAPI.Packets/Enums/JewelOptionType.cs | 16 + srcs/WingsAPI.Packets/Enums/LevelType.cs | 16 + srcs/WingsAPI.Packets/Enums/LoginFailType.cs | 19 + .../WingsAPI.Packets/Enums/MShopPacketType.cs | 13 + srcs/WingsAPI.Packets/Enums/Mails/MailType.cs | 8 + .../Enums/Mails/ParcelActionType.cs | 14 + .../WingsAPI.Packets/Enums/MateLevelUpType.cs | 8 + srcs/WingsAPI.Packets/Enums/MateType.cs | 12 + srcs/WingsAPI.Packets/Enums/MedalType.cs | 13 + srcs/WingsAPI.Packets/Enums/MinilandState.cs | 13 + srcs/WingsAPI.Packets/Enums/ModalType.cs | 8 + srcs/WingsAPI.Packets/Enums/MoveType.cs | 18 + srcs/WingsAPI.Packets/Enums/NpcRunType.cs | 201 ++ srcs/WingsAPI.Packets/Enums/PdtiType.cs | 20 + srcs/WingsAPI.Packets/Enums/PenaltyType.cs | 12 + srcs/WingsAPI.Packets/Enums/PortalType.cs | 25 + srcs/WingsAPI.Packets/Enums/QnamlType.cs | 16 + .../WingsAPI.Packets/Enums/QuestRewardType.cs | 23 + srcs/WingsAPI.Packets/Enums/QuestType.cs | 37 + srcs/WingsAPI.Packets/Enums/QueueWindow.cs | 15 + srcs/WingsAPI.Packets/Enums/RaidType.cs | 46 + .../Rainbow/RainbowBattleFlagTeamType.cs | 9 + .../Enums/Rainbow/RainbowBattleFlagType.cs | 9 + .../Enums/Rainbow/RainbowBattleTeamType.cs | 8 + .../Enums/Rainbow/RainbowTimeType.cs | 9 + srcs/WingsAPI.Packets/Enums/RarifyMode.cs | 14 + .../Enums/RarifyProtection.cs | 16 + srcs/WingsAPI.Packets/Enums/ReceiverType.cs | 17 + srcs/WingsAPI.Packets/Enums/RefinerType.cs | 13 + .../Enums/Relations/CharacterRelationType.cs | 14 + .../Enums/RequestExchangeType.cs | 16 + srcs/WingsAPI.Packets/Enums/RespawnType.cs | 14 + .../Enums/ScriptedInstanceType.cs | 14 + srcs/WingsAPI.Packets/Enums/ServerState.cs | 12 + srcs/WingsAPI.Packets/Enums/SheepScoreType.cs | 13 + .../Enums/Shells/ShellEffectCategory.cs | 33 + .../Enums/Shells/ShellEffectType.cs | 135 + .../Enums/Shells/ShellType.cs | 16 + srcs/WingsAPI.Packets/Enums/ShopEndType.cs | 9 + srcs/WingsAPI.Packets/Enums/SmemoType.cs | 16 + .../WingsAPI.Packets/Enums/SpUpgradeResult.cs | 9 + .../Enums/SpecialMapIdType.cs | 13 + .../WingsAPI.Packets/Enums/SuPacketHitMode.cs | 16 + .../Enums/TalentArenaOptionType.cs | 14 + srcs/WingsAPI.Packets/Enums/TeleporterType.cs | 12 + .../Enums/Titles/TitEqPacketType.cs | 8 + .../Enums/Titles/TitleStatus.cs | 14 + srcs/WingsAPI.Packets/Enums/UpgradeMode.cs | 13 + .../Enums/UpgradeProtection.cs | 12 + srcs/WingsAPI.Packets/Enums/UpgradeResult.cs | 9 + srcs/WingsAPI.Packets/Enums/VisualNameType.cs | 8 + srcs/WingsAPI.Packets/Enums/VisualType.cs | 14 + srcs/WingsAPI.Packets/Enums/WindowType.cs | 35 + srcs/WingsAPI.Packets/IClientPacket.cs | 12 + srcs/WingsAPI.Packets/IPacket.cs | 6 + srcs/WingsAPI.Packets/IPacketDeserializer.cs | 9 + srcs/WingsAPI.Packets/IPacketSerializer.cs | 7 + srcs/WingsAPI.Packets/IServerPacket.cs | 9 + srcs/WingsAPI.Packets/PacketAliasAttribute.cs | 24 + srcs/WingsAPI.Packets/PacketDeserializer.cs | 397 +++ srcs/WingsAPI.Packets/PacketExtensions.cs | 24 + .../WingsAPI.Packets/PacketHeaderAttribute.cs | 24 + srcs/WingsAPI.Packets/PacketIndexAttribute.cs | 63 + .../PacketSerializationInformation.cs | 16 + srcs/WingsAPI.Packets/PacketSerializer.cs | 246 ++ srcs/WingsAPI.Packets/ServerPacket.cs | 10 + .../ServerPackets/EffectServerPacket.cs | 23 + .../ServerPackets/Titles/TitInfoPacket.cs | 20 + .../ServerPackets/Titles/TitlePacket.cs | 15 + .../ServerPackets/Titles/TitleSubPacket.cs | 14 + srcs/WingsAPI.Packets/UnresolvedPacket.cs | 10 + srcs/WingsAPI.Packets/WingsAPI.Packets.csproj | 13 + .../Exceptions/CriticalPluginException.cs | 16 + .../Exceptions/PluginException.cs | 15 + .../Extensions/FeatureToggleExtensions.cs | 91 + srcs/WingsAPI.Plugins/GameServerLoader.cs | 9 + .../IDependencyInjectorPlugin.cs | 20 + srcs/WingsAPI.Plugins/IGamePlugin.cs | 14 + srcs/WingsAPI.Plugins/IGameServerPlugin.cs | 9 + srcs/WingsAPI.Plugins/IPlugin.cs | 14 + srcs/WingsAPI.Plugins/IPluginManager.cs | 14 + .../IPluginPathConfigurationProvider.cs | 11 + .../PluginPathConfigurationProvider.cs | 9 + srcs/WingsAPI.Plugins/WingsAPI.Plugins.csproj | 17 + .../Converter/Converter.cs | 54 + .../Converter/GuidConverter.cs | 14 + .../Converter/SEventConverter.cs | 57 + .../Converter/SMapObjectConverter.cs | 29 + .../Extension/AssemblyExtensions.cs | 15 + .../Extension/DynValueExtension.cs | 12 + .../LuaScriptFactory.cs | 98 + srcs/WingsAPI.Scripting.LUA/ObjectFactory.cs | 43 + .../ScriptFactoryConfiguration.cs | 14 + .../WingsAPI.Scripting.LUA.csproj | 17 + .../Attribute/ScriptEventAttribute.cs | 18 + .../Attribute/ScriptObjectAttribute.cs | 9 + .../Converter/SRemovePortalEventConverter.cs | 22 + .../SThrowRaidDropsEventConverter.cs | 31 + .../Converter/ScriptedEventConverter.cs | 19 + .../Enum/Dungeon/SDungeonType.cs | 10 + srcs/WingsAPI.Scripting/Enum/MapObjectType.cs | 8 + .../Enum/Raid/SRaidFinishType.cs | 7 + .../WingsAPI.Scripting/Enum/Raid/SRaidType.cs | 46 + srcs/WingsAPI.Scripting/Enum/SMapFlags.cs | 42 + srcs/WingsAPI.Scripting/Enum/SMapType.cs | 8 + .../WingsAPI.Scripting/Enum/SObjectiveType.cs | 11 + srcs/WingsAPI.Scripting/Enum/SPortalType.cs | 9 + .../TimeSpace/SPortalMinimapOrientation.cs | 10 + .../Enum/TimeSpace/STimeSpaceFinishType.cs | 12 + .../Enum/TimeSpace/STimeSpaceTaskType.cs | 9 + .../Event/Common/SMonsterSummonEvent.cs | 18 + .../Event/Common/SOpenPortalEvent.cs | 11 + .../Event/Common/SRemovePortalEvent.cs | 12 + .../Event/Common/STeleportMembersEvent.cs | 15 + .../Event/Dungeon/SAct4DungeonRewardEvent.cs | 9 + .../Event/Raid/SFinishRaidEvent.cs | 11 + .../Event/Raid/SRaidIncreaseObjectiveEvent.cs | 17 + .../Event/Raid/SThrowRaidDropsEvent.cs | 17 + srcs/WingsAPI.Scripting/Event/SEvent.cs | 12 + .../Event/TimeSpace/SAddTimeEvent.cs | 10 + .../TimeSpace/SCheckForTasksCompletedEvent.cs | 13 + .../Event/TimeSpace/SClosePortal.cs | 11 + .../TimeSpace/SDespawnAllMobsInRoomEvent.cs | 11 + .../Event/TimeSpace/SRemoveItemsEvent.cs | 12 + .../Event/TimeSpace/STimeSpaceFinishEvent.cs | 11 + .../Event/TimeSpace/STogglePortalEvent.cs | 11 + .../Event/TimeSpace/STryStartTaskEvent.cs | 11 + .../Event/TimeSpace/ScriptSetTimeEvent.cs | 10 + srcs/WingsAPI.Scripting/IScriptFactory.cs | 21 + .../InvalidScriptException.cs | 11 + .../Object/Common/Map/SButton.cs | 29 + .../Object/Common/Map/SItem.cs | 14 + .../Object/Common/Map/SMapObject.cs | 23 + .../Object/Common/Map/SMonsterWave.cs | 19 + .../Object/Common/Map/SPortal.cs | 47 + .../WingsAPI.Scripting/Object/Common/SDrop.cs | 19 + .../Object/Common/SLocation.cs | 12 + srcs/WingsAPI.Scripting/Object/Common/SMap.cs | 74 + .../Object/Common/SMapNpc.cs | 33 + .../Object/Common/SMonster.cs | 74 + .../Object/Common/SPosition.cs | 21 + .../Object/Common/SRange.cs | 11 + .../Object/Dungeon/SDungeon.cs | 32 + srcs/WingsAPI.Scripting/Object/Raid/SRaid.cs | 47 + .../Object/Raid/SRaidBox.cs | 13 + .../Object/Raid/SRaidBoxRarity.cs | 11 + .../Object/Raid/SRaidRequirement.cs | 15 + .../Object/Raid/SRaidReward.cs | 12 + .../Object/Raid/SWaypoint.cs | 12 + .../Object/Timespace/STimeSpaceObject.cs | 53 + .../Object/Timespace/STimeSpaceObjective.cs | 23 + .../Timespace/STimeSpaceRequirementObject.cs | 16 + .../Timespace/STimeSpaceRewardsObject.cs | 17 + .../Object/Timespace/STimeSpaceTask.cs | 22 + .../Object/Timespace/STimespaceItemReward.cs | 11 + .../Timespace/TimespaceConstEventKeys.cs | 15 + .../ScriptManager/IDungeonScriptManager.cs | 11 + .../ScriptManager/IRaidScriptManager.cs | 11 + .../ScriptManager/ITimeSpaceScriptManager.cs | 10 + .../Validator/Common/Map/SButtonValidator.cs | 18 + .../Common/Map/SMapObjectValidator.cs | 13 + .../Validator/Common/Map/SPortalValidator.cs | 9 + .../Validator/Common/SEventsValidator.cs | 10 + .../Validator/Common/SLocationValidator.cs | 9 + .../Validator/Common/SMapValidator.cs | 21 + .../Validator/Common/SMonsterValidator.cs | 17 + .../Validator/ItemVnumValidator.cs | 13 + .../Validator/MapVnumValidator.cs | 13 + .../Validator/MonsterVnumValidator.cs | 13 + .../WingsAPI.Scripting.csproj | 16 + .../Extensions/ServiceProviderExtensions.cs | 133 + .../WingsEmu.Communication.gRPC.csproj | 34 + .../DependencyInjectionExtensions.cs | 23 + .../HealthCheckHostedService.cs | 38 + srcs/WingsEmu.Health/IMaintenanceManager.cs | 10 + .../MaintenanceActivateMessageConsumer.cs | 29 + .../MaintenanceDeactivateMessageConsumer.cs | 29 + srcs/WingsEmu.Health/MaintenanceManager.cs | 21 + .../ServiceMaintenanceActivateMessage.cs | 15 + .../ServiceMaintenanceDeactivateMessage.cs | 12 + srcs/WingsEmu.Health/ServiceStatusType.cs | 9 + .../ServiceStatusUpdateMessage.cs | 18 + srcs/WingsEmu.Health/WingsEmu.Health.csproj | 18 + .../Plugin.Act4/Act4DungeonManager.cs | 84 + srcs/_plugins/Plugin.Act4/Act4Manager.cs | 154 ++ srcs/_plugins/Plugin.Act4/Act4Plugin.cs | 20 + srcs/_plugins/Plugin.Act4/Act4PluginCore.cs | 66 + .../Commands/Act4CommandsModule.cs | 27 + .../Commands/Act4DungeonCommandsModule.cs | 106 + .../Const/DungeonConstEventKeys.cs | 7 + srcs/_plugins/Plugin.Act4/DungeonFactory.cs | 299 +++ .../Plugin.Act4/DungeonScriptManager.cs | 72 + .../Act4DungeonBossMapCleanUpEventHandler.cs | 20 + ...4DungeonBroadcastBossClosedEventHandler.cs | 31 + ...ct4DungeonBroadcastBossOpenEventHandler.cs | 34 + .../Act4DungeonBroadcastPacketEventHandler.cs | 40 + .../Event/Act4DungeonEnterEventHandler.cs | 92 + .../Act4DungeonMonsterThrowEventHandler.cs | 68 + .../Event/Act4DungeonRewardEventHandler.cs | 124 + .../Event/Act4DungeonStopEventHandler.cs | 49 + .../Act4DungeonSystemStartEventHandler.cs | 117 + .../Act4DungeonSystemStopEventHandler.cs | 63 + ...Act4FactionPointsGenerationEventHandler.cs | 45 + .../Act4FactionPointsIncreaseEventHandler.cs | 24 + .../Event/Act4JoinMapEndEventHandler.cs | 75 + .../Event/Act4KillBonusEventHandler.cs | 62 + .../Event/Act4MukrajuDeathEventHandler.cs | 43 + .../Event/Act4MukrajuDespawnEventHandler.cs | 40 + .../Event/Act4MukrajuSpawnEventHandler.cs | 88 + .../Event/Act4PutFlagEventHandler.cs | 122 + .../Act4SystemFcBroadcastEventHandler.cs | 32 + .../Event/PortalTriggerAct4EventHandler.cs | 80 + .../Event/PortalTriggerDungeonEventHandler.cs | 82 + .../Event/RevivalAskEventDungeonHandler.cs | 42 + .../Event/RevivalEventAct4Handler.cs | 152 ++ .../Event/RevivalEventDungeonHandler.cs | 90 + .../RevivalStartProcedureEventAct4Handler.cs | 292 +++ ...evivalStartProcedureEventDungeonHandler.cs | 35 + .../Extension/Act4DungeonExtension.cs | 12 + .../Plugin.Act4/Extension/ScriptExtension.cs | 9 + srcs/_plugins/Plugin.Act4/Plugin.Act4.csproj | 24 + .../RecurrentJob/Act4DungeonSystem.cs | 530 ++++ .../Plugin.Act4/RecurrentJob/Act4System.cs | 66 + .../SAct4DungeonRewardEventConverter.cs | 20 + .../Scripting/Validator/SDungeonValidator.cs | 19 + .../Configs/HardcodedDialogsByNpcVnum.cs | 8 + .../HardcodedDialogsByNpcVnumFileConfig.cs | 8 + .../CoreImplDependencyPlugin.cs | 50 + .../Plugin.CoreImpl/Entities/MonsterEntity.cs | 652 +++++ .../Entities/MonsterEntityFactory.cs | 100 + .../Plugin.CoreImpl/Entities/NpcEntity.cs | 582 +++++ .../Entities/NpcEntityFactory.cs | 82 + .../Plugin.CoreImpl/Entities/PortalEntity.cs | 99 + .../Plugin.CoreImpl/Entities/PortalFactory.cs | 35 + .../Entities/TimeSpacePortalEntity.cs | 69 + .../Entities/TimeSpacePortalFactory.cs | 15 + .../Maps/GenericEntityIdManager.cs | 26 + .../Plugin.CoreImpl/Maps/MapInstance.cs | 737 ++++++ .../Maps/MapInstanceFactory.cs | 60 + .../Maps/Systems/BCardTickSystem.cs | 218 ++ .../Maps/Systems/BattleSystem.cs | 737 ++++++ .../Maps/Systems/CharacterSystem.cs | 908 +++++++ .../Maps/Systems/DropSystem.cs | 118 + .../Maps/Systems/MateSystem.cs | 425 ++++ .../Systems/MonsterQuestSystemException.cs | 11 + .../Maps/Systems/MonsterSystem.cs | 2189 +++++++++++++++++ .../Plugin.CoreImpl/Maps/Systems/NpcSystem.cs | 1162 +++++++++ .../Maps/Systems/SkillCooldownSystem.cs | 68 + .../Maps/Systems/SnackFoodSystem.cs | 381 +++ .../Pathfinding/ComparePfNodeMatrix.cs | 27 + .../Plugin.CoreImpl/Pathfinding/Heuristic.cs | 49 + .../Pathfinding/HeuristicFormula.cs | 15 + .../Pathfinding/IPathFinder.cs | 10 + .../Pathfinding/IPriorityQueue.cs | 11 + .../Plugin.CoreImpl/Pathfinding/PathFinder.cs | 253 ++ .../Pathfinding/PathFinderNode.cs | 10 + .../Pathfinding/PathFinderNodeFast.cs | 14 + .../Pathfinding/PathFinderOptions.cs | 27 + .../Pathfinding/PriorityQueueB.cs | 160 ++ .../Plugin.CoreImpl/Plugin.CoreImpl.csproj | 17 + .../Skills/SkillEntityFactory.cs | 23 + .../AuthorizedClientVersionEntity.cs | 28 + .../EfAuthorizedClientVersionRepository.cs | 72 + .../IAuthorizedClientVersionRepository.cs | 11 + .../Auth/HWID/BlacklistedHwidDao.cs | 100 + .../Auth/HWID/BlacklistedHwidEntity.cs | 20 + .../Auth/HWID/IBlacklistedHwidDao.cs | 14 + .../Plugin.DB.EF/Bazaar/BazaarItemDAO.cs | 87 + .../Plugin.DB.EF/Bazaar/DbBazaarItemEntity.cs | 48 + .../Plugin.DB.EF/DAOs/AccountBanDao.cs | 71 + srcs/_plugins/Plugin.DB.EF/DAOs/AccountDAO.cs | 97 + .../Plugin.DB.EF/DAOs/AccountPenaltyDao.cs | 56 + .../Plugin.DB.EF/DAOs/CharacterDAO.cs | 312 +++ .../Plugin.DB.EF/DAOs/CharacterRelationDAO.cs | 121 + .../Plugin.DB.EF/DAOs/TimeSpaceRecordDao.cs | 88 + .../Configs/AccountBansTypeConfiguration.cs | 17 + .../AccountPenaltyTypeConfiguration.cs | 17 + .../DB/Configs/AccountTypeConfiguration.cs | 40 + .../BaseAuditableEntityTypeConfiguration.cs | 18 + ...racterBazaarItemEntityTypeConfiguration.cs | 17 + .../CharacterEntityTypeConfiguration.cs | 63 + ...haracterRelationEntityTypeConfiguration.cs | 22 + .../DbTimeSpaceRecordTypeConfiguration.cs | 13 + .../Plugin.DB.EF/DB/DatabaseConfiguration.cs | 46 + .../Plugin.DB.EF/DB/DatabaseSchemas.cs | 25 + .../DB/DesignTimeContextFactory.cs | 20 + srcs/_plugins/Plugin.DB.EF/DB/GameContext.cs | 144 ++ srcs/_plugins/Plugin.DB.EF/DatabasePlugin.cs | 118 + .../Entities/Account/AccountBanEntity.cs | 32 + .../Entities/Account/AccountEntity.cs | 46 + .../Entities/Account/AccountPenaltyEntity.cs | 35 + .../Entities/BaseAuditableEntity.cs | 17 + .../Plugin.DB.EF/Entities/IAuditableEntity.cs | 15 + .../PlayersData/CharacterRelationEntity.cs | 25 + .../Entities/PlayersData/DbCharacter.cs | 228 ++ .../Entities/ServerData/DbTimeSpaceRecord.cs | 21 + .../DependencyInjectionExtensions.cs | 53 + .../GameContextFactoryExtensions.cs | 32 + .../Plugin.DB.EF/Families/DbFamily.cs | 68 + .../DbFamilyCharacterTypeConfiguration.cs | 20 + .../Families/DbFamilyLogEntity.cs | 43 + .../Families/DbFamilyLogTypeConfiguration.cs | 20 + .../Families/DbFamilyMembership.cs | 45 + .../Families/DbFamilyTypeConfiguration.cs | 39 + .../Plugin.DB.EF/Families/FamilyDAO.cs | 63 + .../Plugin.DB.EF/Families/FamilyLogDAO.cs | 66 + .../Families/FamilyMembershipDao.cs | 81 + .../Families/FamilyWarehouseItemDao.cs | 71 + .../Families/FamilyWarehouseItemEntity.cs | 21 + ...ilyWarehouseItemEntityTypeConfiguration.cs | 17 + .../Families/FamilyWarehouseLogDao.cs | 52 + .../Families/FamilyWarehouseLogEntity.cs | 20 + ...milyWarehouseLogEntityTypeConfiguration.cs | 16 + .../Plugin.DB.EF/Mail/CharacterMailDao.cs | 56 + .../CharacterMailEntityTypeConfiguration.cs | 16 + .../Plugin.DB.EF/Mail/CharacterNoteDao.cs | 53 + .../Plugin.DB.EF/Mail/DbCharacterMail.cs | 39 + .../Plugin.DB.EF/Mail/DbCharacterNote.cs | 57 + .../DbCharacterNoteEntityTypeConfiguration.cs | 21 + .../Plugin.DB.EF/Mapping/GameMappingRules.cs | 134 + .../Plugin.DB.EF/Mapping/MapsterMapper.cs | 39 + .../Mapping/NonGameMappingRules.cs | 73 + .../20211227011918_Init.Designer.cs | 1107 +++++++++ .../Migrations/20211227011918_Init.cs | 709 ++++++ .../Migrations/GameContextModelSnapshot.cs | 1105 +++++++++ .../_plugins/Plugin.DB.EF/Plugin.DB.EF.csproj | 36 + .../Warehouse/AccountWarehouseItemEntity.cs | 22 + ...untWarehouseItemEntityTypeConfiguration.cs | 18 + .../Warehouse/AccountWarehouseItemItemDao.cs | 72 + .../Achievements/AdministratorFamilyModule.cs | 132 + .../Achievements/FamilyAchievementReward.cs | 12 + .../FamilyAchievementSpecificConfiguration.cs | 12 + .../FamilyAchievementUnlockedMessage.cs | 12 + ...amilyAchievementUnlockedMessageConsumer.cs | 58 + .../FamilyAchievementsConfiguration.cs | 13 + .../Handlers/Act4KillEventHandler.cs | 27 + .../FamilyAchievementHandlerAct4DungeonWon.cs | 63 + .../InstantBattleAchievementHandler.cs | 28 + .../Handlers/RaidWonAchievementHandler.cs | 45 + .../Commands/AdministratorFamilyModule.cs | 107 + .../Commands/FamilyKeeperChange.cs | 8 + .../Commands/FamilyModule.cs | 169 ++ .../Commands/FamilyNostaleUiCommandsModule.cs | 168 ++ ...lyAcknowledgeExperiencesMessageConsumer.cs | 33 + .../FamilyAcknowledgeLogsMessageConsumer.cs | 33 + .../FamilyChangeFactionMessageConsumer.cs | 93 + .../FamilyCharacterJoinMessageConsumer.cs | 38 + .../FamilyCharacterLeaveMessageConsumer.cs | 34 + .../Consumers/FamilyChatMessageConsumer.cs | 46 + .../Consumers/FamilyCreatedMessageConsumer.cs | 29 + .../Consumers/FamilyDisbandMessageConsumer.cs | 45 + .../FamilyMemberAddedMessageConsumer.cs | 71 + .../FamilyMemberInviteMessageConsumer.cs | 34 + .../FamilyMemberRemovedMessageConsumer.cs | 41 + .../FamilyMemberUpdateMessageConsumer.cs | 76 + .../Consumers/FamilyShoutMessageConsumer.cs | 37 + .../Consumers/FamilyUpdateMessageConsumer.cs | 183 ++ ...amilyWarehouseItemUpdateMessageConsumer.cs | 57 + .../FamilyWarehouseLogAddMessageConsumer.cs | 19 + .../FamiliesModuleExtensions.cs | 81 + .../FamilyAddExperienceEventHandler.cs | 19 + .../FamilyAddLogEventHandler.cs | 25 + .../FamilyAddMemberEventHandler.cs | 62 + .../FamilyChangeAuthorityEventHandler.cs | 236 ++ .../FamilyChangeDeputyEventHandler.cs | 199 ++ .../FamilyChangeFactionEventHandler.cs | 76 + .../FamilyChangeSettingsEventHandler.cs | 155 ++ .../FamilyChangeSexEventHandler.cs | 65 + .../FamilyChangeTitleEventHandler.cs | 56 + .../FamilyCharacterDisconnectEventHandler.cs | 37 + .../FamilyChatMessageEventHandler.cs | 67 + .../FamilyCreateEventHandler.cs | 217 ++ .../FamilyDisbandEventHandler.cs | 38 + .../FamilyExperienceManager.cs | 34 + .../FamilyInviteResponseEventHandler.cs | 92 + .../FamilyLeaveEventHandler.cs | 64 + .../FamilyListMembersEventHandler.cs | 56 + .../Plugin.FamilyImpl/FamilyManager.cs | 234 ++ .../FamilyNoticeMessageEventHandler.cs | 70 + .../Plugin.FamilyImpl/FamilyPlugin.cs | 56 + .../Plugin.FamilyImpl/FamilyPluginCore.cs | 34 + .../FamilyReceiveInviteEventHandler.cs | 64 + .../FamilyRemoveMemberEventHandler.cs | 79 + .../FamilySendInviteEventHandler.cs | 98 + .../FamilyShoutEventHandler.cs | 80 + .../FamilyTodayEventHandler.cs | 97 + .../FamilyWarehouseAddItemEventHandler.cs | 99 + .../FamilyWarehouseCloseEventHandler.cs | 21 + .../FamilyWarehouseLogsOpenEventHandler.cs | 77 + .../FamilyWarehouseManager.cs | 272 ++ .../FamilyWarehouseMoveItemEventHandler.cs | 68 + .../FamilyWarehouseOpenEventHandler.cs | 87 + .../FamilyWarehouseShowItemEventHandler.cs | 75 + ...FamilyWarehouseWithdrawItemEventHandler.cs | 110 + .../IFamilyExperienceManager.cs | 11 + .../IFamilyWarehouseManager.cs | 20 + .../Logs/FamilyLogManager.cs | 32 + .../Logs/IFamilyLogManager.cs | 11 + ...amilyAcknowledgeExperienceGainedMessage.cs | 12 + .../Messages/FamilyAcknowledgeLogsMessage.cs | 13 + .../Messages/FamilyChangeFactionMessage.cs | 13 + .../Messages/FamilyCharacterJoinMessage.cs | 12 + .../Messages/FamilyCharacterLeaveMessage.cs | 12 + .../Messages/FamilyChatMessage.cs | 17 + .../Messages/FamilyCreatedMessage.cs | 11 + .../FamilyDeclareExperienceGainedMessage.cs | 13 + .../Messages/FamilyDeclareLogsMessage.cs | 13 + .../Messages/FamilyDisbandMessage.cs | 11 + .../Messages/FamilyHeadSexMessage.cs | 14 + .../Messages/FamilyInviteMessage.cs | 17 + .../Messages/FamilyMemberAddedMessage.cs | 16 + .../Messages/FamilyMemberRemovedMessage.cs | 13 + .../Messages/FamilyMemberTodayMessage.cs | 12 + .../Messages/FamilyMemberUpdateMessage.cs | 23 + .../Messages/FamilyNoticeMessage.cs | 12 + .../Messages/FamilyShoutMessage.cs | 15 + .../Messages/FamilyUpdateMessage.cs | 26 + .../FamilyWarehouseItemUpdateMessage.cs | 15 + .../Messages/FamilyWarehouseLogAddMessage .cs | 14 + .../Missions/FamilyMissionReward.cs | 7 + .../FamilyMissionSpecificConfiguration.cs | 13 + .../Missions/FamilyMissionsConfiguration.cs | 8 + .../NpcDialogs/CreateFamilyHandler.cs | 69 + .../NpcDialogs/OpenFamilyWarehouseHandler.cs | 19 + .../OpenFamilyWarehouseHistHandler.cs | 19 + .../Plugin.FamilyImpl.csproj | 27 + .../RecurrentJob/FamilyExperienceSystem.cs | 45 + .../RecurrentJob/FamilyLogSystem.cs | 46 + .../Upgrades/FamilyUpgradeBuyHandler.cs | 136 + .../Act4/Act4DungeonStartedLogEntity.cs | 20 + .../Act4/Act4FamilyDungeonWonLogEntity.cs | 23 + .../Entities/Act4/Act4PvpKillLogEntity.cs | 20 + .../Bazaar/BazaarBoughtItemsLogEntity.cs | 25 + .../Bazaar/BazaarItemExpiredLogEntity.cs | 23 + .../Bazaar/BazaarItemInsertedLogEntity.cs | 24 + .../Bazaar/BazaarItemWithdrawnLogEntity.cs | 24 + .../Entities/Family/FamilyCreatedLogEntity.cs | 22 + .../Family/FamilyDisbandedLogEntity.cs | 19 + .../Entities/Family/FamilyInvitedLogEntity.cs | 20 + .../Entities/Family/FamilyJoinedLogEntity.cs | 20 + .../Entities/Family/FamilyKickedLogEntity.cs | 20 + .../Entities/Family/FamilyLeftLogEntity.cs | 19 + .../Entities/Family/FamilyMessageLogEntity.cs | 21 + .../Family/FamilyUpgradeBoughtLogEntity.cs | 22 + .../FamilyWarehouseItemPlacedLogEntity.cs | 23 + .../FamilyWarehouseItemWithdrawnLogEntity.cs | 23 + .../Entities/IPlayerLogEntity.cs | 11 + .../InventoryItemDeletedLogEntity.cs | 21 + .../Inventory/InventoryItemUsedLogEntity.cs | 19 + .../InventoryPickedUpItemLogEntity.cs | 22 + .../InventoryPickedUpPlayerItemLogEntity.cs | 23 + .../Entities/Mate/LevelUpNosMateLogEntity.cs | 24 + .../MinigameRewardClaimedLogEntity.cs | 26 + .../Miniland/MinigameScoreLogEntity.cs | 25 + .../Miniland/WarehouseItemPlacedLogEntity.cs | 22 + .../WarehouseItemWithdrawnLogEntity.cs | 22 + .../Entities/Npc/ItemProducedLogEntity.cs | 21 + .../Entities/Player/BoxOpenedLogEntity.cs | 22 + .../Entities/Player/GroupInvitedLogEntity.cs | 20 + .../Entities/Player/MailClaimedLogEntity.cs | 22 + .../Entities/Player/MailRemovedLogEntity.cs | 22 + .../Entities/Player/NoteSentLogEntity.cs | 22 + .../Player/PlayerChatGeneralLogEntity.cs | 20 + .../Player/PlayerDisconnectedLogEntity.cs | 30 + .../Player/PlayerExchangeLogEntity.cs | 29 + .../Player/TradeRequestedLogEntity.cs | 19 + .../Entities/Quest/QuestAbandonedLogEntity.cs | 20 + .../Entities/Quest/QuestAddedLogEntity.cs | 20 + .../Entities/Quest/QuestCompletedLogEntity.cs | 22 + .../Quest/QuestObjectiveUpdatedLogEntity.cs | 23 + .../Entities/Raid/RaidAbandonedLogEntity.cs | 19 + .../Entities/Raid/RaidCreatedLogEntity.cs | 20 + .../Entities/Raid/RaidDiedLogEntity.cs | 19 + .../Entities/Raid/RaidInvitedLogEntity.cs | 20 + .../Entities/Raid/RaidJoinedLogEntity.cs | 20 + .../Entities/Raid/RaidLeftLogEntity.cs | 19 + .../Entities/Raid/RaidLostLogEntity.cs | 20 + .../Entities/Raid/RaidRevivedLogEntity.cs | 19 + .../Raid/RaidRewardReceivedLogEntity.cs | 20 + .../Entities/Raid/RaidStartedLogEntity.cs | 21 + .../Raid/RaidSwitchButtonToggledLogEntity.cs | 20 + .../Raid/RaidTargetKilledLogEntity.cs | 20 + .../Entities/Raid/RaidWonLogEntity.cs | 20 + .../RainbowBattleFrozenLogEntity.cs | 19 + .../RainbowBattleJoinLogEntity.cs | 15 + .../RainbowBattleLoseLogEntity.cs | 17 + .../RainbowBattleTieLogEntity.cs | 17 + .../RainbowBattleWonLogEntity.cs | 17 + .../Entities/Shop/ShopClosedLogEntity.cs | 20 + .../Shop/ShopNpcBoughtItemLogEntity.cs | 24 + .../Entities/Shop/ShopNpcSoldItemLogEntity.cs | 22 + .../Entities/Shop/ShopOpenedLogEntity.cs | 21 + .../Shop/ShopPlayerBoughtItemLogEntity.cs | 24 + .../Entities/Shop/ShopSkillBoughtLogEntity.cs | 19 + .../Entities/Shop/ShopSkillSoldLogEntity.cs | 19 + .../Upgrade/CellonUpgradedLogEntity.cs | 22 + .../Entities/Upgrade/ItemGambledLogEntity.cs | 25 + .../Entities/Upgrade/ItemSummedLogEntity.cs | 23 + .../Entities/Upgrade/ItemUpgradedLogEntity.cs | 26 + .../Upgrade/LevelUpCharacterLogEntity.cs | 23 + .../Upgrade/ShellIdentifiedLogEntity.cs | 20 + .../Entities/Upgrade/SpPerfectedLogEntity.cs | 22 + .../Entities/Upgrade/SpUpgradedLogEntity.cs | 24 + .../IgnoreDefaultPropertiesConvention.cs | 28 + .../Extensions/MongoDatabaseExtensions.cs | 68 + .../Extensions/MongoLoggerExtensions.cs | 157 ++ .../Plugin.MongoLogs/Plugin.MongoLogs.csproj | 18 + .../Services/MongoLogsBackgroundService.cs | 69 + .../Services/MongoLogsService.cs | 36 + .../Utils/CollectionNameAttribute.cs | 17 + .../Plugin.MongoLogs/Utils/CollectionNames.cs | 124 + .../Utils/DisplayCollectionNames.cs | 123 + .../Utils/EntityForAttribute.cs | 12 + .../Utils/GenericLogConsumer.cs | 21 + .../Plugin.MongoLogs/Utils/LogType.cs | 9 + .../Utils/MongoDatabaseHelper.cs | 60 + .../Utils/MongoLogsConfiguration.cs | 33 + .../GenericPlayerEventLogMessageFactory.cs | 35 + .../GenericPlayerGameEventToLogProcessor.cs | 30 + .../Core/ILogMessageEnricher.cs | 7 + .../Core/IPlayerEventLogMessageFactory.cs | 11 + .../Core/LogDependencyInjectionExtensions.cs | 21 + .../LogAct4DungeonStartedMessageEnricher.cs | 15 + .../LogAct4FamilyDungeonWonMessageEnricher.cs | 17 + .../Act4/LogAct4PvpKillMessageEnricher.cs | 15 + .../LogBazaarItemBoughtMessageEnricher.cs | 19 + .../LogBazaarItemExpiredMessageEnricher.cs | 17 + .../LogBazaarItemInsertedMessageEnricher.cs | 18 + .../LogBazaarItemWithdrawnMessageEnricher.cs | 18 + .../Family/LogFamilyCreatedMessageEnricher.cs | 18 + .../LogFamilyDisbandedMessageEnricher.cs | 14 + .../Family/LogFamilyInvitedMessageEnricher.cs | 15 + .../Family/LogFamilyJoinedMessageEnricher.cs | 15 + .../Family/LogFamilyKickedMessageEnricher.cs | 15 + .../Family/LogFamilyLeftMessageEnricher.cs | 14 + .../Family/LogFamilyMessageMessageEnricher.cs | 16 + .../LogFamilyUpgradeBoughtMessageEnricher.cs | 17 + ...amilyWarehouseItemPlacedMessageEnricher.cs | 17 + ...lyWarehouseItemWithdrawnMessageEnricher.cs | 17 + .../LogInventoryItemDeletedMessageEnricher.cs | 15 + .../LogInventoryItemUsedMessageEnricher.cs | 14 + ...LogInventoryPickedUpItemMessageEnricher.cs | 16 + ...entoryPickedUpPlayerItemMessageEnricher.cs | 16 + .../LogLevelUpCharacterMessageEnricher.cs | 26 + .../LogLevelUpNosMateMessageEnricher.cs | 18 + .../LogGMCommandExecutedMessageEnricher.cs | 16 + .../LogStrangeBehaviorMessageEnricher.cs | 15 + .../Mail/LogMailClaimedMessageEnricher.cs | 16 + .../Mail/LogMailRemovedMessageEnricher.cs | 16 + .../Mail/LogNoteSentMessageEnricher.cs | 17 + ...LogMinigameRewardClaimedMessageEnricher.cs | 20 + .../LogMinigameScoreMessageEnricher.cs | 19 + .../LogWarehouseItemPlacedMessageEnricher.cs | 16 + ...ogWarehouseItemWithdrawnMessageEnricher.cs | 16 + .../Npc/LogItemProducedMessageEnricher.cs | 15 + .../Player/LogBoxOpenedMessageEnricher.cs | 15 + .../Player/LogGroupInvitedMessageEnricher.cs | 15 + .../Player/LogPlayerChatMessageEnricher.cs | 16 + ...LogPlayerCommandExecutedMessageEnricher.cs | 14 + .../LogPlayerDisconnectedMessageEnricher.cs | 18 + .../LogPlayerExchangeMessageEnricher.cs | 46 + .../LogTradeRequestedMessageEnricher.cs | 14 + .../Quest/LogQuestAbandonedMessageEnricher.cs | 15 + .../Quest/LogQuestAddedMessageEnricher.cs | 15 + .../Quest/LogQuestCompletedMessageEnricher.cs | 16 + ...LogQuestObjectiveUpdatedMessageEnricher.cs | 16 + .../Raid/LogRaidAbandonedMessageEnricher.cs | 14 + .../Raid/LogRaidCreatedMessageEnricher.cs | 17 + .../Raid/LogRaidDiedMessageEnricher.cs | 15 + .../Raid/LogRaidInvitedMessageEnricher.cs | 15 + .../Raid/LogRaidJoinedMessageEnricher.cs | 15 + .../Raid/LogRaidLeftMessageEnricher.cs | 14 + .../Raid/LogRaidLostMessageEnricher.cs | 15 + .../Raid/LogRaidRevivedMessageEnricher.cs | 15 + .../LogRaidRewardReceivedMessageEnricher.cs | 15 + .../Raid/LogRaidStartedMessageEnricher.cs | 20 + .../LogRaidSwitchButtonToggledEnricher.cs | 20 + .../LogRaidTargetKilledMessageEnricher.cs | 15 + .../Raid/LogRaidWonMessageEnricher.cs | 15 + .../LogRainbowBattleFrozenMessageEnricher.cs | 16 + .../LogRainbowBattleJoinMessageEnricher.cs | 13 + .../LogRainbowBattleLoseMessageEnricher.cs | 15 + .../LogRainbowBattleTieMessageEnricher.cs | 15 + .../LogRainbowBattleWonMessageEnricher.cs | 14 + .../Shop/LogShopClosedMessageEnricher.cs | 15 + .../LogShopNpcBoughtItemMessageEnricher.cs | 18 + .../Shop/LogShopNpcSoldItemMessageEnricher.cs | 16 + .../Shop/LogShopOpenedMessageEnricher.cs | 16 + .../LogShopPlayerBoughtItemMessageEnricher.cs | 18 + .../Shop/LogShopSkillBoughtMessageEnricher.cs | 14 + .../Shop/LogShopSkillSoldMessageEnricher.cs | 14 + .../LogCellonUpgradedMessageEnricher.cs | 16 + .../Upgrade/LogItemGambledMessageEnricher.cs | 20 + .../Upgrade/LogItemSummedMessageEnricher.cs | 17 + .../Upgrade/LogItemUpgradedMessageEnricher.cs | 20 + .../LogShellIdentifiedMessageEnricher.cs | 14 + .../Upgrade/LogSpPerfectedMessageEnricher.cs | 16 + .../Upgrade/LogSpUpgradedMessageEnricher.cs | 18 + .../IPlayerActionLogMessage.cs | 9 + .../Act4/LogAct4DungeonStartedMessage.cs | 17 + .../Act4/LogAct4FamilyDungeonWonMessage.cs | 20 + .../Messages/Act4/LogAct4PvpKillMessage.cs | 17 + .../Bazaar/LogBazaarItemBoughtMessage.cs | 22 + .../Bazaar/LogBazaarItemExpiredMessage.cs | 20 + .../Bazaar/LogBazaarItemInsertedMessage.cs | 21 + .../Bazaar/LogBazaarItemWithdrawnMessage.cs | 21 + .../Family/LogFamilyCreatedMessage.cs | 19 + .../Family/LogFamilyDisbandedMessage.cs | 16 + .../Family/LogFamilyInvitedMessage.cs | 17 + .../Messages/Family/LogFamilyJoinedMessage.cs | 17 + .../Messages/Family/LogFamilyKickedMessage.cs | 18 + .../Messages/Family/LogFamilyLeftMessage.cs | 16 + .../Family/LogFamilyMessageMessage.cs | 18 + .../Family/LogFamilyUpgradeBoughtMessage.cs | 19 + .../LogFamilyWarehouseItemPlacedMessage.cs | 20 + .../LogFamilyWarehouseItemWithdrawnMessage.cs | 20 + .../LogInventoryItemDeletedMessage.cs | 18 + .../Inventory/LogInventoryItemUsedMessage.cs | 16 + .../LogInventoryPickedUpItemMessage.cs | 19 + .../LogInventoryPickedUpPlayerItemMessage.cs | 20 + .../LevelUp/LogLevelUpCharacterMessage.cs | 20 + .../LevelUp/LogLevelUpNosMateMessage.cs | 21 + .../Messages/LogGMCommandExecutedMessage.cs | 13 + .../Messages/LogStrangeBehaviorMessage.cs | 17 + .../Messages/Mail/LogMailClaimedMessage.cs | 19 + .../Messages/Mail/LogMailRemovedMessage.cs | 19 + .../Messages/Mail/LogMailSentMessage.cs | 19 + .../Messages/Mail/LogNoteSentMessage.cs | 19 + .../LogMinigameRewardClaimedMessage.cs | 23 + .../Miniland/LogMinigameScoreMessage.cs | 22 + .../Miniland/LogWarehouseItemPlacedMessage.cs | 19 + .../LogWarehouseItemWithdrawnMessage.cs | 19 + .../Messages/Npc/LogItemProducedMessage.cs | 18 + .../Messages/Player/LogBoxOpenedMessage.cs | 19 + .../Messages/Player/LogGroupInvitedMessage.cs | 17 + .../Messages/Player/LogPlayerChatMessage.cs | 23 + .../Player/LogPlayerCommandExecutedMessage.cs | 16 + .../Player/LogPlayerDisconnectedMessage.cs | 20 + .../Player/LogPlayerExchangeMessage.cs | 25 + .../Player/LogTradeRequestedMessage.cs | 16 + .../Quest/LogQuestAbandonedMessage.cs | 17 + .../Messages/Quest/LogQuestAddedMessage.cs | 17 + .../Quest/LogQuestCompletedMessage.cs | 19 + .../Quest/LogQuestObjectiveUpdatedMessage.cs | 20 + .../Messages/Raid/LogRaidAbandonedMessage.cs | 16 + .../Messages/Raid/LogRaidCreatedMessage.cs | 17 + .../Messages/Raid/LogRaidDiedMessage.cs | 16 + .../Messages/Raid/LogRaidInvitedMessage.cs | 17 + .../Messages/Raid/LogRaidJoinedMessage.cs | 17 + .../Messages/Raid/LogRaidLeftMessage.cs | 16 + .../Messages/Raid/LogRaidLostMessage.cs | 17 + .../Messages/Raid/LogRaidRevivedMessage.cs | 17 + .../Raid/LogRaidRewardReceivedMessage.cs | 17 + .../Messages/Raid/LogRaidStartedMessage.cs | 18 + .../Raid/LogRaidSwitchButtonToggledMessage.cs | 17 + .../Raid/LogRaidTargetKilledMessage.cs | 17 + .../Messages/Raid/LogRaidWonMessage.cs | 18 + .../LogRainbowBattleFrozenMessage.cs | 19 + .../LogRainbowBattleJoinMessage.cs | 15 + .../LogRainbowBattleLoseMessage.cs | 17 + .../LogRainbowBattleTieMessage.cs | 17 + .../LogRainbowBattleWonMessage.cs | 17 + .../Messages/Shop/LogShopClosedMessage.cs | 17 + .../Shop/LogShopNpcBoughtItemMessage.cs | 21 + .../Shop/LogShopNpcSoldItemMessage.cs | 19 + .../Messages/Shop/LogShopOpenedMessage.cs | 18 + .../Shop/LogShopPlayerBoughtItemMessage.cs | 21 + .../Shop/LogShopSkillBoughtMessage.cs | 16 + .../Messages/Shop/LogShopSkillSoldMessage.cs | 16 + .../Upgrade/LogCellonUpgradedMessage.cs | 19 + .../Messages/Upgrade/LogItemGambledMessage.cs | 22 + .../Messages/Upgrade/LogItemSummedMessage.cs | 20 + .../Upgrade/LogItemUpgradedMessage.cs | 23 + .../Upgrade/LogShellIdentifiedMessage.cs | 17 + .../Messages/Upgrade/LogSpPerfectedMessage.cs | 20 + .../Messages/Upgrade/LogSpUpgradedMessage.cs | 21 + .../Plugin.PlayerLogs/PlayerLogManager.cs | 74 + .../PlayerLoggingDependencyPlugin.cs | 182 ++ .../Plugin.PlayerLogs.csproj | 19 + .../Plugin.QuestImpl/BaseRunScriptHandler.cs | 54 + .../Handlers/AddGeneralQuestEventHandler.cs | 105 + .../Handlers/AddMainQuestEventHandler.cs | 126 + .../Handlers/AddSecondaryQuestEventHandler.cs | 107 + .../AddSoundFlowerQuestEventHandler.cs | 83 + .../Handlers/QuestCompletedEventHandler.cs | 127 + .../Handlers/QuestDailyRefreshEventHandler.cs | 31 + .../Handlers/QuestHarvestEventHandler.cs | 60 + .../Handlers/QuestItemPickUpEventHandler.cs | 90 + .../Handlers/QuestMonsterDeathEventHandler.cs | 90 + .../Handlers/QuestNpcTalkEventHandler.cs | 265 ++ .../Handlers/QuestRemoveEventHandler.cs | 60 + .../Handlers/QuestRewardEventHandler.cs | 323 +++ .../Plugin.QuestImpl/Managers/QuestManager.cs | 222 ++ .../Plugin.QuestImpl/Plugin.QuestImpl.csproj | 20 + .../QuestDependencyInjectionExtensions.cs | 21 + .../_plugins/Plugin.QuestImpl/QuestFactory.cs | 24 + srcs/_plugins/Plugin.QuestImpl/QuestModule.cs | 293 +++ srcs/_plugins/Plugin.QuestImpl/QuestPlugin.cs | 49 + .../Plugin.QuestImpl/QuestPluginCore.cs | 45 + .../Plugin.QuestImpl/RunScriptEventHandler.cs | 20 + .../TeleportRunScriptHandler.cs | 74 + .../Commands/RaidAdminCommandsModule.cs | 99 + .../Commands/RaidAdminStartModule.cs | 30 + .../Configs/RaidStartConfiguration.cs | 11 + .../Configs/RaidStartFileConfiguration.cs | 11 + .../Plugin.Raids/Const/RaidConstEventKeys.cs | 9 + .../Extension/ScriptExtensions.cs | 9 + .../Handlers/PortalTriggerRaidEventHandler.cs | 56 + .../Handlers/RaidGiveRewardsEventHandler.cs | 122 + .../Handlers/RaidInstanceActivateRaidWaves.cs | 42 + .../RaidInstanceDestroyEventHandler.cs | 57 + .../RaidInstanceFinishEventHandler.cs | 188 ++ .../RaidInstanceLivesIncDecEventHandler.cs | 37 + .../RaidInstanceRefreshInfoEventHandler.cs | 29 + .../Handlers/RaidInstanceStartEventHandler.cs | 144 ++ .../Handlers/RaidJoinMapEndEventHandler.cs | 29 + .../Handlers/RaidListJoinEventHandler.cs | 66 + .../Handlers/RaidListOpenEventHandler.cs | 29 + .../Handlers/RaidListRegisterEventHandler.cs | 54 + .../RaidListUnregisterEventHandler.cs | 47 + .../Handlers/RaidMonsterThrowEventHandler.cs | 68 + .../RaidObjectiveIncreaseEventHandler.cs | 53 + .../Handlers/RaidPartyCreateEventHandler.cs | 161 ++ .../Handlers/RaidPartyDisbandEventHandler.cs | 87 + .../RaidPartyInvitePlayerEventHandler.cs | 122 + .../Handlers/RaidPartyJoinEventHandler.cs | 215 ++ .../RaidPartyKickPlayerEventHandler.cs | 59 + .../Handlers/RaidPartyLeaveEventHandler.cs | 137 ++ .../RaidPlayerSwitchButtonEventHandler.cs | 80 + .../Handlers/RaidPortalOpenEventHandler.cs | 18 + .../RaidResetRestrictionEventHandler.cs | 31 + .../RaidTeleportMemberEventHandler.cs | 62 + .../Handlers/RevivalEventRaidHandler.cs | 35 + .../RevivalStartProcedureEventRaidHandler.cs | 61 + .../_plugins/Plugin.Raids/Plugin.Raids.csproj | 24 + srcs/_plugins/Plugin.Raids/RaidFactory.cs | 336 +++ srcs/_plugins/Plugin.Raids/RaidManager.cs | 44 + srcs/_plugins/Plugin.Raids/RaidsPlugin.cs | 20 + srcs/_plugins/Plugin.Raids/RaidsPluginCore.cs | 61 + .../Plugin.Raids/RecurrentJob/RaidSystem.cs | 158 ++ .../Converter/SFinishRaidEventConverter.cs | 17 + .../Converter/SMonsterSummonEventConverter.cs | 61 + .../SOpenRaidPortalEventConverter.cs | 24 + .../SRaidIncreaseObjectiveEventConverter.cs | 16 + .../STeleportMembersEventConverter.cs | 26 + .../Scripting/RaidScriptManager.cs | 72 + .../Raid/SRaidRequirementValidator.cs | 8 + .../Validator/Raid/SRaidValidator.cs | 21 + .../BattleEntityAlgorithmService.cs | 724 ++++++ .../FileResourceLoaderPlugin.cs | 56 + .../InMemoryGameDataLanguageService.cs | 55 + .../InMemoryMultilanguageService.cs | 88 + .../Loaders/ActDescResourceFileLoader.cs | 70 + .../Loaders/CardResourceFileLoader.cs | 203 ++ .../Loaders/GameDataLanguageFileLoader.cs | 157 ++ .../Loaders/GenericTranslationGrpcLoader.cs | 17 + .../Loaders/ItemResourceFileLoader.cs | 724 ++++++ .../Loaders/MapResourceFileLoader.cs | 85 + .../Loaders/NpcMonsterFileLoader.cs | 487 ++++ .../Loaders/NpcQuestResourceFileLoader.cs | 70 + .../Loaders/QuestResourceFileLoader.cs | 189 ++ .../Loaders/SkillResourceFileLoader.cs | 317 +++ .../Loaders/TutorialResourceFileLoader.cs | 103 + .../Plugin.ResourceLoader.csproj | 15 + .../ResourceLoadingConfiguration.cs | 15 + .../Commands/TimeSpaceAdminStartModule.cs | 55 + .../RevivalAskEventTimeSpaceHandler.cs | 38 + .../Handlers/RevivalEventTimeSpaceHandler.cs | 48 + ...ivalStartProcedureEventTimeSpaceHandler.cs | 37 + .../TimeSpaceAddTimeToTimerEventHandler.cs | 45 + .../TimeSpaceBonusMonsterEventHandler.cs | 44 + ...SpaceCheckForTasksCompletedEventHandler.cs | 55 + .../TimeSpaceCheckMonsterEventHandler.cs | 112 + .../TimeSpaceCheckObjectivesEventHandler.cs | 101 + .../TimeSpaceClosePortalEventHandler.cs | 28 + .../Handlers/TimeSpaceDeathEventHandler.cs | 27 + .../TimeSpaceDecreaseLiveEventHandler.cs | 50 + ...eSpaceDespawnMonstersInRoomEventHandler.cs | 40 + .../Handlers/TimeSpaceDestroyEventHandler.cs | 49 + .../TimeSpaceGroupTryJoinEventHandler.cs | 167 ++ .../TimeSpaceIncreaseScoreEventHandler.cs | 42 + .../TimeSpaceInstanceFinishEventHandler.cs | 276 +++ .../TimeSpaceInstanceStartEventHandler.cs | 98 + .../TimeSpaceJoinMapEndEventHandler.cs | 138 ++ .../TimeSpaceLeavePartyEventHandler.cs | 105 + .../Handlers/TimeSpaceMonsterDeathHandler.cs | 97 + .../TimeSpacePartyCreateEventHandler.cs | 138 ++ .../TimeSpacePickUpItemEventHandler.cs | 117 + .../TimeSpacePortalOpenEventHandler.cs | 86 + .../TimeSpacePortalTriggerEventHandler.cs | 102 + ...aceRefreshObjectiveProgressEventHandler.cs | 37 + .../TimeSpaceRemoveItemsEventHandler.cs | 26 + .../TimeSpaceSelectRewardEventHandler.cs | 156 ++ .../Handlers/TimeSpaceSetTimeEventHandler.cs | 36 + .../TimeSpaceStartClockEventHandler.cs | 38 + .../TimeSpaceStartPortalEventHandler.cs | 81 + .../TimeSpaceStartTaskEventHandler.cs | 136 + .../TimeSpaceTogglePortalEventHandler.cs | 74 + .../TimeSpaceTryFinishTaskEventHandler.cs | 153 ++ .../TimeSpaceTryStartHiddenEventHandler.cs | 224 ++ .../TryStartTaskForMapEventHandler.cs | 43 + .../LuaTimeSpaceScriptManager.cs | 78 + .../Plugin.TimeSpaces.csproj | 20 + .../RecurrentJob/TimeSpaceSystem.cs | 223 ++ .../Plugin.TimeSpaces/STimeSpaceValidator.cs | 18 + .../Scripting/SAddTimeEventConverter.cs | 22 + .../SCheckForTasksCompletedEventConverter.cs | 51 + .../Scripting/SClosePortalEventConverter.cs | 22 + .../SDespawnAllMobsInRoomEventConverter.cs | 22 + .../Scripting/SMonsterSummonEventConverter.cs | 34 + .../SOpenTimeSpacePortalEventConverter.cs | 25 + .../Scripting/SRemoveItemsEventConverter.cs | 30 + .../STimeSpaceFinishEventConverter.cs | 17 + .../Scripting/STogglePortalEventConverter.cs | 22 + .../Scripting/STryStartTaskEventConverter.cs | 22 + .../Scripting/ScriptSetTimeEventConverter.cs | 22 + .../Plugin.TimeSpaces/TimeSpaceFactory.cs | 510 ++++ .../Plugin.TimeSpaces/TimeSpaceManager.cs | 48 + .../Plugin.TimeSpaces/TimeSpacesPlugin.cs | 27 + .../Plugin.TimeSpaces/TimeSpacesPluginCore.cs | 23 + .../Act5/Act5OpenNpcRunEventHandler.cs | 103 + .../Algorithms/AlgorithmPluginCore.cs | 15 + .../Algorithms/CellonGenerationAlgorithm.cs | 45 + .../CharacterAlgorithm/CharacterAlgorithm.cs | 772 ++++++ .../Algorithms/DamageAlgorithm.cs | 98 + .../Algorithms/ExperienceExtension.cs | 130 + .../FamilyLevelBasedAlgorithm.cs | 17 + .../Algorithms/ILevelBasedDataAlgorithm.cs | 7 + .../Algorithms/ShellGenerationAlgorithm.cs | 98 + .../Shells/ShellCategoryConfiguration.cs | 14 + .../Shells/ShellLevelEffectConfiguration.cs | 59 + .../Shells/ShellOptionTypeConfiguration.cs | 66 + .../Shells/ShellPerfumeConfiguration.cs | 52 + .../Arena/ArenaManager.cs | 38 + .../BCards/BCardEffectContextFactory.cs | 13 + .../BCards/BCardGamePlugin.cs | 43 + .../BCards/BCardHandlerContainer.cs | 49 + .../BCards/BCardHandlersServicesExtensions.cs | 30 + .../BCards/BCardPluginCore.cs | 15 + .../BCards/BcardEffectContext.cs | 25 + .../BCards/Handlers/BCardBeriosHandler.cs | 113 + .../BCards/Handlers/BCardBuffHandler.cs | 216 ++ .../BCards/Handlers/BCardCalvinasHandler.cs | 86 + .../BCards/Handlers/BCardCaptureHandler.cs | 86 + .../BCards/Handlers/BCardCountHandler.cs | 61 + .../Handlers/BCardDarkCloneSummonHandler.cs | 77 + .../BCards/Handlers/BCardDestroyerHandler.cs | 85 + .../Handlers/BCardDrainAndStealHandler.cs | 185 ++ .../BCards/Handlers/BCardDrainHandler.cs | 59 + .../BCards/Handlers/BCardFearSkillHandler.cs | 73 + .../BCards/Handlers/BCardHatusHandler.cs | 87 + .../BCardHealingBurningAndCastingHandler.cs | 109 + .../BCards/Handlers/BCardHideHandler.cs | 98 + .../Handlers/BCardJumpBackPushHandler.cs | 195 ++ .../BCards/Handlers/BCardKnockdownHandler.cs | 177 ++ .../Handlers/BCardLightAndShadowHandler.cs | 80 + .../BCards/Handlers/BCardMateSummonHandler.cs | 134 + .../Handlers/BCardMeditationSkillHandler.cs | 167 ++ .../Handlers/BCardMeteoriteTeleportHandler.cs | 206 ++ .../BCards/Handlers/BCardModeHandler.cs | 48 + .../BCards/Handlers/BCardMorcosHandler.cs | 115 + .../BCards/Handlers/BCardMoveHandler.cs | 35 + .../BCards/Handlers/BCardQuestHandler.cs | 49 + .../BCardRecoveryAndDamagePercentHandler.cs | 85 + .../BCards/Handlers/BCardReflectionHandler.cs | 37 + .../Handlers/BCardSESpecialistHandler.cs | 38 + .../Handlers/BCardSpecialActionsHandler.cs | 381 +++ .../Handlers/BCardSpecialBehaviorHandler.cs | 111 + .../Handlers/BCardSpecialDamageHandler.cs | 130 + .../Handlers/BCardSpecialEffect2Handler.cs | 144 ++ .../Handlers/BCardSpecialEffectHandler.cs | 103 + .../BCardSpecialisationBuffResistance.cs | 49 + .../BCards/Handlers/BCardSummonHandler.cs | 170 ++ .../Handlers/BCardTeleportToLocation.cs | 27 + .../Handlers/BCardTimeTwisterHandler.cs | 56 + .../Handlers/TimeCircleSkillsHandler.cs | 33 + .../BaseGuriHandler.cs | 49 + .../BazaarGetListedItemsEventHandler.cs | 35 + .../Bazaar/BazaarItemAddEventHandler.cs | 138 ++ .../Bazaar/BazaarItemBuyEventHandler.cs | 128 + .../BazaarItemChangePriceEventHandler.cs | 133 + .../Bazaar/BazaarItemRemoveEventHandler.cs | 116 + .../Bazaar/BazaarManager.cs | 162 ++ .../Bazaar/BazaarModuleExtensions.cs | 18 + .../Bazaar/BazaarOpenUiEventHandler.cs | 44 + .../Bazaar/BazaarSearchItemsEventHandler.cs | 61 + .../TotalDamageDealtEventHandler.cs | 36 + .../TotalDeathsEventHandler.cs | 38 + .../TotalGoldDroppedEventHandler.cs | 22 + ...otalGoldEarnedInBazaarItemsEventHandler.cs | 16 + .../TotalGoldSpentInBazaarFeesEventHandler.cs | 17 + ...TotalGoldSpentInBazaarItemsEventHandler.cs | 17 + .../TotalGoldSpentInNpcShopEventHandler.cs | 17 + .../TotalInstantBattleWonEventHandler.cs | 16 + .../TotalItemsUsedEventHandler.cs | 31 + .../TotalMonstersKilledEventHandler.cs | 47 + .../TotalRaidsLostEventHandler.cs | 16 + .../TotalRaidsWonEventHandler.cs | 16 + .../TotalSkillsCastedEventHandler.cs | 20 + .../Chat/ChatSendFriendMessageEventHandler.cs | 102 + .../Chat/ChatSpeakerEventHandler.cs | 101 + .../Compliments/ComplimentsManager.cs | 21 + .../ComplimentsMonthlyRefreshEventHandler.cs | 28 + .../DbServer/CharacterSaveSystem.cs | 113 + .../DbServer/DbServerModuleExtensions.cs | 14 + .../GenerateEntityDeathEventHandler.cs | 104 + .../MapJoinMonsterEntityEventHandler.cs | 76 + .../Entities/MapJoinNpcEntityEventHandler.cs | 40 + .../MapLeaveMonsterEntityEventHandler.cs | 23 + .../Entities/MapLeaveNpcEntityEventHandler.cs | 23 + .../Entities/ShopFactory.cs | 16 + .../GenerateExperienceEventHandler.cs | 514 ++++ ...sterNotifierStrangeBehaviorEventHandler.cs | 22 + .../Battle/ApplyProcessedHitEventHandler.cs | 439 ++++ .../Battle/BattleExecuteSkillEventHandler.cs | 74 + .../Event/Battle/EntityDamageEventHandler.cs | 753 ++++++ .../Event/Battle/ProcessBuffEventHandler.cs | 123 + .../Event/Battle/ProcessHitEventHandler.cs | 216 ++ ...ngelSpecialistElementalBuffEventHandler.cs | 77 + .../Event/Buffs/BuffAddEventHandler.cs | 261 ++ .../Buffs/BuffPartnerCheckEventHandler.cs | 94 + .../Event/Buffs/BuffRemoveEventHandler.cs | 314 +++ .../AddAdditionalHpMpEventHandler.cs | 59 + .../Event/Characters/AddExpEventHandler.cs | 125 + .../Characters/AddStaticBonusEventHandler.cs | 16 + .../Event/Characters/BankOpenEventHandler.cs | 88 + .../BattleEntityHealEventHandler.cs | 82 + .../Characters/ChangeClassEventHandler.cs | 269 ++ .../Characters/ChangeFactionEventHandler.cs | 44 + .../CharacterBonusExpiredEventHandler.cs | 55 + .../Characters/CharacterLoadEventHandler.cs | 958 ++++++++ .../CharacterPreLoadEventHandler.cs | 146 ++ .../CharacterRemoveManagersEventHandler.cs | 58 + .../CharacterSaveOnDisconnectEventHandler.cs | 113 + .../Characters/GenerateGoldEventHandler.cs | 62 + .../GenerateReputationEventHandler.cs | 69 + .../Characters/GetDefaultMorphEventHandler.cs | 44 + .../InviteJoinMinilandEventHandler.cs | 169 ++ .../Event/Characters/KillBonusEventHandler.cs | 805 ++++++ .../Event/Characters/LevelUpEventHandler.cs | 208 ++ .../Characters/MonsterCaptureEventHandler.cs | 196 ++ .../Characters/NormalChatEventHandler.cs | 55 + .../PartnerKillBonusEventHandler.cs | 77 + .../PlayerChangeChannelAct4EventHandler.cs | 90 + .../PlayerChangeChannelEventHandler.cs | 43 + .../Characters/PlayerDeathEventHandler.cs | 75 + .../Characters/PlayerRestEventHandler.cs | 73 + .../PlayerReturnFromAct4EventHandler.cs | 65 + .../RemoveAdditionalHpMpEventHandler.cs | 30 + .../Characters/RemoveItemTimeEventHandler.cs | 65 + .../Characters/RollItemBoxEventHandler.cs | 143 ++ .../Characters/SessionSaveEventHandler.cs | 130 + .../Event/Characters/SpPerfectEvent.cs | 11 + .../Event/Characters/SpPerfectEventHandler.cs | 223 ++ .../Characters/SpTransformEventHandler.cs | 168 ++ .../Characters/SpUntransformEventHandler.cs | 130 + .../Event/Characters/SpUpgradeEvent.cs | 19 + .../Event/Characters/SpUpgradeEventHandler.cs | 240 ++ .../SpecialistRefreshEventHandler.cs | 38 + .../Characters/UpgradeItemEventHandler.cs | 264 ++ .../VehicleCheckMapSpeedEventHandler.cs | 60 + .../Characters/VehicleRemoveEventHandler.cs | 56 + .../Exchange/ExchangeCloseEventHandler.cs | 40 + .../Exchange/ExchangeJoinEventHandler.cs | 54 + .../Exchange/ExchangeRegisterEventHandler.cs | 50 + .../ExchangeTransferItemsEventHandler.cs | 275 +++ .../Event/Groups/GroupActionEventHandler.cs | 387 +++ .../Groups/GroupAddMemberEventHandler.cs | 27 + .../Event/Groups/GroupJoinEventHandler.cs | 22 + .../Event/Groups/GroupLeaveEventHandler.cs | 120 + .../Groups/GroupRemoveMemberEventHandler.cs | 27 + .../Event/Groups/GroupWeedingEventHandler.cs | 75 + .../Event/Guri/GuriEventHandler.cs | 19 + .../Event/Items/CellonUpgradeEventHandler.cs | 126 + .../Event/Items/DropItemEventHandler.cs | 56 + .../Event/Items/DropRarityConfiguration.cs | 9 + .../Items/DropRarityConfigurationProvider.cs | 39 + .../Event/Items/GamblingEventHandler.cs | 300 +++ .../Items/GamblingRarityConfiguration.cs | 33 + .../Event/Items/GamblingRarityInfo.cs | 23 + .../Event/Items/ItemSumConfiguration.cs | 17 + .../Event/Items/ItemSumEventHandler.cs | 160 ++ .../PartnerSpecialistRollConfiguration.cs | 38 + .../PartnerSpecialistSkillEventHandler.cs | 154 ++ .../PlayerItemToPartnerItemEventHandler.cs | 153 ++ .../Event/Items/SpeedBoosterEventHandler.cs | 160 ++ .../Event/Maps/DisposeMapEventHandler.cs | 15 + .../Event/Maps/JoinMapEventHandler.cs | 541 ++++ .../Event/Maps/LeaveMapEventHandler.cs | 160 ++ .../Event/Maps/MapActivatedEventHandler.cs | 29 + .../Event/Maps/PortalTriggerEventHandler.cs | 125 + .../Event/Maps/RemovePortalEventHandler.cs | 12 + .../Event/Maps/SpawnPortalEventHandler.cs | 12 + .../Mates/MateBackToMinilandEventHandler.cs | 33 + .../Event/Mates/MateDeathEventHandler.cs | 159 ++ .../Event/Mates/MateHealEventHandler.cs | 36 + .../Event/Mates/MateInitializeEventHandler.cs | 43 + .../Mates/MateJoinInMinilandEventHandler.cs | 94 + .../Event/Mates/MateJoinTeamEventHandler.cs | 103 + .../Event/Mates/MateLeaveTeamEventHandler.cs | 103 + .../MateProcessExperienceEventHandler.cs | 117 + .../Event/Mates/MateRemoveEventHandler.cs | 31 + .../Event/Mates/MateRestEventHandler.cs | 41 + .../Event/Mates/MateReviveEventHandler.cs | 89 + .../Mates/MateSpTransformEventHandler.cs | 114 + .../Mates/MateSpUntransformEventHandler.cs | 81 + .../MateStayInsideMinilandEventHandler.cs | 80 + .../Event/Mates/MateSummonEventHandler.cs | 124 + .../AddObjMinilandEndLogicEventHandler.cs | 253 ++ .../Miniland/AddObjMinilandEventHandler.cs | 52 + .../MinigameDurabilityCouponEventHandler.cs | 79 + .../MinigameDurabilityInfoEventHandler.cs | 47 + .../MinigameGetYieldInfoEventHandler.cs | 47 + .../MinigameGetYieldRewardEventHandler.cs | 137 ++ .../Miniland/MinigamePlayEventHandler.cs | 76 + .../MinigameRepairDurabilityEventHandler.cs | 74 + .../Miniland/MinigameRewardEventHandler.cs | 156 ++ .../Miniland/MinigameScoreEventHandler.cs | 258 ++ .../Miniland/MinigameStopEventHandler.cs | 39 + .../Miniland/MinilandIntroEventHandler.cs | 40 + .../MinilandSignPostJoinEventHandler.cs | 73 + .../Miniland/MinilandStateEventHandler.cs | 68 + .../Miniland/RmvObjMinilandEventHandler.cs | 63 + .../Miniland/UseObjMinilandEventHandler.cs | 111 + .../Event/Monster/MonsterDeathEventHandler.cs | 230 ++ .../Monster/MonsterSummonEventHandler.cs | 209 ++ .../Npcs/MapNpcGenerateDeathEventHandler.cs | 75 + .../Event/Npcs/NpcDialogEventHandler.cs | 19 + .../Event/Npcs/NpcSummonEventHandler.cs | 127 + .../Relations/AddRelationEventHandler.cs | 32 + .../Event/Relations/InvitationEventHandler.cs | 88 + .../Relations/RelationBlockEventHandler.cs | 81 + .../Relations/RelationFriendEventHandler.cs | 121 + .../Relations/RemoveRelationEventHandler.cs | 28 + .../RespawnChangeEventHandler.cs | 52 + .../RespawnPlayerEventHandler.cs | 73 + .../RespawnReturn/ReturnChangeEventHandler.cs | 40 + .../Factories/IGameObjectFactory.cs | 6 + .../Factories/MapDesignObjectFactory.cs | 46 + .../Factories/MateTransportFactory.cs | 26 + .../ReloadableForbiddenNamesManager.cs | 44 + .../GameManagerPlugin.cs | 45 + .../GameManagersPluginCore.cs | 198 ++ .../GenericEventPluginCore.cs | 15 + .../Guri/DanceGuriHandler.cs | 50 + .../Guri/EmoticonGuriHandler.cs | 53 + .../Guri/FactionSwitchGuriHandler.cs | 78 + .../Guri/FairyBeadGuriHandler.cs | 86 + .../Guri/FifthSceneGuriHandler.cs | 13 + .../Guri/FirstSceneGuriHandler.cs | 14 + .../Guri/FourthSceneGuriHandler.cs | 13 + .../Guri/GenerateGuriGuriHandler.cs | 25 + .../Guri/HarvestGuriHandler.cs | 210 ++ .../Guri/IcebreakerEventGuriHandler.cs | 52 + .../Guri/InstantBattleGuriHandler.cs | 38 + .../Guri/InteractionGuriHandler.cs | 306 +++ .../Guri/MapsTeleportersGuriHandler.cs | 97 + .../Guri/MeteoreEventGuriHandler.cs | 29 + .../Guri/MountBeadGuriHandler.cs | 64 + .../Guri/PartnerBackPackGuriHandler.cs | 23 + .../Guri/PerfumeGuriHandler.cs | 100 + .../Guri/PetBasketGuriHandler.cs | 31 + .../Guri/PositionGuriHandler.cs | 54 + .../Guri/QuestTeleportDialogGuriHandler.cs | 73 + .../RainbowBattleCaptureFlagGuriHandler.cs | 41 + .../Guri/RainbowBattleRegisterGuriHandler.cs | 38 + .../Guri/RainbowBattleUnfreezeGuriHandler.cs | 62 + .../Guri/RelictExaminationEventHandler.cs | 120 + .../Guri/ResetSpGuriHandler.cs | 83 + .../Guri/RollGeneratedItemGuriHandler.cs | 39 + .../Guri/SecondSceneGuriHandler.cs | 13 + .../Guri/SheepEventGuriHandler.cs | 29 + .../Guri/ShellIdGuriHandler.cs | 96 + .../Guri/ThirdSceneGuriHandler.cs | 13 + .../Guri/TitleGuriHandler.cs | 58 + .../Guri/UseBoxGuriHandler.cs | 42 + .../Guri/WeddingGuriHandler.cs | 78 + .../Guri/WingsOfFriendshipGuriHandler.cs | 143 ++ .../GuriPlugin.cs | 44 + .../GuriPluginCore.cs | 14 + .../Helpers/GameHelpersPlugin.cs | 22 + .../BazaarNotificationMessageConsumer.cs | 41 + .../Consumer/ChatShoutAdminMessageConsumer.cs | 28 + ...nnelChatMessageBroadcastMessageConsumer.cs | 26 + ...annelSendChatMsgByCharIdMessageConsumer.cs | 28 + ...nelSendChatMsgByNicknameMessageConsumer.cs | 28 + ...rChannelSendInfoByCharIdMessageConsumer.cs | 28 + ...hannelSendInfoByNicknameMessageConsumer.cs | 28 + .../InterChannelSendWhisperMessageConsumer.cs | 28 + .../Event/ChatShoutAdminEventHandler.cs | 37 + ...ChannelChatMessageBroadcastEventHandler.cs | 37 + .../InterChannelReceiveWhisperEventHandler.cs | 46 + ...rChannelSendChatMsgByCharIdEventHandler.cs | 49 + ...hannelSendChatMsgByNicknameEventHandler.cs | 50 + ...nterChannelSendInfoByCharIdEventHandler.cs | 48 + ...erChannelSendInfoByNicknameEventHandler.cs | 48 + .../InterChannelSendWhisperEventHandler.cs | 95 + .../InterChannelModuleExtensions.cs | 36 + .../Inventory/InventoryAddItemEventHandler.cs | 103 + .../InventoryDropItemEventHandler.cs | 111 + .../InventoryEquipItemEventHandler.cs | 232 ++ .../InventoryMoveItemEventHandler.cs | 146 ++ .../InventoryPickUpItemEventHandler.cs | 554 +++++ .../InventoryRemoveItemEventHandler.cs | 154 ++ .../InventorySortItemEventHandler.cs | 60 + .../InventoryTakeOffItemEventHandler.cs | 124 + .../Inventory/InventoryUseItemEventHandler.cs | 19 + .../Inventory/ItemInstanceFactory.cs | 178 ++ .../PartnerInventoryEquipItemEventHandler.cs | 202 ++ ...PartnerInventoryTakeOffItemEventHandler.cs | 120 + .../ItemHandlerContainer.cs | 120 + .../ItemHandlerPlugin.cs | 63 + .../ItemHandlerPluginCore.cs | 58 + .../ItemServiceCollectionExtensions.cs | 17 + .../Box/GameGeneratedMateBeadHandler.cs | 112 + .../Equipment/Box/MateBeadHandler.cs | 148 ++ .../ItemUsage/Equipment/Box/RaidBoxHandler.cs | 55 + .../Equipment/Box/SpHolderHandler.cs | 101 + .../Equipment/Box/UserBeadHandler.cs | 260 ++ .../Etc/Magical/Act6PassiveItemHandler.cs | 90 + .../ItemUsage/Etc/Magical/BubbleHandler.cs | 46 + .../Etc/Magical/DignityPotionHandler.cs | 54 + .../ItemUsage/Etc/Magical/DyeBombHandler.cs | 56 + .../Etc/Magical/GeneralMagicalItemHandler.cs | 57 + .../ItemUsage/Etc/Magical/HairDyeHandler.cs | 90 + .../ItemUsage/Etc/Magical/HairStyleHandler.cs | 63 + .../Etc/Magical/MagicalBuffPotionHandler.cs | 51 + .../ItemUsage/Etc/Magical/ShellItemHandler.cs | 201 ++ .../ItemUsage/Etc/Magical/SpeakerHandler.cs | 40 + .../ItemUsage/Etc/Magical/TeamStoneHandler.cs | 69 + .../Etc/Magical/TeleportationItemHandler.cs | 335 +++ .../ItemUsage/Etc/ProduceItemHandler.cs | 66 + .../ItemUsage/Etc/RefinerItemHandler.cs | 107 + .../Etc/Special/AncelloanBlessingHandler.cs | 46 + .../Etc/Special/FairyBoostHandler.cs | 57 + .../Etc/Special/GuardianAngelHandler.cs | 46 + .../Etc/Special/IceFlowerOilHandler.cs | 50 + .../ItemUsage/Etc/Special/LuiniaHandler.cs | 58 + .../ItemUsage/Etc/Special/RaidSealHandler.cs | 23 + .../Etc/Special/ReinitializeItemHandler.cs | 176 ++ .../ReinitializePartnerSpAllSkillsHandler.cs | 145 ++ .../Teacher/IncreaseLevelPartnerHandler.cs | 72 + .../Teacher/IncreaseLevelPetFoodHandler.cs | 82 + .../ItemUsage/Etc/Teacher/MateFoodHandler.cs | 61 + .../ItemUsage/Etc/Teacher/MateGuriHandler.cs | 30 + .../Etc/Teacher/MateReleaseHandler.cs | 51 + .../Etc/Teacher/NosMateTrainerHandler.cs | 104 + .../Etc/Teacher/PartnerFoodHandler.cs | 65 + .../Etc/Teacher/PartnerReleaseHandler.cs | 58 + .../Etc/Teacher/PetSummoningScrollHandler.cs | 54 + .../Etc/Teacher/PickUpPetFoodHandler.cs | 59 + .../ItemUsage/Etc/Teacher/SteelNetHandler.cs | 92 + .../Etc/Teacher/StrangePartnerFoodHandler.cs | 94 + .../Etc/Teacher/StrangePetFoodHandler.cs | 81 + .../ItemUsage/Etc/UpgradeItemsHandler.cs | 137 ++ .../ItemUsage/IItemUsageToggleManager.cs | 54 + .../ItemUsage/Main/FireworkHandler.cs | 25 + .../ItemUsage/Main/FoodHandler.cs | 114 + .../ItemUsage/Main/PotionHandler.cs | 267 ++ .../ItemUsage/Main/SnackHandler.cs | 124 + .../ItemUsage/Main/Special/BackpackHandler.cs | 51 + .../Main/Special/ChangePartnerSkinHandler.cs | 122 + .../Main/Special/CostumeScrollHandler.cs | 101 + .../Main/Special/CupidArrowHandler.cs | 78 + .../Main/Special/FactionEggHandler.cs | 82 + .../Main/Special/GeneralItemsHandler.cs | 243 ++ .../Main/Special/InventoryExpansionHandler.cs | 67 + .../Main/Special/ItemSpawnHandler.cs | 33 + .../Main/Special/MagicLampHandler.cs | 56 + .../Main/Special/MateSlotExpansionHandler.cs | 73 + .../Main/Special/MinilandSignHandler.cs | 71 + .../Main/Special/NosBazaarGoldMedalHandler.cs | 47 + .../Special/NosBazaarSilverMedalHandler.cs | 47 + .../Main/Special/PartnerBackpackHandler.cs | 53 + .../Main/Special/PetBasketHandler.cs | 62 + .../Special/PresentationMessageHandler.cs | 30 + .../Main/Special/ReputationMedalHandler.cs | 30 + .../Main/Special/SealedVesselHandler.cs | 121 + .../Main/Special/SeparationLetterHandler.cs | 57 + .../Main/Special/SpPointPotionHandler.cs | 47 + .../ItemUsage/Main/Special/SpWingHandler.cs | 107 + .../Main/Special/SpecialPotionHandler.cs | 121 + .../Main/Special/SpecialistSigilHandler.cs | 71 + .../Main/Special/SpeedBoosterHandler.cs | 44 + .../Main/Special/StatPotionHandler.cs | 32 + .../Main/Special/SuctionFunnelHandler.cs | 68 + .../Main/Special/TimeSpaceStoneItemHandler.cs | 41 + .../ItemUsage/Main/Special/VehicleHandler.cs | 170 ++ .../ItemUsage/Main/TitleHandler.cs | 22 + .../Mail/MailCreateEventHandler.cs | 44 + .../Mail/MailCreationManager.cs | 106 + .../Mail/MailOpenEventHandler.cs | 69 + .../Mail/MailRemoveEventHandler.cs | 51 + .../Mail/NoteCreateEventHandler.cs | 107 + .../Mail/NoteOpenEventHandler.cs | 48 + .../Mail/NoteRemoveEventHandler.cs | 44 + .../Managers/DelayManager.cs | 81 + .../Managers/GroupManager.cs | 199 ++ .../Managers/IMapAttributeFactory.cs | 9 + .../Managers/MapAttributeFactory.cs | 12 + .../Managers/RankingManager.cs | 76 + .../Managers/RevivalManager.cs | 27 + .../Managers/ScriptedInstanceManager.cs | 7 + .../Managers/ServerManager.cs | 250 ++ .../Managers/SessionManager.cs | 451 ++++ .../Managers/StaticData/CardsManager.cs | 52 + .../Managers/StaticData/ItemsManager.cs | 66 + .../Managers/StaticData/NpcMonsterManager.cs | 89 + .../Managers/StaticData/SkillsManager.cs | 45 + .../Miniland/DependencyInjectionExtensions.cs | 72 + .../Miniland/MinigameManager.cs | 67 + .../MinigameRefreshEventProcessor.cs | 43 + .../Miniland/MinilandManager.cs | 183 ++ .../NpcDialogHandlerContainer.cs | 56 + .../NpcDialogPlugin.cs | 43 + .../NpcDialogPluginCore.cs | 16 + .../Act4_Act5/Act4EnterShipHandler.cs | 56 + .../NpcDialogs/Act4_Act5/Act4LeaveHandler.cs | 41 + .../Act4_Act5/Act4LeaveShipHandler.cs | 41 + .../NpcDialogs/Act4_Act5/Act5LeaveHandler.cs | 41 + .../Act4_Act5/Act5LeaveShipHandler.cs | 41 + .../Act4_Act5/Act5ShipEnterHandler.cs | 48 + .../NpcDialogs/Act6/Act6FirstMission.cs | 16 + .../NpcDialogs/Act6/TeleportCylloanHandler.cs | 45 + .../Arena/ArenaMastersRegisterHandler.cs | 16 + .../NpcDialogs/Arena/ArenaSpectatorHandler.cs | 28 + .../Arena/ArenaTalentsRegisterHandler.cs | 27 + .../NpcDialogs/Arena/JoinArenaHandler.cs | 64 + .../NpcDialogs/BankSavingBookHandler.cs | 53 + .../NpcDialogs/ChangeClassHandler.cs | 73 + .../NpcDialogs/ChangeSpawnHandler.cs | 45 + .../NpcDialogs/GenerateNpcDialogHandler.cs | 14 + .../NpcDialogs/JewelryWindowHandler.cs | 25 + .../NpcDialogs/JoinLodHandler.cs | 16 + .../NpcDialogs/MateHandler.cs | 136 + .../NpcDialogs/OpenBankHandler.cs | 21 + .../NpcDialogs/OpenNosBazaarHandler.cs | 31 + .../NpcDialogs/OpenWindowHandler.cs | 14 + .../Quests/QuestAdditionalAct5Handler.cs | 65 + .../Quests/QuestAdditionalBlueHandler.cs | 75 + .../Quests/QuestAdditionalHandler.cs | 72 + .../NpcDialogs/Quests/QuestDailyHandler.cs | 79 + .../NpcDialogs/Quests/QuestReceiveHandler.cs | 25 + .../Quests/QuestReceiveMainHandler.cs | 82 + .../NpcDialogs/RecipeListHandler.cs | 39 + .../SP5_SP6/Act5ItemCraftingHandler.cs | 29 + .../NpcDialogs/SP5_SP6/WatterGrotoHandler.cs | 33 + .../SP8/Sp8PowderProductionHandler.cs | 53 + .../SP8/Sp8SealProductionHandler.cs | 54 + .../NpcDialogs/ShowPlayerShopHandler.cs | 28 + .../Teleport/TeleportGrenigasSquareHandler.cs | 29 + .../NpcDialogs/Teleport/TeleportHandler.cs | 105 + .../Teleport/TeleportSpRockHandler.cs | 54 + .../Teleport/WarpTeleportAct5Handler.cs | 110 + .../Teleport/WeedingGazeboHandler.cs | 63 + .../NpcDialogs/TimeSpace/GetPartnerHandler.cs | 279 +++ .../TimeSpace/TimeSpaceDialogHandler.cs | 117 + .../TimeSpace/TimeSpaceEnterHandler.cs | 59 + .../TimeSpaceOnFinishDialogHandler.cs | 62 + .../NpcDialogs/TimeSpaceTimerHandler.cs | 116 + .../NpcDialogs/UpgradeItemNpcHandler.cs | 25 + .../PlayerEntityFactory.cs | 216 ++ .../Quicklist/QuicklistAddEventHandler.cs | 52 + .../Quicklist/QuicklistRemoveEventHandler.cs | 30 + .../Quicklist/QuicklistSwapEventHandler.cs | 48 + .../RandomFactory.cs | 25 + .../RecipeFactory.cs | 13 + .../Recipes/RecipeOpenWindowEventHandler.cs | 63 + .../Revival/RevivalAskEventArenaHandler.cs | 36 + .../Revival/RevivalAskEventHandler.cs | 38 + .../Revival/RevivalEventArenaHandler.cs | 79 + .../Revival/RevivalEventBaseHandler.cs | 165 ++ .../Revival/RevivalEventIceBreakerHandler.cs | 37 + .../Revival/RevivalEventNormalHandler.cs | 69 + .../RevivalStartProcedureEventArenaHandler.cs | 119 + .../RevivalStartProcedureEventBaseHandler.cs | 92 + ...RevivalStartProcedureEventNormalHandler.cs | 44 + .../ServerConfigs/DropManager.cs | 98 + .../ImportObjects/Drops/DropImportFile.cs | 15 + .../ImportObjects/Drops/DropObject.cs | 25 + .../ImportObjects/Files/IExportableFile.cs | 7 + .../ImportObjects/Files/IFileData.cs | 5 + .../ImportObjects/ImportObjectExtensions.cs | 265 ++ .../ItemBoxes/ItemBoxImportFile.cs | 23 + .../ItemBoxes/RandomBoxCategory.cs | 13 + .../ItemBoxes/RandomBoxImportFile.cs | 11 + .../ImportObjects/ItemBoxes/RandomBoxItem.cs | 30 + .../ItemBoxes/RandomBoxObject.cs | 26 + .../Maps/ConfiguredMapImportFile.cs | 12 + .../ImportObjects/Maps/ConfiguredMapObject.cs | 26 + .../Monsters/MapMonsterImportFile.cs | 14 + .../Monsters/MapMonsterObject.cs | 30 + .../ImportObjects/Npcs/MapNpcImportFile.cs | 18 + .../ImportObjects/Npcs/MapNpcObject.cs | 60 + .../Npcs/MapNpcShopItemObject.cs | 21 + .../ImportObjects/Npcs/MapNpcShopObject.cs | 19 + .../Npcs/MapNpcShopSkillObject.cs | 9 + .../ImportObjects/Npcs/MapNpcShopTabObject.cs | 13 + .../ImportObjects/Portals/PortalImportFile.cs | 11 + .../ImportObjects/Portals/PortalObject.cs | 40 + .../ImportObjects/Recipes/RecipeImportFile.cs | 11 + .../ImportObjects/Recipes/RecipeItemObject.cs | 12 + .../ImportObjects/Recipes/RecipeObject.cs | 29 + .../Teleporters/TeleporterImportFile.cs | 14 + .../Teleporters/TeleporterObject.cs | 25 + .../ServerConfigs/ItemBoxManager.cs | 59 + .../ServerConfigs/MapManager.cs | 319 +++ .../ServerConfigs/MapMonsterManager.cs | 51 + .../ServerConfigs/MapNpcManager.cs | 53 + .../ServerConfigs/RecipeManager.cs | 143 ++ .../ServerConfigs/ShopManager.cs | 106 + .../ServerConfigs/TeleporterManager.cs | 54 + .../ServiceCollectionExtensions.cs | 20 + .../Ship/Event/ShipEnterEventHandler.cs | 82 + .../Ship/Event/ShipLeaveEventHandler.cs | 12 + .../Ship/Event/ShipProcessEventAct4Handler.cs | 101 + .../Ship/Event/ShipProcessEventAct5Handler.cs | 61 + .../Ship/Event/ShipProcessEventHandler.cs | 42 + .../Ship/RecurrentJob/ShipSystem.cs | 62 + .../Ship/ShipConfigurationProvider.cs | 25 + .../Ship/ShipManager.cs | 23 + .../Ship/ShipModuleExtensions.cs | 33 + .../Shop/BuyItemNpcShopEventHandler.cs | 69 + .../Shop/BuyShopItemEventHandler.cs | 176 ++ .../Shop/BuyShopSkillEventHandler.cs | 227 ++ .../Shop/ShopNpcListItemsEventHandler.cs | 217 ++ .../Shop/ShopPlayerBuyItemEventHandler.cs | 153 ++ .../Shop/ShopPlayerCloseEventHandler.cs | 35 + .../Shop/ShopPlayerOpenEventHandler.cs | 75 + .../Vehicles/IVehicleConfigurationProvider.cs | 9 + .../Vehicles/VehicleConfiguration.cs | 43 + .../Vehicles/VehicleConfigurationProvider.cs | 28 + .../Vehicles/VehicleMapSpeedConfiguration.cs | 16 + .../AccountWarehouseAddEventHandler.cs | 88 + .../Warehouse/AccountWarehouseManager.cs | 170 ++ .../AccountWarehouseMoveEventHandler.cs | 67 + .../AccountWarehouseOpenEventHandler.cs | 82 + .../AccountWarehouseShowItemEventHandler.cs | 83 + .../AccountWarehouseWithdrawEventHandler.cs | 105 + .../Warehouse/IWarehouseFactory.cs | 9 + .../PartnerWarehouseAddItemEventHandler.cs | 177 ++ .../PartnerWarehouseDepositEventHandler.cs | 150 ++ .../PartnerWarehouseMoveEventHandler.cs | 177 ++ .../PartnerWarehouseWithdrawEventHandler.cs | 118 + .../Warehouse/Warehouse.cs | 42 + .../Warehouse/WarehouseExtensions.cs | 18 + .../Warehouse/WarehouseFactory.cs | 9 + ...ngsEmu.Plugins.BasicImplementations.csproj | 26 + .../BotMessages/BotMessageEventHandler.cs | 25 + .../BotMessages/BotMessageMessage.cs | 15 + .../DependencyInjectionExtensions.cs | 25 + .../ScheduledBotMessageConfiguration.cs | 9 + .../InterChannel/BazaarNotificationMessage.cs | 15 + .../InterChannel/ChatShoutAdminMessage.cs | 11 + ...InterChannelChatMessageBroadcastMessage.cs | 17 + .../InterChannelSendChatMsgByCharIdMessage.cs | 17 + ...nterChannelSendChatMsgByNicknameMessage.cs | 17 + .../InterChannelSendInfoByCharIdMessage.cs | 14 + .../InterChannelSendInfoByNicknameMessage.cs | 14 + .../InterChannelSendWhisperMessage.cs | 20 + .../MailReceivePendingOnConnectedMessage.cs | 22 + ...eceivePendingOnConnectedMessageConsumer.cs | 54 + .../Mails/MailReceivedMessage.cs | 15 + .../Mails/MailReceivedMessageConsumer.cs | 49 + .../NoteReceivePendingOnConnectedMessage.cs | 18 + ...eceivePendingOnConnectedMessageConsumer.cs | 57 + .../Mails/NoteReceivedMessage.cs | 14 + .../Mails/NoteReceivedMessageConsumer.cs | 64 + .../PlayerEvents/KickAccountMessage.cs | 11 + .../PlayerConnectedOnChannelMessage.cs | 24 + .../PlayerDisconnectedChannelMessage.cs | 16 + .../Relation/RelationCharacterAddMessage.cs | 13 + .../RelationCharacterAddMessageConsumer.cs | 59 + .../Relation/RelationCharacterJoinMessage.cs | 17 + .../RelationCharacterJoinMessageConsumer.cs | 57 + .../Relation/RelationCharacterLeaveMessage.cs | 12 + .../RelationCharacterLeaveMessageConsumer.cs | 82 + .../RelationCharacterRemoveMessage.cs | 14 + .../RelationCharacterRemoveMessageConsumer.cs | 52 + .../Relation/RelationSendTalkMessage.cs | 13 + .../RelationSendTalkMessageConsumer.cs | 26 + .../SchedulableConfiguration.cs | 7 + .../ScheduledEventPublisherCorePlugin.cs | 17 + .../ScheduledEventSubscriberCorePlugin.cs | 17 + ...gsEmu.Plugins.DistributedGameEvents.csproj | 18 + .../Account/AccountModule.cs | 136 + .../AdministratorBazaarModule.cs | 57 + .../AdministratorCheatModule_Rune.cs | 31 + .../AdministratorCheatModule_Shell.cs | 180 ++ .../AdministratorCooldownModule.cs | 31 + .../AdministratorLanguageModule.cs | 69 + .../Administrator/AdministratorMailModule.cs | 137 ++ .../AdministratorMaintenanceModule.cs | 311 +++ .../Administrator/AdministratorModule.cs | 1117 +++++++++ .../Administrator/Items/ItemManagement.cs | 66 + .../Administrator/Items/RefundModule.cs | 365 +++ .../EssentialsPlugin.cs | 80 + .../GameMaster/CharacterModule.cs | 448 ++++ .../GameMaster/ItemModule.cs | 128 + .../GameMaster/MonsterSummoningModule.cs | 145 ++ .../GameMaster/PunishmentModule.cs | 535 ++++ .../GameMaster/SearchDataModule.cs | 151 ++ .../GameMaster/SkillsModule.cs | 94 + .../GameMaster/SpecialistModule.cs | 52 + .../God/GodSetRankModule.cs | 103 + .../Help/HelpModule.cs | 136 + .../NPC/BuffPack.cs | 22 + .../NPC/BuffPackConfiguration.cs | 7 + .../NPC/BuffPackElement.cs | 19 + .../NPC/MateCreationModule.cs | 96 + .../NPC/NPCModule.cs | 123 + .../Skills/AdministratorCheatModule_Skills.cs | 144 ++ .../SuperGameMaster/BetaGameTester.cs | 417 ++++ .../SuperGameMaster/MinilandModule.cs | 56 + .../SuperGameMasterMonsterModule.cs | 104 + .../Teleport/TeleportModule.cs | 113 + .../WingsEmu.Plugins.Essentials.csproj | 19 + .../CommandModules/GameEventsBasicModule.cs | 82 + .../GlobalInstantBattleConfiguration.cs | 51 + .../IGlobalInstantBattleConfiguration.cs | 10 + .../InstantBattleConfiguration.cs | 41 + .../InstantBattle/InstantBattleRequirement.cs | 11 + .../InstantBattle/InstantBattleReward.cs | 11 + .../InstantBattle/InstantBattleWave.cs | 27 + ...omplimentsMonthlyRefreshMessageConsumer.cs | 31 + .../InstantBattleStartMessageConsumer.cs | 29 + ...eRefreshProductionPointsMessageConsumer.cs | 28 + .../QuestDailyRefreshMessageConsumer.cs | 31 + .../RaidRestrictionRefreshMessageConsumer.cs | 28 + .../RankingRefreshMessageConsumer.cs | 40 + .../SpecialistPointsRefreshMessageConsumer.cs | 31 + .../TranslationsRefreshMessageConsumer.cs | 27 + .../DataHolder/InstantBattleInstance.cs | 49 + .../Event/Global/GameEvent.cs | 12 + .../Global/GameEventLockRegistrationEvent.cs | 11 + .../Event/Global/GameEventMatchmakeEvent.cs | 13 + .../Event/Global/GameEventPrepareEvent.cs | 13 + .../GameEventUnlockRegistrationEvent.cs | 11 + .../InstantBattleDestroyEvent.cs | 11 + .../InstantBattle/InstantBattleDropEvent.cs | 12 + .../Event/InstantBattle/InstantBattleEvent.cs | 12 + .../InstantBattle/InstantBattleRewardEvent.cs | 11 + .../InstantBattleStartWaveEvent.cs | 11 + .../GameEventLockRegistrationEventHandler.cs | 76 + .../Global/GameEventMatchmakeEventHandler.cs | 86 + .../Global/GameEventPrepareEventHandler.cs | 121 + ...GameEventUnlockRegistrationEventHandler.cs | 55 + ...nstanceProcessEventInstantBattleHandler.cs | 145 ++ ...tInstanceStartEventInstantBattleHandler.cs | 50 + .../InstantBattleCompleteEventHandler.cs | 106 + .../InstantBattleDestroyEventHandler.cs | 35 + .../InstantBattleDropEventHandler.cs | 46 + .../InstantBattleStartWaveEventHandler.cs | 58 + .../GameEventInstanceManager.cs | 35 + .../GameEventRegistrationManager.cs | 91 + .../GameEventsPlugin.cs | 20 + .../GameEventsPluginCore.cs | 64 + .../Matchmaking/Filter/InBaseMapFilter.cs | 11 + .../Matchmaker/InstantBattleMatchmaker.cs | 80 + .../Matchmaking/Matchmaking.cs | 27 + .../Result/IceBreakerMatchmakingResult.cs | 22 + .../Result/InstantBattleMatchmakingResult.cs | 22 + .../RecurrentJob/GameEventSystem.cs | 60 + .../WingsEmu.Plugins.GameEvents.csproj | 20 + .../CharScreen/CreateBrawlerPacketHandler.cs | 17 + .../CreateCharacterPacketHandler.cs | 239 ++ .../CrossServerEntryPointPacketHandler.cs | 163 ++ .../DeleteCharacterPacketHandler.cs | 175 ++ .../CharScreen/EntryPointPacketHandler.cs | 272 ++ .../CharScreen/SelectPacketHandler.cs | 30 + .../CharScreenPacketHandlerCorePlugin.cs | 26 + .../CharScreenPacketHandlerGamePlugin.cs | 45 + .../Customization/BaseCharacter.cs | 41 + .../Customization/BaseInventory.cs | 60 + .../Customization/BaseQuicklist.cs | 43 + .../Customization/BaseSkill.cs | 20 + .../Customization/CustomizationCorePlugin.cs | 43 + .../Game/Banks/BankManagementPacketHandler.cs | 144 ++ .../Game/Basic/ArenaPacketHandler.cs | 81 + .../Game/Basic/BtkPacketHandler.cs | 18 + .../Basic/CharacterOptionPacketHandler.cs | 189 ++ .../Game/Basic/ComplimentPacketHandler.cs | 72 + .../Game/Basic/CspPacketHandler.cs | 32 + .../Game/Basic/CspePacketHandler.cs | 15 + .../Game/Basic/DirectionPacketHandler.cs | 64 + .../Game/Basic/FlPacketHandler.cs | 35 + .../Game/Basic/GameStartPacketHandler.cs | 14 + .../Game/Basic/GetGiftPacketHandler.cs | 28 + .../Game/Basic/GuriPacketHandler.cs | 27 + .../Game/Basic/HeroPacketHandler.cs | 53 + .../Game/Basic/ISortPacketHandler.cs | 25 + .../Game/Basic/NcifPacketHandler.cs | 40 + .../Game/Basic/NpinfoPacketHandler.cs | 16 + .../Game/Basic/PreqPacketHandler.cs | 64 + .../Game/Basic/PstPacketHandler.cs | 49 + .../Game/Basic/PulsePacketHandler.cs | 50 + .../Game/Basic/QSetPacketHandler.cs | 72 + .../Game/Basic/QtPacketHandler.cs | 87 + .../Game/Basic/RStartPacketHandler.cs | 19 + .../Game/Basic/RankSkPacketHandler.cs | 28 + .../Game/Basic/ReqInfoPacketHandler.cs | 137 ++ .../Game/Basic/RevivalPacketHandler.cs | 26 + .../Game/Basic/SayPacketHandler.cs | 79 + .../Game/Basic/SitPacketHandler.cs | 53 + .../Game/Basic/TitleEquipPacketHandler.cs | 72 + .../Game/Basic/WalkPacketHandler.cs | 189 ++ .../Game/Basic/WhisperPacketHandler.cs | 31 + .../Battle/MultitargetListPacketHandler.cs | 51 + .../Game/Battle/ObaPacketHandler.cs | 100 + .../Game/Battle/UseAtSkillPacketHandler.cs | 232 ++ .../Game/Battle/UseSkillPacketHandler.cs | 571 +++++ .../Game/Bazaar/CScaclcPacketHandler.cs | 14 + .../Game/Bazaar/CSkillPacketHandler.cs | 41 + .../Game/Bazaar/CbListPacketHandler.cs | 96 + .../Game/Bazaar/CbuyPacketHandler.cs | 36 + .../Game/Bazaar/CmodPacketHandler.cs | 14 + .../Game/Bazaar/CregPacketHandler.cs | 115 + .../Game/Bazaar/CsListPacketHandler.cs | 34 + .../Game/Chat/FamilyChatPacketHandler.cs | 79 + .../Families/CreateFamilyPacketHandler.cs | 31 + .../Game/Families/FDepositPacketHandler.cs | 32 + .../Game/Families/FReposPacketHandler.cs | 24 + .../Game/Families/FWithdrawPacketHandler.cs | 23 + .../Families/FamilyDisbandPacketHandler.cs | 18 + .../Game/Families/FamilyFAuthPacketHandler.cs | 30 + .../Game/Families/FhistCtsPacketHandler.cs | 26 + .../Game/Families/FmgPacketHandler.cs | 18 + .../Game/Families/FrankCtsPacketHandler.cs | 12 + .../Game/Families/FsLogCtsPacketHandler.cs | 17 + .../Game/Families/GLeavePacketHandler.cs | 14 + .../Game/Families/GListPacketHandler.cs | 49 + .../Game/Families/JoinFamilyPacketHandler.cs | 25 + .../Game/Families/TodayPacketHandler.cs | 38 + .../Game/Group/GroupSayPacketHandler.cs | 55 + .../Game/Group/PJoinPacketHandler.cs | 24 + .../Game/Group/PLeavePacketHandler.cs | 14 + .../Game/Group/RdPacketHandler.cs | 63 + .../Game/Group/RlPacketHandler.cs | 47 + .../Game/Inventory/BiPacketHandler.cs | 51 + .../Inventory/EquipmentInfoPacketHandler.cs | 241 ++ .../Game/Inventory/ExcListPacketHandler.cs | 192 ++ .../Inventory/ExchangeRequestPacketHandler.cs | 306 +++ .../Game/Inventory/GetPacketHandler.cs | 21 + .../Game/Inventory/MvePacketHandler.cs | 58 + .../Game/Inventory/MviPacketHandler.cs | 48 + .../Game/Inventory/PutPacketHandler.cs | 14 + .../Game/Inventory/RemovePacketHandler.cs | 25 + .../Game/Inventory/ReposPacketHandler.cs | 20 + .../Game/Inventory/SortOpenPacketHandler.cs | 46 + .../Inventory/SpTransformPacketHandler.cs | 164 ++ .../SpecialistHolderPacketHandler.cs | 95 + .../Game/Inventory/UpgradePacketHandler.cs | 431 ++++ .../Game/Inventory/UseItemPacketHandler.cs | 247 ++ .../Inventory/WarehouseAddPacketHandler.cs | 38 + .../Game/Inventory/WearPacketHandler.cs | 25 + .../Inventory/WearPartnerCardPacketHandler.cs | 23 + .../Game/Inventory/WithdrawPacketHandler.cs | 20 + .../Game/Mate/PsOpPacketHandler.cs | 19 + .../Game/Mate/PslPacketHandler.cs | 102 + .../Game/Mate/PtctlPacketHandler.cs | 170 ++ .../Game/Mate/SayPPacketHandler.cs | 55 + .../Game/Mate/SuctlPacketHandler.cs | 383 +++ .../Game/Mate/UpetPacketHandler.cs | 189 ++ .../Game/Mate/UpsPacketHandler.cs | 214 ++ .../Game/Miniland/AddobjPacketHandler.cs | 12 + .../Game/Miniland/MiniGamePacketHandler.cs | 99 + .../Miniland/MinilandEditPacketHandler.cs | 40 + .../Game/Miniland/MjoinPacketHandler.cs | 71 + .../Game/Miniland/RmvobjPacketHandler.cs | 12 + .../Game/Miniland/UseobjPacketHandler.cs | 12 + .../Game/Npc/BuyPacketHandler.cs | 48 + .../Game/Npc/MShopPacketHandler.cs | 150 ++ .../Game/Npc/NrunPacketHandler.cs | 31 + .../Game/Npc/PdtsePacketHandler.cs | 325 +++ .../Game/Npc/RequestNpcPacketHandler.cs | 211 ++ .../Game/Npc/SellPacketHandler.cs | 198 ++ .../Game/Npc/ShoppingPacketHandler.cs | 33 + .../Game/Relations/BlDelpacketHandler.cs | 27 + .../Game/Relations/BlinsPacketHandler.cs | 17 + .../Game/Relations/FDelPacketHandler.cs | 32 + .../Game/Relations/FInsPacketHandler.cs | 24 + .../Game/ScriptPacketHandler.cs | 143 ++ .../Game/ScriptedInstance/BscPacketHandler.cs | 49 + .../ScriptedInstance/EscapePacketHandler.cs | 34 + .../Game/ScriptedInstance/FbPacketHandler.cs | 22 + .../Game/ScriptedInstance/GitPacketHandler.cs | 50 + .../ScriptedInstance/MkRaidPacketHandler.cs | 59 + .../ScriptedInstance/RselPacketHandler.cs | 17 + .../ScriptedInstance/RxitPacketHandler.cs | 51 + .../ScriptedInstance/TaCallPacketHandler.cs | 12 + .../Game/ScriptedInstance/TawPacketHandler.cs | 12 + .../ScriptedInstance/TreqPacketHandler.cs | 207 ++ .../ScriptedInstance/WreqPacketHandler.cs | 211 ++ .../Game/Useless/CClosePacketHandler.cs | 18 + .../Game/Useless/FStashEndPacketHandler.cs | 14 + .../Game/Useless/LbsPacketHandler.cs | 12 + .../Game/Useless/PdtClosePacketHandler.cs | 14 + .../Game/Useless/ScpCtsPacketHandler.cs | 12 + .../Game/Useless/ShopClosePacketHandler.cs | 12 + .../Game/Useless/SnapPacketHandler.cs | 13 + .../Game/Useless/StashEndPacketHandler.cs | 14 + .../GamePacketHandlersCorePlugin.cs | 30 + .../GamePacketHandlersGamePlugin.cs | 50 + .../GenericCharScreenPacketHandlerBase.cs | 28 + .../GenericGamePacketHandlerBase.cs | 24 + .../GenericPacketHandlerContainer.cs | 47 + .../WingsEmu.Plugins.PacketHandling.csproj | 18 + 3516 files changed, 194596 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 README.md create mode 100644 WingsEmu.sln create mode 100644 nuget.config create mode 100644 properties_for_visual_studio.zip create mode 100644 scripts/Database/add-migration.ps1 create mode 100644 scripts/Database/default-accounts.ps1 create mode 100644 scripts/Database/remove-last-migration.ps1 create mode 100644 scripts/Database/update-database.ps1 create mode 100644 scripts/Docker/MQTT.ps1 create mode 100644 scripts/Docker/MonboDB.ps1 create mode 100644 scripts/Docker/PostgreSQL.ps1 create mode 100644 scripts/Docker/Redis.ps1 create mode 100644 scripts/translations-update.ps1 create mode 100644 scripts/update-server-files.ps1 create mode 100644 srcs/BazaarServer/BazaarServer.csproj create mode 100644 srcs/BazaarServer/Consumers/ServiceMaintenanceNotificationMessageConsumer.cs create mode 100644 srcs/BazaarServer/DockerGracefulStopService.cs create mode 100644 srcs/BazaarServer/Managers/BazaarManager.cs create mode 100644 srcs/BazaarServer/Managers/BazaarSearchManager.cs create mode 100644 srcs/BazaarServer/Program.cs create mode 100644 srcs/BazaarServer/RecurrentJobs/BazaarSystem.cs create mode 100644 srcs/BazaarServer/Services/BazaarService.cs create mode 100644 srcs/BazaarServer/Startup.cs create mode 100644 srcs/DatabaseServer/Consumers/ServiceFlushAllMessageConsumer.cs create mode 100644 srcs/DatabaseServer/DatabaseServer.csproj create mode 100644 srcs/DatabaseServer/DockerGracefulStopService.cs create mode 100644 srcs/DatabaseServer/EnvironmentConsts.cs create mode 100644 srcs/DatabaseServer/Managers/AccountWarehouseManager.cs create mode 100644 srcs/DatabaseServer/Managers/AddWarehouseItemResult.cs create mode 100644 srcs/DatabaseServer/Managers/CharacterManager.cs create mode 100644 srcs/DatabaseServer/Managers/IAccountWarehouseManager.cs create mode 100644 srcs/DatabaseServer/Managers/ICharacterManager.cs create mode 100644 srcs/DatabaseServer/Managers/IRankingManager.cs create mode 100644 srcs/DatabaseServer/Managers/ITimeSpaceManager.cs create mode 100644 srcs/DatabaseServer/Managers/MoveWarehouseItemResult.cs create mode 100644 srcs/DatabaseServer/Managers/RankingManager.cs create mode 100644 srcs/DatabaseServer/Managers/SaveRequest.cs create mode 100644 srcs/DatabaseServer/Managers/TimeSpaceManager.cs create mode 100644 srcs/DatabaseServer/Managers/WithdrawWarehouseItemResult.cs create mode 100644 srcs/DatabaseServer/Program.cs create mode 100644 srcs/DatabaseServer/Services/AccountService.cs create mode 100644 srcs/DatabaseServer/Services/AccountWarehouseService.cs create mode 100644 srcs/DatabaseServer/Services/CharacterService.cs create mode 100644 srcs/DatabaseServer/Services/TimeSpaceService.cs create mode 100644 srcs/DatabaseServer/Startup.cs create mode 100644 srcs/DiscordNotifier/Consumers/Chat/LogChatMessageMessageFormatter.cs create mode 100644 srcs/DiscordNotifier/Consumers/Family/LogFamilyCreatedEmbedMessageFormatter.cs create mode 100644 srcs/DiscordNotifier/Consumers/Family/LogFamilyCreatedMessageFormatter.cs create mode 100644 srcs/DiscordNotifier/Consumers/Family/LogFamilyDisbandedMessageFormatter.cs create mode 100644 srcs/DiscordNotifier/Consumers/Family/LogFamilyJoinedMessageFormatter.cs create mode 100644 srcs/DiscordNotifier/Consumers/Family/LogFamilyKickedMessageFormatter.cs create mode 100644 srcs/DiscordNotifier/Consumers/Family/LogFamilyLeftMessageFormatter.cs create mode 100644 srcs/DiscordNotifier/Consumers/Family/LogFamilyMessageMessageFormatter.cs create mode 100644 srcs/DiscordNotifier/Consumers/GameEvents/LogInstantBattleStartDiscordConsumer.cs create mode 100644 srcs/DiscordNotifier/Consumers/Item/LogItemGambledMessageFormatter.cs create mode 100644 srcs/DiscordNotifier/Consumers/Item/LogItemUpgradedMessageFormatter.cs create mode 100644 srcs/DiscordNotifier/Consumers/Maintenance/ServiceDownMessageConsumer.cs create mode 100644 srcs/DiscordNotifier/Consumers/Maintenance/ServiceMaintenanceNotificationMessageConsumer.cs create mode 100644 srcs/DiscordNotifier/Consumers/Minigame/LogMinigameRewardClaimedMessageFormatter.cs create mode 100644 srcs/DiscordNotifier/Consumers/Minigame/LogMinigameScoreMessageFormatter.cs create mode 100644 srcs/DiscordNotifier/Consumers/Player/LogGMCommandExecutedMessageFormatter.cs create mode 100644 srcs/DiscordNotifier/Consumers/Player/LogPlayerCommandExecutedMessageFormatter.cs create mode 100644 srcs/DiscordNotifier/Consumers/Player/LogPlayerLevelUpMessageFormatter.cs create mode 100644 srcs/DiscordNotifier/Consumers/Player/LogStrangeBehaviorMessageFormatter.cs create mode 100644 srcs/DiscordNotifier/Discord/DiscordWebhookConfiguration.cs create mode 100644 srcs/DiscordNotifier/Discord/DiscordWebhookLogsService.cs create mode 100644 srcs/DiscordNotifier/Discord/IDiscordWebhookLogsService.cs create mode 100644 srcs/DiscordNotifier/DiscordNotifier.csproj create mode 100644 srcs/DiscordNotifier/DockerGracefulStopService.cs create mode 100644 srcs/DiscordNotifier/Formatting/DiscordLogExtensions.cs create mode 100644 srcs/DiscordNotifier/Formatting/GenericDiscordEmbedLogConsumer.cs create mode 100644 srcs/DiscordNotifier/Formatting/GenericDiscordLogConsumer.cs create mode 100644 srcs/DiscordNotifier/Formatting/IDiscordEmbedLogFormatter.cs create mode 100644 srcs/DiscordNotifier/Formatting/IDiscordLogFormatter.cs create mode 100644 srcs/DiscordNotifier/LogType.cs create mode 100644 srcs/DiscordNotifier/Managers/ItemManager.cs create mode 100644 srcs/DiscordNotifier/Program.cs create mode 100644 srcs/DiscordNotifier/Startup.cs create mode 100644 srcs/DiscordNotifier/StaticHardcodedCode.cs create mode 100644 srcs/FamilyServer/Achievements/FamilyAchievementIncrement.cs create mode 100644 srcs/FamilyServer/Achievements/FamilyAchievementIncrementEventHandler.cs create mode 100644 srcs/FamilyServer/Achievements/FamilyAchievementIncrementMessageConsumer.cs create mode 100644 srcs/FamilyServer/Achievements/FamilyAchievementManager.cs create mode 100644 srcs/FamilyServer/Achievements/FamilyMissionIncrement.cs create mode 100644 srcs/FamilyServer/Achievements/FamilyMissionIncrementMessageConsumer.cs create mode 100644 srcs/FamilyServer/Consumers/FamilyCharacterConnectMessageConsumer.cs create mode 100644 srcs/FamilyServer/Consumers/FamilyCharacterDisconnectMessageConsumer.cs create mode 100644 srcs/FamilyServer/Consumers/FamilyDeclareExperienceGainedMessageConsumer.cs create mode 100644 srcs/FamilyServer/Consumers/FamilyDeclareLogsMessageConsumer.cs create mode 100644 srcs/FamilyServer/Consumers/FamilyHeadSexMessageConsumer.cs create mode 100644 srcs/FamilyServer/Consumers/FamilyMemberTodayMessageConsumer.cs create mode 100644 srcs/FamilyServer/Consumers/FamilyMissionsResetMessageConsumer.cs create mode 100644 srcs/FamilyServer/Consumers/FamilyNoticeMessageConsumer.cs create mode 100644 srcs/FamilyServer/Consumers/ServiceFlushAllMessageConsumer.cs create mode 100644 srcs/FamilyServer/DockerGracefulStopService.cs create mode 100644 srcs/FamilyServer/EnvironmentConsts.cs create mode 100644 srcs/FamilyServer/FamilyServer.csproj create mode 100644 srcs/FamilyServer/FamilySystem.cs create mode 100644 srcs/FamilyServer/Logs/FamilyLogManager.cs create mode 100644 srcs/FamilyServer/Managers/AddWarehouseItemResult.cs create mode 100644 srcs/FamilyServer/Managers/ExperienceIncrementRequest.cs create mode 100644 srcs/FamilyServer/Managers/FamilyExperienceManager.cs create mode 100644 srcs/FamilyServer/Managers/FamilyManager.cs create mode 100644 srcs/FamilyServer/Managers/FamilyMembershipManager.cs create mode 100644 srcs/FamilyServer/Managers/FamilyWarehouseLogManager.cs create mode 100644 srcs/FamilyServer/Managers/FamilyWarehouseManager.cs create mode 100644 srcs/FamilyServer/Managers/IFamilyWarehouseManager.cs create mode 100644 srcs/FamilyServer/Managers/MoveWarehouseItemResult.cs create mode 100644 srcs/FamilyServer/Managers/WithdrawWarehouseItemResult.cs create mode 100644 srcs/FamilyServer/Program.cs create mode 100644 srcs/FamilyServer/Services/FamilyInvitationService.cs create mode 100644 srcs/FamilyServer/Services/FamilyService.cs create mode 100644 srcs/FamilyServer/Services/FamilyWarehouseService.cs create mode 100644 srcs/FamilyServer/Startup.cs create mode 100644 srcs/GameChannel/Consumers/KickAccountConsumer.cs create mode 100644 srcs/GameChannel/Consumers/PlayerConnectedChannelGameConsumer.cs create mode 100644 srcs/GameChannel/Consumers/PlayerDisconnectedChannelConsumer.cs create mode 100644 srcs/GameChannel/Consumers/PlayerKickConsumer.cs create mode 100644 srcs/GameChannel/Consumers/ServiceKickAllMessageConsumer.cs create mode 100644 srcs/GameChannel/Consumers/ServiceMaintenanceNotificationMessageConsumer.cs create mode 100644 srcs/GameChannel/Consumers/WorldServerShutdownConsumer.cs create mode 100644 srcs/GameChannel/Controllers/HealthController.cs create mode 100644 srcs/GameChannel/Cryptography/EncodingExtensions.cs create mode 100644 srcs/GameChannel/Cryptography/WorldDecrypter.cs create mode 100644 srcs/GameChannel/Cryptography/WorldEncrypter.cs create mode 100644 srcs/GameChannel/GameChannel.csproj create mode 100644 srcs/GameChannel/Network/GameSession.cs create mode 100644 srcs/GameChannel/Network/GameSessionFactory.cs create mode 100644 srcs/GameChannel/Network/GameTcpServer.cs create mode 100644 srcs/GameChannel/Network/IClientSessionFactory.cs create mode 100644 srcs/GameChannel/Program.cs create mode 100644 srcs/GameChannel/RecurrentJobs/WorldKeepAliveSystem.cs create mode 100644 srcs/GameChannel/Services/GameChannelStopService.cs create mode 100644 srcs/GameChannel/Startup.cs create mode 100644 srcs/GameChannel/Ticks/TickConfiguration.cs create mode 100644 srcs/GameChannel/Ticks/TickExtensions.cs create mode 100644 srcs/GameChannel/Ticks/TickWorker.cs create mode 100644 srcs/GameChannel/Ticks/WorkerDispatchTickManager.cs create mode 100644 srcs/GameChannel/Utils/EnvironmentConsts.cs create mode 100644 srcs/GameChannel/Utils/GracefulShutdown.cs create mode 100644 srcs/GameChannel/Utils/ISpamProtector.cs create mode 100644 srcs/GameChannel/Utils/MapsterMapper.cs create mode 100644 srcs/GameChannel/Utils/SmartSpamProtector.cs create mode 100644 srcs/GameChannel/WorldServerSingleton.cs create mode 100644 srcs/LoginServer/Auth/IClientVersionCheckingService.cs create mode 100644 srcs/LoginServer/Auth/RedisCachedHardwareIdService.cs create mode 100644 srcs/LoginServer/Auth/RedisClientVersionCheckingService.cs create mode 100644 srcs/LoginServer/Auth/StringHashExtensions.cs create mode 100644 srcs/LoginServer/Handlers/GenericLoginPacketHandlerBase.cs create mode 100644 srcs/LoginServer/Handlers/GlobalPacketProcessor.cs create mode 100644 srcs/LoginServer/Handlers/IGlobalPacketProcessor.cs create mode 100644 srcs/LoginServer/Handlers/IPacketHandler.cs create mode 100644 srcs/LoginServer/Handlers/LoginPacketsExtensions.cs create mode 100644 srcs/LoginServer/Handlers/TypedCredentialsLoginPacketHandler.cs create mode 100644 srcs/LoginServer/LoginServer.csproj create mode 100644 srcs/LoginServer/Network/LoginClientSession.cs create mode 100644 srcs/LoginServer/Network/LoginServer.cs create mode 100644 srcs/LoginServer/NostaleLoginDecrypter.cs create mode 100644 srcs/LoginServer/NostaleLoginEncrypter.cs create mode 100644 srcs/LoginServer/Program.cs create mode 100644 srcs/LoginServer/Startup.cs create mode 100644 srcs/LoginServer/Utils/DockerGracefulStopService.cs create mode 100644 srcs/LoginServer/Utils/ISpamProtector.cs create mode 100644 srcs/LoginServer/Utils/SmartSpamProtector.cs create mode 100644 srcs/LogsServer/Consumers/ServiceFlushAllMessageConsumer.cs create mode 100644 srcs/LogsServer/DockerGracefulStopService.cs create mode 100644 srcs/LogsServer/LogsServer.csproj create mode 100644 srcs/LogsServer/Program.cs create mode 100644 srcs/LogsServer/Startup.cs create mode 100644 srcs/MailServer/Consumers/MailCharacterConnectedMessageConsumer.cs create mode 100644 srcs/MailServer/Consumers/NoteCharacterConnectedMessageConsumer.cs create mode 100644 srcs/MailServer/Consumers/ServiceFlushAllMessageConsumer.cs create mode 100644 srcs/MailServer/DockerGracefulStopService.cs create mode 100644 srcs/MailServer/MailServer.csproj create mode 100644 srcs/MailServer/Managers/MailManager.cs create mode 100644 srcs/MailServer/Program.cs create mode 100644 srcs/MailServer/RecurrentJobs/MailSystem.cs create mode 100644 srcs/MailServer/Services/MailService.cs create mode 100644 srcs/MailServer/Services/NoteService.cs create mode 100644 srcs/MailServer/Startup.cs create mode 100644 srcs/Master/Consumers/PlayerConnectedOnChannelMessageConsumer.cs create mode 100644 srcs/Master/Consumers/PlayerDisconnectedChannelMessageConsumer.cs create mode 100644 srcs/Master/Datas/WorldServer.cs create mode 100644 srcs/Master/DockerGracefulStopService.cs create mode 100644 srcs/Master/Extensions/WorldExtensions.cs create mode 100644 srcs/Master/Managers/ClusterCharacterManager.cs create mode 100644 srcs/Master/Managers/WorldServerManager.cs create mode 100644 srcs/Master/Master.csproj create mode 100644 srcs/Master/Program.cs create mode 100644 srcs/Master/Proxies/ServerApiService.cs create mode 100644 srcs/Master/RecurrentJobs/GameChannelHeartbeatService.cs create mode 100644 srcs/Master/Services/ClusterCharacterService.cs create mode 100644 srcs/Master/Services/Maintenance/GrpcClusterStatusService.cs create mode 100644 srcs/Master/Services/Maintenance/IStatusManager.cs create mode 100644 srcs/Master/Services/Maintenance/ServiceStatus.cs create mode 100644 srcs/Master/Services/Maintenance/StatusManager.cs create mode 100644 srcs/Master/Services/Maintenance/StatusRefreshMessageConsumer.cs create mode 100644 srcs/Master/Services/Sessions/EncryptionKeyFactory.cs create mode 100644 srcs/Master/Services/Sessions/ISessionManager.cs create mode 100644 srcs/Master/Services/Sessions/RedisSessionManager.cs create mode 100644 srcs/Master/Services/Sessions/SessionService.cs create mode 100644 srcs/Master/Startup.cs create mode 100644 srcs/PhoenixLib.Auth.JWT/DependencyInjectionExtensions.cs create mode 100644 srcs/PhoenixLib.Auth.JWT/HashingHelper.cs create mode 100644 srcs/PhoenixLib.Auth.JWT/IJwtTokenFactory.cs create mode 100644 srcs/PhoenixLib.Auth.JWT/JwtExtensions.cs create mode 100644 srcs/PhoenixLib.Auth.JWT/JwtTokenFactory.cs create mode 100644 srcs/PhoenixLib.Auth.JWT/PhoenixLib.Auth.JWT.csproj create mode 100644 srcs/PhoenixLib.Caching/ICachedRepository.cs create mode 100644 srcs/PhoenixLib.Caching/IKeyValueCache.cs create mode 100644 srcs/PhoenixLib.Caching/ILongKeyCachedRepository.cs create mode 100644 srcs/PhoenixLib.Caching/IUuidKeyCachedRepository.cs create mode 100644 srcs/PhoenixLib.Caching/InMemoryCacheRepository.cs create mode 100644 srcs/PhoenixLib.Caching/InMemoryKeyValueCache.cs create mode 100644 srcs/PhoenixLib.Caching/InMemoryUuidCacheRepository.cs create mode 100644 srcs/PhoenixLib.Caching/MemoryCacheHandle.cs create mode 100644 srcs/PhoenixLib.Caching/PhoenixLib.Caching.csproj create mode 100644 srcs/PhoenixLib.Caching/RuntimeCachingBuilderExtensions.cs create mode 100644 srcs/PhoenixLib.Configuration/ConfigurationHelperConfig.cs create mode 100644 srcs/PhoenixLib.Configuration/ConfigurationPathProvider.cs create mode 100644 srcs/PhoenixLib.Configuration/DependencyInjectionExtensions.cs create mode 100644 srcs/PhoenixLib.Configuration/IConfigurationHelper.cs create mode 100644 srcs/PhoenixLib.Configuration/IConfigurationPathProvider.cs create mode 100644 srcs/PhoenixLib.Configuration/PhoenixLib.Configuration.csproj create mode 100644 srcs/PhoenixLib.Configuration/StringExtensions.cs create mode 100644 srcs/PhoenixLib.Configuration/YamlConfigurationHelper.cs create mode 100644 srcs/PhoenixLib.DAL.Abstractions/BaseUuidDto.cs create mode 100644 srcs/PhoenixLib.DAL.Abstractions/IDto.cs create mode 100644 srcs/PhoenixLib.DAL.Abstractions/IGenericAsyncIntRepository.cs create mode 100644 srcs/PhoenixLib.DAL.Abstractions/IGenericAsyncLongRepository.cs create mode 100644 srcs/PhoenixLib.DAL.Abstractions/IGenericAsyncRepository.cs create mode 100644 srcs/PhoenixLib.DAL.Abstractions/IGenericAsyncUuidRepository.cs create mode 100644 srcs/PhoenixLib.DAL.Abstractions/IIntDto.cs create mode 100644 srcs/PhoenixLib.DAL.Abstractions/IKeyValueAsyncStorage.cs create mode 100644 srcs/PhoenixLib.DAL.Abstractions/ILongDto.cs create mode 100644 srcs/PhoenixLib.DAL.Abstractions/IMapper.cs create mode 100644 srcs/PhoenixLib.DAL.Abstractions/IUuidDto.cs create mode 100644 srcs/PhoenixLib.DAL.Abstractions/PhoenixLib.DAL.Abstractions.csproj create mode 100644 srcs/PhoenixLib.DAL.EFCore.PGSQL/Extensions/DatabaseClearExtensions.cs create mode 100644 srcs/PhoenixLib.DAL.EFCore.PGSQL/Extensions/ServiceCollectionExtensions.cs create mode 100644 srcs/PhoenixLib.DAL.EFCore.PGSQL/GenericIntRepository.cs create mode 100644 srcs/PhoenixLib.DAL.EFCore.PGSQL/GenericLongRepository.cs create mode 100644 srcs/PhoenixLib.DAL.EFCore.PGSQL/GenericMappedIntRepository.cs create mode 100644 srcs/PhoenixLib.DAL.EFCore.PGSQL/GenericMappedLongRepository.cs create mode 100644 srcs/PhoenixLib.DAL.EFCore.PGSQL/GenericMappedUuidRepository.cs create mode 100644 srcs/PhoenixLib.DAL.EFCore.PGSQL/GenericOldMappedIntRepository.cs create mode 100644 srcs/PhoenixLib.DAL.EFCore.PGSQL/GenericOldMappedLongRepository.cs create mode 100644 srcs/PhoenixLib.DAL.EFCore.PGSQL/GenericOldMappedUuidRepository.cs create mode 100644 srcs/PhoenixLib.DAL.EFCore.PGSQL/GenericStringRepository.cs create mode 100644 srcs/PhoenixLib.DAL.EFCore.PGSQL/GenericUuidRepository.cs create mode 100644 srcs/PhoenixLib.DAL.EFCore.PGSQL/IContextFactory.cs create mode 100644 srcs/PhoenixLib.DAL.EFCore.PGSQL/IGenericStringRepository.cs create mode 100644 srcs/PhoenixLib.DAL.EFCore.PGSQL/IIntEntity.cs create mode 100644 srcs/PhoenixLib.DAL.EFCore.PGSQL/ILongEntity.cs create mode 100644 srcs/PhoenixLib.DAL.EFCore.PGSQL/IStringKeyEntity.cs create mode 100644 srcs/PhoenixLib.DAL.EFCore.PGSQL/IUuidEntity.cs create mode 100644 srcs/PhoenixLib.DAL.EFCore.PGSQL/PgSqlDatabaseConfiguration.cs create mode 100644 srcs/PhoenixLib.DAL.EFCore.PGSQL/PhoenixLib.DAL.EFCore.PGSQL.csproj create mode 100644 srcs/PhoenixLib.DAL.MongoDB/MappedMongoRepository.cs create mode 100644 srcs/PhoenixLib.DAL.MongoDB/MongoConfiguration.cs create mode 100644 srcs/PhoenixLib.DAL.MongoDB/MongoConfigurationBuilder.cs create mode 100644 srcs/PhoenixLib.DAL.MongoDB/PhoenixLib.DAL.MongoDB.csproj create mode 100644 srcs/PhoenixLib.DAL.MongoDB/SynchronizedMongoRepository.cs create mode 100644 srcs/PhoenixLib.DAL.Redis/DependencyInjectionExtensions.cs create mode 100644 srcs/PhoenixLib.DAL.Redis/Locks/IExpirableLockService.cs create mode 100644 srcs/PhoenixLib.DAL.Redis/Locks/ILockService.cs create mode 100644 srcs/PhoenixLib.DAL.Redis/Locks/IScopedLock.cs create mode 100644 srcs/PhoenixLib.DAL.Redis/Locks/RedisCheckableLock.cs create mode 100644 srcs/PhoenixLib.DAL.Redis/Locks/RedisLockService.cs create mode 100644 srcs/PhoenixLib.DAL.Redis/Locks/RedisScopedLock.cs create mode 100644 srcs/PhoenixLib.DAL.Redis/PhoenixLib.DAL.Redis.csproj create mode 100644 srcs/PhoenixLib.DAL.Redis/RedisConfiguration.cs create mode 100644 srcs/PhoenixLib.DAL.Redis/RedisGenericKeyValueAsyncStorage.cs create mode 100644 srcs/PhoenixLib.Events/AsyncEventPipeline.cs create mode 100644 srcs/PhoenixLib.Events/ConvertedEventForwarder.cs create mode 100644 srcs/PhoenixLib.Events/DependencyInjection.cs create mode 100644 srcs/PhoenixLib.Events/EventPipeline.cs create mode 100644 srcs/PhoenixLib.Events/IAsyncEvent.cs create mode 100644 srcs/PhoenixLib.Events/IAsyncEventPipeline.cs create mode 100644 srcs/PhoenixLib.Events/IAsyncEventProcessor.cs create mode 100644 srcs/PhoenixLib.Events/IConverter.cs create mode 100644 srcs/PhoenixLib.Events/IEvent.cs create mode 100644 srcs/PhoenixLib.Events/IEventProcessor.cs create mode 100644 srcs/PhoenixLib.Events/IEventProcessorPipeline.cs create mode 100644 srcs/PhoenixLib.Events/Internal/AssemblyExtensions.cs create mode 100644 srcs/PhoenixLib.Events/PhoenixLib.Events.csproj create mode 100644 srcs/PhoenixLib.Events/ServiceProviderServiceExtensions.cs create mode 100644 srcs/PhoenixLib.Extensions/AssemblyExtensions.cs create mode 100644 srcs/PhoenixLib.Extensions/ByteArrayExtensions.cs create mode 100644 srcs/PhoenixLib.Extensions/PhoenixLib.Extensions.csproj create mode 100644 srcs/PhoenixLib.Extensions/StringExtensions.cs create mode 100644 srcs/PhoenixLib.Logging/ILogger.cs create mode 100644 srcs/PhoenixLib.Logging/Log.cs create mode 100644 srcs/PhoenixLib.Logging/LoggingExtensions.cs create mode 100644 srcs/PhoenixLib.Logging/PhoenixLib.Logging.csproj create mode 100644 srcs/PhoenixLib.Logging/SerilogLogger.cs create mode 100644 srcs/PhoenixLib.Messaging/Extensions/DependencyInjectionExtensions.cs create mode 100644 srcs/PhoenixLib.Messaging/GenericSubscribedMessage.cs create mode 100644 srcs/PhoenixLib.Messaging/IMessage.cs create mode 100644 srcs/PhoenixLib.Messaging/IMessageConsumer.cs create mode 100644 srcs/PhoenixLib.Messaging/IMessagePublisher.cs create mode 100644 srcs/PhoenixLib.Messaging/IMessagingService.cs create mode 100644 srcs/PhoenixLib.Messaging/Internal/GenericMessagePublisher.cs create mode 100644 srcs/PhoenixLib.Messaging/Internal/IServiceBusInstance.cs create mode 100644 srcs/PhoenixLib.Messaging/Internal/MQTT/CloudEventsJsonMessageSerializer.cs create mode 100644 srcs/PhoenixLib.Messaging/Internal/MQTT/IMessageSerializer.cs create mode 100644 srcs/PhoenixLib.Messaging/Internal/MQTT/MqttConfiguration.cs create mode 100644 srcs/PhoenixLib.Messaging/Internal/MQTT/MqttMessagingService.cs create mode 100644 srcs/PhoenixLib.Messaging/Internal/Routing/IMessageRouter.cs create mode 100644 srcs/PhoenixLib.Messaging/Internal/Routing/IRoutingInformation.cs create mode 100644 srcs/PhoenixLib.Messaging/Internal/Routing/IRoutingInformationFactory.cs create mode 100644 srcs/PhoenixLib.Messaging/Internal/Routing/ISubscribedMessage.cs create mode 100644 srcs/PhoenixLib.Messaging/Internal/Routing/MessageExtensions.cs create mode 100644 srcs/PhoenixLib.Messaging/Internal/Routing/MessageRouter.cs create mode 100644 srcs/PhoenixLib.Messaging/Internal/Routing/MessageTypeAttribute.cs create mode 100644 srcs/PhoenixLib.Messaging/Internal/Routing/RoutingInformation.cs create mode 100644 srcs/PhoenixLib.Messaging/Internal/Routing/RoutingInformationFactory.cs create mode 100644 srcs/PhoenixLib.Messaging/Internal/ServiceBusInstance.cs create mode 100644 srcs/PhoenixLib.Messaging/PhoenixLib.Messaging.csproj create mode 100644 srcs/PhoenixLib.Multilanguage/DependencyInjectionExtensions.cs create mode 100644 srcs/PhoenixLib.Multilanguage/GenericMultilanguageService.cs create mode 100644 srcs/PhoenixLib.Multilanguage/IEnumBasedLanguageService.cs create mode 100644 srcs/PhoenixLib.Multilanguage/ILanguageService.cs create mode 100644 srcs/PhoenixLib.Multilanguage/IStringBasedLanguageService.cs create mode 100644 srcs/PhoenixLib.Multilanguage/PhoenixLib.Multilanguage.csproj create mode 100644 srcs/PhoenixLib.Multilanguage/RegionLanguageType.cs create mode 100644 srcs/PhoenixLib.Scheduler.ReactiveX/FuncTaskWorkerDisposable.cs create mode 100644 srcs/PhoenixLib.Scheduler.ReactiveX/ObservableCron.cs create mode 100644 srcs/PhoenixLib.Scheduler.ReactiveX/ObservableScheduler.cs create mode 100644 srcs/PhoenixLib.Scheduler.ReactiveX/PhoenixLib.Scheduler.ReactiveX.csproj create mode 100644 srcs/PhoenixLib.Scheduler.ReactiveX/SchedulerServiceCollectionExtensions.cs create mode 100644 srcs/PhoenixLib.Scheduler.ReactiveX/TaskWorkerDisposable.cs create mode 100644 srcs/PhoenixLib.Scheduler/ICron.cs create mode 100644 srcs/PhoenixLib.Scheduler/ICronJobPool.cs create mode 100644 srcs/PhoenixLib.Scheduler/IGenericCronPool.cs create mode 100644 srcs/PhoenixLib.Scheduler/IGenericJobPool.cs create mode 100644 srcs/PhoenixLib.Scheduler/IScheduledJob.cs create mode 100644 srcs/PhoenixLib.Scheduler/IScheduler.cs create mode 100644 srcs/PhoenixLib.Scheduler/Interval.cs create mode 100644 srcs/PhoenixLib.Scheduler/PhoenixLib.Scheduler.csproj create mode 100644 srcs/PhoenixLib.Scheduler/TimePoint.cs create mode 100644 srcs/Plugin.RainbowBattle/Command/RainbowBattleCommandModule.cs create mode 100644 srcs/Plugin.RainbowBattle/EventHandlers/RainbowBattleCaptureFlagEventHandler.cs create mode 100644 srcs/Plugin.RainbowBattle/EventHandlers/RainbowBattleDestroyEventHandler.cs create mode 100644 srcs/Plugin.RainbowBattle/EventHandlers/RainbowBattleEndEventHandler.cs create mode 100644 srcs/Plugin.RainbowBattle/EventHandlers/RainbowBattleFreezeEventHandler.cs create mode 100644 srcs/Plugin.RainbowBattle/EventHandlers/RainbowBattleLeaveEventHandler.cs create mode 100644 srcs/Plugin.RainbowBattle/EventHandlers/RainbowBattleLeaverBusterRefreshEventHandler.cs create mode 100644 srcs/Plugin.RainbowBattle/EventHandlers/RainbowBattleProcessActivityPointsEventHandler.cs create mode 100644 srcs/Plugin.RainbowBattle/EventHandlers/RainbowBattleProcessFlagPointsEventHandler.cs create mode 100644 srcs/Plugin.RainbowBattle/EventHandlers/RainbowBattleProcessLifeEventHandler.cs create mode 100644 srcs/Plugin.RainbowBattle/EventHandlers/RainbowBattleRefreshScoreEventHandler.cs create mode 100644 srcs/Plugin.RainbowBattle/EventHandlers/RainbowBattleStartEventHandler.cs create mode 100644 srcs/Plugin.RainbowBattle/EventHandlers/RainbowBattleStartProcessRegistrationEventHandler.cs create mode 100644 srcs/Plugin.RainbowBattle/EventHandlers/RainbowBattleStartRegisterEventHandler.cs create mode 100644 srcs/Plugin.RainbowBattle/EventHandlers/RainbowBattleUnfreezeEventHandler.cs create mode 100644 srcs/Plugin.RainbowBattle/EventHandlers/RainbowBattleUnfreezeProcessEventHandler.cs create mode 100644 srcs/Plugin.RainbowBattle/Managers/IRainbowFactory.cs create mode 100644 srcs/Plugin.RainbowBattle/Managers/RainbowBattleLeaverBusterResetMessageConsumer.cs create mode 100644 srcs/Plugin.RainbowBattle/Managers/RainbowBattleStartMessageConsumer.cs create mode 100644 srcs/Plugin.RainbowBattle/Plugin.RainbowBattle.csproj create mode 100644 srcs/Plugin.RainbowBattle/RainbowBattlePluginCore.cs create mode 100644 srcs/Plugin.RainbowBattle/RecurrentJob/RainbowBattleSystem.cs create mode 100644 srcs/RelationServer/Consumer/RelationCharacterConnectMessageConsumer.cs create mode 100644 srcs/RelationServer/Consumer/RelationCharacterDisconnectMessageConsumer.cs create mode 100644 srcs/RelationServer/DockerGracefulStopService.cs create mode 100644 srcs/RelationServer/Program.cs create mode 100644 srcs/RelationServer/RelationServer.csproj create mode 100644 srcs/RelationServer/Services/RelationService.cs create mode 100644 srcs/RelationServer/Startup.cs create mode 100644 srcs/Scheduler/Configs/InstantBattleStartFileConfiguration.cs create mode 100644 srcs/Scheduler/DockerGracefulStopService.cs create mode 100644 srcs/Scheduler/HangfireJobActivator.cs create mode 100644 srcs/Scheduler/Program.cs create mode 100644 srcs/Scheduler/Ranking/RankingRefreshEvent.cs create mode 100644 srcs/Scheduler/Ranking/RankingRefreshEventHandler.cs create mode 100644 srcs/Scheduler/Scheduler.csproj create mode 100644 srcs/Scheduler/Service/ComplimentsMonthlyRefreshCronScheduler.cs create mode 100644 srcs/Scheduler/Service/FamilyMissionsResetCronScheduler.cs create mode 100644 srcs/Scheduler/Service/InstantBattleCronScheduler.cs create mode 100644 srcs/Scheduler/Service/MinigameProductionRefreshCronScheduler.cs create mode 100644 srcs/Scheduler/Service/QuestDailyRefreshCronScheduler.cs create mode 100644 srcs/Scheduler/Service/RaidRestrictionRefreshCronScheduler.cs create mode 100644 srcs/Scheduler/Service/RainbowBattleCronScheduler.cs create mode 100644 srcs/Scheduler/Service/RainbowBattleLeaverBusterCronScheduler.cs create mode 100644 srcs/Scheduler/Service/RankingRefreshCronScheduler.cs create mode 100644 srcs/Scheduler/Service/SpecialistPointsRefreshCronScheduler.cs create mode 100644 srcs/Scheduler/Startup.cs create mode 100644 srcs/Scheduler/Utility/RandomGenerator.cs create mode 100644 srcs/Toolkit/CommandHandlers/CheckTranslationsCommandHandler.cs create mode 100644 srcs/Toolkit/CommandHandlers/CreateAccountCommandHandler.cs create mode 100644 srcs/Toolkit/CommandHandlers/GenerateTranslationsCommandHandler.cs create mode 100644 srcs/Toolkit/Commands/CheckTranslationsCommand.cs create mode 100644 srcs/Toolkit/Commands/CreateAccountCommand.cs create mode 100644 srcs/Toolkit/Commands/GenerateTranslationsCommand.cs create mode 100644 srcs/Toolkit/Extensions/RegionLanguageTypeExtensions.cs create mode 100644 srcs/Toolkit/Program.cs create mode 100644 srcs/Toolkit/Toolkit.csproj create mode 100644 srcs/TranslationsServer/DockerGracefulStopService.cs create mode 100644 srcs/TranslationsServer/Loader/BannedNamesConfiguration.cs create mode 100644 srcs/TranslationsServer/Loader/GenericTranslationFileLoader.cs create mode 100644 srcs/TranslationsServer/Loader/TranslationsFileLoaderOptions.cs create mode 100644 srcs/TranslationsServer/Program.cs create mode 100644 srcs/TranslationsServer/Services/GrpcGameLanguageService.cs create mode 100644 srcs/TranslationsServer/Startup.cs create mode 100644 srcs/TranslationsServer/TranslationsServer.csproj create mode 100644 srcs/WingsAPI.Commands/Checks/RequireAuthorityAttribute.cs create mode 100644 srcs/WingsAPI.Commands/CommandGlobalExecutorWrapper.cs create mode 100644 srcs/WingsAPI.Commands/CommandHandler.cs create mode 100644 srcs/WingsAPI.Commands/Entities/SaltyCommandResult.cs create mode 100644 srcs/WingsAPI.Commands/Entities/SaltyModuleBase.cs create mode 100644 srcs/WingsAPI.Commands/Entities/WingsEmuIngameCommandContext.cs create mode 100644 srcs/WingsAPI.Commands/Interfaces/ICommandContainer.cs create mode 100644 srcs/WingsAPI.Commands/TypeParsers/ItemTypeParser.cs create mode 100644 srcs/WingsAPI.Commands/TypeParsers/MapInstanceTypeParser.cs create mode 100644 srcs/WingsAPI.Commands/TypeParsers/PlayerEntityTypeParser.cs create mode 100644 srcs/WingsAPI.Commands/WingsAPI.Commands.csproj create mode 100644 srcs/WingsAPI.Communication/Auth/AuthorizedClientVersionDto.cs create mode 100644 srcs/WingsAPI.Communication/Auth/BlacklistedHwidDto.cs create mode 100644 srcs/WingsAPI.Communication/Auth/IHardwareIdService.cs create mode 100644 srcs/WingsAPI.Communication/BasicRpcResponse.cs create mode 100644 srcs/WingsAPI.Communication/Bazaar/BazaarAddItemRequest.cs create mode 100644 srcs/WingsAPI.Communication/Bazaar/BazaarBuyItemRequest.cs create mode 100644 srcs/WingsAPI.Communication/Bazaar/BazaarChangeItemPriceRequest.cs create mode 100644 srcs/WingsAPI.Communication/Bazaar/BazaarGetItemByIdRequest.cs create mode 100644 srcs/WingsAPI.Communication/Bazaar/BazaarGetItemsByCharIdRequest.cs create mode 100644 srcs/WingsAPI.Communication/Bazaar/BazaarGetItemsByCharIdResponse.cs create mode 100644 srcs/WingsAPI.Communication/Bazaar/BazaarItemResponse.cs create mode 100644 srcs/WingsAPI.Communication/Bazaar/BazaarRemoveItemRequest.cs create mode 100644 srcs/WingsAPI.Communication/Bazaar/BazaarRemoveItemsByCharIdRequest.cs create mode 100644 srcs/WingsAPI.Communication/Bazaar/BazaarRemoveItemsByCharIdResponse.cs create mode 100644 srcs/WingsAPI.Communication/Bazaar/BazaarSearchBazaarItemsRequest.cs create mode 100644 srcs/WingsAPI.Communication/Bazaar/BazaarSearchBazaarItemsResponse.cs create mode 100644 srcs/WingsAPI.Communication/Bazaar/BazaarSearchContext.cs create mode 100644 srcs/WingsAPI.Communication/Bazaar/IBazaarService.cs create mode 100644 srcs/WingsAPI.Communication/Compliments/ComplimentsMonthlyRefreshMessage.cs create mode 100644 srcs/WingsAPI.Communication/DbServer/AccountService/AccountBanGetRequest.cs create mode 100644 srcs/WingsAPI.Communication/DbServer/AccountService/AccountBanGetResponse.cs create mode 100644 srcs/WingsAPI.Communication/DbServer/AccountService/AccountBanSaveRequest.cs create mode 100644 srcs/WingsAPI.Communication/DbServer/AccountService/AccountBanSaveResponse.cs create mode 100644 srcs/WingsAPI.Communication/DbServer/AccountService/AccountLoadByIdRequest.cs create mode 100644 srcs/WingsAPI.Communication/DbServer/AccountService/AccountLoadByNameRequest.cs create mode 100644 srcs/WingsAPI.Communication/DbServer/AccountService/AccountLoadResponse.cs create mode 100644 srcs/WingsAPI.Communication/DbServer/AccountService/AccountPenaltyGetAllResponse.cs create mode 100644 srcs/WingsAPI.Communication/DbServer/AccountService/AccountPenaltyGetRequest.cs create mode 100644 srcs/WingsAPI.Communication/DbServer/AccountService/AccountPenaltyMultiSaveRequest.cs create mode 100644 srcs/WingsAPI.Communication/DbServer/AccountService/AccountPenaltyMultiSaveResponse.cs create mode 100644 srcs/WingsAPI.Communication/DbServer/AccountService/AccountSaveRequest.cs create mode 100644 srcs/WingsAPI.Communication/DbServer/AccountService/AccountSaveResponse.cs create mode 100644 srcs/WingsAPI.Communication/DbServer/AccountService/IAccountService.cs create mode 100644 srcs/WingsAPI.Communication/DbServer/CharacterService/CharacterGetTopResponse.cs create mode 100644 srcs/WingsAPI.Communication/DbServer/CharacterService/CharacterRefreshRankingResponse.cs create mode 100644 srcs/WingsAPI.Communication/DbServer/CharacterService/DbServerDeleteCharacterRequest.cs create mode 100644 srcs/WingsAPI.Communication/DbServer/CharacterService/DbServerDeleteCharacterResponse.cs create mode 100644 srcs/WingsAPI.Communication/DbServer/CharacterService/DbServerFlushCharacterSavesRequest.cs create mode 100644 srcs/WingsAPI.Communication/DbServer/CharacterService/DbServerFlushCharacterSavesResponse.cs create mode 100644 srcs/WingsAPI.Communication/DbServer/CharacterService/DbServerGetCharacterByIdRequest.cs create mode 100644 srcs/WingsAPI.Communication/DbServer/CharacterService/DbServerGetCharacterFromSlotRequest.cs create mode 100644 srcs/WingsAPI.Communication/DbServer/CharacterService/DbServerGetCharacterRequestByName.cs create mode 100644 srcs/WingsAPI.Communication/DbServer/CharacterService/DbServerGetCharacterResponse.cs create mode 100644 srcs/WingsAPI.Communication/DbServer/CharacterService/DbServerGetCharactersRequest.cs create mode 100644 srcs/WingsAPI.Communication/DbServer/CharacterService/DbServerGetCharactersResponse.cs create mode 100644 srcs/WingsAPI.Communication/DbServer/CharacterService/DbServerSaveCharacterRequest.cs create mode 100644 srcs/WingsAPI.Communication/DbServer/CharacterService/DbServerSaveCharacterResponse.cs create mode 100644 srcs/WingsAPI.Communication/DbServer/CharacterService/DbServerSaveCharactersRequest.cs create mode 100644 srcs/WingsAPI.Communication/DbServer/CharacterService/DbServerSaveCharactersResponse.cs create mode 100644 srcs/WingsAPI.Communication/DbServer/CharacterService/ICharacterService.cs create mode 100644 srcs/WingsAPI.Communication/DbServer/TimeSpaceService/ITimeSpaceService.cs create mode 100644 srcs/WingsAPI.Communication/DbServer/TimeSpaceService/TimeSpaceIsNewRecordRequest.cs create mode 100644 srcs/WingsAPI.Communication/DbServer/TimeSpaceService/TimeSpaceIsNewRecordResponse.cs create mode 100644 srcs/WingsAPI.Communication/DbServer/TimeSpaceService/TimeSpaceNewRecordRequest.cs create mode 100644 srcs/WingsAPI.Communication/DbServer/TimeSpaceService/TimeSpaceRecordRequest.cs create mode 100644 srcs/WingsAPI.Communication/DbServer/TimeSpaceService/TimeSpaceRecordResponse.cs create mode 100644 srcs/WingsAPI.Communication/DbServer/WarehouseService/AccountWarehouseAddItemRequest.cs create mode 100644 srcs/WingsAPI.Communication/DbServer/WarehouseService/AccountWarehouseAddItemResponse.cs create mode 100644 srcs/WingsAPI.Communication/DbServer/WarehouseService/AccountWarehouseGetItemRequest.cs create mode 100644 srcs/WingsAPI.Communication/DbServer/WarehouseService/AccountWarehouseGetItemResponse.cs create mode 100644 srcs/WingsAPI.Communication/DbServer/WarehouseService/AccountWarehouseGetItemsRequest.cs create mode 100644 srcs/WingsAPI.Communication/DbServer/WarehouseService/AccountWarehouseGetItemsResponse.cs create mode 100644 srcs/WingsAPI.Communication/DbServer/WarehouseService/AccountWarehouseMoveItemRequest.cs create mode 100644 srcs/WingsAPI.Communication/DbServer/WarehouseService/AccountWarehouseMoveItemResponse.cs create mode 100644 srcs/WingsAPI.Communication/DbServer/WarehouseService/AccountWarehouseWithdrawItemRequest.cs create mode 100644 srcs/WingsAPI.Communication/DbServer/WarehouseService/AccountWarehouseWithdrawItemResponse.cs create mode 100644 srcs/WingsAPI.Communication/DbServer/WarehouseService/IAccountWarehouseService.cs create mode 100644 srcs/WingsAPI.Communication/EmptyResponse.cs create mode 100644 srcs/WingsAPI.Communication/EmptyRpcRequest.cs create mode 100644 srcs/WingsAPI.Communication/Families/FamilyAddMemberRequest.cs create mode 100644 srcs/WingsAPI.Communication/Families/FamilyAddMemberResponse.cs create mode 100644 srcs/WingsAPI.Communication/Families/FamilyChangeAuthorityRequest.cs create mode 100644 srcs/WingsAPI.Communication/Families/FamilyChangeContainer.cs create mode 100644 srcs/WingsAPI.Communication/Families/FamilyChangeFactionRequest.cs create mode 100644 srcs/WingsAPI.Communication/Families/FamilyChangeFactionResponse.cs create mode 100644 srcs/WingsAPI.Communication/Families/FamilyChangeFactionResponseType.cs create mode 100644 srcs/WingsAPI.Communication/Families/FamilyChangeTitleRequest.cs create mode 100644 srcs/WingsAPI.Communication/Families/FamilyCreateRequest.cs create mode 100644 srcs/WingsAPI.Communication/Families/FamilyCreateResponse.cs create mode 100644 srcs/WingsAPI.Communication/Families/FamilyCreateResponseType.cs create mode 100644 srcs/WingsAPI.Communication/Families/FamilyDisbandRequest.cs create mode 100644 srcs/WingsAPI.Communication/Families/FamilyIdRequest.cs create mode 100644 srcs/WingsAPI.Communication/Families/FamilyIdResponse.cs create mode 100644 srcs/WingsAPI.Communication/Families/FamilyInvitation.cs create mode 100644 srcs/WingsAPI.Communication/Families/FamilyInvitationContainsResponse.cs create mode 100644 srcs/WingsAPI.Communication/Families/FamilyInvitationGetResponse.cs create mode 100644 srcs/WingsAPI.Communication/Families/FamilyInvitationRemoveRequest.cs create mode 100644 srcs/WingsAPI.Communication/Families/FamilyInvitationRequest.cs create mode 100644 srcs/WingsAPI.Communication/Families/FamilyInvitationSaveRequest.cs create mode 100644 srcs/WingsAPI.Communication/Families/FamilyListMembersResponse.cs create mode 100644 srcs/WingsAPI.Communication/Families/FamilyMemberDisconnectedRequest.cs create mode 100644 srcs/WingsAPI.Communication/Families/FamilyMissionsResetMessage.cs create mode 100644 srcs/WingsAPI.Communication/Families/FamilyRemoveMemberByCharIdRequest.cs create mode 100644 srcs/WingsAPI.Communication/Families/FamilyRemoveMemberRequest.cs create mode 100644 srcs/WingsAPI.Communication/Families/FamilySettingsRequest.cs create mode 100644 srcs/WingsAPI.Communication/Families/FamilyUpgradeAddResponseType.cs create mode 100644 srcs/WingsAPI.Communication/Families/FamilyUpgradeRequest.cs create mode 100644 srcs/WingsAPI.Communication/Families/FamilyUpgradeResponse.cs create mode 100644 srcs/WingsAPI.Communication/Families/IFamilyInvitationService.cs create mode 100644 srcs/WingsAPI.Communication/Families/IFamilyService.cs create mode 100644 srcs/WingsAPI.Communication/Families/MembershipRequest.cs create mode 100644 srcs/WingsAPI.Communication/Families/MembershipResponse.cs create mode 100644 srcs/WingsAPI.Communication/Families/MembershipTodayRequest.cs create mode 100644 srcs/WingsAPI.Communication/Families/MembershipTodayResponse.cs create mode 100644 srcs/WingsAPI.Communication/Families/Warehouse/FamilyWarehouseAddItemRequest.cs create mode 100644 srcs/WingsAPI.Communication/Families/Warehouse/FamilyWarehouseAddItemResponse.cs create mode 100644 srcs/WingsAPI.Communication/Families/Warehouse/FamilyWarehouseGetItemRequest.cs create mode 100644 srcs/WingsAPI.Communication/Families/Warehouse/FamilyWarehouseGetItemResponse.cs create mode 100644 srcs/WingsAPI.Communication/Families/Warehouse/FamilyWarehouseGetItemsRequest.cs create mode 100644 srcs/WingsAPI.Communication/Families/Warehouse/FamilyWarehouseGetItemsResponse.cs create mode 100644 srcs/WingsAPI.Communication/Families/Warehouse/FamilyWarehouseGetLogsRequest.cs create mode 100644 srcs/WingsAPI.Communication/Families/Warehouse/FamilyWarehouseGetLogsResponse.cs create mode 100644 srcs/WingsAPI.Communication/Families/Warehouse/FamilyWarehouseMoveItemRequest.cs create mode 100644 srcs/WingsAPI.Communication/Families/Warehouse/FamilyWarehouseMoveItemResponse.cs create mode 100644 srcs/WingsAPI.Communication/Families/Warehouse/FamilyWarehouseWithdrawItemRequest.cs create mode 100644 srcs/WingsAPI.Communication/Families/Warehouse/FamilyWarehouseWithdrawItemResponse.cs create mode 100644 srcs/WingsAPI.Communication/Families/Warehouse/IFamilyWarehouseService.cs create mode 100644 srcs/WingsAPI.Communication/InstantBattle/InstantBattleStartMessage.cs create mode 100644 srcs/WingsAPI.Communication/Mail/CreateMailBatchRequest.cs create mode 100644 srcs/WingsAPI.Communication/Mail/CreateMailBatchResponse.cs create mode 100644 srcs/WingsAPI.Communication/Mail/CreateMailRequest.cs create mode 100644 srcs/WingsAPI.Communication/Mail/CreateMailResponse.cs create mode 100644 srcs/WingsAPI.Communication/Mail/CreateNoteRequest.cs create mode 100644 srcs/WingsAPI.Communication/Mail/CreateNoteResponse.cs create mode 100644 srcs/WingsAPI.Communication/Mail/GetMailsRequest.cs create mode 100644 srcs/WingsAPI.Communication/Mail/GetMailsResponse.cs create mode 100644 srcs/WingsAPI.Communication/Mail/IMailService.cs create mode 100644 srcs/WingsAPI.Communication/Mail/INoteService.cs create mode 100644 srcs/WingsAPI.Communication/Mail/OpenNoteRequest.cs create mode 100644 srcs/WingsAPI.Communication/Mail/RemoveMailRequest.cs create mode 100644 srcs/WingsAPI.Communication/Mail/RemoveNoteRequest.cs create mode 100644 srcs/WingsAPI.Communication/Miniland/MinigameRefreshProductionPointsMessage.cs create mode 100644 srcs/WingsAPI.Communication/Player/ClusterCharacterByChannelIdRequest.cs create mode 100644 srcs/WingsAPI.Communication/Player/ClusterCharacterByIdRequest.cs create mode 100644 srcs/WingsAPI.Communication/Player/ClusterCharacterByNameRequest.cs create mode 100644 srcs/WingsAPI.Communication/Player/ClusterCharacterGetMultipleResponse.cs create mode 100644 srcs/WingsAPI.Communication/Player/ClusterCharacterGetSortedResponse.cs create mode 100644 srcs/WingsAPI.Communication/Player/ClusterCharacterInfo.cs create mode 100644 srcs/WingsAPI.Communication/Player/ClusterCharacterResponse.cs create mode 100644 srcs/WingsAPI.Communication/Player/IClusterCharacterService.cs create mode 100644 srcs/WingsAPI.Communication/Player/RankingRefreshMessage.cs create mode 100644 srcs/WingsAPI.Communication/Player/SpecialistPointsRefreshMessage.cs create mode 100644 srcs/WingsAPI.Communication/Punishment/PlayerKickMessage.cs create mode 100644 srcs/WingsAPI.Communication/Quests/QuestDailyRefreshMessage.cs create mode 100644 srcs/WingsAPI.Communication/Raid/RaidRestrictionRefreshMessage.cs create mode 100644 srcs/WingsAPI.Communication/RainbowBattle/RainbowBattleLeaverBusterResetMessage.cs create mode 100644 srcs/WingsAPI.Communication/RainbowBattle/RainbowBattleStartMessage.cs create mode 100644 srcs/WingsAPI.Communication/Relation/IRelationService.cs create mode 100644 srcs/WingsAPI.Communication/Relation/RelationAddRequest.cs create mode 100644 srcs/WingsAPI.Communication/Relation/RelationAddResponse.cs create mode 100644 srcs/WingsAPI.Communication/Relation/RelationGetAllRequest.cs create mode 100644 srcs/WingsAPI.Communication/Relation/RelationGetAllResponse.cs create mode 100644 srcs/WingsAPI.Communication/Relation/RelationRemoveRequest.cs create mode 100644 srcs/WingsAPI.Communication/RpcResponseType.cs create mode 100644 srcs/WingsAPI.Communication/ServerApi/IServerApiService.cs create mode 100644 srcs/WingsAPI.Communication/ServerApi/Protocol/GameChannelType.cs create mode 100644 srcs/WingsAPI.Communication/ServerApi/Protocol/GetAct4ChannelInfoRequest.cs create mode 100644 srcs/WingsAPI.Communication/ServerApi/Protocol/GetChannelInfoRequest.cs create mode 100644 srcs/WingsAPI.Communication/ServerApi/Protocol/GetChannelInfoResponse.cs create mode 100644 srcs/WingsAPI.Communication/ServerApi/Protocol/PulseWorldServerRequest.cs create mode 100644 srcs/WingsAPI.Communication/ServerApi/Protocol/RegisterLoginResponse.cs create mode 100644 srcs/WingsAPI.Communication/ServerApi/Protocol/RegisterWorldRequest.cs create mode 100644 srcs/WingsAPI.Communication/ServerApi/Protocol/RegisterWorldResponse.cs create mode 100644 srcs/WingsAPI.Communication/ServerApi/Protocol/RegisterWorldServerRequest.cs create mode 100644 srcs/WingsAPI.Communication/ServerApi/Protocol/RetrieveRegisteredWorldServersRequest.cs create mode 100644 srcs/WingsAPI.Communication/ServerApi/Protocol/RetrieveRegisteredWorldServersResponse.cs create mode 100644 srcs/WingsAPI.Communication/ServerApi/Protocol/SerializableWorldServer.cs create mode 100644 srcs/WingsAPI.Communication/ServerApi/Protocol/SetMaintenanceRequest.cs create mode 100644 srcs/WingsAPI.Communication/ServerApi/Protocol/SetWorldServerVisibilityRequest.cs create mode 100644 srcs/WingsAPI.Communication/ServerApi/Protocol/ShutdownRequest.cs create mode 100644 srcs/WingsAPI.Communication/ServerApi/Protocol/UnregisterWorldServerRequest.cs create mode 100644 srcs/WingsAPI.Communication/ServerApi/WorldServerShutdownMessage.cs create mode 100644 srcs/WingsAPI.Communication/Services/IClusterStatusService.cs create mode 100644 srcs/WingsAPI.Communication/Services/Messages/ServiceDownMessage.cs create mode 100644 srcs/WingsAPI.Communication/Services/Messages/ServiceFlushAllMessage.cs create mode 100644 srcs/WingsAPI.Communication/Services/Messages/ServiceKickAllMessage.cs create mode 100644 srcs/WingsAPI.Communication/Services/Messages/ServiceMaintenanceNotificationMessage.cs create mode 100644 srcs/WingsAPI.Communication/Services/Requests/ServiceBasicRequest.cs create mode 100644 srcs/WingsAPI.Communication/Services/Requests/ServiceExecuteGeneralEmergencyMaintenanceRequest.cs create mode 100644 srcs/WingsAPI.Communication/Services/Requests/ServiceScheduleGeneralMaintenanceRequest.cs create mode 100644 srcs/WingsAPI.Communication/Services/Responses/ServiceGetAllResponse.cs create mode 100644 srcs/WingsAPI.Communication/Services/Responses/ServiceGetStatusByNameResponse.cs create mode 100644 srcs/WingsAPI.Communication/Services/Service.cs create mode 100644 srcs/WingsAPI.Communication/Services/ServiceHealthStatus.cs create mode 100644 srcs/WingsAPI.Communication/Sessions/ISessionService.cs create mode 100644 srcs/WingsAPI.Communication/Sessions/Model/Session.cs create mode 100644 srcs/WingsAPI.Communication/Sessions/Model/SessionState.cs create mode 100644 srcs/WingsAPI.Communication/Sessions/Request/ActivateCrossChannelAuthenticationRequest.cs create mode 100644 srcs/WingsAPI.Communication/Sessions/Request/ConnectCharacterRequest.cs create mode 100644 srcs/WingsAPI.Communication/Sessions/Request/ConnectToLoginServerRequest.cs create mode 100644 srcs/WingsAPI.Communication/Sessions/Request/ConnectToWorldServerRequest.cs create mode 100644 srcs/WingsAPI.Communication/Sessions/Request/CreateSessionRequest.cs create mode 100644 srcs/WingsAPI.Communication/Sessions/Request/DisconnectSessionRequest.cs create mode 100644 srcs/WingsAPI.Communication/Sessions/Request/GetSessionByAccountIdRequest.cs create mode 100644 srcs/WingsAPI.Communication/Sessions/Request/GetSessionByAccountNameRequest.cs create mode 100644 srcs/WingsAPI.Communication/Sessions/Request/GetSessionByIdRequest.cs create mode 100644 srcs/WingsAPI.Communication/Sessions/Request/PulseRequest.cs create mode 100644 srcs/WingsAPI.Communication/Sessions/Response/SessionResponse.cs create mode 100644 srcs/WingsAPI.Communication/Translations/TranslationService.cs create mode 100644 srcs/WingsAPI.Communication/Translations/TranslationsRefreshMessage.cs create mode 100644 srcs/WingsAPI.Communication/WingsAPI.Communication.csproj create mode 100644 srcs/WingsAPI.Data/Account/AccountBanDto.cs create mode 100644 srcs/WingsAPI.Data/Account/AccountDTO.cs create mode 100644 srcs/WingsAPI.Data/Account/AccountLanguage.cs create mode 100644 srcs/WingsAPI.Data/Account/AccountPenaltyDto.cs create mode 100644 srcs/WingsAPI.Data/Account/AccountWarehouseItemDto.cs create mode 100644 srcs/WingsAPI.Data/Account/AuthorityType.cs create mode 100644 srcs/WingsAPI.Data/Account/IAccountBanDao.cs create mode 100644 srcs/WingsAPI.Data/Account/IAccountDAO.cs create mode 100644 srcs/WingsAPI.Data/Account/IAccountPenaltyDao.cs create mode 100644 srcs/WingsAPI.Data/ActDesc/ActDescDTO.cs create mode 100644 srcs/WingsAPI.Data/BCards/BCardDTO.cs create mode 100644 srcs/WingsAPI.Data/BCards/BCardNpcMonsterTriggerType.cs create mode 100644 srcs/WingsAPI.Data/BCards/BCardNpcTriggerType.cs create mode 100644 srcs/WingsAPI.Data/Bazaar/BazaarItemDTO.cs create mode 100644 srcs/WingsAPI.Data/Bazaar/IBazaarItemDAO.cs create mode 100644 srcs/WingsAPI.Data/Bonus/CharacterStaticBonusDto.cs create mode 100644 srcs/WingsAPI.Data/Bonus/StaticBonusType.cs create mode 100644 srcs/WingsAPI.Data/Buffs/CardDTO.cs create mode 100644 srcs/WingsAPI.Data/Buffs/CharacterStaticBuffDto.cs create mode 100644 srcs/WingsAPI.Data/Character/CharacterDTO.cs create mode 100644 srcs/WingsAPI.Data/Character/CharacterLifetimeStatsDto.cs create mode 100644 srcs/WingsAPI.Data/Character/CharacterRaidRestrictionDto.cs create mode 100644 srcs/WingsAPI.Data/Character/ICharacterDAO.cs create mode 100644 srcs/WingsAPI.Data/Character/RainbowBattleLeaverBusterDto.cs create mode 100644 srcs/WingsAPI.Data/Drops/DropDTO.cs create mode 100644 srcs/WingsAPI.Data/Enums/DeleteResult.cs create mode 100644 srcs/WingsAPI.Data/Exchanges/LogPlayerExchangeItemInfo.cs create mode 100644 srcs/WingsAPI.Data/Families/FamilyAchievementCompletionDto.cs create mode 100644 srcs/WingsAPI.Data/Families/FamilyAchievementProgressDto.cs create mode 100644 srcs/WingsAPI.Data/Families/FamilyAchievementsDto.cs create mode 100644 srcs/WingsAPI.Data/Families/FamilyDTO.cs create mode 100644 srcs/WingsAPI.Data/Families/FamilyLogDTO.cs create mode 100644 srcs/WingsAPI.Data/Families/FamilyMembershipDto.cs create mode 100644 srcs/WingsAPI.Data/Families/FamilyUpgradeDto.cs create mode 100644 srcs/WingsAPI.Data/Families/FamilyUpgradeState.cs create mode 100644 srcs/WingsAPI.Data/Families/FamilyUpgradeType.cs create mode 100644 srcs/WingsAPI.Data/Families/FamilyWarehouseItemDto.cs create mode 100644 srcs/WingsAPI.Data/Families/FamilyWarehouseLogEntryDto.cs create mode 100644 srcs/WingsAPI.Data/Families/IFamilyDAO.cs create mode 100644 srcs/WingsAPI.Data/Families/IFamilyLogDAO.cs create mode 100644 srcs/WingsAPI.Data/Families/IFamilyMembershipDao.cs create mode 100644 srcs/WingsAPI.Data/Families/IFamilyWarehouseItemDAO.cs create mode 100644 srcs/WingsAPI.Data/Families/IFamilyWarehouseLogDao.cs create mode 100644 srcs/WingsAPI.Data/GameData/IResourceLoader.cs create mode 100644 srcs/WingsAPI.Data/Inventory/CharacterInventoryItemDto.cs create mode 100644 srcs/WingsAPI.Data/Inventory/CharacterPartnerInventoryItemDto.cs create mode 100644 srcs/WingsAPI.Data/Items/EquipmentOptionDTO.cs create mode 100644 srcs/WingsAPI.Data/Items/EquipmentOptionType.cs create mode 100644 srcs/WingsAPI.Data/Items/ItemDTO.cs create mode 100644 srcs/WingsAPI.Data/Items/ItemInstanceDTO.cs create mode 100644 srcs/WingsAPI.Data/Items/ItemInstanceType.cs create mode 100644 srcs/WingsAPI.Data/Mails/AccountMailDto.cs create mode 100644 srcs/WingsAPI.Data/Mails/CharacterMailDTO.cs create mode 100644 srcs/WingsAPI.Data/Mails/CharacterNoteDto.cs create mode 100644 srcs/WingsAPI.Data/Mails/IAccountMailDao.cs create mode 100644 srcs/WingsAPI.Data/Mails/ICharacterMailDao.cs create mode 100644 srcs/WingsAPI.Data/Mails/ICharacterNoteDao.cs create mode 100644 srcs/WingsAPI.Data/Mails/MailGiftType.cs create mode 100644 srcs/WingsAPI.Data/Maps/MapDataDTO.cs create mode 100644 srcs/WingsAPI.Data/Maps/MapFlags.cs create mode 100644 srcs/WingsAPI.Data/Maps/MapMonsterDTO.cs create mode 100644 srcs/WingsAPI.Data/Maps/MapNpcDTO.cs create mode 100644 srcs/WingsAPI.Data/Maps/ServerMapDto.cs create mode 100644 srcs/WingsAPI.Data/Mates/MateDTO.cs create mode 100644 srcs/WingsAPI.Data/Miniland/CharacterMinilandObjectDto.cs create mode 100644 srcs/WingsAPI.Data/NpcMonster/NpcMonsterDTO.cs create mode 100644 srcs/WingsAPI.Data/Quests/CharacterQuestCompletedDto.cs create mode 100644 srcs/WingsAPI.Data/Quests/CharacterQuestDto.cs create mode 100644 srcs/WingsAPI.Data/Quests/CharacterQuestGeneratedReward.cs create mode 100644 srcs/WingsAPI.Data/Quests/CharacterQuestObjectiveDto.cs create mode 100644 srcs/WingsAPI.Data/Quests/CompletedScriptsDto.cs create mode 100644 srcs/WingsAPI.Data/Quests/ITutorialDao.cs create mode 100644 srcs/WingsAPI.Data/Quests/QuestDto.cs create mode 100644 srcs/WingsAPI.Data/Quests/QuestNpcDto.cs create mode 100644 srcs/WingsAPI.Data/Quests/QuestObjectiveDto.cs create mode 100644 srcs/WingsAPI.Data/Quests/QuestPrizeDto.cs create mode 100644 srcs/WingsAPI.Data/Quests/QuestSlotType.cs create mode 100644 srcs/WingsAPI.Data/Quests/TutorialActionType.cs create mode 100644 srcs/WingsAPI.Data/Quests/TutorialDto.cs create mode 100644 srcs/WingsAPI.Data/Quicklist/CharacterQuicklistEntryDto.cs create mode 100644 srcs/WingsAPI.Data/Recipes/RecipeDTO.cs create mode 100644 srcs/WingsAPI.Data/Recipes/RecipeItemDTO.cs create mode 100644 srcs/WingsAPI.Data/Relations/CharacterRelationDTO.cs create mode 100644 srcs/WingsAPI.Data/Relations/ICharacterRelationDAO.cs create mode 100644 srcs/WingsAPI.Data/Respawns/CharacterReturnDto.cs create mode 100644 srcs/WingsAPI.Data/ServerDatas/ItemBoxDto.cs create mode 100644 srcs/WingsAPI.Data/ServerDatas/ItemBoxItemDto.cs create mode 100644 srcs/WingsAPI.Data/ServerDatas/ItemBoxType.cs create mode 100644 srcs/WingsAPI.Data/ServerDatas/PortalDTO.cs create mode 100644 srcs/WingsAPI.Data/ServerDatas/TeleporterDTO.cs create mode 100644 srcs/WingsAPI.Data/Shops/ShopDTO.cs create mode 100644 srcs/WingsAPI.Data/Shops/ShopItemDTO.cs create mode 100644 srcs/WingsAPI.Data/Shops/ShopSkillDTO.cs create mode 100644 srcs/WingsAPI.Data/Skills/CharacterSkillDTO.cs create mode 100644 srcs/WingsAPI.Data/Skills/ComboDTO.cs create mode 100644 srcs/WingsAPI.Data/Skills/NpcMonsterSkillDTO.cs create mode 100644 srcs/WingsAPI.Data/Skills/PartnerSkillDTO.cs create mode 100644 srcs/WingsAPI.Data/Skills/SkillDTO.cs create mode 100644 srcs/WingsAPI.Data/Skills/SkillType.cs create mode 100644 srcs/WingsAPI.Data/Skills/TargetAffectedEntities.cs create mode 100644 srcs/WingsAPI.Data/Skills/TargetType.cs create mode 100644 srcs/WingsAPI.Data/TimeSpace/ITimeSpaceRecordDao.cs create mode 100644 srcs/WingsAPI.Data/TimeSpace/TimeSpaceRecordDto.cs create mode 100644 srcs/WingsAPI.Data/Titles/CharacterTitleDto.cs create mode 100644 srcs/WingsAPI.Data/Titles/TitleDto.cs create mode 100644 srcs/WingsAPI.Data/Translations/GameDataType.cs create mode 100644 srcs/WingsAPI.Data/Warehouse/IAccountWarehouseItemDao.cs create mode 100644 srcs/WingsAPI.Data/Warehouse/PartnerWarehouseItemDto.cs create mode 100644 srcs/WingsAPI.Data/WingsAPI.Data.csproj create mode 100644 srcs/WingsAPI.Game.Extensions/Arena/ArenaExtensions.cs create mode 100644 srcs/WingsAPI.Game.Extensions/Bazaar/BazaarExtensions.cs create mode 100644 srcs/WingsAPI.Game.Extensions/CharacterExtensions/FriendsExtensions.cs create mode 100644 srcs/WingsAPI.Game.Extensions/CharacterExtensions/ManagerExtensions.cs create mode 100644 srcs/WingsAPI.Game.Extensions/CharacterExtensions/PlayerEntityExtensions.cs create mode 100644 srcs/WingsAPI.Game.Extensions/Families/FamilyEventExtensions.cs create mode 100644 srcs/WingsAPI.Game.Extensions/Families/FamilyPacketExtensions.cs create mode 100644 srcs/WingsAPI.Game.Extensions/Families/FamilyWarehouseExtensions.cs create mode 100644 srcs/WingsAPI.Game.Extensions/Families/FamilyWarehousePacketExtension.cs create mode 100644 srcs/WingsAPI.Game.Extensions/Groups/GroupPacketExtensions.cs create mode 100644 srcs/WingsAPI.Game.Extensions/Inventory/InventoryPacketExtensions.cs create mode 100644 srcs/WingsAPI.Game.Extensions/ItemExtension/BuyExtension.cs create mode 100644 srcs/WingsAPI.Game.Extensions/ItemExtension/ExchangeExtension.cs create mode 100644 srcs/WingsAPI.Game.Extensions/ItemExtension/InventoryExtension.cs create mode 100644 srcs/WingsAPI.Game.Extensions/ItemExtension/ItemExtension.cs create mode 100644 srcs/WingsAPI.Game.Extensions/ManagerExtensions.cs create mode 100644 srcs/WingsAPI.Game.Extensions/MinilandExtensions/MinigameExtensions.cs create mode 100644 srcs/WingsAPI.Game.Extensions/MinilandExtensions/MinigamePacketExtensions.cs create mode 100644 srcs/WingsAPI.Game.Extensions/MinilandExtensions/MinigameUtilitiesExtensions.cs create mode 100644 srcs/WingsAPI.Game.Extensions/MinilandExtensions/MinilandPacketExtensions.cs create mode 100644 srcs/WingsAPI.Game.Extensions/MinilandExtensions/MinilandUtilitiesExtensions.cs create mode 100644 srcs/WingsAPI.Game.Extensions/PacketGeneration/DelayPacketsExtensions.cs create mode 100644 srcs/WingsAPI.Game.Extensions/PacketGeneration/InfoPacketsExtensions.cs create mode 100644 srcs/WingsAPI.Game.Extensions/PacketGeneration/LoginPacketsExtensions.cs create mode 100644 srcs/WingsAPI.Game.Extensions/PacketGeneration/MailPacketsExtensions.cs create mode 100644 srcs/WingsAPI.Game.Extensions/PacketGeneration/MapExtensions.cs create mode 100644 srcs/WingsAPI.Game.Extensions/PacketGeneration/ModalPacketsExtensions.cs create mode 100644 srcs/WingsAPI.Game.Extensions/PacketGeneration/NosBazaarPacketsExtensions.cs create mode 100644 srcs/WingsAPI.Game.Extensions/PacketGeneration/NpcPacketsExtensions.cs create mode 100644 srcs/WingsAPI.Game.Extensions/Quests/QuestExtensions.cs create mode 100644 srcs/WingsAPI.Game.Extensions/Quests/QuestScriptExtensions.cs create mode 100644 srcs/WingsAPI.Game.Extensions/Quicklist/QuicklistPacketExtensions.cs create mode 100644 srcs/WingsAPI.Game.Extensions/RelationsExtensions/RelationsExtensions.cs create mode 100644 srcs/WingsAPI.Game.Extensions/Skills/LearningSkillsExtensions.cs create mode 100644 srcs/WingsAPI.Game.Extensions/Warehouse/WarehousePacketExtensions.cs create mode 100644 srcs/WingsAPI.Game.Extensions/WingsAPI.Game.Extensions.csproj create mode 100644 srcs/WingsAPI.Game/Account.cs create mode 100644 srcs/WingsAPI.Game/Act4/Configuration/Act4Configuration.cs create mode 100644 srcs/WingsAPI.Game/Act4/Configuration/Act4DungeonsConfiguration.cs create mode 100644 srcs/WingsAPI.Game/Act4/DungeonInstance.cs create mode 100644 srcs/WingsAPI.Game/Act4/DungeonInstanceWrapper.cs create mode 100644 srcs/WingsAPI.Game/Act4/DungeonSubInstance.cs create mode 100644 srcs/WingsAPI.Game/Act4/DungeonType.cs create mode 100644 srcs/WingsAPI.Game/Act4/Entities/CalvinasDragon.cs create mode 100644 srcs/WingsAPI.Game/Act4/Entities/HatusHeads.cs create mode 100644 srcs/WingsAPI.Game/Act4/Event/Act4DungeonBossMapCleanUpEvent.cs create mode 100644 srcs/WingsAPI.Game/Act4/Event/Act4DungeonBroadcastBossClosedEvent.cs create mode 100644 srcs/WingsAPI.Game/Act4/Event/Act4DungeonBroadcastBossOpenEvent.cs create mode 100644 srcs/WingsAPI.Game/Act4/Event/Act4DungeonBroadcastPacketEvent.cs create mode 100644 srcs/WingsAPI.Game/Act4/Event/Act4DungeonEnterEvent.cs create mode 100644 srcs/WingsAPI.Game/Act4/Event/Act4DungeonRewardEvent.cs create mode 100644 srcs/WingsAPI.Game/Act4/Event/Act4DungeonStartedEvent.cs create mode 100644 srcs/WingsAPI.Game/Act4/Event/Act4DungeonStopEvent.cs create mode 100644 srcs/WingsAPI.Game/Act4/Event/Act4DungeonSystemStartEvent.cs create mode 100644 srcs/WingsAPI.Game/Act4/Event/Act4DungeonSystemStopEvent.cs create mode 100644 srcs/WingsAPI.Game/Act4/Event/Act4FactionPointsGenerationEvent.cs create mode 100644 srcs/WingsAPI.Game/Act4/Event/Act4FactionPointsIncreaseEvent.cs create mode 100644 srcs/WingsAPI.Game/Act4/Event/Act4FamilyDungeonWonEvent.cs create mode 100644 srcs/WingsAPI.Game/Act4/Event/Act4MukrajuDeathEvent.cs create mode 100644 srcs/WingsAPI.Game/Act4/Event/Act4MukrajuDespawnEvent.cs create mode 100644 srcs/WingsAPI.Game/Act4/Event/Act4MukrajuSpawnEvent.cs create mode 100644 srcs/WingsAPI.Game/Act4/Event/Act4PutFlagEvent.cs create mode 100644 srcs/WingsAPI.Game/Act4/Event/Act4SystemFcBroadcastEvent.cs create mode 100644 srcs/WingsAPI.Game/Act4/IAct4DungeonManager.cs create mode 100644 srcs/WingsAPI.Game/Act4/IAct4FlagManager.cs create mode 100644 srcs/WingsAPI.Game/Act4/IDungeonFactory.cs create mode 100644 srcs/WingsAPI.Game/Act4/IDungeonManager.cs create mode 100644 srcs/WingsAPI.Game/Act5/Act5OpenNpcRunEvent.cs create mode 100644 srcs/WingsAPI.Game/Algorithm/Events/GenerateExperienceEvent.cs create mode 100644 srcs/WingsAPI.Game/Algorithm/IBattleEntityAlgorithmService.cs create mode 100644 srcs/WingsAPI.Game/Algorithm/ICellonGenerationAlgorithm.cs create mode 100644 srcs/WingsAPI.Game/Algorithm/ICharacterAlgorithm.cs create mode 100644 srcs/WingsAPI.Game/Algorithm/IDamageAlgorithm.cs create mode 100644 srcs/WingsAPI.Game/Algorithm/IShellGenerationAlgorithm.cs create mode 100644 srcs/WingsAPI.Game/Arena/IArenaManager.cs create mode 100644 srcs/WingsAPI.Game/BCard/BCardComponent.cs create mode 100644 srcs/WingsAPI.Game/BCard/BCardExtension.cs create mode 100644 srcs/WingsAPI.Game/BCard/BCardTriggerType.cs create mode 100644 srcs/WingsAPI.Game/BCard/BCardType.cs create mode 100644 srcs/WingsAPI.Game/BCard/IBCardComponent.cs create mode 100644 srcs/WingsAPI.Game/BCard/IBCardEffectAsyncHandler.cs create mode 100644 srcs/WingsAPI.Game/BCard/IBCardEffectContext.cs create mode 100644 srcs/WingsAPI.Game/BCard/IBCardEffectHandlerContainer.cs create mode 100644 srcs/WingsAPI.Game/BCard/IBCardEventContextFactory.cs create mode 100644 srcs/WingsAPI.Game/BCard/StaticBCardEffectHandlerService.cs create mode 100644 srcs/WingsAPI.Game/Battle/BattleEntityDumpExtensions.cs create mode 100644 srcs/WingsAPI.Game/Battle/BattleEntitySharedExtensions.cs create mode 100644 srcs/WingsAPI.Game/Battle/BattleExecuteSkillEvent.cs create mode 100644 srcs/WingsAPI.Game/Battle/BuffProcessable.cs create mode 100644 srcs/WingsAPI.Game/Battle/BuffRequest.cs create mode 100644 srcs/WingsAPI.Game/Battle/CastingComponent.cs create mode 100644 srcs/WingsAPI.Game/Battle/ComboState.cs create mode 100644 srcs/WingsAPI.Game/Battle/ElementType.cs create mode 100644 srcs/WingsAPI.Game/Battle/Event/ApplyHitEvent.cs create mode 100644 srcs/WingsAPI.Game/Battle/Event/EntityDamageEvent.cs create mode 100644 srcs/WingsAPI.Game/Battle/Event/ProcessBuffEvent.cs create mode 100644 srcs/WingsAPI.Game/Battle/Event/ProcessHitEvent.cs create mode 100644 srcs/WingsAPI.Game/Battle/HitInformation.cs create mode 100644 srcs/WingsAPI.Game/Battle/HitProcessable.cs create mode 100644 srcs/WingsAPI.Game/Battle/HitRequest.cs create mode 100644 srcs/WingsAPI.Game/Battle/HostilityType.cs create mode 100644 srcs/WingsAPI.Game/Battle/IBattleEntityDump.cs create mode 100644 srcs/WingsAPI.Game/Battle/MTListHitTarget.cs create mode 100644 srcs/WingsAPI.Game/Battle/Managers/BattleEntityDumpFactory.cs create mode 100644 srcs/WingsAPI.Game/Battle/Managers/ChargeManager.cs create mode 100644 srcs/WingsAPI.Game/Battle/Managers/IBattleEntityDumpFactory.cs create mode 100644 srcs/WingsAPI.Game/Battle/Managers/IChargeManager.cs create mode 100644 srcs/WingsAPI.Game/Battle/Managers/IMeditationManager.cs create mode 100644 srcs/WingsAPI.Game/Battle/Managers/IPhantomPositionManager.cs create mode 100644 srcs/WingsAPI.Game/Battle/Managers/ISacrificeManager.cs create mode 100644 srcs/WingsAPI.Game/Battle/Managers/ISkillExecutor.cs create mode 100644 srcs/WingsAPI.Game/Battle/Managers/ISkillUsageManager.cs create mode 100644 srcs/WingsAPI.Game/Battle/Managers/ITeleportManager.cs create mode 100644 srcs/WingsAPI.Game/Battle/Managers/MeditationManager.cs create mode 100644 srcs/WingsAPI.Game/Battle/Managers/SkillExecutor.cs create mode 100644 srcs/WingsAPI.Game/Battle/Managers/SkillUsageManager.cs create mode 100644 srcs/WingsAPI.Game/Battle/Managers/StaticSkillExecutor.cs create mode 100644 srcs/WingsAPI.Game/Battle/Managers/TeleportManager.cs create mode 100644 srcs/WingsAPI.Game/Battle/MateBattleEntityDump.cs create mode 100644 srcs/WingsAPI.Game/Battle/NpcMonsterEntityDump.cs create mode 100644 srcs/WingsAPI.Game/Battle/PlayerBattleEntityDump.cs create mode 100644 srcs/WingsAPI.Game/Battle/SacrificeManager.cs create mode 100644 srcs/WingsAPI.Game/Battle/SkillInfo.cs create mode 100644 srcs/WingsAPI.Game/Bazaar/BazaarItem.cs create mode 100644 srcs/WingsAPI.Game/Bazaar/Configuration/BazaarConfiguration.cs create mode 100644 srcs/WingsAPI.Game/Bazaar/Events/BazaarGetListedItemsEvent.cs create mode 100644 srcs/WingsAPI.Game/Bazaar/Events/BazaarItemAddEvent.cs create mode 100644 srcs/WingsAPI.Game/Bazaar/Events/BazaarItemAddedEvent.cs create mode 100644 srcs/WingsAPI.Game/Bazaar/Events/BazaarItemBoughtEvent.cs create mode 100644 srcs/WingsAPI.Game/Bazaar/Events/BazaarItemBuyEvent.cs create mode 100644 srcs/WingsAPI.Game/Bazaar/Events/BazaarItemChangePriceEvent.cs create mode 100644 srcs/WingsAPI.Game/Bazaar/Events/BazaarItemExpiredEvent.cs create mode 100644 srcs/WingsAPI.Game/Bazaar/Events/BazaarItemInsertedEvent.cs create mode 100644 srcs/WingsAPI.Game/Bazaar/Events/BazaarItemRemoveEvent.cs create mode 100644 srcs/WingsAPI.Game/Bazaar/Events/BazaarItemRemovedEvent.cs create mode 100644 srcs/WingsAPI.Game/Bazaar/Events/BazaarItemWithdrawnEvent.cs create mode 100644 srcs/WingsAPI.Game/Bazaar/Events/BazaarOpenUiEvent.cs create mode 100644 srcs/WingsAPI.Game/Bazaar/Events/BazaarSearchItemsEvent.cs create mode 100644 srcs/WingsAPI.Game/Bazaar/IBazaarManager.cs create mode 100644 srcs/WingsAPI.Game/Buffs/Buff.cs create mode 100644 srcs/WingsAPI.Game/Buffs/BuffComponent.cs create mode 100644 srcs/WingsAPI.Game/Buffs/BuffFactory.cs create mode 100644 srcs/WingsAPI.Game/Buffs/BuffFactoryExtensions.cs create mode 100644 srcs/WingsAPI.Game/Buffs/BuffsDurationConfiguration.cs create mode 100644 srcs/WingsAPI.Game/Buffs/Card.cs create mode 100644 srcs/WingsAPI.Game/Buffs/EquipmentOptionContainer.cs create mode 100644 srcs/WingsAPI.Game/Buffs/Events/AngelSpecialistElementalBuffEvent.cs create mode 100644 srcs/WingsAPI.Game/Buffs/Events/BuffAddEvent.cs create mode 100644 srcs/WingsAPI.Game/Buffs/Events/BuffPartnerCheckEvent.cs create mode 100644 srcs/WingsAPI.Game/Buffs/Events/BuffRemoveEvent.cs create mode 100644 srcs/WingsAPI.Game/Buffs/IBuffComponent.cs create mode 100644 srcs/WingsAPI.Game/Buffs/IBuffFactory.cs create mode 100644 srcs/WingsAPI.Game/Buffs/IEquipmentOptionContainer.cs create mode 100644 srcs/WingsAPI.Game/Buffs/StaticBuffFactory.cs create mode 100644 srcs/WingsAPI.Game/Buffs/StaticMeditationManager.cs create mode 100644 srcs/WingsAPI.Game/Characters/BasicPlayerDump.cs create mode 100644 srcs/WingsAPI.Game/Characters/Events/AddExpEvent.cs create mode 100644 srcs/WingsAPI.Game/Characters/Events/AddStaticBonusEvent.cs create mode 100644 srcs/WingsAPI.Game/Characters/Events/BankOpenEvent.cs create mode 100644 srcs/WingsAPI.Game/Characters/Events/BattleEntityHealEvent.cs create mode 100644 srcs/WingsAPI.Game/Characters/Events/BoxOpenedEvent.cs create mode 100644 srcs/WingsAPI.Game/Characters/Events/CellonUpgradeEvent.cs create mode 100644 srcs/WingsAPI.Game/Characters/Events/CellonUpgradedEvent.cs create mode 100644 srcs/WingsAPI.Game/Characters/Events/ChangeClassEvent.cs create mode 100644 srcs/WingsAPI.Game/Characters/Events/ChangeFactionEvent.cs create mode 100644 srcs/WingsAPI.Game/Characters/Events/CharacterBonusExpiredEvent.cs create mode 100644 srcs/WingsAPI.Game/Characters/Events/CharacterLoadEvent.cs create mode 100644 srcs/WingsAPI.Game/Characters/Events/CharacterRemoveManagersEvent.cs create mode 100644 srcs/WingsAPI.Game/Characters/Events/GamblingEvent.cs create mode 100644 srcs/WingsAPI.Game/Characters/Events/GenerateGoldEvent.cs create mode 100644 srcs/WingsAPI.Game/Characters/Events/GenerateReputationEvent.cs create mode 100644 srcs/WingsAPI.Game/Characters/Events/GetDefaultMorphEvent.cs create mode 100644 srcs/WingsAPI.Game/Characters/Events/InvitationEvent.cs create mode 100644 srcs/WingsAPI.Game/Characters/Events/InviteJoinMinilandEvent.cs create mode 100644 srcs/WingsAPI.Game/Characters/Events/ItemGambledEvent.cs create mode 100644 srcs/WingsAPI.Game/Characters/Events/ItemSummedEvent.cs create mode 100644 srcs/WingsAPI.Game/Characters/Events/ItemUpgradedEvent.cs create mode 100644 srcs/WingsAPI.Game/Characters/Events/KillBonusEvent.cs create mode 100644 srcs/WingsAPI.Game/Characters/Events/LevelUpEvent.cs create mode 100644 srcs/WingsAPI.Game/Characters/Events/LevelUpMateEvent.cs create mode 100644 srcs/WingsAPI.Game/Characters/Events/MinilandSignPostJoinEvent.cs create mode 100644 srcs/WingsAPI.Game/Characters/Events/MonsterCaptureEvent.cs create mode 100644 srcs/WingsAPI.Game/Characters/Events/NormalChatEvent.cs create mode 100644 srcs/WingsAPI.Game/Characters/Events/PlayerChangeChannelAct4Event.cs create mode 100644 srcs/WingsAPI.Game/Characters/Events/PlayerChangeChannelEvent.cs create mode 100644 srcs/WingsAPI.Game/Characters/Events/PlayerDeathEvent.cs create mode 100644 srcs/WingsAPI.Game/Characters/Events/PlayerRestEvent.cs create mode 100644 srcs/WingsAPI.Game/Characters/Events/PlayerReturnFromAct4Event.cs create mode 100644 srcs/WingsAPI.Game/Characters/Events/RemoveAdditionalHpMpEvent.cs create mode 100644 srcs/WingsAPI.Game/Characters/Events/RemoveItemTimeEvent.cs create mode 100644 srcs/WingsAPI.Game/Characters/Events/RollItemBoxEvent.cs create mode 100644 srcs/WingsAPI.Game/Characters/Events/ShellIdentifiedEvent.cs create mode 100644 srcs/WingsAPI.Game/Characters/Events/SpPerfectedEvent.cs create mode 100644 srcs/WingsAPI.Game/Characters/Events/SpTransformEvent.cs create mode 100644 srcs/WingsAPI.Game/Characters/Events/SpUntransformEvent.cs create mode 100644 srcs/WingsAPI.Game/Characters/Events/SpUpgradedEvent.cs create mode 100644 srcs/WingsAPI.Game/Characters/Events/SpecialistRefreshEvent.cs create mode 100644 srcs/WingsAPI.Game/Characters/Events/SpeedBoosterEvent.cs create mode 100644 srcs/WingsAPI.Game/Characters/Events/StrangeBehaviorEvent.cs create mode 100644 srcs/WingsAPI.Game/Characters/Events/StrangeBehaviorSeverity.cs create mode 100644 srcs/WingsAPI.Game/Characters/Events/UpgradeItemEvent.cs create mode 100644 srcs/WingsAPI.Game/Characters/Events/VehicleCheckMapSpeedEvent.cs create mode 100644 srcs/WingsAPI.Game/Characters/Events/VehicleRemoveEvent.cs create mode 100644 srcs/WingsAPI.Game/Characters/IPlayerEntity.cs create mode 100644 srcs/WingsAPI.Game/Characters/IPlayerEntityFactory.cs create mode 100644 srcs/WingsAPI.Game/Characters/LastWalk.cs create mode 100644 srcs/WingsAPI.Game/Characters/PlayerEntity.Family.cs create mode 100644 srcs/WingsAPI.Game/Characters/PlayerEntity.Revival.cs create mode 100644 srcs/WingsAPI.Game/Characters/PlayerEntity.Skills.cs create mode 100644 srcs/WingsAPI.Game/Characters/PlayerEntity.Stats.cs create mode 100644 srcs/WingsAPI.Game/Characters/PlayerEntity.cs create mode 100644 srcs/WingsAPI.Game/Chat/ChatGenericEvent.cs create mode 100644 srcs/WingsAPI.Game/Chat/ChatMessageReceivedEvent.cs create mode 100644 srcs/WingsAPI.Game/Chat/ChatSendFriendMessageEvent.cs create mode 100644 srcs/WingsAPI.Game/Chat/ChatSpeakerEvent.cs create mode 100644 srcs/WingsAPI.Game/Chat/ChatType.cs create mode 100644 srcs/WingsAPI.Game/Chat/FamilyChatReceivedEvent.cs create mode 100644 srcs/WingsAPI.Game/Chat/FriendChatReceivedEvent.cs create mode 100644 srcs/WingsAPI.Game/Chat/GlobalPlayerChatReceivedEvent.cs create mode 100644 srcs/WingsAPI.Game/Cheats/CheatComponent.cs create mode 100644 srcs/WingsAPI.Game/Cheats/ICheatComponent.cs create mode 100644 srcs/WingsAPI.Game/Commands/IGlobalCommandExecutor.cs create mode 100644 srcs/WingsAPI.Game/Compliments/ComplimentsMonthlyRefreshEvent.cs create mode 100644 srcs/WingsAPI.Game/Compliments/IComplimentsManager.cs create mode 100644 srcs/WingsAPI.Game/Configurations/Act5NpcRunCraftItemConfig.cs create mode 100644 srcs/WingsAPI.Game/Configurations/BankReputationConfiguration.cs create mode 100644 srcs/WingsAPI.Game/Configurations/BuffsToRemoveConfiguration.cs create mode 100644 srcs/WingsAPI.Game/Configurations/ChestDropItemConfiguration.cs create mode 100644 srcs/WingsAPI.Game/Configurations/GameMinMaxConfiguration.cs create mode 100644 srcs/WingsAPI.Game/Configurations/GameRateConfiguration.cs create mode 100644 srcs/WingsAPI.Game/Configurations/GameRevivalConfiguration.cs create mode 100644 srcs/WingsAPI.Game/Configurations/GeneralQuestsConfiguration.cs create mode 100644 srcs/WingsAPI.Game/Configurations/GibberishConfiguration.cs create mode 100644 srcs/WingsAPI.Game/Configurations/MateRevivalConfiguration.cs create mode 100644 srcs/WingsAPI.Game/Configurations/Miniland/AntiExploitConfiguration.cs create mode 100644 srcs/WingsAPI.Game/Configurations/Miniland/ForcedPlacing.cs create mode 100644 srcs/WingsAPI.Game/Configurations/Miniland/GlobalMinigameConfiguration.cs create mode 100644 srcs/WingsAPI.Game/Configurations/Miniland/Minigame.cs create mode 100644 srcs/WingsAPI.Game/Configurations/Miniland/MinigameConfiguration.cs create mode 100644 srcs/WingsAPI.Game/Configurations/Miniland/MinigameReward.cs create mode 100644 srcs/WingsAPI.Game/Configurations/Miniland/MinigameRewards.cs create mode 100644 srcs/WingsAPI.Game/Configurations/Miniland/MinigameScoresHolder.cs create mode 100644 srcs/WingsAPI.Game/Configurations/Miniland/MinigameType.cs create mode 100644 srcs/WingsAPI.Game/Configurations/Miniland/Miniland.cs create mode 100644 srcs/WingsAPI.Game/Configurations/Miniland/MinilandConfiguration.cs create mode 100644 srcs/WingsAPI.Game/Configurations/Miniland/RestrictedZone.cs create mode 100644 srcs/WingsAPI.Game/Configurations/Miniland/RestrictionType.cs create mode 100644 srcs/WingsAPI.Game/Configurations/Miniland/RewardLevel.cs create mode 100644 srcs/WingsAPI.Game/Configurations/Miniland/ScoreHolder.cs create mode 100644 srcs/WingsAPI.Game/Configurations/Miniland/SerializablePosition.cs create mode 100644 srcs/WingsAPI.Game/Configurations/MonsterTalkingConfiguration.cs create mode 100644 srcs/WingsAPI.Game/Configurations/NpcRunTypeQuestsConfiguration.cs create mode 100644 srcs/WingsAPI.Game/Configurations/PartnerSpecialistBasicConfiguration.cs create mode 100644 srcs/WingsAPI.Game/Configurations/PerfUpgradeConfiguration.cs create mode 100644 srcs/WingsAPI.Game/Configurations/PeriodicQuestsConfiguration.cs create mode 100644 srcs/WingsAPI.Game/Configurations/PlayerRevivalConfiguration.cs create mode 100644 srcs/WingsAPI.Game/Configurations/PlayerRevivalPenalization.cs create mode 100644 srcs/WingsAPI.Game/Configurations/QuestTeleportDialogConfiguration.cs create mode 100644 srcs/WingsAPI.Game/Configurations/QuestsRatesConfiguration.cs create mode 100644 srcs/WingsAPI.Game/Configurations/RainbowBattleConfiguration.cs create mode 100644 srcs/WingsAPI.Game/Configurations/RelictConfiguration.cs create mode 100644 srcs/WingsAPI.Game/Configurations/ReputationConfiguration.cs create mode 100644 srcs/WingsAPI.Game/Configurations/ReturnDefaultConfiguration.cs create mode 100644 srcs/WingsAPI.Game/Configurations/SpPerfStats.cs create mode 100644 srcs/WingsAPI.Game/Configurations/SpPerfectionConfiguration.cs create mode 100644 srcs/WingsAPI.Game/Configurations/SpStoneLink.cs create mode 100644 srcs/WingsAPI.Game/Configurations/SpUpgradeConfiguration.cs create mode 100644 srcs/WingsAPI.Game/Configurations/SpecialItem.cs create mode 100644 srcs/WingsAPI.Game/Configurations/SubActsConfiguration.cs create mode 100644 srcs/WingsAPI.Game/Configurations/TimeSpaceConfiguration.cs create mode 100644 srcs/WingsAPI.Game/Configurations/TimeSpaceNpcRunConfiguration.cs create mode 100644 srcs/WingsAPI.Game/Configurations/UpgradeConfiguration.cs create mode 100644 srcs/WingsAPI.Game/Configurations/UpgradeItemConfiguration.cs create mode 100644 srcs/WingsAPI.Game/Configurations/UpgradeItemStats.cs create mode 100644 srcs/WingsAPI.Game/Configurations/UpgradeNormalItemConfiguration.cs create mode 100644 srcs/WingsAPI.Game/Configurations/UpgradePhenomenalItemConfiguration.cs create mode 100644 srcs/WingsAPI.Game/Core/ECS/IMapSystem.cs create mode 100644 srcs/WingsAPI.Game/Core/ECS/ITickManager.cs create mode 100644 srcs/WingsAPI.Game/Core/ECS/ITickProcessable.cs create mode 100644 srcs/WingsAPI.Game/Core/ECS/Systems/IBattleSystem.cs create mode 100644 srcs/WingsAPI.Game/Core/ECS/Systems/ICharacterSystem.cs create mode 100644 srcs/WingsAPI.Game/Core/ECS/Systems/IDropSystem.cs create mode 100644 srcs/WingsAPI.Game/Core/ECS/Systems/IMateSystem.cs create mode 100644 srcs/WingsAPI.Game/Core/ECS/Systems/IMonsterSystem.cs create mode 100644 srcs/WingsAPI.Game/Core/ECS/Systems/INpcSystem.cs create mode 100644 srcs/WingsAPI.Game/Core/GuriHandling/Event/GuriEvent.cs create mode 100644 srcs/WingsAPI.Game/Core/GuriHandling/IGuriHandler.cs create mode 100644 srcs/WingsAPI.Game/Core/GuriHandling/IGuriHandlerContainer.cs create mode 100644 srcs/WingsAPI.Game/Core/ItemHandling/Configuration/ICostumeScrollConfiguration.cs create mode 100644 srcs/WingsAPI.Game/Core/ItemHandling/Configuration/ISpPartnerConfiguration.cs create mode 100644 srcs/WingsAPI.Game/Core/ItemHandling/Configuration/ISpWingConfiguration.cs create mode 100644 srcs/WingsAPI.Game/Core/ItemHandling/Event/InventoryUseItemEvent.cs create mode 100644 srcs/WingsAPI.Game/Core/ItemHandling/Event/InventoryUsedItemEvent.cs create mode 100644 srcs/WingsAPI.Game/Core/ItemHandling/IItemHandler.cs create mode 100644 srcs/WingsAPI.Game/Core/ItemHandling/IItemHandlerContainer.cs create mode 100644 srcs/WingsAPI.Game/Core/ItemHandling/IItemUsageByVnumHandler.cs create mode 100644 srcs/WingsAPI.Game/Core/Multilanguage/GameDialogKey.cs create mode 100644 srcs/WingsAPI.Game/Core/Multilanguage/GameLanguageServiceExtensions.cs create mode 100644 srcs/WingsAPI.Game/Core/Multilanguage/IGameDataLanguageService.cs create mode 100644 srcs/WingsAPI.Game/Core/Multilanguage/IGameLanguageService.cs create mode 100644 srcs/WingsAPI.Game/Core/Multilanguage/IUserLanguageService.cs create mode 100644 srcs/WingsAPI.Game/Core/Multilanguage/StaticGameLanguageService.cs create mode 100644 srcs/WingsAPI.Game/Core/NpcDialogHandling/Event/NpcDialogEvent.cs create mode 100644 srcs/WingsAPI.Game/Core/NpcDialogHandling/INpcDialogAsyncHandler.cs create mode 100644 srcs/WingsAPI.Game/Core/NpcDialogHandling/INpcDialogHandlerContainer.cs create mode 100644 srcs/WingsAPI.Game/Core/PacketHandling/ICharacterScreenPacketHandler.cs create mode 100644 srcs/WingsAPI.Game/Core/PacketHandling/IGamePacketHandler.cs create mode 100644 srcs/WingsAPI.Game/Core/PacketHandling/IPacketHandler.cs create mode 100644 srcs/WingsAPI.Game/Core/PacketHandling/IPacketHandlerContainer.cs create mode 100644 srcs/WingsAPI.Game/Core/PacketHandling/IUnauthedPacketHandler.cs create mode 100644 srcs/WingsAPI.Game/Core/PacketHandling/PlayerEvent.cs create mode 100644 srcs/WingsAPI.Game/Entities/Event/GenerateEntityDeathEvent.cs create mode 100644 srcs/WingsAPI.Game/Entities/Event/MapJoinMonsterEntityEvent.cs create mode 100644 srcs/WingsAPI.Game/Entities/Event/MapJoinNpcEntityEvent.cs create mode 100644 srcs/WingsAPI.Game/Entities/Event/MapLeaveMonsterEntityEvent.cs create mode 100644 srcs/WingsAPI.Game/Entities/Event/MapLeaveNpcEntityEvent.cs create mode 100644 srcs/WingsAPI.Game/Entities/Extensions/VisualEntitiesExtensions.cs create mode 100644 srcs/WingsAPI.Game/Entities/IAiEntity.cs create mode 100644 srcs/WingsAPI.Game/Entities/IBattleEntity.cs create mode 100644 srcs/WingsAPI.Game/Entities/IBattleEntityEvent.cs create mode 100644 srcs/WingsAPI.Game/Entities/IBattleEntityEventEmitter.cs create mode 100644 srcs/WingsAPI.Game/Entities/IEntity.cs create mode 100644 srcs/WingsAPI.Game/Entities/IGenericEventEmitter.cs create mode 100644 srcs/WingsAPI.Game/Entities/IMonsterAdditionalData.cs create mode 100644 srcs/WingsAPI.Game/Entities/IMonsterData.cs create mode 100644 srcs/WingsAPI.Game/Entities/IMonsterEntity.cs create mode 100644 srcs/WingsAPI.Game/Entities/IMonsterEntityFactory.cs create mode 100644 srcs/WingsAPI.Game/Entities/IMoveableEntity.cs create mode 100644 srcs/WingsAPI.Game/Entities/INpcAdditionalData.cs create mode 100644 srcs/WingsAPI.Game/Entities/INpcEntity.cs create mode 100644 srcs/WingsAPI.Game/Entities/INpcEntityFactory.cs create mode 100644 srcs/WingsAPI.Game/Entities/INpcMonsterEntity.cs create mode 100644 srcs/WingsAPI.Game/Entities/IShopFactory.cs create mode 100644 srcs/WingsAPI.Game/Entities/MonsterEntityEvent.cs create mode 100644 srcs/WingsAPI.Game/Entities/NpcAdditionalData.cs create mode 100644 srcs/WingsAPI.Game/Entities/NpcEntityEvent.cs create mode 100644 srcs/WingsAPI.Game/EntityStatistics/IMateStatisticsComponent.cs create mode 100644 srcs/WingsAPI.Game/EntityStatistics/IPlayerStatisticsComponent.cs create mode 100644 srcs/WingsAPI.Game/EntityStatistics/MateStatisticsComponent.cs create mode 100644 srcs/WingsAPI.Game/EntityStatistics/PlayerStatisticsComponent.cs create mode 100644 srcs/WingsAPI.Game/EntityStatistics/Statistics.cs create mode 100644 srcs/WingsAPI.Game/Exchange/Event/ExchangeCloseEvent.cs create mode 100644 srcs/WingsAPI.Game/Exchange/Event/ExchangeJoinEvent.cs create mode 100644 srcs/WingsAPI.Game/Exchange/Event/ExchangeRegisterEvent.cs create mode 100644 srcs/WingsAPI.Game/Exchange/Event/ExchangeTransferItemsEvent.cs create mode 100644 srcs/WingsAPI.Game/Exchange/Event/TradeRequestedEvent.cs create mode 100644 srcs/WingsAPI.Game/Exchange/ExchangeComponent.cs create mode 100644 srcs/WingsAPI.Game/Exchange/IExchangeComponent.cs create mode 100644 srcs/WingsAPI.Game/Exchange/PlayerExchange.cs create mode 100644 srcs/WingsAPI.Game/Extensions/AlgorithmExtension.cs create mode 100644 srcs/WingsAPI.Game/Extensions/BattleEntityExtension.cs create mode 100644 srcs/WingsAPI.Game/Extensions/BroadcasterExtensions.cs create mode 100644 srcs/WingsAPI.Game/Extensions/BuffExtension.cs create mode 100644 srcs/WingsAPI.Game/Extensions/CharacterExtension.cs create mode 100644 srcs/WingsAPI.Game/Extensions/CharacterPacketExtension.cs create mode 100644 srcs/WingsAPI.Game/Extensions/CollectionExtension.cs create mode 100644 srcs/WingsAPI.Game/Extensions/ConcurrentBagExtension.cs create mode 100644 srcs/WingsAPI.Game/Extensions/DamageExtension.cs create mode 100644 srcs/WingsAPI.Game/Extensions/DateTimeExtension.cs create mode 100644 srcs/WingsAPI.Game/Extensions/DictionaryExtension.cs create mode 100644 srcs/WingsAPI.Game/Extensions/EncodingExtensions.cs create mode 100644 srcs/WingsAPI.Game/Extensions/EquipmentOptionExtensions.cs create mode 100644 srcs/WingsAPI.Game/Extensions/Lists.cs create mode 100644 srcs/WingsAPI.Game/Extensions/MapItemExtension.cs create mode 100644 srcs/WingsAPI.Game/Extensions/Mates/MateExtensions.cs create mode 100644 srcs/WingsAPI.Game/Extensions/Mates/MatePacketExtensions.cs create mode 100644 srcs/WingsAPI.Game/Extensions/Mates/MateStatisticsExtensions.cs create mode 100644 srcs/WingsAPI.Game/Extensions/NpcMonsterExtension.cs create mode 100644 srcs/WingsAPI.Game/Extensions/RarifyExtension.cs create mode 100644 srcs/WingsAPI.Game/Extensions/SkillExtension.cs create mode 100644 srcs/WingsAPI.Game/Extensions/SpecialistExtension.cs create mode 100644 srcs/WingsAPI.Game/Extensions/StringExtensions.cs create mode 100644 srcs/WingsAPI.Game/Extensions/UiPacketExtension.cs create mode 100644 srcs/WingsAPI.Game/Extensions/WearableInstanceExtensions.cs create mode 100644 srcs/WingsAPI.Game/Families/Configuration/FamilyConfiguration.cs create mode 100644 srcs/WingsAPI.Game/Families/Configuration/FamilyUpgradesConfiguration.cs create mode 100644 srcs/WingsAPI.Game/Families/Configuration/LevelExperience.cs create mode 100644 srcs/WingsAPI.Game/Families/Enum/FamXpObtainedFromType.cs create mode 100644 srcs/WingsAPI.Game/Families/Event/FamilyAddExperienceEvent.cs create mode 100644 srcs/WingsAPI.Game/Families/Event/FamilyAddLogEvent.cs create mode 100644 srcs/WingsAPI.Game/Families/Event/FamilyAddMemberEvent.cs create mode 100644 srcs/WingsAPI.Game/Families/Event/FamilyChangeAuthorityEvent.cs create mode 100644 srcs/WingsAPI.Game/Families/Event/FamilyChangeDeputyEvent.cs create mode 100644 srcs/WingsAPI.Game/Families/Event/FamilyChangeFactionEvent.cs create mode 100644 srcs/WingsAPI.Game/Families/Event/FamilyChangeSettingsEvent.cs create mode 100644 srcs/WingsAPI.Game/Families/Event/FamilyChangeSexEvent.cs create mode 100644 srcs/WingsAPI.Game/Families/Event/FamilyChangeTitleEvent.cs create mode 100644 srcs/WingsAPI.Game/Families/Event/FamilyCreateEvent.cs create mode 100644 srcs/WingsAPI.Game/Families/Event/FamilyCreatedEvent.cs create mode 100644 srcs/WingsAPI.Game/Families/Event/FamilyDisbandEvent.cs create mode 100644 srcs/WingsAPI.Game/Families/Event/FamilyDisbandedEvent.cs create mode 100644 srcs/WingsAPI.Game/Families/Event/FamilyInviteResponseEvent.cs create mode 100644 srcs/WingsAPI.Game/Families/Event/FamilyInvitedEvent.cs create mode 100644 srcs/WingsAPI.Game/Families/Event/FamilyJoinedEvent.cs create mode 100644 srcs/WingsAPI.Game/Families/Event/FamilyKickedMemberEvent.cs create mode 100644 srcs/WingsAPI.Game/Families/Event/FamilyLeaveEvent.cs create mode 100644 srcs/WingsAPI.Game/Families/Event/FamilyLeftEvent.cs create mode 100644 srcs/WingsAPI.Game/Families/Event/FamilyListMembersEvent.cs create mode 100644 srcs/WingsAPI.Game/Families/Event/FamilyMessageSentEvent.cs create mode 100644 srcs/WingsAPI.Game/Families/Event/FamilyNoticeMessageEvent.cs create mode 100644 srcs/WingsAPI.Game/Families/Event/FamilyReceiveInviteEvent.cs create mode 100644 srcs/WingsAPI.Game/Families/Event/FamilyRemoveMemberEvent.cs create mode 100644 srcs/WingsAPI.Game/Families/Event/FamilySendInviteEvent.cs create mode 100644 srcs/WingsAPI.Game/Families/Event/FamilyShoutEvent.cs create mode 100644 srcs/WingsAPI.Game/Families/Event/FamilyTodayEvent.cs create mode 100644 srcs/WingsAPI.Game/Families/Event/FamilyUpgradeBoughtEvent.cs create mode 100644 srcs/WingsAPI.Game/Families/Event/FamilyWarehouseAddItemEvent.cs create mode 100644 srcs/WingsAPI.Game/Families/Event/FamilyWarehouseCloseEvent.cs create mode 100644 srcs/WingsAPI.Game/Families/Event/FamilyWarehouseItemPlacedEvent.cs create mode 100644 srcs/WingsAPI.Game/Families/Event/FamilyWarehouseItemWithdrawnEvent.cs create mode 100644 srcs/WingsAPI.Game/Families/Event/FamilyWarehouseLogsOpenEvent.cs create mode 100644 srcs/WingsAPI.Game/Families/Event/FamilyWarehouseMoveItemEvent.cs create mode 100644 srcs/WingsAPI.Game/Families/Event/FamilyWarehouseOpenEvent.cs create mode 100644 srcs/WingsAPI.Game/Families/Event/FamilyWarehouseShowItemEvent.cs create mode 100644 srcs/WingsAPI.Game/Families/Event/FamilyWarehouseWithdrawItemEvent.cs create mode 100644 srcs/WingsAPI.Game/Families/ExperienceGainedSubMessage.cs create mode 100644 srcs/WingsAPI.Game/Families/Family.cs create mode 100644 srcs/WingsAPI.Game/Families/FamilyAchievementsVnum.cs create mode 100644 srcs/WingsAPI.Game/Families/FamilyComponent.cs create mode 100644 srcs/WingsAPI.Game/Families/FamilyMembership.cs create mode 100644 srcs/WingsAPI.Game/Families/FamilyMessageType.cs create mode 100644 srcs/WingsAPI.Game/Families/FamilyMissionVnums.cs create mode 100644 srcs/WingsAPI.Game/Families/FamilyUpgrade.cs create mode 100644 srcs/WingsAPI.Game/Families/FamilyUpgradeBuyFromShopEvent.cs create mode 100644 srcs/WingsAPI.Game/Families/FamilyUpgradeBuyableState.cs create mode 100644 srcs/WingsAPI.Game/Families/IFamily.cs create mode 100644 srcs/WingsAPI.Game/Families/IFamilyComponent.cs create mode 100644 srcs/WingsAPI.Game/Families/IFamilyManager.cs create mode 100644 srcs/WingsAPI.Game/Families/Managers/FamilyAchievementManager.cs create mode 100644 srcs/WingsAPI.Game/Families/Managers/IFamilyAchievementManager.cs create mode 100644 srcs/WingsAPI.Game/Families/Managers/IFamilyMissionManager.cs create mode 100644 srcs/WingsAPI.Game/Families/Messages/FamilyAchievementIncrementMessage.cs create mode 100644 srcs/WingsAPI.Game/Families/Messages/FamilyMissionIncrementMessage.cs create mode 100644 srcs/WingsAPI.Game/Families/StaticFamilyManager.cs create mode 100644 srcs/WingsAPI.Game/Features/GameFeature.cs create mode 100644 srcs/WingsAPI.Game/Features/IGameFeatureToggleManager.cs create mode 100644 srcs/WingsAPI.Game/Features/RedisGameFeatureToggleManager.cs create mode 100644 srcs/WingsAPI.Game/GameEvent/Configuration/IGameEventConfiguration.cs create mode 100644 srcs/WingsAPI.Game/GameEvent/Configuration/IGlobalGameEventConfiguration.cs create mode 100644 srcs/WingsAPI.Game/GameEvent/Event/GameEventInstanceProcessEvent.cs create mode 100644 srcs/WingsAPI.Game/GameEvent/Event/GameEventInstanceStartEvent.cs create mode 100644 srcs/WingsAPI.Game/GameEvent/GameEventType.cs create mode 100644 srcs/WingsAPI.Game/GameEvent/IGameEventInstance.cs create mode 100644 srcs/WingsAPI.Game/GameEvent/IGameEventInstanceManager.cs create mode 100644 srcs/WingsAPI.Game/GameEvent/IGameEventRegistrationManager.cs create mode 100644 srcs/WingsAPI.Game/GameEvent/InstantBattle/InstantBattleWonEvent.cs create mode 100644 srcs/WingsAPI.Game/GameEvent/Matchmaking/Filter/FilterResult.cs create mode 100644 srcs/WingsAPI.Game/GameEvent/Matchmaking/Filter/IMatchmakingFilter.cs create mode 100644 srcs/WingsAPI.Game/GameEvent/Matchmaking/IMatchmaking.cs create mode 100644 srcs/WingsAPI.Game/GameEvent/Matchmaking/Matchmaker/IMatchmaker.cs create mode 100644 srcs/WingsAPI.Game/GameEvent/Matchmaking/Result/IMatchmakingResult.cs create mode 100644 srcs/WingsAPI.Game/Generics/ThreadSafeHashSet.cs create mode 100644 srcs/WingsAPI.Game/Generics/ThreadSafeList.cs create mode 100644 srcs/WingsAPI.Game/Generics/ThreadSafeSortedList.cs create mode 100644 srcs/WingsAPI.Game/GmCommandEvent.cs create mode 100644 srcs/WingsAPI.Game/Groups/Events/GroupActionEvent.cs create mode 100644 srcs/WingsAPI.Game/Groups/Events/GroupAddMemberEvent.cs create mode 100644 srcs/WingsAPI.Game/Groups/Events/GroupInvitedEvent.cs create mode 100644 srcs/WingsAPI.Game/Groups/Events/GroupJoinEvent.cs create mode 100644 srcs/WingsAPI.Game/Groups/Events/GroupLeaveEvent.cs create mode 100644 srcs/WingsAPI.Game/Groups/Events/GroupRemoveMemberEvent.cs create mode 100644 srcs/WingsAPI.Game/Groups/Events/GroupWeedingEvent.cs create mode 100644 srcs/WingsAPI.Game/Groups/GroupComponent.cs create mode 100644 srcs/WingsAPI.Game/Groups/GroupFactory.cs create mode 100644 srcs/WingsAPI.Game/Groups/IGroupComponent.cs create mode 100644 srcs/WingsAPI.Game/Groups/IGroupFactory.cs create mode 100644 srcs/WingsAPI.Game/Groups/IGroupManager.cs create mode 100644 srcs/WingsAPI.Game/Groups/PlayerGroup.cs create mode 100644 srcs/WingsAPI.Game/Helpers/Damages/Calculation/CalculationBasicStatistics.cs create mode 100644 srcs/WingsAPI.Game/Helpers/Damages/Calculation/CalculationDefense.cs create mode 100644 srcs/WingsAPI.Game/Helpers/Damages/Calculation/CalculationElementDamage.cs create mode 100644 srcs/WingsAPI.Game/Helpers/Damages/Calculation/CalculationPhysicalDamage.cs create mode 100644 srcs/WingsAPI.Game/Helpers/Damages/Calculation/CalculationResult.cs create mode 100644 srcs/WingsAPI.Game/Helpers/Damages/DamageAlgorithmResult.cs create mode 100644 srcs/WingsAPI.Game/Helpers/Damages/Position.cs create mode 100644 srcs/WingsAPI.Game/Helpers/Damages/PositionExtensions.cs create mode 100644 srcs/WingsAPI.Game/Helpers/Location.cs create mode 100644 srcs/WingsAPI.Game/Helpers/MapLocation.cs create mode 100644 srcs/WingsAPI.Game/Helpers/PathfindingHelper.cs create mode 100644 srcs/WingsAPI.Game/IRandomGenerator.cs create mode 100644 srcs/WingsAPI.Game/InterChannel/ChatShoutAdminEvent.cs create mode 100644 srcs/WingsAPI.Game/InterChannel/FamilyChatMessageEvent.cs create mode 100644 srcs/WingsAPI.Game/InterChannel/InterChannelChatMessageBroadcastEvent.cs create mode 100644 srcs/WingsAPI.Game/InterChannel/InterChannelReceiveWhisperEvent.cs create mode 100644 srcs/WingsAPI.Game/InterChannel/InterChannelSendChatMsgByCharIdEvent.cs create mode 100644 srcs/WingsAPI.Game/InterChannel/InterChannelSendChatMsgByNicknameEvent.cs create mode 100644 srcs/WingsAPI.Game/InterChannel/InterChannelSendInfoByCharIdEvent.cs create mode 100644 srcs/WingsAPI.Game/InterChannel/InterChannelSendInfoByNicknameEvent.cs create mode 100644 srcs/WingsAPI.Game/InterChannel/InterChannelSendWhisperEvent.cs create mode 100644 srcs/WingsAPI.Game/Inventory/Event/DropMapItemEvent.cs create mode 100644 srcs/WingsAPI.Game/Inventory/Event/InventoryAddItemEvent.cs create mode 100644 srcs/WingsAPI.Game/Inventory/Event/InventoryDropItemEvent.cs create mode 100644 srcs/WingsAPI.Game/Inventory/Event/InventoryEquipItemEvent.cs create mode 100644 srcs/WingsAPI.Game/Inventory/Event/InventoryItemDeletedEvent.cs create mode 100644 srcs/WingsAPI.Game/Inventory/Event/InventoryItemUsedEvent.cs create mode 100644 srcs/WingsAPI.Game/Inventory/Event/InventoryMoveItemEvent.cs create mode 100644 srcs/WingsAPI.Game/Inventory/Event/InventoryPickUpItemEvent.cs create mode 100644 srcs/WingsAPI.Game/Inventory/Event/InventoryPickedUpItemEvent.cs create mode 100644 srcs/WingsAPI.Game/Inventory/Event/InventoryPickedUpPlayerItemEvent.cs create mode 100644 srcs/WingsAPI.Game/Inventory/Event/InventoryRemoveItemEvent.cs create mode 100644 srcs/WingsAPI.Game/Inventory/Event/InventorySortItemEvent.cs create mode 100644 srcs/WingsAPI.Game/Inventory/Event/InventoryTakeOffItemEvent.cs create mode 100644 srcs/WingsAPI.Game/Inventory/Event/ItemSumEvent.cs create mode 100644 srcs/WingsAPI.Game/Inventory/Event/PartnerInventoryEquipItemEvent.cs create mode 100644 srcs/WingsAPI.Game/Inventory/Event/PartnerInventoryTakeOffItemEvent.cs create mode 100644 srcs/WingsAPI.Game/Inventory/Event/PartnerSpecialistSkillEvent.cs create mode 100644 srcs/WingsAPI.Game/Inventory/Event/PlayerItemToPartnerItemEvent.cs create mode 100644 srcs/WingsAPI.Game/Inventory/IInventoryComponent.cs create mode 100644 srcs/WingsAPI.Game/Inventory/IPartnerInventoryComponent.cs create mode 100644 srcs/WingsAPI.Game/Inventory/InventoryComponent.cs create mode 100644 srcs/WingsAPI.Game/Inventory/InventoryException.cs create mode 100644 srcs/WingsAPI.Game/Inventory/InventoryItem.cs create mode 100644 srcs/WingsAPI.Game/Inventory/PartnerInventoryComponent.cs create mode 100644 srcs/WingsAPI.Game/Inventory/PartnerInventoryItem.cs create mode 100644 srcs/WingsAPI.Game/Items/CustomItem.cs create mode 100644 srcs/WingsAPI.Game/Items/GameItem.cs create mode 100644 srcs/WingsAPI.Game/Items/GameItemInstance.cs create mode 100644 srcs/WingsAPI.Game/Items/IDropRarityConfigurationProvider.cs create mode 100644 srcs/WingsAPI.Game/Items/IGameItem.cs create mode 100644 srcs/WingsAPI.Game/Items/IGameItemInstanceFactory.cs create mode 100644 srcs/WingsAPI.Game/Items/IItemBoxManager.cs create mode 100644 srcs/WingsAPI.Game/Logs/IPlayerActionLog.cs create mode 100644 srcs/WingsAPI.Game/Logs/IPlayerActionManager.cs create mode 100644 srcs/WingsAPI.Game/Mails/CharacterMail.cs create mode 100644 srcs/WingsAPI.Game/Mails/CharacterNote.cs create mode 100644 srcs/WingsAPI.Game/Mails/Events/MailClaimedEvent.cs create mode 100644 srcs/WingsAPI.Game/Mails/Events/MailCreateEvent.cs create mode 100644 srcs/WingsAPI.Game/Mails/Events/MailOpenEvent.cs create mode 100644 srcs/WingsAPI.Game/Mails/Events/MailRemoveEvent.cs create mode 100644 srcs/WingsAPI.Game/Mails/Events/MailRemovedEvent.cs create mode 100644 srcs/WingsAPI.Game/Mails/Events/NoteCreateEvent.cs create mode 100644 srcs/WingsAPI.Game/Mails/Events/NoteOpenEvent.cs create mode 100644 srcs/WingsAPI.Game/Mails/Events/NoteRemoveEvent.cs create mode 100644 srcs/WingsAPI.Game/Mails/Events/NoteSentEvent.cs create mode 100644 srcs/WingsAPI.Game/Mails/IMailNoteComponent.cs create mode 100644 srcs/WingsAPI.Game/Managers/BubbleComponent.cs create mode 100644 srcs/WingsAPI.Game/Managers/IAct4Manager.cs create mode 100644 srcs/WingsAPI.Game/Managers/IBazaarManager.cs create mode 100644 srcs/WingsAPI.Game/Managers/IBubbleComponent.cs create mode 100644 srcs/WingsAPI.Game/Managers/IDelayManager.cs create mode 100644 srcs/WingsAPI.Game/Managers/IForbiddenNamesManager.cs create mode 100644 srcs/WingsAPI.Game/Managers/IItemUsageManager.cs create mode 100644 srcs/WingsAPI.Game/Managers/IRankingManager.cs create mode 100644 srcs/WingsAPI.Game/Managers/IRevivalManager.cs create mode 100644 srcs/WingsAPI.Game/Managers/IScriptedInstanceManager.cs create mode 100644 srcs/WingsAPI.Game/Managers/IServerManager.cs create mode 100644 srcs/WingsAPI.Game/Managers/ISessionManager.cs create mode 100644 srcs/WingsAPI.Game/Managers/ServerData/IDropManager.cs create mode 100644 srcs/WingsAPI.Game/Managers/ServerData/IMapMonsterManager.cs create mode 100644 srcs/WingsAPI.Game/Managers/ServerData/IMapNpcManager.cs create mode 100644 srcs/WingsAPI.Game/Managers/ServerData/IRecipeManager.cs create mode 100644 srcs/WingsAPI.Game/Managers/ServerData/IShopManager.cs create mode 100644 srcs/WingsAPI.Game/Managers/StaticData/ICardsManager.cs create mode 100644 srcs/WingsAPI.Game/Managers/StaticData/IItemsManager.cs create mode 100644 srcs/WingsAPI.Game/Managers/StaticData/INpcMonsterManager.cs create mode 100644 srcs/WingsAPI.Game/Managers/StaticData/ISkillsManager.cs create mode 100644 srcs/WingsAPI.Game/Managers/TransportFactory.cs create mode 100644 srcs/WingsAPI.Game/Maps/CharacterMapItem.cs create mode 100644 srcs/WingsAPI.Game/Maps/Event/DisposeMapEvent.cs create mode 100644 srcs/WingsAPI.Game/Maps/Event/JoinMapEndEvent.cs create mode 100644 srcs/WingsAPI.Game/Maps/Event/JoinMapEvent.cs create mode 100644 srcs/WingsAPI.Game/Maps/Event/LeaveMapEvent.cs create mode 100644 srcs/WingsAPI.Game/Maps/Event/MapActivatedEvent.cs create mode 100644 srcs/WingsAPI.Game/Maps/Event/MapDeactivatedEvent.cs create mode 100644 srcs/WingsAPI.Game/Maps/Event/PortalRemoveEvent.cs create mode 100644 srcs/WingsAPI.Game/Maps/Event/PortalTriggerEvent.cs create mode 100644 srcs/WingsAPI.Game/Maps/Event/SpawnPortalEvent.cs create mode 100644 srcs/WingsAPI.Game/Maps/IMapAttribute.cs create mode 100644 srcs/WingsAPI.Game/Maps/IMapInstance.cs create mode 100644 srcs/WingsAPI.Game/Maps/IMapInstanceFactory.cs create mode 100644 srcs/WingsAPI.Game/Maps/IMapManager.cs create mode 100644 srcs/WingsAPI.Game/Maps/Map.cs create mode 100644 srcs/WingsAPI.Game/Maps/MapExtensions.cs create mode 100644 srcs/WingsAPI.Game/Maps/MapInstancePortalHandler.cs create mode 100644 srcs/WingsAPI.Game/Maps/MapInstanceState.cs create mode 100644 srcs/WingsAPI.Game/Maps/MapInstanceType.cs create mode 100644 srcs/WingsAPI.Game/Maps/MapItem.cs create mode 100644 srcs/WingsAPI.Game/Maps/TimeSpaceMapItem.cs create mode 100644 srcs/WingsAPI.Game/Mates/Events/MateBackToMinilandEvent.cs create mode 100644 srcs/WingsAPI.Game/Mates/Events/MateDeathEvent.cs create mode 100644 srcs/WingsAPI.Game/Mates/Events/MateHealEvent.cs create mode 100644 srcs/WingsAPI.Game/Mates/Events/MateInitializeEvent.cs create mode 100644 srcs/WingsAPI.Game/Mates/Events/MateJoinInsideMinilandEvent.cs create mode 100644 srcs/WingsAPI.Game/Mates/Events/MateJoinTeamEvent.cs create mode 100644 srcs/WingsAPI.Game/Mates/Events/MateLeaveTeamEvent.cs create mode 100644 srcs/WingsAPI.Game/Mates/Events/MateProcessExperienceEvent.cs create mode 100644 srcs/WingsAPI.Game/Mates/Events/MateRemoveEvent.cs create mode 100644 srcs/WingsAPI.Game/Mates/Events/MateRestEvent.cs create mode 100644 srcs/WingsAPI.Game/Mates/Events/MateReviveEvent.cs create mode 100644 srcs/WingsAPI.Game/Mates/Events/MateSpTransformEvent.cs create mode 100644 srcs/WingsAPI.Game/Mates/Events/MateSpUntransformEvent.cs create mode 100644 srcs/WingsAPI.Game/Mates/Events/MateStayInsideMinilandEvent.cs create mode 100644 srcs/WingsAPI.Game/Mates/Events/MateSummonEvent.cs create mode 100644 srcs/WingsAPI.Game/Mates/IMateComponent.cs create mode 100644 srcs/WingsAPI.Game/Mates/IMateEntity.cs create mode 100644 srcs/WingsAPI.Game/Mates/IMateEntityFactory.cs create mode 100644 srcs/WingsAPI.Game/Mates/IMateTransportFactory.cs create mode 100644 srcs/WingsAPI.Game/Mates/MateEntity.MonsterData.cs create mode 100644 srcs/WingsAPI.Game/Mates/MateEntity.Stats.cs create mode 100644 srcs/WingsAPI.Game/Mates/MateEntity.cs create mode 100644 srcs/WingsAPI.Game/Mates/MateEntityFactory.cs create mode 100644 srcs/WingsAPI.Game/Mates/StaticMateTransportFactory.cs create mode 100644 srcs/WingsAPI.Game/Miniland/Events/AddObjMinilandEndLogicEvent.cs create mode 100644 srcs/WingsAPI.Game/Miniland/Events/AddObjMinilandEvent.cs create mode 100644 srcs/WingsAPI.Game/Miniland/Events/MinigameDurabilityCouponEvent.cs create mode 100644 srcs/WingsAPI.Game/Miniland/Events/MinigameDurabilityInfoEvent.cs create mode 100644 srcs/WingsAPI.Game/Miniland/Events/MinigameGetYieldInfoEvent.cs create mode 100644 srcs/WingsAPI.Game/Miniland/Events/MinigameGetYieldRewardEvent.cs create mode 100644 srcs/WingsAPI.Game/Miniland/Events/MinigamePlayEvent.cs create mode 100644 srcs/WingsAPI.Game/Miniland/Events/MinigameRepairDurabilityEvent.cs create mode 100644 srcs/WingsAPI.Game/Miniland/Events/MinigameRewardClaimedEvent.cs create mode 100644 srcs/WingsAPI.Game/Miniland/Events/MinigameRewardEvent.cs create mode 100644 srcs/WingsAPI.Game/Miniland/Events/MinigameScoreEvent.cs create mode 100644 srcs/WingsAPI.Game/Miniland/Events/MinigameScoreLogEvent.cs create mode 100644 srcs/WingsAPI.Game/Miniland/Events/MinigameStopEvent.cs create mode 100644 srcs/WingsAPI.Game/Miniland/Events/MinilandIntroEvent.cs create mode 100644 srcs/WingsAPI.Game/Miniland/Events/MinilandStateEvent.cs create mode 100644 srcs/WingsAPI.Game/Miniland/Events/RmvObjMinilandEvent.cs create mode 100644 srcs/WingsAPI.Game/Miniland/Events/UseObjMinilandEvent.cs create mode 100644 srcs/WingsAPI.Game/Miniland/IMinigameManager.cs create mode 100644 srcs/WingsAPI.Game/Miniland/IMinilandManager.cs create mode 100644 srcs/WingsAPI.Game/Miniland/InteractionInformationHolder.cs create mode 100644 srcs/WingsAPI.Game/Miniland/MapDesignObject.cs create mode 100644 srcs/WingsAPI.Game/Miniland/MinigameInteraction.cs create mode 100644 srcs/WingsAPI.Game/Miniland/Minigames/MinigameRefreshProductionEvent.cs create mode 100644 srcs/WingsAPI.Game/Miniland/StaticMinilandManager.cs create mode 100644 srcs/WingsAPI.Game/Monster/Event/MonsterDeathEvent.cs create mode 100644 srcs/WingsAPI.Game/Monster/Event/MonsterRespawnedEvent.cs create mode 100644 srcs/WingsAPI.Game/Monster/Event/MonsterSummonEvent.cs create mode 100644 srcs/WingsAPI.Game/Monster/Waypoint.cs create mode 100644 srcs/WingsAPI.Game/MonsterMapItem.cs create mode 100644 srcs/WingsAPI.Game/Networking/BroadcastPacket.cs create mode 100644 srcs/WingsAPI.Game/Networking/Broadcasting/EmoticonsBroadcast.cs create mode 100644 srcs/WingsAPI.Game/Networking/Broadcasting/ExceptGroupBroadcast.cs create mode 100644 srcs/WingsAPI.Game/Networking/Broadcasting/ExceptRaidBroadcast.cs create mode 100644 srcs/WingsAPI.Game/Networking/Broadcasting/ExceptSessionBroadcast.cs create mode 100644 srcs/WingsAPI.Game/Networking/Broadcasting/ExpectBlockedPlayerBroadcast.cs create mode 100644 srcs/WingsAPI.Game/Networking/Broadcasting/ExpectRainbowEnemyTeamBroadcast.cs create mode 100644 srcs/WingsAPI.Game/Networking/Broadcasting/FactionBroadcast.cs create mode 100644 srcs/WingsAPI.Game/Networking/Broadcasting/FamilyBroadcast.cs create mode 100644 srcs/WingsAPI.Game/Networking/Broadcasting/GroupBroadcast.cs create mode 100644 srcs/WingsAPI.Game/Networking/Broadcasting/IBroadcastRule.cs create mode 100644 srcs/WingsAPI.Game/Networking/Broadcasting/InBaseMapBroadcast.cs create mode 100644 srcs/WingsAPI.Game/Networking/Broadcasting/InRaidBroadcast.cs create mode 100644 srcs/WingsAPI.Game/Networking/Broadcasting/LevelBroadcast.cs create mode 100644 srcs/WingsAPI.Game/Networking/Broadcasting/NotMutedBroadcast.cs create mode 100644 srcs/WingsAPI.Game/Networking/Broadcasting/OnlyGameMasters.cs create mode 100644 srcs/WingsAPI.Game/Networking/Broadcasting/OnlyPlayersBroadcast.cs create mode 100644 srcs/WingsAPI.Game/Networking/Broadcasting/RaidBroadcast.cs create mode 100644 srcs/WingsAPI.Game/Networking/Broadcasting/RangeBroadcast.cs create mode 100644 srcs/WingsAPI.Game/Networking/Broadcasting/SpeakerHeroBroadcast.cs create mode 100644 srcs/WingsAPI.Game/Networking/Broadcasting/TimeSpaceBroadcast.cs create mode 100644 srcs/WingsAPI.Game/Networking/IBroadcaster.cs create mode 100644 srcs/WingsAPI.Game/Networking/IClientSession.cs create mode 100644 srcs/WingsAPI.Game/Networking/SessionsContainer.cs create mode 100644 srcs/WingsAPI.Game/Npcs/Event/ItemProducedEvent.cs create mode 100644 srcs/WingsAPI.Game/Npcs/Event/MapNpcGenerateDeathEvent.cs create mode 100644 srcs/WingsAPI.Game/Npcs/Event/NpcSummonEvent.cs create mode 100644 srcs/WingsAPI.Game/Npcs/MonsterData.cs create mode 100644 srcs/WingsAPI.Game/PlayerCommandEvent.cs create mode 100644 srcs/WingsAPI.Game/Portals/IPortalEntity.cs create mode 100644 srcs/WingsAPI.Game/Portals/IPortalFactory.cs create mode 100644 srcs/WingsAPI.Game/Portals/ITimeSpacePortalEntity.cs create mode 100644 srcs/WingsAPI.Game/Portals/ITimeSpacePortalFactory.cs create mode 100644 srcs/WingsAPI.Game/Portals/PortalPacketExtensions.cs create mode 100644 srcs/WingsAPI.Game/Quests/BasicQuestContainer.cs create mode 100644 srcs/WingsAPI.Game/Quests/CharacterQuest.cs create mode 100644 srcs/WingsAPI.Game/Quests/Configurations/SoundFlowerConfiguration.cs create mode 100644 srcs/WingsAPI.Game/Quests/Event/AddQuestEvent.cs create mode 100644 srcs/WingsAPI.Game/Quests/Event/AddSoundFlowerQuestEvent.cs create mode 100644 srcs/WingsAPI.Game/Quests/Event/QuestAbandonedEvent.cs create mode 100644 srcs/WingsAPI.Game/Quests/Event/QuestAddedEvent.cs create mode 100644 srcs/WingsAPI.Game/Quests/Event/QuestCompletedEvent.cs create mode 100644 srcs/WingsAPI.Game/Quests/Event/QuestCompletedLogEvent.cs create mode 100644 srcs/WingsAPI.Game/Quests/Event/QuestDailyRefreshEvent.cs create mode 100644 srcs/WingsAPI.Game/Quests/Event/QuestHarvestEvent.cs create mode 100644 srcs/WingsAPI.Game/Quests/Event/QuestItemPickUpEvent.cs create mode 100644 srcs/WingsAPI.Game/Quests/Event/QuestMonsterDeathEvent.cs create mode 100644 srcs/WingsAPI.Game/Quests/Event/QuestNpcTalkEvent.cs create mode 100644 srcs/WingsAPI.Game/Quests/Event/QuestObjectiveUpdatedEvent.cs create mode 100644 srcs/WingsAPI.Game/Quests/Event/QuestRemoveEvent.cs create mode 100644 srcs/WingsAPI.Game/Quests/Event/QuestRewardEvent.cs create mode 100644 srcs/WingsAPI.Game/Quests/Event/RunScriptEvent.cs create mode 100644 srcs/WingsAPI.Game/Quests/IQuestContainer.cs create mode 100644 srcs/WingsAPI.Game/Quests/IQuestFactory.cs create mode 100644 srcs/WingsAPI.Game/Quests/IQuestManager.cs create mode 100644 srcs/WingsAPI.Game/Quests/IRunScriptHandler.cs create mode 100644 srcs/WingsAPI.Game/Quests/IRunScriptHandlerContainer.cs create mode 100644 srcs/WingsAPI.Game/Quicklist/IQuicklistComponent.cs create mode 100644 srcs/WingsAPI.Game/Quicklist/QuicklistAddEvent.cs create mode 100644 srcs/WingsAPI.Game/Quicklist/QuicklistComponent.cs create mode 100644 srcs/WingsAPI.Game/Quicklist/QuicklistRemoveEvent.cs create mode 100644 srcs/WingsAPI.Game/Quicklist/QuicklistSwapEvent.cs create mode 100644 srcs/WingsAPI.Game/Raids/ButtonMapItem.cs create mode 100644 srcs/WingsAPI.Game/Raids/Configuration/RaidConfiguration.cs create mode 100644 srcs/WingsAPI.Game/Raids/DropChance.cs create mode 100644 srcs/WingsAPI.Game/Raids/Enum/RaidFinishType.cs create mode 100644 srcs/WingsAPI.Game/Raids/Enum/RaidJoinType.cs create mode 100644 srcs/WingsAPI.Game/Raids/Events/RaidAbandonedEvent.cs create mode 100644 srcs/WingsAPI.Game/Raids/Events/RaidCreatedEvent.cs create mode 100644 srcs/WingsAPI.Game/Raids/Events/RaidDiedEvent.cs create mode 100644 srcs/WingsAPI.Game/Raids/Events/RaidGiveRewardsEvent.cs create mode 100644 srcs/WingsAPI.Game/Raids/Events/RaidInstanceDestroyEvent.cs create mode 100644 srcs/WingsAPI.Game/Raids/Events/RaidInstanceFinishEvent.cs create mode 100644 srcs/WingsAPI.Game/Raids/Events/RaidInstanceLivesIncDecEvent.cs create mode 100644 srcs/WingsAPI.Game/Raids/Events/RaidInstanceRefreshInfoEvent.cs create mode 100644 srcs/WingsAPI.Game/Raids/Events/RaidInstanceStartEvent.cs create mode 100644 srcs/WingsAPI.Game/Raids/Events/RaidInvitedEvent.cs create mode 100644 srcs/WingsAPI.Game/Raids/Events/RaidJoinedEvent.cs create mode 100644 srcs/WingsAPI.Game/Raids/Events/RaidLeftEvent.cs create mode 100644 srcs/WingsAPI.Game/Raids/Events/RaidListJoinEvent.cs create mode 100644 srcs/WingsAPI.Game/Raids/Events/RaidListOpenEvent.cs create mode 100644 srcs/WingsAPI.Game/Raids/Events/RaidListRegisterEvent.cs create mode 100644 srcs/WingsAPI.Game/Raids/Events/RaidListUnregisterEvent.cs create mode 100644 srcs/WingsAPI.Game/Raids/Events/RaidLostEvent.cs create mode 100644 srcs/WingsAPI.Game/Raids/Events/RaidMonsterThrowEvent.cs create mode 100644 srcs/WingsAPI.Game/Raids/Events/RaidObjectiveIncreaseEvent.cs create mode 100644 srcs/WingsAPI.Game/Raids/Events/RaidPartyCreateEvent.cs create mode 100644 srcs/WingsAPI.Game/Raids/Events/RaidPartyDisbandEvent.cs create mode 100644 srcs/WingsAPI.Game/Raids/Events/RaidPartyInvitePlayerEvent.cs create mode 100644 srcs/WingsAPI.Game/Raids/Events/RaidPartyJoinEvent.cs create mode 100644 srcs/WingsAPI.Game/Raids/Events/RaidPartyKickPlayerEvent.cs create mode 100644 srcs/WingsAPI.Game/Raids/Events/RaidPartyLeaveEvent.cs create mode 100644 srcs/WingsAPI.Game/Raids/Events/RaidPlayerSwitchButtonEvent.cs create mode 100644 srcs/WingsAPI.Game/Raids/Events/RaidPortalOpenEvent.cs create mode 100644 srcs/WingsAPI.Game/Raids/Events/RaidResetRestrictionEvent.cs create mode 100644 srcs/WingsAPI.Game/Raids/Events/RaidRevivedEvent.cs create mode 100644 srcs/WingsAPI.Game/Raids/Events/RaidRewardReceivedEvent.cs create mode 100644 srcs/WingsAPI.Game/Raids/Events/RaidStartedEvent.cs create mode 100644 srcs/WingsAPI.Game/Raids/Events/RaidSwitchButtonToggledEvent.cs create mode 100644 srcs/WingsAPI.Game/Raids/Events/RaidTargetKilledEvent.cs create mode 100644 srcs/WingsAPI.Game/Raids/Events/RaidTeleportMemberEvent.cs create mode 100644 srcs/WingsAPI.Game/Raids/Events/RaidWonEvent.cs create mode 100644 srcs/WingsAPI.Game/Raids/IRaidComponent.cs create mode 100644 srcs/WingsAPI.Game/Raids/IRaidFactory.cs create mode 100644 srcs/WingsAPI.Game/Raids/IRaidManager.cs create mode 100644 srcs/WingsAPI.Game/Raids/RaidBox.cs create mode 100644 srcs/WingsAPI.Game/Raids/RaidBoxRarity.cs create mode 100644 srcs/WingsAPI.Game/Raids/RaidComponent.cs create mode 100644 srcs/WingsAPI.Game/Raids/RaidInstance.cs create mode 100644 srcs/WingsAPI.Game/Raids/RaidPacketType.cs create mode 100644 srcs/WingsAPI.Game/Raids/RaidParty.cs create mode 100644 srcs/WingsAPI.Game/Raids/RaidReward.cs create mode 100644 srcs/WingsAPI.Game/Raids/RaidSubInstance.cs create mode 100644 srcs/WingsAPI.Game/Raids/RaidWave.cs create mode 100644 srcs/WingsAPI.Game/Raids/RaidWindowType.cs create mode 100644 srcs/WingsAPI.Game/Raids/RaidsPacketExtensions.cs create mode 100644 srcs/WingsAPI.Game/Raids/RdClientPacketType.cs create mode 100644 srcs/WingsAPI.Game/Raids/RdServerPacketType.cs create mode 100644 srcs/WingsAPI.Game/Raids/RlClientPacketType.cs create mode 100644 srcs/WingsAPI.Game/Raids/RlServerPacketType.cs create mode 100644 srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleCaptureFlagEvent.cs create mode 100644 srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleDestroyEvent.cs create mode 100644 srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleEndEvent.cs create mode 100644 srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleEnterEvent.cs create mode 100644 srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleFreezeEvent.cs create mode 100644 srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleFrozenEvent.cs create mode 100644 srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleJoinEvent.cs create mode 100644 srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleLeaveEvent.cs create mode 100644 srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleLeaverBusterRefreshEvent.cs create mode 100644 srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleLoseEvent.cs create mode 100644 srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleProcessActivityPointsEvent.cs create mode 100644 srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleProcessFlagPointsEvent.cs create mode 100644 srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleProcessLifeEvent.cs create mode 100644 srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleRefreshScoreEvent.cs create mode 100644 srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleStartEvent.cs create mode 100644 srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleStartProcessRegistrationEvent.cs create mode 100644 srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleStartRegisterEvent.cs create mode 100644 srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleTieEvent.cs create mode 100644 srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleUnfreezeEvent.cs create mode 100644 srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleUnfreezeProcessEvent.cs create mode 100644 srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleWonEvent.cs create mode 100644 srcs/WingsAPI.Game/RainbowBattle/IRainbowBattleManager.cs create mode 100644 srcs/WingsAPI.Game/RainbowBattle/RainBowFlag.cs create mode 100644 srcs/WingsAPI.Game/RainbowBattle/RainbowBattleComponent.cs create mode 100644 srcs/WingsAPI.Game/RainbowBattle/RainbowBattleExtensions.cs create mode 100644 srcs/WingsAPI.Game/RainbowBattle/RainbowBattleParty.cs create mode 100644 srcs/WingsAPI.Game/RainbowBattle/RainbowBattlePlayerDump.cs create mode 100644 srcs/WingsAPI.Game/RandomBag.cs create mode 100644 srcs/WingsAPI.Game/Range.cs create mode 100644 srcs/WingsAPI.Game/Recipes/IRecipeFactory.cs create mode 100644 srcs/WingsAPI.Game/Recipes/Recipe.cs create mode 100644 srcs/WingsAPI.Game/Relations/AddRelationEvent.cs create mode 100644 srcs/WingsAPI.Game/Relations/IInvitationManager.cs create mode 100644 srcs/WingsAPI.Game/Relations/IRelationComponent.cs create mode 100644 srcs/WingsAPI.Game/Relations/InvitationManager.cs create mode 100644 srcs/WingsAPI.Game/Relations/RelationBlockEvent.cs create mode 100644 srcs/WingsAPI.Game/Relations/RelationComponent.cs create mode 100644 srcs/WingsAPI.Game/Relations/RelationFriendEvent.cs create mode 100644 srcs/WingsAPI.Game/Relations/RemoveRelationEvent.cs create mode 100644 srcs/WingsAPI.Game/RespawnReturn/Event/RespawnChangeEvent.cs create mode 100644 srcs/WingsAPI.Game/RespawnReturn/Event/RespawnPlayerEvent.cs create mode 100644 srcs/WingsAPI.Game/RespawnReturn/Event/ReturnChangeEvent.cs create mode 100644 srcs/WingsAPI.Game/RespawnReturn/IHomeComponent.cs create mode 100644 srcs/WingsAPI.Game/Revival/Act4KillEvent.cs create mode 100644 srcs/WingsAPI.Game/Revival/CharacterRevivalComponent.cs create mode 100644 srcs/WingsAPI.Game/Revival/MateRevivalComponent.cs create mode 100644 srcs/WingsAPI.Game/Revival/RevivalAskEvent.cs create mode 100644 srcs/WingsAPI.Game/Revival/RevivalReviveEvent.cs create mode 100644 srcs/WingsAPI.Game/Revival/RevivalStartProcedureEvent.cs create mode 100644 srcs/WingsAPI.Game/Ship/Configuration/IShipConfigurationProvider.cs create mode 100644 srcs/WingsAPI.Game/Ship/Configuration/ShipConfiguration.cs create mode 100644 srcs/WingsAPI.Game/Ship/Event/ShipEnterEvent.cs create mode 100644 srcs/WingsAPI.Game/Ship/Event/ShipLeaveEvent.cs create mode 100644 srcs/WingsAPI.Game/Ship/Event/ShipProcessEvent.cs create mode 100644 srcs/WingsAPI.Game/Ship/IShipManager.cs create mode 100644 srcs/WingsAPI.Game/Ship/ShipInstance.cs create mode 100644 srcs/WingsAPI.Game/Shops/Event/BuyItemNpcShopEvent.cs create mode 100644 srcs/WingsAPI.Game/Shops/Event/BuyShopItemEvent.cs create mode 100644 srcs/WingsAPI.Game/Shops/Event/BuyShopSkillEvent.cs create mode 100644 srcs/WingsAPI.Game/Shops/Event/CurrencyType.cs create mode 100644 srcs/WingsAPI.Game/Shops/Event/ShopClosedEvent.cs create mode 100644 srcs/WingsAPI.Game/Shops/Event/ShopNpcBoughtItemEvent.cs create mode 100644 srcs/WingsAPI.Game/Shops/Event/ShopNpcListItemsEvent.cs create mode 100644 srcs/WingsAPI.Game/Shops/Event/ShopNpcSoldItemEvent.cs create mode 100644 srcs/WingsAPI.Game/Shops/Event/ShopOpenedEvent.cs create mode 100644 srcs/WingsAPI.Game/Shops/Event/ShopPlayerBoughtItemEvent.cs create mode 100644 srcs/WingsAPI.Game/Shops/Event/ShopPlayerBuyItemEvent.cs create mode 100644 srcs/WingsAPI.Game/Shops/Event/ShopPlayerCloseEvent.cs create mode 100644 srcs/WingsAPI.Game/Shops/Event/ShopPlayerOpenEvent.cs create mode 100644 srcs/WingsAPI.Game/Shops/Event/ShopSkillBoughtEvent.cs create mode 100644 srcs/WingsAPI.Game/Shops/Event/ShopSkillSoldEvent.cs create mode 100644 srcs/WingsAPI.Game/Shops/ShopComponent.cs create mode 100644 srcs/WingsAPI.Game/Shops/ShopNpc.cs create mode 100644 srcs/WingsAPI.Game/Shops/ShopNpcMenuType.cs create mode 100644 srcs/WingsAPI.Game/Shops/ShopPlayerItem.cs create mode 100644 srcs/WingsAPI.Game/Skills/AngelElementBuffComponent.cs create mode 100644 srcs/WingsAPI.Game/Skills/CharacterSkill.cs create mode 100644 srcs/WingsAPI.Game/Skills/ComboSkillComponent.cs create mode 100644 srcs/WingsAPI.Game/Skills/IAngelElementBuffComponent.cs create mode 100644 srcs/WingsAPI.Game/Skills/IBattleEntitySkill.cs create mode 100644 srcs/WingsAPI.Game/Skills/IComboSkillComponent.cs create mode 100644 srcs/WingsAPI.Game/Skills/IEndBuffDamageComponent.cs create mode 100644 srcs/WingsAPI.Game/Skills/IScoutComponent.cs create mode 100644 srcs/WingsAPI.Game/Skills/ISkillComponent.cs create mode 100644 srcs/WingsAPI.Game/Skills/ISkillCooldownComponent.cs create mode 100644 srcs/WingsAPI.Game/Skills/ISpyOutManager.cs create mode 100644 srcs/WingsAPI.Game/Skills/IWildKeeperComponent.cs create mode 100644 srcs/WingsAPI.Game/Skills/NpcMonsterSkill.cs create mode 100644 srcs/WingsAPI.Game/Skills/PartnerSkill.cs create mode 100644 srcs/WingsAPI.Game/Skills/SkillCooldownComponent.cs create mode 100644 srcs/WingsAPI.Game/Skills/SpyOutManager.cs create mode 100644 srcs/WingsAPI.Game/SnackFood/AdditionalFoodProgress.cs create mode 100644 srcs/WingsAPI.Game/SnackFood/AdditionalSnackProgress.cs create mode 100644 srcs/WingsAPI.Game/SnackFood/Events/AddAdditionalHpMpEvent.cs create mode 100644 srcs/WingsAPI.Game/SnackFood/FoodProgress.cs create mode 100644 srcs/WingsAPI.Game/SnackFood/FoodSnackComponent.cs create mode 100644 srcs/WingsAPI.Game/SnackFood/FoodSnackComponentFactory.cs create mode 100644 srcs/WingsAPI.Game/SnackFood/IFoodSnackComponent.cs create mode 100644 srcs/WingsAPI.Game/SnackFood/IFoodSnackComponentFactory.cs create mode 100644 srcs/WingsAPI.Game/SnackFood/SnackFoodConfiguration.cs create mode 100644 srcs/WingsAPI.Game/SnackFood/SnackProgress.cs create mode 100644 srcs/WingsAPI.Game/Specialists/ISpecialistStatsComponent.cs create mode 100644 srcs/WingsAPI.Game/Specialists/SpecialistStatsComponent.cs create mode 100644 srcs/WingsAPI.Game/TimeSpaces/Enums/PortalMinimapOrientation.cs create mode 100644 srcs/WingsAPI.Game/TimeSpaces/Enums/TimeSpaceAction.cs create mode 100644 srcs/WingsAPI.Game/TimeSpaces/Enums/TimeSpaceFinishType.cs create mode 100644 srcs/WingsAPI.Game/TimeSpaces/Enums/TimeSpaceTaskType.cs create mode 100644 srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceAddTimeToTimerEvent.cs create mode 100644 srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceBonusMonsterEvent.cs create mode 100644 srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceCheckForTasksCompletedEvent.cs create mode 100644 srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceCheckMonsterEvent.cs create mode 100644 srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceCheckObjectivesEvent.cs create mode 100644 srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceClosePortalEvent.cs create mode 100644 srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceDeathEvent.cs create mode 100644 srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceDecreaseLiveEvent.cs create mode 100644 srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceDespawnMonstersInRoomEvent.cs create mode 100644 srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceDestroyEvent.cs create mode 100644 srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceGroupTryJoinEvent.cs create mode 100644 srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceIncreaseScoreEvent.cs create mode 100644 srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceInstanceFinishEvent.cs create mode 100644 srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceInstanceStartEvent.cs create mode 100644 srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceLeavePartyEvent.cs create mode 100644 srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpacePartyCreateEvent.cs create mode 100644 srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpacePickUpItemEvent.cs create mode 100644 srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpacePortalOpenEvent.cs create mode 100644 srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceRefreshObjectiveProgressEvent.cs create mode 100644 srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceRemoveItemsEvent.cs create mode 100644 srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceSelectRewardEvent.cs create mode 100644 srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceSetTimeEvent.cs create mode 100644 srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceStartClockEvent.cs create mode 100644 srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceStartPortalEvent.cs create mode 100644 srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceStartTaskEvent.cs create mode 100644 srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceTaskCheckEvent.cs create mode 100644 srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceTogglePortalEvent.cs create mode 100644 srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceTryFinishTaskEvent.cs create mode 100644 srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceTryStartHiddenEvent.cs create mode 100644 srcs/WingsAPI.Game/TimeSpaces/Events/TryStartTaskEvent.cs create mode 100644 srcs/WingsAPI.Game/TimeSpaces/ITimeSpaceComponent.cs create mode 100644 srcs/WingsAPI.Game/TimeSpaces/ITimeSpaceFactory.cs create mode 100644 srcs/WingsAPI.Game/TimeSpaces/ITimeSpaceManager.cs create mode 100644 srcs/WingsAPI.Game/TimeSpaces/TimeSpaceComponent.cs create mode 100644 srcs/WingsAPI.Game/TimeSpaces/TimeSpaceInstance.cs create mode 100644 srcs/WingsAPI.Game/TimeSpaces/TimeSpaceObjective.cs create mode 100644 srcs/WingsAPI.Game/TimeSpaces/TimeSpacePacketExtensions.cs create mode 100644 srcs/WingsAPI.Game/TimeSpaces/TimeSpaceParty.cs create mode 100644 srcs/WingsAPI.Game/TimeSpaces/TimeSpaceRewardItem.cs create mode 100644 srcs/WingsAPI.Game/TimeSpaces/TimeSpaceSubInstance.cs create mode 100644 srcs/WingsAPI.Game/TimeSpaces/TimeSpaceTask.cs create mode 100644 srcs/WingsAPI.Game/ToSummon.cs create mode 100644 srcs/WingsAPI.Game/Triggers/BattleTriggers.cs create mode 100644 srcs/WingsAPI.Game/Triggers/EventTriggerContainer.cs create mode 100644 srcs/WingsAPI.Game/Triggers/IEventTriggerContainer.cs create mode 100644 srcs/WingsAPI.Game/Upgrades/Cellons/CellonChances.cs create mode 100644 srcs/WingsAPI.Game/Upgrades/Cellons/CellonOption.cs create mode 100644 srcs/WingsAPI.Game/Upgrades/Cellons/CellonPossibilities.cs create mode 100644 srcs/WingsAPI.Game/Upgrades/Cellons/CellonSystemConfiguration.cs create mode 100644 srcs/WingsAPI.Game/Warehouse/Events/AccountWarehouseAddItemEvent.cs create mode 100644 srcs/WingsAPI.Game/Warehouse/Events/AccountWarehouseMoveEvent.cs create mode 100644 srcs/WingsAPI.Game/Warehouse/Events/AccountWarehouseOpenEvent.cs create mode 100644 srcs/WingsAPI.Game/Warehouse/Events/AccountWarehouseShowItemEvent.cs create mode 100644 srcs/WingsAPI.Game/Warehouse/Events/AccountWarehouseWithdrawItemEvent.cs create mode 100644 srcs/WingsAPI.Game/Warehouse/Events/PartnerWarehouseAddItemEvent.cs create mode 100644 srcs/WingsAPI.Game/Warehouse/Events/PartnerWarehouseDepositEvent.cs create mode 100644 srcs/WingsAPI.Game/Warehouse/Events/PartnerWarehouseMoveEvent.cs create mode 100644 srcs/WingsAPI.Game/Warehouse/Events/PartnerWarehouseWithdrawEvent.cs create mode 100644 srcs/WingsAPI.Game/Warehouse/Events/WarehouseItemPlacedEvent.cs create mode 100644 srcs/WingsAPI.Game/Warehouse/Events/WarehouseItemWithdrawnEvent.cs create mode 100644 srcs/WingsAPI.Game/Warehouse/IAccountWarehouseManager.cs create mode 100644 srcs/WingsAPI.Game/Warehouse/IWarehouse.cs create mode 100644 srcs/WingsAPI.Game/Warehouse/PartnerWarehouseItem.cs create mode 100644 srcs/WingsAPI.Game/Warehouse/WarehouseItem.cs create mode 100644 srcs/WingsAPI.Game/WingsAPI.Game.csproj create mode 100644 srcs/WingsAPI.Game/_enum/ActRaidTypes.cs create mode 100644 srcs/WingsAPI.Game/_enum/ArmorItemSubType.cs create mode 100644 srcs/WingsAPI.Game/_enum/BankRankType.cs create mode 100644 srcs/WingsAPI.Game/_enum/BankType.cs create mode 100644 srcs/WingsAPI.Game/_enum/BuffFlag.cs create mode 100644 srcs/WingsAPI.Game/_enum/BuffGroup.cs create mode 100644 srcs/WingsAPI.Game/_enum/BuffGroupIds.cs create mode 100644 srcs/WingsAPI.Game/_enum/BuffVnums.cs create mode 100644 srcs/WingsAPI.Game/_enum/CastType.cs create mode 100644 srcs/WingsAPI.Game/_enum/CellonType.cs create mode 100644 srcs/WingsAPI.Game/_enum/CostumeItemSubType.cs create mode 100644 srcs/WingsAPI.Game/_enum/DialogVnums.cs create mode 100644 srcs/WingsAPI.Game/_enum/EffectType.cs create mode 100644 srcs/WingsAPI.Game/_enum/EtcItemType.cs create mode 100644 srcs/WingsAPI.Game/_enum/GroupAttackType.cs create mode 100644 srcs/WingsAPI.Game/_enum/ItemClassType.cs create mode 100644 srcs/WingsAPI.Game/_enum/ItemEffectVnums.cs create mode 100644 srcs/WingsAPI.Game/_enum/ItemVnums.cs create mode 100644 srcs/WingsAPI.Game/_enum/ManagerResponseType.cs create mode 100644 srcs/WingsAPI.Game/_enum/MapIds.cs create mode 100644 srcs/WingsAPI.Game/_enum/MateNrunType.cs create mode 100644 srcs/WingsAPI.Game/_enum/MinilandItemSubType.cs create mode 100644 srcs/WingsAPI.Game/_enum/MonsterRaceType.cs create mode 100644 srcs/WingsAPI.Game/_enum/MonsterSubRace.cs create mode 100644 srcs/WingsAPI.Game/_enum/MonsterVnum.cs create mode 100644 srcs/WingsAPI.Game/_enum/MorphIdType.cs create mode 100644 srcs/WingsAPI.Game/_enum/NpcMonsterRaceType.cs create mode 100644 srcs/WingsAPI.Game/_enum/NpcShopType.cs create mode 100644 srcs/WingsAPI.Game/_enum/QuestsVnums.cs create mode 100644 srcs/WingsAPI.Game/_enum/ReputationType.cs create mode 100644 srcs/WingsAPI.Game/_enum/ShellItemSubType.cs create mode 100644 srcs/WingsAPI.Game/_enum/SkillCastType.cs create mode 100644 srcs/WingsAPI.Game/_enum/SkillsVnums.cs create mode 100644 srcs/WingsAPI.Game/_enum/SoundFlowerType.cs create mode 100644 srcs/WingsAPI.Game/_enum/SoundType.cs create mode 100644 srcs/WingsAPI.Game/_enum/SpecialistItemSubType.cs create mode 100644 srcs/WingsAPI.Game/_enum/SpecialistPointsType.cs create mode 100644 srcs/WingsAPI.Game/_enum/StatisticType.cs create mode 100644 srcs/WingsAPI.Game/_enum/TimeType.cs create mode 100644 srcs/WingsAPI.Game/_enum/UsableItemSubType.cs create mode 100644 srcs/WingsAPI.Game/_enum/WeaponItemSubType.cs create mode 100644 srcs/WingsAPI.Packets.Handling/GenericGamePacketHandlerBase.cs create mode 100644 srcs/WingsAPI.Packets.Handling/PacketHandlingExtensions.cs create mode 100644 srcs/WingsAPI.Packets.Handling/RegisteredPacketHandler.cs create mode 100644 srcs/WingsAPI.Packets.Handling/WingsAPI.Packets.Handling.csproj create mode 100644 srcs/WingsAPI.Packets/ClientPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPacketRegistered.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/AddobjPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/ArenaPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/BIPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/BrawlerCreatePacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/BscPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/BtkPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/BuyPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/CBlistPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/CBuyPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/CClosePacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/CModPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/CRegPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/CScalcPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/CSkillPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/CSlistPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/CharacterCreatePacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/CharacterDeletePacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/CharacterOptionPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/CharacterRenamePacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/ComplimentPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/CreateFamilyPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/CrossServerEntrypointPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/CspServerPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/CspePacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/CsprPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/DepositPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/DirectionPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/EntryPointPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/EquipmentInfoPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/EscapePacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/ExcListPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/ExchangeRequestPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/FDelPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/FDepositPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/FInsPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/FInsPacketType.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/FReposPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/FStashEndPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/FWithdrawPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/FamilyChatPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/FamilyDisbandPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/FamilyManagementPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/FauthPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/FbPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/FhistCtsPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/FlPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/FrankCtsPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/FsLogCtsPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/GLeavePacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/GListPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/GListPacketType.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/GameStartPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/GboxPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/GetGiftPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/GetGiftType.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/GetPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/GitPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/GroupSayPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/GuriPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/HeroPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/ISortPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/JoinFamilyPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/LbsPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/MJoinPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/MLEditPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/MShopPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/MallPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/MinigamePacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/MkraidPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/MultiTargetListPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/MultiTargetListSubPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/MvePacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/MviPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/MzPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/NRunPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/NcifPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/Nos0575Packet.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/NpinfoPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/ObaPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/PJoinPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/PdtClosePacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/PdtsePacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/PleavePacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/PreqPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/PslPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/PsopServerPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/PstPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/PstPacketType.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/PtCtlPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/PtCtlSubPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/PulsePacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/PutPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/QSetPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/QsetPacketType.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/QtPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/RInfoPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/RankSkPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/RdPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/Relations/BlDelPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/Relations/BlInsPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/RemovePacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/ReposPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/ReqInfoPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/RequestNpcPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/RevivalPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/RlPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/RmvobjPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/RselPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/RstartPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/RxitPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/SayPPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/SayPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/ScpCtsPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/ScriptPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/SelectPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/SellPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/ShopClosePacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/ShoppingPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/SitPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/SitSubPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/SnapPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/SortOpenPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/SpTransformPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/SpecialistHolderPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/StashEndPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/SuctlPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/TaCallPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/TawPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/TitEqPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/TodayPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/TreqClientPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/UpetPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/UpgradePacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/UpgradePacketType.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/UpsPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/UseAtSkillPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/UseItemPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/UseSkillPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/UseobjPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/WalkPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/WearPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/WearPartnerCardPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/WhisperPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/WithdrawPacket.cs create mode 100644 srcs/WingsAPI.Packets/ClientPackets/WreqPacket.cs create mode 100644 srcs/WingsAPI.Packets/Enums/Act4/Act4FactionStateType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/Act4/DungeonEventType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/Act5RespawnType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/AdditionalTypes.cs create mode 100644 srcs/WingsAPI.Packets/Enums/ArenaTeamType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/BCardScalingType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/BankActionType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/Battle/AttackType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/Battle/CancelType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/Battle/TargetHitType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/Bazaar/BazaarListedItemType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/Bazaar/Filter/BazaarCategoryFilterType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/Bazaar/Filter/BazaarLevelFilterType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/Bazaar/Filter/BazaarPerfectionFilterType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/Bazaar/Filter/BazaarRarityFilterType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/Bazaar/Filter/BazaarSortFilterType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/Bazaar/Filter/BazaarUpgradeFilterType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/Bazaar/SubFilter/BazaarCategoryAccessoriesSubFilterType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/Bazaar/SubFilter/BazaarCategoryConsumerItemSubFilterType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/Bazaar/SubFilter/BazaarCategoryEquipmentSubFilterType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/Bazaar/SubFilter/BazaarCategoryMainItemSubFilterType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/Bazaar/SubFilter/BazaarCategoryPartnerSubFilterType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/Bazaar/SubFilter/BazaarCategoryPetSubFilterType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/Bazaar/SubFilter/BazaarCategoryShellSubFilterType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/Bazaar/SubFilter/BazaarCategorySpecialistSubFilterType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/Bazaar/SubFilter/BazaarCategoryStoreMountSubFilterType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/Bazaar/SubFilter/BazaarCategoryWeaponArmourSubFilterType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/BrawlerMorphType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/BsInfoType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/BuffCategory.cs create mode 100644 srcs/WingsAPI.Packets/Enums/BuyShopType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/Character/CharacterOption.cs create mode 100644 srcs/WingsAPI.Packets/Enums/Character/CharacterState.cs create mode 100644 srcs/WingsAPI.Packets/Enums/Character/ClassType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/Character/GenderType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/Chat/ChatMessageColorType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/Chat/ChatType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/Chat/MsgMessageType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/Chat/SpeakType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/Chat/SpeakerType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/ClockType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/EntityType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/EquipmentType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/ExcCloseType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/FactionType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/Families/FamilyActionType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/Families/FamilyAuthority.cs create mode 100644 srcs/WingsAPI.Packets/Enums/Families/FamilyJoinType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/Families/FamilyLogType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/Families/FamilySkillsType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/Families/FamilyTitle.cs create mode 100644 srcs/WingsAPI.Packets/Enums/Families/FamilyWarehouseAuthorityType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/FixedUpMode.cs create mode 100644 srcs/WingsAPI.Packets/Enums/GameType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/GroupRequestType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/GroupSharingType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/GuriType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/HairColorType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/HairStyleType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/InRespawnType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/InventoryType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/InvitationType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/ItModeType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/ItemType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/JewelOptionType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/LevelType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/LoginFailType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/MShopPacketType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/Mails/MailType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/Mails/ParcelActionType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/MateLevelUpType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/MateType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/MedalType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/MinilandState.cs create mode 100644 srcs/WingsAPI.Packets/Enums/ModalType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/MoveType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/NpcRunType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/PdtiType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/PenaltyType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/PortalType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/QnamlType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/QuestRewardType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/QuestType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/QueueWindow.cs create mode 100644 srcs/WingsAPI.Packets/Enums/RaidType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/Rainbow/RainbowBattleFlagTeamType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/Rainbow/RainbowBattleFlagType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/Rainbow/RainbowBattleTeamType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/Rainbow/RainbowTimeType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/RarifyMode.cs create mode 100644 srcs/WingsAPI.Packets/Enums/RarifyProtection.cs create mode 100644 srcs/WingsAPI.Packets/Enums/ReceiverType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/RefinerType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/Relations/CharacterRelationType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/RequestExchangeType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/RespawnType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/ScriptedInstanceType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/ServerState.cs create mode 100644 srcs/WingsAPI.Packets/Enums/SheepScoreType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/Shells/ShellEffectCategory.cs create mode 100644 srcs/WingsAPI.Packets/Enums/Shells/ShellEffectType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/Shells/ShellType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/ShopEndType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/SmemoType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/SpUpgradeResult.cs create mode 100644 srcs/WingsAPI.Packets/Enums/SpecialMapIdType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/SuPacketHitMode.cs create mode 100644 srcs/WingsAPI.Packets/Enums/TalentArenaOptionType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/TeleporterType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/Titles/TitEqPacketType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/Titles/TitleStatus.cs create mode 100644 srcs/WingsAPI.Packets/Enums/UpgradeMode.cs create mode 100644 srcs/WingsAPI.Packets/Enums/UpgradeProtection.cs create mode 100644 srcs/WingsAPI.Packets/Enums/UpgradeResult.cs create mode 100644 srcs/WingsAPI.Packets/Enums/VisualNameType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/VisualType.cs create mode 100644 srcs/WingsAPI.Packets/Enums/WindowType.cs create mode 100644 srcs/WingsAPI.Packets/IClientPacket.cs create mode 100644 srcs/WingsAPI.Packets/IPacket.cs create mode 100644 srcs/WingsAPI.Packets/IPacketDeserializer.cs create mode 100644 srcs/WingsAPI.Packets/IPacketSerializer.cs create mode 100644 srcs/WingsAPI.Packets/IServerPacket.cs create mode 100644 srcs/WingsAPI.Packets/PacketAliasAttribute.cs create mode 100644 srcs/WingsAPI.Packets/PacketDeserializer.cs create mode 100644 srcs/WingsAPI.Packets/PacketExtensions.cs create mode 100644 srcs/WingsAPI.Packets/PacketHeaderAttribute.cs create mode 100644 srcs/WingsAPI.Packets/PacketIndexAttribute.cs create mode 100644 srcs/WingsAPI.Packets/PacketSerializationInformation.cs create mode 100644 srcs/WingsAPI.Packets/PacketSerializer.cs create mode 100644 srcs/WingsAPI.Packets/ServerPacket.cs create mode 100644 srcs/WingsAPI.Packets/ServerPackets/EffectServerPacket.cs create mode 100644 srcs/WingsAPI.Packets/ServerPackets/Titles/TitInfoPacket.cs create mode 100644 srcs/WingsAPI.Packets/ServerPackets/Titles/TitlePacket.cs create mode 100644 srcs/WingsAPI.Packets/ServerPackets/Titles/TitleSubPacket.cs create mode 100644 srcs/WingsAPI.Packets/UnresolvedPacket.cs create mode 100644 srcs/WingsAPI.Packets/WingsAPI.Packets.csproj create mode 100644 srcs/WingsAPI.Plugins/Exceptions/CriticalPluginException.cs create mode 100644 srcs/WingsAPI.Plugins/Exceptions/PluginException.cs create mode 100644 srcs/WingsAPI.Plugins/Extensions/FeatureToggleExtensions.cs create mode 100644 srcs/WingsAPI.Plugins/GameServerLoader.cs create mode 100644 srcs/WingsAPI.Plugins/IDependencyInjectorPlugin.cs create mode 100644 srcs/WingsAPI.Plugins/IGamePlugin.cs create mode 100644 srcs/WingsAPI.Plugins/IGameServerPlugin.cs create mode 100644 srcs/WingsAPI.Plugins/IPlugin.cs create mode 100644 srcs/WingsAPI.Plugins/IPluginManager.cs create mode 100644 srcs/WingsAPI.Plugins/IPluginPathConfigurationProvider.cs create mode 100644 srcs/WingsAPI.Plugins/PluginPathConfigurationProvider.cs create mode 100644 srcs/WingsAPI.Plugins/WingsAPI.Plugins.csproj create mode 100644 srcs/WingsAPI.Scripting.LUA/Converter/Converter.cs create mode 100644 srcs/WingsAPI.Scripting.LUA/Converter/GuidConverter.cs create mode 100644 srcs/WingsAPI.Scripting.LUA/Converter/SEventConverter.cs create mode 100644 srcs/WingsAPI.Scripting.LUA/Converter/SMapObjectConverter.cs create mode 100644 srcs/WingsAPI.Scripting.LUA/Extension/AssemblyExtensions.cs create mode 100644 srcs/WingsAPI.Scripting.LUA/Extension/DynValueExtension.cs create mode 100644 srcs/WingsAPI.Scripting.LUA/LuaScriptFactory.cs create mode 100644 srcs/WingsAPI.Scripting.LUA/ObjectFactory.cs create mode 100644 srcs/WingsAPI.Scripting.LUA/ScriptFactoryConfiguration.cs create mode 100644 srcs/WingsAPI.Scripting.LUA/WingsAPI.Scripting.LUA.csproj create mode 100644 srcs/WingsAPI.Scripting/Attribute/ScriptEventAttribute.cs create mode 100644 srcs/WingsAPI.Scripting/Attribute/ScriptObjectAttribute.cs create mode 100644 srcs/WingsAPI.Scripting/Converter/SRemovePortalEventConverter.cs create mode 100644 srcs/WingsAPI.Scripting/Converter/SThrowRaidDropsEventConverter.cs create mode 100644 srcs/WingsAPI.Scripting/Converter/ScriptedEventConverter.cs create mode 100644 srcs/WingsAPI.Scripting/Enum/Dungeon/SDungeonType.cs create mode 100644 srcs/WingsAPI.Scripting/Enum/MapObjectType.cs create mode 100644 srcs/WingsAPI.Scripting/Enum/Raid/SRaidFinishType.cs create mode 100644 srcs/WingsAPI.Scripting/Enum/Raid/SRaidType.cs create mode 100644 srcs/WingsAPI.Scripting/Enum/SMapFlags.cs create mode 100644 srcs/WingsAPI.Scripting/Enum/SMapType.cs create mode 100644 srcs/WingsAPI.Scripting/Enum/SObjectiveType.cs create mode 100644 srcs/WingsAPI.Scripting/Enum/SPortalType.cs create mode 100644 srcs/WingsAPI.Scripting/Enum/TimeSpace/SPortalMinimapOrientation.cs create mode 100644 srcs/WingsAPI.Scripting/Enum/TimeSpace/STimeSpaceFinishType.cs create mode 100644 srcs/WingsAPI.Scripting/Enum/TimeSpace/STimeSpaceTaskType.cs create mode 100644 srcs/WingsAPI.Scripting/Event/Common/SMonsterSummonEvent.cs create mode 100644 srcs/WingsAPI.Scripting/Event/Common/SOpenPortalEvent.cs create mode 100644 srcs/WingsAPI.Scripting/Event/Common/SRemovePortalEvent.cs create mode 100644 srcs/WingsAPI.Scripting/Event/Common/STeleportMembersEvent.cs create mode 100644 srcs/WingsAPI.Scripting/Event/Dungeon/SAct4DungeonRewardEvent.cs create mode 100644 srcs/WingsAPI.Scripting/Event/Raid/SFinishRaidEvent.cs create mode 100644 srcs/WingsAPI.Scripting/Event/Raid/SRaidIncreaseObjectiveEvent.cs create mode 100644 srcs/WingsAPI.Scripting/Event/Raid/SThrowRaidDropsEvent.cs create mode 100644 srcs/WingsAPI.Scripting/Event/SEvent.cs create mode 100644 srcs/WingsAPI.Scripting/Event/TimeSpace/SAddTimeEvent.cs create mode 100644 srcs/WingsAPI.Scripting/Event/TimeSpace/SCheckForTasksCompletedEvent.cs create mode 100644 srcs/WingsAPI.Scripting/Event/TimeSpace/SClosePortal.cs create mode 100644 srcs/WingsAPI.Scripting/Event/TimeSpace/SDespawnAllMobsInRoomEvent.cs create mode 100644 srcs/WingsAPI.Scripting/Event/TimeSpace/SRemoveItemsEvent.cs create mode 100644 srcs/WingsAPI.Scripting/Event/TimeSpace/STimeSpaceFinishEvent.cs create mode 100644 srcs/WingsAPI.Scripting/Event/TimeSpace/STogglePortalEvent.cs create mode 100644 srcs/WingsAPI.Scripting/Event/TimeSpace/STryStartTaskEvent.cs create mode 100644 srcs/WingsAPI.Scripting/Event/TimeSpace/ScriptSetTimeEvent.cs create mode 100644 srcs/WingsAPI.Scripting/IScriptFactory.cs create mode 100644 srcs/WingsAPI.Scripting/InvalidScriptException.cs create mode 100644 srcs/WingsAPI.Scripting/Object/Common/Map/SButton.cs create mode 100644 srcs/WingsAPI.Scripting/Object/Common/Map/SItem.cs create mode 100644 srcs/WingsAPI.Scripting/Object/Common/Map/SMapObject.cs create mode 100644 srcs/WingsAPI.Scripting/Object/Common/Map/SMonsterWave.cs create mode 100644 srcs/WingsAPI.Scripting/Object/Common/Map/SPortal.cs create mode 100644 srcs/WingsAPI.Scripting/Object/Common/SDrop.cs create mode 100644 srcs/WingsAPI.Scripting/Object/Common/SLocation.cs create mode 100644 srcs/WingsAPI.Scripting/Object/Common/SMap.cs create mode 100644 srcs/WingsAPI.Scripting/Object/Common/SMapNpc.cs create mode 100644 srcs/WingsAPI.Scripting/Object/Common/SMonster.cs create mode 100644 srcs/WingsAPI.Scripting/Object/Common/SPosition.cs create mode 100644 srcs/WingsAPI.Scripting/Object/Common/SRange.cs create mode 100644 srcs/WingsAPI.Scripting/Object/Dungeon/SDungeon.cs create mode 100644 srcs/WingsAPI.Scripting/Object/Raid/SRaid.cs create mode 100644 srcs/WingsAPI.Scripting/Object/Raid/SRaidBox.cs create mode 100644 srcs/WingsAPI.Scripting/Object/Raid/SRaidBoxRarity.cs create mode 100644 srcs/WingsAPI.Scripting/Object/Raid/SRaidRequirement.cs create mode 100644 srcs/WingsAPI.Scripting/Object/Raid/SRaidReward.cs create mode 100644 srcs/WingsAPI.Scripting/Object/Raid/SWaypoint.cs create mode 100644 srcs/WingsAPI.Scripting/Object/Timespace/STimeSpaceObject.cs create mode 100644 srcs/WingsAPI.Scripting/Object/Timespace/STimeSpaceObjective.cs create mode 100644 srcs/WingsAPI.Scripting/Object/Timespace/STimeSpaceRequirementObject.cs create mode 100644 srcs/WingsAPI.Scripting/Object/Timespace/STimeSpaceRewardsObject.cs create mode 100644 srcs/WingsAPI.Scripting/Object/Timespace/STimeSpaceTask.cs create mode 100644 srcs/WingsAPI.Scripting/Object/Timespace/STimespaceItemReward.cs create mode 100644 srcs/WingsAPI.Scripting/Object/Timespace/TimespaceConstEventKeys.cs create mode 100644 srcs/WingsAPI.Scripting/ScriptManager/IDungeonScriptManager.cs create mode 100644 srcs/WingsAPI.Scripting/ScriptManager/IRaidScriptManager.cs create mode 100644 srcs/WingsAPI.Scripting/ScriptManager/ITimeSpaceScriptManager.cs create mode 100644 srcs/WingsAPI.Scripting/Validator/Common/Map/SButtonValidator.cs create mode 100644 srcs/WingsAPI.Scripting/Validator/Common/Map/SMapObjectValidator.cs create mode 100644 srcs/WingsAPI.Scripting/Validator/Common/Map/SPortalValidator.cs create mode 100644 srcs/WingsAPI.Scripting/Validator/Common/SEventsValidator.cs create mode 100644 srcs/WingsAPI.Scripting/Validator/Common/SLocationValidator.cs create mode 100644 srcs/WingsAPI.Scripting/Validator/Common/SMapValidator.cs create mode 100644 srcs/WingsAPI.Scripting/Validator/Common/SMonsterValidator.cs create mode 100644 srcs/WingsAPI.Scripting/Validator/ItemVnumValidator.cs create mode 100644 srcs/WingsAPI.Scripting/Validator/MapVnumValidator.cs create mode 100644 srcs/WingsAPI.Scripting/Validator/MonsterVnumValidator.cs create mode 100644 srcs/WingsAPI.Scripting/WingsAPI.Scripting.csproj create mode 100644 srcs/WingsEmu.Communication.gRPC/Extensions/ServiceProviderExtensions.cs create mode 100644 srcs/WingsEmu.Communication.gRPC/WingsEmu.Communication.gRPC.csproj create mode 100644 srcs/WingsEmu.Health/Extensions/DependencyInjectionExtensions.cs create mode 100644 srcs/WingsEmu.Health/HealthCheckHostedService.cs create mode 100644 srcs/WingsEmu.Health/IMaintenanceManager.cs create mode 100644 srcs/WingsEmu.Health/MaintenanceActivateMessageConsumer.cs create mode 100644 srcs/WingsEmu.Health/MaintenanceDeactivateMessageConsumer.cs create mode 100644 srcs/WingsEmu.Health/MaintenanceManager.cs create mode 100644 srcs/WingsEmu.Health/ServiceMaintenanceActivateMessage.cs create mode 100644 srcs/WingsEmu.Health/ServiceMaintenanceDeactivateMessage.cs create mode 100644 srcs/WingsEmu.Health/ServiceStatusType.cs create mode 100644 srcs/WingsEmu.Health/ServiceStatusUpdateMessage.cs create mode 100644 srcs/WingsEmu.Health/WingsEmu.Health.csproj create mode 100644 srcs/_plugins/Plugin.Act4/Act4DungeonManager.cs create mode 100644 srcs/_plugins/Plugin.Act4/Act4Manager.cs create mode 100644 srcs/_plugins/Plugin.Act4/Act4Plugin.cs create mode 100644 srcs/_plugins/Plugin.Act4/Act4PluginCore.cs create mode 100644 srcs/_plugins/Plugin.Act4/Commands/Act4CommandsModule.cs create mode 100644 srcs/_plugins/Plugin.Act4/Commands/Act4DungeonCommandsModule.cs create mode 100644 srcs/_plugins/Plugin.Act4/Const/DungeonConstEventKeys.cs create mode 100644 srcs/_plugins/Plugin.Act4/DungeonFactory.cs create mode 100644 srcs/_plugins/Plugin.Act4/DungeonScriptManager.cs create mode 100644 srcs/_plugins/Plugin.Act4/Event/Act4DungeonBossMapCleanUpEventHandler.cs create mode 100644 srcs/_plugins/Plugin.Act4/Event/Act4DungeonBroadcastBossClosedEventHandler.cs create mode 100644 srcs/_plugins/Plugin.Act4/Event/Act4DungeonBroadcastBossOpenEventHandler.cs create mode 100644 srcs/_plugins/Plugin.Act4/Event/Act4DungeonBroadcastPacketEventHandler.cs create mode 100644 srcs/_plugins/Plugin.Act4/Event/Act4DungeonEnterEventHandler.cs create mode 100644 srcs/_plugins/Plugin.Act4/Event/Act4DungeonMonsterThrowEventHandler.cs create mode 100644 srcs/_plugins/Plugin.Act4/Event/Act4DungeonRewardEventHandler.cs create mode 100644 srcs/_plugins/Plugin.Act4/Event/Act4DungeonStopEventHandler.cs create mode 100644 srcs/_plugins/Plugin.Act4/Event/Act4DungeonSystemStartEventHandler.cs create mode 100644 srcs/_plugins/Plugin.Act4/Event/Act4DungeonSystemStopEventHandler.cs create mode 100644 srcs/_plugins/Plugin.Act4/Event/Act4FactionPointsGenerationEventHandler.cs create mode 100644 srcs/_plugins/Plugin.Act4/Event/Act4FactionPointsIncreaseEventHandler.cs create mode 100644 srcs/_plugins/Plugin.Act4/Event/Act4JoinMapEndEventHandler.cs create mode 100644 srcs/_plugins/Plugin.Act4/Event/Act4KillBonusEventHandler.cs create mode 100644 srcs/_plugins/Plugin.Act4/Event/Act4MukrajuDeathEventHandler.cs create mode 100644 srcs/_plugins/Plugin.Act4/Event/Act4MukrajuDespawnEventHandler.cs create mode 100644 srcs/_plugins/Plugin.Act4/Event/Act4MukrajuSpawnEventHandler.cs create mode 100644 srcs/_plugins/Plugin.Act4/Event/Act4PutFlagEventHandler.cs create mode 100644 srcs/_plugins/Plugin.Act4/Event/Act4SystemFcBroadcastEventHandler.cs create mode 100644 srcs/_plugins/Plugin.Act4/Event/PortalTriggerAct4EventHandler.cs create mode 100644 srcs/_plugins/Plugin.Act4/Event/PortalTriggerDungeonEventHandler.cs create mode 100644 srcs/_plugins/Plugin.Act4/Event/RevivalAskEventDungeonHandler.cs create mode 100644 srcs/_plugins/Plugin.Act4/Event/RevivalEventAct4Handler.cs create mode 100644 srcs/_plugins/Plugin.Act4/Event/RevivalEventDungeonHandler.cs create mode 100644 srcs/_plugins/Plugin.Act4/Event/RevivalStartProcedureEventAct4Handler.cs create mode 100644 srcs/_plugins/Plugin.Act4/Event/RevivalStartProcedureEventDungeonHandler.cs create mode 100644 srcs/_plugins/Plugin.Act4/Extension/Act4DungeonExtension.cs create mode 100644 srcs/_plugins/Plugin.Act4/Extension/ScriptExtension.cs create mode 100644 srcs/_plugins/Plugin.Act4/Plugin.Act4.csproj create mode 100644 srcs/_plugins/Plugin.Act4/RecurrentJob/Act4DungeonSystem.cs create mode 100644 srcs/_plugins/Plugin.Act4/RecurrentJob/Act4System.cs create mode 100644 srcs/_plugins/Plugin.Act4/Scripting/Converter/SAct4DungeonRewardEventConverter.cs create mode 100644 srcs/_plugins/Plugin.Act4/Scripting/Validator/SDungeonValidator.cs create mode 100644 srcs/_plugins/Plugin.CoreImpl/Configs/HardcodedDialogsByNpcVnum.cs create mode 100644 srcs/_plugins/Plugin.CoreImpl/Configs/HardcodedDialogsByNpcVnumFileConfig.cs create mode 100644 srcs/_plugins/Plugin.CoreImpl/CoreImplDependencyPlugin.cs create mode 100644 srcs/_plugins/Plugin.CoreImpl/Entities/MonsterEntity.cs create mode 100644 srcs/_plugins/Plugin.CoreImpl/Entities/MonsterEntityFactory.cs create mode 100644 srcs/_plugins/Plugin.CoreImpl/Entities/NpcEntity.cs create mode 100644 srcs/_plugins/Plugin.CoreImpl/Entities/NpcEntityFactory.cs create mode 100644 srcs/_plugins/Plugin.CoreImpl/Entities/PortalEntity.cs create mode 100644 srcs/_plugins/Plugin.CoreImpl/Entities/PortalFactory.cs create mode 100644 srcs/_plugins/Plugin.CoreImpl/Entities/TimeSpacePortalEntity.cs create mode 100644 srcs/_plugins/Plugin.CoreImpl/Entities/TimeSpacePortalFactory.cs create mode 100644 srcs/_plugins/Plugin.CoreImpl/Maps/GenericEntityIdManager.cs create mode 100644 srcs/_plugins/Plugin.CoreImpl/Maps/MapInstance.cs create mode 100644 srcs/_plugins/Plugin.CoreImpl/Maps/MapInstanceFactory.cs create mode 100644 srcs/_plugins/Plugin.CoreImpl/Maps/Systems/BCardTickSystem.cs create mode 100644 srcs/_plugins/Plugin.CoreImpl/Maps/Systems/BattleSystem.cs create mode 100644 srcs/_plugins/Plugin.CoreImpl/Maps/Systems/CharacterSystem.cs create mode 100644 srcs/_plugins/Plugin.CoreImpl/Maps/Systems/DropSystem.cs create mode 100644 srcs/_plugins/Plugin.CoreImpl/Maps/Systems/MateSystem.cs create mode 100644 srcs/_plugins/Plugin.CoreImpl/Maps/Systems/MonsterQuestSystemException.cs create mode 100644 srcs/_plugins/Plugin.CoreImpl/Maps/Systems/MonsterSystem.cs create mode 100644 srcs/_plugins/Plugin.CoreImpl/Maps/Systems/NpcSystem.cs create mode 100644 srcs/_plugins/Plugin.CoreImpl/Maps/Systems/SkillCooldownSystem.cs create mode 100644 srcs/_plugins/Plugin.CoreImpl/Maps/Systems/SnackFoodSystem.cs create mode 100644 srcs/_plugins/Plugin.CoreImpl/Pathfinding/ComparePfNodeMatrix.cs create mode 100644 srcs/_plugins/Plugin.CoreImpl/Pathfinding/Heuristic.cs create mode 100644 srcs/_plugins/Plugin.CoreImpl/Pathfinding/HeuristicFormula.cs create mode 100644 srcs/_plugins/Plugin.CoreImpl/Pathfinding/IPathFinder.cs create mode 100644 srcs/_plugins/Plugin.CoreImpl/Pathfinding/IPriorityQueue.cs create mode 100644 srcs/_plugins/Plugin.CoreImpl/Pathfinding/PathFinder.cs create mode 100644 srcs/_plugins/Plugin.CoreImpl/Pathfinding/PathFinderNode.cs create mode 100644 srcs/_plugins/Plugin.CoreImpl/Pathfinding/PathFinderNodeFast.cs create mode 100644 srcs/_plugins/Plugin.CoreImpl/Pathfinding/PathFinderOptions.cs create mode 100644 srcs/_plugins/Plugin.CoreImpl/Pathfinding/PriorityQueueB.cs create mode 100644 srcs/_plugins/Plugin.CoreImpl/Plugin.CoreImpl.csproj create mode 100644 srcs/_plugins/Plugin.CoreImpl/Skills/SkillEntityFactory.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/Auth/ClientVersion/AuthorizedClientVersionEntity.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/Auth/ClientVersion/EfAuthorizedClientVersionRepository.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/Auth/ClientVersion/IAuthorizedClientVersionRepository.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/Auth/HWID/BlacklistedHwidDao.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/Auth/HWID/BlacklistedHwidEntity.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/Auth/HWID/IBlacklistedHwidDao.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/Bazaar/BazaarItemDAO.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/Bazaar/DbBazaarItemEntity.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/DAOs/AccountBanDao.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/DAOs/AccountDAO.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/DAOs/AccountPenaltyDao.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/DAOs/CharacterDAO.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/DAOs/CharacterRelationDAO.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/DAOs/TimeSpaceRecordDao.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/DB/Configs/AccountBansTypeConfiguration.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/DB/Configs/AccountPenaltyTypeConfiguration.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/DB/Configs/AccountTypeConfiguration.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/DB/Configs/BaseAuditableEntityTypeConfiguration.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/DB/Configs/CharacterBazaarItemEntityTypeConfiguration.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/DB/Configs/CharacterEntityTypeConfiguration.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/DB/Configs/CharacterRelationEntityTypeConfiguration.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/DB/Configs/DbTimeSpaceRecordTypeConfiguration.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/DB/DatabaseConfiguration.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/DB/DatabaseSchemas.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/DB/DesignTimeContextFactory.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/DB/GameContext.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/DatabasePlugin.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/Entities/Account/AccountBanEntity.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/Entities/Account/AccountEntity.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/Entities/Account/AccountPenaltyEntity.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/Entities/BaseAuditableEntity.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/Entities/IAuditableEntity.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/Entities/PlayersData/CharacterRelationEntity.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/Entities/PlayersData/DbCharacter.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/Entities/ServerData/DbTimeSpaceRecord.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/Extensions/DependencyInjectionExtensions.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/Extensions/GameContextFactoryExtensions.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/Families/DbFamily.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/Families/DbFamilyCharacterTypeConfiguration.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/Families/DbFamilyLogEntity.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/Families/DbFamilyLogTypeConfiguration.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/Families/DbFamilyMembership.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/Families/DbFamilyTypeConfiguration.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/Families/FamilyDAO.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/Families/FamilyLogDAO.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/Families/FamilyMembershipDao.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/Families/FamilyWarehouseItemDao.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/Families/FamilyWarehouseItemEntity.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/Families/FamilyWarehouseItemEntityTypeConfiguration.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/Families/FamilyWarehouseLogDao.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/Families/FamilyWarehouseLogEntity.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/Families/FamilyWarehouseLogEntityTypeConfiguration.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/Mail/CharacterMailDao.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/Mail/CharacterMailEntityTypeConfiguration.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/Mail/CharacterNoteDao.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/Mail/DbCharacterMail.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/Mail/DbCharacterNote.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/Mail/DbCharacterNoteEntityTypeConfiguration.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/Mapping/GameMappingRules.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/Mapping/MapsterMapper.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/Mapping/NonGameMappingRules.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/Migrations/20211227011918_Init.Designer.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/Migrations/20211227011918_Init.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/Migrations/GameContextModelSnapshot.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/Plugin.DB.EF.csproj create mode 100644 srcs/_plugins/Plugin.DB.EF/Warehouse/AccountWarehouseItemEntity.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/Warehouse/AccountWarehouseItemEntityTypeConfiguration.cs create mode 100644 srcs/_plugins/Plugin.DB.EF/Warehouse/AccountWarehouseItemItemDao.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/Achievements/AdministratorFamilyModule.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/Achievements/FamilyAchievementReward.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/Achievements/FamilyAchievementSpecificConfiguration.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/Achievements/FamilyAchievementUnlockedMessage.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/Achievements/FamilyAchievementUnlockedMessageConsumer.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/Achievements/FamilyAchievementsConfiguration.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/Achievements/Handlers/Act4KillEventHandler.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/Achievements/Handlers/FamilyAchievementHandlerAct4DungeonWon.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/Achievements/Handlers/InstantBattleAchievementHandler.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/Achievements/Handlers/RaidWonAchievementHandler.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/Commands/AdministratorFamilyModule.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/Commands/FamilyKeeperChange.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/Commands/FamilyModule.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/Commands/FamilyNostaleUiCommandsModule.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/Consumers/FamilyAcknowledgeExperiencesMessageConsumer.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/Consumers/FamilyAcknowledgeLogsMessageConsumer.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/Consumers/FamilyChangeFactionMessageConsumer.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/Consumers/FamilyCharacterJoinMessageConsumer.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/Consumers/FamilyCharacterLeaveMessageConsumer.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/Consumers/FamilyChatMessageConsumer.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/Consumers/FamilyCreatedMessageConsumer.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/Consumers/FamilyDisbandMessageConsumer.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/Consumers/FamilyMemberAddedMessageConsumer.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/Consumers/FamilyMemberInviteMessageConsumer.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/Consumers/FamilyMemberRemovedMessageConsumer.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/Consumers/FamilyMemberUpdateMessageConsumer.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/Consumers/FamilyShoutMessageConsumer.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/Consumers/FamilyUpdateMessageConsumer.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/Consumers/FamilyWarehouseItemUpdateMessageConsumer.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/Consumers/FamilyWarehouseLogAddMessageConsumer.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/FamiliesModuleExtensions.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/FamilyAddExperienceEventHandler.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/FamilyAddLogEventHandler.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/FamilyAddMemberEventHandler.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/FamilyChangeAuthorityEventHandler.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/FamilyChangeDeputyEventHandler.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/FamilyChangeFactionEventHandler.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/FamilyChangeSettingsEventHandler.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/FamilyChangeSexEventHandler.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/FamilyChangeTitleEventHandler.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/FamilyCharacterDisconnectEventHandler.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/FamilyChatMessageEventHandler.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/FamilyCreateEventHandler.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/FamilyDisbandEventHandler.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/FamilyExperienceManager.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/FamilyInviteResponseEventHandler.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/FamilyLeaveEventHandler.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/FamilyListMembersEventHandler.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/FamilyManager.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/FamilyNoticeMessageEventHandler.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/FamilyPlugin.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/FamilyPluginCore.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/FamilyReceiveInviteEventHandler.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/FamilyRemoveMemberEventHandler.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/FamilySendInviteEventHandler.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/FamilyShoutEventHandler.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/FamilyTodayEventHandler.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/FamilyWarehouseAddItemEventHandler.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/FamilyWarehouseCloseEventHandler.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/FamilyWarehouseLogsOpenEventHandler.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/FamilyWarehouseManager.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/FamilyWarehouseMoveItemEventHandler.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/FamilyWarehouseOpenEventHandler.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/FamilyWarehouseShowItemEventHandler.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/FamilyWarehouseWithdrawItemEventHandler.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/IFamilyExperienceManager.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/IFamilyWarehouseManager.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/Logs/FamilyLogManager.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/Logs/IFamilyLogManager.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyAcknowledgeExperienceGainedMessage.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyAcknowledgeLogsMessage.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyChangeFactionMessage.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyCharacterJoinMessage.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyCharacterLeaveMessage.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyChatMessage.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyCreatedMessage.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyDeclareExperienceGainedMessage.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyDeclareLogsMessage.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyDisbandMessage.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyHeadSexMessage.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyInviteMessage.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyMemberAddedMessage.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyMemberRemovedMessage.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyMemberTodayMessage.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyMemberUpdateMessage.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyNoticeMessage.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyShoutMessage.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyUpdateMessage.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyWarehouseItemUpdateMessage.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyWarehouseLogAddMessage .cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/Missions/FamilyMissionReward.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/Missions/FamilyMissionSpecificConfiguration.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/Missions/FamilyMissionsConfiguration.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/NpcDialogs/CreateFamilyHandler.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/NpcDialogs/OpenFamilyWarehouseHandler.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/NpcDialogs/OpenFamilyWarehouseHistHandler.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/Plugin.FamilyImpl.csproj create mode 100644 srcs/_plugins/Plugin.FamilyImpl/RecurrentJob/FamilyExperienceSystem.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/RecurrentJob/FamilyLogSystem.cs create mode 100644 srcs/_plugins/Plugin.FamilyImpl/Upgrades/FamilyUpgradeBuyHandler.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Act4/Act4DungeonStartedLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Act4/Act4FamilyDungeonWonLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Act4/Act4PvpKillLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Bazaar/BazaarBoughtItemsLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Bazaar/BazaarItemExpiredLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Bazaar/BazaarItemInsertedLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Bazaar/BazaarItemWithdrawnLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Family/FamilyCreatedLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Family/FamilyDisbandedLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Family/FamilyInvitedLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Family/FamilyJoinedLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Family/FamilyKickedLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Family/FamilyLeftLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Family/FamilyMessageLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Family/FamilyUpgradeBoughtLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Family/FamilyWarehouseItemPlacedLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Family/FamilyWarehouseItemWithdrawnLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/IPlayerLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Inventory/InventoryItemDeletedLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Inventory/InventoryItemUsedLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Inventory/InventoryPickedUpItemLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Inventory/InventoryPickedUpPlayerItemLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Mate/LevelUpNosMateLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Miniland/MinigameRewardClaimedLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Miniland/MinigameScoreLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Miniland/WarehouseItemPlacedLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Miniland/WarehouseItemWithdrawnLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Npc/ItemProducedLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Player/BoxOpenedLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Player/GroupInvitedLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Player/MailClaimedLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Player/MailRemovedLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Player/NoteSentLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Player/PlayerChatGeneralLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Player/PlayerDisconnectedLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Player/PlayerExchangeLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Player/TradeRequestedLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Quest/QuestAbandonedLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Quest/QuestAddedLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Quest/QuestCompletedLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Quest/QuestObjectiveUpdatedLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Raid/RaidAbandonedLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Raid/RaidCreatedLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Raid/RaidDiedLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Raid/RaidInvitedLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Raid/RaidJoinedLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Raid/RaidLeftLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Raid/RaidLostLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Raid/RaidRevivedLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Raid/RaidRewardReceivedLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Raid/RaidStartedLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Raid/RaidSwitchButtonToggledLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Raid/RaidTargetKilledLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Raid/RaidWonLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/RainbowBattle/RainbowBattleFrozenLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/RainbowBattle/RainbowBattleJoinLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/RainbowBattle/RainbowBattleLoseLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/RainbowBattle/RainbowBattleTieLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/RainbowBattle/RainbowBattleWonLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Shop/ShopClosedLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Shop/ShopNpcBoughtItemLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Shop/ShopNpcSoldItemLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Shop/ShopOpenedLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Shop/ShopPlayerBoughtItemLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Shop/ShopSkillBoughtLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Shop/ShopSkillSoldLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Upgrade/CellonUpgradedLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Upgrade/ItemGambledLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Upgrade/ItemSummedLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Upgrade/ItemUpgradedLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Upgrade/LevelUpCharacterLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Upgrade/ShellIdentifiedLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Upgrade/SpPerfectedLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Entities/Upgrade/SpUpgradedLogEntity.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Extensions/IgnoreDefaultPropertiesConvention.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Extensions/MongoDatabaseExtensions.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Extensions/MongoLoggerExtensions.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Plugin.MongoLogs.csproj create mode 100644 srcs/_plugins/Plugin.MongoLogs/Services/MongoLogsBackgroundService.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Services/MongoLogsService.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Utils/CollectionNameAttribute.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Utils/CollectionNames.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Utils/DisplayCollectionNames.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Utils/EntityForAttribute.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Utils/GenericLogConsumer.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Utils/LogType.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Utils/MongoDatabaseHelper.cs create mode 100644 srcs/_plugins/Plugin.MongoLogs/Utils/MongoLogsConfiguration.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Core/GenericPlayerEventLogMessageFactory.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Core/GenericPlayerGameEventToLogProcessor.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Core/ILogMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Core/IPlayerEventLogMessageFactory.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Core/LogDependencyInjectionExtensions.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Act4/LogAct4DungeonStartedMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Act4/LogAct4FamilyDungeonWonMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Act4/LogAct4PvpKillMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Bazaar/LogBazaarItemBoughtMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Bazaar/LogBazaarItemExpiredMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Bazaar/LogBazaarItemInsertedMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Bazaar/LogBazaarItemWithdrawnMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Family/LogFamilyCreatedMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Family/LogFamilyDisbandedMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Family/LogFamilyInvitedMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Family/LogFamilyJoinedMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Family/LogFamilyKickedMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Family/LogFamilyLeftMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Family/LogFamilyMessageMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Family/LogFamilyUpgradeBoughtMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Family/LogFamilyWarehouseItemPlacedMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Family/LogFamilyWarehouseItemWithdrawnMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Inventory/LogInventoryItemDeletedMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Inventory/LogInventoryItemUsedMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Inventory/LogInventoryPickedUpItemMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Inventory/LogInventoryPickedUpPlayerItemMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/LevelUp/LogLevelUpCharacterMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/LevelUp/LogLevelUpNosMateMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/LogGMCommandExecutedMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/LogStrangeBehaviorMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Mail/LogMailClaimedMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Mail/LogMailRemovedMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Mail/LogNoteSentMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Miniland/LogMinigameRewardClaimedMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Miniland/LogMinigameScoreMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Miniland/LogWarehouseItemPlacedMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Miniland/LogWarehouseItemWithdrawnMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Npc/LogItemProducedMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Player/LogBoxOpenedMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Player/LogGroupInvitedMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Player/LogPlayerChatMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Player/LogPlayerCommandExecutedMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Player/LogPlayerDisconnectedMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Player/LogPlayerExchangeMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Player/LogTradeRequestedMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Quest/LogQuestAbandonedMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Quest/LogQuestAddedMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Quest/LogQuestCompletedMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Quest/LogQuestObjectiveUpdatedMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Raid/LogRaidAbandonedMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Raid/LogRaidCreatedMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Raid/LogRaidDiedMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Raid/LogRaidInvitedMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Raid/LogRaidJoinedMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Raid/LogRaidLeftMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Raid/LogRaidLostMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Raid/LogRaidRevivedMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Raid/LogRaidRewardReceivedMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Raid/LogRaidStartedMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Raid/LogRaidSwitchButtonToggledEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Raid/LogRaidTargetKilledMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Raid/LogRaidWonMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/RainbowBattle/LogRainbowBattleFrozenMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/RainbowBattle/LogRainbowBattleJoinMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/RainbowBattle/LogRainbowBattleLoseMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/RainbowBattle/LogRainbowBattleTieMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/RainbowBattle/LogRainbowBattleWonMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Shop/LogShopClosedMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Shop/LogShopNpcBoughtItemMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Shop/LogShopNpcSoldItemMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Shop/LogShopOpenedMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Shop/LogShopPlayerBoughtItemMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Shop/LogShopSkillBoughtMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Shop/LogShopSkillSoldMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Upgrade/LogCellonUpgradedMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Upgrade/LogItemGambledMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Upgrade/LogItemSummedMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Upgrade/LogItemUpgradedMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Upgrade/LogShellIdentifiedMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Upgrade/LogSpPerfectedMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Enrichers/Upgrade/LogSpUpgradedMessageEnricher.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/IPlayerActionLogMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Act4/LogAct4DungeonStartedMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Act4/LogAct4FamilyDungeonWonMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Act4/LogAct4PvpKillMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Bazaar/LogBazaarItemBoughtMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Bazaar/LogBazaarItemExpiredMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Bazaar/LogBazaarItemInsertedMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Bazaar/LogBazaarItemWithdrawnMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Family/LogFamilyCreatedMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Family/LogFamilyDisbandedMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Family/LogFamilyInvitedMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Family/LogFamilyJoinedMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Family/LogFamilyKickedMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Family/LogFamilyLeftMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Family/LogFamilyMessageMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Family/LogFamilyUpgradeBoughtMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Family/LogFamilyWarehouseItemPlacedMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Family/LogFamilyWarehouseItemWithdrawnMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Inventory/LogInventoryItemDeletedMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Inventory/LogInventoryItemUsedMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Inventory/LogInventoryPickedUpItemMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Inventory/LogInventoryPickedUpPlayerItemMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/LevelUp/LogLevelUpCharacterMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/LevelUp/LogLevelUpNosMateMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/LogGMCommandExecutedMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/LogStrangeBehaviorMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Mail/LogMailClaimedMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Mail/LogMailRemovedMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Mail/LogMailSentMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Mail/LogNoteSentMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Miniland/LogMinigameRewardClaimedMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Miniland/LogMinigameScoreMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Miniland/LogWarehouseItemPlacedMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Miniland/LogWarehouseItemWithdrawnMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Npc/LogItemProducedMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Player/LogBoxOpenedMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Player/LogGroupInvitedMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Player/LogPlayerChatMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Player/LogPlayerCommandExecutedMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Player/LogPlayerDisconnectedMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Player/LogPlayerExchangeMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Player/LogTradeRequestedMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Quest/LogQuestAbandonedMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Quest/LogQuestAddedMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Quest/LogQuestCompletedMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Quest/LogQuestObjectiveUpdatedMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Raid/LogRaidAbandonedMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Raid/LogRaidCreatedMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Raid/LogRaidDiedMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Raid/LogRaidInvitedMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Raid/LogRaidJoinedMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Raid/LogRaidLeftMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Raid/LogRaidLostMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Raid/LogRaidRevivedMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Raid/LogRaidRewardReceivedMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Raid/LogRaidStartedMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Raid/LogRaidSwitchButtonToggledMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Raid/LogRaidTargetKilledMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Raid/LogRaidWonMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/RainbowBattle/LogRainbowBattleFrozenMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/RainbowBattle/LogRainbowBattleJoinMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/RainbowBattle/LogRainbowBattleLoseMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/RainbowBattle/LogRainbowBattleTieMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/RainbowBattle/LogRainbowBattleWonMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Shop/LogShopClosedMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Shop/LogShopNpcBoughtItemMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Shop/LogShopNpcSoldItemMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Shop/LogShopOpenedMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Shop/LogShopPlayerBoughtItemMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Shop/LogShopSkillBoughtMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Shop/LogShopSkillSoldMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Upgrade/LogCellonUpgradedMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Upgrade/LogItemGambledMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Upgrade/LogItemSummedMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Upgrade/LogItemUpgradedMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Upgrade/LogShellIdentifiedMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Upgrade/LogSpPerfectedMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Messages/Upgrade/LogSpUpgradedMessage.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/PlayerLogManager.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/PlayerLoggingDependencyPlugin.cs create mode 100644 srcs/_plugins/Plugin.PlayerLogs/Plugin.PlayerLogs.csproj create mode 100644 srcs/_plugins/Plugin.QuestImpl/BaseRunScriptHandler.cs create mode 100644 srcs/_plugins/Plugin.QuestImpl/Handlers/AddGeneralQuestEventHandler.cs create mode 100644 srcs/_plugins/Plugin.QuestImpl/Handlers/AddMainQuestEventHandler.cs create mode 100644 srcs/_plugins/Plugin.QuestImpl/Handlers/AddSecondaryQuestEventHandler.cs create mode 100644 srcs/_plugins/Plugin.QuestImpl/Handlers/AddSoundFlowerQuestEventHandler.cs create mode 100644 srcs/_plugins/Plugin.QuestImpl/Handlers/QuestCompletedEventHandler.cs create mode 100644 srcs/_plugins/Plugin.QuestImpl/Handlers/QuestDailyRefreshEventHandler.cs create mode 100644 srcs/_plugins/Plugin.QuestImpl/Handlers/QuestHarvestEventHandler.cs create mode 100644 srcs/_plugins/Plugin.QuestImpl/Handlers/QuestItemPickUpEventHandler.cs create mode 100644 srcs/_plugins/Plugin.QuestImpl/Handlers/QuestMonsterDeathEventHandler.cs create mode 100644 srcs/_plugins/Plugin.QuestImpl/Handlers/QuestNpcTalkEventHandler.cs create mode 100644 srcs/_plugins/Plugin.QuestImpl/Handlers/QuestRemoveEventHandler.cs create mode 100644 srcs/_plugins/Plugin.QuestImpl/Handlers/QuestRewardEventHandler.cs create mode 100644 srcs/_plugins/Plugin.QuestImpl/Managers/QuestManager.cs create mode 100644 srcs/_plugins/Plugin.QuestImpl/Plugin.QuestImpl.csproj create mode 100644 srcs/_plugins/Plugin.QuestImpl/QuestDependencyInjectionExtensions.cs create mode 100644 srcs/_plugins/Plugin.QuestImpl/QuestFactory.cs create mode 100644 srcs/_plugins/Plugin.QuestImpl/QuestModule.cs create mode 100644 srcs/_plugins/Plugin.QuestImpl/QuestPlugin.cs create mode 100644 srcs/_plugins/Plugin.QuestImpl/QuestPluginCore.cs create mode 100644 srcs/_plugins/Plugin.QuestImpl/RunScriptEventHandler.cs create mode 100644 srcs/_plugins/Plugin.QuestImpl/RunScriptHandlers/TeleportRunScriptHandler.cs create mode 100644 srcs/_plugins/Plugin.Raids/Commands/RaidAdminCommandsModule.cs create mode 100644 srcs/_plugins/Plugin.Raids/Commands/RaidAdminStartModule.cs create mode 100644 srcs/_plugins/Plugin.Raids/Configs/RaidStartConfiguration.cs create mode 100644 srcs/_plugins/Plugin.Raids/Configs/RaidStartFileConfiguration.cs create mode 100644 srcs/_plugins/Plugin.Raids/Const/RaidConstEventKeys.cs create mode 100644 srcs/_plugins/Plugin.Raids/Extension/ScriptExtensions.cs create mode 100644 srcs/_plugins/Plugin.Raids/Handlers/PortalTriggerRaidEventHandler.cs create mode 100644 srcs/_plugins/Plugin.Raids/Handlers/RaidGiveRewardsEventHandler.cs create mode 100644 srcs/_plugins/Plugin.Raids/Handlers/RaidInstanceActivateRaidWaves.cs create mode 100644 srcs/_plugins/Plugin.Raids/Handlers/RaidInstanceDestroyEventHandler.cs create mode 100644 srcs/_plugins/Plugin.Raids/Handlers/RaidInstanceFinishEventHandler.cs create mode 100644 srcs/_plugins/Plugin.Raids/Handlers/RaidInstanceLivesIncDecEventHandler.cs create mode 100644 srcs/_plugins/Plugin.Raids/Handlers/RaidInstanceRefreshInfoEventHandler.cs create mode 100644 srcs/_plugins/Plugin.Raids/Handlers/RaidInstanceStartEventHandler.cs create mode 100644 srcs/_plugins/Plugin.Raids/Handlers/RaidJoinMapEndEventHandler.cs create mode 100644 srcs/_plugins/Plugin.Raids/Handlers/RaidListJoinEventHandler.cs create mode 100644 srcs/_plugins/Plugin.Raids/Handlers/RaidListOpenEventHandler.cs create mode 100644 srcs/_plugins/Plugin.Raids/Handlers/RaidListRegisterEventHandler.cs create mode 100644 srcs/_plugins/Plugin.Raids/Handlers/RaidListUnregisterEventHandler.cs create mode 100644 srcs/_plugins/Plugin.Raids/Handlers/RaidMonsterThrowEventHandler.cs create mode 100644 srcs/_plugins/Plugin.Raids/Handlers/RaidObjectiveIncreaseEventHandler.cs create mode 100644 srcs/_plugins/Plugin.Raids/Handlers/RaidPartyCreateEventHandler.cs create mode 100644 srcs/_plugins/Plugin.Raids/Handlers/RaidPartyDisbandEventHandler.cs create mode 100644 srcs/_plugins/Plugin.Raids/Handlers/RaidPartyInvitePlayerEventHandler.cs create mode 100644 srcs/_plugins/Plugin.Raids/Handlers/RaidPartyJoinEventHandler.cs create mode 100644 srcs/_plugins/Plugin.Raids/Handlers/RaidPartyKickPlayerEventHandler.cs create mode 100644 srcs/_plugins/Plugin.Raids/Handlers/RaidPartyLeaveEventHandler.cs create mode 100644 srcs/_plugins/Plugin.Raids/Handlers/RaidPlayerSwitchButtonEventHandler.cs create mode 100644 srcs/_plugins/Plugin.Raids/Handlers/RaidPortalOpenEventHandler.cs create mode 100644 srcs/_plugins/Plugin.Raids/Handlers/RaidResetRestrictionEventHandler.cs create mode 100644 srcs/_plugins/Plugin.Raids/Handlers/RaidTeleportMemberEventHandler.cs create mode 100644 srcs/_plugins/Plugin.Raids/Handlers/RevivalEventRaidHandler.cs create mode 100644 srcs/_plugins/Plugin.Raids/Handlers/RevivalStartProcedureEventRaidHandler.cs create mode 100644 srcs/_plugins/Plugin.Raids/Plugin.Raids.csproj create mode 100644 srcs/_plugins/Plugin.Raids/RaidFactory.cs create mode 100644 srcs/_plugins/Plugin.Raids/RaidManager.cs create mode 100644 srcs/_plugins/Plugin.Raids/RaidsPlugin.cs create mode 100644 srcs/_plugins/Plugin.Raids/RaidsPluginCore.cs create mode 100644 srcs/_plugins/Plugin.Raids/RecurrentJob/RaidSystem.cs create mode 100644 srcs/_plugins/Plugin.Raids/Scripting/Converter/SFinishRaidEventConverter.cs create mode 100644 srcs/_plugins/Plugin.Raids/Scripting/Converter/SMonsterSummonEventConverter.cs create mode 100644 srcs/_plugins/Plugin.Raids/Scripting/Converter/SOpenRaidPortalEventConverter.cs create mode 100644 srcs/_plugins/Plugin.Raids/Scripting/Converter/SRaidIncreaseObjectiveEventConverter.cs create mode 100644 srcs/_plugins/Plugin.Raids/Scripting/Converter/STeleportMembersEventConverter.cs create mode 100644 srcs/_plugins/Plugin.Raids/Scripting/RaidScriptManager.cs create mode 100644 srcs/_plugins/Plugin.Raids/Scripting/Validator/Raid/SRaidRequirementValidator.cs create mode 100644 srcs/_plugins/Plugin.Raids/Scripting/Validator/Raid/SRaidValidator.cs create mode 100644 srcs/_plugins/Plugin.ResourceLoader/BattleEntityAlgorithmService.cs create mode 100644 srcs/_plugins/Plugin.ResourceLoader/FileResourceLoaderPlugin.cs create mode 100644 srcs/_plugins/Plugin.ResourceLoader/InMemoryGameDataLanguageService.cs create mode 100644 srcs/_plugins/Plugin.ResourceLoader/InMemoryMultilanguageService.cs create mode 100644 srcs/_plugins/Plugin.ResourceLoader/Loaders/ActDescResourceFileLoader.cs create mode 100644 srcs/_plugins/Plugin.ResourceLoader/Loaders/CardResourceFileLoader.cs create mode 100644 srcs/_plugins/Plugin.ResourceLoader/Loaders/GameDataLanguageFileLoader.cs create mode 100644 srcs/_plugins/Plugin.ResourceLoader/Loaders/GenericTranslationGrpcLoader.cs create mode 100644 srcs/_plugins/Plugin.ResourceLoader/Loaders/ItemResourceFileLoader.cs create mode 100644 srcs/_plugins/Plugin.ResourceLoader/Loaders/MapResourceFileLoader.cs create mode 100644 srcs/_plugins/Plugin.ResourceLoader/Loaders/NpcMonsterFileLoader.cs create mode 100644 srcs/_plugins/Plugin.ResourceLoader/Loaders/NpcQuestResourceFileLoader.cs create mode 100644 srcs/_plugins/Plugin.ResourceLoader/Loaders/QuestResourceFileLoader.cs create mode 100644 srcs/_plugins/Plugin.ResourceLoader/Loaders/SkillResourceFileLoader.cs create mode 100644 srcs/_plugins/Plugin.ResourceLoader/Loaders/TutorialResourceFileLoader.cs create mode 100644 srcs/_plugins/Plugin.ResourceLoader/Plugin.ResourceLoader.csproj create mode 100644 srcs/_plugins/Plugin.ResourceLoader/ResourceLoadingConfiguration.cs create mode 100644 srcs/_plugins/Plugin.TimeSpaces/Commands/TimeSpaceAdminStartModule.cs create mode 100644 srcs/_plugins/Plugin.TimeSpaces/Handlers/RevivalAskEventTimeSpaceHandler.cs create mode 100644 srcs/_plugins/Plugin.TimeSpaces/Handlers/RevivalEventTimeSpaceHandler.cs create mode 100644 srcs/_plugins/Plugin.TimeSpaces/Handlers/RevivalStartProcedureEventTimeSpaceHandler.cs create mode 100644 srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceAddTimeToTimerEventHandler.cs create mode 100644 srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceBonusMonsterEventHandler.cs create mode 100644 srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceCheckForTasksCompletedEventHandler.cs create mode 100644 srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceCheckMonsterEventHandler.cs create mode 100644 srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceCheckObjectivesEventHandler.cs create mode 100644 srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceClosePortalEventHandler.cs create mode 100644 srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceDeathEventHandler.cs create mode 100644 srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceDecreaseLiveEventHandler.cs create mode 100644 srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceDespawnMonstersInRoomEventHandler.cs create mode 100644 srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceDestroyEventHandler.cs create mode 100644 srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceGroupTryJoinEventHandler.cs create mode 100644 srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceIncreaseScoreEventHandler.cs create mode 100644 srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceInstanceFinishEventHandler.cs create mode 100644 srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceInstanceStartEventHandler.cs create mode 100644 srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceJoinMapEndEventHandler.cs create mode 100644 srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceLeavePartyEventHandler.cs create mode 100644 srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceMonsterDeathHandler.cs create mode 100644 srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpacePartyCreateEventHandler.cs create mode 100644 srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpacePickUpItemEventHandler.cs create mode 100644 srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpacePortalOpenEventHandler.cs create mode 100644 srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpacePortalTriggerEventHandler.cs create mode 100644 srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceRefreshObjectiveProgressEventHandler.cs create mode 100644 srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceRemoveItemsEventHandler.cs create mode 100644 srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceSelectRewardEventHandler.cs create mode 100644 srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceSetTimeEventHandler.cs create mode 100644 srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceStartClockEventHandler.cs create mode 100644 srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceStartPortalEventHandler.cs create mode 100644 srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceStartTaskEventHandler.cs create mode 100644 srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceTogglePortalEventHandler.cs create mode 100644 srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceTryFinishTaskEventHandler.cs create mode 100644 srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceTryStartHiddenEventHandler.cs create mode 100644 srcs/_plugins/Plugin.TimeSpaces/Handlers/TryStartTaskForMapEventHandler.cs create mode 100644 srcs/_plugins/Plugin.TimeSpaces/LuaTimeSpaceScriptManager.cs create mode 100644 srcs/_plugins/Plugin.TimeSpaces/Plugin.TimeSpaces.csproj create mode 100644 srcs/_plugins/Plugin.TimeSpaces/RecurrentJob/TimeSpaceSystem.cs create mode 100644 srcs/_plugins/Plugin.TimeSpaces/STimeSpaceValidator.cs create mode 100644 srcs/_plugins/Plugin.TimeSpaces/Scripting/SAddTimeEventConverter.cs create mode 100644 srcs/_plugins/Plugin.TimeSpaces/Scripting/SCheckForTasksCompletedEventConverter.cs create mode 100644 srcs/_plugins/Plugin.TimeSpaces/Scripting/SClosePortalEventConverter.cs create mode 100644 srcs/_plugins/Plugin.TimeSpaces/Scripting/SDespawnAllMobsInRoomEventConverter.cs create mode 100644 srcs/_plugins/Plugin.TimeSpaces/Scripting/SMonsterSummonEventConverter.cs create mode 100644 srcs/_plugins/Plugin.TimeSpaces/Scripting/SOpenTimeSpacePortalEventConverter.cs create mode 100644 srcs/_plugins/Plugin.TimeSpaces/Scripting/SRemoveItemsEventConverter.cs create mode 100644 srcs/_plugins/Plugin.TimeSpaces/Scripting/STimeSpaceFinishEventConverter.cs create mode 100644 srcs/_plugins/Plugin.TimeSpaces/Scripting/STogglePortalEventConverter.cs create mode 100644 srcs/_plugins/Plugin.TimeSpaces/Scripting/STryStartTaskEventConverter.cs create mode 100644 srcs/_plugins/Plugin.TimeSpaces/Scripting/ScriptSetTimeEventConverter.cs create mode 100644 srcs/_plugins/Plugin.TimeSpaces/TimeSpaceFactory.cs create mode 100644 srcs/_plugins/Plugin.TimeSpaces/TimeSpaceManager.cs create mode 100644 srcs/_plugins/Plugin.TimeSpaces/TimeSpacesPlugin.cs create mode 100644 srcs/_plugins/Plugin.TimeSpaces/TimeSpacesPluginCore.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Act5/Act5OpenNpcRunEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Algorithms/AlgorithmPluginCore.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Algorithms/CellonGenerationAlgorithm.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Algorithms/CharacterAlgorithm/CharacterAlgorithm.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Algorithms/DamageAlgorithm.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Algorithms/ExperienceExtension.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Algorithms/FamilyAlgorithms/FamilyLevelBasedAlgorithm.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Algorithms/ILevelBasedDataAlgorithm.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Algorithms/ShellGenerationAlgorithm.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Algorithms/Shells/ShellCategoryConfiguration.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Algorithms/Shells/ShellLevelEffectConfiguration.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Algorithms/Shells/ShellOptionTypeConfiguration.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Algorithms/Shells/ShellPerfumeConfiguration.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Arena/ArenaManager.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/BCardEffectContextFactory.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/BCardGamePlugin.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/BCardHandlerContainer.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/BCardHandlersServicesExtensions.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/BCardPluginCore.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/BcardEffectContext.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardBeriosHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardBuffHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardCalvinasHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardCaptureHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardCountHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardDarkCloneSummonHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardDestroyerHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardDrainAndStealHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardDrainHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardFearSkillHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardHatusHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardHealingBurningAndCastingHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardHideHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardJumpBackPushHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardKnockdownHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardLightAndShadowHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardMateSummonHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardMeditationSkillHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardMeteoriteTeleportHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardModeHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardMorcosHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardMoveHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardQuestHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardRecoveryAndDamagePercentHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardReflectionHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardSESpecialistHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardSpecialActionsHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardSpecialBehaviorHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardSpecialDamageHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardSpecialEffect2Handler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardSpecialEffectHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardSpecialisationBuffResistance.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardSummonHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardTeleportToLocation.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardTimeTwisterHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/TimeCircleSkillsHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BaseGuriHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Bazaar/BazaarGetListedItemsEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Bazaar/BazaarItemAddEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Bazaar/BazaarItemBuyEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Bazaar/BazaarItemChangePriceEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Bazaar/BazaarItemRemoveEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Bazaar/BazaarManager.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Bazaar/BazaarModuleExtensions.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Bazaar/BazaarOpenUiEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Bazaar/BazaarSearchItemsEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/CharacterLifetimeStats/TotalDamageDealtEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/CharacterLifetimeStats/TotalDeathsEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/CharacterLifetimeStats/TotalGoldDroppedEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/CharacterLifetimeStats/TotalGoldEarnedInBazaarItemsEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/CharacterLifetimeStats/TotalGoldSpentInBazaarFeesEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/CharacterLifetimeStats/TotalGoldSpentInBazaarItemsEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/CharacterLifetimeStats/TotalGoldSpentInNpcShopEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/CharacterLifetimeStats/TotalInstantBattleWonEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/CharacterLifetimeStats/TotalItemsUsedEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/CharacterLifetimeStats/TotalMonstersKilledEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/CharacterLifetimeStats/TotalRaidsLostEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/CharacterLifetimeStats/TotalRaidsWonEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/CharacterLifetimeStats/TotalSkillsCastedEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Chat/ChatSendFriendMessageEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Chat/ChatSpeakerEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Compliments/ComplimentsManager.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Compliments/ComplimentsMonthlyRefreshEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/DbServer/CharacterSaveSystem.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/DbServer/DbServerModuleExtensions.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Entities/GenerateEntityDeathEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Entities/MapJoinMonsterEntityEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Entities/MapJoinNpcEntityEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Entities/MapLeaveMonsterEntityEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Entities/MapLeaveNpcEntityEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Entities/ShopFactory.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Algorithm/GenerateExperienceEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/AntiCheat/GameMasterNotifierStrangeBehaviorEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Battle/ApplyProcessedHitEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Battle/BattleExecuteSkillEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Battle/EntityDamageEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Battle/ProcessBuffEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Battle/ProcessHitEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Buffs/AngelSpecialistElementalBuffEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Buffs/BuffAddEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Buffs/BuffPartnerCheckEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Buffs/BuffRemoveEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/AddAdditionalHpMpEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/AddExpEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/AddStaticBonusEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/BankOpenEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/BattleEntityHealEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/ChangeClassEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/ChangeFactionEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/CharacterBonusExpiredEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/CharacterLoadEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/CharacterPreLoadEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/CharacterRemoveManagersEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/CharacterSaveOnDisconnectEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/GenerateGoldEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/GenerateReputationEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/GetDefaultMorphEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/InviteJoinMinilandEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/KillBonusEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/LevelUpEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/MonsterCaptureEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/NormalChatEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/PartnerKillBonusEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/PlayerChangeChannelAct4EventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/PlayerChangeChannelEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/PlayerDeathEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/PlayerRestEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/PlayerReturnFromAct4EventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/RemoveAdditionalHpMpEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/RemoveItemTimeEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/RollItemBoxEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/SessionSaveEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/SpPerfectEvent.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/SpPerfectEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/SpTransformEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/SpUntransformEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/SpUpgradeEvent.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/SpUpgradeEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/SpecialistRefreshEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/UpgradeItemEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/VehicleCheckMapSpeedEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/VehicleRemoveEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Exchange/ExchangeCloseEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Exchange/ExchangeJoinEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Exchange/ExchangeRegisterEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Exchange/ExchangeTransferItemsEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Groups/GroupActionEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Groups/GroupAddMemberEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Groups/GroupJoinEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Groups/GroupLeaveEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Groups/GroupRemoveMemberEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Groups/GroupWeedingEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Guri/GuriEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Items/CellonUpgradeEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Items/DropItemEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Items/DropRarityConfiguration.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Items/DropRarityConfigurationProvider.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Items/GamblingEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Items/GamblingRarityConfiguration.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Items/GamblingRarityInfo.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Items/ItemSumConfiguration.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Items/ItemSumEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Items/PartnerSpecialistRollConfiguration.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Items/PartnerSpecialistSkillEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Items/PlayerItemToPartnerItemEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Items/SpeedBoosterEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Maps/DisposeMapEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Maps/JoinMapEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Maps/LeaveMapEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Maps/MapActivatedEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Maps/PortalTriggerEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Maps/RemovePortalEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Maps/SpawnPortalEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Mates/MateBackToMinilandEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Mates/MateDeathEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Mates/MateHealEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Mates/MateInitializeEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Mates/MateJoinInMinilandEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Mates/MateJoinTeamEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Mates/MateLeaveTeamEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Mates/MateProcessExperienceEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Mates/MateRemoveEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Mates/MateRestEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Mates/MateReviveEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Mates/MateSpTransformEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Mates/MateSpUntransformEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Mates/MateStayInsideMinilandEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Mates/MateSummonEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Miniland/AddObjMinilandEndLogicEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Miniland/AddObjMinilandEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Miniland/MinigameDurabilityCouponEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Miniland/MinigameDurabilityInfoEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Miniland/MinigameGetYieldInfoEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Miniland/MinigameGetYieldRewardEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Miniland/MinigamePlayEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Miniland/MinigameRepairDurabilityEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Miniland/MinigameRewardEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Miniland/MinigameScoreEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Miniland/MinigameStopEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Miniland/MinilandIntroEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Miniland/MinilandSignPostJoinEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Miniland/MinilandStateEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Miniland/RmvObjMinilandEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Miniland/UseObjMinilandEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Monster/MonsterDeathEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Monster/MonsterSummonEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Npcs/MapNpcGenerateDeathEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Npcs/NpcDialogEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Npcs/NpcSummonEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Relations/AddRelationEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Relations/InvitationEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Relations/RelationBlockEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Relations/RelationFriendEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Relations/RemoveRelationEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/RespawnReturn/RespawnChangeEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/RespawnReturn/RespawnPlayerEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/RespawnReturn/ReturnChangeEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Factories/IGameObjectFactory.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Factories/MapDesignObjectFactory.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Factories/MateTransportFactory.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ForbiddenNames/ReloadableForbiddenNamesManager.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/GameManagerPlugin.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/GameManagersPluginCore.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/GenericEventPluginCore.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/DanceGuriHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/EmoticonGuriHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/FactionSwitchGuriHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/FairyBeadGuriHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/FifthSceneGuriHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/FirstSceneGuriHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/FourthSceneGuriHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/GenerateGuriGuriHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/HarvestGuriHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/IcebreakerEventGuriHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/InstantBattleGuriHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/InteractionGuriHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/MapsTeleportersGuriHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/MeteoreEventGuriHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/MountBeadGuriHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/PartnerBackPackGuriHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/PerfumeGuriHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/PetBasketGuriHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/PositionGuriHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/QuestTeleportDialogGuriHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/RainbowBattleCaptureFlagGuriHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/RainbowBattleRegisterGuriHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/RainbowBattleUnfreezeGuriHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/RelictExaminationEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/ResetSpGuriHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/RollGeneratedItemGuriHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/SecondSceneGuriHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/SheepEventGuriHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/ShellIdGuriHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/ThirdSceneGuriHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/TitleGuriHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/UseBoxGuriHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/WeddingGuriHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/WingsOfFriendshipGuriHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/GuriPlugin.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/GuriPluginCore.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Helpers/GameHelpersPlugin.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/Consumer/BazaarNotificationMessageConsumer.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/Consumer/ChatShoutAdminMessageConsumer.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/Consumer/InterChannelChatMessageBroadcastMessageConsumer.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/Consumer/InterChannelSendChatMsgByCharIdMessageConsumer.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/Consumer/InterChannelSendChatMsgByNicknameMessageConsumer.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/Consumer/InterChannelSendInfoByCharIdMessageConsumer.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/Consumer/InterChannelSendInfoByNicknameMessageConsumer.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/Consumer/InterChannelSendWhisperMessageConsumer.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/Event/ChatShoutAdminEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/Event/InterChannelChatMessageBroadcastEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/Event/InterChannelReceiveWhisperEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/Event/InterChannelSendChatMsgByCharIdEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/Event/InterChannelSendChatMsgByNicknameEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/Event/InterChannelSendInfoByCharIdEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/Event/InterChannelSendInfoByNicknameEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/Event/InterChannelSendWhisperEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/InterChannelModuleExtensions.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Inventory/InventoryAddItemEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Inventory/InventoryDropItemEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Inventory/InventoryEquipItemEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Inventory/InventoryMoveItemEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Inventory/InventoryPickUpItemEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Inventory/InventoryRemoveItemEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Inventory/InventorySortItemEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Inventory/InventoryTakeOffItemEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Inventory/InventoryUseItemEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Inventory/ItemInstanceFactory.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Inventory/PartnerInventoryEquipItemEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Inventory/PartnerInventoryTakeOffItemEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemHandlerContainer.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemHandlerPlugin.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemHandlerPluginCore.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemServiceCollectionExtensions.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Equipment/Box/GameGeneratedMateBeadHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Equipment/Box/MateBeadHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Equipment/Box/RaidBoxHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Equipment/Box/SpHolderHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Equipment/Box/UserBeadHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Magical/Act6PassiveItemHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Magical/BubbleHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Magical/DignityPotionHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Magical/DyeBombHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Magical/GeneralMagicalItemHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Magical/HairDyeHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Magical/HairStyleHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Magical/MagicalBuffPotionHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Magical/ShellItemHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Magical/SpeakerHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Magical/TeamStoneHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Magical/TeleportationItemHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/ProduceItemHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/RefinerItemHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Special/AncelloanBlessingHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Special/FairyBoostHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Special/GuardianAngelHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Special/IceFlowerOilHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Special/LuiniaHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Special/RaidSealHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Special/ReinitializeItemHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Special/ReinitializePartnerSpAllSkillsHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Teacher/IncreaseLevelPartnerHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Teacher/IncreaseLevelPetFoodHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Teacher/MateFoodHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Teacher/MateGuriHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Teacher/MateReleaseHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Teacher/NosMateTrainerHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Teacher/PartnerFoodHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Teacher/PartnerReleaseHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Teacher/PetSummoningScrollHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Teacher/PickUpPetFoodHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Teacher/SteelNetHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Teacher/StrangePartnerFoodHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Teacher/StrangePetFoodHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/UpgradeItemsHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/IItemUsageToggleManager.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/FireworkHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/FoodHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/PotionHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/SnackHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/BackpackHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/ChangePartnerSkinHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/CostumeScrollHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/CupidArrowHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/FactionEggHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/GeneralItemsHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/InventoryExpansionHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/ItemSpawnHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/MagicLampHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/MateSlotExpansionHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/MinilandSignHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/NosBazaarGoldMedalHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/NosBazaarSilverMedalHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/PartnerBackpackHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/PetBasketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/PresentationMessageHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/ReputationMedalHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/SealedVesselHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/SeparationLetterHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/SpPointPotionHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/SpWingHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/SpecialPotionHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/SpecialistSigilHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/SpeedBoosterHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/StatPotionHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/SuctionFunnelHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/TimeSpaceStoneItemHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/VehicleHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/TitleHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Mail/MailCreateEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Mail/MailCreationManager.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Mail/MailOpenEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Mail/MailRemoveEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Mail/NoteCreateEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Mail/NoteOpenEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Mail/NoteRemoveEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Managers/DelayManager.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Managers/GroupManager.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Managers/IMapAttributeFactory.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Managers/MapAttributeFactory.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Managers/RankingManager.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Managers/RevivalManager.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Managers/ScriptedInstanceManager.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Managers/ServerManager.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Managers/SessionManager.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Managers/StaticData/CardsManager.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Managers/StaticData/ItemsManager.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Managers/StaticData/NpcMonsterManager.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Managers/StaticData/SkillsManager.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Miniland/DependencyInjectionExtensions.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Miniland/MinigameManager.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Miniland/Minigames/MinigameRefreshEventProcessor.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Miniland/MinilandManager.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogHandlerContainer.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogPlugin.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogPluginCore.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Act4_Act5/Act4EnterShipHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Act4_Act5/Act4LeaveHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Act4_Act5/Act4LeaveShipHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Act4_Act5/Act5LeaveHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Act4_Act5/Act5LeaveShipHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Act4_Act5/Act5ShipEnterHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Act6/Act6FirstMission.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Act6/TeleportCylloanHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Arena/ArenaMastersRegisterHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Arena/ArenaSpectatorHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Arena/ArenaTalentsRegisterHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Arena/JoinArenaHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/BankSavingBookHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/ChangeClassHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/ChangeSpawnHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/GenerateNpcDialogHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/JewelryWindowHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/JoinLodHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/MateHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/OpenBankHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/OpenNosBazaarHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/OpenWindowHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Quests/QuestAdditionalAct5Handler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Quests/QuestAdditionalBlueHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Quests/QuestAdditionalHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Quests/QuestDailyHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Quests/QuestReceiveHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Quests/QuestReceiveMainHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/RecipeListHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/SP5_SP6/Act5ItemCraftingHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/SP5_SP6/WatterGrotoHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/SP8/Sp8PowderProductionHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/SP8/Sp8SealProductionHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/ShowPlayerShopHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Teleport/TeleportGrenigasSquareHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Teleport/TeleportHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Teleport/TeleportSpRockHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Teleport/WarpTeleportAct5Handler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Teleport/WeedingGazeboHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/TimeSpace/GetPartnerHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/TimeSpace/TimeSpaceDialogHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/TimeSpace/TimeSpaceEnterHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/TimeSpace/TimeSpaceOnFinishDialogHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/TimeSpaceTimerHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/UpgradeItemNpcHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/PlayerEntityFactory.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Quicklist/QuicklistAddEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Quicklist/QuicklistRemoveEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Quicklist/QuicklistSwapEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/RandomFactory.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/RecipeFactory.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Recipes/RecipeOpenWindowEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Revival/RevivalAskEventArenaHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Revival/RevivalAskEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Revival/RevivalEventArenaHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Revival/RevivalEventBaseHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Revival/RevivalEventIceBreakerHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Revival/RevivalEventNormalHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Revival/RevivalStartProcedureEventArenaHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Revival/RevivalStartProcedureEventBaseHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Revival/RevivalStartProcedureEventNormalHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/DropManager.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Drops/DropImportFile.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Drops/DropObject.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Files/IExportableFile.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Files/IFileData.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/ImportObjectExtensions.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/ItemBoxes/ItemBoxImportFile.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/ItemBoxes/RandomBoxCategory.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/ItemBoxes/RandomBoxImportFile.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/ItemBoxes/RandomBoxItem.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/ItemBoxes/RandomBoxObject.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Maps/ConfiguredMapImportFile.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Maps/ConfiguredMapObject.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Monsters/MapMonsterImportFile.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Monsters/MapMonsterObject.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Npcs/MapNpcImportFile.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Npcs/MapNpcObject.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Npcs/MapNpcShopItemObject.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Npcs/MapNpcShopObject.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Npcs/MapNpcShopSkillObject.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Npcs/MapNpcShopTabObject.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Portals/PortalImportFile.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Portals/PortalObject.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Recipes/RecipeImportFile.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Recipes/RecipeItemObject.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Recipes/RecipeObject.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Teleporters/TeleporterImportFile.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Teleporters/TeleporterObject.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ItemBoxManager.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/MapManager.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/MapMonsterManager.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/MapNpcManager.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/RecipeManager.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ShopManager.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/TeleporterManager.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServiceCollectionExtensions.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Ship/Event/ShipEnterEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Ship/Event/ShipLeaveEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Ship/Event/ShipProcessEventAct4Handler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Ship/Event/ShipProcessEventAct5Handler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Ship/Event/ShipProcessEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Ship/RecurrentJob/ShipSystem.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Ship/ShipConfigurationProvider.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Ship/ShipManager.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Ship/ShipModuleExtensions.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Shop/BuyItemNpcShopEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Shop/BuyShopItemEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Shop/BuyShopSkillEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Shop/ShopNpcListItemsEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Shop/ShopPlayerBuyItemEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Shop/ShopPlayerCloseEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Shop/ShopPlayerOpenEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Vehicles/IVehicleConfigurationProvider.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Vehicles/VehicleConfiguration.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Vehicles/VehicleConfigurationProvider.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Vehicles/VehicleMapSpeedConfiguration.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Warehouse/AccountWarehouseAddEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Warehouse/AccountWarehouseManager.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Warehouse/AccountWarehouseMoveEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Warehouse/AccountWarehouseOpenEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Warehouse/AccountWarehouseShowItemEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Warehouse/AccountWarehouseWithdrawEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Warehouse/IWarehouseFactory.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Warehouse/PartnerWarehouseAddItemEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Warehouse/PartnerWarehouseDepositEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Warehouse/PartnerWarehouseMoveEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Warehouse/PartnerWarehouseWithdrawEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Warehouse/Warehouse.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Warehouse/WarehouseExtensions.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Warehouse/WarehouseFactory.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/WingsEmu.Plugins.BasicImplementations.csproj create mode 100644 srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/BotMessages/BotMessageEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/BotMessages/BotMessageMessage.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/BotMessages/Extensions/DependencyInjectionExtensions.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/BotMessages/ScheduledBotMessageConfiguration.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/InterChannel/BazaarNotificationMessage.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/InterChannel/ChatShoutAdminMessage.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/InterChannel/InterChannelChatMessageBroadcastMessage.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/InterChannel/InterChannelSendChatMsgByCharIdMessage.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/InterChannel/InterChannelSendChatMsgByNicknameMessage.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/InterChannel/InterChannelSendInfoByCharIdMessage.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/InterChannel/InterChannelSendInfoByNicknameMessage.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/InterChannel/InterChannelSendWhisperMessage.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Mails/MailReceivePendingOnConnectedMessage.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Mails/MailReceivePendingOnConnectedMessageConsumer.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Mails/MailReceivedMessage.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Mails/MailReceivedMessageConsumer.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Mails/NoteReceivePendingOnConnectedMessage.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Mails/NoteReceivePendingOnConnectedMessageConsumer.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Mails/NoteReceivedMessage.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Mails/NoteReceivedMessageConsumer.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/PlayerEvents/KickAccountMessage.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/PlayerEvents/PlayerConnectedOnChannelMessage.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/PlayerEvents/PlayerDisconnectedChannelMessage.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Relation/RelationCharacterAddMessage.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Relation/RelationCharacterAddMessageConsumer.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Relation/RelationCharacterJoinMessage.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Relation/RelationCharacterJoinMessageConsumer.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Relation/RelationCharacterLeaveMessage.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Relation/RelationCharacterLeaveMessageConsumer.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Relation/RelationCharacterRemoveMessage.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Relation/RelationCharacterRemoveMessageConsumer.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Relation/RelationSendTalkMessage.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Relation/RelationSendTalkMessageConsumer.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/SchedulableConfiguration.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/ScheduledEventPublisherCorePlugin.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/ScheduledEventSubscriberCorePlugin.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/WingsEmu.Plugins.DistributedGameEvents.csproj create mode 100644 srcs/_plugins/WingsEmu.Plugins.Essentials/Account/AccountModule.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.Essentials/Administrator/AdministratorBazaarModule.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.Essentials/Administrator/AdministratorCheatModule_Rune.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.Essentials/Administrator/AdministratorCheatModule_Shell.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.Essentials/Administrator/AdministratorCooldownModule.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.Essentials/Administrator/AdministratorLanguageModule.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.Essentials/Administrator/AdministratorMailModule.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.Essentials/Administrator/AdministratorMaintenanceModule.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.Essentials/Administrator/AdministratorModule.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.Essentials/Administrator/Items/ItemManagement.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.Essentials/Administrator/Items/RefundModule.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.Essentials/EssentialsPlugin.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.Essentials/GameMaster/CharacterModule.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.Essentials/GameMaster/ItemModule.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.Essentials/GameMaster/MonsterSummoningModule.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.Essentials/GameMaster/PunishmentModule.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.Essentials/GameMaster/SearchDataModule.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.Essentials/GameMaster/SkillsModule.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.Essentials/GameMaster/SpecialistModule.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.Essentials/God/GodSetRankModule.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.Essentials/Help/HelpModule.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.Essentials/NPC/BuffPack.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.Essentials/NPC/BuffPackConfiguration.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.Essentials/NPC/BuffPackElement.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.Essentials/NPC/MateCreationModule.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.Essentials/NPC/NPCModule.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.Essentials/Skills/AdministratorCheatModule_Skills.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.Essentials/SuperGameMaster/BetaGameTester.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.Essentials/SuperGameMaster/MinilandModule.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.Essentials/SuperGameMaster/SuperGameMasterMonsterModule.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.Essentials/Teleport/TeleportModule.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.Essentials/WingsEmu.Plugins.Essentials.csproj create mode 100644 srcs/_plugins/WingsEmu.Plugins.GameEvents/CommandModules/GameEventsBasicModule.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.GameEvents/Configuration/InstantBattle/GlobalInstantBattleConfiguration.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.GameEvents/Configuration/InstantBattle/IGlobalInstantBattleConfiguration.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.GameEvents/Configuration/InstantBattle/InstantBattleConfiguration.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.GameEvents/Configuration/InstantBattle/InstantBattleRequirement.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.GameEvents/Configuration/InstantBattle/InstantBattleReward.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.GameEvents/Configuration/InstantBattle/InstantBattleWave.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.GameEvents/Consumers/ComplimentsMonthlyRefreshMessageConsumer.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.GameEvents/Consumers/InstantBattleStartMessageConsumer.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.GameEvents/Consumers/MinigameRefreshProductionPointsMessageConsumer.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.GameEvents/Consumers/QuestDailyRefreshMessageConsumer.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.GameEvents/Consumers/RaidRestrictionRefreshMessageConsumer.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.GameEvents/Consumers/RankingRefreshMessageConsumer.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.GameEvents/Consumers/SpecialistPointsRefreshMessageConsumer.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.GameEvents/Consumers/TranslationsRefreshMessageConsumer.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.GameEvents/DataHolder/InstantBattleInstance.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.GameEvents/Event/Global/GameEvent.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.GameEvents/Event/Global/GameEventLockRegistrationEvent.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.GameEvents/Event/Global/GameEventMatchmakeEvent.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.GameEvents/Event/Global/GameEventPrepareEvent.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.GameEvents/Event/Global/GameEventUnlockRegistrationEvent.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.GameEvents/Event/InstantBattle/InstantBattleDestroyEvent.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.GameEvents/Event/InstantBattle/InstantBattleDropEvent.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.GameEvents/Event/InstantBattle/InstantBattleEvent.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.GameEvents/Event/InstantBattle/InstantBattleRewardEvent.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.GameEvents/Event/InstantBattle/InstantBattleStartWaveEvent.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.GameEvents/EventHandler/Global/GameEventLockRegistrationEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.GameEvents/EventHandler/Global/GameEventMatchmakeEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.GameEvents/EventHandler/Global/GameEventPrepareEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.GameEvents/EventHandler/Global/GameEventUnlockRegistrationEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.GameEvents/EventHandler/InstantBattle/GameEventInstanceProcessEventInstantBattleHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.GameEvents/EventHandler/InstantBattle/GameEventInstanceStartEventInstantBattleHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.GameEvents/EventHandler/InstantBattle/InstantBattleCompleteEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.GameEvents/EventHandler/InstantBattle/InstantBattleDestroyEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.GameEvents/EventHandler/InstantBattle/InstantBattleDropEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.GameEvents/EventHandler/InstantBattle/InstantBattleStartWaveEventHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.GameEvents/GameEventInstanceManager.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.GameEvents/GameEventRegistrationManager.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.GameEvents/GameEventsPlugin.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.GameEvents/GameEventsPluginCore.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.GameEvents/Matchmaking/Filter/InBaseMapFilter.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.GameEvents/Matchmaking/Matchmaker/InstantBattleMatchmaker.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.GameEvents/Matchmaking/Matchmaking.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.GameEvents/Matchmaking/Result/IceBreakerMatchmakingResult.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.GameEvents/Matchmaking/Result/InstantBattleMatchmakingResult.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.GameEvents/RecurrentJob/GameEventSystem.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.GameEvents/WingsEmu.Plugins.GameEvents.csproj create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/CharScreen/CreateBrawlerPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/CharScreen/CreateCharacterPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/CharScreen/CrossServerEntryPointPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/CharScreen/DeleteCharacterPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/CharScreen/EntryPointPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/CharScreen/SelectPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/CharScreenPacketHandlerCorePlugin.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/CharScreenPacketHandlerGamePlugin.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Customization/BaseCharacter.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Customization/BaseInventory.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Customization/BaseQuicklist.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Customization/BaseSkill.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Customization/CustomizationCorePlugin.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Banks/BankManagementPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/ArenaPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/BtkPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/CharacterOptionPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/ComplimentPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/CspPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/CspePacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/DirectionPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/FlPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/GameStartPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/GetGiftPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/GuriPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/HeroPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/ISortPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/NcifPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/NpinfoPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/PreqPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/PstPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/PulsePacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/QSetPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/QtPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/RStartPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/RankSkPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/ReqInfoPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/RevivalPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/SayPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/SitPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/TitleEquipPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/WalkPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/WhisperPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Battle/MultitargetListPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Battle/ObaPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Battle/UseAtSkillPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Battle/UseSkillPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Bazaar/CScaclcPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Bazaar/CSkillPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Bazaar/CbListPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Bazaar/CbuyPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Bazaar/CmodPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Bazaar/CregPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Bazaar/CsListPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Chat/FamilyChatPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Families/CreateFamilyPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Families/FDepositPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Families/FReposPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Families/FWithdrawPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Families/FamilyDisbandPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Families/FamilyFAuthPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Families/FhistCtsPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Families/FmgPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Families/FrankCtsPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Families/FsLogCtsPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Families/GLeavePacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Families/GListPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Families/JoinFamilyPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Families/TodayPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Group/GroupSayPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Group/PJoinPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Group/PLeavePacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Group/RdPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Group/RlPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/BiPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/EquipmentInfoPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/ExcListPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/ExchangeRequestPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/GetPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/MvePacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/MviPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/PutPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/RemovePacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/ReposPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/SortOpenPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/SpTransformPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/SpecialistHolderPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/UpgradePacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/UseItemPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/WarehouseAddPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/WearPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/WearPartnerCardPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/WithdrawPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Mate/PsOpPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Mate/PslPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Mate/PtctlPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Mate/SayPPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Mate/SuctlPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Mate/UpetPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Mate/UpsPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Miniland/AddobjPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Miniland/MiniGamePacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Miniland/MinilandEditPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Miniland/MjoinPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Miniland/RmvobjPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Miniland/UseobjPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Npc/BuyPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Npc/MShopPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Npc/NrunPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Npc/PdtsePacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Npc/RequestNpcPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Npc/SellPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Npc/ShoppingPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Relations/BlDelpacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Relations/BlinsPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Relations/FDelPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Relations/FInsPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/ScriptPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/ScriptedInstance/BscPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/ScriptedInstance/EscapePacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/ScriptedInstance/FbPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/ScriptedInstance/GitPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/ScriptedInstance/MkRaidPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/ScriptedInstance/RselPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/ScriptedInstance/RxitPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/ScriptedInstance/TaCallPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/ScriptedInstance/TawPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/ScriptedInstance/TreqPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/ScriptedInstance/WreqPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Useless/CClosePacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Useless/FStashEndPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Useless/LbsPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Useless/PdtClosePacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Useless/ScpCtsPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Useless/ShopClosePacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Useless/SnapPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Useless/StashEndPacketHandler.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/GamePacketHandlersCorePlugin.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/GamePacketHandlersGamePlugin.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/GenericCharScreenPacketHandlerBase.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/GenericGamePacketHandlerBase.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/GenericPacketHandlerContainer.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.PacketHandling/WingsEmu.Plugins.PacketHandling.csproj diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..318a3b5 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,65 @@ +[*] +charset = utf-8 +end_of_line = crlf +trim_trailing_whitespace = false +insert_final_newline = false +indent_style = space +indent_size = 4 + +# Microsoft .NET properties +csharp_new_line_before_members_in_object_initializers = false +csharp_preferred_modifier_order = public, private, protected, internal, new, abstract, virtual, sealed, override, static, readonly, extern, unsafe, volatile, async:suggestion +csharp_prefer_braces = true:hint +csharp_space_after_cast = false +csharp_style_var_elsewhere = false:error +csharp_style_var_for_built_in_types = false:error +csharp_style_var_when_type_is_apparent = true:error +dotnet_style_predefined_type_for_locals_parameters_members = true:hint +dotnet_style_predefined_type_for_member_access = true:hint +dotnet_style_qualification_for_event = false:hint +dotnet_style_qualification_for_field = false:hint +dotnet_style_qualification_for_method = false:hint +dotnet_style_qualification_for_property = false:hint +dotnet_style_require_accessibility_modifiers = for_non_interface_members:hint + +# ReSharper properties +resharper_align_linq_query = true +resharper_align_multiline_binary_expressions_chain = false +resharper_align_multline_type_parameter_constrains = true +resharper_align_multline_type_parameter_list = true +resharper_apply_on_completion = true +resharper_autodetect_indent_settings = true +resharper_braces_redundant = true +resharper_constructor_or_destructor_body = expression_body +resharper_csharp_max_line_length = 200 +resharper_csharp_stick_comment = false +resharper_indent_type_constraints = false +resharper_local_function_body = expression_body +resharper_method_or_operator_body = expression_body +resharper_parentheses_non_obvious_operations = equality +resharper_place_accessorholder_attribute_on_same_line = False +resharper_space_within_single_line_array_initializer_braces = true +resharper_use_indent_from_vs = false + +# ReSharper inspection severities +resharper_arrange_constructor_or_destructor_body_highlighting = hint +resharper_arrange_local_function_body_highlighting = hint +resharper_arrange_method_or_operator_body_highlighting = hint +resharper_arrange_missing_parentheses_highlighting = hint +resharper_redundant_base_qualifier_highlighting = warning +resharper_web_config_module_not_resolved_highlighting = warning +resharper_web_config_type_not_resolved_highlighting = warning +resharper_web_config_wrong_module_highlighting = warning + +[{*.yml,*.yaml}] +indent_style = space +indent_size = 2 + +[{.eslintrc,.babelrc,.stylelintrc,jest.config,bowerrc,*.jsb3,*.jsb2,*.json}] +indent_style = space +indent_size = 2 + +[*.{appxmanifest,asax,ascx,aspx,build,cs,cshtml,dtd,fs,fsi,fsscript,fsx,master,ml,mli,nuspec,razor,resw,resx,skin,vb,xaml,xamlx,xoml,xsd}] +indent_style = space +indent_size = 4 +tab_width = 4 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1ff0c42 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7d68b5c --- /dev/null +++ b/.gitignore @@ -0,0 +1,660 @@ + +### ASPNETCore ### +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +dist/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +output/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +project.fragment.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/ + +### Csharp ### +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser + +# User-specific files (MonoDevelop/Xamarin Studio) + +# Mono auto generated files +mono_crash.* + +# Build results +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ + +# Visual Studio 2015/2017 cache/options directory +# Uncomment if you have tasks that create the project's static files in wwwroot + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results + +# NUNIT + +# Build Results of an ATL Project + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_h.h +*.iobj +*.ipdb +*_wpftmp.csproj + +# Chutzpah Test files + +# Visual C++ cache files + +# Visual Studio profiler + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace + +# Guidance Automation Toolkit + +# ReSharper is a .NET coding add-in + +# JustCode is a .NET coding add-in + +# TeamCity is a build add-in + +# DotCover is a Code Coverage Tool + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results + +# NCrunch + +# MightyMoose + +# Web workbench (sass) + +# Installshield output folder + +# DocProject is a documentation generator add-in + +# Click-Once directory + +# Publish Web Output +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted + +# NuGet Packages +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files + +# Microsoft Azure Build Output + +# Microsoft Azure Emulator + +# Windows Store app package directories and files +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) + +# RIA/Silverlight projects + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.ndf + +# Business Intelligence projects +*.rptproj.rsuser +*- Backup*.rdl + +# Microsoft Fakes + +# GhostDoc plugin setting file + +# Node.js Tools for Visual Studio + +# Visual Studio 6 build log + +# Visual Studio 6 workspace options file + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output + +# Paket dependency manager + +# FAKE - F# Make + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +### Go ### +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +### Go Patch ### +/vendor/ +/Godeps/ + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history + +### VisualStudio ### + +# User-specific files + +# User-specific files (MonoDevelop/Xamarin Studio) + +# Mono auto generated files + +# Build results + +# Visual Studio 2015/2017 cache/options directory +# Uncomment if you have tasks that create the project's static files in wwwroot + +# Visual Studio 2017 auto generated files + +# MSTest test Results + +# NUNIT + +# Build Results of an ATL Project + +# Benchmark Results + +# .NET Core + +# StyleCop + +# Files built by Visual Studio + +# Chutzpah Test files + +# Visual C++ cache files + +# Visual Studio profiler + +# Visual Studio Trace Files + +# TFS 2012 Local Workspace + +# Guidance Automation Toolkit + +# ReSharper is a .NET coding add-in + +# JustCode is a .NET coding add-in + +# TeamCity is a build add-in + +# DotCover is a Code Coverage Tool + +# AxoCover is a Code Coverage Tool + +# Visual Studio code coverage results + +# NCrunch + +# MightyMoose + +# Web workbench (sass) + +# Installshield output folder + +# DocProject is a documentation generator add-in + +# Click-Once directory + +# Publish Web Output +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted + +# NuGet Packages +# The packages folder can be ignored because of Package Restore +# except build/, which is used as an MSBuild target. +# Uncomment if necessary however generally it will be regenerated when needed +# NuGet v3's project.json files produces more ignorable files + +# Microsoft Azure Build Output + +# Microsoft Azure Emulator + +# Windows Store app package directories and files + +# Visual Studio cache files +# files ending in .cache can be ignored +# but keep track of directories ending in .cache + +# Others + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) + +# RIA/Silverlight projects + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) + +# SQL Server files + +# Business Intelligence projects + +# Microsoft Fakes + +# GhostDoc plugin setting file + +# Node.js Tools for Visual Studio + +# Visual Studio 6 build log + +# Visual Studio 6 workspace options file + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) + +# Visual Studio LightSwitch build output + +# Paket dependency manager + +# FAKE - F# Make + +# CodeRush personal settings + +# Python Tools for Visual Studio (PTVS) + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio + +# Telerik's JustMock configuration file + +# BizTalk build output + +# OpenCover UI analysis results + +# Azure Stream Analytics local run output + +# MSBuild Binary and Structured Log + +# NVidia Nsight GPU debugger configuration file + +# MFractors (Xamarin productivity tool) working folder + +# Local History for Visual Studio + +# BeatPulse healthcheck temp database + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +configs/parse_command.yaml + +# Config files +srcs/GameChannel/config/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..e8fdc1e --- /dev/null +++ b/README.md @@ -0,0 +1,1192 @@ +# Vanosilla - server + +## Disclamier + +If you're here just to get the server running without knowing anything - read the entire [Getting Started](#Getting-Started) section (`Mid-type PC` point below is still valid). + +**Before doing any actions, you should have**: +- some knowledge about: + - object-oriented programming in C#, + - asynchronous programming (Task, async, await), + - LINQ, + - Depedency Injection +- some knowledge about NosTale packet structure +- some knowledge about what SQL, Lua and YAML are +- Mid-type PC: + - Processor: + - Intel Core i5 or higher + - AMD Ryzen 5 or higher + - 8GB of RAM or more + - ~35GB of available disk space + +___ + +## About + +WingsEmu (Nos`WingsEmu`lator) is an emulator for the game NosTale. The source is based on NosWings code, but without any NosWings-specific changes. + +The source code is from July 17th 2021. + +Authors: +- `Blowa` - responsible for the structure of the project, the use of appropriate technologies and tools. +- `Quarry` - responsible for most of the gameplay part - from the Battle System, Algorithms, Relation System, Mail & Note System to Time-Spaces, Rainbow Battle, AI and more. +- `Adanlink` - responsible for NosBazaar, Instant Combat, Act4 Dungeon, Family systems, Mini-games, Database Server. +- `Yoshi` - responsible for Quest System, in-game logs. +- `Roxeez` - responsible for Lua handling, Session and cross-channel rework. +- `Tuskk` - responsible for in-game logs. +- `Allan` - responsible for Logs system. +___ + +## Technologies + +- **PostgreSQL** - player database +- **Redis** - player sessions and player "daily" data caching +- **MongoDB** - player in-game logs +- **gRPC** - connecting RPCs between services +- **EMQX** - service Bus transportation layer broker, MQTT protocol + +___ + +## Getting Started + +Install or have: + +- `IDE` (just one of those is enough): + - [JetBrains Rider](https://www.jetbrains.com/rider) + - [Visual Studio 2022](https://visualstudio.microsoft.com) +- [.NET 5 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/5.0) +- [Docker](https://download.docker.com/win/stable/Docker%20for%20Windows%20Installer.exe) + +___ + +**Additional information for Visual Studio**: + +Extract `properties_for_visual_studio.zip` file and paste each `Properties` directory to each given project. + +I recommend using: +- [JetBrains .NET Resharper](https://www.jetbrains.com/resharper) for better code quality +- [SwitchStartupProject](https://marketplace.visualstudio.com/items?itemName=vs-publisher-141975.SwitchStartupProject) extension to create your own projects startup configs. It will be useful to start more projects at once and later more game channels. + +Download it for your Visual Studio version, install it and restart Visual Studio. + +___ + + +If you want to exceute `.ps1` script files via PowerShell, you have to set execution policy. To do that: +- Run PowerShell as Administrator +- Type `Set-ExecutionPolicy RemoteSigned` and press the [ENTER] key + +You can find more information about it by clicking [here](https://stackoverflow.com/a/4038991). + +___ + +### Docker Installation + +First we need to install [Docker](https://opensource.com/resources/what-docker). Run `Docker for Windows Installer.exe` as Administrator and after some seconds, you should see this window: + +![](https://i.imgur.com/jv2SdvR.png) + +If you want, you can uncheck `Add shortcut to desktop`. Then click the `Ok` button: + +![](https://i.imgur.com/Put6Oza.png) + +After successfully installing Docker, the program will ask you to restart your computer (PC restarting in 2022, yikes). + +![](https://i.imgur.com/aubgD0A.png) + +Once you've restarted your computer, run Docker. Before doing anything, you have to accept Docker's terms - click `I accept the terms` checkbox and click `Accept` button: + +![](https://i.imgur.com/YjvEvmT.png) + +Okay, it's almost over. Docker needs [WSL 2](https://docs.microsoft.com/en-us/windows/wsl/about) for Linux virtualization. + +![](https://i.imgur.com/Xhbddfu.png) + +Go to the [aka.ms/wsl2kernel](https://docs.microsoft.com/en-us/windows/wsl/install-manual#step-4---download-the-linux-kernel-update-package) website and download the installer by clicking `WSL2 Linux kernel update package for x64 machines` link: + +![](https://i.imgur.com/eVlbP78.png) + +Installation is very simple - just run the `wsl_update_x64.msi` installer, click `Next` button and wait for the end and close the installer's window. + +After installation, restart Docker and wait for everything to load. After a short while you should see `Getting Started with Docker` window: + +![](https://i.imgur.com/Xk4KRyF.png) + +Let's skip it using `Skip tutorial` button and... that's it - Congratulations! + +![](https://i.imgur.com/AUY0n7c.png) + +### Running Docker + +To run PostgreSQL, Redis, MongoDB and MQTT Broker for our server, we have to create [Docker containers](https://www.docker.com/resources/what-container). + +- **PowerShell** + - Go to the `.server/scripts/Docker` directory + - For each file in the directory, click right mouse button on the file and choose `Run with PowerShell` + - ![](https://i.imgur.com/1ar8Nkd.png) +- **Terminal** + - Go to the `.server/scripts/Docker` directory + - Open each `.ps1` file, select the entire script (starts with `docker run`) and copy it into Terminal (you can use this script anywhere) and then press [`ENTER`] key: + - ![](https://i.imgur.com/auCrU6q.png) + +After successfully using the commands, you should see 4 new containers in your Docker Hub. + +![](https://i.imgur.com/Lz5O8PL.png) + +___ + +### Running the server + +Finally, we can run the server. First, let's setup multiple startup projects: + + +
+ --- JSON for Visual Studio --- + +```json +{ + "Version": 3, + "ListAllProjects": false, + "MultiProjectConfigurations": + { + "Server": + { + "Projects": + { + "LoginServer": + { + "ProfileName": "LoginServer", + "StartProject": true + }, + "Master": { + "ProfileName": "Master", + "StartProject": true + }, + "DatabaseServer": { + "ProfileName": "DatabaseServer", + "StartProject": true + }, + "TranslationsServer": { + "ProfileName": "TranslationsServer", + "StartProject": true + }, + "FamilyServer": { + "ProfileName": "FamilyServer", + "StartProject": true + }, + "BazaarServer": { + "ProfileName": "BazaarServer", + "StartProject": true + }, + "LogsServer": { + "ProfileName": "LogsServer", + "StartProject": true + }, + "MailServer": { + "ProfileName": "MailServer", + "StartProject": true + }, + "RelationServer": { + "ProfileName": "RelationServer", + "StartProject": true + }, + "Scheduler": { + "ProfileName": "Scheduler", + "StartProject": true + }, + "GameChannel": { + "ProfileName": "GameChannel", + "StartProject": true + } + } + } + } +} +``` +
+ +  + +- **Visual Studio 2022**: + - Click on empty label, expand it and click `Configure...` option: + - ![](https://i.imgur.com/wEhW37W.png) + - Remove generated code and copy all content hidden in the **JSON for Visual Studio** section and paste it to the file: + - ![](https://i.imgur.com/0viABLk.png) + - Save the file using `CTRL + S` keys and you should see new config when you expand the label again: + - ![](https://i.imgur.com/hc306lu.png) + - Now we need to set `Working directory` for each project. To do that, click on small arrow and click ` Debug Properties`: + - ![](https://i.imgur.com/7SUxX3A.png) + - Now set the path to the `dist/`, example: + - `..\server\dist\bazaar-server` + - ![](https://i.imgur.com/SD93YDK.png) + - Close the window and repeat for every executable project. +- **JetBrains Rider**: + - Click `Run` button from the toolbar and choose `Edit Configurations` button: + - ![](https://i.imgur.com/oCXVptO.png) + - Click `+` button, scroll down and choose `Compound` option: + - ![](https://i.imgur.com/WJG7qGG.png) + - Name it whatever you want + - Add these projects by clicking `+` button: + - BazaarServer + - DatabaseServer + - FamilyServer + - GameChannel + - LoginServer + - LogsServer + - MailServer + - Master + - RelationServer + - Scheduler + - TranslationsServer + - ![](https://i.imgur.com/Tw7ZY9W.png) + - ![](https://i.imgur.com/IV2n667.png) + - For each added project, change `Working directory` path to `dist/`, example: + - `.../server/dist/bazaar-server` + - ![](https://i.imgur.com/lWbvgcY.png) + +**Before starting the server, we need to copy resources for our game-server**: +- `./server-files` +- `./server-translations` +- `./client-files` + +Follow given instruction: + +- Open PowerShell in `./server` directory +- Type `.\scripts\update-server-files.ps1` and press [`ENTER`] key + +Next, let's create default accounts to be able to log in: + +- **PowerShell**: + - Build `Toolkit` project + - Go to the `./server` directory + - Type `.\scripts\Database\default-accounts.ps1` and press [`ENTER`] key + +- **Terminal**: + - Build `Toolkit` project + - Go to the `./server/dist/toolkit` directory + - Type `Toolkit.exe create-accounts` and press [`ENTER`] + +Default accounts are: +- Login: `admin` with password: `test` +- Login: `test` with password: `test` + +After that, click magic button `Run` in your IDE. Wait for each project to build up and run. + +If all went well, congratulations - you did it! +If not, check the message from the exception. + +___ + +## Environment Variables + +[Environment Variables](https://docs.microsoft.com/en-us/dotnet/api/system.environment.getenvironmentvariable) are used to changed some data in diffrent environments - for example, we can have diffrent connection to database while being on localhost and diffrent in the production mode - that's why we can just set environment variables without changing anything in source code. + +Example of Environment Variables for the database connection: + +```csharp +Environment.GetEnvironmentVariable("DATABASE_IP") ?? "localhost"; +``` + +The `GetEnvironmentVariable` method will return the string - if `DATABASE_IP` key will be present in env. variables (let's give for example `127.0.0.1`), it will return `"127.0.0.1"` string - if not, the method will return [nullable](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/nullable-reference-types) string. To set default value, we're gonna use [`??`](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/null-coalescing-operator) operator to set `"localhost"` string if that happend. + +We will be using Environment Variables later in this documentation. + +___ + +## IBattleEntity + +`IBattleEntity` is an interface that identifies an entity that can move and attack. There are 4 entities that inherit this interface: + +- `IPlayerEntity` - interface that represents player +- `IMateEntity` - interface that represents player's NosMate +- `IMonsterEntity`- interface that represents monster +- `INpcEntity` - interface that represents NPC + +Each entity contains the same properties: +- Id +- Type +- Position +- Level +- Speed +- Hp - current health +- Maximum Health +- Mp - current mana +- Maximum Mana +- Faction +- Resistances + - Fire + - Water + - Light + - Shadow +- Element +- Element Rate +- Size +...and much more. + +Each `IBattleEntity` also has its own loop system: +- `ICharacterSystem` +- `IMateSystem` +- `IMonsterSystem` +- `INpcSystem` + +Each `IMapInstance` map contains all 4 systems - more information about systems on this below. + +## Entity Component System + +WingsEmu is using [ECS](https://www.guru99.com/entity-component-system.html) for entity. + +Each `IMapInstance` contains list of entity systems. You can find all information about the number of a given entities (number of players, monsters on the map), but also the method responsible for ticking the map. +`ProcessTick` method refreshes all entities on the map every x ms - removing old entities (e.g. when the player has changed the map or when the monster has been killed and will never respawn), but also the AI of monsters / NPCs - finding opponents, attacking, moving etc. + +### Component + +Component helps in keeping order for each entity. In short, instead creating a lot of properties and methods inside entity class, the best solution is creating component to hold some data. + +A list of some most-used player's components: +- `IMateComponent` +- `IBCardComponent` +- `IBuffComponent` +- `IEquipmentOptionContainer` + +___ + +### Creating your own Component + +Let's say you want to save the number of attempts to upgrade your Specialist Cards and equipment. We will store: +- **Specialist Card**: + - Successful attempts + - Failed attempts + - Burnt Souls +- **Equipment**: + - Successful attempts + - Failed attempts + - Level fixed + +First, let's create our new interface and store it in `WingsAPI.Game` project inside `EntityStatistics` directory. My component will have a name `IUpgradeStatisticsComponent`: + +```csharp +public interface IUpgradeStatisticsComponent +{ +} +``` + +Okay and now let's add the data we are interested in to this interface: + +```csharp +public interface IUpgradeStatisticsComponent +{ + ushort SpecialistSuccess { get; set; } + ushort SpecialistFail { get; set; } + ushort SpecialistBurntSouls { get; set; } + + ushort EqupimentSuccess { get; set; } + ushort EqupimentFail { get; set; } + ushort EqupimentLevelFixed { get; set; } +} +``` + +Now it's time to implement this interface into some class, so let's create a new one and name it `UpgradeStatisticsComponent`: + +```csharp +public class UpgradeStatisticsComponent +{ +} +``` + +Now inherit the class with the interface: + +```csharp +public class UpgradeStatisticsComponent : IUpgradeStatisticsComponent +{ +} +``` + +After implementing the methods, the final class should look like this: + +```csharp +public class UpgradeStatisticsComponent : IUpgradeStatisticsComponent +{ + public ushort SpecialistSuccess { get; set; } + public ushort SpecialistFail { get; set; } + public ushort SpecialistBurntSouls { get; set; } + + public ushort EqupimentSuccess { get; set; } + public ushort EqupimentFail { get; set; } + public ushort EqupimentLevelFixed { get; set; } +} +``` + +Great! Now, it's time to add our created component to the `IPlayerEntity` (it's located in `WingsAPI.Data` project under `Characters` directory). + +Go to the end of the file and add our new component: + +```csharp +IUpgradeStatisticsComponent UpgradeStatisticsComponent { get; } +``` + +Now let's move to `PlayerEntity.Stats` class (more information about it below) and implement our component to the class (somewhere at the beginning of the file where all components are stored): + +```csharp +public IUpgradeStatisticsComponent UpgradeStatisticsComponent { get; } +``` + +After that, go to the `PlayerEntity.cs` file and inside the constructor add component: + +```csharp +UpgradeStatisticsComponent = new UpgradeStatisticsComponent(); +``` + +That's it! Great, it's time to use our properties. For demonstration purposes I will only take care of Specialist Card upgrade. + +Go to the `SpUpgradeEventHandler.cs` file and then find `SpUpgrade` method and find `upgradeResult` variable. + +```csharp +SpUpgradeResult upgradeResult = randomBag.GetRandom(); +``` + +There should be a switch underneath it and 4 cases: + +```csharp +switch (upgradeResult) +{ + case SpUpgradeResult.Break when isProtected: + case SpUpgradeResult.Break: + case SpUpgradeResult.Succeed: + case SpUpgradeResult.Fail: +} +``` + +For both `SpUpgradeResult.Break` cases let's increase `SpecialistBurntSouls`: + +```csharp +session.PlayerEntity.UpgradeStatisticsComponent.SpecialistBurntSouls++; +``` + +And for `SpUpgradeResult.Succeed` and `SpUpgradeResult.Fail` appropriate properties: + +**Succeed**: +```csharp +session.PlayerEntity.UpgradeStatisticsComponent.SpecialistSuccess++; +``` + +**Fail**: +```csharp +session.PlayerEntity.UpgradeStatisticsComponent.SpecialistFail++; +``` + +Congratulations, that's it! Everytime when the player will upgrade his Specialist Card, he will be able to track his amount of attempts. + +## Player + +### IClientSession + +When player connects to the server, the `IClientSession` is created. It handles all packets sent and received from player, holds data about TcpSession and `IPlayerEntity` itself. + +While writing various methods, events and commands, you will surely come across `IClientSession`. + +___ + +### IPlayerEntity + +`PlayerEntity` class holds all information about player - the `PlayerEntity` class is separated into 5 [partial](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/partial-classes-and-methods) classes: +- `PlayerEntity` - holds main data about the player +- `PlayerEntity.Family` - holds data about the player's family +- `PlayerEntity.Revival` - holds data about player's revival +- `PlayerEntity.Skills` - holds data about player's skills, cooldowns, skill upgrades etc. +- `PlayerEntity.Stats` - holds generic data like player's statistics, quests, mail, notes, equipment etc. + +___ + +## Commands + +WingsEmu is using [Qmmands](https://github.com/Quahu/Qmmands) to process player's commands. Commands prefixes are `$` and `%`. If you want to add or remove prefixes, go to the `IClientSession.cs` file and modify this line: + +```csharp +private static readonly char[] COMMAND_PREFIX = { '$', '%' }; +``` + +First, let's look at the main base of commands - the module + +```csharp +public class SaltyModuleBase : ModuleBase +{ +} +``` + +`SaltyModuleBase` module will help us to identify and store our commands in different places in the solution. + +I recommend using the name syntax of adding suffix `Module` to created module. +Let's create our module and inherit it with `SaltyModuleBase`. Most of the commands are in `WingsEmu.Plugins.Essentials` project, so we will create our module there as well: + +```csharp +public class MyCommandsModule : SaltyModuleBase +{ +} +``` + +Now when loading a module, all commands inside it will be loaded. Before we even add new commands, let's give this module a name, description and the required authority to execute the commands: + +```csharp +[Name("My commands")] +[Description("This module is related to new commands.")] +[RequireAuthority(AuthorityType.User)] +public class MyCommandsModule : SaltyModuleBase +{ +} +``` + +We can change the authority type so that only Game Master and higher ranks can use these commands: + +```csharp +[RequireAuthority(AuthorityType.GameMaster)] +```` + +Now it's time to create the command. To do that, create a method inside your module and add `Command` attribute with the name of the command: + +```csharp +[Name("My commands")] +[Description("This module is related to new commands.")] +[RequireAuthority(AuthorityType.User)] +public class MyCommandsModule : SaltyModuleBase +{ + [Command("ping")] + public void Ping() + { + } +} +``` + +We can also add a detailed description of what this command is for: + +```csharp +[Name("My commands")] +[Description("This module is related to new commands.")] +[RequireAuthority(AuthorityType.User)] +public class MyCommandsModule : SaltyModuleBase +{ + [Command("ping")] + [Description("This commands send you a pong message.")] + public void Ping() + { + } +} +``` + +Before we turn on the server, we need to load our module to the server. Go to the `EssentialsPlugin.cs` file in `WingsEmu.Plugins.Essentials` namespace and inside `OnLoad` method add this line: + +```csharp +_commands.AddModule(); +``` + +Great, now server knows that there is a command inside this module. When starting the server you should notice that your module and your command have loaded: + +![](https://i.imgur.com/hoBYm4T.png) + + +To use a given command in the game, use any of the command prefixes (`$` or `%`) and the name of the command in the chat. In my case it will look like this: + +``` +$ping +``` + +OK, nothing happened... because `Ping()` doesn't do anything yet. If you put a breakpoint inside the command and type the command in the chat, you will see that it worked and that the breakpoint was caught. + +![](https://i.imgur.com/2Y3TpgG.png) + +Now, it's time to send a message as the command description says. To get the player who executed the command, you can just take it from `SaltyModuleBase` that inherits `WingsEmuIngameCommandContext` and it inherits `CommandContext` - here is all the information about the command. + +Okay, lets pull it out finally. We will use `Context.Player` for this and add it to a variable: + +```csharp +[Command("ping")] +[Description("This command sends you a pong message.")] +public void Ping() +{ + IClientSession session = Context.Player; +} +``` + +Now, let's send him a `Pong` message using green color: + +```csharp +[Command("ping")] +[Description("This command sends you a pong message.")] +public void Ping() +{ + IClientSession session = Context.Player; + session.SendChatMessage("Pong", ChatMessageColorType.Green); +} +``` + +Let's run the server and check the results: + +![](https://i.imgur.com/e5H8cx4.png) + +Congratulations! Now it's time to play with the parameters a bit - let's add our first parameter to the command. + +Let's say the command sends the message x times - let's add parameter of type `byte` and name it `times`. + +```csharp +[Command("ping")] +[Description("This command send you a pong message x times.")] +public void Ping(byte times) +{ +} +``` + +Next let's create a `for` loop that send x times our `Pong` message: + +```csharp +[Command("ping")] +[Description("This command send you a pong message x times.")] +public void Ping(byte times) +{ + IClientSession session = Context.Player; + + for (int i = 0; i < times; i++) + { + session.SendChatMessage($"Pong", ChatMessageColorType.Green); + } +} +``` + +Now it's time to use the command - let's say I want get 5 times the `Pong` message - this time I will use command but with the new parameter: + +``` +$ping 5 +``` + +Let's run the server again and check the results: + +![](https://i.imgur.com/2MUxvQo.png) + +Well done! + +Now it's time to use `SaltyCommandResult` class. Let's suppose something bad happened while executing a command or we were expecting different parameters - the player knows nothing about what went wrong. + +The `SaltyCommandResult` has two parameters in the constructor: +```csharp +public SaltyCommandResult(bool isSuccessful, string message = null) +{ + IsSuccessful = isSuccessful; + Message = message; +} +``` + +- If given command has been executed successfully +- (optional) The final message when the command is successful or not + +Now let's change our method from returning nothing to `SaltyCommandResult` class: + +```csharp +[Command("ping")] +public SaltyCommandResult Ping() +{ +} +``` + +Now we have to always return the `SaltyCommandResult` class - let's return successful result: + +```csharp +[Command("ping")] +public SaltyCommandResult Ping() +{ + return new SaltyCommandResult(true, "Command has been executed successfully! Pong."); +} +``` + +Now, let's return failed executed command: + +```csharp +[Command("ping")] +public SaltyCommandResult Ping() +{ + return new SaltyCommandResult(false, "Oops, something went wrong..."); +} +``` + +I recommend always using `SaltyCommandResult` class as return, because we always know if something bad happened. + +___ + +### Custom Type Parser + +Let's say you want to create your own parameter for your command, because you are tired of constantly checking if a certain monster exists. + +Let's create our own Type Parser sealed class with inherited `TypeParser<>` in `WingsAPI.Commands` in `TypeParsers` directory and named it `MonsterDataTypeParser`: + +```csharp +public sealed class MonsterDataTypeParser : TypeParser +{ +} +``` + +The next step is to implement the `ParseAsync` method: + +```csharp +public sealed class MonsterDataTypeParser : TypeParser +{ + public override ValueTask> ParseAsync(Parameter parameter, string value, CommandContext context) => throw new NotImplementedException(); +} +``` + +and finally, let's find if monster exists: + + +```csharp +public sealed class MonsterDataTypeParser : TypeParser +{ + private readonly INpcMonsterManager _npcMonsterManager; + + public MonsterDataTypeParser(INpcMonsterManager npcMonsterManager) + { + _npcMonsterManager = npcMonsterManager; + } + + public override ValueTask> ParseAsync(Parameter param, string value, CommandContext context) + { + if (!int.TryParse(value, out int monsterVnum)) + { + return new ValueTask>(new TypeParserResult($"Couldn't parse value: {value}.")); + } + + IMonsterData monsterData = _npcMonsterManager.GetNpc(monsterVnum); + + return monsterData is null + ? new ValueTask>(new TypeParserResult($"Monster with given vnum {value} doesn't exist.")) + : new ValueTask>(new TypeParserResult(monsterData)); + } +} +``` + +All that's left is to add a new TypeParser to the `EssentialsPlugin.cs` file in `OnLoad` method: + +```csharp +_commands.AddTypeParser(new MonsterDataTypeParser(_npcMonsterManager)); +``` + +Now, we can check what's speed have a Fox (monster vnum: 1): + +```csharp +// Using $monster 1 +[Command("monster-speed")] +public SaltyCommandResult CheckMonsterSpeed(IMonsterData monsterData) +{ + return new SaltyCommandResult(true, $"Monster vnum: {monsterData.MonsterVNum} have {monsterData.BaseSpeed} speed."); +} +``` + +___ + +### Remainder + +`Remainder` class is an attribute that allows us to set the last parameter of the method. + +Let's say I want to send a message to my friend by using a command: + +```csharp +[Command("message")] +public SaltyCommandResult FriendMessage(IClientSession friend, string message) +{ + IClientSession session = Context.Player; + + friend.SendChatMessage($"{session.PlayerEntity.Name} sent you a message: {message}"); + + return new SaltyCommandResult(true); +} +``` + +Now when I will use command with some message that contains spaces after the `friend` parameter, let's say: + +``` +$message Jacob Hey, thanks for having me! +``` + +It won't execute. Why? It's because command executor is looking for a command `message` with a certain number of parameters. To let him know, just set ``[Remainder]`` attribute before `string message` parameter: + +```csharp +[Command("message")] +public SaltyCommandResult FriendMessage(IClientSession friend, [Remainder] string message) +{ + IClientSession session = Context.Player; + + friend.SendChatMessage($"{session.PlayerEntity.Name} sent you a message: {message}"); + + return new SaltyCommandResult(true); +} +``` + +This time the command executor will know that the `message` parameter is the last one and it will all strings after first parameter. + +___ + +## Events and Event Handlers + +WingsEmu is based on `Event Driven Architecture` - that means everything is based on events and event handlers. In a nutshell, these are global asynchronous methods available for the entire project(s). + +The base of the event is `IAsyncEvent`: + +```csharp +public interface IAsyncEvent +{ +} +``` + +Every `IAsyncEvent` has its own handler or even multiple handlers. + +To create your own event, you have to inherit `IAsyncEvent` in your class. I recommend using the name syntax of adding suffix `Event` to your class: + +```csharp +public class GiveItemsEvent : IAsyncEvent +{ +} +``` + +Of course each event can have its own properties - data that can be later used in an event handler: + +```csharp +public class GiveItemsEvent : IAsyncEvent +{ + public List ItemVnums { get; set; } + public IClientSession Receiver { get; set; } +} +``` + +OK, we have our own event - it's time to create the event handler. + +`IAsyncEventProcessor` is responsible for event handling, where the `T` is the `IAsyncEvent` like our event class. + +Let's create our handler by making a new class `GiveItemsEventHandler` - and again, I recommend using the name syntax of adding suffix `EventHandler` to your class: + +```csharp +public class GiveItemsEventHandler : IAsyncEventProcessor +{ +} +``` + +`IAsyncEventProcessor` interface contains `Task HandleAsync()` method which we need to implement: + +```csharp +public class GiveItemsEventHandler : IAsyncEventProcessor +{ + public async Task HandleAsync(GiveItemsEvent e, CancellationToken cancellation) + { + } +} +``` + +When the event happens, it will go to the `HandleAsync` method of the event handler with the `e` paramether - the event's data that we sent earlier: + +```csharp +public class GiveItemsEventHandler : IAsyncEventProcessor +{ + public async Task HandleAsync(GiveItemsEvent e, CancellationToken cancellation) + { + List itemVnums = e.ItemVnums; + IClientSession receiver = e.Receiver; + } +} +``` + +Now you're probably asking: +- `Okay, everything is ready... but how do you execute this event?` + +The answer is... `IAsyncEventPipeline` - with its help you can trigger an event. + +Let's say I have a command that gives a list of items to some player (for more information about commands, check [Commands](#Commands) section): + +```csharp +[Name("Items Module")] +[RequireAuthority(AuthorityType.SuperGameMaster)] +public class ItemModule : SaltyModuleBase +{ + // Event executor + private readonly IAsyncEventPipeline _asyncEventPipeline; + + public ItemModule(IAsyncEventPipeline asyncEventPipeline) + { + _asyncEventPipeline = asyncEventPipeline; + } + + [Command("give")] + public async Task GiveAsync(IClientSession receiver, string itemVnums) + { + if (string.IsNullOrWhiteSpace(itemVnums)) + { + return new SaltyCommandResult(false, "You must specify an items to give."); + } + + // We will use string for itemVnums to seperate numbers using ; as seperator -> 1;42;50 etc. + string[] itemVnumsArray = itemVnums.Split(';'); + + if (itemVnumsArray.Length == 0) + { + return new SaltyCommandResult(false, "You must specify an items to give."); + } + + var itemVnumsList = new List(); + + foreach (string itemVnum in itemVnumsArray) + { + if (!int.TryParse(itemVnum, out int itemVnumParsed)) + { + continue; + } + + itemVnumsList.Add(itemVnumParsed); + } + + // Create new event + var giveItemsEvent = new GiveItemsEvent() + { + ItemVnums = itemVnumsList, + Receiver = recevier + }; + + // Execute event + await _asyncEventPipeline.ProcessEventAsync(giveItemsEvent); + + return new SaltyCommandResult(true, "Items has been sent!"); + } +} +``` + +Now let's move to the our event handler and let's change it to give items to the receiver: + +```csharp +public class GiveItemsEventHandler : IAsyncEventProcessor +{ + private readonly IGameItemInstanceFactory _gameItemInstanceFactory; + + public GiveItemsEventHandler(IGameItemInstanceFactory gameItemInstanceFactory) + { + _gameItemInstanceFactory = gameItemInstanceFactory; + } + + public async Task HandleAsync(GiveItemsEvent e, CancellationToken cancellation) + { + List itemVnums = e.ItemVnums; + IClientSession receiver = e.Receiver; + + if (receiver is null) + { + return; + } + + if (itemVnums is null || itemVnums.Count < 1) + { + return; + } + + foreach (int itemVnum in itemVnums) + { + GameItemInstance newItem = _gameItemInstanceFactory.CreateItem(itemVnum); + if (newItem is null) + { + // The item couldn't be created because it doesn't exist + continue; + } + + await receiver.AddNewItemToInventory(newItem); + } + } +} +``` + +Congratulations - that's it! Now each time this event is executed, the `receiver` will receive the +items that were added to the list. + +___ + +### PlayerEvent + +`PlayerEvent` is a a base class to create an event for `IClientSession`. Instead of constantly initializing `IAsyncEventPipeline`, we can use `EmitEventAsync` method inside `IClientSession`. + +First, let's check what does `PlayerEvent` contain: + +```csharp +public class PlayerEvent : IAsyncEvent +{ + public IClientSession Sender { get; set; } +} +``` + +As you can see, each time an event is executed, we will have a player who performed that event - and as before, we can add our own data to our own event: + + +```csharp +public class ReportPlayerEvent : PlayerEvent +{ + public string TargetPlayerName { get; set; } + public string Reason { get; set; } +} +``` + +Now, if we want to perform an event, just use `EmitEventAsync` from `IClientSession`: + +```csharp +// Let's say this is me as IClientSession +IClientSession player = me; + +var reportPlayerEvent = new ReportPlayerEvent() +{ + TargetPlayerName = "Jacob", + Reason = "Saying bad words to the Game Master" +}; + +await player.EmitEventAsync(reportPlayerEvent); +``` + +You can even reduce amount of code executing event by doing that: + +```csharp +// Let's say this is me as IClientSession +IClientSession player = me; + +await player.EmitEventAsync(new ReportPlayerEvent() +{ + TargetPlayerName = "Jacob", + Reason = "Saying bad words to the Game Master" +}); +``` + +Of course, the handler for this event will look like this: + +```csharp +public class ReportPlayerEventHandler : IAsyncEventProcessor +{ + private readonly ISessionManager _sessionManager; + + public ReportPlayerEventHandler(ISessionManager sessionManager) + { + _sessionManager = sessionManager; + } + + public async Task HandleAsync(ReportPlayerEvent e, CancellationToken cancellation) + { + IClientSession sender = e.Sender; // Player who executed this event + string targetPlayerName = e.TargetPlayerName; + string reason = e.Reason; + + if (string.IsNullOrEmpty(targetPlayerName)) + { + return; + } + + if (string.IsNullOrEmpty(reason)) + { + return; + } + + // Find player's session in current channel + IClientSession target = _sessionManager.GetSessionByCharacterName(targetPlayerName); + + // If player is offline + if (target is null) + { + return; + } + + // Create final reason to the Game Master + string finalReason = $"{sender.PlayerEntity.Name} reported {target.PlayerEntity.Name}, reason: {reason}"; + + // This method will send a chat message to the Game Masters + await target.NotifyStrangeBehavior(StrangeBehaviorSeverity.NORMAL, finalReason); + } +} +``` + +When you will be creating new events, you will almost always use `PlayerEvent` instead of` IAsyncEvent` class for the player. + +___ + +### IBattleEntityEvent + +`IBattleEntityEvent` is a a base class to create an event for `IBattleEntity`. + +```csharp +public interface IBattleEntityEvent : IAsyncEvent +{ + IBattleEntity Entity { get; } +} +``` + +Examples of using this version of the event are e.g. death of a entity, attacking etc. + +___ + +## New Game Channels + +To run additional channel(s) for our server, we need to create an executable profile. Before do that, let's check what [Environment Variables](#Environment-Variables) are available - we can find them in `WorldServerSingleton` class: + +- `GAME_SERVER_IP` - channel IP +- `GAME_SERVER_PORT` - channel port +- `GAME_SERVER_GROUP` - channel server group +- `GAME_SERVER_SESSION_LIMIT` - channel session limit +- `GAME_SERVER_CHANNEL_ID` - channel ID +- `GAME_SERVER_CHANNEL_TYPE` - channel type (PVE_NORMAL or ACT_4) +- `GAME_SERVER_AUTHORITY` - channel required authority to join the channel + +To create basic channel, we need to change: +- `GAME_SERVER_PORT`, +- `GAME_SERVER_CHANNEL_ID` + +and one more thing for Kestrel port: + +- `HTTP_LISTEN_PORT` + +In example I will create second channel, so my env. my variables will look like this: + +- `GAME_SERVER_PORT` = 8001, +- `GAME_SERVER_CHANNEL_ID` = 2, +- `HTTP_LISTEN_PORT` = 17501 + +  + +- **Visual Studio 2022**: + - Select `GameChannel` project, click small arrow and choose `GameChannel Debug Properties` option: + - ![](https://i.imgur.com/9VQxuXU.png) + - Click first button named `Create a new profile` and choose second option `Executable`: + - ![](https://i.imgur.com/3QA3uaW.png) + - Select your `Exectuable` and `Working directory` path: + - ![](https://i.imgur.com/AeTzXuk.png) + - Scroll down and find `Environment variables` part - now we need to create own env. variables. As description says, we need to seperating each variable using comma `,`. My variables will look like this: + - `GAME_SERVER_PORT=8001,GAME_SERVER_CHANNEL_ID=2,HTTP_LISTEN_PORT=17501` + - ![](https://i.imgur.com/V00WLXA.png) + - Let's change name of this profile to know what channel it is. Click last button in the menu and change it whatever you want (I will name it `Channel 2`): + - ![](https://i.imgur.com/al45dNa.png) + - Now we have to add our created channel to the startup projects. Expand our config label and click `Configure...` option: + - ![](https://i.imgur.com/lu2393h.png) + - Select given part of the code, copy and paste it below: + - ![](https://i.imgur.com/LhOsYoV.png) + - ![](https://i.imgur.com/pXzWmoO.png) + - Rename it as your profile name in both places and save the file: + - ![](https://i.imgur.com/zAKfttC.png) +- **JetBrains Rider**: + - Click `Run` button from the toolbar and choose `Edit Configurations` button: + - ![](https://i.imgur.com/oCXVptO.png) + - Click `+` button and choose `.NET Project` option: + - ![](https://i.imgur.com/rbYFHFJ.png) + - Let's change the name and choose `GameChannel` in `Project` section: + - ![](https://i.imgur.com/l9rEP8h.png) + - Change `Working directory` path to `dist/game-server`: + - ![](https://i.imgur.com/1cNJNNN.png) + - Now it's time to set env. variables. The seperator between variables is `;`, so my variables will look like this: + - `GAME_SERVER_PORT=8001;GAME_SERVER_CHANNEL_ID=2;HTTP_LISTEN_PORT=17501` + - ![](https://i.imgur.com/vhZWjYp.png) + - Apply changes. To add created `.NET Project` to your compound, just click `+` button and choose the project: + - ![](https://i.imgur.com/rYr9SPf.png) + +### Act 4 Channel + +To run Act 4 channel, follow steps above but set these env. variables: + +- `GAME_SERVER_PORT` = 8051, +- `GAME_SERVER_CHANNEL_ID` = 51, +- `GAME_SERVER_CHANNEL_TYPE` = ACT_4, +- `HTTP_LISTEN_PORT` = 17551 + +Environment Variables strings: + +- **Visual Studio 2022** - `GAME_SERVER_PORT=8051,GAME_SERVER_CHANNEL_ID=51,GAME_SERVER_CHANNEL_TYPE=ACT_4,HTTP_LISTEN_PORT=17551` +- **JetBrains Rider** - `GAME_SERVER_PORT=8051;GAME_SERVER_CHANNEL_ID=51;GAME_SERVER_CHANNEL_TYPE=ACT_4;HTTP_LISTEN_PORT=17551` \ No newline at end of file diff --git a/WingsEmu.sln b/WingsEmu.sln new file mode 100644 index 0000000..b39625f --- /dev/null +++ b/WingsEmu.sln @@ -0,0 +1,867 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31919.166 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Executables", "Executables", "{0D88B727-CA5D-48DB-835B-E90C8E282DA7}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WingsEmu.Communication.gRPC", "srcs\WingsEmu.Communication.gRPC\WingsEmu.Communication.gRPC.csproj", "{15948D13-B424-4DF3-B341-36CCB97D4D9F}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "WingsAPI", "WingsAPI", "{404B2AB4-CAFA-4AF7-9593-F231A25A973C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WingsAPI.Packets", "srcs\WingsAPI.Packets\WingsAPI.Packets.csproj", "{596A8193-7A5D-4055-BB8C-D2F27E1C6B94}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WingsAPI.Data", "srcs\WingsAPI.Data\WingsAPI.Data.csproj", "{0F1F75EF-8755-499B-8A28-F6653EB29FED}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WingsAPI.Game", "srcs\WingsAPI.Game\WingsAPI.Game.csproj", "{295450C6-C37F-4AA7-A59A-1BFFF352D0EA}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Toolkit", "srcs\Toolkit\Toolkit.csproj", "{05451402-CA5D-4474-A8BE-8E534AD17748}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{823AD0CF-E764-4113-A166-6A615A4B928F}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Plugins", "Plugins", "{F872A038-D325-4718-B0A0-4CBCEA3714ED}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WingsAPI.Game.Extensions", "srcs\WingsAPI.Game.Extensions\WingsAPI.Game.Extensions.csproj", "{D425AE9F-F25D-4B64-9983-FE76778524A7}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Plugin.DB.EF", "srcs\_plugins\Plugin.DB.EF\Plugin.DB.EF.csproj", "{8ED45EF3-0271-4867-9330-80D300029D8C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WingsAPI.Commands", "srcs\WingsAPI.Commands\WingsAPI.Commands.csproj", "{B0D05BBF-F7FA-46EC-9224-182B2B98C4A3}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WingsEmu.Plugins.Essentials", "srcs\_plugins\WingsEmu.Plugins.Essentials\WingsEmu.Plugins.Essentials.csproj", "{3F789B98-5A34-41CF-B4F0-70B03C0C5ED2}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Master", "srcs\Master\Master.csproj", "{19A46232-72C5-4BB7-AE6B-A9062BE259A0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GameChannel", "srcs\GameChannel\GameChannel.csproj", "{42DE9E8C-747E-46A4-9AC5-BB7ED1425BF0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LoginServer", "srcs\LoginServer\LoginServer.csproj", "{D4506804-B9A1-4E1A-B97C-0F815814FE62}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WingsEmu.Plugins.BasicImplementations", "srcs\_plugins\WingsEmu.Plugins.BasicImplementation\WingsEmu.Plugins.BasicImplementations.csproj", "{D6B19207-B3E3-4B53-9964-19DEFB2089EF}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WingsEmu.Plugins.PacketHandling", "srcs\_plugins\WingsEmu.Plugins.PacketHandling\WingsEmu.Plugins.PacketHandling.csproj", "{F839354B-16C3-4EEF-8872-768B6D832BB2}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WingsEmu.Plugins.DistributedGameEvents", "srcs\_plugins\WingsEmu.Plugins.DistributedGameEvents\WingsEmu.Plugins.DistributedGameEvents.csproj", "{5707003E-0835-4E7A-BAD8-E55F4AE0D583}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WingsAPI.Communication", "srcs\WingsAPI.Communication\WingsAPI.Communication.csproj", "{CA5FFE88-A5FF-42D3-9E88-220A594AC27F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WingsEmu.Plugins.GameEvents", "srcs\_plugins\WingsEmu.Plugins.GameEvents\WingsEmu.Plugins.GameEvents.csproj", "{A57EA67A-133B-4217-BE17-9E25622A95DD}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Scheduler", "srcs\Scheduler\Scheduler.csproj", "{105F39CC-E6C1-4365-B200-75D451E5747F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DiscordNotifier", "srcs\DiscordNotifier\DiscordNotifier.csproj", "{B84EBC46-1CF7-47C9-B38E-F4919A11D6D0}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "srcs", "srcs", "{322AAFB0-04E1-42B9-B9F1-B836E12E1FCD}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FamilyServer", "srcs\FamilyServer\FamilyServer.csproj", "{5F2C76AE-81B2-4A24-80D2-086EFC41627B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DatabaseServer", "srcs\DatabaseServer\DatabaseServer.csproj", "{18F61C6F-997C-459F-A3CE-63AEE311315E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LogsServer", "srcs\LogsServer\LogsServer.csproj", "{7B5A36CE-10AE-4915-84C2-61C205F4C004}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MailServer", "srcs\MailServer\MailServer.csproj", "{58B6177B-2C56-4B37-88AB-8C454767D427}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BazaarServer", "srcs\BazaarServer\BazaarServer.csproj", "{61DB7D8D-7903-47EB-85FA-1E21001C5EAE}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WingsAPI.Scripting.LUA", "srcs\WingsAPI.Scripting.LUA\WingsAPI.Scripting.LUA.csproj", "{D7F1A231-791A-4B34-A9B2-21282EADA82C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RelationServer", "srcs\RelationServer\RelationServer.csproj", "{42CDD6F5-A858-4A32-8982-E5A63689CD58}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Plugin.PlayerLogs", "srcs\_plugins\Plugin.PlayerLogs\Plugin.PlayerLogs.csproj", "{779A5516-5209-491C-878A-63086DB3F566}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Plugin.Raids", "srcs\_plugins\Plugin.Raids\Plugin.Raids.csproj", "{8293EEC9-C747-46C3-B3D2-30C552620269}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Plugin.Act4", "srcs\_plugins\Plugin.Act4\Plugin.Act4.csproj", "{DA4852B6-2FBD-4E5C-972C-049626152E4E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WingsAPI.Scripting", "srcs\WingsAPI.Scripting\WingsAPI.Scripting.csproj", "{CCBE134E-2DAB-4CE7-B9F2-4F315CDEF1FA}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Plugin.TimeSpaces", "srcs\_plugins\Plugin.TimeSpaces\Plugin.TimeSpaces.csproj", "{E4E9646F-7B85-4446-9B6D-846D32BAC3F5}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Plugin.CoreImpl", "srcs\_plugins\Plugin.CoreImpl\Plugin.CoreImpl.csproj", "{3038B4F6-2CB4-4124-86EB-FE8D7FB3A0D7}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Plugin.FamilyImpl", "srcs\_plugins\Plugin.FamilyImpl\Plugin.FamilyImpl.csproj", "{E27BC1CD-D6DF-4EEE-B340-5424BE442AC5}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Plugin.QuestImpl", "srcs\_plugins\Plugin.QuestImpl\Plugin.QuestImpl.csproj", "{793F6EA6-6842-4E8F-908D-C7423734E385}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WingsAPI.Plugins", "srcs\WingsAPI.Plugins\WingsAPI.Plugins.csproj", "{8D22A73C-581D-4A85-917F-C56DFB386C6F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WingsEmu.Health", "srcs\WingsEmu.Health\WingsEmu.Health.csproj", "{DA7F7106-23D0-4DC2-8B71-43A15A6BBDE8}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WingsAPI.Packets.Handling", "srcs\WingsAPI.Packets.Handling\WingsAPI.Packets.Handling.csproj", "{9892ED34-E662-48A2-95AB-F1655412081C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Plugin.MongoLogs", "srcs\_plugins\Plugin.MongoLogs\Plugin.MongoLogs.csproj", "{55094771-469E-4E1A-8E7B-C7B915E023D0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Plugin.ResourceLoader", "srcs\_plugins\Plugin.ResourceLoader\Plugin.ResourceLoader.csproj", "{50A948F4-EF18-4591-8CA9-564909B630A1}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TranslationsServer", "srcs\TranslationsServer\TranslationsServer.csproj", "{A3BDBA44-A720-48FA-8914-736B79830CE9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Plugin.RainbowBattle", "srcs\Plugin.RainbowBattle\Plugin.RainbowBattle.csproj", "{192ECC49-3C79-48CD-A49B-296CC47546F5}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "PheonixLib", "PheonixLib", "{368BD8A7-67D2-4F24-B655-C3B82BBDA840}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PhoenixLib.Auth.JWT", "srcs\PhoenixLib.Auth.JWT\PhoenixLib.Auth.JWT.csproj", "{FAC76C2D-D586-4231-BF60-7766969BBD60}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PhoenixLib.Caching", "srcs\PhoenixLib.Caching\PhoenixLib.Caching.csproj", "{1E379202-AE6A-4A0A-BFE9-C1C0D4605143}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PhoenixLib.Configuration", "srcs\PhoenixLib.Configuration\PhoenixLib.Configuration.csproj", "{45104F9C-A786-442B-8202-B965530AE20E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PhoenixLib.DAL.Abstractions", "srcs\PhoenixLib.DAL.Abstractions\PhoenixLib.DAL.Abstractions.csproj", "{2A8189FE-0E9B-474F-91E7-A92C7C302A43}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PhoenixLib.DAL.EFCore.PGSQL", "srcs\PhoenixLib.DAL.EFCore.PGSQL\PhoenixLib.DAL.EFCore.PGSQL.csproj", "{00EE8632-007B-406B-9CF8-F0CE38A5FEB0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PhoenixLib.DAL.MongoDB", "srcs\PhoenixLib.DAL.MongoDB\PhoenixLib.DAL.MongoDB.csproj", "{1565B041-B687-4EF1-B27D-B0F9A927847B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PhoenixLib.DAL.Redis", "srcs\PhoenixLib.DAL.Redis\PhoenixLib.DAL.Redis.csproj", "{E444200C-5B5D-42C0-88C5-D0E2C11A393F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PhoenixLib.Events", "srcs\PhoenixLib.Events\PhoenixLib.Events.csproj", "{28D2FC99-9DB4-4F15-A99E-F5A76DE7B13A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PhoenixLib.Extensions", "srcs\PhoenixLib.Extensions\PhoenixLib.Extensions.csproj", "{F81D94AB-E336-405D-B7E3-8AC97EE4E758}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PhoenixLib.Logging", "srcs\PhoenixLib.Logging\PhoenixLib.Logging.csproj", "{192C9441-BA98-41D4-A27E-D7DE95BB46F7}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PhoenixLib.Messaging", "srcs\PhoenixLib.Messaging\PhoenixLib.Messaging.csproj", "{D592C239-AFAD-4596-8FE9-BBEA4977B258}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PhoenixLib.Multilanguage", "srcs\PhoenixLib.Multilanguage\PhoenixLib.Multilanguage.csproj", "{33AA9C01-0E8E-4960-AE8C-550C3B13F84A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PhoenixLib.Scheduler", "srcs\PhoenixLib.Scheduler\PhoenixLib.Scheduler.csproj", "{FD79C25F-87CD-47C6-87B2-F497AEA4A12D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PhoenixLib.Scheduler.ReactiveX", "srcs\PhoenixLib.Scheduler.ReactiveX\PhoenixLib.Scheduler.ReactiveX.csproj", "{F723B0BB-C5EA-4EBE-B3D5-50682F45EA80}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {15948D13-B424-4DF3-B341-36CCB97D4D9F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {15948D13-B424-4DF3-B341-36CCB97D4D9F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {15948D13-B424-4DF3-B341-36CCB97D4D9F}.Debug|x64.ActiveCfg = Debug|Any CPU + {15948D13-B424-4DF3-B341-36CCB97D4D9F}.Debug|x64.Build.0 = Debug|Any CPU + {15948D13-B424-4DF3-B341-36CCB97D4D9F}.Debug|x86.ActiveCfg = Debug|Any CPU + {15948D13-B424-4DF3-B341-36CCB97D4D9F}.Debug|x86.Build.0 = Debug|Any CPU + {15948D13-B424-4DF3-B341-36CCB97D4D9F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {15948D13-B424-4DF3-B341-36CCB97D4D9F}.Release|Any CPU.Build.0 = Release|Any CPU + {15948D13-B424-4DF3-B341-36CCB97D4D9F}.Release|x64.ActiveCfg = Release|Any CPU + {15948D13-B424-4DF3-B341-36CCB97D4D9F}.Release|x64.Build.0 = Release|Any CPU + {15948D13-B424-4DF3-B341-36CCB97D4D9F}.Release|x86.ActiveCfg = Release|Any CPU + {15948D13-B424-4DF3-B341-36CCB97D4D9F}.Release|x86.Build.0 = Release|Any CPU + {596A8193-7A5D-4055-BB8C-D2F27E1C6B94}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {596A8193-7A5D-4055-BB8C-D2F27E1C6B94}.Debug|Any CPU.Build.0 = Debug|Any CPU + {596A8193-7A5D-4055-BB8C-D2F27E1C6B94}.Debug|x64.ActiveCfg = Debug|Any CPU + {596A8193-7A5D-4055-BB8C-D2F27E1C6B94}.Debug|x64.Build.0 = Debug|Any CPU + {596A8193-7A5D-4055-BB8C-D2F27E1C6B94}.Debug|x86.ActiveCfg = Debug|Any CPU + {596A8193-7A5D-4055-BB8C-D2F27E1C6B94}.Debug|x86.Build.0 = Debug|Any CPU + {596A8193-7A5D-4055-BB8C-D2F27E1C6B94}.Release|Any CPU.ActiveCfg = Release|Any CPU + {596A8193-7A5D-4055-BB8C-D2F27E1C6B94}.Release|Any CPU.Build.0 = Release|Any CPU + {596A8193-7A5D-4055-BB8C-D2F27E1C6B94}.Release|x64.ActiveCfg = Release|Any CPU + {596A8193-7A5D-4055-BB8C-D2F27E1C6B94}.Release|x64.Build.0 = Release|Any CPU + {596A8193-7A5D-4055-BB8C-D2F27E1C6B94}.Release|x86.ActiveCfg = Release|Any CPU + {596A8193-7A5D-4055-BB8C-D2F27E1C6B94}.Release|x86.Build.0 = Release|Any CPU + {0F1F75EF-8755-499B-8A28-F6653EB29FED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0F1F75EF-8755-499B-8A28-F6653EB29FED}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0F1F75EF-8755-499B-8A28-F6653EB29FED}.Debug|x64.ActiveCfg = Debug|Any CPU + {0F1F75EF-8755-499B-8A28-F6653EB29FED}.Debug|x64.Build.0 = Debug|Any CPU + {0F1F75EF-8755-499B-8A28-F6653EB29FED}.Debug|x86.ActiveCfg = Debug|Any CPU + {0F1F75EF-8755-499B-8A28-F6653EB29FED}.Debug|x86.Build.0 = Debug|Any CPU + {0F1F75EF-8755-499B-8A28-F6653EB29FED}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0F1F75EF-8755-499B-8A28-F6653EB29FED}.Release|Any CPU.Build.0 = Release|Any CPU + {0F1F75EF-8755-499B-8A28-F6653EB29FED}.Release|x64.ActiveCfg = Release|Any CPU + {0F1F75EF-8755-499B-8A28-F6653EB29FED}.Release|x64.Build.0 = Release|Any CPU + {0F1F75EF-8755-499B-8A28-F6653EB29FED}.Release|x86.ActiveCfg = Release|Any CPU + {0F1F75EF-8755-499B-8A28-F6653EB29FED}.Release|x86.Build.0 = Release|Any CPU + {295450C6-C37F-4AA7-A59A-1BFFF352D0EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {295450C6-C37F-4AA7-A59A-1BFFF352D0EA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {295450C6-C37F-4AA7-A59A-1BFFF352D0EA}.Debug|x64.ActiveCfg = Debug|Any CPU + {295450C6-C37F-4AA7-A59A-1BFFF352D0EA}.Debug|x64.Build.0 = Debug|Any CPU + {295450C6-C37F-4AA7-A59A-1BFFF352D0EA}.Debug|x86.ActiveCfg = Debug|Any CPU + {295450C6-C37F-4AA7-A59A-1BFFF352D0EA}.Debug|x86.Build.0 = Debug|Any CPU + {295450C6-C37F-4AA7-A59A-1BFFF352D0EA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {295450C6-C37F-4AA7-A59A-1BFFF352D0EA}.Release|Any CPU.Build.0 = Release|Any CPU + {295450C6-C37F-4AA7-A59A-1BFFF352D0EA}.Release|x64.ActiveCfg = Release|Any CPU + {295450C6-C37F-4AA7-A59A-1BFFF352D0EA}.Release|x64.Build.0 = Release|Any CPU + {295450C6-C37F-4AA7-A59A-1BFFF352D0EA}.Release|x86.ActiveCfg = Release|Any CPU + {295450C6-C37F-4AA7-A59A-1BFFF352D0EA}.Release|x86.Build.0 = Release|Any CPU + {05451402-CA5D-4474-A8BE-8E534AD17748}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {05451402-CA5D-4474-A8BE-8E534AD17748}.Debug|Any CPU.Build.0 = Debug|Any CPU + {05451402-CA5D-4474-A8BE-8E534AD17748}.Debug|x64.ActiveCfg = Debug|Any CPU + {05451402-CA5D-4474-A8BE-8E534AD17748}.Debug|x64.Build.0 = Debug|Any CPU + {05451402-CA5D-4474-A8BE-8E534AD17748}.Debug|x86.ActiveCfg = Debug|Any CPU + {05451402-CA5D-4474-A8BE-8E534AD17748}.Debug|x86.Build.0 = Debug|Any CPU + {05451402-CA5D-4474-A8BE-8E534AD17748}.Release|Any CPU.ActiveCfg = Release|Any CPU + {05451402-CA5D-4474-A8BE-8E534AD17748}.Release|Any CPU.Build.0 = Release|Any CPU + {05451402-CA5D-4474-A8BE-8E534AD17748}.Release|x64.ActiveCfg = Release|Any CPU + {05451402-CA5D-4474-A8BE-8E534AD17748}.Release|x64.Build.0 = Release|Any CPU + {05451402-CA5D-4474-A8BE-8E534AD17748}.Release|x86.ActiveCfg = Release|Any CPU + {05451402-CA5D-4474-A8BE-8E534AD17748}.Release|x86.Build.0 = Release|Any CPU + {D425AE9F-F25D-4B64-9983-FE76778524A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D425AE9F-F25D-4B64-9983-FE76778524A7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D425AE9F-F25D-4B64-9983-FE76778524A7}.Debug|x64.ActiveCfg = Debug|Any CPU + {D425AE9F-F25D-4B64-9983-FE76778524A7}.Debug|x64.Build.0 = Debug|Any CPU + {D425AE9F-F25D-4B64-9983-FE76778524A7}.Debug|x86.ActiveCfg = Debug|Any CPU + {D425AE9F-F25D-4B64-9983-FE76778524A7}.Debug|x86.Build.0 = Debug|Any CPU + {D425AE9F-F25D-4B64-9983-FE76778524A7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D425AE9F-F25D-4B64-9983-FE76778524A7}.Release|Any CPU.Build.0 = Release|Any CPU + {D425AE9F-F25D-4B64-9983-FE76778524A7}.Release|x64.ActiveCfg = Release|Any CPU + {D425AE9F-F25D-4B64-9983-FE76778524A7}.Release|x64.Build.0 = Release|Any CPU + {D425AE9F-F25D-4B64-9983-FE76778524A7}.Release|x86.ActiveCfg = Release|Any CPU + {D425AE9F-F25D-4B64-9983-FE76778524A7}.Release|x86.Build.0 = Release|Any CPU + {8ED45EF3-0271-4867-9330-80D300029D8C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8ED45EF3-0271-4867-9330-80D300029D8C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8ED45EF3-0271-4867-9330-80D300029D8C}.Debug|x64.ActiveCfg = Debug|Any CPU + {8ED45EF3-0271-4867-9330-80D300029D8C}.Debug|x64.Build.0 = Debug|Any CPU + {8ED45EF3-0271-4867-9330-80D300029D8C}.Debug|x86.ActiveCfg = Debug|Any CPU + {8ED45EF3-0271-4867-9330-80D300029D8C}.Debug|x86.Build.0 = Debug|Any CPU + {8ED45EF3-0271-4867-9330-80D300029D8C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8ED45EF3-0271-4867-9330-80D300029D8C}.Release|Any CPU.Build.0 = Release|Any CPU + {8ED45EF3-0271-4867-9330-80D300029D8C}.Release|x64.ActiveCfg = Release|Any CPU + {8ED45EF3-0271-4867-9330-80D300029D8C}.Release|x64.Build.0 = Release|Any CPU + {8ED45EF3-0271-4867-9330-80D300029D8C}.Release|x86.ActiveCfg = Release|Any CPU + {8ED45EF3-0271-4867-9330-80D300029D8C}.Release|x86.Build.0 = Release|Any CPU + {B0D05BBF-F7FA-46EC-9224-182B2B98C4A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B0D05BBF-F7FA-46EC-9224-182B2B98C4A3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B0D05BBF-F7FA-46EC-9224-182B2B98C4A3}.Debug|x64.ActiveCfg = Debug|Any CPU + {B0D05BBF-F7FA-46EC-9224-182B2B98C4A3}.Debug|x64.Build.0 = Debug|Any CPU + {B0D05BBF-F7FA-46EC-9224-182B2B98C4A3}.Debug|x86.ActiveCfg = Debug|Any CPU + {B0D05BBF-F7FA-46EC-9224-182B2B98C4A3}.Debug|x86.Build.0 = Debug|Any CPU + {B0D05BBF-F7FA-46EC-9224-182B2B98C4A3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B0D05BBF-F7FA-46EC-9224-182B2B98C4A3}.Release|Any CPU.Build.0 = Release|Any CPU + {B0D05BBF-F7FA-46EC-9224-182B2B98C4A3}.Release|x64.ActiveCfg = Release|Any CPU + {B0D05BBF-F7FA-46EC-9224-182B2B98C4A3}.Release|x64.Build.0 = Release|Any CPU + {B0D05BBF-F7FA-46EC-9224-182B2B98C4A3}.Release|x86.ActiveCfg = Release|Any CPU + {B0D05BBF-F7FA-46EC-9224-182B2B98C4A3}.Release|x86.Build.0 = Release|Any CPU + {3F789B98-5A34-41CF-B4F0-70B03C0C5ED2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3F789B98-5A34-41CF-B4F0-70B03C0C5ED2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3F789B98-5A34-41CF-B4F0-70B03C0C5ED2}.Debug|x64.ActiveCfg = Debug|Any CPU + {3F789B98-5A34-41CF-B4F0-70B03C0C5ED2}.Debug|x64.Build.0 = Debug|Any CPU + {3F789B98-5A34-41CF-B4F0-70B03C0C5ED2}.Debug|x86.ActiveCfg = Debug|Any CPU + {3F789B98-5A34-41CF-B4F0-70B03C0C5ED2}.Debug|x86.Build.0 = Debug|Any CPU + {3F789B98-5A34-41CF-B4F0-70B03C0C5ED2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3F789B98-5A34-41CF-B4F0-70B03C0C5ED2}.Release|Any CPU.Build.0 = Release|Any CPU + {3F789B98-5A34-41CF-B4F0-70B03C0C5ED2}.Release|x64.ActiveCfg = Release|Any CPU + {3F789B98-5A34-41CF-B4F0-70B03C0C5ED2}.Release|x64.Build.0 = Release|Any CPU + {3F789B98-5A34-41CF-B4F0-70B03C0C5ED2}.Release|x86.ActiveCfg = Release|Any CPU + {3F789B98-5A34-41CF-B4F0-70B03C0C5ED2}.Release|x86.Build.0 = Release|Any CPU + {19A46232-72C5-4BB7-AE6B-A9062BE259A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {19A46232-72C5-4BB7-AE6B-A9062BE259A0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {19A46232-72C5-4BB7-AE6B-A9062BE259A0}.Debug|x64.ActiveCfg = Debug|Any CPU + {19A46232-72C5-4BB7-AE6B-A9062BE259A0}.Debug|x64.Build.0 = Debug|Any CPU + {19A46232-72C5-4BB7-AE6B-A9062BE259A0}.Debug|x86.ActiveCfg = Debug|Any CPU + {19A46232-72C5-4BB7-AE6B-A9062BE259A0}.Debug|x86.Build.0 = Debug|Any CPU + {19A46232-72C5-4BB7-AE6B-A9062BE259A0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {19A46232-72C5-4BB7-AE6B-A9062BE259A0}.Release|Any CPU.Build.0 = Release|Any CPU + {19A46232-72C5-4BB7-AE6B-A9062BE259A0}.Release|x64.ActiveCfg = Release|Any CPU + {19A46232-72C5-4BB7-AE6B-A9062BE259A0}.Release|x64.Build.0 = Release|Any CPU + {19A46232-72C5-4BB7-AE6B-A9062BE259A0}.Release|x86.ActiveCfg = Release|Any CPU + {19A46232-72C5-4BB7-AE6B-A9062BE259A0}.Release|x86.Build.0 = Release|Any CPU + {42DE9E8C-747E-46A4-9AC5-BB7ED1425BF0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {42DE9E8C-747E-46A4-9AC5-BB7ED1425BF0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {42DE9E8C-747E-46A4-9AC5-BB7ED1425BF0}.Debug|x64.ActiveCfg = Debug|Any CPU + {42DE9E8C-747E-46A4-9AC5-BB7ED1425BF0}.Debug|x64.Build.0 = Debug|Any CPU + {42DE9E8C-747E-46A4-9AC5-BB7ED1425BF0}.Debug|x86.ActiveCfg = Debug|Any CPU + {42DE9E8C-747E-46A4-9AC5-BB7ED1425BF0}.Debug|x86.Build.0 = Debug|Any CPU + {42DE9E8C-747E-46A4-9AC5-BB7ED1425BF0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {42DE9E8C-747E-46A4-9AC5-BB7ED1425BF0}.Release|Any CPU.Build.0 = Release|Any CPU + {42DE9E8C-747E-46A4-9AC5-BB7ED1425BF0}.Release|x64.ActiveCfg = Release|Any CPU + {42DE9E8C-747E-46A4-9AC5-BB7ED1425BF0}.Release|x64.Build.0 = Release|Any CPU + {42DE9E8C-747E-46A4-9AC5-BB7ED1425BF0}.Release|x86.ActiveCfg = Release|Any CPU + {42DE9E8C-747E-46A4-9AC5-BB7ED1425BF0}.Release|x86.Build.0 = Release|Any CPU + {D4506804-B9A1-4E1A-B97C-0F815814FE62}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D4506804-B9A1-4E1A-B97C-0F815814FE62}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D4506804-B9A1-4E1A-B97C-0F815814FE62}.Debug|x64.ActiveCfg = Debug|Any CPU + {D4506804-B9A1-4E1A-B97C-0F815814FE62}.Debug|x64.Build.0 = Debug|Any CPU + {D4506804-B9A1-4E1A-B97C-0F815814FE62}.Debug|x86.ActiveCfg = Debug|Any CPU + {D4506804-B9A1-4E1A-B97C-0F815814FE62}.Debug|x86.Build.0 = Debug|Any CPU + {D4506804-B9A1-4E1A-B97C-0F815814FE62}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D4506804-B9A1-4E1A-B97C-0F815814FE62}.Release|Any CPU.Build.0 = Release|Any CPU + {D4506804-B9A1-4E1A-B97C-0F815814FE62}.Release|x64.ActiveCfg = Release|Any CPU + {D4506804-B9A1-4E1A-B97C-0F815814FE62}.Release|x64.Build.0 = Release|Any CPU + {D4506804-B9A1-4E1A-B97C-0F815814FE62}.Release|x86.ActiveCfg = Release|Any CPU + {D4506804-B9A1-4E1A-B97C-0F815814FE62}.Release|x86.Build.0 = Release|Any CPU + {D6B19207-B3E3-4B53-9964-19DEFB2089EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D6B19207-B3E3-4B53-9964-19DEFB2089EF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D6B19207-B3E3-4B53-9964-19DEFB2089EF}.Debug|x64.ActiveCfg = Debug|Any CPU + {D6B19207-B3E3-4B53-9964-19DEFB2089EF}.Debug|x64.Build.0 = Debug|Any CPU + {D6B19207-B3E3-4B53-9964-19DEFB2089EF}.Debug|x86.ActiveCfg = Debug|Any CPU + {D6B19207-B3E3-4B53-9964-19DEFB2089EF}.Debug|x86.Build.0 = Debug|Any CPU + {D6B19207-B3E3-4B53-9964-19DEFB2089EF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D6B19207-B3E3-4B53-9964-19DEFB2089EF}.Release|Any CPU.Build.0 = Release|Any CPU + {D6B19207-B3E3-4B53-9964-19DEFB2089EF}.Release|x64.ActiveCfg = Release|Any CPU + {D6B19207-B3E3-4B53-9964-19DEFB2089EF}.Release|x64.Build.0 = Release|Any CPU + {D6B19207-B3E3-4B53-9964-19DEFB2089EF}.Release|x86.ActiveCfg = Release|Any CPU + {D6B19207-B3E3-4B53-9964-19DEFB2089EF}.Release|x86.Build.0 = Release|Any CPU + {F839354B-16C3-4EEF-8872-768B6D832BB2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F839354B-16C3-4EEF-8872-768B6D832BB2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F839354B-16C3-4EEF-8872-768B6D832BB2}.Debug|x64.ActiveCfg = Debug|Any CPU + {F839354B-16C3-4EEF-8872-768B6D832BB2}.Debug|x64.Build.0 = Debug|Any CPU + {F839354B-16C3-4EEF-8872-768B6D832BB2}.Debug|x86.ActiveCfg = Debug|Any CPU + {F839354B-16C3-4EEF-8872-768B6D832BB2}.Debug|x86.Build.0 = Debug|Any CPU + {F839354B-16C3-4EEF-8872-768B6D832BB2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F839354B-16C3-4EEF-8872-768B6D832BB2}.Release|Any CPU.Build.0 = Release|Any CPU + {F839354B-16C3-4EEF-8872-768B6D832BB2}.Release|x64.ActiveCfg = Release|Any CPU + {F839354B-16C3-4EEF-8872-768B6D832BB2}.Release|x64.Build.0 = Release|Any CPU + {F839354B-16C3-4EEF-8872-768B6D832BB2}.Release|x86.ActiveCfg = Release|Any CPU + {F839354B-16C3-4EEF-8872-768B6D832BB2}.Release|x86.Build.0 = Release|Any CPU + {5707003E-0835-4E7A-BAD8-E55F4AE0D583}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5707003E-0835-4E7A-BAD8-E55F4AE0D583}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5707003E-0835-4E7A-BAD8-E55F4AE0D583}.Debug|x64.ActiveCfg = Debug|Any CPU + {5707003E-0835-4E7A-BAD8-E55F4AE0D583}.Debug|x64.Build.0 = Debug|Any CPU + {5707003E-0835-4E7A-BAD8-E55F4AE0D583}.Debug|x86.ActiveCfg = Debug|Any CPU + {5707003E-0835-4E7A-BAD8-E55F4AE0D583}.Debug|x86.Build.0 = Debug|Any CPU + {5707003E-0835-4E7A-BAD8-E55F4AE0D583}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5707003E-0835-4E7A-BAD8-E55F4AE0D583}.Release|Any CPU.Build.0 = Release|Any CPU + {5707003E-0835-4E7A-BAD8-E55F4AE0D583}.Release|x64.ActiveCfg = Release|Any CPU + {5707003E-0835-4E7A-BAD8-E55F4AE0D583}.Release|x64.Build.0 = Release|Any CPU + {5707003E-0835-4E7A-BAD8-E55F4AE0D583}.Release|x86.ActiveCfg = Release|Any CPU + {5707003E-0835-4E7A-BAD8-E55F4AE0D583}.Release|x86.Build.0 = Release|Any CPU + {CA5FFE88-A5FF-42D3-9E88-220A594AC27F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CA5FFE88-A5FF-42D3-9E88-220A594AC27F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CA5FFE88-A5FF-42D3-9E88-220A594AC27F}.Debug|x64.ActiveCfg = Debug|Any CPU + {CA5FFE88-A5FF-42D3-9E88-220A594AC27F}.Debug|x64.Build.0 = Debug|Any CPU + {CA5FFE88-A5FF-42D3-9E88-220A594AC27F}.Debug|x86.ActiveCfg = Debug|Any CPU + {CA5FFE88-A5FF-42D3-9E88-220A594AC27F}.Debug|x86.Build.0 = Debug|Any CPU + {CA5FFE88-A5FF-42D3-9E88-220A594AC27F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CA5FFE88-A5FF-42D3-9E88-220A594AC27F}.Release|Any CPU.Build.0 = Release|Any CPU + {CA5FFE88-A5FF-42D3-9E88-220A594AC27F}.Release|x64.ActiveCfg = Release|Any CPU + {CA5FFE88-A5FF-42D3-9E88-220A594AC27F}.Release|x64.Build.0 = Release|Any CPU + {CA5FFE88-A5FF-42D3-9E88-220A594AC27F}.Release|x86.ActiveCfg = Release|Any CPU + {CA5FFE88-A5FF-42D3-9E88-220A594AC27F}.Release|x86.Build.0 = Release|Any CPU + {A57EA67A-133B-4217-BE17-9E25622A95DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A57EA67A-133B-4217-BE17-9E25622A95DD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A57EA67A-133B-4217-BE17-9E25622A95DD}.Debug|x64.ActiveCfg = Debug|Any CPU + {A57EA67A-133B-4217-BE17-9E25622A95DD}.Debug|x64.Build.0 = Debug|Any CPU + {A57EA67A-133B-4217-BE17-9E25622A95DD}.Debug|x86.ActiveCfg = Debug|Any CPU + {A57EA67A-133B-4217-BE17-9E25622A95DD}.Debug|x86.Build.0 = Debug|Any CPU + {A57EA67A-133B-4217-BE17-9E25622A95DD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A57EA67A-133B-4217-BE17-9E25622A95DD}.Release|Any CPU.Build.0 = Release|Any CPU + {A57EA67A-133B-4217-BE17-9E25622A95DD}.Release|x64.ActiveCfg = Release|Any CPU + {A57EA67A-133B-4217-BE17-9E25622A95DD}.Release|x64.Build.0 = Release|Any CPU + {A57EA67A-133B-4217-BE17-9E25622A95DD}.Release|x86.ActiveCfg = Release|Any CPU + {A57EA67A-133B-4217-BE17-9E25622A95DD}.Release|x86.Build.0 = Release|Any CPU + {105F39CC-E6C1-4365-B200-75D451E5747F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {105F39CC-E6C1-4365-B200-75D451E5747F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {105F39CC-E6C1-4365-B200-75D451E5747F}.Debug|x64.ActiveCfg = Debug|Any CPU + {105F39CC-E6C1-4365-B200-75D451E5747F}.Debug|x64.Build.0 = Debug|Any CPU + {105F39CC-E6C1-4365-B200-75D451E5747F}.Debug|x86.ActiveCfg = Debug|Any CPU + {105F39CC-E6C1-4365-B200-75D451E5747F}.Debug|x86.Build.0 = Debug|Any CPU + {105F39CC-E6C1-4365-B200-75D451E5747F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {105F39CC-E6C1-4365-B200-75D451E5747F}.Release|Any CPU.Build.0 = Release|Any CPU + {105F39CC-E6C1-4365-B200-75D451E5747F}.Release|x64.ActiveCfg = Release|Any CPU + {105F39CC-E6C1-4365-B200-75D451E5747F}.Release|x64.Build.0 = Release|Any CPU + {105F39CC-E6C1-4365-B200-75D451E5747F}.Release|x86.ActiveCfg = Release|Any CPU + {105F39CC-E6C1-4365-B200-75D451E5747F}.Release|x86.Build.0 = Release|Any CPU + {B84EBC46-1CF7-47C9-B38E-F4919A11D6D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B84EBC46-1CF7-47C9-B38E-F4919A11D6D0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B84EBC46-1CF7-47C9-B38E-F4919A11D6D0}.Debug|x64.ActiveCfg = Debug|Any CPU + {B84EBC46-1CF7-47C9-B38E-F4919A11D6D0}.Debug|x64.Build.0 = Debug|Any CPU + {B84EBC46-1CF7-47C9-B38E-F4919A11D6D0}.Debug|x86.ActiveCfg = Debug|Any CPU + {B84EBC46-1CF7-47C9-B38E-F4919A11D6D0}.Debug|x86.Build.0 = Debug|Any CPU + {B84EBC46-1CF7-47C9-B38E-F4919A11D6D0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B84EBC46-1CF7-47C9-B38E-F4919A11D6D0}.Release|Any CPU.Build.0 = Release|Any CPU + {B84EBC46-1CF7-47C9-B38E-F4919A11D6D0}.Release|x64.ActiveCfg = Release|Any CPU + {B84EBC46-1CF7-47C9-B38E-F4919A11D6D0}.Release|x64.Build.0 = Release|Any CPU + {B84EBC46-1CF7-47C9-B38E-F4919A11D6D0}.Release|x86.ActiveCfg = Release|Any CPU + {B84EBC46-1CF7-47C9-B38E-F4919A11D6D0}.Release|x86.Build.0 = Release|Any CPU + {5F2C76AE-81B2-4A24-80D2-086EFC41627B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5F2C76AE-81B2-4A24-80D2-086EFC41627B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5F2C76AE-81B2-4A24-80D2-086EFC41627B}.Debug|x64.ActiveCfg = Debug|Any CPU + {5F2C76AE-81B2-4A24-80D2-086EFC41627B}.Debug|x64.Build.0 = Debug|Any CPU + {5F2C76AE-81B2-4A24-80D2-086EFC41627B}.Debug|x86.ActiveCfg = Debug|Any CPU + {5F2C76AE-81B2-4A24-80D2-086EFC41627B}.Debug|x86.Build.0 = Debug|Any CPU + {5F2C76AE-81B2-4A24-80D2-086EFC41627B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5F2C76AE-81B2-4A24-80D2-086EFC41627B}.Release|Any CPU.Build.0 = Release|Any CPU + {5F2C76AE-81B2-4A24-80D2-086EFC41627B}.Release|x64.ActiveCfg = Release|Any CPU + {5F2C76AE-81B2-4A24-80D2-086EFC41627B}.Release|x64.Build.0 = Release|Any CPU + {5F2C76AE-81B2-4A24-80D2-086EFC41627B}.Release|x86.ActiveCfg = Release|Any CPU + {5F2C76AE-81B2-4A24-80D2-086EFC41627B}.Release|x86.Build.0 = Release|Any CPU + {18F61C6F-997C-459F-A3CE-63AEE311315E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {18F61C6F-997C-459F-A3CE-63AEE311315E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {18F61C6F-997C-459F-A3CE-63AEE311315E}.Debug|x64.ActiveCfg = Debug|Any CPU + {18F61C6F-997C-459F-A3CE-63AEE311315E}.Debug|x64.Build.0 = Debug|Any CPU + {18F61C6F-997C-459F-A3CE-63AEE311315E}.Debug|x86.ActiveCfg = Debug|Any CPU + {18F61C6F-997C-459F-A3CE-63AEE311315E}.Debug|x86.Build.0 = Debug|Any CPU + {18F61C6F-997C-459F-A3CE-63AEE311315E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {18F61C6F-997C-459F-A3CE-63AEE311315E}.Release|Any CPU.Build.0 = Release|Any CPU + {18F61C6F-997C-459F-A3CE-63AEE311315E}.Release|x64.ActiveCfg = Release|Any CPU + {18F61C6F-997C-459F-A3CE-63AEE311315E}.Release|x64.Build.0 = Release|Any CPU + {18F61C6F-997C-459F-A3CE-63AEE311315E}.Release|x86.ActiveCfg = Release|Any CPU + {18F61C6F-997C-459F-A3CE-63AEE311315E}.Release|x86.Build.0 = Release|Any CPU + {7B5A36CE-10AE-4915-84C2-61C205F4C004}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7B5A36CE-10AE-4915-84C2-61C205F4C004}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7B5A36CE-10AE-4915-84C2-61C205F4C004}.Debug|x64.ActiveCfg = Debug|Any CPU + {7B5A36CE-10AE-4915-84C2-61C205F4C004}.Debug|x64.Build.0 = Debug|Any CPU + {7B5A36CE-10AE-4915-84C2-61C205F4C004}.Debug|x86.ActiveCfg = Debug|Any CPU + {7B5A36CE-10AE-4915-84C2-61C205F4C004}.Debug|x86.Build.0 = Debug|Any CPU + {7B5A36CE-10AE-4915-84C2-61C205F4C004}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7B5A36CE-10AE-4915-84C2-61C205F4C004}.Release|Any CPU.Build.0 = Release|Any CPU + {7B5A36CE-10AE-4915-84C2-61C205F4C004}.Release|x64.ActiveCfg = Release|Any CPU + {7B5A36CE-10AE-4915-84C2-61C205F4C004}.Release|x64.Build.0 = Release|Any CPU + {7B5A36CE-10AE-4915-84C2-61C205F4C004}.Release|x86.ActiveCfg = Release|Any CPU + {7B5A36CE-10AE-4915-84C2-61C205F4C004}.Release|x86.Build.0 = Release|Any CPU + {58B6177B-2C56-4B37-88AB-8C454767D427}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {58B6177B-2C56-4B37-88AB-8C454767D427}.Debug|Any CPU.Build.0 = Debug|Any CPU + {58B6177B-2C56-4B37-88AB-8C454767D427}.Debug|x64.ActiveCfg = Debug|Any CPU + {58B6177B-2C56-4B37-88AB-8C454767D427}.Debug|x64.Build.0 = Debug|Any CPU + {58B6177B-2C56-4B37-88AB-8C454767D427}.Debug|x86.ActiveCfg = Debug|Any CPU + {58B6177B-2C56-4B37-88AB-8C454767D427}.Debug|x86.Build.0 = Debug|Any CPU + {58B6177B-2C56-4B37-88AB-8C454767D427}.Release|Any CPU.ActiveCfg = Release|Any CPU + {58B6177B-2C56-4B37-88AB-8C454767D427}.Release|Any CPU.Build.0 = Release|Any CPU + {58B6177B-2C56-4B37-88AB-8C454767D427}.Release|x64.ActiveCfg = Release|Any CPU + {58B6177B-2C56-4B37-88AB-8C454767D427}.Release|x64.Build.0 = Release|Any CPU + {58B6177B-2C56-4B37-88AB-8C454767D427}.Release|x86.ActiveCfg = Release|Any CPU + {58B6177B-2C56-4B37-88AB-8C454767D427}.Release|x86.Build.0 = Release|Any CPU + {61DB7D8D-7903-47EB-85FA-1E21001C5EAE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {61DB7D8D-7903-47EB-85FA-1E21001C5EAE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {61DB7D8D-7903-47EB-85FA-1E21001C5EAE}.Debug|x64.ActiveCfg = Debug|Any CPU + {61DB7D8D-7903-47EB-85FA-1E21001C5EAE}.Debug|x64.Build.0 = Debug|Any CPU + {61DB7D8D-7903-47EB-85FA-1E21001C5EAE}.Debug|x86.ActiveCfg = Debug|Any CPU + {61DB7D8D-7903-47EB-85FA-1E21001C5EAE}.Debug|x86.Build.0 = Debug|Any CPU + {61DB7D8D-7903-47EB-85FA-1E21001C5EAE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {61DB7D8D-7903-47EB-85FA-1E21001C5EAE}.Release|Any CPU.Build.0 = Release|Any CPU + {61DB7D8D-7903-47EB-85FA-1E21001C5EAE}.Release|x64.ActiveCfg = Release|Any CPU + {61DB7D8D-7903-47EB-85FA-1E21001C5EAE}.Release|x64.Build.0 = Release|Any CPU + {61DB7D8D-7903-47EB-85FA-1E21001C5EAE}.Release|x86.ActiveCfg = Release|Any CPU + {61DB7D8D-7903-47EB-85FA-1E21001C5EAE}.Release|x86.Build.0 = Release|Any CPU + {D7F1A231-791A-4B34-A9B2-21282EADA82C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D7F1A231-791A-4B34-A9B2-21282EADA82C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D7F1A231-791A-4B34-A9B2-21282EADA82C}.Debug|x64.ActiveCfg = Debug|Any CPU + {D7F1A231-791A-4B34-A9B2-21282EADA82C}.Debug|x64.Build.0 = Debug|Any CPU + {D7F1A231-791A-4B34-A9B2-21282EADA82C}.Debug|x86.ActiveCfg = Debug|Any CPU + {D7F1A231-791A-4B34-A9B2-21282EADA82C}.Debug|x86.Build.0 = Debug|Any CPU + {D7F1A231-791A-4B34-A9B2-21282EADA82C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D7F1A231-791A-4B34-A9B2-21282EADA82C}.Release|Any CPU.Build.0 = Release|Any CPU + {D7F1A231-791A-4B34-A9B2-21282EADA82C}.Release|x64.ActiveCfg = Release|Any CPU + {D7F1A231-791A-4B34-A9B2-21282EADA82C}.Release|x64.Build.0 = Release|Any CPU + {D7F1A231-791A-4B34-A9B2-21282EADA82C}.Release|x86.ActiveCfg = Release|Any CPU + {D7F1A231-791A-4B34-A9B2-21282EADA82C}.Release|x86.Build.0 = Release|Any CPU + {42CDD6F5-A858-4A32-8982-E5A63689CD58}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {42CDD6F5-A858-4A32-8982-E5A63689CD58}.Debug|Any CPU.Build.0 = Debug|Any CPU + {42CDD6F5-A858-4A32-8982-E5A63689CD58}.Debug|x64.ActiveCfg = Debug|Any CPU + {42CDD6F5-A858-4A32-8982-E5A63689CD58}.Debug|x64.Build.0 = Debug|Any CPU + {42CDD6F5-A858-4A32-8982-E5A63689CD58}.Debug|x86.ActiveCfg = Debug|Any CPU + {42CDD6F5-A858-4A32-8982-E5A63689CD58}.Debug|x86.Build.0 = Debug|Any CPU + {42CDD6F5-A858-4A32-8982-E5A63689CD58}.Release|Any CPU.ActiveCfg = Release|Any CPU + {42CDD6F5-A858-4A32-8982-E5A63689CD58}.Release|Any CPU.Build.0 = Release|Any CPU + {42CDD6F5-A858-4A32-8982-E5A63689CD58}.Release|x64.ActiveCfg = Release|Any CPU + {42CDD6F5-A858-4A32-8982-E5A63689CD58}.Release|x64.Build.0 = Release|Any CPU + {42CDD6F5-A858-4A32-8982-E5A63689CD58}.Release|x86.ActiveCfg = Release|Any CPU + {42CDD6F5-A858-4A32-8982-E5A63689CD58}.Release|x86.Build.0 = Release|Any CPU + {779A5516-5209-491C-878A-63086DB3F566}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {779A5516-5209-491C-878A-63086DB3F566}.Debug|Any CPU.Build.0 = Debug|Any CPU + {779A5516-5209-491C-878A-63086DB3F566}.Debug|x64.ActiveCfg = Debug|Any CPU + {779A5516-5209-491C-878A-63086DB3F566}.Debug|x64.Build.0 = Debug|Any CPU + {779A5516-5209-491C-878A-63086DB3F566}.Debug|x86.ActiveCfg = Debug|Any CPU + {779A5516-5209-491C-878A-63086DB3F566}.Debug|x86.Build.0 = Debug|Any CPU + {779A5516-5209-491C-878A-63086DB3F566}.Release|Any CPU.ActiveCfg = Release|Any CPU + {779A5516-5209-491C-878A-63086DB3F566}.Release|Any CPU.Build.0 = Release|Any CPU + {779A5516-5209-491C-878A-63086DB3F566}.Release|x64.ActiveCfg = Release|Any CPU + {779A5516-5209-491C-878A-63086DB3F566}.Release|x64.Build.0 = Release|Any CPU + {779A5516-5209-491C-878A-63086DB3F566}.Release|x86.ActiveCfg = Release|Any CPU + {779A5516-5209-491C-878A-63086DB3F566}.Release|x86.Build.0 = Release|Any CPU + {8293EEC9-C747-46C3-B3D2-30C552620269}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8293EEC9-C747-46C3-B3D2-30C552620269}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8293EEC9-C747-46C3-B3D2-30C552620269}.Debug|x64.ActiveCfg = Debug|Any CPU + {8293EEC9-C747-46C3-B3D2-30C552620269}.Debug|x64.Build.0 = Debug|Any CPU + {8293EEC9-C747-46C3-B3D2-30C552620269}.Debug|x86.ActiveCfg = Debug|Any CPU + {8293EEC9-C747-46C3-B3D2-30C552620269}.Debug|x86.Build.0 = Debug|Any CPU + {8293EEC9-C747-46C3-B3D2-30C552620269}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8293EEC9-C747-46C3-B3D2-30C552620269}.Release|Any CPU.Build.0 = Release|Any CPU + {8293EEC9-C747-46C3-B3D2-30C552620269}.Release|x64.ActiveCfg = Release|Any CPU + {8293EEC9-C747-46C3-B3D2-30C552620269}.Release|x64.Build.0 = Release|Any CPU + {8293EEC9-C747-46C3-B3D2-30C552620269}.Release|x86.ActiveCfg = Release|Any CPU + {8293EEC9-C747-46C3-B3D2-30C552620269}.Release|x86.Build.0 = Release|Any CPU + {DA4852B6-2FBD-4E5C-972C-049626152E4E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DA4852B6-2FBD-4E5C-972C-049626152E4E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DA4852B6-2FBD-4E5C-972C-049626152E4E}.Debug|x64.ActiveCfg = Debug|Any CPU + {DA4852B6-2FBD-4E5C-972C-049626152E4E}.Debug|x64.Build.0 = Debug|Any CPU + {DA4852B6-2FBD-4E5C-972C-049626152E4E}.Debug|x86.ActiveCfg = Debug|Any CPU + {DA4852B6-2FBD-4E5C-972C-049626152E4E}.Debug|x86.Build.0 = Debug|Any CPU + {DA4852B6-2FBD-4E5C-972C-049626152E4E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DA4852B6-2FBD-4E5C-972C-049626152E4E}.Release|Any CPU.Build.0 = Release|Any CPU + {DA4852B6-2FBD-4E5C-972C-049626152E4E}.Release|x64.ActiveCfg = Release|Any CPU + {DA4852B6-2FBD-4E5C-972C-049626152E4E}.Release|x64.Build.0 = Release|Any CPU + {DA4852B6-2FBD-4E5C-972C-049626152E4E}.Release|x86.ActiveCfg = Release|Any CPU + {DA4852B6-2FBD-4E5C-972C-049626152E4E}.Release|x86.Build.0 = Release|Any CPU + {CCBE134E-2DAB-4CE7-B9F2-4F315CDEF1FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CCBE134E-2DAB-4CE7-B9F2-4F315CDEF1FA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CCBE134E-2DAB-4CE7-B9F2-4F315CDEF1FA}.Debug|x64.ActiveCfg = Debug|Any CPU + {CCBE134E-2DAB-4CE7-B9F2-4F315CDEF1FA}.Debug|x64.Build.0 = Debug|Any CPU + {CCBE134E-2DAB-4CE7-B9F2-4F315CDEF1FA}.Debug|x86.ActiveCfg = Debug|Any CPU + {CCBE134E-2DAB-4CE7-B9F2-4F315CDEF1FA}.Debug|x86.Build.0 = Debug|Any CPU + {CCBE134E-2DAB-4CE7-B9F2-4F315CDEF1FA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CCBE134E-2DAB-4CE7-B9F2-4F315CDEF1FA}.Release|Any CPU.Build.0 = Release|Any CPU + {CCBE134E-2DAB-4CE7-B9F2-4F315CDEF1FA}.Release|x64.ActiveCfg = Release|Any CPU + {CCBE134E-2DAB-4CE7-B9F2-4F315CDEF1FA}.Release|x64.Build.0 = Release|Any CPU + {CCBE134E-2DAB-4CE7-B9F2-4F315CDEF1FA}.Release|x86.ActiveCfg = Release|Any CPU + {CCBE134E-2DAB-4CE7-B9F2-4F315CDEF1FA}.Release|x86.Build.0 = Release|Any CPU + {E4E9646F-7B85-4446-9B6D-846D32BAC3F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E4E9646F-7B85-4446-9B6D-846D32BAC3F5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E4E9646F-7B85-4446-9B6D-846D32BAC3F5}.Debug|x64.ActiveCfg = Debug|Any CPU + {E4E9646F-7B85-4446-9B6D-846D32BAC3F5}.Debug|x64.Build.0 = Debug|Any CPU + {E4E9646F-7B85-4446-9B6D-846D32BAC3F5}.Debug|x86.ActiveCfg = Debug|Any CPU + {E4E9646F-7B85-4446-9B6D-846D32BAC3F5}.Debug|x86.Build.0 = Debug|Any CPU + {E4E9646F-7B85-4446-9B6D-846D32BAC3F5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E4E9646F-7B85-4446-9B6D-846D32BAC3F5}.Release|Any CPU.Build.0 = Release|Any CPU + {E4E9646F-7B85-4446-9B6D-846D32BAC3F5}.Release|x64.ActiveCfg = Release|Any CPU + {E4E9646F-7B85-4446-9B6D-846D32BAC3F5}.Release|x64.Build.0 = Release|Any CPU + {E4E9646F-7B85-4446-9B6D-846D32BAC3F5}.Release|x86.ActiveCfg = Release|Any CPU + {E4E9646F-7B85-4446-9B6D-846D32BAC3F5}.Release|x86.Build.0 = Release|Any CPU + {3038B4F6-2CB4-4124-86EB-FE8D7FB3A0D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3038B4F6-2CB4-4124-86EB-FE8D7FB3A0D7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3038B4F6-2CB4-4124-86EB-FE8D7FB3A0D7}.Debug|x64.ActiveCfg = Debug|Any CPU + {3038B4F6-2CB4-4124-86EB-FE8D7FB3A0D7}.Debug|x64.Build.0 = Debug|Any CPU + {3038B4F6-2CB4-4124-86EB-FE8D7FB3A0D7}.Debug|x86.ActiveCfg = Debug|Any CPU + {3038B4F6-2CB4-4124-86EB-FE8D7FB3A0D7}.Debug|x86.Build.0 = Debug|Any CPU + {3038B4F6-2CB4-4124-86EB-FE8D7FB3A0D7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3038B4F6-2CB4-4124-86EB-FE8D7FB3A0D7}.Release|Any CPU.Build.0 = Release|Any CPU + {3038B4F6-2CB4-4124-86EB-FE8D7FB3A0D7}.Release|x64.ActiveCfg = Release|Any CPU + {3038B4F6-2CB4-4124-86EB-FE8D7FB3A0D7}.Release|x64.Build.0 = Release|Any CPU + {3038B4F6-2CB4-4124-86EB-FE8D7FB3A0D7}.Release|x86.ActiveCfg = Release|Any CPU + {3038B4F6-2CB4-4124-86EB-FE8D7FB3A0D7}.Release|x86.Build.0 = Release|Any CPU + {E27BC1CD-D6DF-4EEE-B340-5424BE442AC5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E27BC1CD-D6DF-4EEE-B340-5424BE442AC5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E27BC1CD-D6DF-4EEE-B340-5424BE442AC5}.Debug|x64.ActiveCfg = Debug|Any CPU + {E27BC1CD-D6DF-4EEE-B340-5424BE442AC5}.Debug|x64.Build.0 = Debug|Any CPU + {E27BC1CD-D6DF-4EEE-B340-5424BE442AC5}.Debug|x86.ActiveCfg = Debug|Any CPU + {E27BC1CD-D6DF-4EEE-B340-5424BE442AC5}.Debug|x86.Build.0 = Debug|Any CPU + {E27BC1CD-D6DF-4EEE-B340-5424BE442AC5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E27BC1CD-D6DF-4EEE-B340-5424BE442AC5}.Release|Any CPU.Build.0 = Release|Any CPU + {E27BC1CD-D6DF-4EEE-B340-5424BE442AC5}.Release|x64.ActiveCfg = Release|Any CPU + {E27BC1CD-D6DF-4EEE-B340-5424BE442AC5}.Release|x64.Build.0 = Release|Any CPU + {E27BC1CD-D6DF-4EEE-B340-5424BE442AC5}.Release|x86.ActiveCfg = Release|Any CPU + {E27BC1CD-D6DF-4EEE-B340-5424BE442AC5}.Release|x86.Build.0 = Release|Any CPU + {793F6EA6-6842-4E8F-908D-C7423734E385}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {793F6EA6-6842-4E8F-908D-C7423734E385}.Debug|Any CPU.Build.0 = Debug|Any CPU + {793F6EA6-6842-4E8F-908D-C7423734E385}.Debug|x64.ActiveCfg = Debug|Any CPU + {793F6EA6-6842-4E8F-908D-C7423734E385}.Debug|x64.Build.0 = Debug|Any CPU + {793F6EA6-6842-4E8F-908D-C7423734E385}.Debug|x86.ActiveCfg = Debug|Any CPU + {793F6EA6-6842-4E8F-908D-C7423734E385}.Debug|x86.Build.0 = Debug|Any CPU + {793F6EA6-6842-4E8F-908D-C7423734E385}.Release|Any CPU.ActiveCfg = Release|Any CPU + {793F6EA6-6842-4E8F-908D-C7423734E385}.Release|Any CPU.Build.0 = Release|Any CPU + {793F6EA6-6842-4E8F-908D-C7423734E385}.Release|x64.ActiveCfg = Release|Any CPU + {793F6EA6-6842-4E8F-908D-C7423734E385}.Release|x64.Build.0 = Release|Any CPU + {793F6EA6-6842-4E8F-908D-C7423734E385}.Release|x86.ActiveCfg = Release|Any CPU + {793F6EA6-6842-4E8F-908D-C7423734E385}.Release|x86.Build.0 = Release|Any CPU + {8D22A73C-581D-4A85-917F-C56DFB386C6F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8D22A73C-581D-4A85-917F-C56DFB386C6F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8D22A73C-581D-4A85-917F-C56DFB386C6F}.Debug|x64.ActiveCfg = Debug|Any CPU + {8D22A73C-581D-4A85-917F-C56DFB386C6F}.Debug|x64.Build.0 = Debug|Any CPU + {8D22A73C-581D-4A85-917F-C56DFB386C6F}.Debug|x86.ActiveCfg = Debug|Any CPU + {8D22A73C-581D-4A85-917F-C56DFB386C6F}.Debug|x86.Build.0 = Debug|Any CPU + {8D22A73C-581D-4A85-917F-C56DFB386C6F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8D22A73C-581D-4A85-917F-C56DFB386C6F}.Release|Any CPU.Build.0 = Release|Any CPU + {8D22A73C-581D-4A85-917F-C56DFB386C6F}.Release|x64.ActiveCfg = Release|Any CPU + {8D22A73C-581D-4A85-917F-C56DFB386C6F}.Release|x64.Build.0 = Release|Any CPU + {8D22A73C-581D-4A85-917F-C56DFB386C6F}.Release|x86.ActiveCfg = Release|Any CPU + {8D22A73C-581D-4A85-917F-C56DFB386C6F}.Release|x86.Build.0 = Release|Any CPU + {DA7F7106-23D0-4DC2-8B71-43A15A6BBDE8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DA7F7106-23D0-4DC2-8B71-43A15A6BBDE8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DA7F7106-23D0-4DC2-8B71-43A15A6BBDE8}.Debug|x64.ActiveCfg = Debug|Any CPU + {DA7F7106-23D0-4DC2-8B71-43A15A6BBDE8}.Debug|x64.Build.0 = Debug|Any CPU + {DA7F7106-23D0-4DC2-8B71-43A15A6BBDE8}.Debug|x86.ActiveCfg = Debug|Any CPU + {DA7F7106-23D0-4DC2-8B71-43A15A6BBDE8}.Debug|x86.Build.0 = Debug|Any CPU + {DA7F7106-23D0-4DC2-8B71-43A15A6BBDE8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DA7F7106-23D0-4DC2-8B71-43A15A6BBDE8}.Release|Any CPU.Build.0 = Release|Any CPU + {DA7F7106-23D0-4DC2-8B71-43A15A6BBDE8}.Release|x64.ActiveCfg = Release|Any CPU + {DA7F7106-23D0-4DC2-8B71-43A15A6BBDE8}.Release|x64.Build.0 = Release|Any CPU + {DA7F7106-23D0-4DC2-8B71-43A15A6BBDE8}.Release|x86.ActiveCfg = Release|Any CPU + {DA7F7106-23D0-4DC2-8B71-43A15A6BBDE8}.Release|x86.Build.0 = Release|Any CPU + {9892ED34-E662-48A2-95AB-F1655412081C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9892ED34-E662-48A2-95AB-F1655412081C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9892ED34-E662-48A2-95AB-F1655412081C}.Debug|x64.ActiveCfg = Debug|Any CPU + {9892ED34-E662-48A2-95AB-F1655412081C}.Debug|x64.Build.0 = Debug|Any CPU + {9892ED34-E662-48A2-95AB-F1655412081C}.Debug|x86.ActiveCfg = Debug|Any CPU + {9892ED34-E662-48A2-95AB-F1655412081C}.Debug|x86.Build.0 = Debug|Any CPU + {9892ED34-E662-48A2-95AB-F1655412081C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9892ED34-E662-48A2-95AB-F1655412081C}.Release|Any CPU.Build.0 = Release|Any CPU + {9892ED34-E662-48A2-95AB-F1655412081C}.Release|x64.ActiveCfg = Release|Any CPU + {9892ED34-E662-48A2-95AB-F1655412081C}.Release|x64.Build.0 = Release|Any CPU + {9892ED34-E662-48A2-95AB-F1655412081C}.Release|x86.ActiveCfg = Release|Any CPU + {9892ED34-E662-48A2-95AB-F1655412081C}.Release|x86.Build.0 = Release|Any CPU + {55094771-469E-4E1A-8E7B-C7B915E023D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {55094771-469E-4E1A-8E7B-C7B915E023D0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {55094771-469E-4E1A-8E7B-C7B915E023D0}.Debug|x64.ActiveCfg = Debug|Any CPU + {55094771-469E-4E1A-8E7B-C7B915E023D0}.Debug|x64.Build.0 = Debug|Any CPU + {55094771-469E-4E1A-8E7B-C7B915E023D0}.Debug|x86.ActiveCfg = Debug|Any CPU + {55094771-469E-4E1A-8E7B-C7B915E023D0}.Debug|x86.Build.0 = Debug|Any CPU + {55094771-469E-4E1A-8E7B-C7B915E023D0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {55094771-469E-4E1A-8E7B-C7B915E023D0}.Release|Any CPU.Build.0 = Release|Any CPU + {55094771-469E-4E1A-8E7B-C7B915E023D0}.Release|x64.ActiveCfg = Release|Any CPU + {55094771-469E-4E1A-8E7B-C7B915E023D0}.Release|x64.Build.0 = Release|Any CPU + {55094771-469E-4E1A-8E7B-C7B915E023D0}.Release|x86.ActiveCfg = Release|Any CPU + {55094771-469E-4E1A-8E7B-C7B915E023D0}.Release|x86.Build.0 = Release|Any CPU + {50A948F4-EF18-4591-8CA9-564909B630A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {50A948F4-EF18-4591-8CA9-564909B630A1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {50A948F4-EF18-4591-8CA9-564909B630A1}.Debug|x64.ActiveCfg = Debug|Any CPU + {50A948F4-EF18-4591-8CA9-564909B630A1}.Debug|x64.Build.0 = Debug|Any CPU + {50A948F4-EF18-4591-8CA9-564909B630A1}.Debug|x86.ActiveCfg = Debug|Any CPU + {50A948F4-EF18-4591-8CA9-564909B630A1}.Debug|x86.Build.0 = Debug|Any CPU + {50A948F4-EF18-4591-8CA9-564909B630A1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {50A948F4-EF18-4591-8CA9-564909B630A1}.Release|Any CPU.Build.0 = Release|Any CPU + {50A948F4-EF18-4591-8CA9-564909B630A1}.Release|x64.ActiveCfg = Release|Any CPU + {50A948F4-EF18-4591-8CA9-564909B630A1}.Release|x64.Build.0 = Release|Any CPU + {50A948F4-EF18-4591-8CA9-564909B630A1}.Release|x86.ActiveCfg = Release|Any CPU + {50A948F4-EF18-4591-8CA9-564909B630A1}.Release|x86.Build.0 = Release|Any CPU + {A3BDBA44-A720-48FA-8914-736B79830CE9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A3BDBA44-A720-48FA-8914-736B79830CE9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A3BDBA44-A720-48FA-8914-736B79830CE9}.Debug|x64.ActiveCfg = Debug|Any CPU + {A3BDBA44-A720-48FA-8914-736B79830CE9}.Debug|x64.Build.0 = Debug|Any CPU + {A3BDBA44-A720-48FA-8914-736B79830CE9}.Debug|x86.ActiveCfg = Debug|Any CPU + {A3BDBA44-A720-48FA-8914-736B79830CE9}.Debug|x86.Build.0 = Debug|Any CPU + {A3BDBA44-A720-48FA-8914-736B79830CE9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A3BDBA44-A720-48FA-8914-736B79830CE9}.Release|Any CPU.Build.0 = Release|Any CPU + {A3BDBA44-A720-48FA-8914-736B79830CE9}.Release|x64.ActiveCfg = Release|Any CPU + {A3BDBA44-A720-48FA-8914-736B79830CE9}.Release|x64.Build.0 = Release|Any CPU + {A3BDBA44-A720-48FA-8914-736B79830CE9}.Release|x86.ActiveCfg = Release|Any CPU + {A3BDBA44-A720-48FA-8914-736B79830CE9}.Release|x86.Build.0 = Release|Any CPU + {192ECC49-3C79-48CD-A49B-296CC47546F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {192ECC49-3C79-48CD-A49B-296CC47546F5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {192ECC49-3C79-48CD-A49B-296CC47546F5}.Debug|x64.ActiveCfg = Debug|Any CPU + {192ECC49-3C79-48CD-A49B-296CC47546F5}.Debug|x64.Build.0 = Debug|Any CPU + {192ECC49-3C79-48CD-A49B-296CC47546F5}.Debug|x86.ActiveCfg = Debug|Any CPU + {192ECC49-3C79-48CD-A49B-296CC47546F5}.Debug|x86.Build.0 = Debug|Any CPU + {192ECC49-3C79-48CD-A49B-296CC47546F5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {192ECC49-3C79-48CD-A49B-296CC47546F5}.Release|Any CPU.Build.0 = Release|Any CPU + {192ECC49-3C79-48CD-A49B-296CC47546F5}.Release|x64.ActiveCfg = Release|Any CPU + {192ECC49-3C79-48CD-A49B-296CC47546F5}.Release|x64.Build.0 = Release|Any CPU + {192ECC49-3C79-48CD-A49B-296CC47546F5}.Release|x86.ActiveCfg = Release|Any CPU + {192ECC49-3C79-48CD-A49B-296CC47546F5}.Release|x86.Build.0 = Release|Any CPU + {FAC76C2D-D586-4231-BF60-7766969BBD60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FAC76C2D-D586-4231-BF60-7766969BBD60}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FAC76C2D-D586-4231-BF60-7766969BBD60}.Debug|x64.ActiveCfg = Debug|Any CPU + {FAC76C2D-D586-4231-BF60-7766969BBD60}.Debug|x64.Build.0 = Debug|Any CPU + {FAC76C2D-D586-4231-BF60-7766969BBD60}.Debug|x86.ActiveCfg = Debug|Any CPU + {FAC76C2D-D586-4231-BF60-7766969BBD60}.Debug|x86.Build.0 = Debug|Any CPU + {FAC76C2D-D586-4231-BF60-7766969BBD60}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FAC76C2D-D586-4231-BF60-7766969BBD60}.Release|Any CPU.Build.0 = Release|Any CPU + {FAC76C2D-D586-4231-BF60-7766969BBD60}.Release|x64.ActiveCfg = Release|Any CPU + {FAC76C2D-D586-4231-BF60-7766969BBD60}.Release|x64.Build.0 = Release|Any CPU + {FAC76C2D-D586-4231-BF60-7766969BBD60}.Release|x86.ActiveCfg = Release|Any CPU + {FAC76C2D-D586-4231-BF60-7766969BBD60}.Release|x86.Build.0 = Release|Any CPU + {1E379202-AE6A-4A0A-BFE9-C1C0D4605143}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1E379202-AE6A-4A0A-BFE9-C1C0D4605143}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1E379202-AE6A-4A0A-BFE9-C1C0D4605143}.Debug|x64.ActiveCfg = Debug|Any CPU + {1E379202-AE6A-4A0A-BFE9-C1C0D4605143}.Debug|x64.Build.0 = Debug|Any CPU + {1E379202-AE6A-4A0A-BFE9-C1C0D4605143}.Debug|x86.ActiveCfg = Debug|Any CPU + {1E379202-AE6A-4A0A-BFE9-C1C0D4605143}.Debug|x86.Build.0 = Debug|Any CPU + {1E379202-AE6A-4A0A-BFE9-C1C0D4605143}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1E379202-AE6A-4A0A-BFE9-C1C0D4605143}.Release|Any CPU.Build.0 = Release|Any CPU + {1E379202-AE6A-4A0A-BFE9-C1C0D4605143}.Release|x64.ActiveCfg = Release|Any CPU + {1E379202-AE6A-4A0A-BFE9-C1C0D4605143}.Release|x64.Build.0 = Release|Any CPU + {1E379202-AE6A-4A0A-BFE9-C1C0D4605143}.Release|x86.ActiveCfg = Release|Any CPU + {1E379202-AE6A-4A0A-BFE9-C1C0D4605143}.Release|x86.Build.0 = Release|Any CPU + {45104F9C-A786-442B-8202-B965530AE20E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {45104F9C-A786-442B-8202-B965530AE20E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {45104F9C-A786-442B-8202-B965530AE20E}.Debug|x64.ActiveCfg = Debug|Any CPU + {45104F9C-A786-442B-8202-B965530AE20E}.Debug|x64.Build.0 = Debug|Any CPU + {45104F9C-A786-442B-8202-B965530AE20E}.Debug|x86.ActiveCfg = Debug|Any CPU + {45104F9C-A786-442B-8202-B965530AE20E}.Debug|x86.Build.0 = Debug|Any CPU + {45104F9C-A786-442B-8202-B965530AE20E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {45104F9C-A786-442B-8202-B965530AE20E}.Release|Any CPU.Build.0 = Release|Any CPU + {45104F9C-A786-442B-8202-B965530AE20E}.Release|x64.ActiveCfg = Release|Any CPU + {45104F9C-A786-442B-8202-B965530AE20E}.Release|x64.Build.0 = Release|Any CPU + {45104F9C-A786-442B-8202-B965530AE20E}.Release|x86.ActiveCfg = Release|Any CPU + {45104F9C-A786-442B-8202-B965530AE20E}.Release|x86.Build.0 = Release|Any CPU + {2A8189FE-0E9B-474F-91E7-A92C7C302A43}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2A8189FE-0E9B-474F-91E7-A92C7C302A43}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2A8189FE-0E9B-474F-91E7-A92C7C302A43}.Debug|x64.ActiveCfg = Debug|Any CPU + {2A8189FE-0E9B-474F-91E7-A92C7C302A43}.Debug|x64.Build.0 = Debug|Any CPU + {2A8189FE-0E9B-474F-91E7-A92C7C302A43}.Debug|x86.ActiveCfg = Debug|Any CPU + {2A8189FE-0E9B-474F-91E7-A92C7C302A43}.Debug|x86.Build.0 = Debug|Any CPU + {2A8189FE-0E9B-474F-91E7-A92C7C302A43}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2A8189FE-0E9B-474F-91E7-A92C7C302A43}.Release|Any CPU.Build.0 = Release|Any CPU + {2A8189FE-0E9B-474F-91E7-A92C7C302A43}.Release|x64.ActiveCfg = Release|Any CPU + {2A8189FE-0E9B-474F-91E7-A92C7C302A43}.Release|x64.Build.0 = Release|Any CPU + {2A8189FE-0E9B-474F-91E7-A92C7C302A43}.Release|x86.ActiveCfg = Release|Any CPU + {2A8189FE-0E9B-474F-91E7-A92C7C302A43}.Release|x86.Build.0 = Release|Any CPU + {00EE8632-007B-406B-9CF8-F0CE38A5FEB0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {00EE8632-007B-406B-9CF8-F0CE38A5FEB0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {00EE8632-007B-406B-9CF8-F0CE38A5FEB0}.Debug|x64.ActiveCfg = Debug|Any CPU + {00EE8632-007B-406B-9CF8-F0CE38A5FEB0}.Debug|x64.Build.0 = Debug|Any CPU + {00EE8632-007B-406B-9CF8-F0CE38A5FEB0}.Debug|x86.ActiveCfg = Debug|Any CPU + {00EE8632-007B-406B-9CF8-F0CE38A5FEB0}.Debug|x86.Build.0 = Debug|Any CPU + {00EE8632-007B-406B-9CF8-F0CE38A5FEB0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {00EE8632-007B-406B-9CF8-F0CE38A5FEB0}.Release|Any CPU.Build.0 = Release|Any CPU + {00EE8632-007B-406B-9CF8-F0CE38A5FEB0}.Release|x64.ActiveCfg = Release|Any CPU + {00EE8632-007B-406B-9CF8-F0CE38A5FEB0}.Release|x64.Build.0 = Release|Any CPU + {00EE8632-007B-406B-9CF8-F0CE38A5FEB0}.Release|x86.ActiveCfg = Release|Any CPU + {00EE8632-007B-406B-9CF8-F0CE38A5FEB0}.Release|x86.Build.0 = Release|Any CPU + {1565B041-B687-4EF1-B27D-B0F9A927847B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1565B041-B687-4EF1-B27D-B0F9A927847B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1565B041-B687-4EF1-B27D-B0F9A927847B}.Debug|x64.ActiveCfg = Debug|Any CPU + {1565B041-B687-4EF1-B27D-B0F9A927847B}.Debug|x64.Build.0 = Debug|Any CPU + {1565B041-B687-4EF1-B27D-B0F9A927847B}.Debug|x86.ActiveCfg = Debug|Any CPU + {1565B041-B687-4EF1-B27D-B0F9A927847B}.Debug|x86.Build.0 = Debug|Any CPU + {1565B041-B687-4EF1-B27D-B0F9A927847B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1565B041-B687-4EF1-B27D-B0F9A927847B}.Release|Any CPU.Build.0 = Release|Any CPU + {1565B041-B687-4EF1-B27D-B0F9A927847B}.Release|x64.ActiveCfg = Release|Any CPU + {1565B041-B687-4EF1-B27D-B0F9A927847B}.Release|x64.Build.0 = Release|Any CPU + {1565B041-B687-4EF1-B27D-B0F9A927847B}.Release|x86.ActiveCfg = Release|Any CPU + {1565B041-B687-4EF1-B27D-B0F9A927847B}.Release|x86.Build.0 = Release|Any CPU + {E444200C-5B5D-42C0-88C5-D0E2C11A393F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E444200C-5B5D-42C0-88C5-D0E2C11A393F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E444200C-5B5D-42C0-88C5-D0E2C11A393F}.Debug|x64.ActiveCfg = Debug|Any CPU + {E444200C-5B5D-42C0-88C5-D0E2C11A393F}.Debug|x64.Build.0 = Debug|Any CPU + {E444200C-5B5D-42C0-88C5-D0E2C11A393F}.Debug|x86.ActiveCfg = Debug|Any CPU + {E444200C-5B5D-42C0-88C5-D0E2C11A393F}.Debug|x86.Build.0 = Debug|Any CPU + {E444200C-5B5D-42C0-88C5-D0E2C11A393F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E444200C-5B5D-42C0-88C5-D0E2C11A393F}.Release|Any CPU.Build.0 = Release|Any CPU + {E444200C-5B5D-42C0-88C5-D0E2C11A393F}.Release|x64.ActiveCfg = Release|Any CPU + {E444200C-5B5D-42C0-88C5-D0E2C11A393F}.Release|x64.Build.0 = Release|Any CPU + {E444200C-5B5D-42C0-88C5-D0E2C11A393F}.Release|x86.ActiveCfg = Release|Any CPU + {E444200C-5B5D-42C0-88C5-D0E2C11A393F}.Release|x86.Build.0 = Release|Any CPU + {28D2FC99-9DB4-4F15-A99E-F5A76DE7B13A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {28D2FC99-9DB4-4F15-A99E-F5A76DE7B13A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {28D2FC99-9DB4-4F15-A99E-F5A76DE7B13A}.Debug|x64.ActiveCfg = Debug|Any CPU + {28D2FC99-9DB4-4F15-A99E-F5A76DE7B13A}.Debug|x64.Build.0 = Debug|Any CPU + {28D2FC99-9DB4-4F15-A99E-F5A76DE7B13A}.Debug|x86.ActiveCfg = Debug|Any CPU + {28D2FC99-9DB4-4F15-A99E-F5A76DE7B13A}.Debug|x86.Build.0 = Debug|Any CPU + {28D2FC99-9DB4-4F15-A99E-F5A76DE7B13A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {28D2FC99-9DB4-4F15-A99E-F5A76DE7B13A}.Release|Any CPU.Build.0 = Release|Any CPU + {28D2FC99-9DB4-4F15-A99E-F5A76DE7B13A}.Release|x64.ActiveCfg = Release|Any CPU + {28D2FC99-9DB4-4F15-A99E-F5A76DE7B13A}.Release|x64.Build.0 = Release|Any CPU + {28D2FC99-9DB4-4F15-A99E-F5A76DE7B13A}.Release|x86.ActiveCfg = Release|Any CPU + {28D2FC99-9DB4-4F15-A99E-F5A76DE7B13A}.Release|x86.Build.0 = Release|Any CPU + {F81D94AB-E336-405D-B7E3-8AC97EE4E758}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F81D94AB-E336-405D-B7E3-8AC97EE4E758}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F81D94AB-E336-405D-B7E3-8AC97EE4E758}.Debug|x64.ActiveCfg = Debug|Any CPU + {F81D94AB-E336-405D-B7E3-8AC97EE4E758}.Debug|x64.Build.0 = Debug|Any CPU + {F81D94AB-E336-405D-B7E3-8AC97EE4E758}.Debug|x86.ActiveCfg = Debug|Any CPU + {F81D94AB-E336-405D-B7E3-8AC97EE4E758}.Debug|x86.Build.0 = Debug|Any CPU + {F81D94AB-E336-405D-B7E3-8AC97EE4E758}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F81D94AB-E336-405D-B7E3-8AC97EE4E758}.Release|Any CPU.Build.0 = Release|Any CPU + {F81D94AB-E336-405D-B7E3-8AC97EE4E758}.Release|x64.ActiveCfg = Release|Any CPU + {F81D94AB-E336-405D-B7E3-8AC97EE4E758}.Release|x64.Build.0 = Release|Any CPU + {F81D94AB-E336-405D-B7E3-8AC97EE4E758}.Release|x86.ActiveCfg = Release|Any CPU + {F81D94AB-E336-405D-B7E3-8AC97EE4E758}.Release|x86.Build.0 = Release|Any CPU + {192C9441-BA98-41D4-A27E-D7DE95BB46F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {192C9441-BA98-41D4-A27E-D7DE95BB46F7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {192C9441-BA98-41D4-A27E-D7DE95BB46F7}.Debug|x64.ActiveCfg = Debug|Any CPU + {192C9441-BA98-41D4-A27E-D7DE95BB46F7}.Debug|x64.Build.0 = Debug|Any CPU + {192C9441-BA98-41D4-A27E-D7DE95BB46F7}.Debug|x86.ActiveCfg = Debug|Any CPU + {192C9441-BA98-41D4-A27E-D7DE95BB46F7}.Debug|x86.Build.0 = Debug|Any CPU + {192C9441-BA98-41D4-A27E-D7DE95BB46F7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {192C9441-BA98-41D4-A27E-D7DE95BB46F7}.Release|Any CPU.Build.0 = Release|Any CPU + {192C9441-BA98-41D4-A27E-D7DE95BB46F7}.Release|x64.ActiveCfg = Release|Any CPU + {192C9441-BA98-41D4-A27E-D7DE95BB46F7}.Release|x64.Build.0 = Release|Any CPU + {192C9441-BA98-41D4-A27E-D7DE95BB46F7}.Release|x86.ActiveCfg = Release|Any CPU + {192C9441-BA98-41D4-A27E-D7DE95BB46F7}.Release|x86.Build.0 = Release|Any CPU + {D592C239-AFAD-4596-8FE9-BBEA4977B258}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D592C239-AFAD-4596-8FE9-BBEA4977B258}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D592C239-AFAD-4596-8FE9-BBEA4977B258}.Debug|x64.ActiveCfg = Debug|Any CPU + {D592C239-AFAD-4596-8FE9-BBEA4977B258}.Debug|x64.Build.0 = Debug|Any CPU + {D592C239-AFAD-4596-8FE9-BBEA4977B258}.Debug|x86.ActiveCfg = Debug|Any CPU + {D592C239-AFAD-4596-8FE9-BBEA4977B258}.Debug|x86.Build.0 = Debug|Any CPU + {D592C239-AFAD-4596-8FE9-BBEA4977B258}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D592C239-AFAD-4596-8FE9-BBEA4977B258}.Release|Any CPU.Build.0 = Release|Any CPU + {D592C239-AFAD-4596-8FE9-BBEA4977B258}.Release|x64.ActiveCfg = Release|Any CPU + {D592C239-AFAD-4596-8FE9-BBEA4977B258}.Release|x64.Build.0 = Release|Any CPU + {D592C239-AFAD-4596-8FE9-BBEA4977B258}.Release|x86.ActiveCfg = Release|Any CPU + {D592C239-AFAD-4596-8FE9-BBEA4977B258}.Release|x86.Build.0 = Release|Any CPU + {33AA9C01-0E8E-4960-AE8C-550C3B13F84A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {33AA9C01-0E8E-4960-AE8C-550C3B13F84A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {33AA9C01-0E8E-4960-AE8C-550C3B13F84A}.Debug|x64.ActiveCfg = Debug|Any CPU + {33AA9C01-0E8E-4960-AE8C-550C3B13F84A}.Debug|x64.Build.0 = Debug|Any CPU + {33AA9C01-0E8E-4960-AE8C-550C3B13F84A}.Debug|x86.ActiveCfg = Debug|Any CPU + {33AA9C01-0E8E-4960-AE8C-550C3B13F84A}.Debug|x86.Build.0 = Debug|Any CPU + {33AA9C01-0E8E-4960-AE8C-550C3B13F84A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {33AA9C01-0E8E-4960-AE8C-550C3B13F84A}.Release|Any CPU.Build.0 = Release|Any CPU + {33AA9C01-0E8E-4960-AE8C-550C3B13F84A}.Release|x64.ActiveCfg = Release|Any CPU + {33AA9C01-0E8E-4960-AE8C-550C3B13F84A}.Release|x64.Build.0 = Release|Any CPU + {33AA9C01-0E8E-4960-AE8C-550C3B13F84A}.Release|x86.ActiveCfg = Release|Any CPU + {33AA9C01-0E8E-4960-AE8C-550C3B13F84A}.Release|x86.Build.0 = Release|Any CPU + {FD79C25F-87CD-47C6-87B2-F497AEA4A12D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FD79C25F-87CD-47C6-87B2-F497AEA4A12D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FD79C25F-87CD-47C6-87B2-F497AEA4A12D}.Debug|x64.ActiveCfg = Debug|Any CPU + {FD79C25F-87CD-47C6-87B2-F497AEA4A12D}.Debug|x64.Build.0 = Debug|Any CPU + {FD79C25F-87CD-47C6-87B2-F497AEA4A12D}.Debug|x86.ActiveCfg = Debug|Any CPU + {FD79C25F-87CD-47C6-87B2-F497AEA4A12D}.Debug|x86.Build.0 = Debug|Any CPU + {FD79C25F-87CD-47C6-87B2-F497AEA4A12D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FD79C25F-87CD-47C6-87B2-F497AEA4A12D}.Release|Any CPU.Build.0 = Release|Any CPU + {FD79C25F-87CD-47C6-87B2-F497AEA4A12D}.Release|x64.ActiveCfg = Release|Any CPU + {FD79C25F-87CD-47C6-87B2-F497AEA4A12D}.Release|x64.Build.0 = Release|Any CPU + {FD79C25F-87CD-47C6-87B2-F497AEA4A12D}.Release|x86.ActiveCfg = Release|Any CPU + {FD79C25F-87CD-47C6-87B2-F497AEA4A12D}.Release|x86.Build.0 = Release|Any CPU + {F723B0BB-C5EA-4EBE-B3D5-50682F45EA80}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F723B0BB-C5EA-4EBE-B3D5-50682F45EA80}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F723B0BB-C5EA-4EBE-B3D5-50682F45EA80}.Debug|x64.ActiveCfg = Debug|Any CPU + {F723B0BB-C5EA-4EBE-B3D5-50682F45EA80}.Debug|x64.Build.0 = Debug|Any CPU + {F723B0BB-C5EA-4EBE-B3D5-50682F45EA80}.Debug|x86.ActiveCfg = Debug|Any CPU + {F723B0BB-C5EA-4EBE-B3D5-50682F45EA80}.Debug|x86.Build.0 = Debug|Any CPU + {F723B0BB-C5EA-4EBE-B3D5-50682F45EA80}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F723B0BB-C5EA-4EBE-B3D5-50682F45EA80}.Release|Any CPU.Build.0 = Release|Any CPU + {F723B0BB-C5EA-4EBE-B3D5-50682F45EA80}.Release|x64.ActiveCfg = Release|Any CPU + {F723B0BB-C5EA-4EBE-B3D5-50682F45EA80}.Release|x64.Build.0 = Release|Any CPU + {F723B0BB-C5EA-4EBE-B3D5-50682F45EA80}.Release|x86.ActiveCfg = Release|Any CPU + {F723B0BB-C5EA-4EBE-B3D5-50682F45EA80}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {0D88B727-CA5D-48DB-835B-E90C8E282DA7} = {322AAFB0-04E1-42B9-B9F1-B836E12E1FCD} + {15948D13-B424-4DF3-B341-36CCB97D4D9F} = {823AD0CF-E764-4113-A166-6A615A4B928F} + {404B2AB4-CAFA-4AF7-9593-F231A25A973C} = {322AAFB0-04E1-42B9-B9F1-B836E12E1FCD} + {596A8193-7A5D-4055-BB8C-D2F27E1C6B94} = {404B2AB4-CAFA-4AF7-9593-F231A25A973C} + {0F1F75EF-8755-499B-8A28-F6653EB29FED} = {404B2AB4-CAFA-4AF7-9593-F231A25A973C} + {295450C6-C37F-4AA7-A59A-1BFFF352D0EA} = {404B2AB4-CAFA-4AF7-9593-F231A25A973C} + {05451402-CA5D-4474-A8BE-8E534AD17748} = {0D88B727-CA5D-48DB-835B-E90C8E282DA7} + {823AD0CF-E764-4113-A166-6A615A4B928F} = {322AAFB0-04E1-42B9-B9F1-B836E12E1FCD} + {F872A038-D325-4718-B0A0-4CBCEA3714ED} = {322AAFB0-04E1-42B9-B9F1-B836E12E1FCD} + {D425AE9F-F25D-4B64-9983-FE76778524A7} = {404B2AB4-CAFA-4AF7-9593-F231A25A973C} + {8ED45EF3-0271-4867-9330-80D300029D8C} = {F872A038-D325-4718-B0A0-4CBCEA3714ED} + {B0D05BBF-F7FA-46EC-9224-182B2B98C4A3} = {404B2AB4-CAFA-4AF7-9593-F231A25A973C} + {3F789B98-5A34-41CF-B4F0-70B03C0C5ED2} = {F872A038-D325-4718-B0A0-4CBCEA3714ED} + {19A46232-72C5-4BB7-AE6B-A9062BE259A0} = {0D88B727-CA5D-48DB-835B-E90C8E282DA7} + {42DE9E8C-747E-46A4-9AC5-BB7ED1425BF0} = {0D88B727-CA5D-48DB-835B-E90C8E282DA7} + {D4506804-B9A1-4E1A-B97C-0F815814FE62} = {0D88B727-CA5D-48DB-835B-E90C8E282DA7} + {D6B19207-B3E3-4B53-9964-19DEFB2089EF} = {F872A038-D325-4718-B0A0-4CBCEA3714ED} + {F839354B-16C3-4EEF-8872-768B6D832BB2} = {F872A038-D325-4718-B0A0-4CBCEA3714ED} + {5707003E-0835-4E7A-BAD8-E55F4AE0D583} = {F872A038-D325-4718-B0A0-4CBCEA3714ED} + {CA5FFE88-A5FF-42D3-9E88-220A594AC27F} = {404B2AB4-CAFA-4AF7-9593-F231A25A973C} + {A57EA67A-133B-4217-BE17-9E25622A95DD} = {F872A038-D325-4718-B0A0-4CBCEA3714ED} + {105F39CC-E6C1-4365-B200-75D451E5747F} = {0D88B727-CA5D-48DB-835B-E90C8E282DA7} + {B84EBC46-1CF7-47C9-B38E-F4919A11D6D0} = {0D88B727-CA5D-48DB-835B-E90C8E282DA7} + {5F2C76AE-81B2-4A24-80D2-086EFC41627B} = {0D88B727-CA5D-48DB-835B-E90C8E282DA7} + {18F61C6F-997C-459F-A3CE-63AEE311315E} = {0D88B727-CA5D-48DB-835B-E90C8E282DA7} + {7B5A36CE-10AE-4915-84C2-61C205F4C004} = {0D88B727-CA5D-48DB-835B-E90C8E282DA7} + {58B6177B-2C56-4B37-88AB-8C454767D427} = {0D88B727-CA5D-48DB-835B-E90C8E282DA7} + {61DB7D8D-7903-47EB-85FA-1E21001C5EAE} = {0D88B727-CA5D-48DB-835B-E90C8E282DA7} + {D7F1A231-791A-4B34-A9B2-21282EADA82C} = {404B2AB4-CAFA-4AF7-9593-F231A25A973C} + {42CDD6F5-A858-4A32-8982-E5A63689CD58} = {0D88B727-CA5D-48DB-835B-E90C8E282DA7} + {779A5516-5209-491C-878A-63086DB3F566} = {F872A038-D325-4718-B0A0-4CBCEA3714ED} + {8293EEC9-C747-46C3-B3D2-30C552620269} = {F872A038-D325-4718-B0A0-4CBCEA3714ED} + {DA4852B6-2FBD-4E5C-972C-049626152E4E} = {F872A038-D325-4718-B0A0-4CBCEA3714ED} + {CCBE134E-2DAB-4CE7-B9F2-4F315CDEF1FA} = {404B2AB4-CAFA-4AF7-9593-F231A25A973C} + {E4E9646F-7B85-4446-9B6D-846D32BAC3F5} = {F872A038-D325-4718-B0A0-4CBCEA3714ED} + {3038B4F6-2CB4-4124-86EB-FE8D7FB3A0D7} = {F872A038-D325-4718-B0A0-4CBCEA3714ED} + {E27BC1CD-D6DF-4EEE-B340-5424BE442AC5} = {F872A038-D325-4718-B0A0-4CBCEA3714ED} + {793F6EA6-6842-4E8F-908D-C7423734E385} = {F872A038-D325-4718-B0A0-4CBCEA3714ED} + {8D22A73C-581D-4A85-917F-C56DFB386C6F} = {404B2AB4-CAFA-4AF7-9593-F231A25A973C} + {DA7F7106-23D0-4DC2-8B71-43A15A6BBDE8} = {823AD0CF-E764-4113-A166-6A615A4B928F} + {9892ED34-E662-48A2-95AB-F1655412081C} = {404B2AB4-CAFA-4AF7-9593-F231A25A973C} + {55094771-469E-4E1A-8E7B-C7B915E023D0} = {F872A038-D325-4718-B0A0-4CBCEA3714ED} + {50A948F4-EF18-4591-8CA9-564909B630A1} = {F872A038-D325-4718-B0A0-4CBCEA3714ED} + {A3BDBA44-A720-48FA-8914-736B79830CE9} = {0D88B727-CA5D-48DB-835B-E90C8E282DA7} + {192ECC49-3C79-48CD-A49B-296CC47546F5} = {F872A038-D325-4718-B0A0-4CBCEA3714ED} + {368BD8A7-67D2-4F24-B655-C3B82BBDA840} = {322AAFB0-04E1-42B9-B9F1-B836E12E1FCD} + {FAC76C2D-D586-4231-BF60-7766969BBD60} = {368BD8A7-67D2-4F24-B655-C3B82BBDA840} + {1E379202-AE6A-4A0A-BFE9-C1C0D4605143} = {368BD8A7-67D2-4F24-B655-C3B82BBDA840} + {45104F9C-A786-442B-8202-B965530AE20E} = {368BD8A7-67D2-4F24-B655-C3B82BBDA840} + {2A8189FE-0E9B-474F-91E7-A92C7C302A43} = {368BD8A7-67D2-4F24-B655-C3B82BBDA840} + {00EE8632-007B-406B-9CF8-F0CE38A5FEB0} = {368BD8A7-67D2-4F24-B655-C3B82BBDA840} + {1565B041-B687-4EF1-B27D-B0F9A927847B} = {368BD8A7-67D2-4F24-B655-C3B82BBDA840} + {E444200C-5B5D-42C0-88C5-D0E2C11A393F} = {368BD8A7-67D2-4F24-B655-C3B82BBDA840} + {28D2FC99-9DB4-4F15-A99E-F5A76DE7B13A} = {368BD8A7-67D2-4F24-B655-C3B82BBDA840} + {F81D94AB-E336-405D-B7E3-8AC97EE4E758} = {368BD8A7-67D2-4F24-B655-C3B82BBDA840} + {192C9441-BA98-41D4-A27E-D7DE95BB46F7} = {368BD8A7-67D2-4F24-B655-C3B82BBDA840} + {D592C239-AFAD-4596-8FE9-BBEA4977B258} = {368BD8A7-67D2-4F24-B655-C3B82BBDA840} + {33AA9C01-0E8E-4960-AE8C-550C3B13F84A} = {368BD8A7-67D2-4F24-B655-C3B82BBDA840} + {FD79C25F-87CD-47C6-87B2-F497AEA4A12D} = {368BD8A7-67D2-4F24-B655-C3B82BBDA840} + {F723B0BB-C5EA-4EBE-B3D5-50682F45EA80} = {368BD8A7-67D2-4F24-B655-C3B82BBDA840} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {63CC6DCC-A35C-402C-8ECB-3CC7EA821994} + EndGlobalSection +EndGlobal diff --git a/nuget.config b/nuget.config new file mode 100644 index 0000000..891fb7a --- /dev/null +++ b/nuget.config @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/properties_for_visual_studio.zip b/properties_for_visual_studio.zip new file mode 100644 index 0000000000000000000000000000000000000000..f169eb31689663affa236c410d190e992e8042ee GIT binary patch literal 10279 zcmbuF2{=^y8^>o%wvt;yNL_V}Y%z({HH?noS z*(~3+G5ONrB-y3GQ-16YoR1funqsb~tUc~D2b;n!jCrXnQmHet4VkR=0QuX8< z2!;o~uip|88uupMIDK1y-PWLL>Q4Ku3|1^T_5ShW1*}lF?E0XB$sS5MI;zY|^D}zW zR1Uj^We2fRnF_vJx0@3#6^5vFVt&428`k2$%#eg}VBd zhBydp0JyBjo^({mRwxW1dkG-pGA2mx+m+z#y5?@_z@y+i$t9fd2ze>z%-Q!;ff#Xg`G%uDyXHnZ=Jls{)_h8A`YTs4>THXXl)4Y$a^%r8*E?l?+2fO% zx=U*N6`4A8`}2x9kL{wS1e|>ozoYk^x9I7ucdveQdZZuRS?5OQsCRJiuha9Z7RCmP zM-DRjnC!g7MZ?JS5y<9Xdct6PvH~{PvjQ1jjNi5xS?E-Ppp6Hh88hQma)4}~fXn7T zWF0-VxTgDWr3_9VDiRgHUT%}6o!i|W(r@GvG1DsIY38&jXXA03ID7xTD|su|^W3TI zJ&7E%XHzZ`d)>9$PHqjU|D?Cm>jTb4%v)AzrFeC-?Sw4}9`c2{eY&D|&Uvo$DvoYG zqj#n|+PPmXVzTN-U+c0DjQoPKgz$M<4r?z2b~Zm~S9Mg4o7a|_Uo}70(1?ePnwPY2 z7@Hvi8|JcTL1Qv}wqf=e=Ca72M-VV+0NDTJn#O~`Mzt)Clu{tAE5Nd-5D`43{pYe+ z;?dzTGc3ES_=luH>0u|*&42qFG!OO$*L|)ta@jkR68^knWvXKRVb%A3*KBPhU)q~? zm|OY2CnOSU+_g`x%5GEq-uJCGX-2nH)72Pfng8Wf_+6Cj^D5N!sY%IbTBy~q*}9h+ zcaLqY70kYhkMMmA^w=|>2(Dbp?oPM0(SDsbA8>6_mu(VDy?5NM*}vog`P*_&*0ep9 zpYX%9C`N^q)-(#y)(fT@jY-^jxnP^ zRCE6bCrz>lmia;G@ez^MA#2Dy^O(6myDK{(OoQ1duPinASf1`5`s7ORYtsH%0efc@ z%I{rl->}ImR~LK(jAN{Ug8rbR<<%Uw##zyk1LL+ zxmFkEU5VE}Uzl3WqA=}On%<@4d`~9C-ah^I%=4_}HgfSD(+Ll){F@%tpEjSOGRw!$REmqrk&$v{^rY@5g#HK`FY3M5qu zo;6@i^^HclXNGgN%Q1AKZns+PM=H5eRNe9AIJ@kfv^;H@{ijLi#+ihvS-og8r*P*` zgR9++saF{++`YQsTP5d1l%Y|K?z4@~1Eh^7#ZEb1IO%hz$iq<5YyQ`3+**d)tHN2O z8AS`_6-~|*1sIAeEMDK8@qG248?|~DXpjpaNU38zx_}{UOi=TL$s&l848ZC3krQ57K%#fRWihsdLShQnD76!S zi9~#X*S9h|q;@;M_gD$Fxx(Bk_l@KuPN%MXc0x%TV~66R!po5q0;NcaF5b{=TimC1 zwbskM^(e(IE7Uo?CBFIAthDZU=l(7nL1G2CPx*88PMg;xG9A|y0ZSivypXw_xT(H% z!}MEOG1mw6KXT`~ME^NV$y8WMb~J|1f2Rb`B8buyfRb!OTKT!rQWBi-ca&tH9qJp+ z@8qP#&g>k+QyE3+bI1zq;KBWS9p%ZkNK@H3<%^k)%QLN%wiPYT_%h@99a)Gv$@htOlBkaK;%IKdxs%&0rUkQaN=7IR zodF|glYsrndff%T&bkK`>HgZXgnJsEjcYkR1JZ+yHy(D8j=|1=OAwG$v7T8w8awc} zM?c-sW)K8N4uC^gNSvPuBzX;7X2K`?9hhH72Ob!65DdPt96ab<0S5F^ZNG6Xc z4=R<%f3;cquFIwQ^PDrUF6l7h8gG_Up6*}6SVVmCrO3r}bNf8MQqHRDp_kb954Ei> zuD@zTU(2;?yi@mVSCZI6*_@0&hI?lz0t@EbloQ>D7RX?sw<9e?Cd{`fWF|qFHmPKGyBT;`=4CGX>u*BF z>GYSTnR^pct6$D09cnIjsW;94;qjrSGO?JTt(Y0NI!9IgiDOOA+OXBN`FSkO@Q0qR z&!(w8zNAwndSreIr9#3inY0vSc6M%f;itZ)D2M)1!`$0?rxfK%x0JPZefFCA@1i?t zOl#*x@nK|^2%~3XJ-SrSP4o>xK77rDq@^cB5<#G*0-!o=NhxQ6Ja2((>6r2fVifc9 zH2!O?De{y>iLamj!1g%e%kYjirg*-vc8Q@%Qsp?ihp!T zP4PSZT{aYW-6guQEe?2Ew@VFlR#2Gl^gE+u7OMBYU3-){037hNTf4|fu%*rh0f>a< z#fku$FMFu1uul(0W6356nKFRvjM3)7^FYF{z-9m65({F7?k}2O-q1dUr@93|r-|)l z?6Qz26`E46OHW3Rd+1HKPM>^$c*=5~?B(9YCgec!8SUBM&1r34B8gJg`r-}1!SATp z8ijND#GC~yxMJQX-|qR;kOp@dfv8sy!sP_pSK`p8G_zdVq5uUStt&q-i3!VSzuBhQjY>i{o)B zynlwrDd0>`EVt79)`Ev(v83Y~4d64p9C`u?FNcYoZypJ+?V(ko3D$;U&RLck_!(Xs z`Nn5}gV){z+{bUnsC=~@h95Mz3^7qQudG7h@o=#GD{cTnhM-9&Bm=avd8{%7JeF`} zlIjPx1buD6D{2oZgp|S!LQfnaW#F^$0*_%3V*UuP-c@=)2lNAqB?W;9U(he)hJ1MR zM1-q80-%iI5ME*O!L-?+AgA}+5ke$z=*cEz0|+Q`>_RXQb)M)72N4yt4{3z7!V#e- zjF2`EQ8EF`B*eTC&PZ>Uq8?}o6oVdwBCJ6(BJ(lo61MyZkcyOJc$JH1H8Ua&+L5U$^8v#;+L_!fx2|ZAR#DSEYL@~ti&G6ueboQP%01-j4 z#e2voca!EqfhPeWlp(K~Y1C=d>$ z5Q=bv(DOq`8E?lE$AAN$Cc;f|Y_%kVW4PF13wj_3=>oBQ znSg->Hiv^d64LFo2*d)#{ZEG+f>%cFGW=K;OO3wr2v}Cy7xOS^ne_3Rx5=1`!h#+= zLbnfMi5r^;3@kP+PY!`tAbn7Tn}Z$!Lb^aKCnjQ=!y5>~kv#O)F$0l6agOyRgd6Bi z(dFxur=zSo0uE+u8Lwa=H&WF{d(CJ@V}de zhkkHV%4Gkw02>C1#~0m3n1ZIG$Ja1AlSf^61Tf!{YWTn;W#Y5Y?5yqfbGb27Ytz%Fy z0;L7Ve0jw?B8S?iX&)0IMgoVPAVPM4l;%zm-VT@;3|y8$)0044kWMH%*ocY&#c@R7 MPbHwhmN(D)KZ0d30RR91 literal 0 HcmV?d00001 diff --git a/scripts/Database/add-migration.ps1 b/scripts/Database/add-migration.ps1 new file mode 100644 index 0000000..8434b79 --- /dev/null +++ b/scripts/Database/add-migration.ps1 @@ -0,0 +1,2 @@ +# comment +dotnet ef migrations add $Args[0] --project .\srcs\_plugins\Plugin.DB.EF --startup-project .\srcs\_plugins\Plugin.DB.EF \ No newline at end of file diff --git a/scripts/Database/default-accounts.ps1 b/scripts/Database/default-accounts.ps1 new file mode 100644 index 0000000..0d85043 --- /dev/null +++ b/scripts/Database/default-accounts.ps1 @@ -0,0 +1 @@ +./dist/toolkit/Toolkit.exe create-accounts \ No newline at end of file diff --git a/scripts/Database/remove-last-migration.ps1 b/scripts/Database/remove-last-migration.ps1 new file mode 100644 index 0000000..cc7d6ef --- /dev/null +++ b/scripts/Database/remove-last-migration.ps1 @@ -0,0 +1,2 @@ +# comment +dotnet ef migrations remove --project .\srcs\_plugins\Plugin.DB.EF --startup-project .\srcs\_plugins\Plugin.DB.EF diff --git a/scripts/Database/update-database.ps1 b/scripts/Database/update-database.ps1 new file mode 100644 index 0000000..05afdc8 --- /dev/null +++ b/scripts/Database/update-database.ps1 @@ -0,0 +1,2 @@ +# comment +dotnet ef database update $Args[0] --project .\srcs\_plugins\Plugin.DB.EF --startup-project .\srcs\_plugins\Plugin.DB.EF \ No newline at end of file diff --git a/scripts/Docker/MQTT.ps1 b/scripts/Docker/MQTT.ps1 new file mode 100644 index 0000000..2122a0d --- /dev/null +++ b/scripts/Docker/MQTT.ps1 @@ -0,0 +1 @@ +docker run -p 18083:18083 -p 1883:1883 --name mqtt-broker --restart always -d eclipse-mosquitto:1.6 \ No newline at end of file diff --git a/scripts/Docker/MonboDB.ps1 b/scripts/Docker/MonboDB.ps1 new file mode 100644 index 0000000..1b39263 --- /dev/null +++ b/scripts/Docker/MonboDB.ps1 @@ -0,0 +1 @@ +docker run --name mongodb -p 27017:27017 -e MONGO_INITDB_ROOT_USERNAME=root -e MONGO_INITDB_ROOT_PASSWORD=root --restart always -d mongo:4 \ No newline at end of file diff --git a/scripts/Docker/PostgreSQL.ps1 b/scripts/Docker/PostgreSQL.ps1 new file mode 100644 index 0000000..71d736a --- /dev/null +++ b/scripts/Docker/PostgreSQL.ps1 @@ -0,0 +1 @@ +docker run -p 5432:5432 --name postgres-SQL -e POSTGRES_PASSWORD=VaNOSilla2022 --restart always -d postgres:13 \ No newline at end of file diff --git a/scripts/Docker/Redis.ps1 b/scripts/Docker/Redis.ps1 new file mode 100644 index 0000000..abcdd33 --- /dev/null +++ b/scripts/Docker/Redis.ps1 @@ -0,0 +1 @@ +docker run -p 6379:6379 --name redis --restart always -d redis:latest redis-server --notify-keyspace-events Kx --appendonly yes \ No newline at end of file diff --git a/scripts/translations-update.ps1 b/scripts/translations-update.ps1 new file mode 100644 index 0000000..03c2413 --- /dev/null +++ b/scripts/translations-update.ps1 @@ -0,0 +1,3 @@ +$wingsemu_translations_directory = "../server-translations" + +./dist/toolkit/Toolkit.exe translations -i $wingsemu_translations_directory -o $wingsemu_translations_directory \ No newline at end of file diff --git a/scripts/update-server-files.ps1 b/scripts/update-server-files.ps1 new file mode 100644 index 0000000..db654e4 --- /dev/null +++ b/scripts/update-server-files.ps1 @@ -0,0 +1,73 @@ +# Translations keys # +$resources_src = '../server-translations' +$files = Get-ChildItem $resources_src -Filter "*.yaml" + +$resources_dst = "./dist/translation-server/translations" +New-Item -ItemType Directory -Force -Path $resources_dst +Copy-Item $resources_src\en -Destination $resources_dst -Force -Recurse +Copy-Item $resources_src\fr -Destination $resources_dst -Force -Recurse +Copy-Item $resources_src\pl -Destination $resources_dst -Force -Recurse +Copy-Item $resources_src\es -Destination $resources_dst -Force -Recurse +Copy-Item $resources_src\it -Destination $resources_dst -Force -Recurse +Copy-Item $resources_src\de -Destination $resources_dst -Force -Recurse +Copy-Item $resources_src\tr -Destination $resources_dst -Force -Recurse +Copy-Item $resources_src\cz -Destination $resources_dst -Force -Recurse +Write-Host Copied $files.Count generic translations files to translations + +# Dat +$resources_src = '../client-files/dat' +$files = Get-ChildItem $resources_src + +$resources_dst = "./dist/game-server/resources/dat" +New-Item -ItemType Directory -Force -Path $resources_dst +Copy-Item $resources_src\* -Destination $resources_dst -Force -Recurse +Write-Host Copied $files.Count .dat files + +$resources_dst = "./dist/bazaar-server/resources/dat" +New-Item -ItemType Directory -Force -Path $resources_dst +Copy-Item $resources_src\* -Destination $resources_dst -Force -Recurse +Write-Host Copied $files.Count .dat files to bazaar + +$resources_dst = "./dist/discord-notifier/resources/dat" +New-Item -ItemType Directory -Force -Path $resources_dst +Copy-Item $resources_src\* -Destination $resources_dst -Force -Recurse +Write-Host Copied $files.Count .dat files to communicator + + +# Lang keys # +$resources_src = '../client-files/lang' +$files = Get-ChildItem $resources_src + +$resources_dst = "./dist/game-server/resources/lang" +New-Item -ItemType Directory -Force -Path $resources_dst +Copy-Item $resources_src\* -Destination $resources_dst -Force -Recurse +Write-Host Copied $files.Count langs files to game server + +# Maps +$resources_src = '../client-files/maps' +$files = Get-ChildItem $resources_src + +$resources_dst = "./dist/game-server/resources/maps" +New-Item -ItemType Directory -Force -Path $resources_dst +Copy-Item $resources_src\* -Destination $resources_dst -Force -Recurse +Write-Host Copied $files.Count maps files + + +# Config files +$server_config_src = '../server-files' +$files = Get-ChildItem $server_config_src + +$server_config_target = './dist/game-server/config' +New-Item -ItemType Directory -Force -Path $server_config_target +Copy-Item $server_config_src\* -Destination $server_config_target -Force -Recurse +Write-Host Copied $files.Count config files to Game Server + +$server_config_target = './dist/family-server/config' +New-Item -ItemType Directory -Force -Path $server_config_target +Copy-Item $server_config_src\* -Destination $server_config_target -Force -Recurse +Write-Host Copied $files.Count config files to Family Server + +$server_config_target = './dist/translation-server/config' +New-Item -ItemType Directory -Force -Path $server_config_target +Copy-Item $server_config_src\* -Destination $server_config_target -Force -Recurse +Write-Host Copied $files.Count config files to Translations Server diff --git a/srcs/BazaarServer/BazaarServer.csproj b/srcs/BazaarServer/BazaarServer.csproj new file mode 100644 index 0000000..fbef9fd --- /dev/null +++ b/srcs/BazaarServer/BazaarServer.csproj @@ -0,0 +1,27 @@ + + + + net5.0 + ..\..\dist\bazaar-server\ + false + + + + + + + + + + + + + + + + + + + + + diff --git a/srcs/BazaarServer/Consumers/ServiceMaintenanceNotificationMessageConsumer.cs b/srcs/BazaarServer/Consumers/ServiceMaintenanceNotificationMessageConsumer.cs new file mode 100644 index 0000000..5f5dfa2 --- /dev/null +++ b/srcs/BazaarServer/Consumers/ServiceMaintenanceNotificationMessageConsumer.cs @@ -0,0 +1,24 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; +using WingsAPI.Communication.Services.Messages; +using WingsEmu.Health; + +namespace BazaarServer.Consumers +{ + public class ServiceMaintenanceNotificationMessageConsumer : IMessageConsumer + { + private readonly IMaintenanceManager _maintenanceManager; + + public ServiceMaintenanceNotificationMessageConsumer(IMaintenanceManager maintenanceManager) => _maintenanceManager = maintenanceManager; + + public async Task HandleAsync(ServiceMaintenanceNotificationMessage notification, CancellationToken token) + { + if (notification.TimeLeft <= TimeSpan.FromMinutes(5)) + { + _maintenanceManager.ActivateMaintenance(); + } + } + } +} \ No newline at end of file diff --git a/srcs/BazaarServer/DockerGracefulStopService.cs b/srcs/BazaarServer/DockerGracefulStopService.cs new file mode 100644 index 0000000..d4e5542 --- /dev/null +++ b/srcs/BazaarServer/DockerGracefulStopService.cs @@ -0,0 +1,54 @@ +using System; +using System.Runtime.Loader; +using System.Threading; +using PhoenixLib.Logging; + +namespace BazaarServer +{ + public class DockerGracefulStopService : IDisposable + { + private readonly CancellationTokenSource _cancellationTokenSource; + private readonly ManualResetEventSlim _stoppedEvent; + + public DockerGracefulStopService() + { + _cancellationTokenSource = new CancellationTokenSource(); + _stoppedEvent = new ManualResetEventSlim(); + + // SIGINT + Console.CancelKeyPress += (sender, eventArgs) => + { + Log.Error("[GRACEFUL_SHUTDOWN] SIGINT received", new Exception("[GRACEFUL_SHUTDOWN] SIGINT received")); + GracefulStop(_cancellationTokenSource, _stoppedEvent); + }; + + // SIGTERM + AssemblyLoadContext.Default.Unloading += context => + { + Log.Error("[GRACEFUL_SHUTDOWN] SIGTERM received", new Exception("[GRACEFUL_SHUTDOWN] SIGTERM received")); + GracefulStop(_cancellationTokenSource, _stoppedEvent); + }; + + AppDomain.CurrentDomain.UnhandledException += (sender, args) => + { + Log.Error("UnhandledException", args.ExceptionObject as Exception); + GracefulStop(_cancellationTokenSource, _stoppedEvent); + }; + } + + public CancellationToken CancellationToken => _cancellationTokenSource.Token; + + public void Dispose() + { + _stoppedEvent.Set(); + } + + private static void GracefulStop(CancellationTokenSource cancellationTokenSource, ManualResetEventSlim stoppedEvent) + { + Log.Info("DockerGracefulStopService Stopping service"); + cancellationTokenSource.Cancel(); + stoppedEvent.Wait(); + Log.Info("DockerGracefulStopService Stop finished"); + } + } +} \ No newline at end of file diff --git a/srcs/BazaarServer/Managers/BazaarManager.cs b/srcs/BazaarServer/Managers/BazaarManager.cs new file mode 100644 index 0000000..8338252 --- /dev/null +++ b/srcs/BazaarServer/Managers/BazaarManager.cs @@ -0,0 +1,186 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Caching; +using WingsAPI.Communication.Bazaar; +using WingsAPI.Data.Bazaar; +using WingsAPI.Game.Extensions.Bazaar; +using WingsAPI.Packets.Enums.Bazaar; +using WingsEmu.Core.Extensions; +using WingsEmu.Core.Generics; +using WingsEmu.DTOs.Bazaar; + +namespace BazaarServer.Managers +{ + public class BazaarManager + { + private readonly IBazaarItemDAO _bazaarItemDao; + private readonly BazaarSearchManager _bazaarSearchManager; + + private readonly ILongKeyCachedRepository _itemsCache; + private readonly ConcurrentDictionary> _itemsIdsByCharacterId = new(); + + private readonly SemaphoreSlim _semaphoreSlim = new(1, 1); + + public BazaarManager(IBazaarItemDAO bazaarItemDao, ILongKeyCachedRepository itemsCache, BazaarSearchManager bazaarSearchManager) + { + _bazaarItemDao = bazaarItemDao; + _itemsCache = itemsCache; + _bazaarSearchManager = bazaarSearchManager; + } + + public async Task CacheAllItemsInDb() + { + IEnumerable items = await _bazaarItemDao.GetAllNonDeletedBazaarItems(); + int count = 0; + foreach (BazaarItemDTO item in items) + { + AddToCache(item); + count++; + } + + await _bazaarSearchManager.Initialize(items); + + return count; + } + + private void AddToCache(BazaarItemDTO item) + { + _itemsCache.Set(item.Id, item); + _itemsIdsByCharacterId.GetOrAdd(item.CharacterId, new ThreadSafeHashSet()).Add(item.Id); + } + + private void RemoveFromCache(BazaarItemDTO item) + { + _itemsCache.Remove(item.Id); + _itemsIdsByCharacterId.GetOrDefault(item.CharacterId)?.Remove(item.Id); + } + + public async Task SaveAsync(BazaarItemDTO item) + { + await _semaphoreSlim.WaitAsync(); + try + { + BazaarItemDTO dto = await _bazaarItemDao.SaveAsync(item); + AddToCache(dto); + _bazaarSearchManager.AddItem(dto); + return dto; + } + finally + { + _semaphoreSlim.Release(); + } + } + + public async Task GetBazaarItemById(long bazaarItemId) + { + await _semaphoreSlim.WaitAsync(); + try + { + return _itemsCache.Get(bazaarItemId); + } + finally + { + _semaphoreSlim.Release(); + } + } + + public ICollection GetItemsByCharacterId(long characterId) + { + return _itemsIdsByCharacterId.GetOrDefault(characterId)?.Select(s => _itemsCache.Get(s)).ToList(); + } + + public async Task DeleteItemWithDto(BazaarItemDTO item, long requesterCharacterId) + { + await _semaphoreSlim.WaitAsync(); + try + { + BazaarItemDTO cachedItem = _itemsCache.Get(item.Id); + if (cachedItem == null || cachedItem.CharacterId != requesterCharacterId) + { + return null; + } + + await _bazaarItemDao.DeleteByIdAsync(item.Id); + RemoveFromCache(item); + _bazaarSearchManager.RemoveItem(item); + return cachedItem; + } + finally + { + _semaphoreSlim.Release(); + } + } + + public async Task ChangeItemPriceWithDto(BazaarItemDTO bazaarItemDto, long characterId, long price, long saleFee) + { + await _semaphoreSlim.WaitAsync(); + try + { + BazaarItemDTO cachedItem = _itemsCache.Get(bazaarItemDto.Id); + if (cachedItem == null || cachedItem.SoldAmount > 0 || characterId != cachedItem.CharacterId || cachedItem.GetBazaarItemStatus() != BazaarListedItemType.ForSale + || BazaarExtensions.PriceOrAmountExceeds(bazaarItemDto.UsedMedal, price, bazaarItemDto.Amount)) + { + return null; + } + + cachedItem.PricePerItem = price; + cachedItem.SaleFee = saleFee; + BazaarItemDTO savedItem = await _bazaarItemDao.SaveAsync(cachedItem); + return savedItem; + } + finally + { + _semaphoreSlim.Release(); + } + } + + public IReadOnlyCollection SearchBazaarItems(BazaarSearchContext bazaarSearchContext) => _bazaarSearchManager.SearchBazaarItems(bazaarSearchContext); + + public async Task BuyItemWithExpectedValues(long bazaarItemId, long buyerCharacterId, short amount, long pricePerItem) + { + await _semaphoreSlim.WaitAsync(); + try + { + BazaarItemDTO cachedItem = _itemsCache.Get(bazaarItemId); + if (cachedItem == null || buyerCharacterId == cachedItem.CharacterId || amount < 1 || cachedItem.Amount - cachedItem.SoldAmount < amount || pricePerItem != cachedItem.PricePerItem + || cachedItem.IsPackage && amount != cachedItem.Amount || cachedItem.GetBazaarItemStatus() != BazaarListedItemType.ForSale) + { + return null; + } + + cachedItem.SoldAmount += amount; + BazaarItemDTO savedItem = await _bazaarItemDao.SaveAsync(cachedItem); + return savedItem; + } + finally + { + _semaphoreSlim.Release(); + } + } + + public async Task> UnlistItemsWithVnums(List items) + { + await _semaphoreSlim.WaitAsync(); + try + { + foreach (BazaarItemDTO bazaarItemDto in items) + { + bazaarItemDto.ExpiryDate = DateTime.UtcNow.AddDays(-1); + BazaarItemDTO dto = await _bazaarItemDao.SaveAsync(bazaarItemDto); + AddToCache(dto); + _bazaarSearchManager.AddItem(dto); + } + + return items; + } + finally + { + _semaphoreSlim.Release(); + } + } + } +} \ No newline at end of file diff --git a/srcs/BazaarServer/Managers/BazaarSearchManager.cs b/srcs/BazaarServer/Managers/BazaarSearchManager.cs new file mode 100644 index 0000000..e257c15 --- /dev/null +++ b/srcs/BazaarServer/Managers/BazaarSearchManager.cs @@ -0,0 +1,719 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using PhoenixLib.Caching; +using PhoenixLib.Logging; +using WingsAPI.Communication.Bazaar; +using WingsAPI.Data.Bazaar; +using WingsAPI.Data.GameData; +using WingsAPI.Game.Extensions.Bazaar; +using WingsAPI.Packets.Enums.Bazaar; +using WingsAPI.Packets.Enums.Bazaar.Filter; +using WingsAPI.Packets.Enums.Bazaar.SubFilter; +using WingsEmu.Core.Generics; +using WingsEmu.DTOs.Items; +using WingsEmu.Game._enum; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Battle; +using WingsEmu.Packets.Enums.Character; + +namespace BazaarServer.Managers +{ + public class BazaarSearchManager + { + private readonly ILongKeyCachedRepository> _bazaarItemByItemVnum; + private readonly ILongKeyCachedRepository<(ItemDTO, BazaarLevelFilterType)> _cachedItems; + private readonly IResourceLoader _itemDao; + + private readonly Dictionary>> _itemVnumByCategoryAndSubCategory = new(); + + private readonly ThreadSafeHashSet _itemVnumsRegistered = new(); + + public BazaarSearchManager(ILongKeyCachedRepository> bazaarItemByItemVnum, IResourceLoader itemDao, + ILongKeyCachedRepository<(ItemDTO, BazaarLevelFilterType)> cachedItems) + { + _bazaarItemByItemVnum = bazaarItemByItemVnum; + _itemDao = itemDao; + _cachedItems = cachedItems; + } + + public async Task Initialize(IEnumerable bazaarItemDtos) + { + await CategorizeItems(); + await CacheSearchableBazaarItems(bazaarItemDtos); + } + + private async Task CacheSearchableBazaarItems(IEnumerable bazaarItemDtos) + { + Log.Info("[BAZAAR_SEARCH_MANAGER] Caching searchable BazaarItems"); + int count = 0; + foreach (BazaarItemDTO bazaarItemDto in bazaarItemDtos) + { + if (bazaarItemDto.GetBazaarItemStatus() != BazaarListedItemType.ForSale) + { + continue; + } + + AddItem(bazaarItemDto); + count++; + } + + Log.Info($"[BAZAAR_SEARCH_MANAGER] Cached: {count} searchable BazaarItems"); + } + + private async Task CategorizeItems() + { + Log.Info("[BAZAAR_SEARCH_MANAGER] Categorizing items from DB"); + IEnumerable items = await _itemDao.LoadAsync(); + + int count = 0; + var itemsToRegister = new Dictionary>(); + foreach (ItemDTO item in items) + { + (BazaarCategoryFilterType category, IEnumerable subCategories, bool registerForAllSubcategories) = GetCategoryAndSubCategoriesByItemVnum(item); + if (registerForAllSubcategories && !itemsToRegister.TryAdd(category, new HashSet { item.Id })) + { + itemsToRegister[category].Add(item.Id); + } + + if (!_itemVnumByCategoryAndSubCategory.ContainsKey(category)) + { + _itemVnumByCategoryAndSubCategory.Add(category, new Dictionary>()); + } + + foreach (byte subCategory in subCategories) + { + if (!_itemVnumByCategoryAndSubCategory[category].ContainsKey(subCategory)) + { + _itemVnumByCategoryAndSubCategory[category].Add(subCategory, new HashSet()); + } + + _itemVnumByCategoryAndSubCategory[category][subCategory].Add(item.Id); + } + + + if (!_itemVnumByCategoryAndSubCategory[category].ContainsKey(0)) + { + _itemVnumByCategoryAndSubCategory[category].Add(0, new HashSet()); + } + + _itemVnumByCategoryAndSubCategory[category][0].Add(item.Id); + + _cachedItems.Set(item.Id, (item, GetItemLevelFilterByLevel(item.LevelMinimum, item.IsHeroic))); + count++; + } + + foreach (KeyValuePair> itemToRegister in itemsToRegister) + { + Type type = GetSubCategoryTypeByCategory(itemToRegister.Key); + if (type == null) + { + continue; + } + + foreach (byte value in Enum.GetValues(type)) + { + if (!_itemVnumByCategoryAndSubCategory[itemToRegister.Key].ContainsKey(value)) + { + _itemVnumByCategoryAndSubCategory[itemToRegister.Key].Add(value, new HashSet()); + } + + foreach (int vnum in itemToRegister.Value) + { + _itemVnumByCategoryAndSubCategory[itemToRegister.Key][value].Add(vnum); + } + } + } + + Log.Info($"[BAZAAR_SEARCH_MANAGER] Categorized: {count.ToString()} items"); + } + + private static Type GetSubCategoryTypeByCategory(BazaarCategoryFilterType category) + { + switch (category) + { + case BazaarCategoryFilterType.Specialist: + return typeof(BazaarCategorySpecialistSubFilterType); + case BazaarCategoryFilterType.Partner: + return typeof(BazaarCategoryPartnerSubFilterType); + case BazaarCategoryFilterType.Pet: + return typeof(BazaarCategoryPetSubFilterType); + case BazaarCategoryFilterType.StoreMount: + return typeof(BazaarCategoryConsumerItemSubFilterType); + default: + return null; + } + } + + public void AddItem(BazaarItemDTO bazaarItemDto) + { + ItemInstanceDTO itemInstanceDto = bazaarItemDto?.ItemInstance; + if (itemInstanceDto == null) + { + return; + } + + ConcurrentDictionary dictionary = _bazaarItemByItemVnum.GetOrSet(itemInstanceDto.ItemVNum, () => new ConcurrentDictionary()); + dictionary[bazaarItemDto.Id] = bazaarItemDto; + _itemVnumsRegistered.Add(itemInstanceDto.ItemVNum); + } + + public void RemoveItem(BazaarItemDTO bazaarItemDto) + { + if (bazaarItemDto == null) + { + return; + } + + ConcurrentDictionary dictionary = _bazaarItemByItemVnum.GetOrSet(bazaarItemDto.ItemInstance.ItemVNum, () => new ConcurrentDictionary()); + dictionary.Remove(bazaarItemDto.Id, out _); + if (!dictionary.IsEmpty) + { + return; + } + + _bazaarItemByItemVnum.Remove(bazaarItemDto.ItemInstance.ItemVNum); + _itemVnumsRegistered.Remove(bazaarItemDto.ItemInstance.ItemVNum); + } + + public IReadOnlyCollection SearchBazaarItems(BazaarSearchContext bazaarSearchContext) + { + IReadOnlyCollection desiredItemVNums = + (bazaarSearchContext.ItemVNumFilter ?? GetItemVNumsByCategoryAndSubCategory(bazaarSearchContext.CategoryFilterType, bazaarSearchContext.SubTypeFilter)) ?? + _itemVnumsRegistered.ToArray(); + + var itemList = new List(); + var tempItemList = new List(); + + int ignoreCounter = bazaarSearchContext.Index * bazaarSearchContext.AmountOfItemsPerIndex; + int sendCounter = 0; + + foreach (int itemVnum in desiredItemVNums.OrderBy(x => x)) + { + ConcurrentDictionary dictionary = _bazaarItemByItemVnum.Get(itemVnum); + if (dictionary == null) + { + continue; + } + + if (ignoreCounter > dictionary.Count && bazaarSearchContext.LevelFilter == BazaarLevelFilterType.All && bazaarSearchContext.RareFilter == BazaarRarityFilterType.All && + bazaarSearchContext.UpgradeFilter == BazaarUpgradeFilterType.All) + { + ignoreCounter -= dictionary.Count; + continue; + } + + (ItemDTO itemDto, BazaarLevelFilterType baseItemLevelFilter) = _cachedItems.Get(itemVnum); + + bool itemLevelIsInstanceDependant = false; + if (baseItemLevelFilter == BazaarLevelFilterType.All) + { + itemLevelIsInstanceDependant = true; + } + else + { + if (bazaarSearchContext.LevelFilter != BazaarLevelFilterType.All && !ItemLevelFilterChecker(bazaarSearchContext.LevelFilter, baseItemLevelFilter)) + { + continue; + } + } + + IOrderedEnumerable> values; + switch (bazaarSearchContext.OrderFilter) + { + case BazaarSortFilterType.PriceAscending: + values = dictionary.OrderBy(x => x.Value.PricePerItem); + break; + case BazaarSortFilterType.PriceDescending: + values = dictionary.OrderByDescending(x => x.Value.PricePerItem); + break; + case BazaarSortFilterType.AmountAscending: + values = dictionary.OrderBy(x => x.Value.Amount - x.Value.SoldAmount); + break; + case BazaarSortFilterType.AmountDescending: + values = dictionary.OrderByDescending(x => x.Value.Amount - x.Value.SoldAmount); + break; + default: + return null; + } + + foreach ((long _, BazaarItemDTO bazaarItemDto) in values) + { + BazaarListedItemType itemStatus = bazaarItemDto.GetBazaarItemStatus(); + if (itemStatus != BazaarListedItemType.ForSale) + { + RemoveItem(bazaarItemDto); + continue; + } + + if (bazaarItemDto.ItemInstance.Type == ItemInstanceType.BoxInstance) + { + BazaarPerfectionFilterType itemRarityFilter = GetPerfectionFilterByInstance(bazaarItemDto.ItemInstance); + + if ((int)bazaarSearchContext.RareFilter != (int)BazaarPerfectionFilterType.All && (int)itemRarityFilter != (int)bazaarSearchContext.RareFilter) + { + continue; + } + } + else + { + BazaarRarityFilterType itemRarityFilter = GetRarityFilterByInstance(bazaarItemDto.ItemInstance); + + if (bazaarSearchContext.RareFilter != BazaarRarityFilterType.All && itemRarityFilter != bazaarSearchContext.RareFilter) + { + continue; + } + } + + BazaarUpgradeFilterType itemUpgradeFilter = GetUpgradeFilterByInstance(bazaarItemDto.ItemInstance); + + if (bazaarSearchContext.UpgradeFilter != BazaarUpgradeFilterType.All && itemUpgradeFilter != bazaarSearchContext.UpgradeFilter) + { + continue; + } + + if (itemLevelIsInstanceDependant) + { + BazaarLevelFilterType itemLevelFilter = GetItemLevelFilterByInstance(bazaarItemDto.ItemInstance, bazaarItemDto.ItemInstance.Type); + + if (bazaarSearchContext.LevelFilter != BazaarLevelFilterType.All && itemLevelFilter != bazaarSearchContext.LevelFilter) + { + continue; + } + } + + if (bazaarSearchContext.SubTypeFilter != 0 && bazaarItemDto.ItemInstance.Type == ItemInstanceType.BoxInstance) + { + if ((UsableItemSubType)itemDto.ItemSubType != UsableItemSubType.RaidBoxOrSealedJajamaruSpOrSealedSakuraBead) + { + ItemInstanceDTO boxInstanceDto = bazaarItemDto.ItemInstance; + + if (!GetBoxInstanceSubCategories(boxInstanceDto, itemDto).Contains(bazaarSearchContext.SubTypeFilter)) + { + continue; + } + } + } + + if (ignoreCounter > 0) + { + ignoreCounter--; + continue; + } + + if (sendCounter >= bazaarSearchContext.AmountOfItemsPerIndex) + { + break; + } + + sendCounter++; + + tempItemList.Add(bazaarItemDto); + } + + itemList.AddRange(tempItemList); + tempItemList.Clear(); + + if (sendCounter >= bazaarSearchContext.AmountOfItemsPerIndex) + { + break; + } + } + + return itemList; + } + + private List GetBoxInstanceSubCategories(ItemInstanceDTO boxInstanceDto, ItemDTO itemInfo) + { + var subCategory = new List(); + switch ((UsableItemSubType)itemInfo.ItemSubType) + { + case UsableItemSubType.PetBead: + subCategory.Add( + (byte)(boxInstanceDto.HoldingVNum is 0 or null ? BazaarCategoryPetSubFilterType.EmptyPetBead : BazaarCategoryPetSubFilterType.PetBead)); + break; + case UsableItemSubType.PartnerBead: + if (boxInstanceDto.HoldingVNum is 0 or null) + { + subCategory.Add((byte)BazaarCategoryPartnerSubFilterType.EmptyPartnerBead); + break; + } + + subCategory.Add((byte)BazaarCategoryPartnerSubFilterType.PartnerBead); + subCategory.Add((byte)GetPartnerSubCategoryOfAttack(_cachedItems.Get(boxInstanceDto.HoldingVNum.Value).Item1)); + break; + case UsableItemSubType.PartnerSpHolder: + if (boxInstanceDto.HoldingVNum is 0 or null) + { + subCategory.Add((byte)BazaarCategoryPartnerSubFilterType.EmptyCardHolder); + break; + } + + subCategory.Add((byte)GetPartnerSubCategoryOfAttack(_cachedItems.Get(boxInstanceDto.HoldingVNum.Value).Item1)); + break; + case UsableItemSubType.SpecialistHolder: + if (boxInstanceDto.HoldingVNum is 0 or null) + { + subCategory.Add((byte)BazaarCategorySpecialistSubFilterType.EmptyCardHolder); + break; + } + + subCategory.Add(SpMorphToSubCategory(_cachedItems.Get(boxInstanceDto.HoldingVNum.Value).Item1)); + break; + case UsableItemSubType.VehicleBead: + subCategory.Add((byte)(boxInstanceDto.HoldingVNum is 0 or null ? BazaarCategoryStoreMountSubFilterType.EmptyMountBead : BazaarCategoryStoreMountSubFilterType.MountBead)); + break; + } + + return subCategory; + } + + private (BazaarCategoryFilterType, IEnumerable, bool) GetCategoryAndSubCategoriesByItemVnum(ItemDTO itemDto) + { + BazaarCategoryFilterType category = BazaarCategoryFilterType.Miscellaneous; + var subCategories = new List(); + bool registerForAllSubcategories = false; + switch (itemDto.ItemType) + { + case ItemType.Weapon: + category = BazaarCategoryFilterType.Weapon; + subCategories.Add(ItemTypeClassToSubCategory(itemDto.Class)); + break; + case ItemType.Armor: + category = BazaarCategoryFilterType.Armour; + subCategories.Add(ItemTypeClassToSubCategory(itemDto.Class)); + break; + case ItemType.Fashion: + category = BazaarCategoryFilterType.Equipment; + subCategories.Add(EquipmentTypeToSubCategory1(itemDto.EquipmentSlot)); + byte? secondSubCategory = EquipmentTypeToSubCategory2(itemDto.EquipmentSlot, itemDto.Sex switch + { + 0 => GenderType.Unisex, + 1 => GenderType.Male, + _ => GenderType.Female + }); + if (secondSubCategory != null) + { + subCategories.Add(secondSubCategory.Value); + } + + break; + case ItemType.Jewelry: + category = BazaarCategoryFilterType.Accessories; + subCategories.Add(EquipmentTypeToSubCategory3(itemDto.EquipmentSlot)); + break; + case ItemType.Specialist: + category = BazaarCategoryFilterType.Specialist; + subCategories.Add(SpMorphToSubCategory(itemDto)); + break; + case ItemType.Box: + var usableItemSubType = (UsableItemSubType)itemDto.ItemSubType; + switch (usableItemSubType) + { + case UsableItemSubType.PetBead: + category = BazaarCategoryFilterType.Pet; + registerForAllSubcategories = true; + break; + case UsableItemSubType.PartnerBead: + category = BazaarCategoryFilterType.Partner; + registerForAllSubcategories = true; + break; + case UsableItemSubType.PartnerSpHolder: + category = BazaarCategoryFilterType.Partner; + registerForAllSubcategories = true; + break; + case UsableItemSubType.SpecialistHolder: + category = BazaarCategoryFilterType.Specialist; + registerForAllSubcategories = true; + break; + case UsableItemSubType.FairyBead: + category = BazaarCategoryFilterType.Accessories; + subCategories.Add((byte)BazaarCategoryAccessoriesSubFilterType.Fairy); + break; + case UsableItemSubType.VehicleBead: + category = BazaarCategoryFilterType.StoreMount; + registerForAllSubcategories = true; + break; + } + + break; + case ItemType.Shell: + category = BazaarCategoryFilterType.Shell; + var shellItemSubType = (ShellItemSubType)itemDto.ItemSubType; + subCategories.Add(shellItemSubType switch + { + ShellItemSubType.Weapon => (byte)BazaarCategoryShellSubFilterType.Weapon, + ShellItemSubType.Armor => (byte)BazaarCategoryShellSubFilterType.Clothing, + _ => default + }); + break; + case ItemType.Main: + category = BazaarCategoryFilterType.MainItem; + subCategories.Add((byte)BazaarCategoryMainItemSubFilterType.GeneralItems); + break; + case ItemType.Upgrade: + category = BazaarCategoryFilterType.MainItem; + subCategories.Add((byte)BazaarCategoryMainItemSubFilterType.Material); + break; + case ItemType.Production: + category = BazaarCategoryFilterType.MainItem; + subCategories.Add((byte)BazaarCategoryMainItemSubFilterType.ProductionItem); + break; + case ItemType.Special: + category = BazaarCategoryFilterType.MainItem; + subCategories.Add((byte)BazaarCategoryMainItemSubFilterType.SpecialItems); + break; + case ItemType.Potion: + category = BazaarCategoryFilterType.MainItem; + subCategories.Add((byte)BazaarCategoryMainItemSubFilterType.HealingPotion); + break; + case ItemType.Event: + category = BazaarCategoryFilterType.MainItem; + subCategories.Add((byte)BazaarCategoryMainItemSubFilterType.Event); + break; + case ItemType.Title: + category = BazaarCategoryFilterType.MainItem; + subCategories.Add((byte)BazaarCategoryMainItemSubFilterType.Title); + break; + case ItemType.Sell: + category = BazaarCategoryFilterType.ConsumerItem; + subCategories.Add((byte)BazaarCategoryConsumerItemSubFilterType.SaleItem); + break; + case ItemType.Food: + category = BazaarCategoryFilterType.ConsumerItem; + subCategories.Add((byte)BazaarCategoryConsumerItemSubFilterType.Food); + break; + case ItemType.Snack: + category = BazaarCategoryFilterType.ConsumerItem; + subCategories.Add((byte)BazaarCategoryConsumerItemSubFilterType.Snack); + break; + case ItemType.Magical: + category = BazaarCategoryFilterType.ConsumerItem; + subCategories.Add((byte)BazaarCategoryConsumerItemSubFilterType.MagicItem); + break; + case ItemType.Material: + category = BazaarCategoryFilterType.ConsumerItem; + subCategories.Add((byte)BazaarCategoryConsumerItemSubFilterType.Ingredients); + break; + case ItemType.PetPartnerItem: + category = BazaarCategoryFilterType.ConsumerItem; + subCategories.Add((byte)BazaarCategoryConsumerItemSubFilterType.PartnerItem); + break; + } + + return (category, subCategories, registerForAllSubcategories); + } + + private static byte ItemTypeClassToSubCategory(byte itemTypeClass) + { + return itemTypeClass switch + { + (int)ItemClassType.Adventurer => (byte)BazaarCategoryWeaponArmourSubFilterType.Adventurer, + (int)ItemClassType.Swordsman => (byte)BazaarCategoryWeaponArmourSubFilterType.Swordsman, + (int)ItemClassType.Archer => (byte)BazaarCategoryWeaponArmourSubFilterType.Archer, + (int)ItemClassType.Mage => (byte)BazaarCategoryWeaponArmourSubFilterType.Magician, + (int)ItemClassType.MartialArtist => (byte)BazaarCategoryWeaponArmourSubFilterType.MartialArtist, + _ => default + }; + } + + private static byte EquipmentTypeToSubCategory1(EquipmentType equipmentType) + { + return equipmentType switch + { + EquipmentType.Hat => (byte)BazaarCategoryEquipmentSubFilterType.Hat, + EquipmentType.Mask => (byte)BazaarCategoryEquipmentSubFilterType.Accessory, + EquipmentType.Gloves => (byte)BazaarCategoryEquipmentSubFilterType.Gloves, + EquipmentType.Boots => (byte)BazaarCategoryEquipmentSubFilterType.Shoes, + EquipmentType.CostumeSuit => (byte)BazaarCategoryEquipmentSubFilterType.Costume, + EquipmentType.CostumeHat => (byte)BazaarCategoryEquipmentSubFilterType.CostumeHat, + EquipmentType.WeaponSkin => (byte)BazaarCategoryEquipmentSubFilterType.CostumeWeapon, + EquipmentType.Wings => (byte)BazaarCategoryEquipmentSubFilterType.CostumeWings, + _ => default + }; + } + + private static byte? EquipmentTypeToSubCategory2(EquipmentType equipmentType, GenderType genderType) + { + if (genderType == GenderType.Unisex) + { + return null; + } + + bool isMale = genderType == GenderType.Male; + + return equipmentType switch + { + EquipmentType.CostumeSuit => (byte)(isMale ? BazaarCategoryEquipmentSubFilterType.CostumeMale : BazaarCategoryEquipmentSubFilterType.CostumeFemale), + EquipmentType.CostumeHat => (byte)(isMale ? BazaarCategoryEquipmentSubFilterType.CostumeHatMale : BazaarCategoryEquipmentSubFilterType.CostumeHatFemale), + _ => null + }; + } + + private static byte EquipmentTypeToSubCategory3(EquipmentType equipmentType) + { + return equipmentType switch + { + EquipmentType.Necklace => (byte)BazaarCategoryAccessoriesSubFilterType.Necklace, + EquipmentType.Ring => (byte)BazaarCategoryAccessoriesSubFilterType.Ring, + EquipmentType.Bracelet => (byte)BazaarCategoryAccessoriesSubFilterType.Bracelet, + EquipmentType.Fairy => (byte)BazaarCategoryAccessoriesSubFilterType.Fairy, + EquipmentType.Amulet => (byte)BazaarCategoryAccessoriesSubFilterType.Amulet, + _ => default + }; + } + + private static byte SpMorphToSubCategory(ItemDTO item) + { + int morphId = Convert.ToInt32(item.Morph); + if (!Enum.IsDefined(typeof(MorphIdType), morphId)) + { + return default; + } + + var morph = (MorphIdType)morphId; + return morph switch + { + MorphIdType.Pyjama => (byte)BazaarCategorySpecialistSubFilterType.Pyjama, + MorphIdType.Warrior => (byte)BazaarCategorySpecialistSubFilterType.Warrior, + MorphIdType.Ninja => (byte)BazaarCategorySpecialistSubFilterType.Ninja, + MorphIdType.Ranger => (byte)BazaarCategorySpecialistSubFilterType.Ranger, + MorphIdType.Assassin => (byte)BazaarCategorySpecialistSubFilterType.Assassin, + MorphIdType.RedMagician => (byte)BazaarCategorySpecialistSubFilterType.RedMagician, + MorphIdType.HolyMage => (byte)BazaarCategorySpecialistSubFilterType.HolyMage, + MorphIdType.Chicken => (byte)BazaarCategorySpecialistSubFilterType.ChickenCostume, + MorphIdType.Jajamaru => (byte)BazaarCategorySpecialistSubFilterType.Jajamaru, + MorphIdType.Crusader => (byte)BazaarCategorySpecialistSubFilterType.Crusader, + MorphIdType.Berserker => (byte)BazaarCategorySpecialistSubFilterType.Berserker, + MorphIdType.Destroyer => (byte)BazaarCategorySpecialistSubFilterType.Destroyer, + MorphIdType.WildKeeper => (byte)BazaarCategorySpecialistSubFilterType.WildKeeper, + MorphIdType.BlueMagician => (byte)BazaarCategorySpecialistSubFilterType.BlueMagician, + MorphIdType.DarkGunner => (byte)BazaarCategorySpecialistSubFilterType.DarkGunner, + MorphIdType.Pirate => (byte)BazaarCategorySpecialistSubFilterType.Pirate, + MorphIdType.Gladiator => (byte)BazaarCategorySpecialistSubFilterType.Gladiator, + MorphIdType.FireCannoneer => (byte)BazaarCategorySpecialistSubFilterType.FireCannoneer, + MorphIdType.Volcano => (byte)BazaarCategorySpecialistSubFilterType.Volcano, + MorphIdType.BattleMonk => (byte)BazaarCategorySpecialistSubFilterType.BattleMonk, + MorphIdType.Scout => (byte)BazaarCategorySpecialistSubFilterType.Scout, + MorphIdType.TideLord => (byte)BazaarCategorySpecialistSubFilterType.TideLord, + MorphIdType.DeathReaper => (byte)BazaarCategorySpecialistSubFilterType.DeathReaper, + MorphIdType.DemonHunter => (byte)BazaarCategorySpecialistSubFilterType.DemonHunter, + MorphIdType.Seer => (byte)BazaarCategorySpecialistSubFilterType.Seer, + MorphIdType.Renegade => (byte)BazaarCategorySpecialistSubFilterType.Renegade, + MorphIdType.AvengingAngel => (byte)BazaarCategorySpecialistSubFilterType.AvengingAngel, + MorphIdType.Archmage => (byte)BazaarCategorySpecialistSubFilterType.Archmage, + MorphIdType.DraconicFist => (byte)BazaarCategorySpecialistSubFilterType.DraconicFist, + MorphIdType.MysticArts => (byte)BazaarCategorySpecialistSubFilterType.MysticArts, + MorphIdType.WeddingCostume => (byte)BazaarCategorySpecialistSubFilterType.WeddingCostume, + MorphIdType.MasterWolf => (byte)BazaarCategorySpecialistSubFilterType.MasterWolf, + MorphIdType.DemonWarrior => (byte)BazaarCategorySpecialistSubFilterType.DemonWarrior, + _ => default + }; + } + + private BazaarLevelFilterType GetItemLevelFilterByInstance(ItemInstanceDTO itemInstanceDto, ItemInstanceType instanceType) + { + switch (instanceType) + { + case ItemInstanceType.BoxInstance: + case ItemInstanceType.SpecialistInstance: + ItemInstanceDTO specialistInstanceDto = itemInstanceDto; + return GetItemLevelFilterByLevel(specialistInstanceDto.SpLevel, false); + default: + (ItemDTO itemDto, BazaarLevelFilterType bazaarLevelFilterType) = _cachedItems.Get(itemInstanceDto.ItemVNum); + return itemDto.ItemType == ItemType.Shell + ? GetItemLevelFilterByLevel(itemInstanceDto.Upgrade, false) + : bazaarLevelFilterType; + } + } + + private static BazaarLevelFilterType GetItemLevelFilterByLevel(byte level, bool isHeroic) + { + if (isHeroic) + { + if (level is < 1 or > 60) + { + return BazaarLevelFilterType.ChampionGear; + } + + return (BazaarLevelFilterType)(Convert.ToByte(Math.Ceiling(level / 10f)) + (byte)BazaarLevelFilterType.ChampionGear); + } + + if (level is < 1 or > 99) + { + return BazaarLevelFilterType.All; + } + + return (BazaarLevelFilterType)Convert.ToByte(Math.Ceiling(level / 10f)); + } + + private static bool ItemLevelFilterChecker(BazaarLevelFilterType demandedFilterType, BazaarLevelFilterType itemFilterType) + { + return demandedFilterType == itemFilterType || demandedFilterType == BazaarLevelFilterType.All || demandedFilterType == BazaarLevelFilterType.ChampionGear && itemFilterType switch + { + BazaarLevelFilterType.ChampionLevelOneToTen => true, + BazaarLevelFilterType.ChampionLevelElevenToTwenty => true, + BazaarLevelFilterType.ChampionLevelTwentyOneToThirty => true, + BazaarLevelFilterType.ChampionLevelThirtyOneToForty => true, + BazaarLevelFilterType.ChampionLevelFortyOneToFifty => true, + BazaarLevelFilterType.ChampionLevelFiftyOneToSixty => true, + _ => false + }; + } + + private static BazaarUpgradeFilterType GetUpgradeFilterByInstance(ItemInstanceDTO itemInstanceDto) + { + if (15 < itemInstanceDto.Upgrade) + { + return BazaarUpgradeFilterType.All; + } + + return (BazaarUpgradeFilterType)(itemInstanceDto.Upgrade + 1); + } + + private static BazaarRarityFilterType GetRarityFilterByInstance(ItemInstanceDTO itemInstanceDto) + { + if (itemInstanceDto.Rarity is < 0 or > 8) + { + return BazaarRarityFilterType.All; + } + + return (BazaarRarityFilterType)(itemInstanceDto.Rarity + 1); + } + + private static BazaarPerfectionFilterType GetPerfectionFilterByInstance(ItemInstanceDTO itemInstanceDto) + { + const int max = 100; + return itemInstanceDto.SpStoneUpgrade switch + { + < 1 or < max => BazaarPerfectionFilterType.All, + max => BazaarPerfectionFilterType.NintyOneToHundred, + _ => (BazaarPerfectionFilterType)Convert.ToByte(Math.Ceiling(itemInstanceDto.SpStoneUpgrade / 10f)) + }; + } + + private static BazaarCategoryPartnerSubFilterType GetPartnerSubCategoryOfAttack(ItemDTO item) + { + return item.PartnerClass switch + { + (byte)AttackType.Melee => BazaarCategoryPartnerSubFilterType.CloseAttack, + (byte)AttackType.Ranged => BazaarCategoryPartnerSubFilterType.RemoteAttack, + (byte)AttackType.Magical => BazaarCategoryPartnerSubFilterType.Magic, + _ => BazaarCategoryPartnerSubFilterType.All + }; + } + + private IReadOnlyCollection GetItemVNumsByCategoryAndSubCategory(BazaarCategoryFilterType categoryFilterType, byte subTypeFilter) + { + if (categoryFilterType == BazaarCategoryFilterType.All || !_itemVnumByCategoryAndSubCategory.ContainsKey(categoryFilterType) || + !_itemVnumByCategoryAndSubCategory[categoryFilterType].ContainsKey(subTypeFilter)) + { + return null; + } + + return _itemVnumByCategoryAndSubCategory[categoryFilterType][subTypeFilter]; + } + } +} \ No newline at end of file diff --git a/srcs/BazaarServer/Program.cs b/srcs/BazaarServer/Program.cs new file mode 100644 index 0000000..bcd70bf --- /dev/null +++ b/srcs/BazaarServer/Program.cs @@ -0,0 +1,82 @@ +using System; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using PhoenixLib.Logging; +using PhoenixLib.ServiceBus.MQTT; +using Plugin.Database.Mapping; +using ProtoBuf.Grpc.Client; + +namespace BazaarServer +{ + public class Program + { + public static async Task Main(string[] args) + { + PrintHeader(); + NonGameMappingRules.InitializeMapping(); + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + GrpcClientFactory.AllowUnencryptedHttp2 = true; + using var stopService = new DockerGracefulStopService(); + using IHost host = CreateHostBuilder(args).Build(); + { + await host.StartAsync(); + IMessagingService messagingService = host.Services.GetRequiredService(); + await messagingService.StartAsync(); + + IServiceProvider services = host.Services; + Log.Info("BazaarServer started"); + await host.WaitForShutdownAsync(stopService.CancellationToken); + await messagingService.DisposeAsync(); + } + } + + private static void PrintHeader() + { + const string text = @" +██╗ ██╗██╗███╗ ██╗ ██████╗ ███████╗███████╗███╗ ███╗██╗ ██╗ +██║ ██║██║████╗ ██║██╔════╝ ██╔════╝██╔════╝████╗ ████║██║ ██║ +██║ █╗ ██║██║██╔██╗ ██║██║ ███╗███████╗█████╗ ██╔████╔██║██║ ██║ +██║███╗██║██║██║╚██╗██║██║ ██║╚════██║██╔══╝ ██║╚██╔╝██║██║ ██║ +╚███╔███╔╝██║██║ ╚████║╚██████╔╝███████║███████╗██║ ╚═╝ ██║╚██████╔╝ + ╚══╝╚══╝ ╚═╝╚═╝ ╚═══╝ ╚═════╝ ╚══════╝╚══════╝╚═╝ ╚═╝ ╚═════╝ + +██████╗ █████╗ ███████╗ █████╗ █████╗ ██████╗ ███████╗███████╗██████╗ ██╗ ██╗███████╗██████╗ +██╔══██╗██╔══██╗╚══███╔╝██╔══██╗██╔══██╗██╔══██╗ ██╔════╝██╔════╝██╔══██╗██║ ██║██╔════╝██╔══██╗ +██████╔╝███████║ ███╔╝ ███████║███████║██████╔╝█████╗███████╗█████╗ ██████╔╝██║ ██║█████╗ ██████╔╝ +██╔══██╗██╔══██║ ███╔╝ ██╔══██║██╔══██║██╔══██╗╚════╝╚════██║██╔══╝ ██╔══██╗╚██╗ ██╔╝██╔══╝ ██╔══██╗ +██████╔╝██║ ██║███████╗██║ ██║██║ ██║██║ ██║ ███████║███████╗██║ ██║ ╚████╔╝ ███████╗██║ ██║ +╚═════╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚══════╝╚══════╝╚═╝ ╚═╝ ╚═══╝ ╚══════╝╚═╝ ╚═╝ + +"; + string separator = new('=', Console.WindowWidth); + string logo = text.Split('\n').Select(s => string.Format("{0," + (Console.WindowWidth / 2 + s.Length / 2) + "}\n", s)) + .Aggregate("", (current, i) => current + i); + + Console.ForegroundColor = ConsoleColor.Cyan; + Console.WriteLine(separator + logo + $"Version: {Assembly.GetExecutingAssembly().GetName().Version}\n" + separator); + Console.ForegroundColor = ConsoleColor.White; + } + + // Additional configuration is required to successfully run gRPC on macOS. + // For instructions on how to configure Kestrel and gRPC clients on macOS, visit https://go.microsoft.com/fwlink/?linkid=2099682 + private static IHostBuilder CreateHostBuilder(string[] args) + { + IHostBuilder host = Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.ConfigureKestrel(s => + { + s.ListenAnyIP(short.Parse(Environment.GetEnvironmentVariable("BAZAAR_SERVER_PORT") ?? "25555"), options => { options.Protocols = HttpProtocols.Http2; }); + }); + webBuilder.UseStartup(); + }); + return host; + } + } +} \ No newline at end of file diff --git a/srcs/BazaarServer/RecurrentJobs/BazaarSystem.cs b/srcs/BazaarServer/RecurrentJobs/BazaarSystem.cs new file mode 100644 index 0000000..704afe6 --- /dev/null +++ b/srcs/BazaarServer/RecurrentJobs/BazaarSystem.cs @@ -0,0 +1,25 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using BazaarServer.Managers; +using Microsoft.Extensions.Hosting; +using PhoenixLib.Logging; + +namespace BazaarServer.RecurrentJobs +{ + public class BazaarSystem : BackgroundService + { + private static readonly TimeSpan Interval = TimeSpan.FromSeconds(10); + private readonly BazaarManager _bazaarManager; + + public BazaarSystem(BazaarManager bazaarManager) => _bazaarManager = bazaarManager; + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + Log.Info("[BAZAAR_SYSTEM] Caching all items in the database..."); + long countCachedItems = await _bazaarManager.CacheAllItemsInDb(); + Log.Info($"[BAZAAR_SYSTEM] Cached: {countCachedItems.ToString()} item/s"); + Log.Info("[BAZAAR_SYSTEM] Started!"); + } + } +} \ No newline at end of file diff --git a/srcs/BazaarServer/Services/BazaarService.cs b/srcs/BazaarServer/Services/BazaarService.cs new file mode 100644 index 0000000..1d128cf --- /dev/null +++ b/srcs/BazaarServer/Services/BazaarService.cs @@ -0,0 +1,243 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using BazaarServer.Managers; +using WingsAPI.Communication; +using WingsAPI.Communication.Bazaar; +using WingsAPI.Data.Bazaar; +using WingsEmu.Health; + +namespace BazaarServer.Services +{ + public class BazaarService : IBazaarService + { + private readonly BazaarManager _bazaarManager; + private readonly IMaintenanceManager _maintenanceManager; + + public BazaarService(BazaarManager bazaarManager, IMaintenanceManager maintenanceManager) + { + _bazaarManager = bazaarManager; + _maintenanceManager = maintenanceManager; + } + + private bool MaintenanceMode => _maintenanceManager.IsMaintenanceActive; + + public async ValueTask GetBazaarItemById(BazaarGetItemByIdRequest request) + { + if (MaintenanceMode) + { + return new BazaarItemResponse + { + ResponseType = RpcResponseType.MAINTENANCE_MODE + }; + } + + BazaarItemDTO bazaarItem = await _bazaarManager.GetBazaarItemById(request.BazaarItemId); + return new BazaarItemResponse + { + ResponseType = bazaarItem == null ? RpcResponseType.GENERIC_SERVER_ERROR : RpcResponseType.SUCCESS, + BazaarItemDto = bazaarItem + }; + } + + public async ValueTask AddItemToBazaar(BazaarAddItemRequest request) + { + if (MaintenanceMode) + { + return new BazaarItemResponse + { + ResponseType = RpcResponseType.MAINTENANCE_MODE + }; + } + + if (request.MaximumListedItems <= _bazaarManager.GetItemsByCharacterId(request.BazaarItemDto.CharacterId)?.Count) + { + return new BazaarItemResponse + { + ResponseType = RpcResponseType.GENERIC_SERVER_ERROR + }; + } + + BazaarItemDTO bazaarItemDto = await _bazaarManager.SaveAsync(request.BazaarItemDto); + return new BazaarItemResponse + { + ResponseType = RpcResponseType.SUCCESS, + BazaarItemDto = bazaarItemDto + }; + } + + public async ValueTask RemoveItemFromBazaar(BazaarRemoveItemRequest request) + { + if (MaintenanceMode) + { + return new BazaarItemResponse + { + ResponseType = RpcResponseType.MAINTENANCE_MODE + }; + } + + BazaarItemDTO deletedItem = await _bazaarManager.DeleteItemWithDto(request.BazaarItemDto, request.RequesterCharacterId); + + if (deletedItem == null) + { + return new BazaarItemResponse + { + ResponseType = RpcResponseType.GENERIC_SERVER_ERROR + }; + } + + return new BazaarItemResponse + { + ResponseType = RpcResponseType.SUCCESS, + BazaarItemDto = deletedItem + }; + } + + public async ValueTask ChangeItemPriceFromBazaar(BazaarChangeItemPriceRequest request) + { + if (MaintenanceMode) + { + return new BazaarItemResponse + { + ResponseType = RpcResponseType.MAINTENANCE_MODE + }; + } + + BazaarItemDTO updatedItem = await _bazaarManager.ChangeItemPriceWithDto(request.BazaarItemDto, request.ChangerCharacterId, request.NewPrice, request.NewSaleFee); + if (updatedItem == null) + { + return new BazaarItemResponse + { + ResponseType = RpcResponseType.GENERIC_SERVER_ERROR, + BazaarItemDto = null + }; + } + + return new BazaarItemResponse + { + ResponseType = RpcResponseType.SUCCESS, + BazaarItemDto = updatedItem + }; + } + + public async ValueTask GetItemsByCharacterIdFromBazaar(BazaarGetItemsByCharIdRequest request) + { + if (MaintenanceMode) + { + return new BazaarGetItemsByCharIdResponse + { + ResponseType = RpcResponseType.MAINTENANCE_MODE + }; + } + + return new BazaarGetItemsByCharIdResponse + { + ResponseType = RpcResponseType.SUCCESS, + BazaarItems = _bazaarManager.GetItemsByCharacterId(request.CharacterId) + }; + } + + public async ValueTask RemoveItemsByCharacterIdFromBazaar(BazaarRemoveItemsByCharIdRequest request) + { + if (MaintenanceMode) + { + return new BazaarRemoveItemsByCharIdResponse + { + ResponseType = RpcResponseType.MAINTENANCE_MODE + }; + } + + ICollection items = _bazaarManager.GetItemsByCharacterId(request.CharacterId); + + if (items == null) + { + return new BazaarRemoveItemsByCharIdResponse + { + ResponseType = RpcResponseType.SUCCESS + }; + } + + foreach (BazaarItemDTO item in items) + { + await _bazaarManager.DeleteItemWithDto(item, request.CharacterId); + } + + return new BazaarRemoveItemsByCharIdResponse + { + ResponseType = RpcResponseType.SUCCESS + }; + } + + public async ValueTask SearchBazaarItems(BazaarSearchBazaarItemsRequest request) + { + if (MaintenanceMode) + { + return new BazaarSearchBazaarItemsResponse + { + ResponseType = RpcResponseType.MAINTENANCE_MODE + }; + } + + return new BazaarSearchBazaarItemsResponse + { + ResponseType = RpcResponseType.SUCCESS, + BazaarItemDtos = _bazaarManager.SearchBazaarItems(request.BazaarSearchContext) + }; + } + + public async ValueTask BuyItemFromBazaar(BazaarBuyItemRequest request) + { + if (MaintenanceMode) + { + return new BazaarItemResponse + { + ResponseType = RpcResponseType.MAINTENANCE_MODE + }; + } + + BazaarItemDTO cachedItem = await _bazaarManager.BuyItemWithExpectedValues(request.BazaarItemId, request.BuyerCharacterId, request.Amount, request.PricePerItem); + if (cachedItem == null) + { + return new BazaarItemResponse + { + BazaarItemDto = null, + ResponseType = RpcResponseType.GENERIC_SERVER_ERROR + }; + } + + return new BazaarItemResponse + { + BazaarItemDto = cachedItem, + ResponseType = RpcResponseType.SUCCESS + }; + } + + public async ValueTask UnlistItemsFromBazaarWithVnumAsync(UnlistItemFromBazaarRequest request) + { + IReadOnlyCollection itemsToUnlist = _bazaarManager.SearchBazaarItems(new BazaarSearchContext + { + ItemVNumFilter = request.Vnum, + Index = 0, + AmountOfItemsPerIndex = 10000 + }); + List unlistedItems = await _bazaarManager.UnlistItemsWithVnums(itemsToUnlist.ToList()); + + return new UnlistItemFromBazaarResponse + { + UnlistedItems = unlistedItems.Count + }; + } + + public async ValueTask UnlistCharacterItemsFromBazaarAsync(UnlistCharacterItemsFromBazaarRequest request) + { + ICollection itemsToUnlist = _bazaarManager.GetItemsByCharacterId(request.Id); + + List unlistedItems = await _bazaarManager.UnlistItemsWithVnums(itemsToUnlist.ToList()); + + return new UnlistItemFromBazaarResponse + { + UnlistedItems = unlistedItems.Count + }; + } + } +} \ No newline at end of file diff --git a/srcs/BazaarServer/Startup.cs b/srcs/BazaarServer/Startup.cs new file mode 100644 index 0000000..08d108d --- /dev/null +++ b/srcs/BazaarServer/Startup.cs @@ -0,0 +1,70 @@ +using BazaarServer.Consumers; +using BazaarServer.Managers; +using BazaarServer.RecurrentJobs; +using BazaarServer.Services; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using PhoenixLib.Caching; +using PhoenixLib.Events; +using PhoenixLib.Logging; +using PhoenixLib.ServiceBus.Extensions; +using Plugin.Database; +using Plugin.ResourceLoader; +using ProtoBuf.Grpc.Server; +using WingsAPI.Communication.Services.Messages; +using WingsEmu.Communication.gRPC.Extensions; +using WingsEmu.Health.Extensions; + +namespace BazaarServer +{ + public class Startup + { + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) + { + services.AddMqttConfigurationFromEnv(); + services.AddMaintenanceMode(); + services.AddEventPipeline(); + services.AddEventHandlersInAssembly(); + new DatabasePlugin().AddDependencies(services); + new FileResourceLoaderPlugin().AddDependencies(services); + + services.TryAddSingleton(typeof(ILongKeyCachedRepository<>), typeof(InMemoryCacheRepository<>)); + services.TryAddSingleton(typeof(IUuidKeyCachedRepository<>), typeof(InMemoryUuidCacheRepository<>)); + + services.AddGrpcBazaarServiceClient(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddHostedService(s => s.GetRequiredService()); + services.AddPhoenixLogging(); + + services.AddCodeFirstGrpc(config => + { + config.MaxReceiveMessageSize = null; + config.MaxSendMessageSize = null; + config.EnableDetailedErrors = true; + }); + + services.AddMessageSubscriber(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseRouting(); + + app.UseEndpoints(endpoints => { endpoints.MapGrpcService(); }); + } + } +} \ No newline at end of file diff --git a/srcs/DatabaseServer/Consumers/ServiceFlushAllMessageConsumer.cs b/srcs/DatabaseServer/Consumers/ServiceFlushAllMessageConsumer.cs new file mode 100644 index 0000000..8bcc9f6 --- /dev/null +++ b/srcs/DatabaseServer/Consumers/ServiceFlushAllMessageConsumer.cs @@ -0,0 +1,29 @@ +using System.Threading; +using System.Threading.Tasks; +using DatabaseServer.Managers; +using PhoenixLib.ServiceBus; +using WingsAPI.Communication.Services.Messages; + +namespace DatabaseServer.Consumers +{ + public class ServiceFlushAllMessageConsumer : IMessageConsumer + { + private readonly IAccountWarehouseManager _accountWarehouseManager; + private readonly ICharacterManager _characterManager; + private readonly ITimeSpaceManager _timeSpaceManager; + + public ServiceFlushAllMessageConsumer(ICharacterManager characterManager, IAccountWarehouseManager accountWarehouseManager, ITimeSpaceManager timeSpaceManager) + { + _characterManager = characterManager; + _accountWarehouseManager = accountWarehouseManager; + _timeSpaceManager = timeSpaceManager; + } + + public async Task HandleAsync(ServiceFlushAllMessage notification, CancellationToken token) + { + await _characterManager.FlushCharacterSaves(); + await _accountWarehouseManager.FlushWarehouseSaves(); + await _timeSpaceManager.FlushTimeSpaceRecords(); + } + } +} \ No newline at end of file diff --git a/srcs/DatabaseServer/DatabaseServer.csproj b/srcs/DatabaseServer/DatabaseServer.csproj new file mode 100644 index 0000000..74c7394 --- /dev/null +++ b/srcs/DatabaseServer/DatabaseServer.csproj @@ -0,0 +1,26 @@ + + + + net5.0 + ..\..\dist\database-server\ + false + + + + + + + + + + + + + + + + + + + + diff --git a/srcs/DatabaseServer/DockerGracefulStopService.cs b/srcs/DatabaseServer/DockerGracefulStopService.cs new file mode 100644 index 0000000..1d97a6e --- /dev/null +++ b/srcs/DatabaseServer/DockerGracefulStopService.cs @@ -0,0 +1,53 @@ +using System; +using System.Runtime.Loader; +using System.Threading; +using PhoenixLib.Logging; + +namespace FamilyServer +{ + public class DockerGracefulStopService : IDisposable + { + private readonly CancellationTokenSource _cancellationTokenSource; + private readonly ManualResetEventSlim _stoppedEvent; + + public DockerGracefulStopService() + { + _cancellationTokenSource = new CancellationTokenSource(); + _stoppedEvent = new ManualResetEventSlim(); + // SIGINT + Console.CancelKeyPress += (sender, eventArgs) => + { + Log.Error("[GRACEFUL_SHUTDOWN] SIGINT received", new Exception("[GRACEFUL_SHUTDOWN] SIGINT received")); + GracefulStop(_cancellationTokenSource, _stoppedEvent); + }; + + // SIGTERM + AssemblyLoadContext.Default.Unloading += context => + { + Log.Error("[GRACEFUL_SHUTDOWN] SIGTERM received", new Exception("[GRACEFUL_SHUTDOWN] SIGTERM received")); + GracefulStop(_cancellationTokenSource, _stoppedEvent); + }; + + AppDomain.CurrentDomain.UnhandledException += (sender, args) => + { + Log.Error("UnhandledException", args.ExceptionObject as Exception); + GracefulStop(_cancellationTokenSource, _stoppedEvent); + }; + } + + public CancellationToken CancellationToken => _cancellationTokenSource.Token; + + public void Dispose() + { + _stoppedEvent.Set(); + } + + private static void GracefulStop(CancellationTokenSource cancellationTokenSource, ManualResetEventSlim stoppedEvent) + { + Log.Info("DockerGracefulStopService Stopping service"); + cancellationTokenSource.Cancel(); + stoppedEvent.Wait(); + Log.Info("DockerGracefulStopService Stop finished"); + } + } +} \ No newline at end of file diff --git a/srcs/DatabaseServer/EnvironmentConsts.cs b/srcs/DatabaseServer/EnvironmentConsts.cs new file mode 100644 index 0000000..848fe3e --- /dev/null +++ b/srcs/DatabaseServer/EnvironmentConsts.cs @@ -0,0 +1,10 @@ +namespace DatabaseServer +{ + public static class EnvironmentConsts + { + public const string DbServerSaveIntervalMinutes = "DB_SERVER_SAVE_INTERVAL_MINUTES"; + public const string DbServerCharTtlMinutes = "DB_SERVER_CHAR_TTL_MINUTES"; + public const string DbServerCharSaveIntervalSeconds = "DB_SERVER_CHAR_SAVE_INTERVAL_SECONDS"; + public const string TsServerSaveIntervalMinutes = "TS_SERVER_SAVE_INTERVAL_MINUTES"; + } +} \ No newline at end of file diff --git a/srcs/DatabaseServer/Managers/AccountWarehouseManager.cs b/srcs/DatabaseServer/Managers/AccountWarehouseManager.cs new file mode 100644 index 0000000..41a8ec2 --- /dev/null +++ b/srcs/DatabaseServer/Managers/AccountWarehouseManager.cs @@ -0,0 +1,440 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Mapster; +using Microsoft.Extensions.Hosting; +using PhoenixLib.Caching; +using PhoenixLib.Logging; +using WingsAPI.Data.Account; +using WingsAPI.Data.Warehouse; +using WingsEmu.Core.Extensions; +using WingsEmu.DTOs.Items; + +namespace DatabaseServer.Managers +{ + public class AccountWarehouseManager : BackgroundService, IAccountWarehouseManager + { + private static readonly TimeSpan Interval = TimeSpan.FromMinutes(Convert.ToUInt32(Environment.GetEnvironmentVariable(EnvironmentConsts.DbServerSaveIntervalMinutes) ?? "5")); + private static readonly TimeSpan LifeTime = Interval * 3; + + private readonly IAccountWarehouseItemDao _accountWarehouseItemDao; + + private readonly ILongKeyCachedRepository> _cachedWarehouseItems; + + private readonly Dictionary> _itemChanges = new(); + private readonly SemaphoreSlim _itemChangesSemaphore = new(1, 1); + private readonly ConcurrentDictionary _warehouseLocks = new(); + + public AccountWarehouseManager(IAccountWarehouseItemDao accountWarehouseItemDao, ILongKeyCachedRepository> cachedWarehouseItems) + { + _accountWarehouseItemDao = accountWarehouseItemDao; + _cachedWarehouseItems = cachedWarehouseItems; + } + + public async Task> GetWarehouse(long accountId) + { + SemaphoreSlim accountLock = GetAccountLock(accountId); + await accountLock.WaitAsync(); + try + { + return (await GetAccountWarehouse(accountId))?.Values; + } + finally + { + accountLock.Release(); + } + } + + public async Task GetWarehouseItem(long accountId, short slot) + { + SemaphoreSlim accountLock = GetAccountLock(accountId); + await accountLock.WaitAsync(); + try + { + return (await GetAccountWarehouse(accountId))?.GetValueOrDefault(slot); + } + finally + { + accountLock.Release(); + } + } + + public async Task AddWarehouseItem(AccountWarehouseItemDto warehouseItemDtoToAdd) + { + long accountId = warehouseItemDtoToAdd.AccountId; + + if (warehouseItemDtoToAdd.ItemInstance.Amount < 1 || 999 < warehouseItemDtoToAdd.ItemInstance.Amount) + { + return new AddWarehouseItemResult + { + Success = false + }; + } + + SemaphoreSlim accountLock = GetAccountLock(accountId); + await accountLock.WaitAsync(); + try + { + Dictionary familyWarehouse = await GetAccountWarehouse(accountId); + if (familyWarehouse == null) + { + return new AddWarehouseItemResult + { + Success = false + }; + } + + AccountWarehouseItemDto alreadyExistentItem = familyWarehouse.GetValueOrDefault(warehouseItemDtoToAdd.Slot); + + if (alreadyExistentItem == null) + { + familyWarehouse[warehouseItemDtoToAdd.Slot] = warehouseItemDtoToAdd; + await SetItemChangeWithLock(warehouseItemDtoToAdd, false); + return new AddWarehouseItemResult + { + Success = true, + UpdatedItem = warehouseItemDtoToAdd + }; + } + + if (warehouseItemDtoToAdd.ItemInstance.ItemVNum != alreadyExistentItem.ItemInstance.ItemVNum) + { + return new AddWarehouseItemResult + { + Success = false + }; + } + + if (warehouseItemDtoToAdd.ItemInstance.Type != ItemInstanceType.NORMAL_ITEM || alreadyExistentItem.ItemInstance.Type != ItemInstanceType.NORMAL_ITEM + || warehouseItemDtoToAdd.ItemInstance.Amount + alreadyExistentItem.ItemInstance.Amount > 999) + { + return new AddWarehouseItemResult + { + Success = false + }; + } + + alreadyExistentItem.ItemInstance.Amount += warehouseItemDtoToAdd.ItemInstance.Amount; + + await SetItemChangeWithLock(alreadyExistentItem, false); + return new AddWarehouseItemResult + { + Success = true, + UpdatedItem = alreadyExistentItem + }; + } + finally + { + accountLock.Release(); + } + } + + public async Task WithdrawWarehouseItem(AccountWarehouseItemDto warehouseItemDtoToWithdraw, int amount) + { + long accountId = warehouseItemDtoToWithdraw.AccountId; + + if (amount < 1 || 999 < amount) + { + return new WithdrawWarehouseItemResult + { + Success = false + }; + } + + SemaphoreSlim accountLock = GetAccountLock(accountId); + await accountLock.WaitAsync(); + try + { + Dictionary familyWarehouse = await GetAccountWarehouse(accountId); + + if (familyWarehouse == null) + { + return new WithdrawWarehouseItemResult + { + Success = false + }; + } + + AccountWarehouseItemDto alreadyExistentItem = familyWarehouse.GetValueOrDefault(warehouseItemDtoToWithdraw.Slot); + if (alreadyExistentItem == null || alreadyExistentItem.ItemInstance.Amount < amount) + { + return new WithdrawWarehouseItemResult + { + Success = false + }; + } + + alreadyExistentItem.ItemInstance.Amount -= amount; + + bool toRemove = alreadyExistentItem.ItemInstance.Amount == 0; + if (toRemove) + { + familyWarehouse.Remove(warehouseItemDtoToWithdraw.Slot); + } + + await SetItemChangeWithLock(alreadyExistentItem, toRemove); + + ItemInstanceDTO newItemInstance = alreadyExistentItem.ItemInstance.Adapt(); + newItemInstance.Amount = amount; + + return new WithdrawWarehouseItemResult + { + Success = true, + UpdatedItem = alreadyExistentItem.ItemInstance.Amount == 0 ? null : alreadyExistentItem, + WithdrawnItem = newItemInstance + }; + } + finally + { + accountLock.Release(); + } + } + + public async Task MoveWarehouseItem(AccountWarehouseItemDto warehouseItemDtoToMove, int amount, short newSlot) + { + long accountId = warehouseItemDtoToMove.AccountId; + + if (amount < 1 || 999 < amount) + { + return new MoveWarehouseItemResult + { + Success = false + }; + } + + SemaphoreSlim accountLock = GetAccountLock(accountId); + await accountLock.WaitAsync(); + try + { + Dictionary familyWarehouse = await GetAccountWarehouse(accountId); + if (familyWarehouse == null) + { + return new MoveWarehouseItemResult + { + Success = false + }; + } + + AccountWarehouseItemDto toMoveItem = familyWarehouse.GetValueOrDefault(warehouseItemDtoToMove.Slot); + AccountWarehouseItemDto toMergeItem = familyWarehouse.GetValueOrDefault(newSlot); + if (toMoveItem == null || toMoveItem.ItemInstance.Amount < amount) + { + return new MoveWarehouseItemResult + { + Success = false + }; + } + + if (toMergeItem == null) + { + if (amount == toMoveItem.ItemInstance.Amount) + { + familyWarehouse.Remove(toMoveItem.Slot); + await SetItemChangeWithLock(new AccountWarehouseItemDto + { + AccountId = accountId, + Slot = toMoveItem.Slot + }, true); + toMoveItem.Slot = newSlot; + familyWarehouse[toMoveItem.Slot] = toMoveItem; + await SetItemChangeWithLock(toMoveItem, false); + return new MoveWarehouseItemResult + { + Success = true, + OldItem = null, + NewItem = toMoveItem + }; + } + + toMoveItem.ItemInstance.Amount -= amount; + await SetItemChangeWithLock(toMoveItem, false); + var newItem = new AccountWarehouseItemDto + { + AccountId = accountId, + ItemInstance = toMoveItem.ItemInstance.Adapt(), + Slot = newSlot + }; + newItem.ItemInstance.Amount = amount; + familyWarehouse[newItem.Slot] = newItem; + await SetItemChangeWithLock(newItem, false); + return new MoveWarehouseItemResult + { + Success = true, + OldItem = toMoveItem, + NewItem = newItem + }; + } + + if (toMoveItem.ItemInstance.ItemVNum != toMergeItem.ItemInstance.ItemVNum) + { + toMergeItem.Slot = toMoveItem.Slot; + toMoveItem.Slot = newSlot; + familyWarehouse[toMoveItem.Slot] = toMoveItem; + await SetItemChangeWithLock(toMoveItem, false); + familyWarehouse[toMergeItem.Slot] = toMergeItem; + await SetItemChangeWithLock(toMergeItem, false); + return new MoveWarehouseItemResult + { + Success = true, + OldItem = toMergeItem, + NewItem = toMoveItem + }; + } + + if (toMoveItem.ItemInstance.Type != ItemInstanceType.NORMAL_ITEM || toMergeItem.ItemInstance.Type != ItemInstanceType.NORMAL_ITEM || amount + toMergeItem.ItemInstance.Amount > 999) + { + return new MoveWarehouseItemResult + { + Success = false + }; + } + + toMoveItem.ItemInstance.Amount -= amount; + toMergeItem.ItemInstance.Amount += amount; + + bool toRemove = toMoveItem.ItemInstance.Amount == 0; + if (toRemove) + { + familyWarehouse.Remove(toMoveItem.Slot); + } + + await SetItemChangeWithLock(toMoveItem, toRemove); + await SetItemChangeWithLock(toMergeItem, false); + + return new MoveWarehouseItemResult + { + Success = true, + OldItem = toRemove ? null : toMoveItem, + NewItem = toMergeItem + }; + } + finally + { + accountLock.Release(); + } + } + + public async Task FlushWarehouseSaves() + { + if (_itemChanges.Count < 1) + { + return; + } + + await _itemChangesSemaphore.WaitAsync(); + try + { + List<(AccountWarehouseItemDto dto, bool remove)> unsavedChanges = new(); + + var globalWatch = Stopwatch.StartNew(); + foreach ((long accountId, Dictionary warehouseChanges) in _itemChanges) + { + List itemsToSave = new(); + List itemsToRemove = new(); + + foreach ((short _, (AccountWarehouseItemDto dto, bool remove)) in warehouseChanges) + { + (remove ? itemsToRemove : itemsToSave).Add(dto); + } + + if (itemsToSave.Count > 0) + { + try + { + int countSavedItems = await _accountWarehouseItemDao.SaveAsync(itemsToSave); + Log.Warn($"[ACCOUNT_WAREHOUSE_MANAGER][FLUSH_SAVES][ACCOUNT_ID: {accountId.ToString()}] Saved {countSavedItems.ToString()} warehouseItems"); + } + catch (Exception e) + { + Log.Error( + $"[ACCOUNT_WAREHOUSE_MANAGER][FLUSH_SAVES][ACCOUNT_ID: {accountId.ToString()}] Error while trying to save {itemsToSave.Count.ToString()} warehouseItems. Re-queueing. ", + e); + unsavedChanges.AddRange(itemsToSave.Select(x => (x, false))); + } + } + + if (itemsToRemove.Count < 1) + { + continue; + } + + try + { + await _accountWarehouseItemDao.DeleteAsync(itemsToRemove); + Log.Warn($"[ACCOUNT_WAREHOUSE_MANAGER][FLUSH_SAVES][ACCOUNT_ID: {accountId.ToString()}] Removed (at maximum) {itemsToRemove.Count.ToString()} warehouseItems"); + } + catch (Exception e) + { + Log.Error( + $"[ACCOUNT_WAREHOUSE_MANAGER][FLUSH_SAVES][ACCOUNT_ID: {accountId.ToString()}] Error while trying to remove {itemsToRemove.Count.ToString()} warehouseItems. Re-queueing. ", + e); + unsavedChanges.AddRange(itemsToRemove.Select(x => (x, true))); + } + } + + globalWatch.Stop(); + Log.Debug($"[ACCOUNT_WAREHOUSE_MANAGER][FLUSH_SAVES] Saving all warehouses took {globalWatch.ElapsedMilliseconds.ToString()}ms"); + + _itemChanges.Clear(); + + foreach ((AccountWarehouseItemDto dto, bool remove) in unsavedChanges) + { + SetItemChange(dto, remove); + } + } + finally + { + _itemChangesSemaphore.Release(); + } + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + await FlushWarehouseSaves(); + await Task.Delay(Interval, stoppingToken); + } + } + + private SemaphoreSlim GetAccountLock(long accountId) => _warehouseLocks.GetOrAdd(accountId, new SemaphoreSlim(1, 1)); + + private async Task SetItemChangeWithLock(AccountWarehouseItemDto dto, bool remove) + { + await _itemChangesSemaphore.WaitAsync(); + try + { + SetItemChange(dto, remove); + } + finally + { + _itemChangesSemaphore.Release(); + } + } + + /// + /// Not to be used outside SemaphoreSlim + /// + private void SetItemChange(AccountWarehouseItemDto dto, bool remove) + { + _itemChanges.GetOrSetDefault(dto.AccountId, new Dictionary())[dto.Slot] = (dto, remove); + } + + private async Task> GetAccountWarehouse(long accountId) + { + Dictionary cachedItems = _cachedWarehouseItems.Get(accountId); + if (cachedItems != null) + { + return cachedItems; + } + + cachedItems = (await _accountWarehouseItemDao.GetByAccountIdAsync(accountId))?.ToDictionary(x => x.Slot); + _cachedWarehouseItems.Set(accountId, cachedItems ?? new Dictionary(), LifeTime); + return cachedItems; + } + } +} \ No newline at end of file diff --git a/srcs/DatabaseServer/Managers/AddWarehouseItemResult.cs b/srcs/DatabaseServer/Managers/AddWarehouseItemResult.cs new file mode 100644 index 0000000..95beb18 --- /dev/null +++ b/srcs/DatabaseServer/Managers/AddWarehouseItemResult.cs @@ -0,0 +1,11 @@ +using WingsAPI.Data.Account; + +namespace DatabaseServer.Managers +{ + public class AddWarehouseItemResult + { + public bool Success { get; init; } + + public AccountWarehouseItemDto UpdatedItem { get; init; } + } +} \ No newline at end of file diff --git a/srcs/DatabaseServer/Managers/CharacterManager.cs b/srcs/DatabaseServer/Managers/CharacterManager.cs new file mode 100644 index 0000000..721fc0c --- /dev/null +++ b/srcs/DatabaseServer/Managers/CharacterManager.cs @@ -0,0 +1,317 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using PhoenixLib.Caching; +using PhoenixLib.Logging; +using WingsAPI.Data.Character; +using WingsEmu.DTOs.Enums; + +namespace DatabaseServer.Managers +{ + public class CharacterManager : BackgroundService, ICharacterManager + { + private static readonly TimeSpan Interval = TimeSpan.FromSeconds(Convert.ToUInt32(Environment.GetEnvironmentVariable(EnvironmentConsts.DbServerCharSaveIntervalSeconds) ?? "60")); + private static readonly TimeSpan LifeTime = TimeSpan.FromMinutes(Convert.ToUInt32(Environment.GetEnvironmentVariable(EnvironmentConsts.DbServerCharTtlMinutes) ?? "30")); + + private readonly ILongKeyCachedRepository _characterById; + + private readonly ICharacterDAO _characterDao; + private readonly IKeyValueCache _characterIdByKey; + + private readonly HashSet _characterIdsToSave = new(); + private readonly SemaphoreSlim _createCharacterSemaphore = new(1, 1); + private readonly SemaphoreSlim _semaphoreSlim = new(1, 1); + + public CharacterManager(ICharacterDAO characterDao, ILongKeyCachedRepository characterById, IKeyValueCache characterIdByKey) + { + _characterDao = characterDao; + _characterById = characterById; + _characterIdByKey = characterIdByKey; + } + + public async Task> GetCharactersByAccountId(long accountId) => await _characterDao.LoadByAccountAsync(accountId); + + public async Task GetCharacterBySlot(long accountId, byte slot) + { + long characterId = _characterIdByKey.Get(GetKeyAccountIdSlot(accountId, slot)); + + // check if the cache does have the value + if (characterId != default) + { + Log.Debug($"[CHARACTER_SAVE_SYSTEM][GetCharacterBySlot] CharacterId fetched from cache via AccountId and Slot. AccountId: '{accountId.ToString()}' Slot: '{slot.ToString()}'"); + return await GetCharacterById(characterId); + } + + CharacterDTO characterDto = await _characterDao.LoadBySlotAsync(accountId, slot); + if (characterDto == null) + { + // shouldn't happen normally + Log.Warn( + $"[CHARACTER_SAVE_SYSTEM][GetCharacterBySlot] The given AccountId and Slot does not pertain to any CharacterDTO in the db. AccountId: '{accountId.ToString()}' Slot: '{slot.ToString()}'"); + return null; + } + + SetCharacter(characterDto); + Log.Debug($"[CHARACTER_SAVE_SYSTEM][GetCharacterBySlot] fetched by AccountId and Slot. AccountId: '{accountId.ToString()}' Slot: '{slot.ToString()}'"); + return characterDto; + } + + public async Task GetCharacterById(long characterId) + { + CharacterDTO dto = _characterById.Get(characterId); + if (dto != null) + { + Log.Debug($"[CHARACTER_SAVE_SYSTEM][GetCharacter] CharacterDTO fetched from cache via CharacterId: '{characterId.ToString()}'"); + return dto; + } + + dto = await _characterDao.GetByIdAsync(characterId); + + if (dto == null) + { + Log.Warn($"[CHARACTER_SAVE_SYSTEM][GetCharacter] The given CharacterId does not pertain to any CharacterDTO in the db. CharacterId: '{characterId.ToString()}'"); + return null; + } + + SetCharacter(dto); + Log.Debug($"[CHARACTER_SAVE_SYSTEM][GetCharacter] {characterId.ToString()} fetched from DB cause was not existing in cache"); + return dto; + } + + public async Task GetCharacterByName(string name) + { + long characterId = _characterIdByKey.Get(GetKey(name)); + + // check if the cache does have the value + if (characterId != default) + { + Log.Debug($"[CHARACTER_SAVE_SYSTEM][GetCharacter] CharacterId fetched from cache through CharacterName. CharacterId: '{characterId.ToString()}' CharacterName: '{name}'"); + return await GetCharacterById(characterId); + } + + CharacterDTO characterDto = await _characterDao.LoadByNameAsync(name); + if (characterDto == null) + { + // shouldn't happen normally + Log.Warn($"[CHARACTER_SAVE_SYSTEM][GetCharacter] The given CharacterName does not exist in the db. CharacterName: '{name}'"); + return null; + } + + SetCharacter(characterDto); + Log.Debug( + $"[CHARACTER_SAVE_SYSTEM][GetCharacter] CharacterDTO fetched from DB through CharacterName cause it was not existing in cache. CharacterId: '{characterId.ToString()}' CharacterName: '{name}'"); + return characterDto; + } + + public async Task CreateCharacter(CharacterDTO characterDto, bool ignoreSlotCheck) + { + await _createCharacterSemaphore.WaitAsync(); + try + { + if (await _characterDao.LoadByNameAsync(characterDto.Name) != null) + { + return null; + } + + CharacterDTO character; + if (ignoreSlotCheck == false) + { + character = await GetCharacterBySlot(characterDto.AccountId, characterDto.Slot); + if (character != null) + { + Log.Warn("[CHARACTER_SAVE_SYSTEM][CreateCharacter] Found a character already in the desired slot." + + $"AccountId: '{character.AccountId.ToString()}' CharacterId: '{character.Id.ToString()}' Slot: '{character.Slot.ToString()}'"); + return null; + } + } + + character = await _characterDao.SaveAsync(characterDto); + SetCharacter(character); + Log.Debug( + $"[CHARACTER_SAVE_SYSTEM][CreateCharacter] Created a new character. AccountId: '{character.AccountId.ToString()}' CharacterId: '{character.Id.ToString()}' Slot: '{character.Slot.ToString()}'"); + return character; + } + finally + { + _createCharacterSemaphore.Release(); + } + } + + public async Task AddCharacterToSavingQueue(CharacterDTO characterDto) + { + SetCharacter(characterDto); + + await _semaphoreSlim.WaitAsync(); + try + { + _characterIdsToSave.Add(characterDto.Id); + } + finally + { + _semaphoreSlim.Release(); + } + + Log.Debug($"[CHARACTER_SAVE_SYSTEM][AddCharacterToSavingQueue] Flagged CharacterId for saving. CharacterId: '{characterDto.Id.ToString()}'"); + } + + public async Task AddCharactersToSavingQueue(IEnumerable characterDtos) + { + int i = 0; + + await _semaphoreSlim.WaitAsync(); + try + { + foreach (CharacterDTO characterDto in characterDtos) + { + SetCharacter(characterDto); + _characterIdsToSave.Add(characterDto.Id); + i++; + } + } + finally + { + _semaphoreSlim.Release(); + } + + Log.Debug($"[CHARACTER_SAVE_SYSTEM][AddCharactersToSavingQueue] Flagged {i.ToString()} characterIds for saving."); + } + + public async Task DeleteCharacter(CharacterDTO characterDto) + { + DeleteResult result = await _characterDao.DeleteByPrimaryKey(characterDto.AccountId, characterDto.Slot); + if (result != DeleteResult.Deleted) + { + Log.Warn( + $"[CHARACTER_SAVE_SYSTEM][DeleteCharacter] Tried to delete a character that doesn't exist. AccountId: '{characterDto.AccountId.ToString()}' Slot: '{characterDto.Slot.ToString()}"); + return false; + } + + RemoveCharacter(characterDto); + Log.Debug( + $"[CHARACTER_SAVE_SYSTEM][DeleteCharacter] Deleted a character. AccountId: '{characterDto.AccountId.ToString()}' CharacterId: '{characterDto.Id.ToString()}' Slot: '{characterDto.Slot.ToString()}'"); + return true; + } + + public async Task FlushCharacterSaves() + { + if (_characterIdsToSave.Count < 1) + { + return 0; + } + + List unsavedCharacterIds = new(); + long[] characterIds; + + await _semaphoreSlim.WaitAsync(); + try + { + characterIds = new long[_characterIdsToSave.Count]; + _characterIdsToSave.CopyTo(characterIds); + _characterIdsToSave.Clear(); + } + finally + { + _semaphoreSlim.Release(); + } + + int count = 0; + var tmp = Stopwatch.StartNew(); + var individualWatch = new Stopwatch(); + foreach (long characterId in characterIds) + { + CharacterDTO toSave = _characterById.Get(characterId); + if (toSave == null) + { + Log.Error($"[CHARACTER_SAVE_SYSTEM] {characterId.ToString()} could not be retrieved from cache", new DataException($"Desynchronised data for character {characterId.ToString()}")); + continue; + } + + individualWatch.Restart(); + try + { + CharacterDTO savedCharacter = await _characterDao.SaveAsync(toSave); + count++; + Log.Warn($"[CHARACTER_SAVE_SYSTEM] Saved a character successfully in {individualWatch.ElapsedMilliseconds.ToString()}ms. " + + $"CharacterName: '{savedCharacter.Name}' CharacterId: '{savedCharacter.Id.ToString()}' AccountId: '{savedCharacter.AccountId.ToString()}'"); + } + catch (Exception e) + { + unsavedCharacterIds.Add(characterId); + Log.Error($"[CHARACTER_SAVE_SYSTEM] Failed to save a character in {individualWatch.ElapsedMilliseconds.ToString()}ms. Re-queueing the save. " + + $"CharacterName: '{toSave.Name}' CharacterId: '{toSave.Id.ToString()}' AccountId: '{toSave.AccountId.ToString()}'", e); + } + + individualWatch.Stop(); + } + + tmp.Stop(); + Log.Debug($"[CHARACTER_SAVE_SYSTEM] Saving of saves took in total {tmp.ElapsedMilliseconds.ToString()}ms"); + + if (unsavedCharacterIds.Count <= 0) + { + return count; + } + + await _semaphoreSlim.WaitAsync(); + try + { + foreach (long characterId in unsavedCharacterIds) + { + _characterIdsToSave.Add(characterId); + } + } + finally + { + _semaphoreSlim.Release(); + } + + Log.Warn($"[CHARACTER_SAVE_SYSTEM] Re-queued {unsavedCharacterIds.Count.ToString()} character saves"); + return count; + } + + public async Task RemoveCachedCharacter(string requestCharacterName) + { + CharacterDTO characterDto = await GetCharacterByName(requestCharacterName); + if (characterDto == null) + { + return null; + } + + await _characterDao.SaveAsync(characterDto); + _characterById.Remove(characterDto.Id); + return characterDto; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + await FlushCharacterSaves(); + await Task.Delay(Interval, stoppingToken); + } + } + + private static string GetKey(string name) => $"char:name:{name}"; + + private static string GetKeyAccountIdSlot(long accountId, byte slot) => $"char:account-id:{accountId.ToString()}:slot:{slot.ToString()}"; + + private void SetCharacter(CharacterDTO characterDto) + { + _characterById.Set(characterDto.Id, characterDto, LifeTime); + _characterIdByKey.Set(GetKey(characterDto.Name), characterDto.Id, LifeTime); + _characterIdByKey.Set(GetKeyAccountIdSlot(characterDto.AccountId, characterDto.Slot), characterDto.Id, LifeTime); + } + + private void RemoveCharacter(CharacterDTO characterDto) + { + _characterById.Remove(characterDto.Id); + _characterIdByKey.Remove(GetKey(characterDto.Name)); + _characterIdByKey.Remove(GetKeyAccountIdSlot(characterDto.AccountId, characterDto.Slot)); + _characterIdsToSave.Remove(characterDto.Id); + } + } +} \ No newline at end of file diff --git a/srcs/DatabaseServer/Managers/IAccountWarehouseManager.cs b/srcs/DatabaseServer/Managers/IAccountWarehouseManager.cs new file mode 100644 index 0000000..cbb09dd --- /dev/null +++ b/srcs/DatabaseServer/Managers/IAccountWarehouseManager.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using WingsAPI.Data.Account; + +namespace DatabaseServer.Managers +{ + public interface IAccountWarehouseManager + { + public Task> GetWarehouse(long accountId); + public Task GetWarehouseItem(long accountId, short slot); + public Task AddWarehouseItem(AccountWarehouseItemDto warehouseItemDtoToAdd); + public Task WithdrawWarehouseItem(AccountWarehouseItemDto warehouseItemDtoToWithdraw, int amount); + public Task MoveWarehouseItem(AccountWarehouseItemDto warehouseItemDtoToMove, int amount, short newSlot); + public Task FlushWarehouseSaves(); + } +} \ No newline at end of file diff --git a/srcs/DatabaseServer/Managers/ICharacterManager.cs b/srcs/DatabaseServer/Managers/ICharacterManager.cs new file mode 100644 index 0000000..396a67c --- /dev/null +++ b/srcs/DatabaseServer/Managers/ICharacterManager.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using WingsAPI.Data.Character; + +namespace DatabaseServer.Managers +{ + public interface ICharacterManager + { + public Task> GetCharactersByAccountId(long accountId); + public Task GetCharacterBySlot(long accountId, byte slot); + public Task GetCharacterById(long characterId); + public Task GetCharacterByName(string name); + public Task CreateCharacter(CharacterDTO characterDto, bool ignoreSlotCheck); + public Task AddCharacterToSavingQueue(CharacterDTO characterDto); + public Task AddCharactersToSavingQueue(IEnumerable characterDtos); + public Task DeleteCharacter(CharacterDTO characterDto); + public Task FlushCharacterSaves(); + public Task RemoveCachedCharacter(string requestCharacterName); + } +} \ No newline at end of file diff --git a/srcs/DatabaseServer/Managers/IRankingManager.cs b/srcs/DatabaseServer/Managers/IRankingManager.cs new file mode 100644 index 0000000..88a4a9d --- /dev/null +++ b/srcs/DatabaseServer/Managers/IRankingManager.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using WingsAPI.Data.Character; + +namespace DatabaseServer.Managers +{ + public interface IRankingManager + { + /// + /// Retrieves a list of the top 30 characters in compliments + /// + /// + Task> GetTopCompliment(); + + /// + /// Retrieves a list of the top 30 characters in points + /// + /// + Task> GetTopPoints(); + + /// + /// Retrieves a list of the top 43 characters in reputation + /// + /// + Task> GetTopReputation(); + + /// + /// Tries to refresh the ranking, in case it fails it will return false + /// + /// + Task TryRefreshRanking(); + } + + public class RefreshResponse + { + public bool Success { get; init; } + + public IReadOnlyList TopCompliment { get; init; } + public IReadOnlyList TopPoints { get; init; } + public IReadOnlyList TopReputation { get; init; } + } +} \ No newline at end of file diff --git a/srcs/DatabaseServer/Managers/ITimeSpaceManager.cs b/srcs/DatabaseServer/Managers/ITimeSpaceManager.cs new file mode 100644 index 0000000..12c7bae --- /dev/null +++ b/srcs/DatabaseServer/Managers/ITimeSpaceManager.cs @@ -0,0 +1,14 @@ +using System.Threading.Tasks; +using WingsAPI.Data.TimeSpace; + +namespace DatabaseServer.Managers +{ + public interface ITimeSpaceManager + { + Task GetRecordByTimeSpaceId(long tsId); + Task FlushTimeSpaceRecords(); + Task Initialize(); + void TryAddNewRecord(TimeSpaceRecordDto record); + Task IsNewRecord(long tsId, long points); + } +} \ No newline at end of file diff --git a/srcs/DatabaseServer/Managers/MoveWarehouseItemResult.cs b/srcs/DatabaseServer/Managers/MoveWarehouseItemResult.cs new file mode 100644 index 0000000..1f39a55 --- /dev/null +++ b/srcs/DatabaseServer/Managers/MoveWarehouseItemResult.cs @@ -0,0 +1,13 @@ +using WingsAPI.Data.Account; + +namespace DatabaseServer.Managers +{ + public class MoveWarehouseItemResult + { + public bool Success { get; init; } + + public AccountWarehouseItemDto OldItem { get; init; } + + public AccountWarehouseItemDto NewItem { get; init; } + } +} \ No newline at end of file diff --git a/srcs/DatabaseServer/Managers/RankingManager.cs b/srcs/DatabaseServer/Managers/RankingManager.cs new file mode 100644 index 0000000..759333a --- /dev/null +++ b/srcs/DatabaseServer/Managers/RankingManager.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using PhoenixLib.Logging; +using WingsAPI.Data.Character; + +namespace DatabaseServer.Managers +{ + public class RankingManager : IRankingManager + { + private readonly ICharacterDAO _characterDao; + + private IReadOnlyList _topCompliment; + private IReadOnlyList _topPoints; + private IReadOnlyList _topReputation; + + public RankingManager(ICharacterDAO characterDao) => _characterDao = characterDao; + + public async Task> GetTopCompliment() + { + if (_topCompliment != null) + { + return _topCompliment; + } + + try + { + _topCompliment = await _characterDao.GetTopCompliment(); + } + catch (Exception e) + { + Log.Error("[RANKING_MANAGER][GET_TOP_COMPLIMENT] Unexpected error:", e); + } + + return _topCompliment; + } + + public async Task> GetTopPoints() + { + if (_topPoints != null) + { + return _topPoints; + } + + try + { + _topPoints = await _characterDao.GetTopPoints(); + } + catch (Exception e) + { + Log.Error("[RANKING_MANAGER][GET_TOP_POINTS] Unexpected error:", e); + } + + return _topPoints; + } + + public async Task> GetTopReputation() + { + if (_topReputation != null) + { + return _topReputation; + } + + try + { + _topReputation = await _characterDao.GetTopReputation(); + } + catch (Exception e) + { + Log.Error("[RANKING_MANAGER][GET_TOP_REPUTATION] Unexpected error:", e); + } + + return _topReputation; + } + + public async Task TryRefreshRanking() + { + try + { + _topCompliment = await _characterDao.GetTopCompliment(); + } + catch (Exception e) + { + Log.Error("[RANKING_MANAGER][TRY_REFRESH_RANKING] Unexpected error:", e); + return new RefreshResponse + { + Success = false + }; + } + + try + { + _topPoints = await _characterDao.GetTopPoints(); + } + catch (Exception e) + { + Log.Error("[RANKING_MANAGER][TRY_REFRESH_RANKING] Unexpected error:", e); + return new RefreshResponse + { + Success = false + }; + } + + try + { + _topReputation = await _characterDao.GetTopReputation(); + } + catch (Exception e) + { + Log.Error("[RANKING_MANAGER][TRY_REFRESH_RANKING] Unexpected error:", e); + return new RefreshResponse + { + Success = false + }; + } + + return new RefreshResponse + { + Success = true, + TopCompliment = _topCompliment, + TopPoints = _topPoints, + TopReputation = _topReputation + }; + } + } +} \ No newline at end of file diff --git a/srcs/DatabaseServer/Managers/SaveRequest.cs b/srcs/DatabaseServer/Managers/SaveRequest.cs new file mode 100644 index 0000000..62dadcc --- /dev/null +++ b/srcs/DatabaseServer/Managers/SaveRequest.cs @@ -0,0 +1,10 @@ +using System; + +namespace DatabaseServer.Managers +{ + internal record SaveRequest + { + public DateTime CreatedAt { get; init; } + public long CharacterId { get; init; } + } +} \ No newline at end of file diff --git a/srcs/DatabaseServer/Managers/TimeSpaceManager.cs b/srcs/DatabaseServer/Managers/TimeSpaceManager.cs new file mode 100644 index 0000000..88b5a7a --- /dev/null +++ b/srcs/DatabaseServer/Managers/TimeSpaceManager.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using PhoenixLib.Caching; +using PhoenixLib.Logging; +using WingsAPI.Data.TimeSpace; + +namespace DatabaseServer.Managers +{ + public class TimeSpaceManager : BackgroundService, ITimeSpaceManager + { + private static readonly TimeSpan Interval = TimeSpan.FromMinutes(Convert.ToUInt32(Environment.GetEnvironmentVariable(EnvironmentConsts.TsServerSaveIntervalMinutes) ?? "5")); + private readonly ConcurrentQueue _queue = new(); + private readonly ILongKeyCachedRepository _records; + + private readonly ITimeSpaceRecordDao _timeSpaceRecordDao; + + public TimeSpaceManager(ITimeSpaceRecordDao timeSpaceRecordDao, ILongKeyCachedRepository records) + { + _timeSpaceRecordDao = timeSpaceRecordDao; + _records = records; + } + + public async Task GetRecordByTimeSpaceId(long tsId) + { + TimeSpaceRecordDto cached = _records.Get(tsId); + if (cached != null) + { + return cached; + } + + TimeSpaceRecordDto record = await _timeSpaceRecordDao.GetRecordById(tsId); + if (record == null) + { + return null; + } + + _records.Set(tsId, record); + return record; + } + + public async Task FlushTimeSpaceRecords() + { + if (_queue.IsEmpty) + { + return; + } + + try + { + while (_queue.TryDequeue(out TimeSpaceRecordDto record)) + { + TimeSpaceRecordDto currentRecord = await GetRecordByTimeSpaceId(record.TimeSpaceId); + if (currentRecord == null) + { + _records.Set(record.TimeSpaceId, record); + await _timeSpaceRecordDao.SaveRecord(record); + continue; + } + + if (currentRecord.Record >= record.Record) + { + continue; + } + + _records.Set(record.TimeSpaceId, record); + await _timeSpaceRecordDao.SaveRecord(record); + } + } + catch (Exception e) + { + Log.Error("FlushTimeSpaceRecords", e); + } + } + + public async Task Initialize() + { + IEnumerable records = await _timeSpaceRecordDao.GetAllRecords(); + int counter = 0; + foreach (TimeSpaceRecordDto recordDto in records) + { + counter++; + _records.Set(recordDto.TimeSpaceId, recordDto); + } + + Log.Info($"Initialized {counter} time-space records."); + } + + public void TryAddNewRecord(TimeSpaceRecordDto record) + { + _queue.Enqueue(record); + } + + public async Task IsNewRecord(long tsId, long points) + { + TimeSpaceRecordDto currentRecord = await GetRecordByTimeSpaceId(tsId); + if (currentRecord == null) + { + return true; + } + + return currentRecord.Record < points; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + await Initialize(); + Log.Info("[TIME_SPACE_MANAGER] Initialized!"); + while (!stoppingToken.IsCancellationRequested) + { + await FlushTimeSpaceRecords(); + await Task.Delay(Interval, stoppingToken); + } + } + } +} \ No newline at end of file diff --git a/srcs/DatabaseServer/Managers/WithdrawWarehouseItemResult.cs b/srcs/DatabaseServer/Managers/WithdrawWarehouseItemResult.cs new file mode 100644 index 0000000..3ad2ec9 --- /dev/null +++ b/srcs/DatabaseServer/Managers/WithdrawWarehouseItemResult.cs @@ -0,0 +1,14 @@ +using WingsAPI.Data.Account; +using WingsEmu.DTOs.Items; + +namespace DatabaseServer.Managers +{ + public class WithdrawWarehouseItemResult + { + public bool Success { get; init; } + + public AccountWarehouseItemDto UpdatedItem { get; init; } + + public ItemInstanceDTO WithdrawnItem { get; init; } + } +} \ No newline at end of file diff --git a/srcs/DatabaseServer/Program.cs b/srcs/DatabaseServer/Program.cs new file mode 100644 index 0000000..dc15e51 --- /dev/null +++ b/srcs/DatabaseServer/Program.cs @@ -0,0 +1,104 @@ +using System; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using DatabaseServer.Managers; +using FamilyServer; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Npgsql; +using PhoenixLib.Logging; +using PhoenixLib.ServiceBus.MQTT; +using Plugin.Database.DB; +using Plugin.Database.Extensions; +using Plugin.Database.Mapping; +using ProtoBuf.Grpc.Client; + +namespace DatabaseServer +{ + public class Program + { + public static async Task Main(string[] args) + { + PrintHeader(); + NonGameMappingRules.InitializeMapping(); + GrpcClientFactory.AllowUnencryptedHttp2 = true; + using var stopService = new DockerGracefulStopService(); + using IHost host = CreateHostBuilder(args).Build(); + IDbContextFactory dbContextFactory = host.Services.GetRequiredService>(); + + if (!await dbContextFactory.TryMigrateAsync()) + { + throw new PostgresException("Couldn't migrate the database", "ERROR", "ERROR", "None"); + } + + await host.StartAsync(); + IMessagingService messagingService = host.Services.GetService(); + if (messagingService != null) + { + await messagingService.StartAsync(); + } + + Log.Info("Database Server started"); + + ICharacterManager characterManager = host.Services.GetRequiredService(); + IAccountWarehouseManager accountWarehouseManager = host.Services.GetRequiredService(); + ITimeSpaceManager timespaceManager = host.Services.GetRequiredService(); + + await host.WaitForShutdownAsync(stopService.CancellationToken); + if (messagingService != null) + { + await messagingService.DisposeAsync(); + } + + await characterManager.FlushCharacterSaves(); + await accountWarehouseManager.FlushWarehouseSaves(); + await timespaceManager.FlushTimeSpaceRecords(); + } + + private static void PrintHeader() + { + const string text = @" +██╗ ██╗██╗███╗ ██╗ ██████╗ ███████╗███████╗███╗ ███╗██╗ ██╗ +██║ ██║██║████╗ ██║██╔════╝ ██╔════╝██╔════╝████╗ ████║██║ ██║ +██║ █╗ ██║██║██╔██╗ ██║██║ ███╗███████╗█████╗ ██╔████╔██║██║ ██║ +██║███╗██║██║██║╚██╗██║██║ ██║╚════██║██╔══╝ ██║╚██╔╝██║██║ ██║ +╚███╔███╔╝██║██║ ╚████║╚██████╔╝███████║███████╗██║ ╚═╝ ██║╚██████╔╝ + ╚══╝╚══╝ ╚═╝╚═╝ ╚═══╝ ╚═════╝ ╚══════╝╚══════╝╚═╝ ╚═╝ ╚═════╝ + +██████╗ ██████╗ ███████╗███████╗██████╗ ██╗ ██╗███████╗██████╗ +██╔══██╗██╔══██╗ ██╔════╝██╔════╝██╔══██╗██║ ██║██╔════╝██╔══██╗ +██║ ██║██████╔╝█████╗███████╗█████╗ ██████╔╝██║ ██║█████╗ ██████╔╝ +██║ ██║██╔══██╗╚════╝╚════██║██╔══╝ ██╔══██╗╚██╗ ██╔╝██╔══╝ ██╔══██╗ +██████╔╝██████╔╝ ███████║███████╗██║ ██║ ╚████╔╝ ███████╗██║ ██║ +╚═════╝ ╚═════╝ ╚══════╝╚══════╝╚═╝ ╚═╝ ╚═══╝ ╚══════╝╚═╝ ╚═╝ +"; + string separator = new('=', Console.WindowWidth); + string logo = text.Split('\n').Select(s => string.Format("{0," + (Console.WindowWidth / 2 + s.Length / 2) + "}\n", s)) + .Aggregate("", (current, i) => current + i); + + Console.ForegroundColor = ConsoleColor.Cyan; + Console.WriteLine(separator + logo + $"Version: {Assembly.GetExecutingAssembly().GetName().Version}\n" + separator); + Console.ForegroundColor = ConsoleColor.White; + } + + // Additional configuration is required to successfully run gRPC on macOS. + // For instructions on how to configure Kestrel and gRPC clients on macOS, visit https://go.microsoft.com/fwlink/?linkid=2099682 + private static IHostBuilder CreateHostBuilder(string[] args) + { + IHostBuilder host = Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.ConfigureKestrel(s => + { + s.ListenAnyIP(short.Parse(Environment.GetEnvironmentVariable("DATABASE_SERVER_PORT") ?? "29999"), options => { options.Protocols = HttpProtocols.Http2; }); + }); + webBuilder.UseStartup(); + }); + return host; + } + } +} \ No newline at end of file diff --git a/srcs/DatabaseServer/Services/AccountService.cs b/srcs/DatabaseServer/Services/AccountService.cs new file mode 100644 index 0000000..9af4be2 --- /dev/null +++ b/srcs/DatabaseServer/Services/AccountService.cs @@ -0,0 +1,173 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using PhoenixLib.Logging; +using WingsAPI.Communication; +using WingsAPI.Communication.DbServer.AccountService; +using WingsAPI.Data.Account; + +namespace DatabaseServer.Services +{ + public class AccountService : IAccountService + { + private readonly IAccountBanDao _accountBanDao; + private readonly IAccountDAO _accountDao; + private readonly IAccountPenaltyDao _accountPenaltyDao; + + public AccountService(IAccountDAO accountDao, IAccountBanDao accountBanDao, IAccountPenaltyDao accountPenaltyDao) + { + _accountDao = accountDao; + _accountBanDao = accountBanDao; + _accountPenaltyDao = accountPenaltyDao; + } + + public async Task LoadAccountByName(AccountLoadByNameRequest request) + { + AccountDTO dto = null; + try + { + dto = await _accountDao.GetByNameAsync(request.Name); + } + catch (Exception e) + { + Log.Error("[ACCOUNT_SERVICE][LOAD_ACCOUNT_BY_NAME] Unexpected error: ", e); + } + + return new AccountLoadResponse + { + ResponseType = dto == null ? RpcResponseType.GENERIC_SERVER_ERROR : RpcResponseType.SUCCESS, + AccountDto = dto + }; + } + + public async Task LoadAccountById(AccountLoadByIdRequest request) + { + AccountDTO dto = null; + try + { + dto = await _accountDao.GetByIdAsync(request.AccountId); + } + catch (Exception e) + { + Log.Error("[ACCOUNT_SERVICE][LOAD_ACCOUNT_BY_ID] Unexpected error: ", e); + } + + return new AccountLoadResponse + { + ResponseType = dto == null ? RpcResponseType.GENERIC_SERVER_ERROR : RpcResponseType.SUCCESS, + AccountDto = dto + }; + } + + public async Task SaveAccount(AccountSaveRequest request) + { + AccountDTO dto = null; + try + { + dto = await _accountDao.SaveAsync(request.AccountDto); + } + catch (Exception e) + { + Log.Error("[ACCOUNT_SERVICE][SAVE_ACCOUNT] Unexpected error: ", e); + } + + return new AccountSaveResponse + { + ResponseType = dto == null ? RpcResponseType.GENERIC_SERVER_ERROR : RpcResponseType.SUCCESS, + AccountDto = dto + }; + } + + public async Task GetAccountBan(AccountBanGetRequest request) + { + try + { + AccountBanDto dto = await _accountBanDao.FindAccountBan(request.AccountId); + + return new AccountBanGetResponse + { + ResponseType = RpcResponseType.SUCCESS, + AccountBanDto = dto + }; + } + catch (Exception e) + { + Log.Error("[ACCOUNT_SERVICE][GET_ACCOUNT_BAN] Unexpected error: ", e); + } + + return new AccountBanGetResponse + { + ResponseType = RpcResponseType.GENERIC_SERVER_ERROR + }; + } + + public async Task SaveAccountBan(AccountBanSaveRequest request) + { + AccountBanDto dto = null; + try + { + dto = await _accountBanDao.SaveAsync(request.AccountBanDto); + } + catch (Exception e) + { + Log.Error("[ACCOUNT_SERVICE][SAVE_ACCOUNT_BAN] Unexpected error: ", e); + } + + return new AccountBanSaveResponse + { + ResponseType = dto == null ? RpcResponseType.GENERIC_SERVER_ERROR : RpcResponseType.SUCCESS, + AccountBanDto = dto + }; + } + + public async Task GetAccountPenalties(AccountPenaltyGetRequest request) + { + try + { + List dtos = await _accountPenaltyDao.GetPenaltiesByAccountId(request.AccountId); + + return new AccountPenaltyGetAllResponse + { + ResponseType = RpcResponseType.SUCCESS, + AccountPenaltyDtos = dtos + }; + } + catch (Exception e) + { + Log.Error("[ACCOUNT_SERVICE][GET_ACCOUNT_PENALTIES] Unexpected error: ", e); + } + + return new AccountPenaltyGetAllResponse + { + ResponseType = RpcResponseType.GENERIC_SERVER_ERROR + }; + } + + public async Task SaveAccountPenalties(AccountPenaltyMultiSaveRequest request) + { + if (request.AccountPenaltyDtos == null) + { + return new AccountPenaltyMultiSaveResponse + { + ResponseType = RpcResponseType.SUCCESS + }; + } + + IEnumerable dtos = null; + try + { + dtos = await _accountPenaltyDao.SaveAsync(request.AccountPenaltyDtos); + } + catch (Exception e) + { + Log.Error("[ACCOUNT_SERVICE][SAVE_ACCOUNT_PENALTIES] Unexpected error: ", e); + } + + return new AccountPenaltyMultiSaveResponse + { + ResponseType = dtos == null ? RpcResponseType.GENERIC_SERVER_ERROR : RpcResponseType.SUCCESS, + AccountPenaltyDtos = dtos + }; + } + } +} \ No newline at end of file diff --git a/srcs/DatabaseServer/Services/AccountWarehouseService.cs b/srcs/DatabaseServer/Services/AccountWarehouseService.cs new file mode 100644 index 0000000..d040f87 --- /dev/null +++ b/srcs/DatabaseServer/Services/AccountWarehouseService.cs @@ -0,0 +1,73 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using DatabaseServer.Managers; +using WingsAPI.Communication; +using WingsAPI.Communication.DbServer.WarehouseService; +using WingsAPI.Data.Account; + +namespace DatabaseServer.Services +{ + public class AccountWarehouseService : IAccountWarehouseService + { + private readonly IAccountWarehouseManager _accountWarehouseManager; + + public AccountWarehouseService(IAccountWarehouseManager accountWarehouseManager) => _accountWarehouseManager = accountWarehouseManager; + + public async ValueTask GetItems(AccountWarehouseGetItemsRequest request) + { + IEnumerable warehouseItemDtos = await _accountWarehouseManager.GetWarehouse(request.AccountId); + + return new AccountWarehouseGetItemsResponse + { + ResponseType = warehouseItemDtos == null ? RpcResponseType.GENERIC_SERVER_ERROR : RpcResponseType.SUCCESS, + Items = warehouseItemDtos + }; + } + + public async ValueTask GetItem(AccountWarehouseGetItemRequest request) + { + AccountWarehouseItemDto warehouseItemDto = await _accountWarehouseManager.GetWarehouseItem(request.AccountId, request.Slot); + + return new AccountWarehouseGetItemResponse + { + ResponseType = warehouseItemDto == null ? RpcResponseType.GENERIC_SERVER_ERROR : RpcResponseType.SUCCESS, + Item = warehouseItemDto + }; + } + + public async ValueTask AddItem(AccountWarehouseAddItemRequest request) + { + AddWarehouseItemResult result = await _accountWarehouseManager.AddWarehouseItem(request.Item); + + return new AccountWarehouseAddItemResponse + { + ResponseType = result.Success ? RpcResponseType.SUCCESS : RpcResponseType.GENERIC_SERVER_ERROR, + Item = result.UpdatedItem + }; + } + + public async ValueTask WithdrawItem(AccountWarehouseWithdrawItemRequest request) + { + WithdrawWarehouseItemResult result = await _accountWarehouseManager.WithdrawWarehouseItem(request.ItemToWithdraw, request.Amount); + + return new AccountWarehouseWithdrawItemResponse + { + ResponseType = result.Success ? RpcResponseType.SUCCESS : RpcResponseType.GENERIC_SERVER_ERROR, + UpdatedItem = result.UpdatedItem, + WithdrawnItem = result.WithdrawnItem + }; + } + + public async ValueTask MoveItem(AccountWarehouseMoveItemRequest request) + { + MoveWarehouseItemResult result = await _accountWarehouseManager.MoveWarehouseItem(request.WarehouseItemDtoToMove, request.Amount, request.NewSlot); + + return new AccountWarehouseMoveItemResponse + { + ResponseType = result.Success ? RpcResponseType.SUCCESS : RpcResponseType.GENERIC_SERVER_ERROR, + OldItem = result.OldItem, + NewItem = result.NewItem + }; + } + } +} \ No newline at end of file diff --git a/srcs/DatabaseServer/Services/CharacterService.cs b/srcs/DatabaseServer/Services/CharacterService.cs new file mode 100644 index 0000000..f7b86f3 --- /dev/null +++ b/srcs/DatabaseServer/Services/CharacterService.cs @@ -0,0 +1,171 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using DatabaseServer.Managers; +using WingsAPI.Communication; +using WingsAPI.Communication.DbServer.CharacterService; +using WingsAPI.Data.Character; + +namespace DatabaseServer.Services +{ + public class CharacterService : ICharacterService + { + private readonly ICharacterManager _characterManager; + private readonly IRankingManager _rankingManager; + + public CharacterService(ICharacterManager characterManager, IRankingManager rankingManager) + { + _characterManager = characterManager; + _rankingManager = rankingManager; + } + + public async Task SaveCharacters(DbServerSaveCharactersRequest request) + { + await _characterManager.AddCharactersToSavingQueue(request.Characters); + return new DbServerSaveCharactersResponse + { + RpcResponseType = RpcResponseType.SUCCESS + }; + } + + public async Task SaveCharacter(DbServerSaveCharacterRequest request) + { + await _characterManager.AddCharacterToSavingQueue(request.Character); + return new DbServerSaveCharacterResponse + { + RpcResponseType = RpcResponseType.SUCCESS + }; + } + + public async Task CreateCharacter(DbServerSaveCharacterRequest request) + { + CharacterDTO character = await _characterManager.CreateCharacter(request.Character, request.IgnoreSlotCheck); + + + return new DbServerSaveCharacterResponse + { + RpcResponseType = character == null ? RpcResponseType.GENERIC_SERVER_ERROR : RpcResponseType.SUCCESS, + Character = character + }; + } + + public async Task GetCharacters(DbServerGetCharactersRequest request) + { + IEnumerable characters = await _characterManager.GetCharactersByAccountId(request.AccountId); + + return new DbServerGetCharactersResponse + { + RpcResponseType = characters == null ? RpcResponseType.GENERIC_SERVER_ERROR : RpcResponseType.SUCCESS, + Characters = characters + }; + } + + public async Task GetCharacterBySlot(DbServerGetCharacterFromSlotRequest fromSlotRequest) + { + CharacterDTO character = await _characterManager.GetCharacterBySlot(fromSlotRequest.AccountId, fromSlotRequest.Slot); + + return new DbServerGetCharacterResponse + { + RpcResponseType = character == null ? RpcResponseType.GENERIC_SERVER_ERROR : RpcResponseType.SUCCESS, + CharacterDto = character + }; + } + + public async Task GetCharacterById(DbServerGetCharacterByIdRequest request) + { + CharacterDTO character = await _characterManager.GetCharacterById(request.CharacterId); + + return new DbServerGetCharacterResponse + { + RpcResponseType = character == null ? RpcResponseType.GENERIC_SERVER_ERROR : RpcResponseType.SUCCESS, + CharacterDto = character + }; + } + + public async Task GetCharacterByName(DbServerGetCharacterRequestByName request) + { + CharacterDTO character = await _characterManager.GetCharacterByName(request.CharacterName); + + return new DbServerGetCharacterResponse + { + RpcResponseType = character == null ? RpcResponseType.GENERIC_SERVER_ERROR : RpcResponseType.SUCCESS, + CharacterDto = character + }; + } + + public async Task FlushCharacterSaves(DbServerFlushCharacterSavesRequest request) + { + await _characterManager.FlushCharacterSaves(); + return new DbServerFlushCharacterSavesResponse + { + RpcResponseType = RpcResponseType.SUCCESS + }; + } + + public async Task DeleteCharacter(DbServerDeleteCharacterRequest request) + { + bool success = await _characterManager.DeleteCharacter(request.CharacterDto); + + return new DbServerDeleteCharacterResponse + { + RpcResponseType = success ? RpcResponseType.SUCCESS : RpcResponseType.GENERIC_SERVER_ERROR + }; + } + + public async Task ForceRemoveCharacterFromCache(DbServerGetCharacterRequestByName request) + { + CharacterDTO character = await _characterManager.RemoveCachedCharacter(request.CharacterName); + + return new DbServerGetCharacterResponse + { + RpcResponseType = character == null ? RpcResponseType.GENERIC_SERVER_ERROR : RpcResponseType.SUCCESS, + CharacterDto = character + }; + } + + public async ValueTask GetTopCompliment(EmptyRpcRequest request) + { + IReadOnlyList top = await _rankingManager.GetTopCompliment(); + + return new CharacterGetTopResponse + { + ResponseType = top == null ? RpcResponseType.GENERIC_SERVER_ERROR : RpcResponseType.SUCCESS, + Top = top + }; + } + + public async ValueTask GetTopPoints(EmptyRpcRequest request) + { + IReadOnlyList top = await _rankingManager.GetTopPoints(); + + return new CharacterGetTopResponse + { + ResponseType = top == null ? RpcResponseType.GENERIC_SERVER_ERROR : RpcResponseType.SUCCESS, + Top = top + }; + } + + public async ValueTask GetTopReputation(EmptyRpcRequest request) + { + IReadOnlyList top = await _rankingManager.GetTopReputation(); + + return new CharacterGetTopResponse + { + ResponseType = top == null ? RpcResponseType.GENERIC_SERVER_ERROR : RpcResponseType.SUCCESS, + Top = top + }; + } + + public async ValueTask RefreshRanking(EmptyRpcRequest request) + { + RefreshResponse response = await _rankingManager.TryRefreshRanking(); + + return new CharacterRefreshRankingResponse + { + ResponseType = response.Success ? RpcResponseType.SUCCESS : RpcResponseType.GENERIC_SERVER_ERROR, + TopCompliment = response.TopCompliment, + TopPoints = response.TopPoints, + TopReputation = response.TopReputation + }; + } + } +} \ No newline at end of file diff --git a/srcs/DatabaseServer/Services/TimeSpaceService.cs b/srcs/DatabaseServer/Services/TimeSpaceService.cs new file mode 100644 index 0000000..1c5cb66 --- /dev/null +++ b/srcs/DatabaseServer/Services/TimeSpaceService.cs @@ -0,0 +1,31 @@ +using System.Threading.Tasks; +using DatabaseServer.Managers; +using WingsAPI.Communication; +using WingsAPI.Communication.DbServer.TimeSpaceService; + +namespace DatabaseServer.Services +{ + public class TimeSpaceService : ITimeSpaceService + { + private readonly ITimeSpaceManager _timeSpaceManager; + + public TimeSpaceService(ITimeSpaceManager timeSpaceManager) => _timeSpaceManager = timeSpaceManager; + + public async ValueTask IsNewRecord(TimeSpaceIsNewRecordRequest request) => new() + { + IsNewRecord = await _timeSpaceManager.IsNewRecord(request.TimeSpaceId, request.Record) + }; + + public async ValueTask SetNewRecord(TimeSpaceNewRecordRequest request) + { + _timeSpaceManager.TryAddNewRecord(request.TimeSpaceRecordDto); + return new EmptyResponse(); + } + + public async ValueTask GetTimeSpaceRecord(TimeSpaceRecordRequest request) => + new TimeSpaceRecordResponse + { + TimeSpaceRecordDto = await _timeSpaceManager.GetRecordByTimeSpaceId(request.TimeSpaceId) + }; + } +} \ No newline at end of file diff --git a/srcs/DatabaseServer/Startup.cs b/srcs/DatabaseServer/Startup.cs new file mode 100644 index 0000000..2c85dda --- /dev/null +++ b/srcs/DatabaseServer/Startup.cs @@ -0,0 +1,85 @@ +using DatabaseServer.Consumers; +using DatabaseServer.Managers; +using DatabaseServer.Services; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using PhoenixLib.Caching; +using PhoenixLib.Logging; +using PhoenixLib.ServiceBus.Extensions; +using Plugin.Database; +using Plugin.Database.Extensions; +using ProtoBuf.Grpc.Server; +using WingsAPI.Communication.Services.Messages; +using WingsEmu.Communication.gRPC.Extensions; +using WingsEmu.Health.Extensions; + +namespace DatabaseServer +{ + public class Startup + { + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) + { + services.AddMqttConfigurationFromEnv(); + new DatabasePlugin().AddDependencies(services); + services.AddMaintenanceMode(); + + services.AddSingleton(); + services.AddSingleton(); + services.AddHostedService(s => s.GetRequiredService()); + services.AddSingleton(s => s.GetRequiredService()); + + services.AddSingleton(); + services.AddSingleton(); + services.AddHostedService(s => s.GetRequiredService()); + services.AddSingleton(s => s.GetRequiredService()); + + services.AddSingleton(); + services.AddSingleton(); + services.AddHostedService(s => s.GetRequiredService()); + services.AddSingleton(s => s.GetRequiredService()); + + services.AddSingleton(); + + services.AddPhoenixLogging(); + services.AddSingleton(); + + services.TryAddSingleton(typeof(ILongKeyCachedRepository<>), typeof(InMemoryCacheRepository<>)); + services.TryAddSingleton(typeof(IKeyValueCache<>), typeof(InMemoryKeyValueCache<>)); + + services.AddGrpcDbServerServiceClient(); + services.AddCodeFirstGrpc(config => + { + config.MaxReceiveMessageSize = null; + config.MaxSendMessageSize = null; + config.EnableDetailedErrors = true; + }); + + services.AddMessageSubscriber(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseRouting(); + + app.UseZEfCoreExtensions(); + app.UseEndpoints(endpoints => + { + endpoints.MapGrpcService(); + endpoints.MapGrpcService(); + endpoints.MapGrpcService(); + endpoints.MapGrpcService(); + }); + } + } +} \ No newline at end of file diff --git a/srcs/DiscordNotifier/Consumers/Chat/LogChatMessageMessageFormatter.cs b/srcs/DiscordNotifier/Consumers/Chat/LogChatMessageMessageFormatter.cs new file mode 100644 index 0000000..a9e59d6 --- /dev/null +++ b/srcs/DiscordNotifier/Consumers/Chat/LogChatMessageMessageFormatter.cs @@ -0,0 +1,40 @@ +// WingsEmu +// +// Developed by NosWings Team + +using DiscordNotifier.Formatting; +using Plugin.PlayerLogs.Messages.Player; +using WingsEmu.Game._playerActionLogs; + +namespace DiscordNotifier.Consumers.Chat +{ + public class LogChatMessageMessageFormatter : IDiscordLogFormatter + { + public LogType LogType { get; private set; } + + public bool TryFormat(LogPlayerChatMessage message, out string formattedString) + { + LogType = message.ChatType switch + { + ChatType.General => LogType.CHAT_GENERAL, + ChatType.HeroChat => LogType.CHAT_GENERAL, + ChatType.SpeechBubble => LogType.CHAT_GENERAL, + + ChatType.Whisper => LogType.CHAT_WHISPERS, + ChatType.FriendChat => LogType.CHAT_FRIENDS, + + ChatType.Shout => LogType.CHAT_SPEAKERS, + + ChatType.FamilyChat => LogType.CHAT_FAMILIES, + ChatType.GroupChat => LogType.CHAT_GROUPS + }; + + formattedString = $"[ChannelId: '{message.ChannelId.ToString()}']" + + $"[ChatType: '{message.ChatType.ToString()}']" + + $"[CharacterId: '{message.CharacterId.ToString()}'{(message.TargetCharacterId.HasValue ? $" -> TargetId: '{message.TargetCharacterId.Value.ToString()}'" : string.Empty)}]" + + $" {message.Message}"; + + return true; + } + } +} \ No newline at end of file diff --git a/srcs/DiscordNotifier/Consumers/Family/LogFamilyCreatedEmbedMessageFormatter.cs b/srcs/DiscordNotifier/Consumers/Family/LogFamilyCreatedEmbedMessageFormatter.cs new file mode 100644 index 0000000..a4f0cdf --- /dev/null +++ b/srcs/DiscordNotifier/Consumers/Family/LogFamilyCreatedEmbedMessageFormatter.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using Discord; +using DiscordNotifier.Formatting; +using Plugin.PlayerLogs.Messages.Family; + +namespace DiscordNotifier.Consumers.Family +{ + public class LogFamilyCreatedEmbedMessageFormatter : IDiscordEmbedLogFormatter + { + public LogType LogType => LogType.PLAYERS_EVENTS_CHANNEL; + + public bool TryFormat(LogFamilyCreatedMessage message, out List embeds) + { + embeds = new List + { + new() + { + Author = new EmbedAuthorBuilder { IconUrl = "https://avatars0.githubusercontent.com/u/40839221?s=200" }, + Title = "[NosWings] Family created", + Description = $"Family {message.FamilyName} has been created", + Color = Color.Orange, + Footer = new EmbedFooterBuilder().WithIconUrl("https://avatars0.githubusercontent.com/u/40839221?s=200").WithText("discord-notifier microservice"), + //ImageUrl = "https://avatars0.githubusercontent.com/u/40839221?s=200", + // ThumbnailUrl = "https://avatars0.githubusercontent.com/u/40839221?s=200", + Timestamp = DateTimeOffset.Now + } + }; + return true; + } + } +} \ No newline at end of file diff --git a/srcs/DiscordNotifier/Consumers/Family/LogFamilyCreatedMessageFormatter.cs b/srcs/DiscordNotifier/Consumers/Family/LogFamilyCreatedMessageFormatter.cs new file mode 100644 index 0000000..7eb2a73 --- /dev/null +++ b/srcs/DiscordNotifier/Consumers/Family/LogFamilyCreatedMessageFormatter.cs @@ -0,0 +1,17 @@ +using DiscordNotifier.Formatting; +using Plugin.PlayerLogs.Messages.Family; + +namespace DiscordNotifier.Consumers.Family +{ + public class LogFamilyCreatedMessageFormatter : IDiscordLogFormatter + { + public LogType LogType => LogType.FAMILY_CREATED; + + public bool TryFormat(LogFamilyCreatedMessage message, out string formattedString) + { + formattedString = $"{message.CreatedAt:yyyy-MM-dd HH:mm:ss} | CHANNEL {message.ChannelId} | HEAD: {message.CharacterName} | DEPUTIES: {string.Join(", ", message.DeputiesIds)} | " + + $"FAM_ID: {message.FamilyId} | FAM_NAME: {message.FamilyName}"; + return true; + } + } +} \ No newline at end of file diff --git a/srcs/DiscordNotifier/Consumers/Family/LogFamilyDisbandedMessageFormatter.cs b/srcs/DiscordNotifier/Consumers/Family/LogFamilyDisbandedMessageFormatter.cs new file mode 100644 index 0000000..478f28d --- /dev/null +++ b/srcs/DiscordNotifier/Consumers/Family/LogFamilyDisbandedMessageFormatter.cs @@ -0,0 +1,17 @@ +using DiscordNotifier.Formatting; +using Plugin.PlayerLogs.Messages.Family; + +namespace DiscordNotifier.Consumers.Family +{ + public class LogFamilyDisbandedMessageFormatter : IDiscordLogFormatter + { + public LogType LogType => LogType.FAMILY_DISBANDED; + + public bool TryFormat(LogFamilyDisbandedMessage message, out string formattedString) + { + formattedString = $"{message.CreatedAt:yyyy-MM-dd HH:mm:ss} | CHANNEL {message.ChannelId} | " + + $"PLAYER: {message.CharacterName} | FAM_ID: {message.FamilyId}"; + return true; + } + } +} \ No newline at end of file diff --git a/srcs/DiscordNotifier/Consumers/Family/LogFamilyJoinedMessageFormatter.cs b/srcs/DiscordNotifier/Consumers/Family/LogFamilyJoinedMessageFormatter.cs new file mode 100644 index 0000000..7656504 --- /dev/null +++ b/srcs/DiscordNotifier/Consumers/Family/LogFamilyJoinedMessageFormatter.cs @@ -0,0 +1,17 @@ +using DiscordNotifier.Formatting; +using Plugin.PlayerLogs.Messages.Family; + +namespace DiscordNotifier.Consumers.Family +{ + public class LogFamilyJoinedMessageFormatter : IDiscordLogFormatter + { + public LogType LogType => LogType.FAMILY_JOINED; + + public bool TryFormat(LogFamilyJoinedMessage message, out string formattedString) + { + formattedString = $"{message.CreatedAt:yyyy-MM-dd HH:mm:ss} | CHANNEL {message.ChannelId} | " + + $"PLAYER: {message.CharacterName} | INVITER_ID: {message.InviterId} | FAM_ID: {message.FamilyId}"; + return true; + } + } +} \ No newline at end of file diff --git a/srcs/DiscordNotifier/Consumers/Family/LogFamilyKickedMessageFormatter.cs b/srcs/DiscordNotifier/Consumers/Family/LogFamilyKickedMessageFormatter.cs new file mode 100644 index 0000000..b8e372e --- /dev/null +++ b/srcs/DiscordNotifier/Consumers/Family/LogFamilyKickedMessageFormatter.cs @@ -0,0 +1,17 @@ +using DiscordNotifier.Formatting; +using Plugin.PlayerLogs.Messages.Family; + +namespace DiscordNotifier.Consumers.Family +{ + public class LogFamilyKickedMessageFormatter : IDiscordLogFormatter + { + public LogType LogType => LogType.FAMILY_KICK; + + public bool TryFormat(LogFamilyKickedMessage message, out string formattedString) + { + formattedString = $"{message.CreatedAt:yyyy-MM-dd HH:mm:ss} | CHANNEL {message.ChannelId} | " + + $"PLAYER: {message.CharacterName} | FAM_ID: {message.FamilyId} | KICKED_NAME: {message.KickedMemberName} | KICKED_ID: {message.KickedMemberId}"; + return true; + } + } +} \ No newline at end of file diff --git a/srcs/DiscordNotifier/Consumers/Family/LogFamilyLeftMessageFormatter.cs b/srcs/DiscordNotifier/Consumers/Family/LogFamilyLeftMessageFormatter.cs new file mode 100644 index 0000000..6c11296 --- /dev/null +++ b/srcs/DiscordNotifier/Consumers/Family/LogFamilyLeftMessageFormatter.cs @@ -0,0 +1,17 @@ +using DiscordNotifier.Formatting; +using Plugin.PlayerLogs.Messages.Family; + +namespace DiscordNotifier.Consumers.Family +{ + public class LogFamilyLeftMessageFormatter : IDiscordLogFormatter + { + public LogType LogType => LogType.FAMILY_LEFT; + + public bool TryFormat(LogFamilyLeftMessage message, out string formattedString) + { + formattedString = $"{message.CreatedAt:yyyy-MM-dd HH:mm:ss} | CHANNEL {message.ChannelId} | " + + $"PLAYER: {message.CharacterName} | FAM_ID: {message.FamilyId}"; + return true; + } + } +} \ No newline at end of file diff --git a/srcs/DiscordNotifier/Consumers/Family/LogFamilyMessageMessageFormatter.cs b/srcs/DiscordNotifier/Consumers/Family/LogFamilyMessageMessageFormatter.cs new file mode 100644 index 0000000..acdd231 --- /dev/null +++ b/srcs/DiscordNotifier/Consumers/Family/LogFamilyMessageMessageFormatter.cs @@ -0,0 +1,19 @@ +using DiscordNotifier.Formatting; +using Plugin.PlayerLogs.Messages.Family; + +namespace DiscordNotifier.Consumers.Family +{ + public class LogFamilyMessageMessageFormatter : IDiscordLogFormatter + { + public LogType LogType => LogType.FAMILY_MESSAGES; + + public bool TryFormat(LogFamilyMessageMessage message, out string formattedString) + { + formattedString = $"[{message.CreatedAt:yyyy-MM-dd HH:mm:ss}] [CHANNEL {message.ChannelId}] [FAM_ID: {message.FamilyId}]\n" + + $"**Player**: {message.CharacterName}\n" + + $"**MessageType**: {message.FamilyMessageType}\n" + + $"**Message**: {message.Message}\n"; + return true; + } + } +} \ No newline at end of file diff --git a/srcs/DiscordNotifier/Consumers/GameEvents/LogInstantBattleStartDiscordConsumer.cs b/srcs/DiscordNotifier/Consumers/GameEvents/LogInstantBattleStartDiscordConsumer.cs new file mode 100644 index 0000000..5f6550e --- /dev/null +++ b/srcs/DiscordNotifier/Consumers/GameEvents/LogInstantBattleStartDiscordConsumer.cs @@ -0,0 +1,68 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Discord; +using DiscordNotifier.Discord; +using PhoenixLib.ServiceBus; +using WingsAPI.Communication.InstantBattle; + +namespace DiscordNotifier.Consumers.GameEvents +{ + public class LogInstantBattleStartDiscordConsumer : IMessageConsumer + { + private readonly IDiscordWebhookLogsService _discordWebhook; + + public LogInstantBattleStartDiscordConsumer(IDiscordWebhookLogsService discordWebhook) => _discordWebhook = discordWebhook; + + public async Task HandleAsync(InstantBattleStartMessage notification, CancellationToken token) + { + if (notification.HasNoDelay) + { + EmbedFooterBuilder embedFooterBuilder = new EmbedFooterBuilder().WithIconUrl(StaticHardcodedCode.AvatarUrl).WithText("Instant Combat"); + var embedAuthorBuilder = new EmbedAuthorBuilder { IconUrl = StaticHardcodedCode.AvatarUrl }; + var embedBuilders = new List + { + new() + { + Author = embedAuthorBuilder, + Title = "[INSTANT-COMBAT] An instant combat has started!", + Description = "May fate be in your favor", + Color = Color.Orange, + Footer = embedFooterBuilder, + // todo ThumbnailUrl = $"https://friends111.nostale.club/list/ip/iconId here.png", + Timestamp = DateTimeOffset.UtcNow + } + }; + + + await _discordWebhook.PublishLogsEmbedded(LogType.PLAYERS_EVENTS_CHANNEL, embedBuilders); + } + else + { + EmbedFooterBuilder embedFooterBuilder = new EmbedFooterBuilder().WithIconUrl(StaticHardcodedCode.AvatarUrl).WithText("Instant Combat"); + var embedAuthorBuilder = new EmbedAuthorBuilder { IconUrl = StaticHardcodedCode.AvatarUrl }; + var embedBuilders = new List + { + new() + { + Author = embedAuthorBuilder, + Title = "[INSTANT-COMBAT] An instant combat will start in 5 minutes!", + Description = "Ready to fight against waves of monsters...?", + Color = Color.Orange, + Footer = embedFooterBuilder, + // todo ThumbnailUrl = $"https://friends111.nostale.club/list/ip/iconId here.png", + Timestamp = DateTimeOffset.UtcNow + } + }; + + + await _discordWebhook.PublishLogsEmbedded(LogType.PLAYERS_EVENTS_CHANNEL, embedBuilders); + } + } + } +} \ No newline at end of file diff --git a/srcs/DiscordNotifier/Consumers/Item/LogItemGambledMessageFormatter.cs b/srcs/DiscordNotifier/Consumers/Item/LogItemGambledMessageFormatter.cs new file mode 100644 index 0000000..6bddc26 --- /dev/null +++ b/srcs/DiscordNotifier/Consumers/Item/LogItemGambledMessageFormatter.cs @@ -0,0 +1,17 @@ +using DiscordNotifier.Formatting; +using Plugin.PlayerLogs.Messages.Upgrade; + +namespace DiscordNotifier.Consumers.Item +{ + public class LogItemGambledMessageFormatter : IDiscordLogFormatter + { + public LogType LogType => LogType.ITEM_GAMBLED; + + public bool TryFormat(LogItemGambledMessage message, out string formattedString) + { + formattedString = $"{message.CreatedAt:yyyy-MM-dd HH:mm:ss} | CHANNEL {message.ChannelId} | PLAYER: {message.CharacterName} | ITEM: {message.ItemVnum} | MODE: {message.Mode} | " + + $"PROTECTION: {message.Protection}{(message.Amulet != null ? " | AMULET: " + message.Amulet : "")} | SUCCEED: {message.Succeed} | RARITY: {message.OriginalRarity}"; + return true; + } + } +} \ No newline at end of file diff --git a/srcs/DiscordNotifier/Consumers/Item/LogItemUpgradedMessageFormatter.cs b/srcs/DiscordNotifier/Consumers/Item/LogItemUpgradedMessageFormatter.cs new file mode 100644 index 0000000..20baae7 --- /dev/null +++ b/srcs/DiscordNotifier/Consumers/Item/LogItemUpgradedMessageFormatter.cs @@ -0,0 +1,17 @@ +using DiscordNotifier.Formatting; +using Plugin.PlayerLogs.Messages.Upgrade; + +namespace DiscordNotifier.Consumers.Item +{ + public class LogItemUpgradedMessageFormatter : IDiscordLogFormatter + { + public LogType LogType => LogType.ITEM_UPGRADED; + + public bool TryFormat(LogItemUpgradedMessage message, out string formattedString) + { + formattedString = $"{message.CreatedAt:yyyy-MM-dd HH:mm:ss} | CHANNEL {message.ChannelId} | PLAYER: {message.CharacterName} | ITEM: {message.Item.ItemVNum} | MODE: {message.Mode} | " + + $"PROTECTION: {message.Protection} | HAS_AMULET: {message.HasAmulet} | ORIGINAL_UPGRADE: {message.OriginalUpgrade} | RESULT: {message.Result}"; + return true; + } + } +} \ No newline at end of file diff --git a/srcs/DiscordNotifier/Consumers/Maintenance/ServiceDownMessageConsumer.cs b/srcs/DiscordNotifier/Consumers/Maintenance/ServiceDownMessageConsumer.cs new file mode 100644 index 0000000..41d9a7c --- /dev/null +++ b/srcs/DiscordNotifier/Consumers/Maintenance/ServiceDownMessageConsumer.cs @@ -0,0 +1,29 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Threading; +using System.Threading.Tasks; +using Discord.Webhook; +using PhoenixLib.ServiceBus; +using WingsAPI.Communication.Services.Messages; + +namespace DiscordNotifier.Consumers.Maintenance +{ + public class ServiceDownMessageConsumer : IMessageConsumer + { + private static readonly string _webhookUrl = Environment.GetEnvironmentVariable("DISCORD_WEBHOOK_HEALTHCHECK_URL"); + + public async Task HandleAsync(ServiceDownMessage notification, CancellationToken token) + { + if (string.IsNullOrEmpty(_webhookUrl)) + { + return; + } + + var client = new DiscordWebhookClient(_webhookUrl); + await client.SendMessageAsync($"```\n[{notification.LastUpdate:yyyy-MM-dd HH:mm:ss}][HEALTHCHECK] {notification.ServiceName} IS OFFLINE!```"); + } + } +} \ No newline at end of file diff --git a/srcs/DiscordNotifier/Consumers/Maintenance/ServiceMaintenanceNotificationMessageConsumer.cs b/srcs/DiscordNotifier/Consumers/Maintenance/ServiceMaintenanceNotificationMessageConsumer.cs new file mode 100644 index 0000000..ddb82a4 --- /dev/null +++ b/srcs/DiscordNotifier/Consumers/Maintenance/ServiceMaintenanceNotificationMessageConsumer.cs @@ -0,0 +1,89 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Discord; +using DiscordNotifier.Discord; +using PhoenixLib.ServiceBus; +using WingsAPI.Communication.Services.Messages; + +namespace DiscordNotifier.Consumers.Maintenance +{ + public class ServiceMaintenanceNotificationMessageConsumer : IMessageConsumer + { + private const string ThumbnailUrl = "https://friends111.nostale.club/list/ip/1117.png"; + + private static readonly EmbedAuthorBuilder Author = new() { IconUrl = StaticHardcodedCode.AvatarUrl }; + private static readonly EmbedFooterBuilder DefaultFooter = new EmbedFooterBuilder().WithIconUrl(StaticHardcodedCode.AvatarUrl).WithText("Timestamp:"); + private static readonly EmbedFooterBuilder FooterForSchedule = new EmbedFooterBuilder().WithIconUrl(StaticHardcodedCode.AvatarUrl).WithText("Maintenance scheduled for:"); + private readonly IDiscordWebhookLogsService _discordWebhook; + + public ServiceMaintenanceNotificationMessageConsumer(IDiscordWebhookLogsService discordWebhook) => _discordWebhook = discordWebhook; + + public async Task HandleAsync(ServiceMaintenanceNotificationMessage notification, CancellationToken token) + { + DateTime scheduledMaintenanceDateTime = DateTime.UtcNow + notification.TimeLeft; + + EmbedBuilder embedBuilder = notification.NotificationType switch + { + ServiceMaintenanceNotificationType.Rescheduled => new EmbedBuilder + { + Author = Author, + Title = "[NosWings Maintenance] The maintenance has been re-scheduled!", + Description = GenerateScheduleDescription(scheduledMaintenanceDateTime, notification.Reason), + Color = Color.Gold, + ThumbnailUrl = ThumbnailUrl, + Footer = FooterForSchedule, + Timestamp = new DateTimeOffset(scheduledMaintenanceDateTime, TimeSpan.Zero) + }, + ServiceMaintenanceNotificationType.ScheduleStopped => new EmbedBuilder + { + Author = Author, + Title = "[NosWings Maintenance] Maintenance canceled!", + Description = string.IsNullOrEmpty(notification.Reason) ? string.Empty : $"Reason: {notification.Reason}", + Color = Color.Red, + ThumbnailUrl = ThumbnailUrl, + Footer = DefaultFooter, + Timestamp = new DateTimeOffset(scheduledMaintenanceDateTime, TimeSpan.Zero) + }, + ServiceMaintenanceNotificationType.Executed => new EmbedBuilder + { + Author = Author, + Title = "[NosWings Maintenance] The Server is now in Maintenance mode!", + Color = Color.DarkBlue, + ThumbnailUrl = ThumbnailUrl, + Footer = DefaultFooter, + Timestamp = new DateTimeOffset(scheduledMaintenanceDateTime, TimeSpan.Zero) + }, + ServiceMaintenanceNotificationType.EmergencyExecuted => new EmbedBuilder + { + Author = Author, + Title = "[NosWings Maintenance] An emergency maintenance has been called!", + Description = $"An unexpected maintenance has been executed, sorry for any inconvenience it may have cause.\n**Reason:** {notification.Reason}", + Color = Color.Purple, + ThumbnailUrl = ThumbnailUrl, + Footer = DefaultFooter, + Timestamp = new DateTimeOffset(scheduledMaintenanceDateTime, TimeSpan.Zero) + }, + ServiceMaintenanceNotificationType.Lifted => new EmbedBuilder + { + Author = Author, + Title = "[NosWings Maintenance] The Server's maintenance has been lifted!", + Color = Color.Green, + ThumbnailUrl = ThumbnailUrl, + Footer = DefaultFooter, + Timestamp = new DateTimeOffset(scheduledMaintenanceDateTime, TimeSpan.Zero) + }, + _ => null + }; + + if (embedBuilder == null) + { + return; + } + + await _discordWebhook.PublishLogEmbedded(LogType.PLAYERS_EVENTS_CHANNEL, embedBuilder); + } + + private static string GenerateScheduleDescription(DateTime scheduledMaintenanceDateTime, string reason) => $"**Date and time:** {scheduledMaintenanceDateTime:U} (UTC)\n**Reason:** {reason}"; + } +} \ No newline at end of file diff --git a/srcs/DiscordNotifier/Consumers/Minigame/LogMinigameRewardClaimedMessageFormatter.cs b/srcs/DiscordNotifier/Consumers/Minigame/LogMinigameRewardClaimedMessageFormatter.cs new file mode 100644 index 0000000..4529ec2 --- /dev/null +++ b/srcs/DiscordNotifier/Consumers/Minigame/LogMinigameRewardClaimedMessageFormatter.cs @@ -0,0 +1,25 @@ +using DiscordNotifier.Formatting; +using Plugin.PlayerLogs.Messages.Miniland; +using WingsEmu.Game.Configurations.Miniland; + +namespace DiscordNotifier.Consumers.Minigame +{ + public class LogMinigameRewardClaimedMessageFormatter : IDiscordLogFormatter + { + public LogType LogType => LogType.MINIGAME_REWARD_CLAIMED; + + public bool TryFormat(LogMinigameRewardClaimedMessage log, out string formattedString) + { + // For now we only want to log on DC the Lv.4 and Lv.5 rewards + if (log.RewardLevel != RewardLevel.FourthReward && log.RewardLevel != RewardLevel.FifthReward) + { + formattedString = string.Empty; + return false; + } + + formattedString = + $"{log.CreatedAt:yyyy-MM-dd HH:mm:ss} | CHANNEL {log.ChannelId} | {log.CharacterName} | MINIGAME {log.MinigameType} | OWNER_ID {log.OwnerId} | Lv.{(short)log.RewardLevel + 1} | ITEM {log.ItemVnum} x{log.Amount}"; + return true; + } + } +} \ No newline at end of file diff --git a/srcs/DiscordNotifier/Consumers/Minigame/LogMinigameScoreMessageFormatter.cs b/srcs/DiscordNotifier/Consumers/Minigame/LogMinigameScoreMessageFormatter.cs new file mode 100644 index 0000000..424f4d0 --- /dev/null +++ b/srcs/DiscordNotifier/Consumers/Minigame/LogMinigameScoreMessageFormatter.cs @@ -0,0 +1,18 @@ +using DiscordNotifier.Formatting; +using Plugin.PlayerLogs.Messages.Miniland; + +namespace DiscordNotifier.Consumers.Minigame +{ + public class LogMinigameScoreMessageFormatter : IDiscordLogFormatter + { + public LogType LogType => LogType.MINIGAME_SCORE; + + public bool TryFormat(LogMinigameScoreMessage message, out string formattedString) + { + formattedString = + $"{message.CreatedAt:yyyy-MM-dd HH:mm:ss} | CHANNEL {message.ChannelId} | {message.CharacterName} | MINIGAME {message.MinigameType} | OWNER_ID {message.OwnerId} | SCORE1 {message.Score1} |" + + $"SCORE2 {message.Score2} | VALIDITY {message.ScoreValidity} | COMPLETION_TIME {message.CompletionTime.ToString()}"; + return true; + } + } +} \ No newline at end of file diff --git a/srcs/DiscordNotifier/Consumers/Player/LogGMCommandExecutedMessageFormatter.cs b/srcs/DiscordNotifier/Consumers/Player/LogGMCommandExecutedMessageFormatter.cs new file mode 100644 index 0000000..d89052c --- /dev/null +++ b/srcs/DiscordNotifier/Consumers/Player/LogGMCommandExecutedMessageFormatter.cs @@ -0,0 +1,17 @@ +using DiscordNotifier.Formatting; +using Plugin.PlayerLogs.Messages; + +namespace DiscordNotifier.Consumers.Player +{ + public class LogGmCommandExecutedMessageFormatter : IDiscordLogFormatter + { + public LogType LogType => LogType.COMMANDS_GM_COMMAND_EXECUTED; + + public bool TryFormat(LogGmCommandExecutedMessage message, out string formattedString) + { + formattedString = + $"{message.CreatedAt:yyyy-MM-dd HH:mm:ss} | CHANNEL {message.ChannelId} | P.Authority: {message.PlayerAuthority} | C.Authority: [{message.CommandAuthority}] {message.CharacterName} | {message.Command}"; + return true; + } + } +} \ No newline at end of file diff --git a/srcs/DiscordNotifier/Consumers/Player/LogPlayerCommandExecutedMessageFormatter.cs b/srcs/DiscordNotifier/Consumers/Player/LogPlayerCommandExecutedMessageFormatter.cs new file mode 100644 index 0000000..cb7f5d7 --- /dev/null +++ b/srcs/DiscordNotifier/Consumers/Player/LogPlayerCommandExecutedMessageFormatter.cs @@ -0,0 +1,16 @@ +using DiscordNotifier.Formatting; +using Plugin.PlayerLogs.Messages.Player; + +namespace DiscordNotifier.Consumers.Player +{ + public class LogPlayerCommandExecutedMessageFormatter : IDiscordLogFormatter + { + public LogType LogType => LogType.COMMANDS_PLAYER_COMMAND_EXECUTED; + + public bool TryFormat(LogPlayerCommandExecutedMessage message, out string formattedString) + { + formattedString = $"{message.CreatedAt:yyyy-MM-dd HH:mm:ss} | CHANNEL {message.ChannelId} | {message.CharacterName} | {message.Command}"; + return true; + } + } +} \ No newline at end of file diff --git a/srcs/DiscordNotifier/Consumers/Player/LogPlayerLevelUpMessageFormatter.cs b/srcs/DiscordNotifier/Consumers/Player/LogPlayerLevelUpMessageFormatter.cs new file mode 100644 index 0000000..e470bf2 --- /dev/null +++ b/srcs/DiscordNotifier/Consumers/Player/LogPlayerLevelUpMessageFormatter.cs @@ -0,0 +1,17 @@ +using DiscordNotifier.Formatting; +using Plugin.PlayerLogs.Messages.LevelUp; + +namespace DiscordNotifier.Consumers.Player +{ + public class LogPlayerLevelUpMessageFormatter : IDiscordLogFormatter + { + public LogType LogType => LogType.FARMING_LEVEL_UP; + + public bool TryFormat(LogLevelUpCharacterMessage message, out string formattedString) + { + formattedString = + $"{message.CreatedAt:yyyy-MM-dd HH:mm:ss} | CHANNEL {message.ChannelId} | {message.CharacterName} {message.LevelType.ToUpperInvariant()} {message.Level}"; + return true; + } + } +} \ No newline at end of file diff --git a/srcs/DiscordNotifier/Consumers/Player/LogStrangeBehaviorMessageFormatter.cs b/srcs/DiscordNotifier/Consumers/Player/LogStrangeBehaviorMessageFormatter.cs new file mode 100644 index 0000000..96f474c --- /dev/null +++ b/srcs/DiscordNotifier/Consumers/Player/LogStrangeBehaviorMessageFormatter.cs @@ -0,0 +1,17 @@ +using DiscordNotifier.Formatting; +using Plugin.PlayerLogs.Messages; + +namespace DiscordNotifier.Consumers.Player +{ + public class LogStrangeBehaviorMessageFormatter : IDiscordLogFormatter + { + public LogType LogType => LogType.STRANGE_BEHAVIORS; + + public bool TryFormat(LogStrangeBehaviorMessage message, out string formattedString) + { + formattedString = + $"{message.CreatedAt:yyyy-MM-dd HH:mm:ss} | CHANNEL {message.ChannelId} | {message.CharacterName} | [{message.SeverityType}] -> {message.Message}"; + return true; + } + } +} \ No newline at end of file diff --git a/srcs/DiscordNotifier/Discord/DiscordWebhookConfiguration.cs b/srcs/DiscordNotifier/Discord/DiscordWebhookConfiguration.cs new file mode 100644 index 0000000..2051d7d --- /dev/null +++ b/srcs/DiscordNotifier/Discord/DiscordWebhookConfiguration.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; + +namespace DiscordNotifier.Discord +{ + public class DiscordWebhookConfiguration : Dictionary + { + } +} \ No newline at end of file diff --git a/srcs/DiscordNotifier/Discord/DiscordWebhookLogsService.cs b/srcs/DiscordNotifier/Discord/DiscordWebhookLogsService.cs new file mode 100644 index 0000000..8fa4bd6 --- /dev/null +++ b/srcs/DiscordNotifier/Discord/DiscordWebhookLogsService.cs @@ -0,0 +1,80 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Discord; +using Discord.Webhook; +using PhoenixLib.Logging; + +namespace DiscordNotifier.Discord +{ + public class DiscordWebhookLogsService : IDiscordWebhookLogsService + { + private readonly DiscordWebhookConfiguration _configs; + + public DiscordWebhookLogsService(DiscordWebhookConfiguration configs) => _configs = configs; + + public async Task PublishLogMessage(LogType logType, string message) + { + try + { + if (!_configs.TryGetValue(logType, out string webhookUrl) || string.IsNullOrEmpty(webhookUrl)) + { + return; + } + + using var webhookClient = new DiscordWebhookClient(webhookUrl); + await webhookClient.SendMessageAsync($"```\n{message}\n```"); + } + catch (Exception e) + { + Log.Error($"[LogsServer] PublishLogEmbedded {logType}", e); + } + } + + public async Task PublishLogEmbedded(LogType logType, EmbedBuilder embed) + { + try + { + if (!_configs.TryGetValue(logType, out string webhookUrl) || string.IsNullOrEmpty(webhookUrl)) + { + return; + } + + using var webhookClient = new DiscordWebhookClient(webhookUrl); + await webhookClient.SendMessageAsync(embeds: new[] { embed.Build() }); + } + catch (Exception e) + { + Log.Error($"[LogsServer] PublishLogEmbedded {logType}", e); + } + } + + public async Task PublishLogsEmbedded(LogType logType, List embeds) + { + var builtEmbeds = new List(); + foreach (EmbedBuilder embed in embeds) + { + builtEmbeds.Add(embed.Build()); + } + + try + { + if (!_configs.TryGetValue(logType, out string webhookUrl) || string.IsNullOrEmpty(webhookUrl)) + { + return; + } + + using var webhookClient = new DiscordWebhookClient(webhookUrl); + await webhookClient.SendMessageAsync(embeds: builtEmbeds); + } + catch (Exception e) + { + Log.Error($"[LogsServer] PublishLogsEmbedded {logType}", e); + } + } + } +} \ No newline at end of file diff --git a/srcs/DiscordNotifier/Discord/IDiscordWebhookLogsService.cs b/srcs/DiscordNotifier/Discord/IDiscordWebhookLogsService.cs new file mode 100644 index 0000000..98c55e8 --- /dev/null +++ b/srcs/DiscordNotifier/Discord/IDiscordWebhookLogsService.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Discord; + +namespace DiscordNotifier.Discord +{ + public interface IDiscordWebhookLogsService + { + Task PublishLogMessage(LogType logType, string message); + Task PublishLogEmbedded(LogType logType, EmbedBuilder embed); + Task PublishLogsEmbedded(LogType logType, List embeds); + } +} \ No newline at end of file diff --git a/srcs/DiscordNotifier/DiscordNotifier.csproj b/srcs/DiscordNotifier/DiscordNotifier.csproj new file mode 100644 index 0000000..40958ac --- /dev/null +++ b/srcs/DiscordNotifier/DiscordNotifier.csproj @@ -0,0 +1,28 @@ + + + + net5.0 + ..\..\dist\discord-notifier\ + false + + + + + + + + + + + + + + + + + + + + + + diff --git a/srcs/DiscordNotifier/DockerGracefulStopService.cs b/srcs/DiscordNotifier/DockerGracefulStopService.cs new file mode 100644 index 0000000..1c58955 --- /dev/null +++ b/srcs/DiscordNotifier/DockerGracefulStopService.cs @@ -0,0 +1,54 @@ +using System; +using System.Runtime.Loader; +using System.Threading; +using PhoenixLib.Logging; + +namespace DiscordNotifier +{ + public class DockerGracefulStopService : IDisposable + { + private readonly CancellationTokenSource _cancellationTokenSource; + private readonly ManualResetEventSlim _stoppedEvent; + + public DockerGracefulStopService() + { + _cancellationTokenSource = new CancellationTokenSource(); + _stoppedEvent = new ManualResetEventSlim(); + + // SIGINT + Console.CancelKeyPress += (sender, eventArgs) => + { + Log.Error("[GRACEFUL_SHUTDOWN] SIGINT received", new Exception("[GRACEFUL_SHUTDOWN] SIGINT received")); + GracefulStop(_cancellationTokenSource, _stoppedEvent); + }; + + // SIGTERM + AssemblyLoadContext.Default.Unloading += context => + { + Log.Error("[GRACEFUL_SHUTDOWN] SIGTERM received", new Exception("[GRACEFUL_SHUTDOWN] SIGTERM received")); + GracefulStop(_cancellationTokenSource, _stoppedEvent); + }; + + AppDomain.CurrentDomain.UnhandledException += (sender, args) => + { + Log.Error("UnhandledException", args.ExceptionObject as Exception); + GracefulStop(_cancellationTokenSource, _stoppedEvent); + }; + } + + public CancellationToken CancellationToken => _cancellationTokenSource.Token; + + public void Dispose() + { + _stoppedEvent.Set(); + } + + private static void GracefulStop(CancellationTokenSource cancellationTokenSource, ManualResetEventSlim stoppedEvent) + { + Log.Info("DockerGracefulStopService Stopping service"); + cancellationTokenSource.Cancel(); + stoppedEvent.Wait(); + Log.Info("DockerGracefulStopService Stop finished"); + } + } +} \ No newline at end of file diff --git a/srcs/DiscordNotifier/Formatting/DiscordLogExtensions.cs b/srcs/DiscordNotifier/Formatting/DiscordLogExtensions.cs new file mode 100644 index 0000000..47465f8 --- /dev/null +++ b/srcs/DiscordNotifier/Formatting/DiscordLogExtensions.cs @@ -0,0 +1,25 @@ +using Microsoft.Extensions.DependencyInjection; +using PhoenixLib.ServiceBus.Extensions; +using Plugin.PlayerLogs; + +namespace DiscordNotifier.Formatting +{ + public static class DiscordLogExtensions + { + public static void AddDiscordFormattedLog(this IServiceCollection services) + where TMessage : class, IPlayerActionLogMessage + where TFormatter : class, IDiscordLogFormatter + { + services.AddMessageSubscriber>(); + services.AddSingleton, TFormatter>(); + } + + public static void AddDiscordEmbedFormattedLog(this IServiceCollection services) + where TMessage : class, IPlayerActionLogMessage + where TFormatter : class, IDiscordEmbedLogFormatter + { + services.AddMessageSubscriber>(); + services.AddSingleton, TFormatter>(); + } + } +} \ No newline at end of file diff --git a/srcs/DiscordNotifier/Formatting/GenericDiscordEmbedLogConsumer.cs b/srcs/DiscordNotifier/Formatting/GenericDiscordEmbedLogConsumer.cs new file mode 100644 index 0000000..c0a56b3 --- /dev/null +++ b/srcs/DiscordNotifier/Formatting/GenericDiscordEmbedLogConsumer.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Discord; +using DiscordNotifier.Discord; +using PhoenixLib.ServiceBus; +using Plugin.PlayerLogs; + +namespace DiscordNotifier.Formatting +{ + public class GenericDiscordEmbedLogConsumer : IMessageConsumer where T : IPlayerActionLogMessage + { + private readonly IDiscordWebhookLogsService _discordWebhook; + private readonly IDiscordEmbedLogFormatter _formatter; + + public GenericDiscordEmbedLogConsumer(IDiscordWebhookLogsService discordWebhook, IDiscordEmbedLogFormatter formatter) + { + _discordWebhook = discordWebhook; + _formatter = formatter; + } + + public async Task HandleAsync(T notification, CancellationToken token) + { + if (!_formatter.TryFormat(notification, out List embeds)) + { + return; + } + + await _discordWebhook.PublishLogsEmbedded(_formatter.LogType, embeds); + } + } +} \ No newline at end of file diff --git a/srcs/DiscordNotifier/Formatting/GenericDiscordLogConsumer.cs b/srcs/DiscordNotifier/Formatting/GenericDiscordLogConsumer.cs new file mode 100644 index 0000000..62d6672 --- /dev/null +++ b/srcs/DiscordNotifier/Formatting/GenericDiscordLogConsumer.cs @@ -0,0 +1,30 @@ +using System.Threading; +using System.Threading.Tasks; +using DiscordNotifier.Discord; +using PhoenixLib.ServiceBus; +using Plugin.PlayerLogs; + +namespace DiscordNotifier.Formatting +{ + public class GenericDiscordLogConsumer : IMessageConsumer where T : IPlayerActionLogMessage + { + private readonly IDiscordWebhookLogsService _discordWebhook; + private readonly IDiscordLogFormatter _formatter; + + public GenericDiscordLogConsumer(IDiscordLogFormatter formatter, IDiscordWebhookLogsService discordWebhook) + { + _formatter = formatter; + _discordWebhook = discordWebhook; + } + + public async Task HandleAsync(T notification, CancellationToken token) + { + if (!_formatter.TryFormat(notification, out string formattedString)) + { + return; + } + + await _discordWebhook.PublishLogMessage(_formatter.LogType, formattedString); + } + } +} \ No newline at end of file diff --git a/srcs/DiscordNotifier/Formatting/IDiscordEmbedLogFormatter.cs b/srcs/DiscordNotifier/Formatting/IDiscordEmbedLogFormatter.cs new file mode 100644 index 0000000..ff3b3fd --- /dev/null +++ b/srcs/DiscordNotifier/Formatting/IDiscordEmbedLogFormatter.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using Discord; +using Plugin.PlayerLogs; + +namespace DiscordNotifier.Formatting +{ + public interface IDiscordEmbedLogFormatter where TMessage : IPlayerActionLogMessage + { + LogType LogType { get; } + bool TryFormat(TMessage message, out List embeds); + } +} \ No newline at end of file diff --git a/srcs/DiscordNotifier/Formatting/IDiscordLogFormatter.cs b/srcs/DiscordNotifier/Formatting/IDiscordLogFormatter.cs new file mode 100644 index 0000000..e5980cf --- /dev/null +++ b/srcs/DiscordNotifier/Formatting/IDiscordLogFormatter.cs @@ -0,0 +1,8 @@ +namespace DiscordNotifier.Formatting +{ + public interface IDiscordLogFormatter + { + LogType LogType { get; } + bool TryFormat(TMessage message, out string formattedString); + } +} \ No newline at end of file diff --git a/srcs/DiscordNotifier/LogType.cs b/srcs/DiscordNotifier/LogType.cs new file mode 100644 index 0000000..d803653 --- /dev/null +++ b/srcs/DiscordNotifier/LogType.cs @@ -0,0 +1,45 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace DiscordNotifier +{ + public enum LogType + { + PLAYERS_EVENTS_CHANNEL, + + CHAT_GENERAL, // channel, characterName, message + CHAT_WHISPERS, // channel, characterName, message + CHAT_FRIENDS, // channel, characterName, message + CHAT_FAMILIES, // channel, characterName, message + CHAT_SPEAKERS, // channel, characterName, message + CHAT_GROUPS, // channel, characterName, groupId, message + + FARMING_LEVEL_UP, // channel, characterName, levelType, level + + FAMILY_CREATED, // channel, characterName, familyId, familyName, deputies + FAMILY_DISBANDED, // channel, characterName, familyId + FAMILY_JOINED, // channel, characterName, familyId, inviterId + FAMILY_LEFT, // channel, characterName, familyId + FAMILY_KICK, // channel, characterName, familyId, kickedId, kickedName + FAMILY_MESSAGES, // channel, characterName, familyId, messageType, message + + MINIGAME_REWARD_CLAIMED, + MINIGAME_SCORE, + + ITEM_GAMBLED, + ITEM_UPGRADED, + + GENERAL_ITEM_USAGE, // channel, characterName, itemVnum + GENERAL_EXCHANGE, // channel, characterName, tradeCharacterName, tradeInfos (items, gold...) + + EXPLOITS_WRONG_PACKET, // channel, characterName, sent packet + EXPLOITS_TRIED_TO_DUPE, // channel, characterName, sent packet + EXPLOITS_WARNINGS, // channel, characterName, WarningType, warning arguments + + STRANGE_BEHAVIORS, // channel, behaviorType, message + + COMMANDS_PLAYER_COMMAND_EXECUTED, // channel, characterName + COMMANDS_GM_COMMAND_EXECUTED // channel, characterName, authority + } +} \ No newline at end of file diff --git a/srcs/DiscordNotifier/Managers/ItemManager.cs b/srcs/DiscordNotifier/Managers/ItemManager.cs new file mode 100644 index 0000000..d771e1b --- /dev/null +++ b/srcs/DiscordNotifier/Managers/ItemManager.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using PhoenixLib.Caching; +using PhoenixLib.Logging; +using WingsAPI.Data.GameData; +using WingsEmu.DTOs.Items; + +namespace DiscordNotifier.Managers +{ + public class ItemManager + { + private readonly ILongKeyCachedRepository _cachedItems; + private readonly IResourceLoader _itemDao; + + public ItemManager(IResourceLoader itemDao, ILongKeyCachedRepository cachedItems) + { + _itemDao = itemDao; + _cachedItems = cachedItems; + } + + public async Task CacheClientItems() + { + Log.Info("[ITEM_MANAGER] Caching items from DB"); + IEnumerable items = await _itemDao.LoadAsync(); + + int count = 0; + foreach (ItemDTO item in items) + { + _cachedItems.Set(item.Id, item); + count++; + } + + Log.Info($"[ITEM_MANAGER] Cached: {count.ToString()} items"); + } + + public int GetItemIconIdByItemId(int itemId) + { + ItemDTO cachedItem = _cachedItems.Get(itemId); + return cachedItem?.IconId ?? default; + } + + public ItemDTO GetItemDtoByItemId(int itemId) => _cachedItems.Get(itemId); + } +} \ No newline at end of file diff --git a/srcs/DiscordNotifier/Program.cs b/srcs/DiscordNotifier/Program.cs new file mode 100644 index 0000000..5406cc3 --- /dev/null +++ b/srcs/DiscordNotifier/Program.cs @@ -0,0 +1,75 @@ +using System; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using DiscordNotifier.Managers; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using PhoenixLib.ServiceBus.MQTT; + +namespace DiscordNotifier +{ + public class Program + { + public static async Task Main(string[] args) + { + PrintHeader(); + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + using var stopService = new DockerGracefulStopService(); + using IHost host = CreateHostBuilder(args).Build(); + { + IServiceProvider services = host.Services; + await host.StartAsync(); + IMessagingService messagingService = services.GetRequiredService(); + await messagingService.StartAsync(); + + ItemManager itemManager = services.GetRequiredService(); + await itemManager.CacheClientItems(); + + await host.WaitForShutdownAsync(stopService.CancellationToken); + await messagingService.DisposeAsync(); + } + } + + private static void PrintHeader() + { + const string text = @" +██╗ ██╗██╗███╗ ██╗ ██████╗ ███████╗███████╗███╗ ███╗██╗ ██╗ +██║ ██║██║████╗ ██║██╔════╝ ██╔════╝██╔════╝████╗ ████║██║ ██║ +██║ █╗ ██║██║██╔██╗ ██║██║ ███╗███████╗█████╗ ██╔████╔██║██║ ██║ +██║███╗██║██║██║╚██╗██║██║ ██║╚════██║██╔══╝ ██║╚██╔╝██║██║ ██║ +╚███╔███╔╝██║██║ ╚████║╚██████╔╝███████║███████╗██║ ╚═╝ ██║╚██████╔╝ + ╚══╝╚══╝ ╚═╝╚═╝ ╚═══╝ ╚═════╝ ╚══════╝╚══════╝╚═╝ ╚═╝ ╚═════╝ + +██████╗ ██╗███████╗ ██████╗ ██████╗ ██████╗ ██████╗ ███╗ ██╗ ██████╗ ████████╗██╗███████╗██╗███████╗██████╗ +██╔══██╗██║██╔════╝██╔════╝██╔═══██╗██╔══██╗██╔══██╗ ████╗ ██║██╔═══██╗╚══██╔══╝██║██╔════╝██║██╔════╝██╔══██╗ +██║ ██║██║███████╗██║ ██║ ██║██████╔╝██║ ██║█████╗██╔██╗ ██║██║ ██║ ██║ ██║█████╗ ██║█████╗ ██████╔╝ +██║ ██║██║╚════██║██║ ██║ ██║██╔══██╗██║ ██║╚════╝██║╚██╗██║██║ ██║ ██║ ██║██╔══╝ ██║██╔══╝ ██╔══██╗ +██████╔╝██║███████║╚██████╗╚██████╔╝██║ ██║██████╔╝ ██║ ╚████║╚██████╔╝ ██║ ██║██║ ██║███████╗██║ ██║ +╚═════╝ ╚═╝╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═════╝ ╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ +"; + string separator = new('=', Console.WindowWidth); + string logo = text.Split('\n').Select(s => string.Format("{0," + (Console.WindowWidth / 2 + s.Length / 2) + "}\n", s)) + .Aggregate("", (current, i) => current + i); + + Console.ForegroundColor = ConsoleColor.Cyan; + Console.WriteLine(separator + logo + $"Version: {Assembly.GetExecutingAssembly().GetName().Version}\n" + separator); + Console.ForegroundColor = ConsoleColor.White; + } + + // Additional configuration is required to successfully run gRPC on macOS. + // For instructions on how to configure Kestrel and gRPC clients on macOS, visit https://go.microsoft.com/fwlink/?linkid=2099682 + private static IHostBuilder CreateHostBuilder(string[] args) + { + IHostBuilder host = Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.ConfigureKestrel(s => { s.ListenAnyIP(28000); }); + webBuilder.UseStartup(); + }); + return host; + } + } +} \ No newline at end of file diff --git a/srcs/DiscordNotifier/Startup.cs b/srcs/DiscordNotifier/Startup.cs new file mode 100644 index 0000000..ca658df --- /dev/null +++ b/srcs/DiscordNotifier/Startup.cs @@ -0,0 +1,127 @@ +using System; +using DiscordNotifier.Consumers.Chat; +using DiscordNotifier.Consumers.Family; +using DiscordNotifier.Consumers.GameEvents; +using DiscordNotifier.Consumers.Item; +using DiscordNotifier.Consumers.Maintenance; +using DiscordNotifier.Consumers.Minigame; +using DiscordNotifier.Consumers.Player; +using DiscordNotifier.Discord; +using DiscordNotifier.Formatting; +using DiscordNotifier.Managers; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using PhoenixLib.Caching; +using PhoenixLib.Configuration; +using PhoenixLib.Events; +using PhoenixLib.Logging; +using PhoenixLib.ServiceBus.Extensions; +using Plugin.Database; +using Plugin.PlayerLogs.Messages; +using Plugin.PlayerLogs.Messages.Family; +using Plugin.PlayerLogs.Messages.LevelUp; +using Plugin.PlayerLogs.Messages.Miniland; +using Plugin.PlayerLogs.Messages.Player; +using Plugin.PlayerLogs.Messages.Upgrade; +using Plugin.ResourceLoader; +using WingsAPI.Communication.InstantBattle; +using WingsAPI.Communication.Services.Messages; +using WingsEmu.Health.Extensions; + +namespace DiscordNotifier +{ + public class Startup + { + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) + { + services.AddMqttConfigurationFromEnv(); + services.AddEventPipeline(); + services.AddEventHandlersInAssembly(); + services.AddMaintenanceMode(); + services.AddPhoenixLogging(); + + services.TryAddSingleton(typeof(ILongKeyCachedRepository<>), typeof(InMemoryCacheRepository<>)); + + new FileResourceLoaderPlugin().AddDependencies(services); + + // discord + services.AddYamlConfigurationHelper(); + services.AddSingleton(new DiscordWebhookConfiguration + { + { LogType.PLAYERS_EVENTS_CHANNEL, Environment.GetEnvironmentVariable("WINGSEMU_DISCORD_WEBHOOK_URL") }, + { LogType.CHAT_FAMILIES, Environment.GetEnvironmentVariable("DISCORD_WEBHOOK_CHAT_FAMILIES") }, + { LogType.CHAT_FRIENDS, Environment.GetEnvironmentVariable("DISCORD_WEBHOOK_CHAT_FRIENDS") }, + { LogType.CHAT_GENERAL, Environment.GetEnvironmentVariable("DISCORD_WEBHOOK_CHAT_GENERAL") }, + { LogType.CHAT_GROUPS, Environment.GetEnvironmentVariable("DISCORD_WEBHOOK_CHAT_GROUPS") }, + { LogType.CHAT_SPEAKERS, Environment.GetEnvironmentVariable("DISCORD_WEBHOOK_CHAT_SPEAKERS") }, + { LogType.CHAT_WHISPERS, Environment.GetEnvironmentVariable("DISCORD_WEBHOOK_CHAT_WHISPERS") }, + { LogType.FARMING_LEVEL_UP, Environment.GetEnvironmentVariable("DISCORD_WEBHOOK_FARMING_LEVEL_UP") }, + { LogType.COMMANDS_PLAYER_COMMAND_EXECUTED, Environment.GetEnvironmentVariable("DISCORD_WEBHOOK_PLAYER_COMMAND_EXECUTED") }, + { LogType.COMMANDS_GM_COMMAND_EXECUTED, Environment.GetEnvironmentVariable("DISCORD_WEBHOOK_GM_COMMAND_EXECUTED") }, + { LogType.FAMILY_CREATED, Environment.GetEnvironmentVariable("DISCORD_WEBHOOK_FAMILY_CREATED") }, + { LogType.FAMILY_DISBANDED, Environment.GetEnvironmentVariable("DISCORD_WEBHOOK_FAMILY_DISBANDED") }, + { LogType.FAMILY_JOINED, Environment.GetEnvironmentVariable("DISCORD_WEBHOOK_FAMILY_JOINED") }, + { LogType.FAMILY_LEFT, Environment.GetEnvironmentVariable("DISCORD_WEBHOOK_FAMILY_LEFT") }, + { LogType.FAMILY_KICK, Environment.GetEnvironmentVariable("DISCORD_WEBHOOK_FAMILY_KICK") }, + { LogType.FAMILY_MESSAGES, Environment.GetEnvironmentVariable("DISCORD_WEBHOOK_FAMILY_MESSAGES") }, + { LogType.MINIGAME_REWARD_CLAIMED, Environment.GetEnvironmentVariable("DISCORD_WEBHOOK_MINIGAME_REWARDS_CLAIMED") }, + { LogType.MINIGAME_SCORE, Environment.GetEnvironmentVariable("DISCORD_WEBHOOK_MINIGAME_SCORE") }, + { LogType.ITEM_GAMBLED, Environment.GetEnvironmentVariable("DISCORD_WEBHOOK_ITEM_GAMBLED") }, + { LogType.ITEM_UPGRADED, Environment.GetEnvironmentVariable("DISCORD_WEBHOOK_ITEM_UPGRADED") }, + { LogType.STRANGE_BEHAVIORS, Environment.GetEnvironmentVariable("DISCORD_WEBHOOK_STRANGE_BEHAVIORS") } + }); + services.AddSingleton(); + services.AddSingleton(); + + new DatabasePlugin().AddDependencies(services); + + services.AddDiscordFormattedLog(); + services.AddDiscordFormattedLog(); + services.AddDiscordFormattedLog(); + services.AddDiscordFormattedLog(); + + // Family discord + services.AddDiscordFormattedLog(); + services.AddDiscordFormattedLog(); + services.AddDiscordFormattedLog(); + services.AddDiscordFormattedLog(); + services.AddDiscordFormattedLog(); + services.AddDiscordFormattedLog(); + services.AddDiscordEmbedFormattedLog(); + + // Minigames discord + services.AddDiscordFormattedLog(); + services.AddDiscordFormattedLog(); + + // Items discord + services.AddDiscordFormattedLog(); + services.AddDiscordFormattedLog(); + + services.AddDiscordFormattedLog(); + + services.AddMessageSubscriber(); + + // healthcheck + services.AddMessageSubscriber(); + + // maintenance + services.AddMessageSubscriber(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseRouting(); + } + } +} \ No newline at end of file diff --git a/srcs/DiscordNotifier/StaticHardcodedCode.cs b/srcs/DiscordNotifier/StaticHardcodedCode.cs new file mode 100644 index 0000000..a1b38f3 --- /dev/null +++ b/srcs/DiscordNotifier/StaticHardcodedCode.cs @@ -0,0 +1,7 @@ +namespace DiscordNotifier +{ + public static class StaticHardcodedCode + { + public const string AvatarUrl = "https://avatars0.githubusercontent.com/u/40839221?s=200"; + } +} \ No newline at end of file diff --git a/srcs/FamilyServer/Achievements/FamilyAchievementIncrement.cs b/srcs/FamilyServer/Achievements/FamilyAchievementIncrement.cs new file mode 100644 index 0000000..4f07fd3 --- /dev/null +++ b/srcs/FamilyServer/Achievements/FamilyAchievementIncrement.cs @@ -0,0 +1,11 @@ +using PhoenixLib.Events; + +namespace FamilyServer.Achievements +{ + public class FamilyAchievementIncrement : IAsyncEvent + { + public long FamilyId { get; set; } + public int AchievementId { get; set; } + public int ValueToAdd { get; set; } + } +} \ No newline at end of file diff --git a/srcs/FamilyServer/Achievements/FamilyAchievementIncrementEventHandler.cs b/srcs/FamilyServer/Achievements/FamilyAchievementIncrementEventHandler.cs new file mode 100644 index 0000000..fc47c03 --- /dev/null +++ b/srcs/FamilyServer/Achievements/FamilyAchievementIncrementEventHandler.cs @@ -0,0 +1,19 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; + +namespace FamilyServer.Achievements +{ + public class FamilyAchievementIncrementEventHandler : IAsyncEventProcessor + { + private readonly FamilyAchievementManager _familyAchievementManager; + + public FamilyAchievementIncrementEventHandler(FamilyAchievementManager familyAchievementManager) => _familyAchievementManager = familyAchievementManager; + + public Task HandleAsync(FamilyAchievementIncrement e, CancellationToken cancellation) + { + _familyAchievementManager.AddIncrementToQueue(e); + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/srcs/FamilyServer/Achievements/FamilyAchievementIncrementMessageConsumer.cs b/srcs/FamilyServer/Achievements/FamilyAchievementIncrementMessageConsumer.cs new file mode 100644 index 0000000..85c2b5c --- /dev/null +++ b/srcs/FamilyServer/Achievements/FamilyAchievementIncrementMessageConsumer.cs @@ -0,0 +1,25 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; +using Plugin.FamilyImpl.Achievements; + +namespace FamilyServer.Achievements +{ + public class FamilyAchievementIncrementMessageConsumer : IMessageConsumer + { + private readonly FamilyAchievementManager _familyAchievementManager; + + public FamilyAchievementIncrementMessageConsumer(FamilyAchievementManager familyAchievementManager) => _familyAchievementManager = familyAchievementManager; + + public Task HandleAsync(FamilyAchievementIncrementMessage notification, CancellationToken token) + { + var incrementRequest = new FamilyAchievementIncrement { AchievementId = notification.AchievementId, FamilyId = notification.FamilyId, ValueToAdd = notification.ValueToAdd }; + _familyAchievementManager.AddIncrementToQueue(incrementRequest); + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/srcs/FamilyServer/Achievements/FamilyAchievementManager.cs b/srcs/FamilyServer/Achievements/FamilyAchievementManager.cs new file mode 100644 index 0000000..3a949bc --- /dev/null +++ b/srcs/FamilyServer/Achievements/FamilyAchievementManager.cs @@ -0,0 +1,361 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using FamilyServer.Logs; +using FamilyServer.Managers; +using Microsoft.Extensions.Hosting; +using PhoenixLib.DAL.Redis.Locks; +using PhoenixLib.Logging; +using PhoenixLib.ServiceBus; +using Plugin.FamilyImpl.Achievements; +using Plugin.FamilyImpl.Messages; +using WingsAPI.Communication.Families; +using WingsAPI.Data.Families; +using WingsEmu.Packets.Enums.Families; + +namespace FamilyServer.Achievements +{ + public class FamilyAchievementManager : BackgroundService + { + private readonly Dictionary _achievementsConfiguration; + private readonly ConcurrentQueue _achievementsQueue = new(); + private readonly FamilyAchievementsConfiguration _configuration; + private readonly IExpirableLockService _expirableLockService; + private readonly FamilyExperienceManager _familyExperienceManager; + private readonly FamilyLogManager _familyLogManager; + private readonly FamilyManager _familyManager; + private readonly IFamilyService _familyService; + private readonly IMessagePublisher _familyUpdatePublisher; + private readonly Dictionary _missionSpecificConfigurations; + private readonly ConcurrentQueue _missionsQueue = new(); + private readonly IMessagePublisher _unlockedMessagePublisher; + + public FamilyAchievementManager(FamilyManager familyManager, FamilyAchievementsConfiguration configuration, IMessagePublisher familyUpdatePublisher, + IMessagePublisher unlockedMessagePublisher, IFamilyService familyService, FamilyMissionsConfiguration missionsConfiguration, + IExpirableLockService expirableLockService, FamilyExperienceManager familyExperienceManager, FamilyLogManager familyLogManager) + { + _familyManager = familyManager; + _configuration = configuration; + _familyUpdatePublisher = familyUpdatePublisher; + _unlockedMessagePublisher = unlockedMessagePublisher; + _familyService = familyService; + _expirableLockService = expirableLockService; + _familyExperienceManager = familyExperienceManager; + _familyLogManager = familyLogManager; + _missionSpecificConfigurations = missionsConfiguration.ToDictionary(s => s.MissionId); + _achievementsConfiguration = configuration.Counters.ToDictionary(s => s.Id); + } + + private static TimeSpan RefreshTime => TimeSpan.FromSeconds(Convert.ToInt32(Environment.GetEnvironmentVariable("FAMILY_ACHIEVEMENT_REFRESH_IN_SECONDS") ?? "5")); + + public void AddIncrementToQueue(FamilyAchievementIncrement message) + { + _achievementsQueue.Enqueue(message); + } + + public void AddIncrementToQueue(FamilyMissionIncrement message) + { + _missionsQueue.Enqueue(message); + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + Dictionary toUpdate = new(); + await FlushPendingAchievements(toUpdate); + await FlushPendingMissions(toUpdate); + + if (toUpdate.Count > 0) + { + foreach (FamilyDTO family in toUpdate.Values) + { + await _familyManager.SaveFamilyAsync(family); + } + + await _familyUpdatePublisher.PublishAsync(new FamilyUpdateMessage + { + Families = toUpdate.Values.ToList(), + ChangedInfoFamilyUpdate = ChangedInfoFamilyUpdate.AchievementsAndMissions + }); + } + + await Task.Delay(RefreshTime, stoppingToken); + } + } + + private async Task FlushPendingMissions(Dictionary toUpdate) + { + try + { + if (_missionsQueue.IsEmpty) + { + return; + } + + HashSet<(long, int)> unlockedMissions = new(); + while (_missionsQueue.TryDequeue(out FamilyMissionIncrement message)) + { + FamilyDTO family = await _familyManager.GetFamilyByFamilyIdAsync(message.FamilyId); + + if (family == null) + { + Log.Debug("Family not found"); + continue; + } + + int missionId = message.MissionId; + int value = message.ValueToAdd; + long? characterId = message.CharacterId; + Log.Debug($"[FAMILY_MISSIONS] Family {family.Id} adding {value} to mission: {missionId}"); + + family.Missions ??= new FamilyMissionsDto(); + family.Missions.Missions ??= new Dictionary(); + + if (!family.Missions.Missions.TryGetValue(missionId, out FamilyMissionDto progress)) + { + family.Missions.Missions[missionId] = progress = new FamilyMissionDto + { + Id = missionId + }; + } + + if (progress.CompletionDate.HasValue && progress.CompletionDate < DateTime.UtcNow) + { + Log.Debug($"[FAMILY_MISSIONS] {progress.CompletionDate.Value} need to wait reset"); + continue; + } + + if (!_missionSpecificConfigurations.TryGetValue(missionId, out FamilyMissionSpecificConfiguration config)) + { + Log.Error($"Family achievement config {missionId} is not configured", new Exception()); + continue; + } + + if (config.OncePerPlayerPerDay && !characterId.HasValue) + { + Log.Debug($"[FAMILY_MISSION] could not increment mission: {missionId} for family: {family.Id} because characterId is missing"); + continue; + } + + if (config.OncePerPlayerPerDay && + !await _expirableLockService.TryAddTemporaryLockAsync($"game:locks:family:{family.Id}:missions:{missionId}:character:{characterId}", DateTime.UtcNow.Date.AddDays(1))) + { + Log.Debug($"[FAMILY_MISSION] {characterId} could not increment mission: {missionId} for family: {family.Id}, already done for today"); + continue; + } + + progress.Count += value; + toUpdate.TryAdd(family.Id, family); + if (progress.Count < config.Value) + { + Log.Debug($"[FAMILY_MISSIONS] Family {family.Id} tried to increment mission: {missionId} progress.Count < config.Value"); + continue; + } + + // mission done + DateTime now = DateTime.UtcNow; + progress.CompletionDate = now; + progress.CompletionCount++; + unlockedMissions.Add((family.Id, missionId)); + + if (config.Rewards != null) + { + foreach (FamilyMissionReward reward in config.Rewards) + { + if (reward.FamilyXp.HasValue) + { + _familyExperienceManager.AddExperienceIncrementRequest(new ExperienceIncrementRequest + { + FamilyId = family.Id, + Experience = reward.FamilyXp.Value + }); + } + } + } + + if (progress.Count > config.Value) + { + progress.Count = config.Value; + continue; + } + + Log.Debug($"[FAMILY_MISSIONS] Family {family.Id} tried to increment mission: {missionId} progress.Count <= config.Value"); + } + + foreach ((long familyId, int missionId) in unlockedMissions) + { + _familyLogManager.SaveFamilyLogs(new[] + { + new FamilyLogDto + { + FamilyLogType = FamilyLogType.FamilyMission, + FamilyId = familyId, + Actor = missionId.ToString(), + Timestamp = DateTime.UtcNow + } + }); + } + } + catch (Exception e) + { + Log.Error("[FAMILY_ACHIEVEMENT_MANAGER]", e); + } + } + + private async Task FlushPendingAchievements(Dictionary toUpdate) + { + try + { + if (_achievementsQueue.IsEmpty) + { + return; + } + + HashSet<(long, int)> unlockedAchievements = new(); + while (_achievementsQueue.TryDequeue(out FamilyAchievementIncrement message)) + { + FamilyDTO family = await _familyManager.GetFamilyByFamilyIdAsync(message.FamilyId); + + if (family == null) + { + Log.Debug("Family not found"); + continue; + } + + int achievementId = message.AchievementId; + int value = message.ValueToAdd; + Log.Debug($"[FAMILY_ACHIEVEMENT] Family {family.Id} adding {value} to achievement: {achievementId}"); + + + family.Achievements ??= new FamilyAchievementsDto(); + family.Achievements.Achievements ??= new Dictionary(); + family.Achievements.Progress ??= new Dictionary(); + if (family.Achievements?.Achievements?.ContainsKey(achievementId) == true) + { + AddNextAchievement(achievementId, family, value); + continue; + } + + if (!family.Achievements.Progress.TryGetValue(achievementId, out FamilyAchievementProgressDto progress)) + { + family.Achievements.Progress[achievementId] = progress = new FamilyAchievementProgressDto + { + Id = achievementId + }; + } + + if (!_achievementsConfiguration.TryGetValue(achievementId, out FamilyAchievementSpecificConfiguration config)) + { + Log.Error($"Family achievement config {achievementId} is not configured", new Exception($"[FAMILY_ACHIEVEMENT_CONFIG] {progress.Id} not configured")); + continue; + } + + if (config.RequiredId.HasValue && !family.Achievements.Achievements.ContainsKey(config.RequiredId.Value)) + { + Log.Debug($"[FAMILY_ACHIEVEMENT] Family {family.Id} tried to increment achievement: {achievementId} but didn't have {config.RequiredId.Value}"); + continue; + } + + progress.Count += value; + + toUpdate.TryAdd(family.Id, family); + if (progress.Count < config.Value) + { + Log.Debug($"[FAMILY_ACHIEVEMENT] Family {family.Id} tried to increment achievement: {achievementId} progress.Count < config.Value"); + continue; + } + + // achievement unlocked + family.Achievements.Achievements[achievementId] = new FamilyAchievementCompletionDto + { + Id = progress.Id, + CompletionDate = DateTime.UtcNow + }; + + (long Id, int achievementId) key = (family.Id, achievementId); + if (!unlockedAchievements.Contains(key)) + { + unlockedAchievements.Add(key); + } + + if (config.Rewards != null) + { + foreach (FamilyAchievementReward reward in config.Rewards) + { + if (reward.FamilyXp.HasValue) + { + _familyExperienceManager.AddExperienceIncrementRequest(new ExperienceIncrementRequest + { + FamilyId = family.Id, + Experience = reward.FamilyXp.Value + }); + } + + if (reward.FamilyUpgradeCategory.HasValue && reward.UpgradeValue.HasValue && reward.UpgradeId.HasValue) + { + await _familyService.TryAddFamilyUpgrade(new FamilyUpgradeRequest + { + FamilyId = family.Id, + FamilyUpgradeType = reward.FamilyUpgradeCategory.Value, + UpgradeId = reward.UpgradeId.Value, + Value = reward.UpgradeValue.Value + }); + } + } + } + + family.Achievements.Progress.Remove(achievementId); + + int progressCount = achievementId is >= 9018 and <= 9036 ? 0 : progress.Count; + + AddNextAchievement(achievementId, family, progressCount); + } + + foreach ((long familyId, int achievementId) in unlockedAchievements) + { + await _unlockedMessagePublisher.PublishAsync(new FamilyAchievementUnlockedMessage + { + FamilyId = familyId, + AchievementId = achievementId + }); + + _familyLogManager.SaveFamilyLogs(new[] + { + new FamilyLogDto + { + FamilyLogType = FamilyLogType.FamilyAchievement, + FamilyId = familyId, + Actor = achievementId.ToString(), + Timestamp = DateTime.UtcNow + } + }); + } + } + catch (Exception e) + { + Log.Error("[FAMILY_ACHIEVEMENT_MANAGER]", e); + } + } + + private void AddNextAchievement(int achievementId, FamilyDTO family, int value) + { + FamilyAchievementSpecificConfiguration tmp = _configuration.Counters.FirstOrDefault(s => s.RequiredId == achievementId); + if (tmp == null) + { + Log.Error($"[FAMILY_ACHIEVEMENT_MANAGER] Couldn't find next achievement for {achievementId} - familyId: {family.Id}", new Exception()); + return; + } + + _achievementsQueue.Enqueue(new FamilyAchievementIncrement + { + FamilyId = family.Id, + AchievementId = tmp.Id, + ValueToAdd = value + }); + } + } +} \ No newline at end of file diff --git a/srcs/FamilyServer/Achievements/FamilyMissionIncrement.cs b/srcs/FamilyServer/Achievements/FamilyMissionIncrement.cs new file mode 100644 index 0000000..027a1ed --- /dev/null +++ b/srcs/FamilyServer/Achievements/FamilyMissionIncrement.cs @@ -0,0 +1,10 @@ +namespace FamilyServer.Achievements +{ + public class FamilyMissionIncrement + { + public long FamilyId { get; set; } + public long? CharacterId { get; set; } + public int MissionId { get; set; } + public int ValueToAdd { get; set; } + } +} \ No newline at end of file diff --git a/srcs/FamilyServer/Achievements/FamilyMissionIncrementMessageConsumer.cs b/srcs/FamilyServer/Achievements/FamilyMissionIncrementMessageConsumer.cs new file mode 100644 index 0000000..6b4d939 --- /dev/null +++ b/srcs/FamilyServer/Achievements/FamilyMissionIncrementMessageConsumer.cs @@ -0,0 +1,27 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; +using Plugin.FamilyImpl.Achievements; + +namespace FamilyServer.Achievements +{ + public class FamilyMissionIncrementMessageConsumer : IMessageConsumer + { + private readonly FamilyAchievementManager _familyAchievementManager; + + public FamilyMissionIncrementMessageConsumer(FamilyAchievementManager familyAchievementManager) => _familyAchievementManager = familyAchievementManager; + + public Task HandleAsync(FamilyMissionIncrementMessage notification, CancellationToken token) + { + var incrementRequest = new FamilyMissionIncrement + { + MissionId = notification.MissionId, + CharacterId = notification.CharacterId, + FamilyId = notification.FamilyId, + ValueToAdd = notification.ValueToAdd + }; + _familyAchievementManager.AddIncrementToQueue(incrementRequest); + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/srcs/FamilyServer/Consumers/FamilyCharacterConnectMessageConsumer.cs b/srcs/FamilyServer/Consumers/FamilyCharacterConnectMessageConsumer.cs new file mode 100644 index 0000000..c75d121 --- /dev/null +++ b/srcs/FamilyServer/Consumers/FamilyCharacterConnectMessageConsumer.cs @@ -0,0 +1,24 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; +using Plugin.FamilyImpl.Messages; +using WingsEmu.Plugins.DistributedGameEvents.PlayerEvents; + +namespace FamilyServer.Consumers +{ + public class FamilyCharacterConnectMessageConsumer : IMessageConsumer + { + private readonly IMessagePublisher _messagePublisher; + + public FamilyCharacterConnectMessageConsumer(IMessagePublisher messagePublisher) => _messagePublisher = messagePublisher; + + public async Task HandleAsync(PlayerConnectedOnChannelMessage notification, CancellationToken token) + { + await _messagePublisher.PublishAsync(new FamilyCharacterJoinMessage + { + CharacterId = notification.CharacterId, + FamilyId = notification.FamilyId + }); + } + } +} \ No newline at end of file diff --git a/srcs/FamilyServer/Consumers/FamilyCharacterDisconnectMessageConsumer.cs b/srcs/FamilyServer/Consumers/FamilyCharacterDisconnectMessageConsumer.cs new file mode 100644 index 0000000..2d40863 --- /dev/null +++ b/srcs/FamilyServer/Consumers/FamilyCharacterDisconnectMessageConsumer.cs @@ -0,0 +1,41 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; +using Plugin.FamilyImpl.Messages; +using WingsAPI.Communication.Families; +using WingsEmu.Plugins.DistributedGameEvents.PlayerEvents; + +namespace FamilyServer.Consumers +{ + public class FamilyCharacterDisconnectMessageConsumer : IMessageConsumer + { + private readonly IFamilyService _familyService; + private readonly IMessagePublisher _messagePublisher; + + public FamilyCharacterDisconnectMessageConsumer(IMessagePublisher messagePublisher, IFamilyService familyService) + { + _messagePublisher = messagePublisher; + _familyService = familyService; + } + + public async Task HandleAsync(PlayerDisconnectedChannelMessage e, CancellationToken cancellation) + { + if (!e.FamilyId.HasValue) + { + return; + } + + await _familyService.MemberDisconnectedAsync(new FamilyMemberDisconnectedRequest + { + CharacterId = e.CharacterId, + DisconnectionTime = e.DisconnectionTime + }); + ; + await _messagePublisher.PublishAsync(new FamilyCharacterLeaveMessage + { + CharacterId = e.CharacterId, + FamilyId = e.FamilyId.Value + }, cancellation); + } + } +} \ No newline at end of file diff --git a/srcs/FamilyServer/Consumers/FamilyDeclareExperienceGainedMessageConsumer.cs b/srcs/FamilyServer/Consumers/FamilyDeclareExperienceGainedMessageConsumer.cs new file mode 100644 index 0000000..2c7fdbf --- /dev/null +++ b/srcs/FamilyServer/Consumers/FamilyDeclareExperienceGainedMessageConsumer.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using FamilyServer.Managers; +using PhoenixLib.ServiceBus; +using Plugin.FamilyImpl.Messages; +using WingsEmu.Game.Families; + +namespace FamilyServer.Consumers +{ + public class FamilyDeclareExperienceGainedMessageConsumer : IMessageConsumer + { + private readonly FamilyExperienceManager _familyExperienceManager; + + public FamilyDeclareExperienceGainedMessageConsumer(FamilyExperienceManager familyExperienceManager) => _familyExperienceManager = familyExperienceManager; + + public async Task HandleAsync(FamilyDeclareExperienceGainedMessage e, CancellationToken cancellation) + { + Dictionary dictionary = FuseDifferentXpValues(e.Experiences); + + foreach ((long characterId, long experienceToAdd) in dictionary) + { + _familyExperienceManager.AddExperienceIncrementRequest(new ExperienceIncrementRequest + { + CharacterId = characterId, + Experience = experienceToAdd + }); + } + } + + private static Dictionary FuseDifferentXpValues(IEnumerable experienceGainedSubMessages) + { + var dictionary = new Dictionary(); + foreach (ExperienceGainedSubMessage exp in experienceGainedSubMessages) + { + if (dictionary.ContainsKey(exp.CharacterId)) + { + dictionary[exp.CharacterId] += exp.FamXpGained; + continue; + } + + dictionary.Add(exp.CharacterId, exp.FamXpGained); + } + + return dictionary; + } + } +} \ No newline at end of file diff --git a/srcs/FamilyServer/Consumers/FamilyDeclareLogsMessageConsumer.cs b/srcs/FamilyServer/Consumers/FamilyDeclareLogsMessageConsumer.cs new file mode 100644 index 0000000..e77fdc1 --- /dev/null +++ b/srcs/FamilyServer/Consumers/FamilyDeclareLogsMessageConsumer.cs @@ -0,0 +1,25 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Threading; +using System.Threading.Tasks; +using FamilyServer.Logs; +using PhoenixLib.ServiceBus; +using Plugin.FamilyImpl.Messages; + +namespace FamilyServer.Consumers +{ + public class FamilyDeclareLogsMessageConsumer : IMessageConsumer + { + private readonly FamilyLogManager _familyLogManager; + + public FamilyDeclareLogsMessageConsumer(FamilyLogManager familyLogManager) => _familyLogManager = familyLogManager; + + public Task HandleAsync(FamilyDeclareLogsMessage e, CancellationToken cancellation) + { + _familyLogManager.SaveFamilyLogs(e.Logs); + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/srcs/FamilyServer/Consumers/FamilyHeadSexMessageConsumer.cs b/srcs/FamilyServer/Consumers/FamilyHeadSexMessageConsumer.cs new file mode 100644 index 0000000..b34bc07 --- /dev/null +++ b/srcs/FamilyServer/Consumers/FamilyHeadSexMessageConsumer.cs @@ -0,0 +1,48 @@ +using System.Threading; +using System.Threading.Tasks; +using FamilyServer.Managers; +using PhoenixLib.ServiceBus; +using Plugin.FamilyImpl.Messages; +using WingsAPI.Data.Families; +using WingsEmu.Packets.Enums.Character; + +namespace FamilyServer.Consumers +{ + public class FamilyHeadSexMessageConsumer : IMessageConsumer + { + private readonly FamilyManager _familyManager; + private readonly IMessagePublisher _messagePublisher; + + public FamilyHeadSexMessageConsumer(FamilyManager familyManager, IMessagePublisher messagePublisher) + { + _familyManager = familyManager; + _messagePublisher = messagePublisher; + } + + public async Task HandleAsync(FamilyHeadSexMessage notification, CancellationToken token) + { + long familyId = notification.FamilyId; + GenderType genderType = notification.NewGenderType; + + FamilyDTO familyDto = await _familyManager.GetFamilyByFamilyIdAsync(familyId); + if (familyDto == null) + { + return; + } + + if (familyDto.HeadGender == genderType) + { + return; + } + + familyDto.HeadGender = genderType; + await _familyManager.SaveFamilyAsync(familyDto); + + await _messagePublisher.PublishAsync(new FamilyUpdateMessage + { + ChangedInfoFamilyUpdate = ChangedInfoFamilyUpdate.HeadSex, + Families = new[] { familyDto } + }); + } + } +} \ No newline at end of file diff --git a/srcs/FamilyServer/Consumers/FamilyMemberTodayMessageConsumer.cs b/srcs/FamilyServer/Consumers/FamilyMemberTodayMessageConsumer.cs new file mode 100644 index 0000000..89dd65d --- /dev/null +++ b/srcs/FamilyServer/Consumers/FamilyMemberTodayMessageConsumer.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using FamilyServer.Managers; +using PhoenixLib.ServiceBus; +using Plugin.FamilyImpl.Messages; +using WingsAPI.Data.Families; + +namespace FamilyServer.Consumers +{ + public class FamilyMemberTodayMessageConsumer : IMessageConsumer + { + private readonly FamilyMembershipManager _familyMembershipManager; + private readonly IMessagePublisher _messagePublisher; + + public FamilyMemberTodayMessageConsumer(FamilyMembershipManager familyMembershipManager, IMessagePublisher messagePublisher) + { + _familyMembershipManager = familyMembershipManager; + _messagePublisher = messagePublisher; + } + + public async Task HandleAsync(FamilyMemberTodayMessage notification, CancellationToken token) + { + FamilyMembershipDto familyMember = await _familyMembershipManager.GetFamilyMembershipByCharacterIdAsync(notification.CharacterId); + if (familyMember == null) + { + return; + } + + familyMember.DailyMessage = notification.Message; + + await _familyMembershipManager.SaveFamilyMembershipAsync(familyMember); + await _messagePublisher.PublishAsync(new FamilyMemberUpdateMessage + { + ChangedInfoMemberUpdate = ChangedInfoMemberUpdate.DailyMessage, + UpdatedMembers = new List + { + familyMember + } + }); + } + } +} \ No newline at end of file diff --git a/srcs/FamilyServer/Consumers/FamilyMissionsResetMessageConsumer.cs b/srcs/FamilyServer/Consumers/FamilyMissionsResetMessageConsumer.cs new file mode 100644 index 0000000..e7e0095 --- /dev/null +++ b/srcs/FamilyServer/Consumers/FamilyMissionsResetMessageConsumer.cs @@ -0,0 +1,20 @@ +using System.Threading; +using System.Threading.Tasks; +using FamilyServer.Services; +using PhoenixLib.ServiceBus; +using WingsAPI.Communication.Families; + +namespace FamilyServer.Consumers +{ + public class FamilyMissionsResetMessageConsumer : IMessageConsumer + { + private readonly FamilyService _familyService; + + public FamilyMissionsResetMessageConsumer(FamilyService familyService) => _familyService = familyService; + + public async Task HandleAsync(FamilyMissionsResetMessage notification, CancellationToken token) + { + await _familyService.ResetFamilyMissions(); + } + } +} \ No newline at end of file diff --git a/srcs/FamilyServer/Consumers/FamilyNoticeMessageConsumer.cs b/srcs/FamilyServer/Consumers/FamilyNoticeMessageConsumer.cs new file mode 100644 index 0000000..ec69c42 --- /dev/null +++ b/srcs/FamilyServer/Consumers/FamilyNoticeMessageConsumer.cs @@ -0,0 +1,42 @@ +using System.Threading; +using System.Threading.Tasks; +using FamilyServer.Managers; +using PhoenixLib.ServiceBus; +using Plugin.FamilyImpl.Messages; +using WingsAPI.Data.Families; + +namespace FamilyServer.Consumers +{ + public class FamilyNoticeMessageConsumer : IMessageConsumer + { + private readonly FamilyManager _familyManager; + private readonly IMessagePublisher _messagePublisher; + + public FamilyNoticeMessageConsumer(FamilyManager familyManager, IMessagePublisher messagePublisher) + { + _familyManager = familyManager; + _messagePublisher = messagePublisher; + } + + public async Task HandleAsync(FamilyNoticeMessage notification, CancellationToken token) + { + long familyId = notification.FamilyId; + string message = notification.Message; + + FamilyDTO familyDto = await _familyManager.GetFamilyByFamilyIdAsync(familyId); + if (familyDto == null) + { + return; + } + + familyDto.Message = message; + await _familyManager.SaveFamilyAsync(familyDto); + + await _messagePublisher.PublishAsync(new FamilyUpdateMessage + { + ChangedInfoFamilyUpdate = ChangedInfoFamilyUpdate.Notice, + Families = new[] { familyDto } + }); + } + } +} \ No newline at end of file diff --git a/srcs/FamilyServer/Consumers/ServiceFlushAllMessageConsumer.cs b/srcs/FamilyServer/Consumers/ServiceFlushAllMessageConsumer.cs new file mode 100644 index 0000000..8e0c63e --- /dev/null +++ b/srcs/FamilyServer/Consumers/ServiceFlushAllMessageConsumer.cs @@ -0,0 +1,19 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; +using WingsAPI.Communication.Services.Messages; + +namespace FamilyServer.Consumers +{ + public class ServiceFlushAllMessageConsumer : IMessageConsumer + { + private readonly FamilySystem _familySystem; + + public ServiceFlushAllMessageConsumer(FamilySystem familySystem) => _familySystem = familySystem; + + public async Task HandleAsync(ServiceFlushAllMessage notification, CancellationToken token) + { + await _familySystem.ProcessMain(); + } + } +} \ No newline at end of file diff --git a/srcs/FamilyServer/DockerGracefulStopService.cs b/srcs/FamilyServer/DockerGracefulStopService.cs new file mode 100644 index 0000000..a07f631 --- /dev/null +++ b/srcs/FamilyServer/DockerGracefulStopService.cs @@ -0,0 +1,54 @@ +using System; +using System.Runtime.Loader; +using System.Threading; +using PhoenixLib.Logging; + +namespace FamilyServer +{ + public class DockerGracefulStopService : IDisposable + { + private readonly CancellationTokenSource _cancellationTokenSource; + private readonly ManualResetEventSlim _stoppedEvent; + + public DockerGracefulStopService() + { + _cancellationTokenSource = new CancellationTokenSource(); + _stoppedEvent = new ManualResetEventSlim(); + + // SIGINT + Console.CancelKeyPress += (sender, eventArgs) => + { + Log.Error("[GRACEFUL_SHUTDOWN] SIGINT received", new Exception("[GRACEFUL_SHUTDOWN] SIGINT received")); + GracefulStop(_cancellationTokenSource, _stoppedEvent); + }; + + // SIGTERM + AssemblyLoadContext.Default.Unloading += context => + { + Log.Error("[GRACEFUL_SHUTDOWN] SIGTERM received", new Exception("[GRACEFUL_SHUTDOWN] SIGTERM received")); + GracefulStop(_cancellationTokenSource, _stoppedEvent); + }; + + AppDomain.CurrentDomain.UnhandledException += (sender, args) => + { + Log.Error("UnhandledException", args.ExceptionObject as Exception); + GracefulStop(_cancellationTokenSource, _stoppedEvent); + }; + } + + public CancellationToken CancellationToken => _cancellationTokenSource.Token; + + public void Dispose() + { + _stoppedEvent.Set(); + } + + private static void GracefulStop(CancellationTokenSource cancellationTokenSource, ManualResetEventSlim stoppedEvent) + { + Log.Info("DockerGracefulStopService Stopping service"); + cancellationTokenSource.Cancel(); + stoppedEvent.Wait(); + Log.Info("DockerGracefulStopService Stop finished"); + } + } +} \ No newline at end of file diff --git a/srcs/FamilyServer/EnvironmentConsts.cs b/srcs/FamilyServer/EnvironmentConsts.cs new file mode 100644 index 0000000..a459a1f --- /dev/null +++ b/srcs/FamilyServer/EnvironmentConsts.cs @@ -0,0 +1,7 @@ +namespace FamilyServer +{ + public static class EnvironmentConsts + { + public const string FamilyServerSaveIntervalMinutes = "FAMILY_SERVER_SAVE_INTERVAL_MINUTES"; + } +} \ No newline at end of file diff --git a/srcs/FamilyServer/FamilyServer.csproj b/srcs/FamilyServer/FamilyServer.csproj new file mode 100644 index 0000000..b4cdf39 --- /dev/null +++ b/srcs/FamilyServer/FamilyServer.csproj @@ -0,0 +1,29 @@ + + + + net5.0 + ..\..\dist\family-server\ + false + + + + + + + + + + + + + + + + + + + + + + + diff --git a/srcs/FamilyServer/FamilySystem.cs b/srcs/FamilyServer/FamilySystem.cs new file mode 100644 index 0000000..ad69a66 --- /dev/null +++ b/srcs/FamilyServer/FamilySystem.cs @@ -0,0 +1,34 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using FamilyServer.Managers; +using Microsoft.Extensions.Hosting; +using PhoenixLib.Logging; + +namespace FamilyServer +{ + public class FamilySystem : BackgroundService + { + private static readonly TimeSpan Interval = TimeSpan.FromMinutes(Convert.ToUInt32(Environment.GetEnvironmentVariable(EnvironmentConsts.FamilyServerSaveIntervalMinutes) ?? "5")); + + private readonly IFamilyWarehouseManager _familyWarehouseManager; + + public FamilySystem(IFamilyWarehouseManager familyWarehouseManager) => _familyWarehouseManager = familyWarehouseManager; + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + Log.Info("[FAMILY_SYSTEM] Loaded"); + + while (!stoppingToken.IsCancellationRequested) + { + await ProcessMain(); + await Task.Delay(Interval, stoppingToken); + } + } + + public async Task ProcessMain() + { + await _familyWarehouseManager.FlushWarehouseSaves(); + } + } +} \ No newline at end of file diff --git a/srcs/FamilyServer/Logs/FamilyLogManager.cs b/srcs/FamilyServer/Logs/FamilyLogManager.cs new file mode 100644 index 0000000..406238b --- /dev/null +++ b/srcs/FamilyServer/Logs/FamilyLogManager.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using PhoenixLib.Caching; +using PhoenixLib.Logging; +using PhoenixLib.ServiceBus; +using Plugin.FamilyImpl.Messages; +using WingsAPI.Data.Families; + +namespace FamilyServer.Logs +{ + public class FamilyLogManager : BackgroundService + { + private readonly ILongKeyCachedRepository> _cachedLogs; + private readonly IFamilyLogDAO _familyLogDao; + private readonly ReaderWriterLockSlim _lock = new(); + private readonly ConcurrentQueue> _logs = new(); + private readonly IMessagePublisher _messagePublisher; + + public FamilyLogManager(IFamilyLogDAO familyLogDao, ILongKeyCachedRepository> cachedLogs, IMessagePublisher messagePublisher) + { + _familyLogDao = familyLogDao; + _cachedLogs = cachedLogs; + _messagePublisher = messagePublisher; + } + + private static TimeSpan RefreshTime => TimeSpan.FromSeconds(Convert.ToInt32(Environment.GetEnvironmentVariable("FAMILY_LOGS_REFRESH_IN_SECONDS") ?? "5")); + + public void SaveFamilyLogs(IReadOnlyList logs) + { + _logs.Enqueue(logs); + } + + public async Task> GetFamilyLogsByFamilyId(long familyId) + { + List cachedLogs = _cachedLogs.Get(familyId); + if (cachedLogs != null) + { + return cachedLogs; + } + + cachedLogs = await _familyLogDao.GetLogsByFamilyIdAsync(familyId); + _cachedLogs.Set(familyId, cachedLogs); + + return cachedLogs; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + Dictionary> toUpdate = new(); + await ProcessLogs(toUpdate); + + if (toUpdate.Count > 0) + { + await _messagePublisher.PublishAsync(new FamilyAcknowledgeLogsMessage + { + Logs = toUpdate + }); + } + + await Task.Delay(RefreshTime, stoppingToken); + } + } + + private async Task ProcessLogs(Dictionary> toUpdate) + { + if (_logs.IsEmpty) + { + return; + } + + try + { + while (_logs.TryDequeue(out IReadOnlyList logs)) + { + IEnumerable savedLogs = await _familyLogDao.SaveAsync(logs); + + foreach (FamilyLogDto log in savedLogs) + { + if (!toUpdate.TryGetValue(log.FamilyId, out List list)) + { + toUpdate.Add(log.FamilyId, new List + { + log + }); + continue; + } + + list.Add(log); + } + + _lock.EnterReadLock(); + try + { + foreach (KeyValuePair> familyLogs in toUpdate) + { + List currentLogs = _cachedLogs.Get(familyLogs.Key); + if (currentLogs == null) + { + continue; + } + + foreach (FamilyLogDto log in familyLogs.Value) + { + currentLogs.Add(log); + } + + currentLogs.Sort((x, y) => DateTime.Compare(y.Timestamp, x.Timestamp)); // Newest -> Older + + if (currentLogs.Count <= 200) + { + continue; + } + + for (int i = 200; i < currentLogs.Count; i++) + { + currentLogs.RemoveAt(i); + } + } + } + finally + { + _lock.ExitReadLock(); + } + } + } + catch (Exception e) + { + Log.Error("[FAMILY_LOGS_MANAGER]", e); + } + } + } +} \ No newline at end of file diff --git a/srcs/FamilyServer/Managers/AddWarehouseItemResult.cs b/srcs/FamilyServer/Managers/AddWarehouseItemResult.cs new file mode 100644 index 0000000..fd7f588 --- /dev/null +++ b/srcs/FamilyServer/Managers/AddWarehouseItemResult.cs @@ -0,0 +1,11 @@ +using WingsAPI.Data.Families; + +namespace FamilyServer.Managers +{ + public class AddWarehouseItemResult + { + public bool Success { get; init; } + + public FamilyWarehouseItemDto UpdatedItem { get; init; } + } +} \ No newline at end of file diff --git a/srcs/FamilyServer/Managers/ExperienceIncrementRequest.cs b/srcs/FamilyServer/Managers/ExperienceIncrementRequest.cs new file mode 100644 index 0000000..f175044 --- /dev/null +++ b/srcs/FamilyServer/Managers/ExperienceIncrementRequest.cs @@ -0,0 +1,9 @@ +namespace FamilyServer.Managers +{ + public class ExperienceIncrementRequest + { + public long? FamilyId { get; set; } + public long? CharacterId { get; set; } + public long Experience { get; set; } + } +} \ No newline at end of file diff --git a/srcs/FamilyServer/Managers/FamilyExperienceManager.cs b/srcs/FamilyServer/Managers/FamilyExperienceManager.cs new file mode 100644 index 0000000..70a80c5 --- /dev/null +++ b/srcs/FamilyServer/Managers/FamilyExperienceManager.cs @@ -0,0 +1,209 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using FamilyServer.Achievements; +using FamilyServer.Logs; +using Microsoft.Extensions.Hosting; +using PhoenixLib.Events; +using PhoenixLib.Logging; +using PhoenixLib.ServiceBus; +using Plugin.FamilyImpl.Achievements; +using Plugin.FamilyImpl.Messages; +using WingsAPI.Data.Families; +using WingsEmu.Game.Families; +using WingsEmu.Game.Families.Configuration; +using WingsEmu.Packets.Enums.Families; + +namespace FamilyServer.Managers +{ + public class FamilyExperienceManager : BackgroundService + { + private static readonly TimeSpan RefreshDelay = TimeSpan.FromSeconds(Convert.ToInt32(Environment.GetEnvironmentVariable("FAMILY_EXPERIENCE_REFRESH_DELAY") ?? "5")); + + private readonly IAsyncEventPipeline _eventPipeline; + private readonly ConcurrentQueue _expIncrementQueue = new(); + private readonly FamilyConfiguration _familyConfiguration; + private readonly FamilyLogManager _familyLogManager; + private readonly FamilyManager _familyManager; + private readonly FamilyMembershipManager _familyMembershipManager; + private readonly IMessagePublisher _messagePublisher; + private readonly IMessagePublisher _messagePublisherLevelUp; + private readonly FamilyMissionsConfiguration _missionsConfiguration; + + public FamilyExperienceManager(IMessagePublisher messagePublisherLevelUp, IMessagePublisher messagePublisher, + FamilyMembershipManager familyMembershipManager, FamilyManager familyManager, FamilyConfiguration familyConfiguration, IAsyncEventPipeline eventPipeline, + FamilyMissionsConfiguration missionsConfiguration, FamilyLogManager familyLogManager) + { + _messagePublisherLevelUp = messagePublisherLevelUp; + _messagePublisher = messagePublisher; + _familyMembershipManager = familyMembershipManager; + _familyManager = familyManager; + _familyConfiguration = familyConfiguration; + _eventPipeline = eventPipeline; + _missionsConfiguration = missionsConfiguration; + _familyLogManager = familyLogManager; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + try + { + await ProcessPendingExpRequests(stoppingToken); + } + catch (Exception e) + { + Log.Error("[FAMILY_EXPERIENCE_MANAGER]", e); + } + + await Task.Delay(RefreshDelay, stoppingToken); + } + } + + public void AddExperienceIncrementRequest(ExperienceIncrementRequest request) + { + _expIncrementQueue.Enqueue(request); + } + + private async Task ProcessPendingExpRequests(CancellationToken cancellationToken) + { + if (_expIncrementQueue.IsEmpty) + { + return; + } + + var memberships = new List(); + var families = new HashSet(); + + while (_expIncrementQueue.TryDequeue(out ExperienceIncrementRequest request)) + { + long? characterId = request.CharacterId; + long? familyId = request.FamilyId; + long experience = request.Experience; + if (characterId.HasValue) + { + FamilyMembershipDto membership = await _familyMembershipManager.GetFamilyMembershipByCharacterIdAsync(characterId.Value); + if (membership == null) + { + continue; + } + + familyId = membership.FamilyId; + membership.Experience += experience; + if (!memberships.Contains(membership)) + { + memberships.Add(membership); + } + } + + if (!familyId.HasValue) + { + // should not happen + continue; + } + + FamilyDTO family = await _familyManager.GetFamilyByFamilyIdAsync(familyId.Value); + if (family == null) + { + continue; + } + + family.Experience += experience; + + if (!families.Contains(family)) + { + families.Add(family); + } + } + + await _familyMembershipManager.SaveFamilyMembershipsAsync(memberships); + + await _messagePublisher.PublishAsync(new FamilyMemberUpdateMessage + { + UpdatedMembers = memberships, + ChangedInfoMemberUpdate = ChangedInfoMemberUpdate.Experience + }, cancellationToken); + + foreach (FamilyDTO family in families) + { + byte oldLevel = family.Level; + byte expectedLevel = _familyConfiguration.GetLevelByFamilyXp(family.Experience); + if (expectedLevel == default) + { + Log.Warn( + $"Found a family that exceeds the expected XP values. FamilyId: {family.Id.ToString()} | CurrentFamilyLvl: {family.Level.ToString()} | FamilyExperience: {family.Experience.ToString()}"); + continue; + } + + if (family.Level > expectedLevel) + { + Log.Warn( + $"A family should have a lower level than it has (based in its experience). FamilyId: {family.Id.ToString()} | CurrentFamilyLvl: {family.Level.ToString()} | FamilyExperience: {family.Experience.ToString()} | ExpectedLvl: {expectedLevel}"); + continue; + } + + family.Level = expectedLevel; + if (expectedLevel <= oldLevel) + { + continue; + } + + _familyLogManager.SaveFamilyLogs(new[] + { + new FamilyLogDto + { + Actor = family.Level.ToString(), + FamilyId = family.Id, + Timestamp = DateTime.UtcNow, + FamilyLogType = FamilyLogType.FamilyLevelUp + } + }); + + await _eventPipeline.ProcessEventAsync(new FamilyAchievementIncrement + { + AchievementId = (short)FamilyAchievementsVnum.FAMILY_LEVEL_2_UNLOCKED, + FamilyId = family.Id, + ValueToAdd = expectedLevel - oldLevel + }); + + family.Missions ??= new FamilyMissionsDto(); + family.Missions.Missions ??= new Dictionary(); + + IEnumerable missionsToAdd = _missionsConfiguration.Where(x => x.MinimumRequiredLevel <= family.Level); + foreach (FamilyMissionSpecificConfiguration mission in missionsToAdd) + { + int missionId = mission.MissionId; + + if (family.Missions.Missions.TryGetValue(missionId, out _)) + { + continue; + } + + family.Missions.Missions[missionId] = new FamilyMissionDto + { + Id = missionId + }; + } + } + + foreach (FamilyDTO family in families) + { + await _familyManager.SaveFamilyAsync(family); + } + + await _messagePublisherLevelUp.PublishAsync(new FamilyUpdateMessage + { + Families = families, + ChangedInfoFamilyUpdate = ChangedInfoFamilyUpdate.Experience + }, cancellationToken); + } + } +} \ No newline at end of file diff --git a/srcs/FamilyServer/Managers/FamilyManager.cs b/srcs/FamilyServer/Managers/FamilyManager.cs new file mode 100644 index 0000000..28666c8 --- /dev/null +++ b/srcs/FamilyServer/Managers/FamilyManager.cs @@ -0,0 +1,68 @@ +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Threading.Tasks; +using PhoenixLib.Caching; +using PhoenixLib.Logging; +using WingsAPI.Data.Families; + +namespace FamilyServer.Managers +{ + public class FamilyManager + { + private readonly ILongKeyCachedRepository _familyCache; + private readonly IFamilyDAO _familyDao; + private readonly HashSet _instantiatedFamilyIds = new(); + + public FamilyManager(ILongKeyCachedRepository familyCache, IFamilyDAO familyDao) + { + _familyCache = familyCache; + _familyDao = familyDao; + } + + public async Task AddFamilyAsync(FamilyDTO familyDto) + { + FamilyDTO family = await SaveFamilyAsync(familyDto); + _familyCache.Set(familyDto.Id, family); + if (!_instantiatedFamilyIds.Contains(family.Id)) + { + _instantiatedFamilyIds.Add(family.Id); + } + + return family; + } + + public async Task RemoveFamilyByIdAsync(long familyId) + { + await _familyDao.DeleteByIdAsync(familyId); + _familyCache.Remove(familyId); + _instantiatedFamilyIds.Remove(familyId); + } + + public async Task GetFamilyByFamilyIdAsync(long familyId) + { + FamilyDTO family = await _familyCache.GetOrSetAsync(familyId, async () => await _familyDao.GetByIdAsync(familyId)); + if (family == null) + { + Log.Error($"[FAMILY_MANAGER] {familyId} could not be found from the cache/db", new DataException($"{familyId} could not be found from the cache/db")); + return null; + } + + if (!_instantiatedFamilyIds.Contains(family.Id)) + { + _instantiatedFamilyIds.Add(family.Id); + } + + return family; + } + + public async Task GetFamilyByNameAsync(string name) => await _familyDao.GetByNameAsync(name); + + public async Task SaveFamilyAsync(FamilyDTO familyDto) => await _familyDao.SaveAsync(familyDto); + + public List GetFamiliesInMemory() + { + return _instantiatedFamilyIds.Select(s => _familyCache.Get(s)).ToList(); + } + } +} \ No newline at end of file diff --git a/srcs/FamilyServer/Managers/FamilyMembershipManager.cs b/srcs/FamilyServer/Managers/FamilyMembershipManager.cs new file mode 100644 index 0000000..f94257d --- /dev/null +++ b/srcs/FamilyServer/Managers/FamilyMembershipManager.cs @@ -0,0 +1,94 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using PhoenixLib.Caching; +using PhoenixLib.Logging; +using WingsAPI.Data.Families; + +namespace FamilyServer.Managers +{ + public class FamilyMembershipManager + { + private readonly ILongKeyCachedRepository _cachedMembers; + private readonly ILongKeyCachedRepository> _cachedMembersByFamilyId; + private readonly IFamilyMembershipDao _familyMembershipDao; + + public FamilyMembershipManager(IFamilyMembershipDao familyMembershipDao, ILongKeyCachedRepository cachedMembers, + ILongKeyCachedRepository> cachedMembersByFamilyId) + { + _familyMembershipDao = familyMembershipDao; + _cachedMembers = cachedMembers; + _cachedMembersByFamilyId = cachedMembersByFamilyId; + } + + public async Task GetFamilyMembershipByCharacterIdAsync(long characterId) + { + FamilyMembershipDto cachedMembership = _cachedMembers.Get(characterId); + return cachedMembership ?? await _familyMembershipDao.GetByCharacterIdAsync(characterId); + } + + public async Task> GetFamilyMembershipsByFamilyIdAsync(long familyId) + { + var list = _cachedMembersByFamilyId.Get(familyId)?.Values.ToList(); + + if (list != null) + { + return list; + } + + list = await _familyMembershipDao.GetByFamilyIdAsync(familyId); + + if (list == null || list.Count < 1) + { + return null; + } + + var dictionary = new Dictionary(); + foreach (FamilyMembershipDto membership in list) + { + if (!dictionary.TryAdd(membership.CharacterId, membership)) + { + Log.Warn( + $"[FAMILY_MEMBERSHIP_CACHE_MANAGER] Found a duplicated membership for CharacterId: {membership.CharacterId.ToString()} | Duplicated Membership's Id: {membership.Id.ToString()}"); + } + + _cachedMembers.Set(membership.CharacterId, membership); + } + + _cachedMembersByFamilyId.Set(familyId, dictionary); + + return list; + } + + public async Task AddFamilyMembershipAsync(FamilyMembershipDto membership) + { + FamilyMembershipDto savedMembership = await SaveFamilyMembershipAsync(membership); + _cachedMembers.Set(savedMembership.CharacterId, savedMembership); + _cachedMembersByFamilyId.Get(savedMembership.FamilyId)?.Add(savedMembership.CharacterId, savedMembership); + return savedMembership; + } + + public async Task> AddFamilyMembershipsAsync(IReadOnlyList memberships) + { + IEnumerable savedMemberships = await SaveFamilyMembershipsAsync(memberships); + foreach (FamilyMembershipDto membership in savedMemberships) + { + _cachedMembers.Set(membership.CharacterId, membership); + _cachedMembersByFamilyId.Get(membership.FamilyId)?.Add(membership.CharacterId, membership); + } + + return savedMemberships; + } + + public async Task SaveFamilyMembershipAsync(FamilyMembershipDto membership) => await _familyMembershipDao.SaveAsync(membership); + + public async Task> SaveFamilyMembershipsAsync(IReadOnlyList memberships) => await _familyMembershipDao.SaveAsync(memberships); + + public async Task RemoveFamilyMembershipByCharAndFamIdAsync(FamilyMembershipDto familyMembership) + { + await _familyMembershipDao.DeleteByIdAsync(familyMembership.Id); + _cachedMembers.Remove(familyMembership.CharacterId); + _cachedMembersByFamilyId.Get(familyMembership.FamilyId)?.Remove(familyMembership.CharacterId); + } + } +} \ No newline at end of file diff --git a/srcs/FamilyServer/Managers/FamilyWarehouseLogManager.cs b/srcs/FamilyServer/Managers/FamilyWarehouseLogManager.cs new file mode 100644 index 0000000..c93913a --- /dev/null +++ b/srcs/FamilyServer/Managers/FamilyWarehouseLogManager.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Foundatio.AsyncEx; +using PhoenixLib.Caching; +using PhoenixLib.Logging; +using PhoenixLib.ServiceBus; +using Plugin.FamilyImpl.Messages; +using WingsAPI.Data.Families; + +namespace FamilyServer.Managers +{ + public class FamilyWarehouseLogManager + { + private const int MaximumAmountOfLogs = 200; + private static readonly TimeSpan LifeTime = TimeSpan.FromMinutes(Convert.ToUInt32(Environment.GetEnvironmentVariable(EnvironmentConsts.FamilyServerSaveIntervalMinutes) ?? "5") * 3); + + private readonly ILongKeyCachedRepository> _cachedFamilyLogs; + + private readonly HashSet _familiesRequiringSave = new(); + private readonly ConcurrentDictionary _familyLocks = new(); + + private readonly IFamilyWarehouseLogDao _familyWarehouseLogDao; + private readonly IMessagePublisher _messagePublisher; + private readonly SemaphoreSlim _semaphoreSlim = new(1, 1); + + public FamilyWarehouseLogManager(IFamilyWarehouseLogDao familyWarehouseLogDao, ILongKeyCachedRepository> cachedFamilyLogs, + IMessagePublisher messagePublisher) + { + _familyWarehouseLogDao = familyWarehouseLogDao; + _cachedFamilyLogs = cachedFamilyLogs; + _messagePublisher = messagePublisher; + } + + private async Task> GetFamilyWarehouseLogs(long familyId) + { + List cachedLogs = _cachedFamilyLogs.Get(familyId); + if (cachedLogs != null) + { + //Refresh TLL + _cachedFamilyLogs.Set(familyId, cachedLogs, LifeTime); + return cachedLogs; + } + + IEnumerable returnedLogs = await _familyWarehouseLogDao.GetByFamilyIdAsync(familyId); + cachedLogs = returnedLogs == null ? new List() : returnedLogs.ToList(); + + _cachedFamilyLogs.Set(familyId, cachedLogs, LifeTime); + return cachedLogs; + } + + private AsyncReaderWriterLock GetFamilyLock(long familyId) => _familyLocks.GetOrAdd(familyId, new AsyncReaderWriterLock()); + + public async Task AddLog(long familyId, FamilyWarehouseLogEntryDto logEntryDto) + { + if (logEntryDto == null) + { + return; + } + + await _semaphoreSlim.WaitAsync(); + try + { + _familiesRequiringSave.Add(familyId); + } + finally + { + _semaphoreSlim.Release(); + } + + AsyncReaderWriterLock familyLock = GetFamilyLock(familyId); + + using (await familyLock.WriterLockAsync()) + { + List logs = await GetFamilyWarehouseLogs(familyId); + logs.Add(logEntryDto); + if (logs.Count <= MaximumAmountOfLogs) + { + return; + } + + logs.RemoveRange(0, logs.Count - MaximumAmountOfLogs); + } + + await _messagePublisher.PublishAsync(new FamilyWarehouseLogAddMessage + { + FamilyId = familyId, + LogToAdd = logEntryDto + }); + } + + public async Task> GetLogs(long familyId) + { + AsyncReaderWriterLock familyLock = GetFamilyLock(familyId); + + using (await familyLock.ReaderLockAsync()) + { + return await GetFamilyWarehouseLogs(familyId); + } + } + + public async Task FlushLogSaves() + { + if (_familiesRequiringSave.Count < 1) + { + return; + } + + await _semaphoreSlim.WaitAsync(); + try + { + List unsavedFamilies = new(); + foreach (long familyId in _familiesRequiringSave) + { + List logs = await GetLogs(familyId); + try + { + int countSavedLogs = await _familyWarehouseLogDao.SaveAsync(familyId, logs); + Log.Warn($"[FAMILY_WAREHOUSE_MANAGER][FLUSH_SAVES][FAMILY_ID: {familyId.ToString()}] Saved {countSavedLogs.ToString()} warehouseLogs"); + } + catch (Exception e) + { + Log.Error($"[FAMILY_WAREHOUSE_MANAGER][FLUSH_SAVES][FAMILY_ID: {familyId.ToString()}] Error while trying to save {logs.Count.ToString()} warehouseLogs. Re-queueing. ", e); + unsavedFamilies.Add(familyId); + } + } + + _familiesRequiringSave.Clear(); + + foreach (long unsavedFamilyId in unsavedFamilies) + { + _familiesRequiringSave.Add(unsavedFamilyId); + } + } + finally + { + _semaphoreSlim.Release(); + } + } + } +} \ No newline at end of file diff --git a/srcs/FamilyServer/Managers/FamilyWarehouseManager.cs b/srcs/FamilyServer/Managers/FamilyWarehouseManager.cs new file mode 100644 index 0000000..92e2f60 --- /dev/null +++ b/srcs/FamilyServer/Managers/FamilyWarehouseManager.cs @@ -0,0 +1,530 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Mapster; +using PhoenixLib.Caching; +using PhoenixLib.Logging; +using WingsAPI.Communication.Families; +using WingsAPI.Data.Families; +using WingsAPI.Game.Extensions.Families; +using WingsAPI.Packets.Enums.Families; +using WingsEmu.Core.Extensions; +using WingsEmu.DTOs.Items; + +namespace FamilyServer.Managers +{ + public class FamilyWarehouseManager : IFamilyWarehouseManager + { + private static readonly TimeSpan LifeTime = TimeSpan.FromMinutes(Convert.ToUInt32(Environment.GetEnvironmentVariable(EnvironmentConsts.FamilyServerSaveIntervalMinutes) ?? "5") * 3); + + private readonly ILongKeyCachedRepository> _cachedFamilyItems; + private readonly SemaphoreSlim _familyLock = new(1, 1); + private readonly IFamilyService _familyService; + + private readonly IFamilyWarehouseItemDao _familyWarehouseItemDao; + private readonly FamilyWarehouseLogManager _familyWarehouseLogManager; + + private readonly Dictionary> _itemChanges = new(); + private readonly SemaphoreSlim _itemChangesSemaphore = new(1, 1); + + public FamilyWarehouseManager(IFamilyWarehouseItemDao familyWarehouseItemDao, ILongKeyCachedRepository> cachedFamilyItems, + IFamilyService familyService, + FamilyWarehouseLogManager familyWarehouseLogManager) + { + _familyWarehouseItemDao = familyWarehouseItemDao; + _cachedFamilyItems = cachedFamilyItems; + _familyService = familyService; + _familyWarehouseLogManager = familyWarehouseLogManager; + } + + public async Task> GetWarehouseLogs(long familyId, long? characterId = null) + { + if (!await CheckLogHistoryPermission(familyId, characterId)) + { + return null; + } + + return await _familyWarehouseLogManager.GetLogs(familyId); + } + + public async Task> GetWarehouse(long familyId, long? characterId = null) + { + if (!await CheckPutWithdrawPermission(familyId, characterId, FamilyWarehouseAuthorityType.Put)) + { + return null; + } + + await _familyLock.WaitAsync(); + try + { + return (await GetFamilyWarehouse(familyId))?.Values; + } + finally + { + _familyLock.Release(); + } + } + + public async Task GetWarehouseItem(long familyId, short slot, long? characterId = null) + { + if (!await CheckPutWithdrawPermission(familyId, characterId, FamilyWarehouseAuthorityType.Put)) + { + return null; + } + + await _familyLock.WaitAsync(); + try + { + return (await GetFamilyWarehouse(familyId))?.GetValueOrDefault(slot); + } + finally + { + _familyLock.Release(); + } + } + + public async Task AddWarehouseItem(FamilyWarehouseItemDto warehouseItemDtoToAdd, long? characterId = null, string characterName = null) + { + long familyId = warehouseItemDtoToAdd.FamilyId; + + if (warehouseItemDtoToAdd.ItemInstance.Amount is < 1 or > 999 || !await CheckSlot(familyId, warehouseItemDtoToAdd.Slot) || + !await CheckPutWithdrawPermission(familyId, characterId, FamilyWarehouseAuthorityType.Put)) + { + return new AddWarehouseItemResult + { + Success = false + }; + } + + await _familyLock.WaitAsync(); + try + { + Dictionary familyWarehouse = await GetFamilyWarehouse(familyId); + if (familyWarehouse == null) + { + return new AddWarehouseItemResult + { + Success = false + }; + } + + FamilyWarehouseItemDto alreadyExistentItem = familyWarehouse.GetValueOrDefault(warehouseItemDtoToAdd.Slot); + + if (alreadyExistentItem == null) + { + familyWarehouse[warehouseItemDtoToAdd.Slot] = warehouseItemDtoToAdd; + await SetItemChangeWithLock(warehouseItemDtoToAdd, false); + await AddLog(familyId, warehouseItemDtoToAdd.ItemInstance, FamilyWarehouseLogEntryType.List, characterId, characterName); + return new AddWarehouseItemResult + { + Success = true, + UpdatedItem = warehouseItemDtoToAdd + }; + } + + if (warehouseItemDtoToAdd.ItemInstance.ItemVNum != alreadyExistentItem.ItemInstance.ItemVNum) + { + return new AddWarehouseItemResult + { + Success = false + }; + } + + if (warehouseItemDtoToAdd.ItemInstance.Type != ItemInstanceType.NORMAL_ITEM || alreadyExistentItem.ItemInstance.Type != ItemInstanceType.NORMAL_ITEM + || warehouseItemDtoToAdd.ItemInstance.Amount + alreadyExistentItem.ItemInstance.Amount > 999) + { + return new AddWarehouseItemResult + { + Success = false + }; + } + + alreadyExistentItem.ItemInstance.Amount += warehouseItemDtoToAdd.ItemInstance.Amount; + + await SetItemChangeWithLock(alreadyExistentItem, false); + await AddLog(familyId, warehouseItemDtoToAdd.ItemInstance, FamilyWarehouseLogEntryType.List, characterId, characterName); + return new AddWarehouseItemResult + { + Success = true, + UpdatedItem = alreadyExistentItem + }; + } + finally + { + _familyLock.Release(); + } + } + + public async Task WithdrawWarehouseItem(FamilyWarehouseItemDto warehouseItemDtoToWithdraw, int amount, long? characterId = null, string characterName = null) + { + long familyId = warehouseItemDtoToWithdraw.FamilyId; + + if (amount is < 1 or > 999 || !await CheckPutWithdrawPermission(familyId, characterId, FamilyWarehouseAuthorityType.PutAndWithdraw)) + { + return new WithdrawWarehouseItemResult + { + Success = false + }; + } + + await _familyLock.WaitAsync(); + try + { + Dictionary familyWarehouse = await GetFamilyWarehouse(familyId); + + if (familyWarehouse == null) + { + return new WithdrawWarehouseItemResult + { + Success = false + }; + } + + FamilyWarehouseItemDto alreadyExistentItem = familyWarehouse.GetValueOrDefault(warehouseItemDtoToWithdraw.Slot); + if (alreadyExistentItem == null || alreadyExistentItem.ItemInstance.Amount < amount) + { + return new WithdrawWarehouseItemResult + { + Success = false + }; + } + + alreadyExistentItem.ItemInstance.Amount -= amount; + + bool toRemove = alreadyExistentItem.ItemInstance.Amount == 0; + if (toRemove) + { + familyWarehouse.Remove(warehouseItemDtoToWithdraw.Slot); + } + + await SetItemChangeWithLock(alreadyExistentItem, toRemove); + + ItemInstanceDTO newItemInstance = alreadyExistentItem.ItemInstance.Adapt(); + newItemInstance.Amount = amount; + + await AddLog(familyId, newItemInstance, FamilyWarehouseLogEntryType.Withdraw, characterId, characterName); + + return new WithdrawWarehouseItemResult + { + Success = true, + UpdatedItem = alreadyExistentItem.ItemInstance.Amount == 0 ? null : alreadyExistentItem, + WithdrawnItem = newItemInstance + }; + } + finally + { + _familyLock.Release(); + } + } + + public async Task MoveWarehouseItem(FamilyWarehouseItemDto warehouseItemDtoToMove, int amount, short newSlot, long? characterId = null) + { + long familyId = warehouseItemDtoToMove.FamilyId; + + if (amount is < 1 or > 999 || !await CheckSlot(familyId, warehouseItemDtoToMove.Slot, newSlot) || + !await CheckPutWithdrawPermission(familyId, characterId, FamilyWarehouseAuthorityType.Put)) + { + return new MoveWarehouseItemResult + { + Success = false + }; + } + + await _familyLock.WaitAsync(); + try + { + Dictionary familyWarehouse = await GetFamilyWarehouse(familyId); + if (familyWarehouse == null) + { + return new MoveWarehouseItemResult + { + Success = false + }; + } + + FamilyWarehouseItemDto toMoveItem = familyWarehouse.GetValueOrDefault(warehouseItemDtoToMove.Slot); + FamilyWarehouseItemDto toMergeItem = familyWarehouse.GetValueOrDefault(newSlot); + if (toMoveItem == null || toMoveItem.ItemInstance.Amount < amount) + { + return new MoveWarehouseItemResult + { + Success = false + }; + } + + if (toMergeItem == null) + { + if (amount == toMoveItem.ItemInstance.Amount) + { + familyWarehouse.Remove(toMoveItem.Slot); + await SetItemChangeWithLock(new FamilyWarehouseItemDto + { + FamilyId = familyId, + Slot = toMoveItem.Slot + }, true); + toMoveItem.Slot = newSlot; + familyWarehouse[toMoveItem.Slot] = toMoveItem; + await SetItemChangeWithLock(toMoveItem, false); + return new MoveWarehouseItemResult + { + Success = true, + OldItem = null, + NewItem = toMoveItem + }; + } + + toMoveItem.ItemInstance.Amount -= amount; + await SetItemChangeWithLock(toMoveItem, false); + var newItem = new FamilyWarehouseItemDto + { + FamilyId = familyId, + ItemInstance = toMoveItem.ItemInstance.Adapt(), + Slot = newSlot + }; + newItem.ItemInstance.Amount = amount; + familyWarehouse[newItem.Slot] = newItem; + await SetItemChangeWithLock(newItem, false); + return new MoveWarehouseItemResult + { + Success = true, + OldItem = toMoveItem, + NewItem = newItem + }; + } + + if (toMoveItem.ItemInstance.ItemVNum != toMergeItem.ItemInstance.ItemVNum) + { + toMergeItem.Slot = toMoveItem.Slot; + toMoveItem.Slot = newSlot; + familyWarehouse[toMoveItem.Slot] = toMoveItem; + await SetItemChangeWithLock(toMoveItem, false); + familyWarehouse[toMergeItem.Slot] = toMergeItem; + await SetItemChangeWithLock(toMergeItem, false); + return new MoveWarehouseItemResult + { + Success = true, + OldItem = toMergeItem, + NewItem = toMoveItem + }; + } + + if (toMoveItem.ItemInstance.Type != ItemInstanceType.NORMAL_ITEM || toMergeItem.ItemInstance.Type != ItemInstanceType.NORMAL_ITEM || amount + toMergeItem.ItemInstance.Amount > 999) + { + return new MoveWarehouseItemResult + { + Success = false + }; + } + + toMoveItem.ItemInstance.Amount -= amount; + toMergeItem.ItemInstance.Amount += amount; + + bool toRemove = toMoveItem.ItemInstance.Amount == 0; + if (toRemove) + { + familyWarehouse.Remove(toMoveItem.Slot); + } + + await SetItemChangeWithLock(toMoveItem, toRemove); + await SetItemChangeWithLock(toMergeItem, false); + + return new MoveWarehouseItemResult + { + Success = true, + OldItem = toRemove ? null : toMoveItem, + NewItem = toMergeItem + }; + } + finally + { + _familyLock.Release(); + } + } + + public async Task FlushWarehouseSaves() + { + if (_itemChanges.Count < 1) + { + return; + } + + await _itemChangesSemaphore.WaitAsync(); + try + { + List<(FamilyWarehouseItemDto dto, bool remove)> unsavedChanges = new(); + + var globalWatch = Stopwatch.StartNew(); + foreach ((long familyId, Dictionary warehouseChanges) in _itemChanges) + { + List itemsToSave = new(); + List itemsToRemove = new(); + + foreach ((short _, (FamilyWarehouseItemDto dto, bool remove)) in warehouseChanges) + { + (remove ? itemsToRemove : itemsToSave).Add(dto); + } + + if (itemsToSave.Count > 0) + { + try + { + int countSavedItems = await _familyWarehouseItemDao.SaveAsync(itemsToSave); + Log.Warn($"[FAMILY_WAREHOUSE_MANAGER][FLUSH_SAVES][FAMILY_ID: {familyId.ToString()}] Saved {countSavedItems.ToString()} warehouseItems"); + } + catch (Exception e) + { + Log.Error( + $"[FAMILY_WAREHOUSE_MANAGER][FLUSH_SAVES][FAMILY_ID: {familyId.ToString()}] Error while trying to save {itemsToSave.Count.ToString()} warehouseItems. Re-queueing. ", + e); + unsavedChanges.AddRange(itemsToSave.Select(x => (x, false))); + } + } + + if (itemsToRemove.Count < 1) + { + continue; + } + + try + { + await _familyWarehouseItemDao.DeleteAsync(itemsToRemove); + Log.Warn($"[FAMILY_WAREHOUSE_MANAGER][FLUSH_SAVES][FAMILY_ID: {familyId.ToString()}] Removed (at maximum) {itemsToRemove.Count.ToString()} warehouseItems"); + } + catch (Exception e) + { + Log.Error( + $"[FAMILY_WAREHOUSE_MANAGER][FLUSH_SAVES][FAMILY_ID: {familyId.ToString()}] Error while trying to remove {itemsToRemove.Count.ToString()} warehouseItems. Re-queueing. ", + e); + unsavedChanges.AddRange(itemsToRemove.Select(x => (x, true))); + } + } + + globalWatch.Stop(); + Log.Debug($"[FAMILY_WAREHOUSE_MANAGER][FLUSH_SAVES] Saving all warehouses took {globalWatch.ElapsedMilliseconds.ToString()}ms"); + + _itemChanges.Clear(); + + foreach ((FamilyWarehouseItemDto dto, bool remove) in unsavedChanges) + { + SetItemChange(dto, remove); + } + } + finally + { + _itemChangesSemaphore.Release(); + } + + await _familyWarehouseLogManager.FlushLogSaves(); + } + + private async Task CheckSlot(long familyId, short slot, short? slot2 = null) + { + FamilyIdResponse family = await _familyService.GetFamilyByIdAsync(new FamilyIdRequest + { + FamilyId = familyId + }); + + if (family == null) + { + return false; + } + + short warehouseCapacity = family.Family.Upgrades.UpgradeValues.GetValueOrDefault(FamilyUpgradeType.INCREASE_FAMILY_WAREHOUSE); + if (slot < 0 || warehouseCapacity <= slot) + { + return false; + } + + if (slot2 < 0 || warehouseCapacity <= slot2) + { + return false; + } + + return true; + } + + private async Task CheckPutWithdrawPermission(long familyId, long? characterId, FamilyWarehouseAuthorityType authorityRequested) + { + if (characterId == null) + { + return true; + } + + FamilyIdResponse family = await _familyService.GetFamilyByIdAsync(new FamilyIdRequest + { + FamilyId = familyId + }); + + FamilyMembershipDto member = family?.Members.Find(x => x.CharacterId == characterId); + + return member != null && member.CheckPutWithdrawPermission(family.Family, authorityRequested); + } + + private async Task CheckLogHistoryPermission(long familyId, long? characterId) + { + if (characterId == null) + { + return true; + } + + FamilyIdResponse family = await _familyService.GetFamilyByIdAsync(new FamilyIdRequest + { + FamilyId = familyId + }); + + FamilyMembershipDto member = family?.Members.Find(x => x.CharacterId == characterId); + + return member != null && member.CheckLogHistoryPermission(family.Family); + } + + private async Task SetItemChangeWithLock(FamilyWarehouseItemDto dto, bool remove) + { + await _itemChangesSemaphore.WaitAsync(); + try + { + SetItemChange(dto, remove); + } + finally + { + _itemChangesSemaphore.Release(); + } + } + + /// + /// Not to be used outside SemaphoreSlim + /// + private void SetItemChange(FamilyWarehouseItemDto dto, bool remove) + { + _itemChanges.GetOrSetDefault(dto.FamilyId, new Dictionary())[dto.Slot] = (dto, remove); + } + + private async Task> GetFamilyWarehouse(long familyId) + { + Dictionary cachedItems = _cachedFamilyItems.Get(familyId); + if (cachedItems != null) + { + return cachedItems; + } + + cachedItems = (await _familyWarehouseItemDao.GetByFamilyIdAsync(familyId))?.ToDictionary(x => x.Slot); + _cachedFamilyItems.Set(familyId, cachedItems ?? new Dictionary(), LifeTime); + return cachedItems; + } + + private async Task AddLog(long familyId, ItemInstanceDTO item, FamilyWarehouseLogEntryType logEntryType, long? characterId, string characterName) + { + await _familyWarehouseLogManager.AddLog(familyId, new FamilyWarehouseLogEntryDto + { + CharacterId = characterId ?? -1, + CharacterName = characterName, + DateOfLog = DateTime.UtcNow, + Type = logEntryType, + ItemVnum = item.ItemVNum, + Amount = item.Amount + }); + } + } +} \ No newline at end of file diff --git a/srcs/FamilyServer/Managers/IFamilyWarehouseManager.cs b/srcs/FamilyServer/Managers/IFamilyWarehouseManager.cs new file mode 100644 index 0000000..681d860 --- /dev/null +++ b/srcs/FamilyServer/Managers/IFamilyWarehouseManager.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using WingsAPI.Data.Families; + +namespace FamilyServer.Managers +{ + public interface IFamilyWarehouseManager + { + public Task> GetWarehouseLogs(long familyId, long? characterId = null); + public Task> GetWarehouse(long familyId, long? characterId = null); + public Task GetWarehouseItem(long familyId, short slot, long? characterId = null); + public Task AddWarehouseItem(FamilyWarehouseItemDto warehouseItemDtoToAdd, long? characterId = null, string characterName = null); + public Task WithdrawWarehouseItem(FamilyWarehouseItemDto warehouseItemDtoToWithdraw, int amount, long? characterId = null, string characterName = null); + public Task MoveWarehouseItem(FamilyWarehouseItemDto warehouseItemDtoToMove, int amount, short newSlot, long? characterId = null); + public Task FlushWarehouseSaves(); + } +} \ No newline at end of file diff --git a/srcs/FamilyServer/Managers/MoveWarehouseItemResult.cs b/srcs/FamilyServer/Managers/MoveWarehouseItemResult.cs new file mode 100644 index 0000000..239775f --- /dev/null +++ b/srcs/FamilyServer/Managers/MoveWarehouseItemResult.cs @@ -0,0 +1,13 @@ +using WingsAPI.Data.Families; + +namespace FamilyServer.Managers +{ + public class MoveWarehouseItemResult + { + public bool Success { get; init; } + + public FamilyWarehouseItemDto OldItem { get; init; } + + public FamilyWarehouseItemDto NewItem { get; init; } + } +} \ No newline at end of file diff --git a/srcs/FamilyServer/Managers/WithdrawWarehouseItemResult.cs b/srcs/FamilyServer/Managers/WithdrawWarehouseItemResult.cs new file mode 100644 index 0000000..2c1f78e --- /dev/null +++ b/srcs/FamilyServer/Managers/WithdrawWarehouseItemResult.cs @@ -0,0 +1,14 @@ +using WingsAPI.Data.Families; +using WingsEmu.DTOs.Items; + +namespace FamilyServer.Managers +{ + public class WithdrawWarehouseItemResult + { + public bool Success { get; init; } + + public FamilyWarehouseItemDto UpdatedItem { get; init; } + + public ItemInstanceDTO WithdrawnItem { get; init; } + } +} \ No newline at end of file diff --git a/srcs/FamilyServer/Program.cs b/srcs/FamilyServer/Program.cs new file mode 100644 index 0000000..a4de00d --- /dev/null +++ b/srcs/FamilyServer/Program.cs @@ -0,0 +1,87 @@ +using System; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using FamilyServer.Managers; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using PhoenixLib.Logging; +using PhoenixLib.ServiceBus.MQTT; +using Plugin.Database.Mapping; +using ProtoBuf.Grpc.Client; + +namespace FamilyServer +{ + public class Program + { + public static async Task Main(string[] args) + { + PrintHeader(); + NonGameMappingRules.InitializeMapping(); + GrpcClientFactory.AllowUnencryptedHttp2 = true; + using var stopService = new DockerGracefulStopService(); + using IHost host = CreateHostBuilder(args).Build(); + { + Log.Warn("Starting host..."); + await host.StartAsync(); + + IMessagingService messagingService = host.Services.GetRequiredService(); + await messagingService.StartAsync(); + + IFamilyWarehouseManager familyWarehouseManager = host.Services.GetRequiredService(); + + Log.Info("FamilyServer started"); + + IServiceProvider services = host.Services; + + await host.WaitForShutdownAsync(stopService.CancellationToken); + await messagingService.DisposeAsync(); + await familyWarehouseManager.FlushWarehouseSaves(); + } + } + + private static void PrintHeader() + { + const string text = @" +██╗ ██╗██╗███╗ ██╗ ██████╗ ███████╗███████╗███╗ ███╗██╗ ██╗ +██║ ██║██║████╗ ██║██╔════╝ ██╔════╝██╔════╝████╗ ████║██║ ██║ +██║ █╗ ██║██║██╔██╗ ██║██║ ███╗███████╗█████╗ ██╔████╔██║██║ ██║ +██║███╗██║██║██║╚██╗██║██║ ██║╚════██║██╔══╝ ██║╚██╔╝██║██║ ██║ +╚███╔███╔╝██║██║ ╚████║╚██████╔╝███████║███████╗██║ ╚═╝ ██║╚██████╔╝ + ╚══╝╚══╝ ╚═╝╚═╝ ╚═══╝ ╚═════╝ ╚══════╝╚══════╝╚═╝ ╚═╝ ╚═════╝ + +███████╗ █████╗ ███╗ ███╗██╗██╗ ██╗ ██╗ ███████╗███████╗██████╗ ██╗ ██╗███████╗██████╗ +██╔════╝██╔══██╗████╗ ████║██║██║ ╚██╗ ██╔╝ ██╔════╝██╔════╝██╔══██╗██║ ██║██╔════╝██╔══██╗ +█████╗ ███████║██╔████╔██║██║██║ ╚████╔╝█████╗███████╗█████╗ ██████╔╝██║ ██║█████╗ ██████╔╝ +██╔══╝ ██╔══██║██║╚██╔╝██║██║██║ ╚██╔╝ ╚════╝╚════██║██╔══╝ ██╔══██╗╚██╗ ██╔╝██╔══╝ ██╔══██╗ +██║ ██║ ██║██║ ╚═╝ ██║██║███████╗██║ ███████║███████╗██║ ██║ ╚████╔╝ ███████╗██║ ██║ +╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚══════╝╚═╝ ╚══════╝╚══════╝╚═╝ ╚═╝ ╚═══╝ ╚══════╝╚═╝ ╚═╝ +"; + string separator = new('=', Console.WindowWidth); + string logo = text.Split('\n').Select(s => string.Format("{0," + (Console.WindowWidth / 2 + s.Length / 2) + "}\n", s)) + .Aggregate("", (current, i) => current + i); + + Console.ForegroundColor = ConsoleColor.Cyan; + Console.WriteLine(separator + logo + $"Version: {Assembly.GetExecutingAssembly().GetName().Version}\n" + separator); + Console.ForegroundColor = ConsoleColor.White; + } + + // Additional configuration is required to successfully run gRPC on macOS. + // For instructions on how to configure Kestrel and gRPC clients on macOS, visit https://go.microsoft.com/fwlink/?linkid=2099682 + private static IHostBuilder CreateHostBuilder(string[] args) + { + IHostBuilder host = Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.ConfigureKestrel(s => + { + s.ListenAnyIP(short.Parse(Environment.GetEnvironmentVariable("FAMILY_SERVER_PORT") ?? "26666"), options => { options.Protocols = HttpProtocols.Http2; }); + }); + webBuilder.UseStartup(); + }); + return host; + } + } +} \ No newline at end of file diff --git a/srcs/FamilyServer/Services/FamilyInvitationService.cs b/srcs/FamilyServer/Services/FamilyInvitationService.cs new file mode 100644 index 0000000..18adba0 --- /dev/null +++ b/srcs/FamilyServer/Services/FamilyInvitationService.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using PhoenixLib.Logging; +using WingsAPI.Communication; +using WingsAPI.Communication.Families; +using WingsEmu.Core.Extensions; + +namespace FamilyServer.Services +{ + public class FamilyInvitationService : IFamilyInvitationService + { + private readonly Dictionary> _invitations = new(); + + public async ValueTask SaveFamilyInvitationAsync(FamilyInvitationSaveRequest request) + { + FamilyInvitation senderInvitation = request.Invitation; + + if (!_invitations.TryGetValue(senderInvitation.SenderId, out List invitations)) + { + invitations = new List(); + _invitations[senderInvitation.SenderId] = invitations; + } + + invitations.Add(senderInvitation); + Log.Info($"Family invitation added: sender: {senderInvitation.SenderId} target: {senderInvitation.TargetId}"); + return new EmptyResponse(); + } + + public async ValueTask ContainsFamilyInvitationAsync(FamilyInvitationRequest request) + { + long senderId = request.SenderId; + long targetId = request.TargetId; + + if (!_invitations.ContainsKey(senderId)) + { + Log.Debug($"[BOOL] Family invitation not found! Sender: {senderId} Target: {targetId}"); + return new FamilyInvitationContainsResponse + { + IsContains = false + }; + } + + List invite = _invitations.GetOrDefault(senderId); + Log.Debug("[BOOL] Family invitations found!"); + Log.Debug($"[BOOL] Family invitation contain target: {targetId} - {invite.All(x => x.TargetId == targetId)}"); + return new FamilyInvitationContainsResponse + { + IsContains = invite.All(x => x.TargetId == targetId) + }; + } + + public async ValueTask GetFamilyInvitationAsync(FamilyInvitationRequest request) => new() + { + Invitation = _invitations.GetOrDefault(request.SenderId)?.FirstOrDefault(x => x.TargetId == request.TargetId) + }; + + public async ValueTask RemoveFamilyInvitationAsync(FamilyInvitationRemoveRequest request) + { + _invitations.Remove(request.SenderId); + return new EmptyResponse(); + } + } +} \ No newline at end of file diff --git a/srcs/FamilyServer/Services/FamilyService.cs b/srcs/FamilyServer/Services/FamilyService.cs new file mode 100644 index 0000000..dab1fdf --- /dev/null +++ b/srcs/FamilyServer/Services/FamilyService.cs @@ -0,0 +1,650 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using FamilyServer.Logs; +using FamilyServer.Managers; +using Foundatio.AsyncEx; +using PhoenixLib.DAL.Redis.Locks; +using PhoenixLib.ServiceBus; +using Plugin.FamilyImpl.Messages; +using WingsAPI.Communication; +using WingsAPI.Communication.Families; +using WingsAPI.Data.Families; +using WingsAPI.Packets.Enums.Families; +using WingsEmu.Game.Families; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Families; + +namespace FamilyServer.Services +{ + public class FamilyService : IFamilyService + { + private readonly IExpirableLockService _expirableLockService; + private readonly FamilyLogManager _familyLogManager; + private readonly FamilyManager _familyManager; + private readonly FamilyMembershipManager _familyMembershipManager; + private readonly IMessagePublisher _messagePublisherChangedFaction; + private readonly IMessagePublisher _messagePublisherCreated; + private readonly IMessagePublisher _messagePublisherDisbandedFamily; + private readonly IMessagePublisher _messagePublisherFamilyUpdate; + private readonly IMessagePublisher _messagePublisherMemberAdded; + private readonly IMessagePublisher _messagePublisherMemberRemoved; + private readonly IMessagePublisher _messagePublisherMemberUpdate; + private readonly AsyncReaderWriterLock _readerWriter = new(); + + public FamilyService(FamilyManager familyManager, IMessagePublisher messagePublisherMemberAdded, IMessagePublisher messagePublisherCreated, + IMessagePublisher messagePublisherMemberUpdate, IMessagePublisher messagePublisherMemberRemoved, + FamilyLogManager familyLogManager, FamilyMembershipManager familyMembershipManager, IMessagePublisher messagePublisherFamilyUpdate, + IMessagePublisher messagePublisherChangedFaction, IMessagePublisher messagePublisherDisbandedFamily, + IExpirableLockService expirableLockService) + { + _familyManager = familyManager; + _messagePublisherMemberAdded = messagePublisherMemberAdded; + _messagePublisherCreated = messagePublisherCreated; + _messagePublisherMemberUpdate = messagePublisherMemberUpdate; + _messagePublisherMemberRemoved = messagePublisherMemberRemoved; + _familyLogManager = familyLogManager; + _familyMembershipManager = familyMembershipManager; + _messagePublisherFamilyUpdate = messagePublisherFamilyUpdate; + _messagePublisherChangedFaction = messagePublisherChangedFaction; + _messagePublisherDisbandedFamily = messagePublisherDisbandedFamily; + _expirableLockService = expirableLockService; + } + + public async ValueTask CreateFamilyAsync(FamilyCreateRequest req) + { + using IDisposable writerLock = await _readerWriter.WriterLockAsync(); + { + if (await _familyManager.GetFamilyByNameAsync(req.Name) != null) + { + return new FamilyCreateResponse + { + Status = FamilyCreateResponseType.NAME_ALREADY_TAKEN + }; + } + + FamilyDTO familyDto = await _familyManager.AddFamilyAsync(new FamilyDTO + { + Name = req.Name, + Level = req.Level, + Faction = req.Faction, + Upgrades = new FamilyUpgradeDto(), + Missions = CreateBasicFamilyMissions(), + Achievements = CreateBasicAchievements() + }); + + foreach (FamilyMembershipDto member in req.Members) + { + member.FamilyId = familyDto.Id; + } + + await _familyMembershipManager.AddFamilyMembershipsAsync(req.Members); + + await _messagePublisherCreated.PublishAsync(new FamilyCreatedMessage + { + FamilyName = req.Name + }); + + return new FamilyCreateResponse + { + Status = FamilyCreateResponseType.SUCCESS, + Family = familyDto + }; + } + } + + public async ValueTask DisbandFamilyAsync(FamilyDisbandRequest request) + { + IReadOnlyCollection memberships = await _familyMembershipManager.GetFamilyMembershipsByFamilyIdAsync(request.FamilyId); + if (memberships == null) + { + return new BasicRpcResponse + { + ResponseType = RpcResponseType.UNKNOWN_ERROR + }; + } + + foreach (FamilyMembershipDto membership in memberships) + { + await _familyMembershipManager.RemoveFamilyMembershipByCharAndFamIdAsync(membership); + } + + await _familyManager.RemoveFamilyByIdAsync(request.FamilyId); + await _messagePublisherDisbandedFamily.PublishAsync(new FamilyDisbandMessage + { + FamilyId = request.FamilyId + }); + + return new BasicRpcResponse + { + ResponseType = RpcResponseType.SUCCESS + }; + } + + public async ValueTask ChangeAuthorityByIdAsync(FamilyChangeAuthorityRequest request) + { + if (request.FamilyMembers.Count <= 0) + { + return new EmptyResponse(); + } + + var list = new List(); + + foreach (FamilyChangeContainer container in request.FamilyMembers) + { + FamilyMembershipDto membership = await _familyMembershipManager.GetFamilyMembershipByCharacterIdAsync(container.CharacterId); + if (membership == null) + { + continue; + } + + membership.Authority = container.RequestedFamilyAuthority; + list.Add(membership); + } + + await _familyMembershipManager.SaveFamilyMembershipsAsync(list); + + await _messagePublisherMemberUpdate.PublishAsync(new FamilyMemberUpdateMessage + { + UpdatedMembers = list, + ChangedInfoMemberUpdate = ChangedInfoMemberUpdate.Authority + }); + return new EmptyResponse(); + } + + public async ValueTask ChangeFactionByIdAsync(FamilyChangeFactionRequest request) + { + FactionType newFaction = request.NewFaction; + FamilyDTO familyDto = await _familyManager.GetFamilyByFamilyIdAsync(request.FamilyId); + if (familyDto == null) + { + return new FamilyChangeFactionResponse + { + Status = FamilyChangeFactionResponseType.GENERIC_ERROR + }; + } + + if (familyDto.Faction == (byte)newFaction) + { + return new FamilyChangeFactionResponse + { + Status = FamilyChangeFactionResponseType.ALREADY_THAT_FACTION + }; + } + + if (!await _expirableLockService.TryAddTemporaryLockAsync($"game:locks:family:{familyDto.Id}:change-faction", DateTime.UtcNow.Date.AddDays(1))) + { + return new FamilyChangeFactionResponse + { + Status = FamilyChangeFactionResponseType.UNDER_COOLDOWN + }; + } + + familyDto.Faction = (byte)newFaction; + await _familyManager.AddFamilyAsync(familyDto); + + await _messagePublisherChangedFaction.PublishAsync(new FamilyChangeFactionMessage + { + FamilyId = request.FamilyId, + NewFaction = newFaction + }); + + return new FamilyChangeFactionResponse + { + Status = FamilyChangeFactionResponseType.SUCCESS + }; + } + + public async ValueTask ChangeTitleByIdAsync(FamilyChangeTitleRequest request) + { + FamilyMembershipDto membership = await _familyMembershipManager.GetFamilyMembershipByCharacterIdAsync(request.CharacterId); + if (membership == null) + { + return new EmptyResponse(); + } + + membership.Title = request.RequestedFamilyTitle; + + await _familyMembershipManager.SaveFamilyMembershipAsync(membership); + + await _messagePublisherMemberUpdate.PublishAsync(new FamilyMemberUpdateMessage + { + UpdatedMembers = new List { membership }, + ChangedInfoMemberUpdate = ChangedInfoMemberUpdate.Authority + }); + return new EmptyResponse(); + } + + public async ValueTask TryAddFamilyUpgrade(FamilyUpgradeRequest request) + { + FamilyDTO family = await _familyManager.GetFamilyByFamilyIdAsync(request.FamilyId); + + if (family == null) + { + return new FamilyUpgradeResponse { ResponseType = FamilyUpgradeAddResponseType.GENERIC_SERVER_ERROR }; + } + + FamilyUpgradeType familyUpgrade = request.FamilyUpgradeType; + short value = request.Value; + + family.Upgrades ??= new FamilyUpgradeDto(); + + family.Upgrades.UpgradesBought ??= new HashSet(); + + if (family.Upgrades.UpgradesBought.Contains(request.UpgradeId)) + { + return new FamilyUpgradeResponse { ResponseType = FamilyUpgradeAddResponseType.UPGRADE_ALREADY_UNLOCKED }; + } + + family.Upgrades.UpgradesBought.Add(request.UpgradeId); + + family.Upgrades.UpgradeValues ??= new Dictionary(); + family.Upgrades.UpgradeValues[familyUpgrade] = value; + + FamilyDTO familyDto = await _familyManager.SaveFamilyAsync(family); + + await _messagePublisherFamilyUpdate.PublishAsync(new FamilyUpdateMessage + { + ChangedInfoFamilyUpdate = ChangedInfoFamilyUpdate.Upgrades, + Families = new[] { familyDto } + }); + return new FamilyUpgradeResponse { ResponseType = FamilyUpgradeAddResponseType.SUCCESS }; + } + + public async ValueTask AddMemberToFamilyAsync(FamilyAddMemberRequest request) + { + await _familyMembershipManager.AddFamilyMembershipAsync(request.Member); + await _messagePublisherMemberAdded.PublishAsync(new FamilyMemberAddedMessage + { + AddedMember = request.Member, + Nickname = request.Nickname, + SenderId = request.SenderId + }); + return new EmptyResponse(); + } + + public async ValueTask MemberDisconnectedAsync(FamilyMemberDisconnectedRequest request) + { + FamilyMembershipDto member = await _familyMembershipManager.GetFamilyMembershipByCharacterIdAsync(request.CharacterId); + if (member == null) + { + return new EmptyResponse(); + } + + member.LastOnlineDate = request.DisconnectionTime; + await _familyMembershipManager.SaveFamilyMembershipAsync(member); + return new EmptyResponse(); + } + + public async ValueTask RemoveMemberToFamilyAsync(FamilyRemoveMemberRequest request) + { + FamilyMembershipDto membership = await _familyMembershipManager.GetFamilyMembershipByCharacterIdAsync(request.CharacterId); + if (membership == null || membership.FamilyId != request.FamilyId) + { + return new EmptyResponse(); + } + + await _familyMembershipManager.RemoveFamilyMembershipByCharAndFamIdAsync(membership); + await _messagePublisherMemberRemoved.PublishAsync(new FamilyMemberRemovedMessage + { + CharacterId = request.CharacterId, + FamilyId = request.FamilyId + }); + return new EmptyResponse(); + } + + public async ValueTask RemoveMemberByCharIdAsync(FamilyRemoveMemberByCharIdRequest request) + { + FamilyMembershipDto membership = await _familyMembershipManager.GetFamilyMembershipByCharacterIdAsync(request.CharacterId); + if (membership == null) + { + return new BasicRpcResponse + { + ResponseType = RpcResponseType.GENERIC_SERVER_ERROR + }; + } + + await _familyMembershipManager.RemoveFamilyMembershipByCharAndFamIdAsync(membership); + await _messagePublisherMemberRemoved.PublishAsync(new FamilyMemberRemovedMessage + { + CharacterId = request.CharacterId, + FamilyId = membership.FamilyId + }); + + return new BasicRpcResponse + { + ResponseType = RpcResponseType.SUCCESS + }; + } + + public async ValueTask GetFamilyByIdAsync(FamilyIdRequest req) + { + FamilyDTO family = await _familyManager.GetFamilyByFamilyIdAsync(req.FamilyId); + List members = await _familyMembershipManager.GetFamilyMembershipsByFamilyIdAsync(req.FamilyId); + List logs = await _familyLogManager.GetFamilyLogsByFamilyId(req.FamilyId); + + await RemoveLevelAchievements(family); + + DateTime resetDate = DateTime.UtcNow.Date; + if (family.Missions?.Missions != null) + { + foreach (FamilyMissionDto mission in family.Missions.Missions.Values) + { + // progressType => 1 = Completed + // progressType => 2 = InProgress + bool hasCompleted = mission.CompletionDate.HasValue; + + if (!hasCompleted) + { + continue; + } + + if (mission.CompletionDate.Value.Date >= resetDate) + { + continue; + } + + mission.CompletionDate = null; + mission.Count = 0; + } + } + + return new FamilyIdResponse + { + Family = family, + Members = members, + Logs = logs + }; + } + + public async ValueTask GetFamilyMembersByFamilyId(FamilyIdRequest req) + { + List members = await _familyMembershipManager.GetFamilyMembershipsByFamilyIdAsync(req.FamilyId); + return new FamilyListMembersResponse + { + Members = members + }; + } + + public async ValueTask GetMembershipByCharacterIdAsync(MembershipRequest req) + { + FamilyMembershipDto membership = await _familyMembershipManager.GetFamilyMembershipByCharacterIdAsync(req.CharacterId); + return new MembershipResponse + { + Membership = membership + }; + } + + public async ValueTask CanPerformTodayMessageAsync(MembershipTodayRequest req) + { + long characterId = req.CharacterId; + string characterName = req.CharacterName; + FamilyMembershipDto familyMember = await _familyMembershipManager.GetFamilyMembershipByCharacterIdAsync(characterId); + if (familyMember == null) + { + return new MembershipTodayResponse + { + CanPerformAction = false + }; + } + + if (!await _expirableLockService.TryAddTemporaryLockAsync($"game:locks:family:{familyMember.FamilyId}:{characterId}:quote-of-the-day", DateTime.UtcNow.Date.AddDays(1))) + { + return new MembershipTodayResponse + { + CanPerformAction = false + }; + } + + return new MembershipTodayResponse + { + CanPerformAction = true + }; + } + + public async ValueTask UpdateFamilySettingsAsync(FamilySettingsRequest request) + { + FamilyDTO familyDto = await _familyManager.GetFamilyByFamilyIdAsync(request.FamilyId); + if (familyDto == null) + { + return new BasicRpcResponse + { + ResponseType = RpcResponseType.UNKNOWN_ERROR + }; + } + + byte value = request.Value; + + switch (request.Authority) + { + case FamilyAuthority.Keeper: + switch (request.FamilyActionType) + { + case FamilyActionType.SendInvite: + familyDto.AssistantCanInvite = value == 1; + break; + case FamilyActionType.Notice: + familyDto.AssistantCanNotice = value == 1; + break; + case FamilyActionType.FamilyShout: + familyDto.AssistantCanShout = value == 1; + break; + case FamilyActionType.FamilyWarehouseHistory: + familyDto.AssistantCanGetHistory = value == 1; + break; + case FamilyActionType.FamilyWarehouse: + if (!Enum.TryParse(value.ToString(), out FamilyWarehouseAuthorityType authorityType)) + { + return new BasicRpcResponse + { + ResponseType = RpcResponseType.UNKNOWN_ERROR + }; + } + + familyDto.AssistantWarehouseAuthorityType = authorityType; + break; + default: + return new BasicRpcResponse + { + ResponseType = RpcResponseType.UNKNOWN_ERROR + }; + } + + break; + + case FamilyAuthority.Member: + switch (request.FamilyActionType) + { + case FamilyActionType.SendInvite: // Member History + familyDto.MemberCanGetHistory = value == 1; + break; + case FamilyActionType.Notice: // Member Warehouse Authority + if (!Enum.TryParse(value.ToString(), out FamilyWarehouseAuthorityType authorityType)) + { + return new BasicRpcResponse + { + ResponseType = RpcResponseType.UNKNOWN_ERROR + }; + } + + familyDto.MemberWarehouseAuthorityType = authorityType; + break; + default: + return new BasicRpcResponse + { + ResponseType = RpcResponseType.UNKNOWN_ERROR + }; + } + + break; + default: + return new BasicRpcResponse + { + ResponseType = RpcResponseType.UNKNOWN_ERROR + }; + } + + FamilyDTO family = await _familyManager.SaveFamilyAsync(familyDto); + + await _messagePublisherFamilyUpdate.PublishAsync(new FamilyUpdateMessage + { + ChangedInfoFamilyUpdate = ChangedInfoFamilyUpdate.Settings, + Families = new[] { family } + }); + + return new BasicRpcResponse + { + ResponseType = RpcResponseType.SUCCESS + }; + } + + public async ValueTask ResetFamilyMissions() + { + DateTime resetDate = DateTime.UtcNow.Date; + List families = _familyManager.GetFamiliesInMemory(); + HashSet toUpdate = new(); + foreach (FamilyDTO family in families.Where(x => x.Missions?.Missions != null)) + { + foreach (FamilyMissionDto mission in family.Missions.Missions.Values) + { + // progressType => 1 = Completed + // progressType => 2 = InProgress + bool hasCompleted = mission.CompletionDate.HasValue; + + if (!hasCompleted) + { + continue; + } + + if (mission.CompletionDate.Value.Date >= resetDate) + { + continue; + } + + mission.CompletionDate = null; + mission.Count = 0; + + if (toUpdate.Contains(family)) + { + continue; + } + + toUpdate.Add(family); + } + } + + await _messagePublisherFamilyUpdate.PublishAsync(new FamilyUpdateMessage + { + Families = toUpdate, + ChangedInfoFamilyUpdate = ChangedInfoFamilyUpdate.AchievementsAndMissions + }); + + return new EmptyResponse(); + } + + private FamilyAchievementsDto CreateBasicAchievements() + { + FamilyAchievementsDto newAchievements = new() + { + Achievements = new Dictionary(), + Progress = new Dictionary() + }; + + Dictionary progress = newAchievements.Progress; + progress[(short)FamilyAchievementsVnum.FAMILY_LEVEL_2_UNLOCKED] = new FamilyAchievementProgressDto { Id = (short)FamilyAchievementsVnum.FAMILY_LEVEL_2_UNLOCKED }; + progress[(short)FamilyAchievementsVnum.ENTER_20_QUOTES_OF_THE_DAY] = new FamilyAchievementProgressDto { Id = (short)FamilyAchievementsVnum.ENTER_20_QUOTES_OF_THE_DAY }; + progress[(short)FamilyAchievementsVnum.DEFEAT_ANY_ACT4_DUNGEON_10_TIMES] = new FamilyAchievementProgressDto { Id = (short)FamilyAchievementsVnum.DEFEAT_ANY_ACT4_DUNGEON_10_TIMES }; + progress[(short)FamilyAchievementsVnum.DEFEAT_MORCOS_ACT4_DUNGEON_1_TIME] = new FamilyAchievementProgressDto { Id = (short)FamilyAchievementsVnum.DEFEAT_MORCOS_ACT4_DUNGEON_1_TIME }; + progress[(short)FamilyAchievementsVnum.DEFEAT_HATUS_ACT4_DUNGEON_1_TIME] = new FamilyAchievementProgressDto { Id = (short)FamilyAchievementsVnum.DEFEAT_HATUS_ACT4_DUNGEON_1_TIME }; + progress[(short)FamilyAchievementsVnum.DEFEAT_CALVINAS_ACT4_DUNGEON_1_TIME] = new FamilyAchievementProgressDto { Id = (short)FamilyAchievementsVnum.DEFEAT_CALVINAS_ACT4_DUNGEON_1_TIME }; + progress[(short)FamilyAchievementsVnum.DEFEAT_BERIOS_ACT4_DUNGEON_1_TIME] = new FamilyAchievementProgressDto { Id = (short)FamilyAchievementsVnum.DEFEAT_BERIOS_ACT4_DUNGEON_1_TIME }; + progress[(short)FamilyAchievementsVnum.COMPLETE_10_RAINBOW_BATTLE] = new FamilyAchievementProgressDto { Id = (short)FamilyAchievementsVnum.COMPLETE_10_RAINBOW_BATTLE }; + progress[(short)FamilyAchievementsVnum.WIN_5_RAINBOW_BATTLE] = new FamilyAchievementProgressDto { Id = (short)FamilyAchievementsVnum.WIN_5_RAINBOW_BATTLE }; + return newAchievements; + } + + private FamilyMissionsDto CreateBasicFamilyMissions() + { + FamilyMissionsDto newFamilyMissions = new() + { + Missions = new Dictionary() + }; + + Dictionary missions = newFamilyMissions.Missions; + missions[(int)FamilyMissionVnums.DAILY_DEFEAT_5_CUBY_RAID] = new FamilyMissionDto { Id = (int)FamilyMissionVnums.DAILY_DEFEAT_5_CUBY_RAID }; + missions[(int)FamilyMissionVnums.DAILY_DEFEAT_5_GINSENG_RAID] = new FamilyMissionDto { Id = (int)FamilyMissionVnums.DAILY_DEFEAT_5_GINSENG_RAID }; + missions[(int)FamilyMissionVnums.DAILY_DEFEAT_5_CASTRA_RAID] = new FamilyMissionDto { Id = (int)FamilyMissionVnums.DAILY_DEFEAT_5_CASTRA_RAID }; + missions[(int)FamilyMissionVnums.DAILY_DEFEAT_5_GIANT_SPIDER_RAID] = new FamilyMissionDto { Id = (int)FamilyMissionVnums.DAILY_DEFEAT_5_GIANT_SPIDER_RAID }; + missions[(int)FamilyMissionVnums.DAILY_DEFEAT_10_INSTANT_BATTLES] = new FamilyMissionDto { Id = (int)FamilyMissionVnums.DAILY_DEFEAT_10_INSTANT_BATTLES }; + return newFamilyMissions; + } + + private async Task RemoveLevelAchievements(FamilyDTO family) + { + if (family == null) + { + return; + } + + if (family.Level <= 1) + { + return; + } + + if (family.Achievements?.Achievements == null) + { + return; + } + + byte familyLevel = family.Level; + short secondLevelFamily = (short)FamilyAchievementsVnum.FAMILY_LEVEL_2_UNLOCKED - 1; + bool isFirst = true; + bool addNewProgress = true; + var toRemove = new List(); + for (int i = 0; i < 20; i++) + { + if (isFirst) + { + isFirst = false; + } + else + { + secondLevelFamily++; + } + + if (!family.Achievements.Achievements.ContainsKey(secondLevelFamily)) + { + continue; + } + + if (familyLevel > i) + { + continue; + } + + if (addNewProgress) + { + addNewProgress = false; + family.Achievements.Progress[secondLevelFamily] = new FamilyAchievementProgressDto + { + Id = secondLevelFamily + }; + } + + toRemove.Add(secondLevelFamily); + } + + foreach (int remove in toRemove) + { + family.Achievements.Achievements.Remove(remove); + } + + await _familyManager.SaveFamilyAsync(family); + } + } +} \ No newline at end of file diff --git a/srcs/FamilyServer/Services/FamilyWarehouseService.cs b/srcs/FamilyServer/Services/FamilyWarehouseService.cs new file mode 100644 index 0000000..62df9f0 --- /dev/null +++ b/srcs/FamilyServer/Services/FamilyWarehouseService.cs @@ -0,0 +1,128 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using FamilyServer.Managers; +using PhoenixLib.ServiceBus; +using Plugin.FamilyImpl.Messages; +using WingsAPI.Communication; +using WingsAPI.Communication.Families.Warehouse; +using WingsAPI.Data.Families; + +namespace FamilyServer.Services +{ + public class FamilyWarehouseService : IFamilyWarehouseService + { + private readonly IFamilyWarehouseManager _familyWarehouseManager; + private readonly IMessagePublisher _messagePublisher; + + public FamilyWarehouseService(IFamilyWarehouseManager familyWarehouseManager, IMessagePublisher messagePublisher) + { + _familyWarehouseManager = familyWarehouseManager; + _messagePublisher = messagePublisher; + } + + public async ValueTask GetLogs(FamilyWarehouseGetLogsRequest request) + { + IList logs = await _familyWarehouseManager.GetWarehouseLogs(request.FamilyId, request.CharacterId); + + return new FamilyWarehouseGetLogsResponse + { + ResponseType = logs == null ? RpcResponseType.GENERIC_SERVER_ERROR : RpcResponseType.SUCCESS, + Logs = logs + }; + } + + public async ValueTask GetItems(FamilyWarehouseGetItemsRequest request) + { + IEnumerable warehouseItemDtos = await _familyWarehouseManager.GetWarehouse(request.FamilyId, request.CharacterId); + + return new FamilyWarehouseGetItemsResponse + { + ResponseType = warehouseItemDtos == null ? RpcResponseType.GENERIC_SERVER_ERROR : RpcResponseType.SUCCESS, + Items = warehouseItemDtos + }; + } + + public async ValueTask GetItem(FamilyWarehouseGetItemRequest request) + { + FamilyWarehouseItemDto warehouseItemDto = await _familyWarehouseManager.GetWarehouseItem(request.FamilyId, request.Slot, request.CharacterId); + + return new FamilyWarehouseGetItemResponse + { + ResponseType = warehouseItemDto == null ? RpcResponseType.GENERIC_SERVER_ERROR : RpcResponseType.SUCCESS, + Item = warehouseItemDto + }; + } + + public async ValueTask AddItem(FamilyWarehouseAddItemRequest request) + { + AddWarehouseItemResult result = await _familyWarehouseManager.AddWarehouseItem(request.Item, request.CharacterId, request.CharacterName); + + if (result.Success) + { + await _messagePublisher.PublishAsync(new FamilyWarehouseItemUpdateMessage + { + FamilyId = request.Item.FamilyId, + UpdatedItems = new[] + { + (result.UpdatedItem, request.Item.Slot) + } + }); + } + + return new FamilyWarehouseAddItemResponse + { + ResponseType = result.Success ? RpcResponseType.SUCCESS : RpcResponseType.GENERIC_SERVER_ERROR, + Item = result.UpdatedItem + }; + } + + public async ValueTask WithdrawItem(FamilyWarehouseWithdrawItemRequest request) + { + WithdrawWarehouseItemResult result = await _familyWarehouseManager.WithdrawWarehouseItem(request.ItemToWithdraw, request.Amount, request.CharacterId, request.CharacterName); + + if (result.Success) + { + await _messagePublisher.PublishAsync(new FamilyWarehouseItemUpdateMessage + { + FamilyId = request.ItemToWithdraw.FamilyId, + UpdatedItems = new[] + { + (result.UpdatedItem, request.ItemToWithdraw.Slot) + } + }); + } + + return new FamilyWarehouseWithdrawItemResponse + { + ResponseType = result.Success ? RpcResponseType.SUCCESS : RpcResponseType.GENERIC_SERVER_ERROR, + UpdatedItem = result.UpdatedItem, + WithdrawnItem = result.WithdrawnItem + }; + } + + public async ValueTask MoveItem(FamilyWarehouseMoveItemRequest request) + { + MoveWarehouseItemResult result = await _familyWarehouseManager.MoveWarehouseItem(request.WarehouseItemDtoToMove, request.Amount, request.NewSlot, request.CharacterId); + + if (result.Success) + { + await _messagePublisher.PublishAsync(new FamilyWarehouseItemUpdateMessage + { + FamilyId = request.WarehouseItemDtoToMove.FamilyId, + UpdatedItems = new[] + { + (result.OldItem, request.WarehouseItemDtoToMove.Slot), + (result.NewItem, request.NewSlot) + } + }); + } + + return new FamilyWarehouseMoveItemResponse + { + ResponseType = result.Success ? RpcResponseType.SUCCESS : RpcResponseType.GENERIC_SERVER_ERROR, + OldItem = result.OldItem, + NewItem = result.NewItem + }; + } + } +} \ No newline at end of file diff --git a/srcs/FamilyServer/Startup.cs b/srcs/FamilyServer/Startup.cs new file mode 100644 index 0000000..334125b --- /dev/null +++ b/srcs/FamilyServer/Startup.cs @@ -0,0 +1,137 @@ +using FamilyServer.Achievements; +using FamilyServer.Consumers; +using FamilyServer.Logs; +using FamilyServer.Managers; +using FamilyServer.Services; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using PhoenixLib.Caching; +using PhoenixLib.Configuration; +using PhoenixLib.DAL.Redis; +using PhoenixLib.DAL.Redis.Locks; +using PhoenixLib.Events; +using PhoenixLib.Logging; +using PhoenixLib.ServiceBus.Extensions; +using Plugin.Database; +using Plugin.Database.Extensions; +using Plugin.Database.Families; +using Plugin.FamilyImpl.Achievements; +using Plugin.FamilyImpl.Messages; +using ProtoBuf.Grpc.Server; +using WingsAPI.Communication.Families; +using WingsAPI.Communication.Services.Messages; +using WingsAPI.Data.Families; +using WingsEmu.Game.Families.Configuration; +using WingsEmu.Health.Extensions; +using WingsEmu.Plugins.DistributedGameEvents.PlayerEvents; +using FamilyAchievementManager = FamilyServer.Achievements.FamilyAchievementManager; + +namespace FamilyServer +{ + public class Startup + { + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) + { + services.AddMqttConfigurationFromEnv(); + services.AddEventPipeline(); + services.AddEventHandlersInAssembly(); + services.AddMaintenanceMode(); + services.TryAddConnectionMultiplexerFromEnv(); + services.TryAddSingleton(); + services.AddPhoenixLogging(); + + + new DatabasePlugin().AddDependencies(services); + + services.TryAddSingleton(typeof(ILongKeyCachedRepository<>), typeof(InMemoryCacheRepository<>)); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + services.AddSingleton(); + services.AddHostedService(s => s.GetRequiredService()); + + services.AddSingleton(); + services.AddCodeFirstGrpc(config => + { + config.MaxReceiveMessageSize = null; + config.MaxSendMessageSize = null; + config.EnableDetailedErrors = true; + }); + + services.AddYamlConfigurationHelper(); + services.AddFileConfiguration(); + + services.TryAddSingleton(); + services.AddHostedService(s => s.GetRequiredService()); + + services.TryAddSingleton(); + services.AddHostedService(s => s.GetRequiredService()); + + services.AddMessageSubscriber(); + services.AddMessageSubscriber(); + services.AddMessageSubscriber(); + services.AddMessageSubscriber(); + services.AddMessageSubscriber(); + services.AddMessageSubscriber(); + services.AddMessageSubscriber(); + services.AddMessageSubscriber(); + + services.AddMessagePublisher(); + services.AddMessagePublisher(); + services.AddMessagePublisher(); + services.AddMessagePublisher(); + services.AddMessagePublisher(); + services.AddMessagePublisher(); + services.AddMessagePublisher(); + services.AddMessagePublisher(); + services.AddMessagePublisher(); + services.AddMessagePublisher(); + services.AddMessagePublisher(); + services.AddMessagePublisher(); + services.AddMessagePublisher(); + + // achievements + services.AddFileConfiguration(); + services.AddFileConfiguration("family_missions_configuration"); + services.AddMessageSubscriber(); + services.AddMessageSubscriber(); + services.AddMessagePublisher(); + services.TryAddSingleton(); + services.AddHostedService(s => s.GetRequiredService()); + + services.AddMessageSubscriber(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseRouting(); + app.UseZEfCoreExtensions(); + + app.UseEndpoints(endpoints => + { + endpoints.MapGrpcService(); + endpoints.MapGrpcService(); + endpoints.MapGrpcService(); + }); + } + } +} \ No newline at end of file diff --git a/srcs/GameChannel/Consumers/KickAccountConsumer.cs b/srcs/GameChannel/Consumers/KickAccountConsumer.cs new file mode 100644 index 0000000..cb267b6 --- /dev/null +++ b/srcs/GameChannel/Consumers/KickAccountConsumer.cs @@ -0,0 +1,23 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Logging; +using PhoenixLib.ServiceBus; +using WingsEmu.Game.Managers; +using WingsEmu.Plugins.DistributedGameEvents.PlayerEvents; + +namespace GameChannel.Consumers +{ + public class KickAccountConsumer : IMessageConsumer + { + private readonly ISessionManager _sessionManager; + + public KickAccountConsumer(ISessionManager sessionManager) => _sessionManager = sessionManager; + + public async Task HandleAsync(KickAccountMessage notification, CancellationToken token) + { + Log.Warn($"[NOTIF_KICK] {notification.AccountId} is supposed to be kicked"); + + await _sessionManager.KickAsync(notification.AccountId); + } + } +} \ No newline at end of file diff --git a/srcs/GameChannel/Consumers/PlayerConnectedChannelGameConsumer.cs b/srcs/GameChannel/Consumers/PlayerConnectedChannelGameConsumer.cs new file mode 100644 index 0000000..74bd36e --- /dev/null +++ b/srcs/GameChannel/Consumers/PlayerConnectedChannelGameConsumer.cs @@ -0,0 +1,46 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; +using WingsAPI.Communication.Player; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Plugins.DistributedGameEvents.PlayerEvents; + +namespace GameChannel.Consumers +{ + public class PlayerConnectedChannelGameConsumer : IMessageConsumer + { + private readonly IServerManager _serverManager; + private readonly ISessionManager _sessionManager; + + public PlayerConnectedChannelGameConsumer(ISessionManager sessionManager, IServerManager serverManager) + { + _sessionManager = sessionManager; + _serverManager = serverManager; + } + + public async Task HandleAsync(PlayerConnectedOnChannelMessage e, CancellationToken cancellation) + { + IClientSession session = _sessionManager.GetSessionByCharacterId(e.CharacterId); + if (session != null) + { + await session.NotifyStrangeBehavior(StrangeBehaviorSeverity.DANGER, $"Looks like {e.CharacterName} was connected from {e.ChannelId} while being on {_serverManager.ChannelId}"); + session.ForceDisconnect(); + return; + } + + _sessionManager.AddOnline(new ClusterCharacterInfo + { + Id = e.CharacterId, + Class = e.Class, + Gender = e.Gender, + Level = e.Level, + Name = e.CharacterName, + ChannelId = (byte?)e.ChannelId, + HeroLevel = e.HeroLevel, + HardwareId = e.HardwareId + }); + } + } +} \ No newline at end of file diff --git a/srcs/GameChannel/Consumers/PlayerDisconnectedChannelConsumer.cs b/srcs/GameChannel/Consumers/PlayerDisconnectedChannelConsumer.cs new file mode 100644 index 0000000..756169b --- /dev/null +++ b/srcs/GameChannel/Consumers/PlayerDisconnectedChannelConsumer.cs @@ -0,0 +1,17 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; +using WingsEmu.Game.Managers; +using WingsEmu.Plugins.DistributedGameEvents.PlayerEvents; + +namespace GameChannel.Consumers +{ + public class PlayerDisconnectedChannelConsumer : IMessageConsumer + { + private readonly ISessionManager _sessionManager; + + public PlayerDisconnectedChannelConsumer(ISessionManager sessionManager) => _sessionManager = sessionManager; + + public async Task HandleAsync(PlayerDisconnectedChannelMessage e, CancellationToken cancellation) => _sessionManager.RemoveOnline(e.CharacterName, e.CharacterId); + } +} \ No newline at end of file diff --git a/srcs/GameChannel/Consumers/PlayerKickConsumer.cs b/srcs/GameChannel/Consumers/PlayerKickConsumer.cs new file mode 100644 index 0000000..5fc8c5a --- /dev/null +++ b/srcs/GameChannel/Consumers/PlayerKickConsumer.cs @@ -0,0 +1,37 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; +using WingsAPI.Communication.Punishment; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; + +namespace GameChannel.Consumers +{ + public class PlayerKickConsumer : IMessageConsumer + { + private readonly ISessionManager _sessionManager; + + public PlayerKickConsumer(ISessionManager sessionManager) => _sessionManager = sessionManager; + + public async Task HandleAsync(PlayerKickMessage notification, CancellationToken token) + { + long? playerId = notification.PlayerId; + string playerName = notification.PlayerName; + + if (playerId.HasValue) + { + IClientSession sessionById = _sessionManager.GetSessionByCharacterId(playerId.Value); + sessionById?.ForceDisconnect(); + return; + } + + if (string.IsNullOrEmpty(playerName)) + { + return; + } + + IClientSession sessionByName = _sessionManager.GetSessionByCharacterName(playerName); + sessionByName?.ForceDisconnect(); + } + } +} \ No newline at end of file diff --git a/srcs/GameChannel/Consumers/ServiceKickAllMessageConsumer.cs b/srcs/GameChannel/Consumers/ServiceKickAllMessageConsumer.cs new file mode 100644 index 0000000..98864ca --- /dev/null +++ b/srcs/GameChannel/Consumers/ServiceKickAllMessageConsumer.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; +using WingsAPI.Communication.Services.Messages; +using WingsEmu.DTOs.Account; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Health; + +namespace GameChannel.Consumers +{ + public class ServiceKickAllMessageConsumer : IMessageConsumer + { + private readonly IMaintenanceManager _maintenanceManager; + private readonly ISessionManager _sessionManager; + + public ServiceKickAllMessageConsumer(ISessionManager sessionManager, IMaintenanceManager maintenanceManager) + { + _sessionManager = sessionManager; + _maintenanceManager = maintenanceManager; + } + + public async Task HandleAsync(ServiceKickAllMessage notification, CancellationToken token) + { + if (!notification.IsGlobal && notification.TargetedService != _maintenanceManager.ServiceName) + { + return; + } + + List sessionsToKick = SessionsToKick(); + if (sessionsToKick.Count < 1) + { + return; + } + + foreach (IClientSession session in sessionsToKick) + { + session.ForceDisconnect(); + } + } + + private List SessionsToKick() + { + var list = new List(); + + foreach (IClientSession session in _sessionManager.Sessions) + { + if (session.Account.Authority >= AuthorityType.GameMaster) + { + continue; + } + + list.Add(session); + } + + return list; + } + } +} \ No newline at end of file diff --git a/srcs/GameChannel/Consumers/ServiceMaintenanceNotificationMessageConsumer.cs b/srcs/GameChannel/Consumers/ServiceMaintenanceNotificationMessageConsumer.cs new file mode 100644 index 0000000..d0a05ee --- /dev/null +++ b/srcs/GameChannel/Consumers/ServiceMaintenanceNotificationMessageConsumer.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.MultiLanguage; +using PhoenixLib.ServiceBus; +using WingsAPI.Communication.Services.Messages; +using WingsEmu.Core.Extensions; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums.Chat; + +namespace GameChannel.Consumers +{ + public class ServiceMaintenanceNotificationMessageConsumer : IMessageConsumer + { + private readonly IGameLanguageService _languageService; + private readonly ISessionManager _sessionManager; + + public ServiceMaintenanceNotificationMessageConsumer(IGameLanguageService languageService, ISessionManager sessionManager) + { + _languageService = languageService; + _sessionManager = sessionManager; + } + + public async Task HandleAsync(ServiceMaintenanceNotificationMessage notification, CancellationToken token) + { + GameDialogKey dialog; + double? value; + + switch (notification.NotificationType) + { + case ServiceMaintenanceNotificationType.Rescheduled: + case ServiceMaintenanceNotificationType.ScheduleWarning: + if (notification.TimeLeft.Hours > 0) + { + dialog = notification.NotificationType switch + { + ServiceMaintenanceNotificationType.Rescheduled => GameDialogKey.MAINTENANCE_SHOUTMESSAGE_NOTIFY_RESCHEDULED_HOURS, + ServiceMaintenanceNotificationType.ScheduleWarning => GameDialogKey.MAINTENANCE_SHOUTMESSAGE_NOTIFY_WARNING_HOURS + }; + value = notification.TimeLeft.TotalHours; + } + else if (notification.TimeLeft.Minutes > 0) + { + dialog = notification.NotificationType switch + { + ServiceMaintenanceNotificationType.Rescheduled => GameDialogKey.MAINTENANCE_SHOUTMESSAGE_NOTIFY_RESCHEDULED_MINUTES, + ServiceMaintenanceNotificationType.ScheduleWarning => GameDialogKey.MAINTENANCE_SHOUTMESSAGE_NOTIFY_WARNING_MINUTES + }; + value = notification.TimeLeft.TotalMinutes; + } + else + { + dialog = notification.NotificationType switch + { + ServiceMaintenanceNotificationType.Rescheduled => GameDialogKey.MAINTENANCE_SHOUTMESSAGE_NOTIFY_RESCHEDULED_SECONDS, + ServiceMaintenanceNotificationType.ScheduleWarning => GameDialogKey.MAINTENANCE_SHOUTMESSAGE_NOTIFY_WARNING_SECONDS + }; + value = notification.TimeLeft.TotalSeconds; + } + + break; + case ServiceMaintenanceNotificationType.ScheduleStopped: + dialog = GameDialogKey.MAINTENANCE_SHOUTMESSAGE_NOTIFY_STOPPED; + value = null; + break; + case ServiceMaintenanceNotificationType.Executed: + case ServiceMaintenanceNotificationType.EmergencyExecuted: + dialog = GameDialogKey.MAINTENANCE_SHOUTMESSAGE_NOTIFY_EXECUTED; + value = null; + break; + case ServiceMaintenanceNotificationType.Lifted: + dialog = GameDialogKey.MAINTENANCE_SHOUTMESSAGE_NOTIFY_LIFTED; + value = null; + break; + default: + return; + } + + Dictionary dictionary = new(); + int? roundedValue = value.HasValue ? (int)Math.Round(value.Value) : null; + + foreach (IClientSession session in _sessionManager.Sessions) + { + string message = dictionary.GetOrSetDefault(session.UserLanguage, + roundedValue.HasValue + ? _languageService.GetLanguageFormat(dialog, session.UserLanguage, roundedValue.ToString()) + : _languageService.GetLanguage(dialog, session.UserLanguage)); + + session.SendPacket(session.GenerateMsgPacket(message, MsgMessageType.MiddleYellow)); + session.SendPacket(session.GenerateSayPacket($"({session.GetLanguage(GameDialogKey.ADMIN_BROADCAST_CHATMESSAGE_SENDER)}): {message}", ChatMessageColorType.Yellow)); + } + } + } +} \ No newline at end of file diff --git a/srcs/GameChannel/Consumers/WorldServerShutdownConsumer.cs b/srcs/GameChannel/Consumers/WorldServerShutdownConsumer.cs new file mode 100644 index 0000000..5461371 --- /dev/null +++ b/srcs/GameChannel/Consumers/WorldServerShutdownConsumer.cs @@ -0,0 +1,25 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; +using WingsAPI.Communication.ServerApi; +using WingsEmu.Game.Managers; + +namespace GameChannel.Consumers +{ + public class WorldServerShutdownConsumer : IMessageConsumer + { + private readonly IServerManager _serverManager; + + public WorldServerShutdownConsumer(IServerManager serverManager) => _serverManager = serverManager; + + public async Task HandleAsync(WorldServerShutdownMessage notification, CancellationToken token) + { + if (notification.ChannelId != _serverManager.ChannelId) + { + return; + } + + _serverManager.Shutdown(); + } + } +} \ No newline at end of file diff --git a/srcs/GameChannel/Controllers/HealthController.cs b/srcs/GameChannel/Controllers/HealthController.cs new file mode 100644 index 0000000..b221930 --- /dev/null +++ b/srcs/GameChannel/Controllers/HealthController.cs @@ -0,0 +1,96 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.Primitives; +using WingsEmu.Game.Managers; + +namespace GameChannel.Controllers +{ + [HealthCheckApiKey] + [Route("/health")] + [ApiController] + public class HealthController : Controller + { + private readonly IServerManager _serverManager; + + public HealthController(IServerManager serverManager) => _serverManager = serverManager; + + + [HttpGet("liveness")] + public IActionResult GetLivenessState() + { + switch (_serverManager.State) + { + case GameServerState.ERROR: + return StatusCode(500, "Server is in error state"); + case GameServerState.STARTING: + return StatusCode(500, "Server is starting"); + case GameServerState.RUNNING: + case GameServerState.IDLE: + return Ok($"{_serverManager.State.ToString()}"); + case GameServerState.STOPPING: + return Ok(); // not sure about that + break; + } + + return Ok(); + } + + [HttpGet("readiness")] + public IActionResult GetReadinessState() + { + switch (_serverManager.State) + { + case GameServerState.ERROR: + return StatusCode(500, "Server is in error state"); + case GameServerState.STARTING: + return StatusCode(500, "Server is starting"); + case GameServerState.RUNNING: + case GameServerState.IDLE: + return Ok($"{_serverManager.State.ToString()}"); + case GameServerState.STOPPING: + return Ok(); // not sure about that + } + + return Ok(); + } + + + [AttributeUsage(AttributeTargets.Class)] + public class HealthCheckApiKey : Attribute, IAsyncActionFilter + { + private const string APIKEYNAME = "HEALTHCHECK_API_KEY"; + private static string HEALTHCHECK_API_KEY = Environment.GetEnvironmentVariable(APIKEYNAME) ?? "123456789"; + + public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + if (!context.HttpContext.Request.Headers.TryGetValue(APIKEYNAME, out StringValues extractedApiKey)) + { + context.Result = new ContentResult + { + StatusCode = 401, + Content = "Api Key was not provided" + }; + return; + } + + if (!extractedApiKey.Equals(extractedApiKey)) + { + context.Result = new ContentResult + { + StatusCode = 401, + Content = "Api Key is not valid" + }; + return; + } + + await next(); + } + } + } +} \ No newline at end of file diff --git a/srcs/GameChannel/Cryptography/EncodingExtensions.cs b/srcs/GameChannel/Cryptography/EncodingExtensions.cs new file mode 100644 index 0000000..8451633 --- /dev/null +++ b/srcs/GameChannel/Cryptography/EncodingExtensions.cs @@ -0,0 +1,33 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Text; +using PhoenixLib.MultiLanguage; + +namespace GameChannel.Cryptography +{ + public static class EncodingExtensions + { + public static Encoding GetEncoding(this RegionLanguageType key) + { + switch (key) + { + case RegionLanguageType.EN: + case RegionLanguageType.FR: + case RegionLanguageType.ES: + return Encoding.GetEncoding(1252); + case RegionLanguageType.DE: + case RegionLanguageType.PL: + case RegionLanguageType.IT: + case RegionLanguageType.CZ: + return Encoding.GetEncoding(1250); + case RegionLanguageType.TR: + return Encoding.GetEncoding(1254); + default: + throw new ArgumentOutOfRangeException(nameof(key), key, null); + } + } + } +} \ No newline at end of file diff --git a/srcs/GameChannel/Cryptography/WorldDecrypter.cs b/srcs/GameChannel/Cryptography/WorldDecrypter.cs new file mode 100644 index 0000000..1133026 --- /dev/null +++ b/srcs/GameChannel/Cryptography/WorldDecrypter.cs @@ -0,0 +1,245 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.IO; +using System.Text; + +namespace GameChannel.Cryptography +{ + public static class WorldDecrypter + { + public static string Decrypt(in ReadOnlySpan bytesBuffer, int encryptionKey, Encoding encoding) + { + if (encryptionKey == 0) + { + return DecryptUnauthed(bytesBuffer); + } + + + return DecryptAuthed(bytesBuffer, encryptionKey, encoding); + } + + + private static string DecryptAuthed(in ReadOnlySpan str, int encryptionKey, Encoding encoding) + { + var encryptedString = new StringBuilder(); + + int sessionKey = encryptionKey & 0xFF; + byte sessionNumber = unchecked((byte)(encryptionKey >> 6)); + sessionNumber &= 0xFF; + sessionNumber &= 3; + + switch (sessionNumber) + { + case 0: + foreach (byte character in str) + { + byte firstbyte = unchecked((byte)(sessionKey + 0x40)); + byte highbyte = unchecked((byte)(character - firstbyte)); + encryptedString.Append((char)highbyte); + } + + break; + + case 1: + foreach (byte character in str) + { + byte firstbyte = unchecked((byte)(sessionKey + 0x40)); + byte highbyte = unchecked((byte)(character + firstbyte)); + encryptedString.Append((char)highbyte); + } + + break; + + case 2: + foreach (byte character in str) + { + byte firstbyte = unchecked((byte)(sessionKey + 0x40)); + byte highbyte = unchecked((byte)(character - firstbyte ^ 0xC3)); + encryptedString.Append((char)highbyte); + } + + break; + + case 3: + foreach (byte character in str) + { + byte firstbyte = unchecked((byte)(sessionKey + 0x40)); + byte highbyte = unchecked((byte)(character + firstbyte ^ 0xC3)); + encryptedString.Append((char)highbyte); + } + + break; + + default: + encryptedString.Append((char)0xF); + break; + } + + string[] temp = encryptedString.ToString().Split((char)0xFF); + + var save = new StringBuilder(); + + for (int i = 0; i < temp.Length; i++) + { + save.Append(DecryptPrivate(temp[i].AsSpan(), encoding)); + if (i < temp.Length - 2) + { + save.Append((char)0xFF); + } + } + + return save.ToString(); + } + + + private static string DecryptPrivate(in ReadOnlySpan str, Encoding encoding) + { + using var receiveData = new MemoryStream(); + char[] table = { ' ', '-', '.', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '\n' }; + for (int count = 0; count < str.Length; count++) + { + if (str[count] <= 0x7A) + { + int len = str[count]; + + for (int i = 0; i < len; i++) + { + count++; + + try + { + receiveData.WriteByte(unchecked((byte)(str[count] ^ 0xFF))); + } + catch + { + receiveData.WriteByte(255); + } + } + } + else + { + int len = str[count]; + len &= 0x7F; + + for (int i = 0; i < len; i++) + { + count++; + int highbyte; + try + { + highbyte = str[count]; + } + catch + { + highbyte = 0; + } + + highbyte &= 0xF0; + highbyte >>= 0x4; + + int lowbyte; + try + { + lowbyte = str[count]; + } + catch + { + lowbyte = 0; + } + + lowbyte &= 0x0F; + + if (highbyte != 0x0 && highbyte != 0xF) + { + receiveData.WriteByte(unchecked((byte)table[highbyte - 1])); + i++; + } + + if (lowbyte != 0x0 && lowbyte != 0xF) + { + receiveData.WriteByte(unchecked((byte)table[lowbyte - 1])); + } + } + } + } + + byte[] tmp = Encoding.Convert(encoding, Encoding.UTF8, receiveData.ToArray()); + return Encoding.UTF8.GetString(tmp); + } + + private static string DecryptUnauthed(in ReadOnlySpan str) + { + try + { + var encryptedStringBuilder = new StringBuilder(); + for (int i = 1; i < str.Length; i++) + { + if (Convert.ToChar(str[i]) == 0xE) + { + return encryptedStringBuilder.ToString(); + } + + int firstbyte = Convert.ToInt32(str[i] - 0xF); + int secondbyte = firstbyte; + secondbyte &= 240; + firstbyte = Convert.ToInt32(firstbyte - secondbyte); + secondbyte >>= 4; + + switch (secondbyte) + { + case 0: + case 1: + encryptedStringBuilder.Append(' '); + break; + + case 2: + encryptedStringBuilder.Append('-'); + break; + + case 3: + encryptedStringBuilder.Append('.'); + break; + + default: + secondbyte += 0x2C; + encryptedStringBuilder.Append(Convert.ToChar(secondbyte)); + break; + } + + switch (firstbyte) + { + case 0: + encryptedStringBuilder.Append(' '); + break; + + case 1: + encryptedStringBuilder.Append(' '); + break; + + case 2: + encryptedStringBuilder.Append('-'); + break; + + case 3: + encryptedStringBuilder.Append('.'); + break; + + default: + firstbyte += 0x2C; + encryptedStringBuilder.Append(Convert.ToChar(firstbyte)); + break; + } + } + + return encryptedStringBuilder.ToString(); + } + catch (OverflowException) + { + return string.Empty; + } + } + } +} \ No newline at end of file diff --git a/srcs/GameChannel/Cryptography/WorldEncrypter.cs b/srcs/GameChannel/Cryptography/WorldEncrypter.cs new file mode 100644 index 0000000..cc79bff --- /dev/null +++ b/srcs/GameChannel/Cryptography/WorldEncrypter.cs @@ -0,0 +1,33 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Text; + +namespace GameChannel.Cryptography +{ + internal static class WorldEncrypter + { + internal static byte[] Encrypt(string packet, Encoding encoding) + { + byte[] strBytes = Encoding.Convert(Encoding.UTF8, encoding, Encoding.UTF8.GetBytes(packet)); + byte[] encryptedData = new byte[strBytes.Length + (int)Math.Ceiling((decimal)strBytes.Length / 126) + 1]; + + int j = 0; + for (int i = 0; i < strBytes.Length; i++) + { + if ((i % 126) == 0) + { + encryptedData[i + j] = (byte)(strBytes.Length - i > 126 ? 126 : strBytes.Length - i); + j++; + } + + encryptedData[i + j] = (byte)~strBytes[i]; + } + + encryptedData[^1] = 0xFF; + return encryptedData; + } + } +} \ No newline at end of file diff --git a/srcs/GameChannel/GameChannel.csproj b/srcs/GameChannel/GameChannel.csproj new file mode 100644 index 0000000..dd09e7f --- /dev/null +++ b/srcs/GameChannel/GameChannel.csproj @@ -0,0 +1,42 @@ + + + + net5.0 + ..\..\dist\game-server\ + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/srcs/GameChannel/Network/GameSession.cs b/srcs/GameChannel/Network/GameSession.cs new file mode 100644 index 0000000..f9c52c8 --- /dev/null +++ b/srcs/GameChannel/Network/GameSession.cs @@ -0,0 +1,708 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using GameChannel.Cryptography; +using NetCoreServer; +using PhoenixLib.Events; +using PhoenixLib.Logging; +using PhoenixLib.MultiLanguage; +using WingsAPI.Communication.Sessions; +using WingsAPI.Communication.Sessions.Model; +using WingsAPI.Communication.Sessions.Request; +using WingsEmu.Game; +using WingsEmu.Game._i18n; +using WingsEmu.Game._packetHandling; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Commands; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Miniland; +using WingsEmu.Game.Networking; +using WingsEmu.Packets; +using WingsEmu.Packets.ClientPackets; + +namespace GameChannel.Network +{ + public class GameSession : TcpSession, IClientSession + { + private static ISessionManager _sessionManager; + private static IGlobalCommandExecutor _commandsExecutor; + private static IAsyncEventPipeline _eventPipeline; + private static IServerManager _serverManager; + private static ISessionService _sessionService; + private static IMinilandManager _minilandManager; + private static IPacketDeserializer _deserializer; + + private static readonly IPacketSerializer _serializer = new PacketSerializer(); + + + private static readonly char[] COMMAND_PREFIX = { '$', '%' }; + private static readonly char[] ChatDelimiter = { '/', ':', ';', '!' }; + + + private readonly IPacketHandlerContainer _charScreenHandlers; + + + private readonly CancellationTokenSource _cts; + private readonly IPacketHandlerContainer _gameHandlers; + private readonly IGameLanguageService _gameLanguageService; + private readonly ConcurrentQueue _packetToHandleQueue; + private readonly ConcurrentQueue _pendingPacketsToSend; + private readonly SemaphoreSlim _semaphoreSlim = new(1, 1); + private readonly IList _waitForPacketList = new List(); + + private readonly char[] PACKET_SPLITTER = { (char)0xFF }; + + private bool _isDisconnecting; + + private int _lastKeepAliveIdentity; + private IPlayerEntity _playerEntity; + + // Packetwait Packets + private int? _waitForPacketsAmount; + + public GameSession(GameTcpServer server, IPacketHandlerContainer gameHandlers, + IPacketHandlerContainer charScreenHandlers, IGameLanguageService gameLanguageService) : base(server) + { + _gameHandlers = gameHandlers; + _charScreenHandlers = charScreenHandlers; + _gameLanguageService = gameLanguageService; + SessionId = 0; + _cts = new CancellationTokenSource(); + _pendingPacketsToSend = new ConcurrentQueue(); + _packetToHandleQueue = new ConcurrentQueue(); + _ = HandlePacketLoop(_cts.Token); + _ = SendPacketLoop(_cts.Token); + Account_OnLangChanged(null, RegionLanguageType.EN); + } + + public string IpAddress { get; private set; } + public string HardwareId { get; private set; } + public string ClientVersion { get; private set; } + + public void SendPackets(IEnumerable packets) + { + if (!CanReadOrSend()) + { + return; + } + + try + { + EnqueuePackets(packets.ToArray()); + } + catch (Exception e) + { + Log.Error("[TCP_SESSION] SendPacket", e); + ForceDisconnect(); + } + } + + public void SendPacket(string packet) + { + if (!CanReadOrSend()) + { + return; + } + + if (string.IsNullOrEmpty(packet)) + { + return; + } + + try + { + EnqueuePackets(packet); + } + catch (Exception e) + { + Log.Error("SendPacket", e); + } + } + + + public RegionLanguageType UserLanguage { get; private set; } + public Account Account { get; private set; } + + public IPlayerEntity PlayerEntity + { + get + { + if (_playerEntity == null || !HasSelectedCharacter) + { + Log.Warn("[GAME_SESSION] Uninitialized PlayerEntity cannot be accessed."); + } + + return _playerEntity; + } + + private set => _playerEntity = value; + } + + + public IMapInstance CurrentMapInstance { get; set; } + + public bool HasCurrentMapInstance => CurrentMapInstance != null; + + public bool HasSelectedCharacter { get; set; } + public byte? SelectedCharacterSlot { get; set; } + + public bool IsAuthenticated { get; set; } + + public bool IsDisposing { get; set; } + + public int SessionId { get; set; } + + public bool DebugMode { get; set; } + public bool GmMode { get; set; } = true; + + public string GetLanguage(string key) => _gameLanguageService.GetLanguage(key, UserLanguage); + + public string GetLanguageFormat(string key, params object[] formatParams) => _gameLanguageService.GetLanguageFormat(key, UserLanguage, formatParams); + + public string GetLanguage(GameDialogKey key) => _gameLanguageService.GetLanguage(key, UserLanguage); + + public string GetLanguageFormat(GameDialogKey key, params object[] formatParams) => _gameLanguageService.GetLanguageFormat(key, UserLanguage, formatParams); + + public static void Initialize(IGlobalCommandExecutor commandExecutor, IAsyncEventPipeline eventPipeline, + IServerManager serverManager, ISessionManager sessionManager, ISessionService sessionService, IMinilandManager minilandManager, IPacketDeserializer packetDeserializer) + { + _sessionManager = sessionManager; + _commandsExecutor = commandExecutor; + _eventPipeline = eventPipeline; + _serverManager = serverManager; + _sessionService = sessionService; + _minilandManager = minilandManager; + _deserializer = packetDeserializer; + } + + private async Task SendPacketLoop(CancellationToken token) + { + while (!token.IsCancellationRequested) + { + try + { + FlushPackets(); + + await Task.Delay(50, token); + } + catch (TaskCanceledException e) + { + return; + } + catch (Exception e) + { + ForceDisconnect(); + Log.Error("[TCP_SESSION] HandlePacketLoop", e); + } + } + } + + private void FlushPackets() + { + try + { + if (_pendingPacketsToSend.IsEmpty) + { + return; + } + + using var stream = new MemoryStream(); + while (_pendingPacketsToSend.TryDequeue(out string[] packets)) + { + foreach (string packet in packets) + { + byte[] bytes = WorldEncrypter.Encrypt(packet, UserLanguage.GetEncoding()); + stream.Write(bytes); + } + } + + SendAsync(stream.ToArray()); + } + catch (Exception e) + { + Log.Error("[TCP_SESSION] FlushPackets", e); + } + } + + private async Task HandlePacketLoop(CancellationToken token) + { + while (!token.IsCancellationRequested) + { + if (_packetToHandleQueue.IsEmpty) + { + await Task.Delay(50, token); + continue; + } + + if (!_packetToHandleQueue.TryDequeue(out string buff)) + { + await Task.Delay(50, token); + continue; + } + + bool isHandling = false; + try + { + isHandling = await _semaphoreSlim.WaitAsync(0, token); + if (isHandling) + { + OnGamePacketReceived(buff); + } + } + catch (TaskCanceledException e) + { + return; + } + catch (Exception e) + { + ForceDisconnect(); + Log.Error("[TCP_SESSION] HandlePacketLoop", e); + } + finally + { + if (isHandling) + { + _semaphoreSlim.Release(); + } + } + + await Task.Delay(50, token); + } + + _semaphoreSlim?.Dispose(); + } + + private void EnqueuePackets(string packet) + { + _pendingPacketsToSend.Enqueue(new[] { packet }); + } + + private void EnqueuePackets(string[] packets) + { + _pendingPacketsToSend.Enqueue(packets); + } + + private void OnGamePacketReceived(string e) + { + try + { + HandlePackets(e); + } + catch (Exception ex) + { + Log.Error("Client_OnPacketReceived : ", ex); + } + } + + protected override void OnConnected() + { + try + { + if (IsDisposed) + { + return; + } + + if (IsSocketDisposed) + { + ForceDisconnect(); + return; + } + + if (Socket == null) + { + } + } + catch (Exception e) + { + Log.Error("[WORLD_SERVER_SESSION] OnConnected", e); + ForceDisconnect(); + } + } + + protected override void OnDisconnected() + { + ForceDisconnect(); + } + + private bool CanReadOrSend() + { + if (IsDisposing) + { + return false; + } + + return !_serverManager.InShutdown; + } + + protected override void OnReceived(byte[] buffer, long offset, long size) + { + if (!CanReadOrSend()) + { + return; + } + + try + { + string buff = WorldDecrypter.Decrypt(buffer.AsSpan((int)offset, (int)size), SessionId, UserLanguage.GetEncoding()); + var packets = buff.Split(PACKET_SPLITTER, StringSplitOptions.RemoveEmptyEntries).ToList(); + + if (HasSelectedCharacter) + { + foreach (string packet in packets) + { + _packetToHandleQueue.Enqueue(packet); + } + + return; + } + + foreach (string packet in packets) + { + OnGamePacketReceived(packet); + } + } + catch (Exception e) + { + Log.Error("[TCP_SESSION] OnReceived", e); + ForceDisconnect(); + } + } + + protected override void OnError(SocketError error) + { + Log.Error("[TCP_SESSION]", new Exception($"OnError {error}")); + ForceDisconnect(); + } + + #region Methods + + public void ForceDisconnect() + { + if (_isDisconnecting) + { + Log.Debug("[TCP_SESSION] Already disconnecting..."); + return; + } + + try + { + Log.Debug("[TCP_SESSION] Force disconnecting..."); + IsDisposing = true; + _isDisconnecting = true; + Log.Debug("[TCP_SESSION] Flushing packets..."); + FlushPackets(); + Log.Debug("[TCP_SESSION] Packets flushed..."); + _pendingPacketsToSend.Clear(); + _packetToHandleQueue.Clear(); + _cts.Cancel(); + if (Account != null) + { + Log.Debug("[TCP_SESSION] Removing Account delegates..."); + Account.LangChanged -= Account_OnLangChanged; + } + + // do everything necessary before removing client, DB save, Whatever + if (!HasSelectedCharacter) + { + Log.Debug("[TCP_SESSION] No character selected..."); + if (Account != null) + { + Log.Debug("[TCP_SESSION] Account not null..."); + _sessionService.Disconnect(new DisconnectSessionRequest + { + AccountId = Account.Id, + EncryptionKey = SessionId + }).ConfigureAwait(false).GetAwaiter().GetResult(); + } + + Log.Debug($"[TCP_SESSION] {IpAddress} disconnected without selecting character"); + Disconnect(); + return; + } + + Log.Debug("[TCP_SESSION] Clearing buffs..."); + PlayerEntity.BuffComponent.ClearNonPersistentBuffs(); + Log.Debug("[TCP_SESSION] Unregistering session from MapInstance..."); + // CurrentMapInstance?.UnregisterSession(this); + Log.Debug("[TCP_SESSION] Saving Character..."); + this.CharacterDisconnect().ConfigureAwait(false).GetAwaiter().GetResult(); + Log.Debug("[TCP_SESSION] Unregistering session from SessionManager..."); + _sessionManager.UnregisterSession(this); + Log.Debug("[TCP_SESSION] Removing session from Master..."); + _minilandManager.RemoveMiniland(PlayerEntity.Id); + Log.Debug("[TCP_SESSION] Unregistering Miniland..."); + if (Account != null) + { + _sessionService.Disconnect(new DisconnectSessionRequest + { + AccountId = Account.Id, + EncryptionKey = SessionId + }).ConfigureAwait(false).GetAwaiter().GetResult(); + } + + Log.Info($"[TCP_SESSION] {PlayerEntity.Name} - {IpAddress} disconnected"); + Disconnect(); + } + catch (Exception e) + { + Log.Error("[TCP_SESSION] Session could not disconnect properly", e); + } + } + + public void InitializeAccount(Account account, Session session) + { + Account = account; + IpAddress = session.IpAddress; + HardwareId = session.HardwareId; + ClientVersion = session.ClientVersion; + Account_OnLangChanged(null, account.Language.ToRegionLanguageType()); + Account.LangChanged += Account_OnLangChanged; + IsAuthenticated = true; + } + + private void Account_OnLangChanged(object sender, RegionLanguageType e) + { + UserLanguage = e; + } + + //[Obsolete("Primitive string operations will be removed in future, use PacketDefinition SendPacket instead. SendPacket with string parameter should only be used for debugging.")] + public void SendPacket(T packet) where T : IPacket + { + if (IsDisposing) + { + return; + } + + SendPacket(_serializer.Serialize(packet)); + } + + + public void InitializePlayerEntity(IPlayerEntity character) + { + HasSelectedCharacter = true; + PlayerEntity = character; + PlayerEntity.SetSession(this); + _sessionManager.RegisterSession(this); + } + + public void EmitEvent(T e) where T : PlayerEvent + { + EmitEventAsync(e).ConfigureAwait(false).GetAwaiter().GetResult(); + } + + public async Task EmitEventAsync(T e) where T : PlayerEvent + { + e.Sender = this; + await _eventPipeline.ProcessEventAsync(e).ConfigureAwait(false); + } + + private void ProcessUnAuthedPacket(string sessionPacket) + { + if (string.IsNullOrEmpty(sessionPacket)) + { + return; + } + + string[] sessionParts = sessionPacket.Split(' '); + if (sessionParts.Length == 0) + { + return; + } + + if (!int.TryParse(sessionParts[0], out int packetId)) + { + ForceDisconnect(); + return; + } + + _lastKeepAliveIdentity = packetId; + + // set the SessionId if Session Packet arrives + if (sessionParts.Length < 2) + { + return; + } + + if (!int.TryParse(sessionParts[1].Split('\\').FirstOrDefault(), out int sessionId)) + { + return; + } + + SessionId = sessionId; + + if (_waitForPacketsAmount.HasValue) + { + return; + } + + _waitForPacketsAmount = 3; + _waitForPacketList.Add(EntryPointPacket.EntryPointPacketHeader); + } + + /// + /// Handle the packet received by the Client. + /// + private void HandlePackets(string packet) + { + // determine first packet + if (SessionId == 0) + { + ProcessUnAuthedPacket(packet); + return; + } + + string packetString = packet.Replace('^', ' '); + string[] packetSplit = packetString.Split(' '); + + if (_waitForPacketsAmount.HasValue) + { + WaitForEntrypointPackets(packetSplit); + return; + } + + ProcessWorldPackets(packetSplit, packetString, packet); + } + + private void ProcessWorldPackets(IList packetSplit, string packetString, string packet) + { + // keep alive + string nextKeepAliveRaw = packetSplit[0]; + if (!int.TryParse(nextKeepAliveRaw, out int nextKeepaliveIdentity) && nextKeepaliveIdentity != (_lastKeepAliveIdentity + 1)) + { + Log.Warn("CORRUPTED_KEEPALIVE " + IpAddress); + ForceDisconnect(); + return; + } + + if (nextKeepaliveIdentity == 0) + { + if (_lastKeepAliveIdentity == ushort.MaxValue) + { + _lastKeepAliveIdentity = nextKeepaliveIdentity; + } + } + else + { + _lastKeepAliveIdentity = nextKeepaliveIdentity; + } + + + if (packetSplit.Count <= 1) + { + return; + } + + if (packetSplit[1].Length < 1) + { + return; + } + + if (COMMAND_PREFIX.Any(s => s == packetSplit[1][0])) + { + _commandsExecutor.HandleCommand(packet.Substring(packet.IndexOf(' ', StringComparison.OrdinalIgnoreCase) + 1), this, packetSplit[1][0].ToString()); + return; + } + + if (packetSplit[1].Length >= 1 && ChatDelimiter.Any(s => s == packetSplit[1][0])) + { + packetSplit[1] = packetSplit[1][0].ToString(); + packetString = packet.Insert(packet.IndexOf(' ', StringComparison.OrdinalIgnoreCase) + 2, " "); + } + + if (packetSplit[1] != "0") + { + TriggerHandler(packetSplit[1].Replace("#", "", StringComparison.OrdinalIgnoreCase), packetString); + } + } + + private void WaitForEntrypointPackets(IReadOnlyList packetSplit) + { + if (packetSplit.Count < 2) + { + return; + } + + // cross server authentication + if (packetSplit.Count > 3 && packetSplit[1] == "DAC") + { + _waitForPacketList.Clear(); + _waitForPacketList.Add(string.Join(" ", packetSplit.Skip(1).ToArray())); + _waitForPacketsAmount = 1; + } + else + { + // username or password + _waitForPacketList.Add(packetSplit[1]); + } + + if (_waitForPacketList.Count != _waitForPacketsAmount) + { + return; // continue; + } + + _waitForPacketsAmount = null; + string queuedPackets = string.Join(" ", _waitForPacketList.ToArray()); + string header = queuedPackets.Split(' ', '^')[0]; + TriggerHandler(header, queuedPackets); + _waitForPacketList.Clear(); + } + + private void TriggerHandler(string packetHeader, string packetString) + { + if (_serverManager.InShutdown) + { + return; + } + + if (IsDisposing) + { + Log.Warn("[CLIENTSESSION] DISPOSING"); + return; + } + + try + { + (IClientPacket typedPacket, Type packetType) = _deserializer.Deserialize(packetString, IsAuthenticated); + + if (packetType == typeof(UnresolvedPacket) && typedPacket != null) + { + Log.Warn($"AccountId: {(Account?.Id ?? 0).ToString()} UNRESOLVED_PACKET : {packetHeader}"); + return; + } + + if (packetType == null && typedPacket == null) + { + Log.Info("DESERIALIZATION_ERROR"); + return; + } + + if (HasSelectedCharacter) + { + _gameHandlers.Execute(this, typedPacket, packetType); + } + else + { + _charScreenHandlers.Execute(this, typedPacket, packetType); + } + } + catch (Exception ex) + { + Log.Error("[Handler Error]", ex); + } + } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/GameChannel/Network/GameSessionFactory.cs b/srcs/GameChannel/Network/GameSessionFactory.cs new file mode 100644 index 0000000..b4b6275 --- /dev/null +++ b/srcs/GameChannel/Network/GameSessionFactory.cs @@ -0,0 +1,22 @@ +using WingsEmu.Game._i18n; +using WingsEmu.Game._packetHandling; + +namespace GameChannel.Network +{ + public class GameSessionFactory + { + private readonly IPacketHandlerContainer _characterScreenHandlers; + private readonly IPacketHandlerContainer _gameHandlers; + private readonly IGameLanguageService _gameLanguage; + + public GameSessionFactory(IPacketHandlerContainer gameHandlers, + IPacketHandlerContainer characterScreenHandlers, IGameLanguageService gameLanguage) + { + _gameHandlers = gameHandlers; + _characterScreenHandlers = characterScreenHandlers; + _gameLanguage = gameLanguage; + } + + public GameSession CreateSession(GameTcpServer server) => new(server, _gameHandlers, _characterScreenHandlers, _gameLanguage); + } +} \ No newline at end of file diff --git a/srcs/GameChannel/Network/GameTcpServer.cs b/srcs/GameChannel/Network/GameTcpServer.cs new file mode 100644 index 0000000..129c44c --- /dev/null +++ b/srcs/GameChannel/Network/GameTcpServer.cs @@ -0,0 +1,80 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Net; +using System.Net.Sockets; +using GameChannel.Utils; +using NetCoreServer; +using PhoenixLib.Logging; +using WingsEmu.Game.Managers; + +namespace GameChannel.Network +{ + public class GameTcpServer : TcpServer + { + private readonly IServerManager _serverManager; + private readonly GameSessionFactory _sessionFactory; + private readonly ISpamProtector _spamProtector; + + public GameTcpServer(IPAddress address, int port, ISpamProtector spamProtector, IServerManager serverManager, GameSessionFactory sessionFactory) : + base(address, port) + { + _spamProtector = spamProtector; + _serverManager = serverManager; + _sessionFactory = sessionFactory; + } + + protected override TcpSession CreateSession() + { + Log.Info("Creating session"); + GameSession tmp = _sessionFactory.CreateSession(this); + Log.Info("Returning session"); + return tmp; + } + + + protected override void OnConnected(TcpSession session) + { + try + { + if (session.IsSocketDisposed) + { + return; + } + + if (!(session.Socket.RemoteEndPoint is IPEndPoint ip)) + { + session.Disconnect(); + return; + } + + if (!_spamProtector.CanConnect(ip.Address.ToString())) + { + session.Disconnect(); + return; + } + + Log.Info($"[TCP_SERVER] Connected : {ip.Address}"); + } + catch (Exception e) + { + Log.Error("[TCP_SERVER] OnConnected", e); + session.Dispose(); + } + } + + protected override void OnStarted() + { + Log.Info("[TCP_SERVER] Started"); + } + + protected override void OnError(SocketError error) + { + Log.Warn("[TCP_SERVER] caught an error with code {error}"); + _serverManager.Shutdown(); + Stop(); + } + } +} \ No newline at end of file diff --git a/srcs/GameChannel/Network/IClientSessionFactory.cs b/srcs/GameChannel/Network/IClientSessionFactory.cs new file mode 100644 index 0000000..c2ecf63 --- /dev/null +++ b/srcs/GameChannel/Network/IClientSessionFactory.cs @@ -0,0 +1,13 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsEmu.Game.Networking; + +namespace GameChannel.Network +{ + public interface IClientSessionFactory + { + IClientSession CreateSession(GameTcpServer session); + } +} \ No newline at end of file diff --git a/srcs/GameChannel/Program.cs b/srcs/GameChannel/Program.cs new file mode 100644 index 0000000..7ef62f2 --- /dev/null +++ b/srcs/GameChannel/Program.cs @@ -0,0 +1,209 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Reflection; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using dotenv.net; +using GameChannel.Network; +using GameChannel.Services; +using GameChannel.Utils; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using PhoenixLib.Events; +using PhoenixLib.Logging; +using PhoenixLib.ServiceBus.MQTT; +using WingsAPI.Communication; +using WingsAPI.Communication.ServerApi; +using WingsAPI.Communication.ServerApi.Protocol; +using WingsAPI.Communication.Sessions; +using WingsAPI.Plugins; +using WingsEmu.Game.Commands; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Miniland; +using WingsEmu.Packets; + +namespace GameChannel +{ + public class Program + { + public static async Task Main(string[] args) + { + Thread.CurrentThread.CurrentCulture = new CultureInfo("en-US"); + CultureInfo.DefaultThreadCurrentCulture = new CultureInfo("en-US"); + CultureInfo.DefaultThreadCurrentUICulture = new CultureInfo("en-US"); + // workaround + // http2 needs SSL + // https://github.com/grpc/grpc-dotnet/issues/626 + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); + PrintHeader(); + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + DotEnv.Load(new DotEnvOptions(true, new[] { "world.env" }, Encoding.UTF8)); + using IHost httpServer = CreateHostBuilder(args).Build(); + { + IServiceProvider services = httpServer.Services; + + IServerApiService serverApiService = services.GetRequiredService(); + BasicRpcResponse response = null; + + while (response == null) + { + try + { + response = await serverApiService.IsMasterOnline(new EmptyRpcRequest()); + } + catch (Exception e) + { + Log.Warn("Failed to contact with Master Server, retrying..."); + await Task.Delay(TimeSpan.FromSeconds(2)); + } + } + + GameSession.Initialize( + services.GetService(), + services.GetService(), + services.GetService(), + services.GetService(), + services.GetService(), + services.GetRequiredService(), + services.GetRequiredService() + ); + + SerializableGameServer gameServer = services.GetRequiredService(); + + IEnumerable plugins = services.GetServices(); + foreach (IGamePlugin gamePlugin in plugins) + { + gamePlugin.OnLoad(); + } + + IServerManager serverManager = services.GetRequiredService(); + StaticServerManager.Initialize(serverManager); + serverManager.InitializeAsync(); + + using var stopService = new DockerGracefulStopService(); + GameTcpServer server; + portLoop: + try + { + Log.Warn($"Binding TCP port {gameServer.EndPointPort}"); + server = new GameTcpServer( + IPAddress.Any, + gameServer.EndPointPort, + new SmartSpamProtector(), + serverManager, httpServer.Services.GetRequiredService()); + server.Start(); + } + catch (SocketException ex) + { + if (ex.ErrorCode == 10048) + { + gameServer.EndPointPort++; + Log.Info("Port already in use! Incrementing..."); + goto portLoop; + } + + Log.Error("General Error", ex); + Environment.Exit(1); + return; + } + + await httpServer.StartAsync(); + + IMessagingService messagingService = services.GetRequiredService(); + await messagingService.StartAsync(); + + Log.Warn($"Registering GameChannel [{gameServer.WorldGroup}] {gameServer.EndPointIp}:{gameServer.EndPointPort}"); + + BasicRpcResponse response2 = null; + try + { + response2 = await serverApiService.RegisterWorldServer(new RegisterWorldServerRequest + { + GameServer = gameServer + }); + } + catch (Exception e) + { + server.Stop(); + Log.Error("Could not register channel. Already running?", e); + Console.ReadKey(); + } + + + if (response2?.ResponseType != RpcResponseType.SUCCESS) + { + Log.Info($"New channel: {gameServer.ChannelId.ToString()} registered"); + } + + Log.Warn($"[GAME_CHANNEL] Started as : {gameServer.WorldGroup}:{gameServer.ChannelId}"); + serverManager.PutIdle(); + + GameChannelStopService serverShutdown = services.GetRequiredService(); + serverManager.ListenCancellation(stopService.TokenSource); + + await httpServer.WaitForShutdownAsync(stopService.CancellationToken); + + await serverShutdown.StopAsync(); + + Log.Warn("Properly shutting down server..."); + server?.Stop(); + await httpServer?.StopAsync(); + await messagingService.DisposeAsync(); + Serilog.Log.CloseAndFlush(); + } + } + + // For instructions on how to configure Kestrel and gRPC clients on macOS, visit https://go.microsoft.com/fwlink/?linkid=2099682 + // Additional configuration is required to successfully run gRPC on macOS. + private static IHostBuilder CreateHostBuilder(string[] args) + { + IHostBuilder hostBuilder = Host.CreateDefaultBuilder(args); + hostBuilder.ConfigureWebHostDefaults(webBuilder => + { + webBuilder.ConfigureKestrel(s => + { + s.ListenAnyIP(Convert.ToInt32(Environment.GetEnvironmentVariable("HTTP_LISTEN_PORT") ?? "17500"), + options => { options.Protocols = HttpProtocols.Http1AndHttp2; }); + }); + webBuilder.UseStartup(); + }); + return hostBuilder; + } + + + private static void PrintHeader() + { + Console.Title = "WingsEmu - World"; + const string text = @" +██╗ ██╗██╗███╗ ██╗ ██████╗ ███████╗███████╗███╗ ███╗██╗ ██╗ +██║ ██║██║████╗ ██║██╔════╝ ██╔════╝██╔════╝████╗ ████║██║ ██║ +██║ █╗ ██║██║██╔██╗ ██║██║ ███╗███████╗█████╗ ██╔████╔██║██║ ██║ +██║███╗██║██║██║╚██╗██║██║ ██║╚════██║██╔══╝ ██║╚██╔╝██║██║ ██║ +╚███╔███╔╝██║██║ ╚████║╚██████╔╝███████║███████╗██║ ╚═╝ ██║╚██████╔╝ + ╚══╝╚══╝ ╚═╝╚═╝ ╚═══╝ ╚═════╝ ╚══════╝╚══════╝╚═╝ ╚═╝ ╚═════╝ + + ██████╗ █████╗ ███╗ ███╗███████╗ ███████╗███████╗██████╗ ██╗ ██╗███████╗██████╗ +██╔════╝ ██╔══██╗████╗ ████║██╔════╝ ██╔════╝██╔════╝██╔══██╗██║ ██║██╔════╝██╔══██╗ +██║ ███╗███████║██╔████╔██║█████╗█████╗███████╗█████╗ ██████╔╝██║ ██║█████╗ ██████╔╝ +██║ ██║██╔══██║██║╚██╔╝██║██╔══╝╚════╝╚════██║██╔══╝ ██╔══██╗╚██╗ ██╔╝██╔══╝ ██╔══██╗ +╚██████╔╝██║ ██║██║ ╚═╝ ██║███████╗ ███████║███████╗██║ ██║ ╚████╔╝ ███████╗██║ ██║ + ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝ ╚══════╝╚══════╝╚═╝ ╚═╝ ╚═══╝ ╚══════╝╚═╝ ╚═╝ +"; + string separator = new('=', Console.WindowWidth); + string logo = text.Split('\n') + .Select(s => string.Format("{0," + (Console.WindowWidth / 2 + s.Length / 2) + "}\n", s)) + .Aggregate("", (current, i) => current + i); + + Console.ForegroundColor = ConsoleColor.Cyan; + Console.WriteLine(separator + logo + $"Version: {Assembly.GetExecutingAssembly().GetName().Version}\n" + separator); + Console.ForegroundColor = ConsoleColor.White; + } + } +} \ No newline at end of file diff --git a/srcs/GameChannel/RecurrentJobs/WorldKeepAliveSystem.cs b/srcs/GameChannel/RecurrentJobs/WorldKeepAliveSystem.cs new file mode 100644 index 0000000..c85199e --- /dev/null +++ b/srcs/GameChannel/RecurrentJobs/WorldKeepAliveSystem.cs @@ -0,0 +1,53 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using PhoenixLib.Logging; +using WingsAPI.Communication.ServerApi; +using WingsAPI.Communication.ServerApi.Protocol; +using WingsEmu.Game.Managers; + +namespace GameChannel.RecurrentJobs +{ + public class WorldKeepAliveSystem : BackgroundService + { + private static readonly TimeSpan Interval = TimeSpan.FromSeconds(5); + private readonly SerializableGameServer _gameInfos; + private readonly IServerApiService _serverApiService; + private readonly ISessionManager _sessionManager; + + public WorldKeepAliveSystem(IServerApiService serverApiService, SerializableGameServer gameInfos, ISessionManager sessionManager) + { + _serverApiService = serverApiService; + _gameInfos = gameInfos; + _sessionManager = sessionManager; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + Log.Info("[GAME_PULSE_SYSTEM] Starting..."); + while (!stoppingToken.IsCancellationRequested) + { + try + { + Log.Debug("[GAME_PULSE_SYSTEM] Pulsing..."); + await _serverApiService.PulseWorldServer(new PulseWorldServerRequest + { + ChannelId = _gameInfos.ChannelId, + SessionsCount = _sessionManager.SessionsCount + }); + } + catch (Exception e) + { + Log.Error("[GAME_PULSE_SYSTEM] Pulse error", e); + } + + await Task.Delay(Interval, stoppingToken); + } + } + } +} \ No newline at end of file diff --git a/srcs/GameChannel/Services/GameChannelStopService.cs b/srcs/GameChannel/Services/GameChannelStopService.cs new file mode 100644 index 0000000..7b4dbcb --- /dev/null +++ b/srcs/GameChannel/Services/GameChannelStopService.cs @@ -0,0 +1,50 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Threading.Tasks; +using WingsAPI.Communication.ServerApi; +using WingsAPI.Communication.ServerApi.Protocol; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers; +using WingsEmu.Packets.Enums.Chat; + +namespace GameChannel.Services +{ + public class GameChannelStopService + { + private readonly IGameLanguageService _gameLanguage; + private readonly SerializableGameServer _gameServer; + private readonly IServerApiService _serverApiService; + private readonly IServerManager _serverManager; + private readonly ISessionManager _sessionManager; + + public GameChannelStopService(ISessionManager sessionManager, IServerApiService serverApiService, IServerManager serverManager, SerializableGameServer gameServer, + IGameLanguageService gameLanguage) + { + _sessionManager = sessionManager; + _serverApiService = serverApiService; + _serverManager = serverManager; + _gameServer = gameServer; + _gameLanguage = gameLanguage; + } + + public async Task StopAsync() + { + await _serverApiService.UnregisterWorldServer(new UnregisterWorldServerRequest + { + ChannelId = _gameServer.ChannelId + }); + + _serverManager.PutIdle(); + await _sessionManager.BroadcastAsync(async session => + { + string message = _gameLanguage.GetLanguageFormat(GameDialogKey.INFORMATION_SHOUTMESSAGE_SHUTDOWN_SEC, session.UserLanguage, 15); + return session.GenerateMsgPacket(message, MsgMessageType.MiddleYellow); + }); + await _sessionManager.DisconnectAllAsync(); + await Task.Delay(15000); + } + } +} \ No newline at end of file diff --git a/srcs/GameChannel/Startup.cs b/srcs/GameChannel/Startup.cs new file mode 100644 index 0000000..3299c03 --- /dev/null +++ b/srcs/GameChannel/Startup.cs @@ -0,0 +1,237 @@ +using GameChannel.Consumers; +using GameChannel.Network; +using GameChannel.RecurrentJobs; +using GameChannel.Services; +using GameChannel.Ticks; +using GameChannel.Utils; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using PhoenixLib.Configuration; +using PhoenixLib.DAL; +using PhoenixLib.DAL.Redis; +using PhoenixLib.Events; +using PhoenixLib.Logging; +using PhoenixLib.Scheduler.ReactiveX; +using PhoenixLib.ServiceBus.Extensions; +using Plugin.Act4; +using Plugin.CoreImpl; +using Plugin.FamilyImpl; +using Plugin.PlayerLogs; +using Plugin.QuestImpl; +using Plugin.Raids; +using Plugin.RainbowBattle; +using Plugin.ResourceLoader; +using Plugin.TimeSpaces; +using WingsAPI.Communication.Punishment; +using WingsAPI.Communication.ServerApi; +using WingsAPI.Communication.Services.Messages; +using WingsAPI.Plugins; +using WingsAPI.Plugins.Exceptions; +using WingsEmu.Commands; +using WingsEmu.Commands.Interfaces; +using WingsEmu.Communication.gRPC.Extensions; +using WingsEmu.Game.Commands; +using WingsEmu.Game.Logs; +using WingsEmu.Health.Extensions; +using WingsEmu.Packets; +using WingsEmu.Plugins.BasicImplementations; +using WingsEmu.Plugins.BasicImplementations.Algorithms; +using WingsEmu.Plugins.BasicImplementations.BCards; +using WingsEmu.Plugins.DistributedGameEvents; +using WingsEmu.Plugins.DistributedGameEvents.Consumer; +using WingsEmu.Plugins.DistributedGameEvents.Mails; +using WingsEmu.Plugins.DistributedGameEvents.PlayerEvents; +using WingsEmu.Plugins.DistributedGameEvents.Relation; +using WingsEmu.Plugins.Essentials; +using WingsEmu.Plugins.GameEvents; +using WingsEmu.Plugins.PacketHandling; +using WingsEmu.Plugins.PacketHandling.Customization; + +namespace GameChannel +{ + public class Startup + { + private static ServiceProvider GetPluginsProvider() + { + var pluginBuilder = new ServiceCollection(); + pluginBuilder.AddTransient(); + pluginBuilder.AddTransient(); + pluginBuilder.AddTransient(); + pluginBuilder.AddTransient(); + pluginBuilder.AddTransient(); + pluginBuilder.AddTransient(); + pluginBuilder.AddTransient(); + pluginBuilder.AddTransient(); + pluginBuilder.AddTransient(); + pluginBuilder.AddTransient(); + pluginBuilder.AddTransient(); + pluginBuilder.AddTransient(); + pluginBuilder.AddTransient(); + pluginBuilder.AddTransient(); + pluginBuilder.AddTransient(); + pluginBuilder.AddTransient(); + pluginBuilder.AddTransient(); + pluginBuilder.AddTransient(); + pluginBuilder.AddTransient(); + pluginBuilder.AddTransient(); + pluginBuilder.AddTransient(); + pluginBuilder.AddTransient(); + + + return pluginBuilder.BuildServiceProvider(); + } + + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) + { + services.AddControllers(); + services.AddGrpc(options => + { + options.MaxReceiveMessageSize = null; + options.MaxSendMessageSize = null; + }); + + //No idea if this SerializableWorld thingy has to be removed in the future, but I need this info for loading or not loading Act4 thingies. + services.AddSingleton(WorldServerSingleton.Instance); + var loader = new GameServerLoader + { + Type = WorldServerSingleton.Instance.ChannelType + }; + using ServiceProvider plugins = GetPluginsProvider(); + + foreach (IDependencyInjectorPlugin plugin in plugins.GetServices()) + { + try + { + Log.Debug($"[PLUGIN_LOADER] Loading generic plugin {plugin.Name}..."); + plugin.AddDependencies(services); + } + catch (PluginException e) + { + Log.Error($"{plugin.Name} : plugin.OnLoad", e); + } + } + + + foreach (IGameServerPlugin plugin in plugins.GetServices()) + { + try + { + Log.Debug($"[PLUGIN_LOADER] Loading game plugin {plugin.Name}..."); + plugin.AddDependencies(services, loader); + } + catch (PluginException e) + { + Log.Error($"{plugin.Name} : plugin.OnLoad", e); + } + } + + services.TryAddSingleton(); + services.AddPhoenixLogging(); + // add redis + services.TryAddSingleton(s => RedisConfiguration.FromEnv()); + services.TryAddSingleton(s => s.GetRequiredService().GetConnectionMultiplexer()); + + services.AddMaintenanceMode(); + + services.AddYamlConfigurationHelper(); + services.AddSingleton(s => new PacketSerializer()); + // client session factory + services.AddTransient(); + + services.TryAddTransient(typeof(IMapper<,>), typeof(MapsterMapper<,>)); + + services.AddServerApiServiceClient(); + services.AddGrpcSessionServiceClient(); + services.AddGrpcMailServiceClient(); + services.AddGrpcRelationServiceClient(); + services.AddGrpcClusterStatusServiceClient(); + services.AddClusterCharacterServiceClient(); + services.AddTranslationsGrpcClient(); + services.AddTickSystem(); + + services.AddMqttConfigurationFromEnv(); + + services.AddMessagePublisher(); + + services.AddMessagePublisher(); + services.AddMessagePublisher(); + services.AddMessageSubscriber(); + services.AddMessageSubscriber(); + + services.AddMessageSubscriber(); + + services.AddMessagePublisher(); + services.AddMessageSubscriber(); + + services.AddMessageSubscriber(); + services.AddMessageSubscriber(); + + services.AddMessageSubscriber(); + services.AddMessageSubscriber(); + + services.AddMessageSubscriber(); + services.AddMessageSubscriber(); + + services.AddMessageSubscriber(); + services.AddMessageSubscriber(); + + services.AddMessageSubscriber(); + services.AddMessageSubscriber(); + + services.AddMessagePublisher(); + services.AddMessageSubscriber(); + + services.AddMessageSubscriber(); + + services.AddEventPipeline(); + services.AddHostedService(); + + services.AddSingleton(); + services.AddSingleton(provider => (PlayerLogManager)provider.GetService()); + + /* + * Helpers to remove + */ + + services.AddScheduler(); + services.AddCron(); + services.AddSingleton(); + services.AddSingleton(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseRouting(); + + app.UseEndpoints(e => e.MapControllers()); + } + } +} \ No newline at end of file diff --git a/srcs/GameChannel/Ticks/TickConfiguration.cs b/srcs/GameChannel/Ticks/TickConfiguration.cs new file mode 100644 index 0000000..f3d87b8 --- /dev/null +++ b/srcs/GameChannel/Ticks/TickConfiguration.cs @@ -0,0 +1,19 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; + +namespace GameChannel.Ticks +{ + public static class TickConfiguration + { + public const string SINGLE_THREAD_TICK_TYPE = "SINGLE_THREAD"; + public const string DISPATCH_WORKERS_TICK_TYPE = "DISPATCH_WORKERS"; + public const string PEEK_WORKERS_TICK_TYPE = "PEEK_WORKERS"; + + public static uint TickFrequency = Convert.ToUInt32(Environment.GetEnvironmentVariable("WINGSEMU_TICK_RATE") ?? "20"); + public static uint TickWorkers = Convert.ToUInt32(Environment.GetEnvironmentVariable("WINGSEMU_TICK_WORKERS") ?? "2"); + public static string TickSystemType = Environment.GetEnvironmentVariable("WINGSEMU_TICK_SYSTEM_TYPE") ?? DISPATCH_WORKERS_TICK_TYPE; + } +} \ No newline at end of file diff --git a/srcs/GameChannel/Ticks/TickExtensions.cs b/srcs/GameChannel/Ticks/TickExtensions.cs new file mode 100644 index 0000000..80e643b --- /dev/null +++ b/srcs/GameChannel/Ticks/TickExtensions.cs @@ -0,0 +1,25 @@ +// WingsEmu +// +// Developed by NosWings Team + +using GameChannel.Ticks.DispatchQueueWork; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using WingsEmu.Game._ECS; + +namespace GameChannel.Ticks +{ + public static class TickExtensions + { + public static void AddTickSystem(this IServiceCollection services) + { + string tickSystemType = TickConfiguration.TickSystemType; + switch (tickSystemType) + { + case TickConfiguration.DISPATCH_WORKERS_TICK_TYPE: + services.TryAddSingleton(); + break; + } + } + } +} \ No newline at end of file diff --git a/srcs/GameChannel/Ticks/TickWorker.cs b/srcs/GameChannel/Ticks/TickWorker.cs new file mode 100644 index 0000000..a792dde --- /dev/null +++ b/srcs/GameChannel/Ticks/TickWorker.cs @@ -0,0 +1,159 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; +using PhoenixLib.Logging; +using WingsEmu.Game._ECS; + +namespace GameChannel.Ticks.DispatchQueueWork +{ + public class DispatchedTickWorker + { + private static readonly uint TickFrequency = TickConfiguration.TickFrequency; + private readonly List _processables = new(); + private readonly ConcurrentQueue _toAddQueue = new(); + private readonly ConcurrentQueue _toRemoveQueue = new(); + private readonly string _workerName; + private readonly string[] _workersLabel; + + private int _averageProcessingTime; + + private DateTime _lastTick = DateTime.UtcNow; + + private Thread _thread; + + + public DispatchedTickWorker(int id) + { + Id = id; + _workerName = "GameTickThread-" + id; + _workersLabel = new[] { _workerName }; + } + + public int Id { get; } + + private bool IsRunning { get; set; } + public int AverageProcessingTime => _averageProcessingTime; + + public void Start() + { + if (IsRunning) + { + return; + } + + IsRunning = true; + _thread = new Thread(TickLoop) + { + Priority = ThreadPriority.AboveNormal, + Name = _workerName + }; + Log.Warn("[TICK_PROCESSOR] Starting worker " + _workerName); + _thread.Start(); + } + + public void Stop() + { + IsRunning = false; + } + + public void AddTickProcessable(ITickProcessable toAdd) + { + _toAddQueue.Enqueue(toAdd); + } + + public void RemoveTickProcessable(ITickProcessable toRemove) + { + _toRemoveQueue.Enqueue(toRemove); + } + + private static TimeSpan TimeBetweenTicks() => TimeSpan.FromSeconds(1) / TickFrequency; + + private DateTime GetNextTick() + { + DateTime nextSupposedTick = _lastTick + TimeBetweenTicks(); + if (DateTime.UtcNow < nextSupposedTick) + { + return nextSupposedTick; + } + + _lastTick = nextSupposedTick; + nextSupposedTick = _lastTick + TimeBetweenTicks(); + + return nextSupposedTick; + } + + private void TickLoop() + { + while (IsRunning) + { + try + { + DateTime timeToNextTick = GetNextTick(); + DateTime tickBegin = DateTime.UtcNow; + + var stopWatch = Stopwatch.StartNew(); + + if (!_toRemoveQueue.IsEmpty) + { + while (_toRemoveQueue.TryDequeue(out ITickProcessable processable)) + { + Log.Warn($"[TICK][{_workerName}] {processable.Name} removed"); + _processables.Remove(processable); + } + } + + if (!_toAddQueue.IsEmpty) + { + while (_toAddQueue.TryDequeue(out ITickProcessable processable)) + { + _processables.Add(processable); + } + } + + for (int index = 0; index < _processables.Count; index++) + { + try + { + ITickProcessable processable = _processables[index]; + var watch = Stopwatch.StartNew(); + + processable.ProcessTick(tickBegin); + + watch.Stop(); + } + catch (Exception e) + { + Log.Error("[TICK][PROCESS]", e); + } + } + + // Log.Debug($"[TICK_SYSTEM] {watch.ElapsedMilliseconds}ms to process {toProcess.Count} processable"); + stopWatch.Stop(); + DateTime finishedTime = DateTime.UtcNow; + long processingTime = stopWatch.ElapsedTicks; + // Log.Warn($"[TICK_SYSTEM][{_workerName}] processing took {processingTime}ms to process {_processables.Count.ToString()} systems"); + + Interlocked.Exchange(ref _averageProcessingTime, (int)processingTime); + TimeSpan sleepTime = timeToNextTick - finishedTime; + // Log.Debug($"[TICK_SYSTEM] sleeping {sleepTime.TotalMilliseconds}ms until next tick"); + if (sleepTime.TotalMilliseconds < 0) + { + continue; + } + + Thread.Sleep(sleepTime); + } + catch (Exception e) + { + Log.Error("[TICK_LOOP]", e); + } + } + } + } +} \ No newline at end of file diff --git a/srcs/GameChannel/Ticks/WorkerDispatchTickManager.cs b/srcs/GameChannel/Ticks/WorkerDispatchTickManager.cs new file mode 100644 index 0000000..f4d2740 --- /dev/null +++ b/srcs/GameChannel/Ticks/WorkerDispatchTickManager.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using WingsEmu.Game._ECS; + +namespace GameChannel.Ticks.DispatchQueueWork +{ + public class WorkerDispatchTickManager : ITickManager + { + private static readonly uint MaxTickWorkers = TickConfiguration.TickWorkers; + private readonly ConcurrentDictionary _processables = new(); + private readonly SemaphoreSlim _semaphoreSlim = new(1, 1); + private readonly DispatchedTickWorker[] _workers = new DispatchedTickWorker[MaxTickWorkers]; + private bool _isStarted; + + public WorkerDispatchTickManager() + { + for (int i = 0; i < MaxTickWorkers; i++) + { + _workers[i] = new DispatchedTickWorker(i); + } + } + + public void AddProcessable(ITickProcessable processable) + { + if (_processables.ContainsKey(processable.Id)) + { + return; + } + + DispatchedTickWorker worker = _workers.OrderBy(s => s.AverageProcessingTime).First(); + _processables.TryAdd(processable.Id, worker.Id); + worker.AddTickProcessable(processable); + } + + public void RemoveProcessable(ITickProcessable processable) + { + if (!_processables.Remove(processable.Id, out int workerId)) + { + return; + } + + _workers[workerId].RemoveTickProcessable(processable); + } + + public void Start() + { + _semaphoreSlim.Wait(); + try + { + if (_isStarted) + { + return; + } + + _isStarted = true; + + for (int i = 0; i < MaxTickWorkers; i++) + { + _workers[i].Start(); + } + } + finally + { + _semaphoreSlim.Release(); + } + } + + public void Stop() + { + _semaphoreSlim.Wait(); + try + { + if (!_isStarted) + { + return; + } + + _isStarted = false; + + for (int i = 0; i < MaxTickWorkers; i++) + { + _workers[i].Stop(); + } + } + finally + { + _semaphoreSlim.Release(); + } + } + } +} \ No newline at end of file diff --git a/srcs/GameChannel/Utils/EnvironmentConsts.cs b/srcs/GameChannel/Utils/EnvironmentConsts.cs new file mode 100644 index 0000000..fc98064 --- /dev/null +++ b/srcs/GameChannel/Utils/EnvironmentConsts.cs @@ -0,0 +1,34 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace GameChannel.Utils +{ + public static class EnvironmentConsts + { + /* + * FilePath configurations + */ + public const string PLUGIN_PATH = "PLUGINS_PATH"; + public const string CONFIG_PATH = "CONFIG_PATH"; + + /* + * MASTER Communication env keys + * GRPC + */ + public const string MASTER_IP = "MASTER_IP"; + public const string MASTER_PORT = "MASTER_PORT"; + public const string MASTER_SSL_PATH = "MASTER_SSL_PATH"; + + /* + * World server + */ + public const string GAME_SERVER_IP = "GAME_SERVER_IP"; + public const string GAME_SERVER_PORT = "GAME_SERVER_PORT"; + public const string GAME_SERVER_GROUP = "GAME_SERVER_GROUP"; + public const string GAME_SERVER_SESSION_LIMIT = "GAME_SERVER_SESSION_LIMIT"; + public const string GAME_SERVER_CHANNEL_ID = "GAME_SERVER_CHANNEL_ID"; + public const string GAME_SERVER_CHANNEL_TYPE = "GAME_SERVER_CHANNEL_TYPE"; + public const string GAME_SERVER_AUTHORITY = "GAME_SERVER_AUTHORITY"; + } +} \ No newline at end of file diff --git a/srcs/GameChannel/Utils/GracefulShutdown.cs b/srcs/GameChannel/Utils/GracefulShutdown.cs new file mode 100644 index 0000000..dfebbc4 --- /dev/null +++ b/srcs/GameChannel/Utils/GracefulShutdown.cs @@ -0,0 +1,48 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Runtime.Loader; +using System.Threading; +using PhoenixLib.Logging; + +namespace GameChannel.Utils +{ + public class DockerGracefulStopService : IDisposable + { + private readonly ManualResetEventSlim _stoppedEvent; + + public DockerGracefulStopService() + { + TokenSource = new CancellationTokenSource(); + _stoppedEvent = new ManualResetEventSlim(); + // SIGINT + Console.CancelKeyPress += (sender, eventArgs) => GracefulStop(TokenSource, _stoppedEvent); + // SIGTERM + AssemblyLoadContext.Default.Unloading += context => GracefulStop(TokenSource, _stoppedEvent); + // EXCEPTION + AppDomain.CurrentDomain.UnhandledException += (sender, args) => + { + Log.Error("UnhandledException", args.ExceptionObject as Exception); + GracefulStop(TokenSource, _stoppedEvent); + }; + } + + public CancellationToken CancellationToken => TokenSource.Token; + public CancellationTokenSource TokenSource { get; } + + public void Dispose() + { + _stoppedEvent.Set(); + } + + private static void GracefulStop(CancellationTokenSource cancellationTokenSource, ManualResetEventSlim stoppedEvent) + { + Log.Info("DockerGracefulStopService Stopping service"); + cancellationTokenSource.Cancel(); + stoppedEvent.Wait(); + Log.Info("DockerGracefulStopService Stop finished"); + } + } +} \ No newline at end of file diff --git a/srcs/GameChannel/Utils/ISpamProtector.cs b/srcs/GameChannel/Utils/ISpamProtector.cs new file mode 100644 index 0000000..d61c4c5 --- /dev/null +++ b/srcs/GameChannel/Utils/ISpamProtector.cs @@ -0,0 +1,11 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace GameChannel.Utils +{ + public interface ISpamProtector + { + bool CanConnect(string ipAddress); + } +} \ No newline at end of file diff --git a/srcs/GameChannel/Utils/MapsterMapper.cs b/srcs/GameChannel/Utils/MapsterMapper.cs new file mode 100644 index 0000000..831134e --- /dev/null +++ b/srcs/GameChannel/Utils/MapsterMapper.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using Mapster; +using PhoenixLib.DAL; + +namespace GameChannel.Utils +{ + public class MapsterMapper : IMapper + { + public void Map(TEntity input, TDto output) + { + input.Adapt(output); + } + + public TEntity Map(TDto input) => input.Adapt(); + + public List Map(List input) => input.Adapt, List>(); + + public IEnumerable Map(IEnumerable input) => input.Adapt, IEnumerable>(); + + public IReadOnlyList Map(IReadOnlyList input) => input.Adapt>(); + + public TDto Map(TEntity input) => input.Adapt(); + + public List Map(List input) => input.Adapt, List>(); + + public IEnumerable Map(IEnumerable input) => input.Adapt, IEnumerable>(); + + public IReadOnlyList Map(IReadOnlyList input) => input.Adapt>(); + + public void Map(TDto input, TEntity output) + { + input.Adapt(output); + } + } +} \ No newline at end of file diff --git a/srcs/GameChannel/Utils/SmartSpamProtector.cs b/srcs/GameChannel/Utils/SmartSpamProtector.cs new file mode 100644 index 0000000..fd4eec2 --- /dev/null +++ b/srcs/GameChannel/Utils/SmartSpamProtector.cs @@ -0,0 +1,58 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using PhoenixLib.Logging; +using WingsEmu.Core.Generics; + +namespace GameChannel.Utils +{ + /// + /// Todo MOVE to Redis + AlertManager + Prometheus + /// + public class SmartSpamProtector : ISpamProtector + { + private const int CONNECTION_ATTEMPTS_BEFORE_BLACKLIST = 2; + private static readonly TimeSpan TimeBetweenConnection = TimeSpan.FromMilliseconds(150); + + private static readonly ThreadSafeHashSet BlacklistedIps = new(); + private static readonly ConcurrentDictionary> ConnectionsByIp = new(); + + public bool CanConnect(string ipAddress) + { + if (BlacklistedIps.Contains(ipAddress)) + { + return false; + } + + if (!ConnectionsByIp.TryGetValue(ipAddress, out List dates)) + { + dates = new List(); + ConnectionsByIp[ipAddress] = dates; + } + + DateTime lastConnection = dates.LastOrDefault(); + dates.Add(DateTime.UtcNow); + + if (dates.Count > CONNECTION_ATTEMPTS_BEFORE_BLACKLIST) + { + BlacklistedIps.Add(ipAddress); + Log.Warn($"[SPAM_PROTECTOR] Blacklisted {ipAddress}"); + return false; + } + + // should be accepted + if (lastConnection.Add(TimeBetweenConnection) >= DateTime.UtcNow) + { + return false; + } + + dates.Clear(); + return true; + } + } +} \ No newline at end of file diff --git a/srcs/GameChannel/WorldServerSingleton.cs b/srcs/GameChannel/WorldServerSingleton.cs new file mode 100644 index 0000000..e428985 --- /dev/null +++ b/srcs/GameChannel/WorldServerSingleton.cs @@ -0,0 +1,21 @@ +using System; +using GameChannel.Utils; +using WingsAPI.Communication.ServerApi.Protocol; +using WingsEmu.DTOs.Account; + +namespace GameChannel +{ + public static class WorldServerSingleton + { + public static SerializableGameServer Instance { get; } = new() + { + EndPointIp = Environment.GetEnvironmentVariable(EnvironmentConsts.GAME_SERVER_IP) ?? "127.0.0.1", + EndPointPort = Convert.ToInt32(Environment.GetEnvironmentVariable(EnvironmentConsts.GAME_SERVER_PORT) ?? "8000"), + WorldGroup = Environment.GetEnvironmentVariable(EnvironmentConsts.GAME_SERVER_GROUP) ?? "NosWings", + AccountLimit = Convert.ToInt32(Environment.GetEnvironmentVariable(EnvironmentConsts.GAME_SERVER_SESSION_LIMIT) ?? "500"), + ChannelId = Convert.ToInt32(Environment.GetEnvironmentVariable(EnvironmentConsts.GAME_SERVER_CHANNEL_ID) ?? "1"), + ChannelType = Enum.Parse(Environment.GetEnvironmentVariable(EnvironmentConsts.GAME_SERVER_CHANNEL_TYPE) ?? GameChannelType.PVE_NORMAL.ToString(), true), + Authority = (AuthorityType)Convert.ToInt32(Environment.GetEnvironmentVariable(EnvironmentConsts.GAME_SERVER_AUTHORITY) ?? $"{((int)AuthorityType.User).ToString()}") + }; + } +} \ No newline at end of file diff --git a/srcs/LoginServer/Auth/IClientVersionCheckingService.cs b/srcs/LoginServer/Auth/IClientVersionCheckingService.cs new file mode 100644 index 0000000..318749b --- /dev/null +++ b/srcs/LoginServer/Auth/IClientVersionCheckingService.cs @@ -0,0 +1,13 @@ +using System.Threading.Tasks; + +namespace LoginServer.Auth +{ + /// + /// Todo dedicated authoritative service + /// + public interface IClientVersionCheckingService + { + Task IsAuthorized(string sentHash); + Task GetClientVersion(string sentHash); + } +} \ No newline at end of file diff --git a/srcs/LoginServer/Auth/RedisCachedHardwareIdService.cs b/srcs/LoginServer/Auth/RedisCachedHardwareIdService.cs new file mode 100644 index 0000000..862c59e --- /dev/null +++ b/srcs/LoginServer/Auth/RedisCachedHardwareIdService.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using StackExchange.Redis; +using WingsAPI.Communication.Auth; + +namespace LoginServer.Auth +{ + public class RedisCachedHardwareIdService : IHardwareIdService + { + private const string DataPrefix = "blacklist:hardware-id:"; + private readonly IDatabase _database; + + public RedisCachedHardwareIdService(IConnectionMultiplexer connectionMultiplexer) => _database = connectionMultiplexer.GetDatabase(1); + + public Task SynchronizeWithDbAsync(IEnumerable dtos) => Task.FromResult(true); + + public async Task CanLogin(string hardwareId) => await _database.KeyExistsAsync(GetKey(hardwareId)) == false; + + public async Task RegisterHardwareId(string hardwareId) + { + await _database.StringSetAsync(GetKey(hardwareId), hardwareId); + } + + public async Task UnregisterHardwareId(string hardwareId) + { + await _database.KeyDeleteAsync(GetKey(hardwareId)); + } + + private static string GetKey(string hwid) => DataPrefix + hwid; + } +} \ No newline at end of file diff --git a/srcs/LoginServer/Auth/RedisClientVersionCheckingService.cs b/srcs/LoginServer/Auth/RedisClientVersionCheckingService.cs new file mode 100644 index 0000000..75a6e09 --- /dev/null +++ b/srcs/LoginServer/Auth/RedisClientVersionCheckingService.cs @@ -0,0 +1,28 @@ +using System.Threading.Tasks; +using StackExchange.Redis; +using WingsAPI.Communication.Auth; + +namespace LoginServer.Auth +{ + public class RedisClientVersionCheckingService : IClientVersionCheckingService + { + private const string KEY_PREFIX = "client-version:cache:"; + private readonly IDatabase _database; + + public RedisClientVersionCheckingService(IConnectionMultiplexer connectionMultiplexer) => _database = connectionMultiplexer.GetDatabase(1); + + public async Task IsAuthorized(string sentHash) => await _database.KeyExistsAsync(KEY_PREFIX + sentHash); + + public async Task GetClientVersion(string sentHash) => await _database.StringGetAsync(KEY_PREFIX + sentHash); + + public async Task AddAuthorizedClient(AuthorizedClientVersionDto clientVersion) + { + await _database.StringSetAsync(KEY_PREFIX + clientVersion.GetComputedClientHash(), clientVersion.ClientVersion); + } + + public async Task RemoveAuthorizedClient(AuthorizedClientVersionDto clientVersion) + { + await _database.KeyDeleteAsync(KEY_PREFIX + clientVersion.GetComputedClientHash()); + } + } +} \ No newline at end of file diff --git a/srcs/LoginServer/Auth/StringHashExtensions.cs b/srcs/LoginServer/Auth/StringHashExtensions.cs new file mode 100644 index 0000000..31c6bb2 --- /dev/null +++ b/srcs/LoginServer/Auth/StringHashExtensions.cs @@ -0,0 +1,25 @@ +using System; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using WingsAPI.Communication.Auth; + +namespace LoginServer.Auth +{ + internal static class StringHashExtensions + { + public static string ToSha256(this string str) + { + using var hashing = SHA256.Create(); + return string.Concat(hashing.ComputeHash(Encoding.UTF8.GetBytes(str)).Select((Func)(item => item.ToString("x2")))); + } + + public static string GetComputedClientHash(this AuthorizedClientVersionDto clientVersion) + { + string dllHash = clientVersion.DllHash; + string executableHash = clientVersion.ExecutableHash; + + return (executableHash + dllHash).ToSha256(); + } + } +} \ No newline at end of file diff --git a/srcs/LoginServer/Handlers/GenericLoginPacketHandlerBase.cs b/srcs/LoginServer/Handlers/GenericLoginPacketHandlerBase.cs new file mode 100644 index 0000000..76d14c1 --- /dev/null +++ b/srcs/LoginServer/Handlers/GenericLoginPacketHandlerBase.cs @@ -0,0 +1,28 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Threading.Tasks; +using LoginServer.Network; +using WingsEmu.Packets; + +namespace LoginServer.Handlers +{ + public abstract class GenericLoginPacketHandlerBase : IPacketHandler where T : IPacket + { + public async Task HandleAsync(LoginClientSession session, IPacket packet) + { + if (packet is T typedPacket) + { + await HandlePacketAsync(session, typedPacket); + } + } + + public void Handle(LoginClientSession session, IPacket packet) + { + HandleAsync(session, packet).ConfigureAwait(false).GetAwaiter().GetResult(); + } + + protected abstract Task HandlePacketAsync(LoginClientSession session, T packet); + } +} \ No newline at end of file diff --git a/srcs/LoginServer/Handlers/GlobalPacketProcessor.cs b/srcs/LoginServer/Handlers/GlobalPacketProcessor.cs new file mode 100644 index 0000000..f52530e --- /dev/null +++ b/srcs/LoginServer/Handlers/GlobalPacketProcessor.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using LoginServer.Network; +using WingsEmu.Packets; + +namespace LoginServer.Handlers +{ + public class GlobalPacketProcessor : IGlobalPacketProcessor + { + private readonly Dictionary _handlers = new(); + + public void RegisterHandler(Type packetType, IPacketHandler packetHandler) + { + _handlers[packetType] = packetHandler; + } + + public void Execute(LoginClientSession session, IPacket packet, Type packetType) + { + if (!_handlers.TryGetValue(packetType, out IPacketHandler handler)) + { + return; + } + + handler.HandleAsync(session, packet).ConfigureAwait(false).GetAwaiter().GetResult(); + } + } +} \ No newline at end of file diff --git a/srcs/LoginServer/Handlers/IGlobalPacketProcessor.cs b/srcs/LoginServer/Handlers/IGlobalPacketProcessor.cs new file mode 100644 index 0000000..3207c3d --- /dev/null +++ b/srcs/LoginServer/Handlers/IGlobalPacketProcessor.cs @@ -0,0 +1,12 @@ +using System; +using LoginServer.Network; +using WingsEmu.Packets; + +namespace LoginServer.Handlers +{ + public interface IGlobalPacketProcessor + { + public void RegisterHandler(Type packetType, IPacketHandler packetHandler); + public void Execute(LoginClientSession session, IPacket packet, Type packetType); + } +} \ No newline at end of file diff --git a/srcs/LoginServer/Handlers/IPacketHandler.cs b/srcs/LoginServer/Handlers/IPacketHandler.cs new file mode 100644 index 0000000..783e9ac --- /dev/null +++ b/srcs/LoginServer/Handlers/IPacketHandler.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; +using LoginServer.Network; +using WingsEmu.Packets; + +namespace LoginServer.Handlers +{ + public interface IPacketHandler + { + public Task HandleAsync(LoginClientSession session, IPacket packet); + } +} \ No newline at end of file diff --git a/srcs/LoginServer/Handlers/LoginPacketsExtensions.cs b/srcs/LoginServer/Handlers/LoginPacketsExtensions.cs new file mode 100644 index 0000000..3e26a0f --- /dev/null +++ b/srcs/LoginServer/Handlers/LoginPacketsExtensions.cs @@ -0,0 +1,53 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using System.Text; +using LoginServer.Network; +using PhoenixLib.Logging; +using PhoenixLib.MultiLanguage; +using WingsAPI.Communication.ServerApi.Protocol; +using WingsEmu.Packets.Enums; + +namespace LoginServer.Handlers +{ + internal static class LoginPacketsExtensions + { + internal static string GenerateFailcPacket(this LoginClientSession session, LoginFailType failType) => $"failc {((short)failType).ToString()}"; + + internal static void SendChannelPacketList(this LoginClientSession session, int encryptionKey, string sessionId, RegionLanguageType region, IEnumerable worldServers, + bool isOldLogin) + { + string lastGroup = string.Empty; + int worldGroupCount = 0; + var packetBuilder = new StringBuilder(); + packetBuilder.AppendFormat($"NsTeST {(byte)region} {sessionId} 2 "); + + packetBuilder.Append( + isOldLogin + ? $"-99 0 -99 0 -99 0 -99 0 -99 0 -99 0 -99 0 -99 0 -99 0 -99 0 -99 0 -99 0 -99 0 -99 0 -99 0 -99 0 -99 0 -99 0 -99 0 -99 0 -99 0 -99 0 -99 0 -99 0 -99 0 -99 0 -99 0 -99 0 -99 0 -99 0 -99 0 -99 0 -99 0 -99 0 -99 0 -99 {encryptionKey} " + : $"-99 0 -99 0 -99 0 -99 0 -99 0 -99 0 -99 0 -99 0 -99 0 -99 0 -99 0 -99 0 -99 0 -99 0 -99 0 -99 0 -99 0 -99 0 -99 0 -99 0 -99 0 -99 0 -99 0 -99 0 -99 0 -99 0 -99 0 -99 0 -99 0 -99 0 -99 0 -99 0 -99 0 -99 0 -99 0 -99 0 {encryptionKey} "); + + foreach (SerializableGameServer world in worldServers) + { + if (lastGroup != world.WorldGroup) + { + worldGroupCount++; + } + + lastGroup = world.WorldGroup; + int color = (int)(world.SessionCount / (double)world.AccountLimit * 20); + packetBuilder.AppendFormat("{0}:{1}:{2}:{3}.{4}.{5} ", world.EndPointIp, world.EndPointPort, color, worldGroupCount, world.ChannelId, world.WorldGroup.Replace(' ', '^')); + } + + packetBuilder.Append("-1:-1:-1:10000.10000.1"); + + string packet = packetBuilder.ToString(); + + Log.Info($"[CHANNEL_LIST] {packet}"); + + session.SendPacket(packet); + } + } +} \ No newline at end of file diff --git a/srcs/LoginServer/Handlers/TypedCredentialsLoginPacketHandler.cs b/srcs/LoginServer/Handlers/TypedCredentialsLoginPacketHandler.cs new file mode 100644 index 0000000..dfae0d6 --- /dev/null +++ b/srcs/LoginServer/Handlers/TypedCredentialsLoginPacketHandler.cs @@ -0,0 +1,186 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Linq; +using System.Threading.Tasks; +using LoginServer.Network; +using PhoenixLib.Logging; +using PhoenixLib.MultiLanguage; +using WingsAPI.Communication; +using WingsAPI.Communication.DbServer.AccountService; +using WingsAPI.Communication.ServerApi; +using WingsAPI.Communication.ServerApi.Protocol; +using WingsAPI.Communication.Sessions; +using WingsAPI.Communication.Sessions.Model; +using WingsAPI.Communication.Sessions.Request; +using WingsAPI.Communication.Sessions.Response; +using WingsAPI.Data.Account; +using WingsEmu.DTOs.Account; +using WingsEmu.Health; +using WingsEmu.Packets.ClientPackets; +using WingsEmu.Packets.Enums; + +namespace LoginServer.Handlers +{ + public class TypedCredentialsLoginPacketHandler : GenericLoginPacketHandlerBase + { + private readonly IAccountService _accountService; + private readonly IMaintenanceManager _maintenanceManager; + private readonly IServerApiService _serverApiService; + private readonly ISessionService _sessionService; + + public TypedCredentialsLoginPacketHandler(ISessionService sessionService, IServerApiService serverApiService, IMaintenanceManager maintenanceManager, IAccountService accountService) + { + _sessionService = sessionService; + _serverApiService = serverApiService; + _maintenanceManager = maintenanceManager; + _accountService = accountService; + } + + protected override async Task HandlePacketAsync(LoginClientSession session, Nos0575Packet nos0575Packet) + { + if (nos0575Packet == null) + { + return; + } + + AccountLoadResponse accountLoadResponse = null; + try + { + accountLoadResponse = await _accountService.LoadAccountByName(new AccountLoadByNameRequest + { + Name = nos0575Packet.Name + }); + } + catch (Exception e) + { + Log.Error("[NEW_TYPED_AUTH] Unexpected error: ", e); + } + + if (accountLoadResponse?.ResponseType != RpcResponseType.SUCCESS) + { + Log.Warn($"[NEW_TYPED_AUTH] Failed to load account for accountName: '{nos0575Packet.Name}'"); + session.SendPacket(session.GenerateFailcPacket(LoginFailType.AccountOrPasswordWrong)); + session.Disconnect(); + return; + } + + AccountDTO loadedAccount = accountLoadResponse.AccountDto; + if (!string.Equals(loadedAccount.Password, nos0575Packet.Password, StringComparison.CurrentCultureIgnoreCase)) + { + session.SendPacket(session.GenerateFailcPacket(LoginFailType.AccountOrPasswordWrong)); + Log.Debug($"[NEW_TYPED_AUTH] WRONG_CREDENTIALS : {loadedAccount.Name}"); + session.Disconnect(); + return; + } + + SessionResponse modelResponse = await _sessionService.CreateSession(new CreateSessionRequest + { + AccountId = loadedAccount.Id, + AccountName = loadedAccount.Name, + AuthorityType = loadedAccount.Authority, + IpAddress = session.IpAddress + }); + + if (modelResponse.ResponseType != RpcResponseType.SUCCESS) + { + Log.Debug($"[NEW_TYPED_AUTH] FAILED TO CREATE SESSION {loadedAccount.Id}"); + session.SendPacket(session.GenerateFailcPacket(LoginFailType.AlreadyConnected)); + session.Disconnect(); + return; + } + + AuthorityType type = loadedAccount.Authority; + + AccountBanGetResponse banResponse = null; + try + { + banResponse = await _accountService.GetAccountBan(new AccountBanGetRequest + { + AccountId = loadedAccount.Id + }); + } + catch (Exception e) + { + Log.Error("[NEW_TYPED_AUTH] Unexpected error: ", e); + } + + if (banResponse?.ResponseType != RpcResponseType.SUCCESS) + { + Log.Warn($"[NEW_TYPED_AUTH] Failed to get account ban for accountId: '{loadedAccount.Id.ToString()}'"); + session.SendPacket(session.GenerateFailcPacket(LoginFailType.UnhandledError)); + session.Disconnect(); + return; + } + + AccountBanDto characterPenalty = banResponse.AccountBanDto; + if (characterPenalty != null) + { + session.SendPacket(session.GenerateFailcPacket(LoginFailType.Banned)); + Log.Debug($"[NEW_TYPED_AUTH] ACCOUNT_BANNED : {loadedAccount.Name}"); + session.Disconnect(); + return; + } + + switch (type) + { + case AuthorityType.Banned: + session.SendPacket(session.GenerateFailcPacket(LoginFailType.Banned)); + Log.Debug("[NEW_TYPED_AUTH] ACCOUNT_BANNED"); + session.Disconnect(); + break; + + case AuthorityType.Unconfirmed: + case AuthorityType.Closed: + session.SendPacket(session.GenerateFailcPacket(LoginFailType.CantConnect)); + Log.Debug("[NEW_TYPED_AUTH] ACCOUNT_NOT_VERIFIED"); + session.Disconnect(); + break; + + default: + if (_maintenanceManager.IsMaintenanceActive && loadedAccount.Authority < AuthorityType.GameMaster) + { + session.SendPacket(session.GenerateFailcPacket(LoginFailType.Maintenance)); + return; + } + + SessionResponse connectResponse = await _sessionService.ConnectToLoginServer(new ConnectToLoginServerRequest + { + AccountId = loadedAccount.Id, + ClientVersion = "BYPASS", + HardwareId = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }); + + if (connectResponse.ResponseType != RpcResponseType.SUCCESS) + { + Log.Warn("[NEW_AUTH] General Error SessionId: " + session.Id); + session.SendPacket(session.GenerateFailcPacket(LoginFailType.CantConnect)); + session.Disconnect(); + return; + } + + Session connectedSession = connectResponse.Session; + + Log.Debug($"[NEW_TYPED_AUTH] Connected : {nos0575Packet.Name}:{connectedSession.EncryptionKey}:{connectedSession.HardwareId}"); + + RetrieveRegisteredWorldServersResponse worldServersResponse = await _serverApiService.RetrieveRegisteredWorldServers(new RetrieveRegisteredWorldServersRequest + { + RequesterAuthority = loadedAccount.Authority + }); + + if (worldServersResponse?.WorldServers is null || !worldServersResponse.WorldServers.Any()) + { + session.SendPacket(session.GenerateFailcPacket(LoginFailType.Maintenance)); + session.Disconnect(); + return; + } + + session.SendChannelPacketList(connectedSession.EncryptionKey, loadedAccount.Name, RegionLanguageType.EN, worldServersResponse.WorldServers, true); + session.Disconnect(); + break; + } + } + } +} \ No newline at end of file diff --git a/srcs/LoginServer/LoginServer.csproj b/srcs/LoginServer/LoginServer.csproj new file mode 100644 index 0000000..8c7e2ed --- /dev/null +++ b/srcs/LoginServer/LoginServer.csproj @@ -0,0 +1,24 @@ + + + + net5.0 + ..\..\dist\login-server\ + false + + + + + + + + + + + + + + + + + + diff --git a/srcs/LoginServer/Network/LoginClientSession.cs b/srcs/LoginServer/Network/LoginClientSession.cs new file mode 100644 index 0000000..f66cc27 --- /dev/null +++ b/srcs/LoginServer/Network/LoginClientSession.cs @@ -0,0 +1,124 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Net; +using System.Net.Sockets; +using System.Text; +using LoginServer.Handlers; +using NetCoreServer; +using PhoenixLib.Logging; +using WingsEmu.Packets; + +namespace LoginServer.Network +{ + public class LoginClientSession : TcpSession + { + private readonly IPacketDeserializer _deserializer; + private readonly IGlobalPacketProcessor _loginHandlers; + + + public LoginClientSession(TcpServer server, IGlobalPacketProcessor globalPacketProcessor, IPacketDeserializer deserializer) : base(server) + { + _loginHandlers = globalPacketProcessor; + _deserializer = deserializer; + } + + public string IpAddress { get; private set; } + + public void SendPacket(string packet) => Send(NostaleLoginEncrypter.Encode(packet, Encoding.Default).ToArray()); + + protected override void OnConnected() + { + try + { + if (IsDisposed) + { + return; + } + + if (IsSocketDisposed) + { + Disconnect(); + return; + } + + if (Socket == null) + { + return; + } + + if (Socket?.RemoteEndPoint is IPEndPoint ip) + { + IpAddress = ip.Address.ToString(); + } + } + catch (Exception e) + { + Log.Error("[LOGIN_SERVER_SESSION] OnConnected", e); + Disconnect(); + } + } + + protected override void OnReceived(byte[] buffer, long offset, long size) + { + try + { + string packet = NostaleLoginDecrypter.Decode(buffer.AsSpan((int)offset, (int)size)); + string[] packetSplit = packet.Replace('^', ' ').Split(' '); + string packetHeader = packetSplit[0]; + if (string.IsNullOrWhiteSpace(packetHeader)) + { + Disconnect(); + return; + } + + TriggerHandler(packetHeader.Replace("#", ""), packet); + } + catch + { + Disconnect(); + } + } + + + private void TriggerHandler(string packetHeader, string packetString) + { + if (IsDisposed) + { + return; + } + + try + { + (IClientPacket typedPacket, Type packetType) = _deserializer.Deserialize(packetString, false); + + if (packetType == typeof(UnresolvedPacket) && typedPacket != null) + { + Log.Warn($"UNRESOLVED_PACKET : {packetHeader}"); + return; + } + + if (packetType == null && typedPacket == null) + { + Log.Debug($"DESERIALIZATION_ERROR : {packetString}"); + return; + } + + _loginHandlers.Execute(this, typedPacket, packetType); + } + catch (Exception ex) + { + // disconnect if something unexpected happens + Log.Error("Handler Error SessionId: " + Id, ex); + Disconnect(); + } + } + + protected override void OnError(SocketError error) + { + Disconnect(); + } + } +} \ No newline at end of file diff --git a/srcs/LoginServer/Network/LoginServer.cs b/srcs/LoginServer/Network/LoginServer.cs new file mode 100644 index 0000000..fea87ab --- /dev/null +++ b/srcs/LoginServer/Network/LoginServer.cs @@ -0,0 +1,77 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Net; +using System.Net.Sockets; +using LoginServer.Handlers; +using LoginServer.Utils; +using NetCoreServer; +using PhoenixLib.Logging; +using WingsEmu.Packets; + +namespace LoginServer.Network +{ + public class LoginServer : TcpServer + { + private readonly IGlobalPacketProcessor _globalPacketProcessor; + private readonly IPacketDeserializer _packetDeserializer; + private readonly ISpamProtector _spamProtector; + + public LoginServer(IPAddress address, int port, ISpamProtector spamProtector, IGlobalPacketProcessor globalPacketProcessor, IPacketDeserializer packetDeserializer) : base(address, port) + { + _spamProtector = spamProtector; + _globalPacketProcessor = globalPacketProcessor; + _packetDeserializer = packetDeserializer; + } + + protected override TcpSession CreateSession() + { + Log.Info("[TCP_SERVER] CreateSession"); + var tmp = new LoginClientSession(this, _globalPacketProcessor, _packetDeserializer); + return tmp; + } + + protected override void OnConnected(TcpSession session) + { + try + { + if (session.IsSocketDisposed) + { + return; + } + + if (session.Socket.RemoteEndPoint is not IPEndPoint ip) + { + session.Disconnect(); + return; + } + + if (!_spamProtector.CanConnect(ip.Address.ToString())) + { + session.Disconnect(); + return; + } + + Log.Info($"[TCP_SERVER] Connected : {ip.Address}"); + } + catch (Exception e) + { + Log.Error("[TCP_SERVER] OnConnected", e); + session.Dispose(); + } + } + + protected override void OnStarted() + { + Log.Info("[TCP-SERVER] Started!"); + } + + protected override void OnError(SocketError error) + { + Log.Info("[TCP-SERVER] SocketError"); + Stop(); + } + } +} \ No newline at end of file diff --git a/srcs/LoginServer/NostaleLoginDecrypter.cs b/srcs/LoginServer/NostaleLoginDecrypter.cs new file mode 100644 index 0000000..1718aaf --- /dev/null +++ b/srcs/LoginServer/NostaleLoginDecrypter.cs @@ -0,0 +1,25 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Text; + +namespace LoginServer +{ + public static class NostaleLoginDecrypter + { + public static string Decode(ReadOnlySpan bytesBuffer) + { + var decryptedPacket = new StringBuilder(); + foreach (byte character in bytesBuffer) + { + decryptedPacket.Append(character > 14 + ? Convert.ToChar(character - 0xF ^ 0xC3) + : Convert.ToChar(0x100 - (0xF - character) ^ 0xC3)); + } + + return decryptedPacket.ToString(); + } + } +} \ No newline at end of file diff --git a/srcs/LoginServer/NostaleLoginEncrypter.cs b/srcs/LoginServer/NostaleLoginEncrypter.cs new file mode 100644 index 0000000..54a285d --- /dev/null +++ b/srcs/LoginServer/NostaleLoginEncrypter.cs @@ -0,0 +1,30 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Text; + +namespace LoginServer +{ + public static class NostaleLoginEncrypter + { + public static ReadOnlyMemory Encode(string packet, Encoding encoding) + { + packet += " "; + byte[] tmp = encoding.GetBytes(packet); + if (tmp.Length == 0) + { + return null; + } + + for (int i = 0; i < packet.Length; i++) + { + tmp[i] = Convert.ToByte(tmp[i] + 15); + } + + tmp[^1] = 25; + return tmp; + } + } +} \ No newline at end of file diff --git a/srcs/LoginServer/Program.cs b/srcs/LoginServer/Program.cs new file mode 100644 index 0000000..b47d679 --- /dev/null +++ b/srcs/LoginServer/Program.cs @@ -0,0 +1,138 @@ +using System; +using System.Linq; +using System.Net; +using System.Reflection; +using System.Threading.Tasks; +using LoginServer.Handlers; +using LoginServer.Utils; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using PhoenixLib.Logging; +using PhoenixLib.ServiceBus.MQTT; +using WingsAPI.Communication; +using WingsAPI.Communication.ServerApi; +using WingsEmu.Packets; +using WingsEmu.Packets.ClientPackets; + +namespace LoginServer +{ + public class Program + { + private static void PrintHeader() + { + Console.Title = "WingsEmu - Login"; + const string text = @" +██╗ ██╗██╗███╗ ██╗ ██████╗ ███████╗███████╗███╗ ███╗██╗ ██╗ +██║ ██║██║████╗ ██║██╔════╝ ██╔════╝██╔════╝████╗ ████║██║ ██║ +██║ █╗ ██║██║██╔██╗ ██║██║ ███╗███████╗█████╗ ██╔████╔██║██║ ██║ +██║███╗██║██║██║╚██╗██║██║ ██║╚════██║██╔══╝ ██║╚██╔╝██║██║ ██║ +╚███╔███╔╝██║██║ ╚████║╚██████╔╝███████║███████╗██║ ╚═╝ ██║╚██████╔╝ + ╚══╝╚══╝ ╚═╝╚═╝ ╚═══╝ ╚═════╝ ╚══════╝╚══════╝╚═╝ ╚═╝ ╚═════╝ + +██╗ ██████╗ ██████╗ ██╗███╗ ██╗ ███████╗███████╗██████╗ ██╗ ██╗███████╗██████╗ +██║ ██╔═══██╗██╔════╝ ██║████╗ ██║ ██╔════╝██╔════╝██╔══██╗██║ ██║██╔════╝██╔══██╗ +██║ ██║ ██║██║ ███╗██║██╔██╗ ██║█████╗███████╗█████╗ ██████╔╝██║ ██║█████╗ ██████╔╝ +██║ ██║ ██║██║ ██║██║██║╚██╗██║╚════╝╚════██║██╔══╝ ██╔══██╗╚██╗ ██╔╝██╔══╝ ██╔══██╗ +███████╗╚██████╔╝╚██████╔╝██║██║ ╚████║ ███████║███████╗██║ ██║ ╚████╔╝ ███████╗██║ ██║ +╚══════╝ ╚═════╝ ╚═════╝ ╚═╝╚═╝ ╚═══╝ ╚══════╝╚══════╝╚═╝ ╚═╝ ╚═══╝ ╚══════╝╚═╝ ╚═╝ +"; + string separator = new('=', Console.WindowWidth); + string logo = text.Split('\n').Select(s => string.Format("{0," + (Console.WindowWidth / 2 + s.Length / 2) + "}\n", s)) + .Aggregate("", (current, i) => current + i); + + Console.ForegroundColor = ConsoleColor.Cyan; + Console.WriteLine(separator + logo + $"Version: {Assembly.GetExecutingAssembly().GetName().Version}\n" + separator); + Console.ForegroundColor = ConsoleColor.White; + } + + public static async Task Main(string[] args) + { + // workaround + // http2 needs SSL + // https://github.com/grpc/grpc-dotnet/issues/626 + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); + using var stopService = new DockerGracefulStopService(); + PrintHeader(); + + using IHost host = CreateHostBuilder(args).Build(); + { + IServiceProvider services = host.Services; + + await host.StartAsync(); + + // register packet handlers + IGlobalPacketProcessor processor = services.GetRequiredService(); + + processor.RegisterHandler(typeof(Nos0575Packet), services.GetRequiredService()); + + IServerApiService master = services.GetRequiredService(); + + BasicRpcResponse response = null; + while (response == null) + { + try + { + response = await master.IsMasterOnline(new ()); + } + catch + { + Log.Warn("Failed to contact with Master Server, retrying in 2 seconds..."); + await Task.Delay(TimeSpan.FromSeconds(2)); + } + } + + if (response.ResponseType != RpcResponseType.SUCCESS) + { + Log.Warn("Master Server has refused the registration of this Login Server"); + return; + } + + Log.Info("Login Server has been registered successfully"); + + IPacketDeserializer packetDeserializer = services.GetRequiredService(); + + int port = Convert.ToInt32(Environment.GetEnvironmentVariable("SERVER_PORT") ?? "4004"); + Network.LoginServer server; + try + { + server = new Network.LoginServer(IPAddress.Any, port, new SmartSpamProtector(), processor, packetDeserializer); + server.Start(); + } + catch (Exception ex) + { + Log.Error("General Error Server", ex); + await host.StopAsync(); + return; + } + + IMessagingService messagingService = host.Services.GetService(); + if (messagingService != null) + { + await messagingService.StartAsync(); + } + + Log.Info("Game Authentication service online !"); + await host.WaitForShutdownAsync(stopService.CancellationToken); + if (messagingService != null) + { + await messagingService.DisposeAsync(); + } + + server?.Stop(); + } + } + + + private static IHostBuilder CreateHostBuilder(string[] args) + { + IHostBuilder builder = Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + webBuilder.UseKestrel(); + }); + return builder; + } + } +} \ No newline at end of file diff --git a/srcs/LoginServer/Startup.cs b/srcs/LoginServer/Startup.cs new file mode 100644 index 0000000..b89f0b7 --- /dev/null +++ b/srcs/LoginServer/Startup.cs @@ -0,0 +1,50 @@ +using LoginServer.Auth; +using LoginServer.Handlers; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using PhoenixLib.Caching; +using PhoenixLib.DAL.Redis; +using PhoenixLib.Logging; +using PhoenixLib.ServiceBus.Extensions; +using WingsAPI.Communication.Auth; +using WingsAPI.Packets; +using WingsEmu.Communication.gRPC.Extensions; +using WingsEmu.Health.Extensions; +using WingsEmu.Packets; + +namespace LoginServer +{ + public class Startup + { + public void ConfigureServices(IServiceCollection services) + { + services.AddMqttConfigurationFromEnv(); + services.AddMaintenanceMode(); + services.AddPhoenixLogging(); + services.AddSingleton(); + + services.AddClientPacketsInAssembly(); + + services.TryAddSingleton(); + + services.AddTransient(); + + services.TryAddConnectionMultiplexerFromEnv(); + + services.TryAddSingleton(); + services.TryAddSingleton(); + + services.TryAddSingleton(typeof(IKeyValueCache<>), typeof(InMemoryKeyValueCache<>)); + services.AddGrpcSessionServiceClient(); + services.AddServerApiServiceClient(); + services.AddGrpcDbServerServiceClient(); + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseRouting(); + } + } +} \ No newline at end of file diff --git a/srcs/LoginServer/Utils/DockerGracefulStopService.cs b/srcs/LoginServer/Utils/DockerGracefulStopService.cs new file mode 100644 index 0000000..339a8f8 --- /dev/null +++ b/srcs/LoginServer/Utils/DockerGracefulStopService.cs @@ -0,0 +1,48 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Runtime.Loader; +using System.Threading; +using PhoenixLib.Logging; + +namespace LoginServer.Utils +{ + public class DockerGracefulStopService : IDisposable + { + private readonly ManualResetEventSlim _stoppedEvent; + + public DockerGracefulStopService() + { + TokenSource = new CancellationTokenSource(); + _stoppedEvent = new ManualResetEventSlim(); + // SIGINT + Console.CancelKeyPress += (sender, eventArgs) => GracefulStop(TokenSource, _stoppedEvent); + // SIGTERM + AssemblyLoadContext.Default.Unloading += context => GracefulStop(TokenSource, _stoppedEvent); + // EXCEPTION + AppDomain.CurrentDomain.UnhandledException += (sender, args) => + { + Log.Error("UnhandledException", args.ExceptionObject as Exception); + GracefulStop(TokenSource, _stoppedEvent); + }; + } + + public CancellationToken CancellationToken => TokenSource.Token; + public CancellationTokenSource TokenSource { get; } + + public void Dispose() + { + _stoppedEvent.Set(); + } + + private static void GracefulStop(CancellationTokenSource cancellationTokenSource, ManualResetEventSlim stoppedEvent) + { + Log.Info("DockerGracefulStopService Stopping service"); + cancellationTokenSource.Cancel(); + stoppedEvent.Wait(); + Log.Info("DockerGracefulStopService Stop finished"); + } + } +} \ No newline at end of file diff --git a/srcs/LoginServer/Utils/ISpamProtector.cs b/srcs/LoginServer/Utils/ISpamProtector.cs new file mode 100644 index 0000000..e0fd04e --- /dev/null +++ b/srcs/LoginServer/Utils/ISpamProtector.cs @@ -0,0 +1,11 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace LoginServer.Utils +{ + public interface ISpamProtector + { + bool CanConnect(string ipAddress); + } +} \ No newline at end of file diff --git a/srcs/LoginServer/Utils/SmartSpamProtector.cs b/srcs/LoginServer/Utils/SmartSpamProtector.cs new file mode 100644 index 0000000..fbc8bc6 --- /dev/null +++ b/srcs/LoginServer/Utils/SmartSpamProtector.cs @@ -0,0 +1,56 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Generic; +using System.Linq; +using PhoenixLib.Logging; + +namespace LoginServer.Utils +{ + /// + /// Todo MOVE to Redis + AlertManager + Prometheus + /// + public class SmartSpamProtector : ISpamProtector + { + private const int CONNECTION_ATTEMPTS_BEFORE_BLACKLIST = 2; + private static readonly TimeSpan TimeBetweenConnection = TimeSpan.FromMilliseconds(150); + + private static readonly HashSet BlacklistedIps = new(); + private static readonly Dictionary> ConnectionsByIp = new(); + + public bool CanConnect(string ipAddress) + { + if (BlacklistedIps.Contains(ipAddress)) + { + return false; + } + + if (!ConnectionsByIp.TryGetValue(ipAddress, out List dates)) + { + dates = new List(); + ConnectionsByIp[ipAddress] = dates; + } + + DateTime lastConnection = dates.LastOrDefault(); + dates.Add(DateTime.UtcNow); + + if (dates.Count > CONNECTION_ATTEMPTS_BEFORE_BLACKLIST) + { + BlacklistedIps.Add(ipAddress); + Log.Warn($"[SPAM_PROTECTOR] Blacklisted {ipAddress}"); + return false; + } + + // should be accepted + if (lastConnection.Add(TimeBetweenConnection) >= DateTime.UtcNow) + { + return false; + } + + dates.Clear(); + return true; + } + } +} \ No newline at end of file diff --git a/srcs/LogsServer/Consumers/ServiceFlushAllMessageConsumer.cs b/srcs/LogsServer/Consumers/ServiceFlushAllMessageConsumer.cs new file mode 100644 index 0000000..77ad976 --- /dev/null +++ b/srcs/LogsServer/Consumers/ServiceFlushAllMessageConsumer.cs @@ -0,0 +1,20 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; +using Plugin.MongoLogs.Services; +using WingsAPI.Communication.Services.Messages; + +namespace LogsServer.Consumers +{ + public class ServiceFlushAllMessageConsumer : IMessageConsumer + { + private readonly MongoLogsBackgroundService _mongoLogsBackgroundService; + + public ServiceFlushAllMessageConsumer(MongoLogsBackgroundService mongoLogsBackgroundService) => _mongoLogsBackgroundService = mongoLogsBackgroundService; + + public async Task HandleAsync(ServiceFlushAllMessage notification, CancellationToken token) + { + await _mongoLogsBackgroundService.ProcessMain(token); + } + } +} \ No newline at end of file diff --git a/srcs/LogsServer/DockerGracefulStopService.cs b/srcs/LogsServer/DockerGracefulStopService.cs new file mode 100644 index 0000000..daa8380 --- /dev/null +++ b/srcs/LogsServer/DockerGracefulStopService.cs @@ -0,0 +1,54 @@ +using System; +using System.Runtime.Loader; +using System.Threading; +using PhoenixLib.Logging; + +namespace LogsServer +{ + public class DockerGracefulStopService : IDisposable + { + private readonly CancellationTokenSource _cancellationTokenSource; + private readonly ManualResetEventSlim _stoppedEvent; + + public DockerGracefulStopService() + { + _cancellationTokenSource = new CancellationTokenSource(); + _stoppedEvent = new ManualResetEventSlim(); + + // SIGINT + Console.CancelKeyPress += (sender, eventArgs) => + { + Log.Error("[GRACEFUL_SHUTDOWN] SIGINT received", new Exception("[GRACEFUL_SHUTDOWN] SIGINT received")); + GracefulStop(_cancellationTokenSource, _stoppedEvent); + }; + + // SIGTERM + AssemblyLoadContext.Default.Unloading += context => + { + Log.Error("[GRACEFUL_SHUTDOWN] SIGTERM received", new Exception("[GRACEFUL_SHUTDOWN] SIGTERM received")); + GracefulStop(_cancellationTokenSource, _stoppedEvent); + }; + + AppDomain.CurrentDomain.UnhandledException += (sender, args) => + { + Log.Error("UnhandledException", args.ExceptionObject as Exception); + GracefulStop(_cancellationTokenSource, _stoppedEvent); + }; + } + + public CancellationToken CancellationToken => _cancellationTokenSource.Token; + + public void Dispose() + { + _stoppedEvent.Set(); + } + + private static void GracefulStop(CancellationTokenSource cancellationTokenSource, ManualResetEventSlim stoppedEvent) + { + Log.Info("DockerGracefulStopService Stopping service"); + cancellationTokenSource.Cancel(); + stoppedEvent.Wait(); + Log.Info("DockerGracefulStopService Stop finished"); + } + } +} \ No newline at end of file diff --git a/srcs/LogsServer/LogsServer.csproj b/srcs/LogsServer/LogsServer.csproj new file mode 100644 index 0000000..0521cde --- /dev/null +++ b/srcs/LogsServer/LogsServer.csproj @@ -0,0 +1,26 @@ + + + + net5.0 + ..\..\dist\logs-server\ + false + + + + + + + + + + + + + + + + + + + + diff --git a/srcs/LogsServer/Program.cs b/srcs/LogsServer/Program.cs new file mode 100644 index 0000000..31c4c4b --- /dev/null +++ b/srcs/LogsServer/Program.cs @@ -0,0 +1,78 @@ +using System; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using PhoenixLib.Logging; +using PhoenixLib.ServiceBus.MQTT; +using Plugin.Database.Mapping; +using ProtoBuf.Grpc.Client; + +namespace LogsServer +{ + public class Program + { + public static async Task Main(string[] args) + { + PrintHeader(); + NonGameMappingRules.InitializeMapping(); + GrpcClientFactory.AllowUnencryptedHttp2 = true; + using var stopService = new DockerGracefulStopService(); + using IHost host = CreateHostBuilder(args).Build(); + { + await host.StartAsync(); + IMessagingService messagingService = host.Services.GetRequiredService(); + await messagingService.StartAsync(); + + Log.Info("Logs Server started"); + await host.WaitForShutdownAsync(stopService.CancellationToken); + await messagingService.DisposeAsync(); + } + } + + private static void PrintHeader() + { + const string text = @" +██╗ ██╗██╗███╗ ██╗ ██████╗ ███████╗███████╗███╗ ███╗██╗ ██╗ +██║ ██║██║████╗ ██║██╔════╝ ██╔════╝██╔════╝████╗ ████║██║ ██║ +██║ █╗ ██║██║██╔██╗ ██║██║ ███╗███████╗█████╗ ██╔████╔██║██║ ██║ +██║███╗██║██║██║╚██╗██║██║ ██║╚════██║██╔══╝ ██║╚██╔╝██║██║ ██║ +╚███╔███╔╝██║██║ ╚████║╚██████╔╝███████║███████╗██║ ╚═╝ ██║╚██████╔╝ + ╚══╝╚══╝ ╚═╝╚═╝ ╚═══╝ ╚═════╝ ╚══════╝╚══════╝╚═╝ ╚═╝ ╚═════╝ + +██╗ ██████╗ ██████╗ ███████╗███████╗███████╗██████╗ ██╗ ██╗███████╗██████╗ +██║ ██╔═══██╗██╔════╝ ██╔════╝██╔════╝██╔════╝██╔══██╗██║ ██║██╔════╝██╔══██╗ +██║ ██║ ██║██║ ███╗███████╗███████╗█████╗ ██████╔╝██║ ██║█████╗ ██████╔╝ +██║ ██║ ██║██║ ██║╚════██║╚════██║██╔══╝ ██╔══██╗╚██╗ ██╔╝██╔══╝ ██╔══██╗ +███████╗╚██████╔╝╚██████╔╝███████║███████║███████╗██║ ██║ ╚████╔╝ ███████╗██║ ██║ +╚══════╝ ╚═════╝ ╚═════╝ ╚══════╝╚══════╝╚══════╝╚═╝ ╚═╝ ╚═══╝ ╚══════╝╚═╝ ╚═╝ +"; + string separator = new('=', Console.WindowWidth); + string logo = text.Split('\n').Select(s => string.Format("{0," + (Console.WindowWidth / 2 + s.Length / 2) + "}\n", s)) + .Aggregate("", (current, i) => current + i); + + Console.ForegroundColor = ConsoleColor.Cyan; + Console.WriteLine(separator + logo + $"Version: {Assembly.GetExecutingAssembly().GetName().Version}\n" + separator); + Console.ForegroundColor = ConsoleColor.White; + } + + // Additional configuration is required to successfully run gRPC on macOS. + // For instructions on how to configure Kestrel and gRPC clients on macOS, visit https://go.microsoft.com/fwlink/?linkid=2099682 + private static IHostBuilder CreateHostBuilder(string[] args) + { + IHostBuilder host = Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.ConfigureKestrel(s => + { + s.ListenAnyIP(short.Parse(Environment.GetEnvironmentVariable("LOGS_SERVER_PORT") ?? "28888"), options => { options.Protocols = HttpProtocols.Http2; }); + }); + webBuilder.UseStartup(); + }); + return host; + } + } +} \ No newline at end of file diff --git a/srcs/LogsServer/Startup.cs b/srcs/LogsServer/Startup.cs new file mode 100644 index 0000000..d96ec5e --- /dev/null +++ b/srcs/LogsServer/Startup.cs @@ -0,0 +1,41 @@ +using LogsServer.Consumers; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using PhoenixLib.Events; +using PhoenixLib.Logging; +using PhoenixLib.ServiceBus.Extensions; +using Plugin.MongoLogs.Extensions; +using WingsAPI.Communication.Services.Messages; +using WingsEmu.Health.Extensions; + +namespace LogsServer +{ + public class Startup + { + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) + { + services.AddMqttConfigurationFromEnv(); + services.AddEventPipeline(); + services.AddEventHandlersInAssembly(); + services.AddMaintenanceMode(); + services.AddPhoenixLogging(); + services.AddMongoLogsPlugin(); + services.AddMessageSubscriber(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseRouting(); + } + } +} \ No newline at end of file diff --git a/srcs/MailServer/Consumers/MailCharacterConnectedMessageConsumer.cs b/srcs/MailServer/Consumers/MailCharacterConnectedMessageConsumer.cs new file mode 100644 index 0000000..6b0d96f --- /dev/null +++ b/srcs/MailServer/Consumers/MailCharacterConnectedMessageConsumer.cs @@ -0,0 +1,52 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; +using WingsEmu.DTOs.Mails; +using WingsEmu.Plugins.DistributedGameEvents.Mails; +using WingsEmu.Plugins.DistributedGameEvents.PlayerEvents; + +namespace MailServer.Consumers +{ + public class MailCharacterConnectedMessageConsumer : IMessageConsumer + { + private readonly ICharacterMailDao _characterMailDao; + private readonly IMessagePublisher _publisher; + + public MailCharacterConnectedMessageConsumer(IMessagePublisher publisher, ICharacterMailDao characterMailDao) + { + _publisher = publisher; + _characterMailDao = characterMailDao; + } + + public async Task HandleAsync(PlayerConnectedOnChannelMessage e, CancellationToken cancellation) + { + long characterId = e.CharacterId; + List mails = await _characterMailDao.GetByCharacterIdAsync(characterId); + + if (!mails.Any()) + { + return; + } + + IEnumerable firstFiftyMails = mails.Take(50); + + var tuple = new List(); + foreach (CharacterMailDto mail in firstFiftyMails) + { + tuple.Add(mail); + } + + await _publisher.PublishAsync(new MailReceivePendingOnConnectedMessage + { + CharacterId = characterId, + Mails = tuple + }); + } + } +} \ No newline at end of file diff --git a/srcs/MailServer/Consumers/NoteCharacterConnectedMessageConsumer.cs b/srcs/MailServer/Consumers/NoteCharacterConnectedMessageConsumer.cs new file mode 100644 index 0000000..e781a85 --- /dev/null +++ b/srcs/MailServer/Consumers/NoteCharacterConnectedMessageConsumer.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; +using WingsEmu.DTOs.Mails; +using WingsEmu.Plugins.DistributedGameEvents.Mails; +using WingsEmu.Plugins.DistributedGameEvents.PlayerEvents; + +namespace MailServer.Consumers +{ + public class NoteCharacterConnectedMessageConsumer : IMessageConsumer + { + private readonly ICharacterNoteDao _characterNoteDao; + private readonly IMessagePublisher _publisher; + + public NoteCharacterConnectedMessageConsumer(IMessagePublisher publisher, ICharacterNoteDao characterNoteDao) + { + _publisher = publisher; + _characterNoteDao = characterNoteDao; + } + + public async Task HandleAsync(PlayerConnectedOnChannelMessage e, CancellationToken cancellation) + { + long characterId = e.CharacterId; + List notes = await _characterNoteDao.GetByCharacterIdAsync(characterId); + + await _publisher.PublishAsync(new NoteReceivePendingOnConnectedMessage + { + CharacterId = characterId, + Notes = notes + }); + } + } +} \ No newline at end of file diff --git a/srcs/MailServer/Consumers/ServiceFlushAllMessageConsumer.cs b/srcs/MailServer/Consumers/ServiceFlushAllMessageConsumer.cs new file mode 100644 index 0000000..8e2ce83 --- /dev/null +++ b/srcs/MailServer/Consumers/ServiceFlushAllMessageConsumer.cs @@ -0,0 +1,20 @@ +using System.Threading; +using System.Threading.Tasks; +using MailServer.RecurrentJobs; +using PhoenixLib.ServiceBus; +using WingsAPI.Communication.Services.Messages; + +namespace MailServer.Consumers +{ + public class ServiceFlushAllMessageConsumer : IMessageConsumer + { + private readonly MailSystem _mailSystem; + + public ServiceFlushAllMessageConsumer(MailSystem mailSystem) => _mailSystem = mailSystem; + + public async Task HandleAsync(ServiceFlushAllMessage notification, CancellationToken token) + { + await _mailSystem.ProcessMain(); + } + } +} \ No newline at end of file diff --git a/srcs/MailServer/DockerGracefulStopService.cs b/srcs/MailServer/DockerGracefulStopService.cs new file mode 100644 index 0000000..a07f631 --- /dev/null +++ b/srcs/MailServer/DockerGracefulStopService.cs @@ -0,0 +1,54 @@ +using System; +using System.Runtime.Loader; +using System.Threading; +using PhoenixLib.Logging; + +namespace FamilyServer +{ + public class DockerGracefulStopService : IDisposable + { + private readonly CancellationTokenSource _cancellationTokenSource; + private readonly ManualResetEventSlim _stoppedEvent; + + public DockerGracefulStopService() + { + _cancellationTokenSource = new CancellationTokenSource(); + _stoppedEvent = new ManualResetEventSlim(); + + // SIGINT + Console.CancelKeyPress += (sender, eventArgs) => + { + Log.Error("[GRACEFUL_SHUTDOWN] SIGINT received", new Exception("[GRACEFUL_SHUTDOWN] SIGINT received")); + GracefulStop(_cancellationTokenSource, _stoppedEvent); + }; + + // SIGTERM + AssemblyLoadContext.Default.Unloading += context => + { + Log.Error("[GRACEFUL_SHUTDOWN] SIGTERM received", new Exception("[GRACEFUL_SHUTDOWN] SIGTERM received")); + GracefulStop(_cancellationTokenSource, _stoppedEvent); + }; + + AppDomain.CurrentDomain.UnhandledException += (sender, args) => + { + Log.Error("UnhandledException", args.ExceptionObject as Exception); + GracefulStop(_cancellationTokenSource, _stoppedEvent); + }; + } + + public CancellationToken CancellationToken => _cancellationTokenSource.Token; + + public void Dispose() + { + _stoppedEvent.Set(); + } + + private static void GracefulStop(CancellationTokenSource cancellationTokenSource, ManualResetEventSlim stoppedEvent) + { + Log.Info("DockerGracefulStopService Stopping service"); + cancellationTokenSource.Cancel(); + stoppedEvent.Wait(); + Log.Info("DockerGracefulStopService Stop finished"); + } + } +} \ No newline at end of file diff --git a/srcs/MailServer/MailServer.csproj b/srcs/MailServer/MailServer.csproj new file mode 100644 index 0000000..8b2be74 --- /dev/null +++ b/srcs/MailServer/MailServer.csproj @@ -0,0 +1,26 @@ + + + + net5.0 + ..\..\dist\mail-server\ + false + + + + + + + + + + + + + + + + + + + + diff --git a/srcs/MailServer/Managers/MailManager.cs b/srcs/MailServer/Managers/MailManager.cs new file mode 100644 index 0000000..cedad05 --- /dev/null +++ b/srcs/MailServer/Managers/MailManager.cs @@ -0,0 +1,238 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Foundatio.AsyncEx; +using PhoenixLib.Caching; +using PhoenixLib.Logging; +using PhoenixLib.ServiceBus; +using WingsEmu.Core.Extensions; +using WingsEmu.DTOs.Mails; +using WingsEmu.Plugins.DistributedGameEvents.Mails; + +namespace MailServer.Managers +{ + public class MailManager + { + private static readonly TimeSpan LifeTime = TimeSpan.FromMinutes(Convert.ToUInt32(Environment.GetEnvironmentVariable("MAIL_SERVER_CACHE_TTL_MINUTES") ?? "15")); + private readonly ILongKeyCachedRepository _characterIdLocks; + private readonly ICharacterMailDao _characterMailDao; + + private readonly ILongKeyCachedRepository> _mailsByCharacterId; + private readonly IMessagePublisher _messagePublisher; + + private readonly ConcurrentQueue<(CharacterMailDto dto, bool remove)> _queue = new(); + + public MailManager(ICharacterMailDao characterMailDao, IMessagePublisher messagePublisher, ILongKeyCachedRepository> mailsByCharacterId, + ILongKeyCachedRepository characterIdLocks) + { + _characterMailDao = characterMailDao; + _messagePublisher = messagePublisher; + _mailsByCharacterId = mailsByCharacterId; + _characterIdLocks = characterIdLocks; + } + + private AsyncReaderWriterLock GetCharacterLock(long characterId) + { + AsyncReaderWriterLock characterLock = _characterIdLocks.Get(characterId); + if (characterLock == null) + { + return _characterIdLocks.GetOrSet(characterId, () => new AsyncReaderWriterLock(), LifeTime); + } + + _characterIdLocks.Set(characterId, characterLock, LifeTime); + return characterLock; + } + + private async Task> GetCharacterMailsWithoutLock(long characterId) + { + Dictionary mails = _mailsByCharacterId.Get(characterId) ?? (await _characterMailDao.GetByCharacterIdAsync(characterId)).ToDictionary(m => m.Id); + + _mailsByCharacterId.Set(characterId, mails, LifeTime); + return mails; + } + + public async Task> GetCharacterMails(long characterId) + { + AsyncReaderWriterLock characterLock = GetCharacterLock(characterId); + using (await characterLock.ReaderLockAsync()) + { + return (await GetCharacterMailsWithoutLock(characterId))?.Values; + } + } + + public async Task> GetCharacterMailsDictionary(long characterId) + { + AsyncReaderWriterLock characterLock = GetCharacterLock(characterId); + using (await characterLock.ReaderLockAsync()) + { + return await GetCharacterMailsWithoutLock(characterId); + } + } + + private async Task AddCharacterMails(long characterId, IEnumerable dtos) + { + AsyncReaderWriterLock characterLock = GetCharacterLock(characterId); + using (await characterLock.WriterLockAsync()) + { + Dictionary characterMails = await GetCharacterMailsWithoutLock(characterId); + foreach (CharacterMailDto dto in dtos) + { + characterMails[dto.Id] = dto; + } + } + + await _messagePublisher.PublishAsync(new MailReceivedMessage + { + CharacterId = characterId, + MailDtos = dtos + }); + } + + private async Task RemoveCharacterMails(long characterId, IEnumerable dtos) + { + AsyncReaderWriterLock characterLock = GetCharacterLock(characterId); + using (await characterLock.WriterLockAsync()) + { + Dictionary characterMails = await GetCharacterMailsWithoutLock(characterId); + foreach (CharacterMailDto dto in dtos) + { + characterMails.Remove(dto.Id); + } + } + } + + private async Task RemoveCharacterMail(long characterId, CharacterMailDto dto) + { + AsyncReaderWriterLock characterLock = GetCharacterLock(characterId); + using (await characterLock.WriterLockAsync()) + { + Dictionary characterMails = await GetCharacterMailsWithoutLock(characterId); + characterMails.Remove(dto.Id); + } + } + + public async Task RemoveMails(long characterId, IEnumerable dtos) + { + await RemoveCharacterMails(characterId, dtos); + foreach (CharacterMailDto dto in dtos) + { + _queue.Enqueue((dto, true)); + } + } + + public async Task RemoveMail(long characterId, CharacterMailDto dto) + { + await RemoveCharacterMail(characterId, dto); + _queue.Enqueue((dto, true)); + } + + public void AddMail((CharacterMailDto dto, bool remove) valueTuple) + { + _queue.Enqueue(valueTuple); + } + + public void AddMails(IEnumerable<(CharacterMailDto dto, bool remove)> mails) + { + foreach ((CharacterMailDto dto, bool remove) mail in mails) + { + _queue.Enqueue(mail); + } + } + + public async Task AddMailsInstantly(IEnumerable dtosToSave) + { + Dictionary> dtosToAddByCharacterId = new(); + + foreach (CharacterMailDto dto in dtosToSave) + { + dtosToAddByCharacterId.GetOrSetDefault(dto.ReceiverId, new List()).Add(dto); + } + + List<(CharacterMailDto dto, bool remove)> dtosFailedToSave = new(); + + foreach ((long characterId, List dtosToAdd) in dtosToAddByCharacterId) + { + try + { + IEnumerable saved = await _characterMailDao.SaveAsync(dtosToAdd); + + await AddCharacterMails(characterId, saved); + Log.Warn($"[MAIL_MANAGER][BATCH_SAVING][CREATE][CharacterId: '{characterId.ToString()}'] Successfully created {dtosToAdd.Count.ToString()} mails"); + } + catch (Exception e) + { + dtosFailedToSave.AddRange(dtosToAdd.Select(s => (s, false))); + Log.Error($"[MAIL_MANAGER][BATCH_SAVING][CREATE][CharacterId: '{characterId.ToString()}'] Failed to create batch of created Mails", e); + } + } + + if (dtosFailedToSave.Count < 1) + { + return; + } + + foreach ((CharacterMailDto dto, bool remove) dto in dtosFailedToSave) + { + _queue.Enqueue(dto); + } + } + + public async Task FlushAll() + { + Dictionary> dtosToAddByCharacterId = new(); + Dictionary> dtosToRemoveByCharacterId = new(); + + while (_queue.TryDequeue(out (CharacterMailDto dto, bool remove) tuple)) + { + Dictionary> dictionaryToModify = tuple.remove ? dtosToRemoveByCharacterId : dtosToAddByCharacterId; + dictionaryToModify.GetOrSetDefault(tuple.dto.ReceiverId, new List()).Add(tuple.dto); + } + + List<(CharacterMailDto dto, bool remove)> dtosFailedToSave = new(); + + foreach ((long characterId, List dtosToAdd) in dtosToAddByCharacterId) + { + try + { + IEnumerable saved = await _characterMailDao.SaveAsync(dtosToAdd); + + await AddCharacterMails(characterId, saved); + Log.Warn($"[MAIL_MANAGER][BATCH_SAVING][CREATE][CharacterId: '{characterId.ToString()}'] Successfully created {dtosToAdd.Count.ToString()} mails"); + } + catch (Exception e) + { + dtosFailedToSave.AddRange(dtosToAdd.Select(s => (s, false))); + Log.Error($"[MAIL_MANAGER][BATCH_SAVING][CREATE][CharacterId: '{characterId.ToString()}'] Failed to create batch of created Mails", e); + } + } + + foreach ((long characterId, List dtosToRemove) in dtosToRemoveByCharacterId) + { + try + { + var toRemoveIds = dtosToRemove.Select(s => s.Id).ToList(); + await _characterMailDao.DeleteByIdsAsync(toRemoveIds); + + Log.Warn($"[MAIL_MANAGER][BATCH_SAVING][REMOVE][CharacterId: '{characterId.ToString()}'] Successfully removed {toRemoveIds.Count.ToString()} mails"); + } + catch (Exception e) + { + dtosFailedToSave.AddRange(dtosToRemove.Select(s => (s, true))); + Log.Error($"[MAIL_MANAGER][BATCH_SAVING][REMOVE][CharacterId: '{characterId.ToString()}'] Failed to create batch of removed Mails", e); + } + } + + if (dtosFailedToSave.Count < 1) + { + return; + } + + foreach ((CharacterMailDto dto, bool remove) dto in dtosFailedToSave) + { + _queue.Enqueue(dto); + } + } + } +} \ No newline at end of file diff --git a/srcs/MailServer/Program.cs b/srcs/MailServer/Program.cs new file mode 100644 index 0000000..df33fba --- /dev/null +++ b/srcs/MailServer/Program.cs @@ -0,0 +1,81 @@ +using System; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using FamilyServer; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using PhoenixLib.Logging; +using PhoenixLib.ServiceBus.MQTT; +using Plugin.Database.Mapping; +using ProtoBuf.Grpc.Client; + +namespace MailServer +{ + public class Program + { + public static async Task Main(string[] args) + { + PrintHeader(); + NonGameMappingRules.InitializeMapping(); + GrpcClientFactory.AllowUnencryptedHttp2 = true; + using var stopService = new DockerGracefulStopService(); + using IHost host = CreateHostBuilder(args).Build(); + { + await host.StartAsync(); + IMessagingService messagingService = host.Services.GetRequiredService(); + await messagingService.StartAsync(); + IServiceProvider services = host.Services; + Log.Info("MailServer is working..."); + await host.WaitForShutdownAsync(stopService.CancellationToken); + await messagingService.DisposeAsync(); + } + } + + + private static void PrintHeader() + { + const string text = @" +██╗ ██╗██╗███╗ ██╗ ██████╗ ███████╗███████╗███╗ ███╗██╗ ██╗ +██║ ██║██║████╗ ██║██╔════╝ ██╔════╝██╔════╝████╗ ████║██║ ██║ +██║ █╗ ██║██║██╔██╗ ██║██║ ███╗███████╗█████╗ ██╔████╔██║██║ ██║ +██║███╗██║██║██║╚██╗██║██║ ██║╚════██║██╔══╝ ██║╚██╔╝██║██║ ██║ +╚███╔███╔╝██║██║ ╚████║╚██████╔╝███████║███████╗██║ ╚═╝ ██║╚██████╔╝ + ╚══╝╚══╝ ╚═╝╚═╝ ╚═══╝ ╚═════╝ ╚══════╝╚══════╝╚═╝ ╚═╝ ╚═════╝ + +███╗ ███╗ █████╗ ██╗██╗ ███████╗███████╗██████╗ ██╗ ██╗███████╗██████╗ +████╗ ████║██╔══██╗██║██║ ██╔════╝██╔════╝██╔══██╗██║ ██║██╔════╝██╔══██╗ +██╔████╔██║███████║██║██║█████╗███████╗█████╗ ██████╔╝██║ ██║█████╗ ██████╔╝ +██║╚██╔╝██║██╔══██║██║██║╚════╝╚════██║██╔══╝ ██╔══██╗╚██╗ ██╔╝██╔══╝ ██╔══██╗ +██║ ╚═╝ ██║██║ ██║██║███████╗ ███████║███████╗██║ ██║ ╚████╔╝ ███████╗██║ ██║ +╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚══════╝ ╚══════╝╚══════╝╚═╝ ╚═╝ ╚═══╝ ╚══════╝╚═╝ ╚═╝ + +"; + string separator = new('=', Console.WindowWidth); + string logo = text.Split('\n').Select(s => string.Format("{0," + (Console.WindowWidth / 2 + s.Length / 2) + "}\n", s)) + .Aggregate("", (current, i) => current + i); + + Console.ForegroundColor = ConsoleColor.Cyan; + Console.WriteLine(separator + logo + $"Version: {Assembly.GetExecutingAssembly().GetName().Version}\n" + separator); + Console.ForegroundColor = ConsoleColor.White; + } + + // Additional configuration is required to successfully run gRPC on macOS. + // For instructions on how to configure Kestrel and gRPC clients on macOS, visit https://go.microsoft.com/fwlink/?linkid=2099682 + private static IHostBuilder CreateHostBuilder(string[] args) + { + IHostBuilder host = Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.ConfigureKestrel(s => + { + s.ListenAnyIP(short.Parse(Environment.GetEnvironmentVariable("MAIL_SERVER_PORT") ?? "27777"), options => { options.Protocols = HttpProtocols.Http2; }); + }); + webBuilder.UseStartup(); + }); + return host; + } + } +} \ No newline at end of file diff --git a/srcs/MailServer/RecurrentJobs/MailSystem.cs b/srcs/MailServer/RecurrentJobs/MailSystem.cs new file mode 100644 index 0000000..379ab82 --- /dev/null +++ b/srcs/MailServer/RecurrentJobs/MailSystem.cs @@ -0,0 +1,30 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using MailServer.Managers; +using Microsoft.Extensions.Hosting; + +namespace MailServer.RecurrentJobs +{ + public class MailSystem : BackgroundService + { + private static readonly TimeSpan Interval = TimeSpan.FromSeconds(Convert.ToUInt32(Environment.GetEnvironmentVariable("MAIL_SERVER_SAVE_INTERVAL_SECONDS") ?? "10")); + private readonly MailManager _mailManager; + + public MailSystem(MailManager mailManager) => _mailManager = mailManager; + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + await ProcessMain(); + await Task.Delay(Interval, stoppingToken); + } + } + + public async Task ProcessMain() + { + await _mailManager.FlushAll(); + } + } +} \ No newline at end of file diff --git a/srcs/MailServer/Services/MailService.cs b/srcs/MailServer/Services/MailService.cs new file mode 100644 index 0000000..d8bf9d6 --- /dev/null +++ b/srcs/MailServer/Services/MailService.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using MailServer.Managers; +using PhoenixLib.ServiceBus; +using WingsAPI.Communication; +using WingsAPI.Communication.Mail; +using WingsEmu.DTOs.Items; +using WingsEmu.DTOs.Mails; +using WingsEmu.Plugins.DistributedGameEvents.Mails; + +namespace MailServer.Services +{ + public class MailService : IMailService + { + private readonly MailManager _mailManager; + private readonly IMessagePublisher _messagePublisher; + + public MailService(IMessagePublisher messagePublisher, MailManager mailManager) + { + _messagePublisher = messagePublisher; + _mailManager = mailManager; + } + + public async ValueTask CreateMailAsync(CreateMailRequest request) + { + string senderName = request.SenderName; + long receiverId = request.ReceiverId; + MailGiftType mailGiftType = request.MailGiftType; + ItemInstanceDTO itemInstance = request.ItemInstance; + + var newMail = new CharacterMailDto + { + Date = DateTime.UtcNow, + SenderName = senderName, + ReceiverId = receiverId, + MailGiftType = mailGiftType, + ItemInstance = itemInstance + }; + + _mailManager.AddMail((newMail, false)); + + return new CreateMailResponse + { + Status = RpcResponseType.SUCCESS, + Mail = newMail + }; + } + + public async Task CreateMailBatchAsync(CreateMailBatchRequest request) + { + if (request.Mails == null) + { + return new CreateMailBatchResponse + { + Status = RpcResponseType.GENERIC_SERVER_ERROR + }; + } + + if (request.Bufferized) + { + _mailManager.AddMails(request.Mails.Select(m => (m, false))); + } + else + { + await _mailManager.AddMailsInstantly(request.Mails); + } + + return new CreateMailBatchResponse + { + Status = RpcResponseType.SUCCESS, + Mail = request.Mails + }; + } + + public async ValueTask RemoveMailAsync(RemoveMailRequest request) + { + IReadOnlyDictionary dictionary = await _mailManager.GetCharacterMailsDictionary(request.CharacterId); + if (!dictionary.TryGetValue(request.MailId, out CharacterMailDto dto)) + { + return new BasicRpcResponse + { + ResponseType = RpcResponseType.GENERIC_SERVER_ERROR + }; + } + + await _mailManager.RemoveMail(request.CharacterId, dto); + + return new BasicRpcResponse + { + ResponseType = RpcResponseType.SUCCESS + }; + } + + public async ValueTask GetMailsByCharacterId(GetMailsRequest request) + { + long characterId = request.CharacterId; + + return new GetMailsResponse + { + CharacterMailsDto = await _mailManager.GetCharacterMails(characterId) + }; + } + } +} \ No newline at end of file diff --git a/srcs/MailServer/Services/NoteService.cs b/srcs/MailServer/Services/NoteService.cs new file mode 100644 index 0000000..3bcdfc7 --- /dev/null +++ b/srcs/MailServer/Services/NoteService.cs @@ -0,0 +1,181 @@ +using System; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; +using WingsAPI.Communication; +using WingsAPI.Communication.Mail; +using WingsEmu.DTOs.Mails; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Character; +using WingsEmu.Plugins.DistributedGameEvents.PlayerEvents; + +namespace MailServer.Services +{ + public class NoteService : INoteService + { + private readonly ICharacterNoteDao _characterNoteDao; + private readonly IMessagePublisher _messagePublisher; + + public NoteService(ICharacterNoteDao characterNoteDao, IMessagePublisher messagePublisher) + { + _characterNoteDao = characterNoteDao; + _messagePublisher = messagePublisher; + } + + public async ValueTask CreateNoteAsync(CreateNoteRequest request) + { + long senderId = request.SenderId; + string senderName = request.SenderName; + long receiverId = request.ReceiverId; + string receiverName = request.ReceiverName; + string title = request.Title; + string message = request.Message; + string equipmentPackets = request.EquipmentPackets; + GenderType senderGender = request.SenderGender; + ClassType senderClass = request.SenderClass; + HairColorType senderHairColor = request.SenderHairColor; + HairStyleType senderHairStyle = request.SenderHairStyle; + + var newSenderMail = new CharacterNoteDto + { + Date = DateTime.UtcNow, + SenderId = senderId, + ReceiverId = receiverId, + Title = title, + Message = message, + EquipmentPackets = equipmentPackets, + IsSenderCopy = true, + IsOpened = false, + SenderGender = senderGender, + SenderClass = senderClass, + SenderHairColor = senderHairColor, + SenderHairStyle = senderHairStyle, + SenderName = senderName, + ReceiverName = receiverName + }; + + CharacterNoteDto senderMail = await _characterNoteDao.SaveAsync(newSenderMail); + + var newRecvMail = new CharacterNoteDto + { + Date = DateTime.UtcNow, + SenderId = senderId, + ReceiverId = receiverId, + Title = title, + Message = message, + EquipmentPackets = equipmentPackets, + IsSenderCopy = false, + IsOpened = false, + SenderGender = senderGender, + SenderClass = senderClass, + SenderHairColor = senderHairColor, + SenderHairStyle = senderHairStyle, + SenderName = senderName, + ReceiverName = receiverName + }; + + CharacterNoteDto recvMail; + try + { + recvMail = await _characterNoteDao.SaveAsync(newRecvMail); + } + catch + { + return new CreateNoteResponse + { + Status = RpcResponseType.GENERIC_SERVER_ERROR + }; + } + + await _messagePublisher.PublishAsync(new NoteReceivedMessage + { + SenderNote = senderMail, + ReceiverNote = recvMail + }); + + return new CreateNoteResponse + { + SenderNote = senderMail, + ReceiverNote = recvMail, + Status = RpcResponseType.SUCCESS + }; + } + + public async ValueTask OpenNoteAsync(OpenNoteRequest request) + { + long noteId = request.NoteId; + CharacterNoteDto note = await _characterNoteDao.GetByIdAsync(noteId); + if (note == null) + { + return new BasicRpcResponse + { + ResponseType = RpcResponseType.GENERIC_SERVER_ERROR + }; + } + + if (note.IsSenderCopy) + { + return new BasicRpcResponse + { + ResponseType = RpcResponseType.GENERIC_SERVER_ERROR + }; + } + + if (note.IsOpened) + { + return new BasicRpcResponse + { + ResponseType = RpcResponseType.GENERIC_SERVER_ERROR + }; + } + + note.IsOpened = true; + try + { + await _characterNoteDao.SaveAsync(note); + } + catch + { + return new BasicRpcResponse + { + ResponseType = RpcResponseType.GENERIC_SERVER_ERROR + }; + } + + return new BasicRpcResponse + { + ResponseType = RpcResponseType.SUCCESS + }; + ; + } + + public async ValueTask RemoveNoteAsync(RemoveNoteRequest request) + { + long noteId = request.NoteId; + CharacterNoteDto note = await _characterNoteDao.GetByIdAsync(noteId); + if (note == null) + { + return new BasicRpcResponse + { + ResponseType = RpcResponseType.GENERIC_SERVER_ERROR + }; + } + + try + { + await _characterNoteDao.DeleteByIdAsync(note.Id); + } + catch + { + return new BasicRpcResponse + { + ResponseType = RpcResponseType.GENERIC_SERVER_ERROR + }; + } + + return new BasicRpcResponse + { + ResponseType = RpcResponseType.SUCCESS + }; + } + } +} \ No newline at end of file diff --git a/srcs/MailServer/Startup.cs b/srcs/MailServer/Startup.cs new file mode 100644 index 0000000..0f42730 --- /dev/null +++ b/srcs/MailServer/Startup.cs @@ -0,0 +1,78 @@ +using MailServer.Consumers; +using MailServer.Managers; +using MailServer.RecurrentJobs; +using MailServer.Services; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using PhoenixLib.Caching; +using PhoenixLib.Events; +using PhoenixLib.Logging; +using PhoenixLib.ServiceBus.Extensions; +using Plugin.Database; +using ProtoBuf.Grpc.Server; +using WingsAPI.Communication.Services.Messages; +using WingsEmu.Health.Extensions; +using WingsEmu.Plugins.DistributedGameEvents.Mails; +using WingsEmu.Plugins.DistributedGameEvents.PlayerEvents; + +namespace MailServer +{ + public class Startup + { + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) + { + services.AddMqttConfigurationFromEnv(); + services.AddEventPipeline(); + services.AddEventHandlersInAssembly(); + services.AddMaintenanceMode(); + services.AddPhoenixLogging(); + + new DatabasePlugin().AddDependencies(services); + + services.TryAddSingleton(typeof(ILongKeyCachedRepository<>), typeof(InMemoryCacheRepository<>)); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddHostedService(s => s.GetRequiredService()); + + services.AddCodeFirstGrpc(config => + { + config.MaxReceiveMessageSize = null; + config.MaxSendMessageSize = null; + config.EnableDetailedErrors = true; + }); + + services.AddMessagePublisher(); + services.AddMessagePublisher(); + services.AddMessageSubscriber(); + services.AddMessageSubscriber(); + services.AddMessagePublisher(); + services.AddMessagePublisher(); + services.AddMessageSubscriber(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseRouting(); + + app.UseEndpoints(endpoints => + { + endpoints.MapGrpcService(); + endpoints.MapGrpcService(); + }); + } + } +} \ No newline at end of file diff --git a/srcs/Master/Consumers/PlayerConnectedOnChannelMessageConsumer.cs b/srcs/Master/Consumers/PlayerConnectedOnChannelMessageConsumer.cs new file mode 100644 index 0000000..a53ada1 --- /dev/null +++ b/srcs/Master/Consumers/PlayerConnectedOnChannelMessageConsumer.cs @@ -0,0 +1,33 @@ +using System.Threading; +using System.Threading.Tasks; +using Master.Managers; +using PhoenixLib.ServiceBus; +using WingsAPI.Communication.Player; +using WingsEmu.Plugins.DistributedGameEvents.PlayerEvents; + +namespace Master.Consumers +{ + public class PlayerConnectedOnChannelMessageConsumer : IMessageConsumer + { + private readonly ClusterCharacterManager _clusterCharacterManager; + + public PlayerConnectedOnChannelMessageConsumer(ClusterCharacterManager clusterCharacterManager) => _clusterCharacterManager = clusterCharacterManager; + + public Task HandleAsync(PlayerConnectedOnChannelMessage notification, CancellationToken token) + { + _clusterCharacterManager.AddClusterCharacter(new ClusterCharacterInfo + { + Id = notification.CharacterId, + Class = notification.Class, + Gender = notification.Gender, + Level = notification.Level, + Name = notification.CharacterName, + ChannelId = (byte?)notification.ChannelId, + HeroLevel = notification.HeroLevel, + HardwareId = notification.HardwareId + }); + + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/srcs/Master/Consumers/PlayerDisconnectedChannelMessageConsumer.cs b/srcs/Master/Consumers/PlayerDisconnectedChannelMessageConsumer.cs new file mode 100644 index 0000000..b336c5f --- /dev/null +++ b/srcs/Master/Consumers/PlayerDisconnectedChannelMessageConsumer.cs @@ -0,0 +1,21 @@ +using System.Threading; +using System.Threading.Tasks; +using Master.Managers; +using PhoenixLib.ServiceBus; +using WingsEmu.Plugins.DistributedGameEvents.PlayerEvents; + +namespace Master.Consumers +{ + public class PlayerDisconnectedChannelMessageConsumer : IMessageConsumer + { + private readonly ClusterCharacterManager _clusterCharacterManager; + + public PlayerDisconnectedChannelMessageConsumer(ClusterCharacterManager clusterCharacterManager) => _clusterCharacterManager = clusterCharacterManager; + + public Task HandleAsync(PlayerDisconnectedChannelMessage notification, CancellationToken token) + { + _clusterCharacterManager.RemoveClusterCharacter(notification.CharacterId); + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/srcs/Master/Datas/WorldServer.cs b/srcs/Master/Datas/WorldServer.cs new file mode 100644 index 0000000..82b806a --- /dev/null +++ b/srcs/Master/Datas/WorldServer.cs @@ -0,0 +1,25 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using Master.Proxies; +using WingsAPI.Communication.ServerApi.Protocol; +using WingsEmu.DTOs.Account; + +namespace Master.Datas +{ + public class WorldServer + { + public int ChannelId { get; init; } + public GameChannelType ChannelType { get; init; } + public int AccountLimit { get; set; } + public string WorldGroup { get; set; } + public AuthorityType AuthorityRequired { get; set; } = AuthorityType.User; + public DateTime LastPulse { get; set; } + public int SessionsCount { get; set; } + public string EndPointIp { get; set; } + public int EndPointPort { get; set; } + public DateTime RegistrationDate { get; init; } + } +} \ No newline at end of file diff --git a/srcs/Master/DockerGracefulStopService.cs b/srcs/Master/DockerGracefulStopService.cs new file mode 100644 index 0000000..f9e0069 --- /dev/null +++ b/srcs/Master/DockerGracefulStopService.cs @@ -0,0 +1,54 @@ +using System; +using System.Runtime.Loader; +using System.Threading; +using PhoenixLib.Logging; + +namespace WingsEmu.ClusterCommunicator +{ + public class DockerGracefulStopService : IDisposable + { + private readonly CancellationTokenSource _cancellationTokenSource; + private readonly ManualResetEventSlim _stoppedEvent; + + public DockerGracefulStopService() + { + _cancellationTokenSource = new CancellationTokenSource(); + _stoppedEvent = new ManualResetEventSlim(); + + // SIGINT + Console.CancelKeyPress += (sender, eventArgs) => + { + Log.Error("[GRACEFUL_SHUTDOWN] SIGINT received", new Exception("[GRACEFUL_SHUTDOWN] SIGINT received")); + GracefulStop(_cancellationTokenSource, _stoppedEvent); + }; + + // SIGTERM + AssemblyLoadContext.Default.Unloading += context => + { + Log.Error("[GRACEFUL_SHUTDOWN] SIGTERM received", new Exception("[GRACEFUL_SHUTDOWN] SIGTERM received")); + GracefulStop(_cancellationTokenSource, _stoppedEvent); + }; + + AppDomain.CurrentDomain.UnhandledException += (sender, args) => + { + Log.Error("UnhandledException", args.ExceptionObject as Exception); + GracefulStop(_cancellationTokenSource, _stoppedEvent); + }; + } + + public CancellationToken CancellationToken => _cancellationTokenSource.Token; + + public void Dispose() + { + _stoppedEvent.Set(); + } + + private static void GracefulStop(CancellationTokenSource cancellationTokenSource, ManualResetEventSlim stoppedEvent) + { + Log.Info("DockerGracefulStopService Stopping service"); + cancellationTokenSource.Cancel(); + stoppedEvent.Wait(); + Log.Info("DockerGracefulStopService Stop finished"); + } + } +} \ No newline at end of file diff --git a/srcs/Master/Extensions/WorldExtensions.cs b/srcs/Master/Extensions/WorldExtensions.cs new file mode 100644 index 0000000..f753ff2 --- /dev/null +++ b/srcs/Master/Extensions/WorldExtensions.cs @@ -0,0 +1,48 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using Master.Datas; +using Master.Proxies; +using Master.Services; +using WingsAPI.Communication.ServerApi.Protocol; +using WingsEmu.Game; + +namespace Master.Extensions +{ + internal static class WorldExtensions + { + public static WorldServer ToWorldServer(this SerializableGameServer serialized) + { + return new WorldServer + { + WorldGroup = serialized.WorldGroup, + ChannelId = serialized.ChannelId, + ChannelType = serialized.ChannelType, + AccountLimit = serialized.AccountLimit, + SessionsCount = serialized.SessionCount, + AuthorityRequired = serialized.Authority, + EndPointIp = serialized.EndPointIp, + EndPointPort = serialized.EndPointPort, + RegistrationDate = DateTime.UtcNow + }; + } + + public static SerializableGameServer ToSerializableWorldServer(this WorldServer world) + { + return new SerializableGameServer + { + WorldGroup = world.WorldGroup, + ChannelId = world.ChannelId, + ChannelType = world.ChannelType, + AccountLimit = world.AccountLimit, + SessionCount = world.SessionsCount, + Authority = world.AuthorityRequired, + EndPointIp = world.EndPointIp, + EndPointPort = world.EndPointPort, + RegistrationDate = world.RegistrationDate + }; + } + } +} \ No newline at end of file diff --git a/srcs/Master/Managers/ClusterCharacterManager.cs b/srcs/Master/Managers/ClusterCharacterManager.cs new file mode 100644 index 0000000..b55180d --- /dev/null +++ b/srcs/Master/Managers/ClusterCharacterManager.cs @@ -0,0 +1,53 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using PhoenixLib.Logging; +using WingsAPI.Communication.Player; + +namespace Master.Managers +{ + public class ClusterCharacterManager + { + private readonly SynchronizedCollection _allCharacters = new(); + private readonly ConcurrentDictionary _characterById = new(); + private readonly ConcurrentDictionary _characterByName = new(); + private readonly ConcurrentDictionary> _charactersByChannel = new(); + + public void AddClusterCharacter(ClusterCharacterInfo characterInfo) + { + if (!characterInfo.ChannelId.HasValue) + { + return; + } + + _allCharacters.Add(characterInfo); + _charactersByChannel.GetOrAdd(characterInfo.ChannelId.Value, new List()).Add(characterInfo); + _characterById[characterInfo.Id] = characterInfo; + _characterByName[characterInfo.Name] = characterInfo; + } + + public void RemoveClusterCharacter(long characterId) + { + if (!_characterById.TryGetValue(characterId, out ClusterCharacterInfo characterInfo) || !characterInfo.ChannelId.HasValue) + { + Log.Warn("[CLUSTER_CHARACTER_MANAGER] Tried to remove a character that wasn't in the Manager."); + return; + } + + _allCharacters.Remove(characterInfo); + _charactersByChannel.GetOrAdd(characterInfo.ChannelId.Value, new List()).Remove(characterInfo); + _characterById[characterInfo.Id] = characterInfo; + _characterByName[characterInfo.Name] = characterInfo; + } + + public ClusterCharacterInfo GetCharacterById(long id) => _characterById.GetValueOrDefault(id, null); + + public ClusterCharacterInfo GetCharacterByName(string name) => _characterByName.GetValueOrDefault(name, null); + + public IReadOnlyList GetCharactersByChannelId(byte channelId) => _charactersByChannel.GetValueOrDefault(channelId, null); + + public IReadOnlyCollection>> GetCharactersSortedByChannel() => _charactersByChannel; + + public IReadOnlyList GetCharacters() => _allCharacters.ToList(); + } +} \ No newline at end of file diff --git a/srcs/Master/Managers/WorldServerManager.cs b/srcs/Master/Managers/WorldServerManager.cs new file mode 100644 index 0000000..6d5fdde --- /dev/null +++ b/srcs/Master/Managers/WorldServerManager.cs @@ -0,0 +1,59 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using Master.Datas; +using Master.Extensions; +using Master.Proxies; +using Master.Services; +using WingsAPI.Communication.ServerApi.Protocol; +using WingsEmu.Core.Extensions; +using WingsEmu.Game; + +namespace Master.Managers +{ + public class WorldServerManager + { + private readonly ConcurrentDictionary _worldServerById = new(); + private readonly ConcurrentDictionary> _worldServersByWorldGroup = new(); + + public IEnumerable GetWorlds() + { + return _worldServerById.Select(keyValuePair => keyValuePair.Value); + } + + public WorldServer GetWorldById(int channelId) => _worldServerById.TryGetValue(channelId, out WorldServer server) ? server : null; + + public IEnumerable GetWorldsByWorldGroup(string requestWorldGroup) => _worldServersByWorldGroup.TryGetValue(requestWorldGroup, out List servers) ? servers : null; + + public bool UnregisterWorld(int channelId) + { + if (!_worldServerById.Remove(channelId, out WorldServer value)) + { + return false; + } + + if (!_worldServersByWorldGroup.TryGetValue(value.WorldGroup, out List servers)) + { + return false; + } + + servers.RemoveAll(s => s.ChannelId == channelId); + return true; + } + + + public void RegisterWorldServer(SerializableGameServer serialized) + { + var worldServer = serialized.ToWorldServer(); + worldServer.LastPulse = DateTime.UtcNow; + + _worldServerById[worldServer.ChannelId] = worldServer; + _worldServersByWorldGroup.GetOrSetDefault(worldServer.WorldGroup, new List()).Add(worldServer); + } + } +} \ No newline at end of file diff --git a/srcs/Master/Master.csproj b/srcs/Master/Master.csproj new file mode 100644 index 0000000..e6e1028 --- /dev/null +++ b/srcs/Master/Master.csproj @@ -0,0 +1,27 @@ + + + + net5.0 + ..\..\dist\master\ + false + + + + + + + + + + + + + + + + + + + + + diff --git a/srcs/Master/Program.cs b/srcs/Master/Program.cs new file mode 100644 index 0000000..333790e --- /dev/null +++ b/srcs/Master/Program.cs @@ -0,0 +1,75 @@ +using System; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using PhoenixLib.Logging; +using PhoenixLib.ServiceBus.MQTT; +using WingsEmu.ClusterCommunicator; + +namespace Master +{ + public class Program + { + public static async Task Main(string[] args) + { + PrintHeader(); + using IHost hostBuilder = CreateHostBuilder(args).Build(); + { + using var stopService = new DockerGracefulStopService(); + Log.Info("Starting..."); + + IMessagingService messagingService = hostBuilder.Services.GetRequiredService(); + await messagingService.StartAsync(); + + await hostBuilder.StartAsync(); + IServiceProvider services = hostBuilder.Services; + + await hostBuilder.WaitForShutdownAsync(stopService.CancellationToken); + await messagingService.DisposeAsync(); + } + } + + private static void PrintHeader() + { + Console.Title = "WingsEmu - Master"; + const string text = @" +██╗ ██╗██╗███╗ ██╗ ██████╗ ███████╗███████╗███╗ ███╗██╗ ██╗ +██║ ██║██║████╗ ██║██╔════╝ ██╔════╝██╔════╝████╗ ████║██║ ██║ +██║ █╗ ██║██║██╔██╗ ██║██║ ███╗███████╗█████╗ ██╔████╔██║██║ ██║ +██║███╗██║██║██║╚██╗██║██║ ██║╚════██║██╔══╝ ██║╚██╔╝██║██║ ██║ +╚███╔███╔╝██║██║ ╚████║╚██████╔╝███████║███████╗██║ ╚═╝ ██║╚██████╔╝ + ╚══╝╚══╝ ╚═╝╚═╝ ╚═══╝ ╚═════╝ ╚══════╝╚══════╝╚═╝ ╚═╝ ╚═════╝ + +███╗ ███╗ █████╗ ███████╗████████╗███████╗██████╗ ███████╗███████╗██████╗ ██╗ ██╗███████╗██████╗ +████╗ ████║██╔══██╗██╔════╝╚══██╔══╝██╔════╝██╔══██╗ ██╔════╝██╔════╝██╔══██╗██║ ██║██╔════╝██╔══██╗ +██╔████╔██║███████║███████╗ ██║ █████╗ ██████╔╝█████╗███████╗█████╗ ██████╔╝██║ ██║█████╗ ██████╔╝ +██║╚██╔╝██║██╔══██║╚════██║ ██║ ██╔══╝ ██╔══██╗╚════╝╚════██║██╔══╝ ██╔══██╗╚██╗ ██╔╝██╔══╝ ██╔══██╗ +██║ ╚═╝ ██║██║ ██║███████║ ██║ ███████╗██║ ██║ ███████║███████╗██║ ██║ ╚████╔╝ ███████╗██║ ██║ +╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝ ╚═╝ ╚══════╝╚═╝ ╚═╝ ╚══════╝╚══════╝╚═╝ ╚═╝ ╚═══╝ ╚══════╝╚═╝ ╚═╝ +"; + string separator = new('=', Console.WindowWidth); + string logo = text.Split('\n').Select(s => string.Format("{0," + (Console.WindowWidth / 2 + s.Length / 2) + "}\n", s)) + .Aggregate("", (current, i) => current + i); + + Console.ForegroundColor = ConsoleColor.Cyan; + Console.WriteLine(separator + logo + $"Version: {Assembly.GetExecutingAssembly().GetName().Version}\n" + separator); + Console.ForegroundColor = ConsoleColor.White; + } + + + private static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.ConfigureKestrel(s => + { + s.ListenAnyIP(Convert.ToInt32(Environment.GetEnvironmentVariable("MASTER_PORT") ?? "20500"), options => options.Protocols = HttpProtocols.Http2); + }); + webBuilder.UseStartup(); + }); + } +} \ No newline at end of file diff --git a/srcs/Master/Proxies/ServerApiService.cs b/srcs/Master/Proxies/ServerApiService.cs new file mode 100644 index 0000000..1da63fe --- /dev/null +++ b/srcs/Master/Proxies/ServerApiService.cs @@ -0,0 +1,189 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Master.Datas; +using Master.Extensions; +using Master.Managers; +using PhoenixLib.Logging; +using WingsAPI.Communication; +using WingsAPI.Communication.ServerApi; +using WingsAPI.Communication.ServerApi.Protocol; + +namespace Master.Proxies +{ + public class ServerApiService : IServerApiService + { + private readonly WorldServerManager _worldManager; + + public ServerApiService(WorldServerManager worldManager) + { + _worldManager = worldManager; + } + + public async ValueTask IsMasterOnline(EmptyRpcRequest request) => + new BasicRpcResponse + { + ResponseType = RpcResponseType.SUCCESS + }; + + /* + * World + */ + public async ValueTask RegisterWorldServer(RegisterWorldServerRequest request) + { + try + { + SerializableGameServer serialized = request.GameServer; + Log.Info($"[SERVER_API_SERVICE][WORLD_SERVER][REGISTER] {serialized.WorldGroup}:{serialized.ChannelId}:{serialized.ChannelType} - {serialized.EndPointIp}:{serialized.EndPointPort}"); + _worldManager.RegisterWorldServer(serialized); + return new BasicRpcResponse + { + ResponseType = RpcResponseType.SUCCESS + }; + } + catch (Exception e) + { + Log.Error("[SERVER_API_SERVICE][WORLD_SERVER][REGISTER] Unexpected error: ", e); + return new BasicRpcResponse + { + ResponseType = RpcResponseType.GENERIC_SERVER_ERROR + }; + } + } + + public async ValueTask PulseWorldServer(PulseWorldServerRequest request) + { + int channelId = request.ChannelId; + WorldServer tmp = _worldManager.GetWorldById(channelId); + if (tmp == null) + { + Log.Warn($"[SERVER_API_SERVICE][WORLD_SERVER][PULSE] Pulse from: {channelId.ToString()} invalid"); + return new BasicRpcResponse + { + ResponseType = RpcResponseType.GENERIC_SERVER_ERROR + }; + } + + Log.Debug($"[SERVER_API_SERVICE][WORLD_SERVER][PULSE] Pulse received from {tmp.WorldGroup}:{tmp.ChannelId.ToString()}"); + tmp.LastPulse = DateTime.UtcNow; + tmp.SessionsCount = request.SessionsCount; + return new BasicRpcResponse + { + ResponseType = RpcResponseType.SUCCESS + }; + } + + public async ValueTask UnregisterWorldServer(UnregisterWorldServerRequest request) + { + Log.Debug($"[SERVER_API_SERVICE][WORLD_SERVER][UNREGISTER] {request.ChannelId.ToString()}"); + bool removed = _worldManager.UnregisterWorld(request.ChannelId); + return new BasicRpcResponse + { + ResponseType = removed ? RpcResponseType.SUCCESS : RpcResponseType.GENERIC_SERVER_ERROR + }; + } + + public async ValueTask SetWorldServerVisibility(SetWorldServerVisibilityRequest request) + { + WorldServer requestedWorld = _worldManager.GetWorldById(request.ChannelId); + if (requestedWorld == null || requestedWorld.WorldGroup != request.WorldGroup) + { + return new BasicRpcResponse + { + ResponseType = RpcResponseType.GENERIC_SERVER_ERROR + }; + } + + requestedWorld.AuthorityRequired = request.AuthorityRequired; + return new BasicRpcResponse + { + ResponseType = RpcResponseType.SUCCESS + }; + } + + public async ValueTask GetChannelInfo(GetChannelInfoRequest request) + { + Log.Debug($"[SERVER_API_SERVICE][WORLD_SERVER][GET_CHANNEL_INFO] {request.WorldGroup} {request.ChannelId}"); + IEnumerable worlds = _worldManager.GetWorldsByWorldGroup(request.WorldGroup); + + WorldServer world = worlds.SingleOrDefault(s => s.ChannelId == request.ChannelId); + if (world == null) + { + return new GetChannelInfoResponse + { + ResponseType = RpcResponseType.GENERIC_SERVER_ERROR + }; + } + + Log.Debug($"[SERVER_API_SERVICE][WORLD_SERVER][GET_CHANNEL_INFO] {request.WorldGroup} {request.ChannelId} found"); + return new GetChannelInfoResponse + { + ResponseType = RpcResponseType.SUCCESS, + GameServer = world.ToSerializableWorldServer() + }; + } + + public async ValueTask GetAct4ChannelInfo(GetAct4ChannelInfoRequest request) + { + Log.Debug($"[SERVER_API_SERVICE][WORLD_SERVER][GET_ACT4_CHANNEL_INFO] {request.WorldGroup}"); + IEnumerable worlds = _worldManager.GetWorldsByWorldGroup(request.WorldGroup); + + if (worlds == null || !worlds.Any()) + { + return new GetChannelInfoResponse + { + ResponseType = RpcResponseType.GENERIC_SERVER_ERROR + }; + } + + WorldServer world = worlds.SingleOrDefault(s => s.ChannelType == GameChannelType.ACT_4); + if (world == null) + { + return new GetChannelInfoResponse + { + ResponseType = RpcResponseType.GENERIC_SERVER_ERROR + }; + } + + Log.Debug("[SERVER_API_SERVICE][WORLD_SERVER][GET_ACT4_CHANNEL_INFO] Act4 Channel found"); + return new GetChannelInfoResponse + { + ResponseType = RpcResponseType.SUCCESS, + GameServer = world.ToSerializableWorldServer() + }; + } + + + public async ValueTask RetrieveRegisteredWorldServers(RetrieveRegisteredWorldServersRequest request) + { + Log.Debug("[SERVER_API_SERVICE][GAME_SERVER_LIST]"); + IEnumerable worlds = _worldManager.GetWorlds().Where(s => s.AuthorityRequired <= request.RequesterAuthority && s.ChannelType != GameChannelType.ACT_4); + + var serverList = worlds.Select(s => s.ToSerializableWorldServer()).ToList(); + + return new RetrieveRegisteredWorldServersResponse + { + WorldServers = serverList + }; + } + + + public Task RetrieveAllGameServers(EmptyRpcRequest request) + { + Log.Debug("[SERVER_API_SERVICE][GET_GAME_SERVERS]"); + IEnumerable worlds = _worldManager.GetWorlds(); + + var serverList = worlds.Select(s => s.ToSerializableWorldServer()).ToList(); + + return Task.FromResult(new RetrieveRegisteredWorldServersResponse + { + WorldServers = serverList + }); + } + } +} \ No newline at end of file diff --git a/srcs/Master/RecurrentJobs/GameChannelHeartbeatService.cs b/srcs/Master/RecurrentJobs/GameChannelHeartbeatService.cs new file mode 100644 index 0000000..bfa3ecd --- /dev/null +++ b/srcs/Master/RecurrentJobs/GameChannelHeartbeatService.cs @@ -0,0 +1,66 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Master.Datas; +using Master.Managers; +using Microsoft.Extensions.Hosting; +using PhoenixLib.Logging; +using PhoenixLib.ServiceBus; +using WingsAPI.Communication.ServerApi; + +namespace Master.RecurrentJobs +{ + public class GameChannelHeartbeatService : BackgroundService + { + private static readonly TimeSpan Interval = TimeSpan.FromSeconds(5); + private static readonly TimeSpan LastPulseThreshold = TimeSpan.FromSeconds(30); + private readonly IMessagePublisher _messagePublisher; + + private readonly WorldServerManager _worldManager; + + public GameChannelHeartbeatService(WorldServerManager worldManager, IMessagePublisher messagePublisher) + { + _worldManager = worldManager; + _messagePublisher = messagePublisher; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + try + { + foreach (WorldServer world in _worldManager.GetWorlds().ToList()) + { + if (world.LastPulse.AddSeconds(LastPulseThreshold.TotalSeconds) > DateTime.UtcNow) + { + Log.Debug($"[WORLD_PULSE_SYSTEM] Pulse OK : {world.WorldGroup}:{world.ChannelId}:{world.ChannelType.ToString()}"); + // pulsed before the 10 seconds timeout + continue; + } + + // add alert later + Log.Error($"[WORLD_PULSE_SYSTEM] PULSE KO - unregistering : {world.WorldGroup}:{world.ChannelId}:{world.ChannelType.ToString()}", + new Exception($"PULSE KO on {world.ChannelId.ToString()}")); + _worldManager.UnregisterWorld(world.ChannelId); + await _messagePublisher.PublishAsync(new WorldServerShutdownMessage + { + ChannelId = world.ChannelId + }, stoppingToken); + } + } + catch (Exception e) + { + Log.Error("[WORLD_PULSE_SYSTEM] Unexpected error: ", e); + } + + await Task.Delay(Interval, stoppingToken); + } + } + } +} \ No newline at end of file diff --git a/srcs/Master/Services/ClusterCharacterService.cs b/srcs/Master/Services/ClusterCharacterService.cs new file mode 100644 index 0000000..e160b82 --- /dev/null +++ b/srcs/Master/Services/ClusterCharacterService.cs @@ -0,0 +1,70 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Master.Managers; +using WingsAPI.Communication; +using WingsAPI.Communication.Player; + +namespace WingsEmu.Master +{ + public class ClusterCharacterService : IClusterCharacterService + { + private readonly ClusterCharacterManager _clusterCharacterManager; + + public ClusterCharacterService(ClusterCharacterManager clusterCharacterManager) => _clusterCharacterManager = clusterCharacterManager; + + public ValueTask GetCharacterById(ClusterCharacterByIdRequest request) + { + ClusterCharacterInfo info = _clusterCharacterManager.GetCharacterById(request.CharacterId); + + return new ValueTask(new ClusterCharacterResponse + { + ResponseType = info == null ? RpcResponseType.GENERIC_SERVER_ERROR : RpcResponseType.SUCCESS, + ClusterCharacterInfo = info + }); + } + + public ValueTask GetCharacterByName(ClusterCharacterByNameRequest request) + { + ClusterCharacterInfo info = _clusterCharacterManager.GetCharacterByName(request.CharacterName); + + return new ValueTask(new ClusterCharacterResponse + { + ResponseType = info == null ? RpcResponseType.GENERIC_SERVER_ERROR : RpcResponseType.SUCCESS, + ClusterCharacterInfo = info + }); + } + + public ValueTask GetCharactersByChannelId(ClusterCharacterByChannelIdRequest request) + { + IReadOnlyList info = _clusterCharacterManager.GetCharactersByChannelId(request.ChannelId); + + return new ValueTask(new ClusterCharacterGetMultipleResponse + { + ResponseType = info == null ? RpcResponseType.GENERIC_SERVER_ERROR : RpcResponseType.SUCCESS, + ClusterCharacterInfo = info + }); + } + + public ValueTask GetCharactersSortedByChannel(EmptyRpcRequest request) + { + IReadOnlyCollection>> info = _clusterCharacterManager.GetCharactersSortedByChannel(); + + return new ValueTask(new ClusterCharacterGetSortedResponse + { + ResponseType = info == null ? RpcResponseType.GENERIC_SERVER_ERROR : RpcResponseType.SUCCESS, + CharactersByChannel = info + }); + } + + public ValueTask GetAllCharacters(EmptyRpcRequest request) + { + IReadOnlyList info = _clusterCharacterManager.GetCharacters(); + + return new ValueTask(new ClusterCharacterGetMultipleResponse + { + ResponseType = RpcResponseType.SUCCESS, + ClusterCharacterInfo = info + }); + } + } +} \ No newline at end of file diff --git a/srcs/Master/Services/Maintenance/GrpcClusterStatusService.cs b/srcs/Master/Services/Maintenance/GrpcClusterStatusService.cs new file mode 100644 index 0000000..5adc20b --- /dev/null +++ b/srcs/Master/Services/Maintenance/GrpcClusterStatusService.cs @@ -0,0 +1,290 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; +using WingsAPI.Communication; +using WingsAPI.Communication.Services; +using WingsAPI.Communication.Services.Messages; +using WingsAPI.Communication.Services.Requests; +using WingsAPI.Communication.Services.Responses; +using WingsEmu.Health; + +namespace Master.Services.Maintenance +{ + public class GrpcClusterStatusService : IClusterStatusService + { + private readonly IMessagePublisher _activateMaintenancePublisher; + private readonly IMessagePublisher _deactivateMaintenancePublisher; + private readonly IStatusManager _maintenanceManager; + private readonly IMessagePublisher _maintenanceNotificationPublisher; + + private readonly TimeSpan[] _scheduledShutdownMessages = + { + TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(15), TimeSpan.FromSeconds(30), + TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(15), TimeSpan.FromMinutes(30), + TimeSpan.FromHours(1), TimeSpan.FromHours(2), TimeSpan.FromHours(4), TimeSpan.FromHours(8), TimeSpan.FromHours(16), + TimeSpan.FromDays(1) + }; + + private readonly IMessagePublisher _serviceFlushAllPublisher; + private readonly IMessagePublisher _serviceKickAllPublisher; + + private bool _maintenanceApplied; + + private CancellationTokenSource _scheduledShutdownTokenSource; + + public GrpcClusterStatusService(IStatusManager maintenanceManager, IMessagePublisher activateMaintenancePublisher, + IMessagePublisher deactivateMaintenancePublisher, IMessagePublisher maintenanceNotificationPublisher, + IMessagePublisher serviceFlushAllPublisher, IMessagePublisher serviceKickAllPublisher) + { + _maintenanceManager = maintenanceManager; + _activateMaintenancePublisher = activateMaintenancePublisher; + _deactivateMaintenancePublisher = deactivateMaintenancePublisher; + _maintenanceNotificationPublisher = maintenanceNotificationPublisher; + _serviceFlushAllPublisher = serviceFlushAllPublisher; + _serviceKickAllPublisher = serviceKickAllPublisher; + } + + public Task GetAllServicesStatus(EmptyRpcRequest req) + { + IReadOnlyList services = _maintenanceManager.GetAllServicesStatus(); + IEnumerable tmp = services.Select(s => new Service + { + Id = s.ServiceName, + Status = (ServiceHealthStatus)s.Status, + LastUpdate = s.LastUpdate + }); + return Task.FromResult(new ServiceGetAllResponse + { + Services = tmp.ToList() + }); + } + + public async Task GetServiceStatusByNameAsync(ServiceBasicRequest req) + { + ServiceStatus service = _maintenanceManager.GetServiceByName(req.ServiceName); + if (service == null) + { + return new ServiceGetStatusByNameResponse { ResponseType = RpcResponseType.GENERIC_SERVER_ERROR }; + } + + return new ServiceGetStatusByNameResponse + { + ResponseType = RpcResponseType.SUCCESS, Service = new Service + { + Id = service.ServiceName, + Status = (ServiceHealthStatus)service.Status, + LastUpdate = service.LastUpdate + } + }; + } + + public async Task EnableMaintenanceMode(ServiceBasicRequest req) + { + string serviceName = req.ServiceName; + ServiceStatus serviceStatus = _maintenanceManager.GetServiceByName(serviceName); + + if (serviceStatus.Status != ServiceStatusType.ONLINE) + { + return new BasicRpcResponse { ResponseType = RpcResponseType.GENERIC_SERVER_ERROR }; + } + + await _activateMaintenancePublisher.PublishAsync(new ServiceMaintenanceActivateMessage + { + TargetServiceName = serviceName + }); + return new BasicRpcResponse { ResponseType = RpcResponseType.SUCCESS }; + } + + public async Task DisableMaintenanceMode(ServiceBasicRequest req) + { + string serviceName = req.ServiceName; + ServiceStatus serviceStatus = _maintenanceManager.GetServiceByName(serviceName); + + if (serviceStatus.Status != ServiceStatusType.UNDER_MAINTENANCE) + { + return new BasicRpcResponse { ResponseType = RpcResponseType.GENERIC_SERVER_ERROR }; + } + + await _deactivateMaintenancePublisher.PublishAsync(new ServiceMaintenanceDeactivateMessage + { + TargetServiceName = serviceName + }); + return new BasicRpcResponse { ResponseType = RpcResponseType.SUCCESS }; + } + + public async Task ScheduleGeneralMaintenance(ServiceScheduleGeneralMaintenanceRequest maintenanceRequest) + { + if (_maintenanceApplied) + { + return new BasicRpcResponse { ResponseType = RpcResponseType.MAINTENANCE_MODE }; + } + + bool rescheduled = _scheduledShutdownTokenSource != null; + if (rescheduled) + { + _scheduledShutdownTokenSource.Cancel(); + _scheduledShutdownTokenSource.Dispose(); + } + + _scheduledShutdownTokenSource = new CancellationTokenSource(); + + Task.Run(() => ScheduleGeneralMaintenance(maintenanceRequest.ShutdownTimeSpan, _scheduledShutdownTokenSource.Token)); + + await _maintenanceNotificationPublisher.PublishAsync(new ServiceMaintenanceNotificationMessage + { + NotificationType = rescheduled ? ServiceMaintenanceNotificationType.Rescheduled : ServiceMaintenanceNotificationType.ScheduleWarning, + Reason = maintenanceRequest.Reason, + TimeLeft = maintenanceRequest.ShutdownTimeSpan + }, _scheduledShutdownTokenSource.Token); + + return new BasicRpcResponse { ResponseType = RpcResponseType.SUCCESS }; + } + + public async Task UnscheduleGeneralMaintenance(EmptyRpcRequest emptyRpcRequest) + { + if (_maintenanceApplied) + { + return new BasicRpcResponse { ResponseType = RpcResponseType.MAINTENANCE_MODE }; + } + + if (_scheduledShutdownTokenSource == null) + { + return new BasicRpcResponse { ResponseType = RpcResponseType.GENERIC_SERVER_ERROR }; + } + + _scheduledShutdownTokenSource.Cancel(); + _scheduledShutdownTokenSource.Dispose(); + _scheduledShutdownTokenSource = null; + + await _maintenanceNotificationPublisher.PublishAsync(new ServiceMaintenanceNotificationMessage + { + NotificationType = ServiceMaintenanceNotificationType.ScheduleStopped + }); + + return new BasicRpcResponse { ResponseType = RpcResponseType.SUCCESS }; + } + + public async Task ExecuteGeneralEmergencyMaintenance(ServiceExecuteGeneralEmergencyMaintenanceRequest shutdownRequest) + { + if (_maintenanceApplied) + { + return new BasicRpcResponse { ResponseType = RpcResponseType.MAINTENANCE_MODE }; + } + + if (_scheduledShutdownTokenSource != null) + { + _scheduledShutdownTokenSource.Cancel(); + _scheduledShutdownTokenSource.Dispose(); + _scheduledShutdownTokenSource = null; + } + + await ExecuteGeneralMaintenance(true, shutdownRequest.Reason); + + return new BasicRpcResponse { ResponseType = RpcResponseType.SUCCESS }; + } + + public async Task LiftGeneralMaintenance(EmptyRpcRequest shutdownRequest) + { + if (!_maintenanceApplied) + { + return new BasicRpcResponse { ResponseType = RpcResponseType.MAINTENANCE_MODE }; + } + + await LiftGeneralMaintenance(); + + return new BasicRpcResponse { ResponseType = RpcResponseType.SUCCESS }; + } + + private async Task ScheduleGeneralMaintenance(TimeSpan shutdownTimeSpan, CancellationToken cancellationToken) + { + DateTime shutdownDateTime = DateTime.UtcNow + shutdownTimeSpan; + int i; + + //Remove non needed messages + for (i = _scheduledShutdownMessages.Length - 1; i >= 0; i--) + { + if (shutdownTimeSpan <= _scheduledShutdownMessages[i]) + { + continue; + } + + break; + } + + //Send notification messages until we run out of them + while (i >= 0) + { + TimeSpan timeLeft = shutdownDateTime - DateTime.UtcNow; + TimeSpan message = _scheduledShutdownMessages[i]; + + if (timeLeft <= message) + { + await _maintenanceNotificationPublisher.PublishAsync(new ServiceMaintenanceNotificationMessage + { + NotificationType = ServiceMaintenanceNotificationType.ScheduleWarning, + TimeLeft = message + }, cancellationToken); + + i--; + continue; + } + + await Task.Delay(500, cancellationToken); + } + + if (cancellationToken.IsCancellationRequested) + { + return; + } + + //Start shutdown + await ExecuteGeneralMaintenance(false); + } + + private async Task ExecuteGeneralMaintenance(bool emergency, string reason = null) + { + _maintenanceApplied = true; + + await _activateMaintenancePublisher.PublishAsync(new ServiceMaintenanceActivateMessage + { + IsGlobal = true + }); + + await _serviceKickAllPublisher.PublishAsync(new ServiceKickAllMessage + { + IsGlobal = true + }); + + await Task.Delay(5000); + + await _serviceFlushAllPublisher.PublishAsync(new ServiceFlushAllMessage + { + IsGlobal = true + }); + + await _maintenanceNotificationPublisher.PublishAsync(new ServiceMaintenanceNotificationMessage + { + NotificationType = emergency ? ServiceMaintenanceNotificationType.EmergencyExecuted : ServiceMaintenanceNotificationType.Executed, + Reason = reason + }); + } + + private async Task LiftGeneralMaintenance() + { + await _deactivateMaintenancePublisher.PublishAsync(new ServiceMaintenanceDeactivateMessage + { + IsGlobal = true + }); + + await _maintenanceNotificationPublisher.PublishAsync(new ServiceMaintenanceNotificationMessage + { + NotificationType = ServiceMaintenanceNotificationType.Lifted + }); + + _maintenanceApplied = false; + } + } +} \ No newline at end of file diff --git a/srcs/Master/Services/Maintenance/IStatusManager.cs b/srcs/Master/Services/Maintenance/IStatusManager.cs new file mode 100644 index 0000000..47ccfd6 --- /dev/null +++ b/srcs/Master/Services/Maintenance/IStatusManager.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace Master.Services.Maintenance +{ + public interface IStatusManager + { + ServiceStatus GetServiceByName(string serviceName); + IReadOnlyList GetAllServicesStatus(); + void UpdateStatus(ServiceStatus service); + } +} \ No newline at end of file diff --git a/srcs/Master/Services/Maintenance/ServiceStatus.cs b/srcs/Master/Services/Maintenance/ServiceStatus.cs new file mode 100644 index 0000000..5aeb3bd --- /dev/null +++ b/srcs/Master/Services/Maintenance/ServiceStatus.cs @@ -0,0 +1,12 @@ +using System; +using WingsEmu.Health; + +namespace Master.Services.Maintenance +{ + public class ServiceStatus + { + public string ServiceName { get; set; } + public ServiceStatusType Status { get; set; } + public DateTime LastUpdate { get; set; } + } +} \ No newline at end of file diff --git a/srcs/Master/Services/Maintenance/StatusManager.cs b/srcs/Master/Services/Maintenance/StatusManager.cs new file mode 100644 index 0000000..fcb4e64 --- /dev/null +++ b/srcs/Master/Services/Maintenance/StatusManager.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using PhoenixLib.Logging; +using PhoenixLib.ServiceBus; +using WingsAPI.Communication.Services.Messages; +using WingsEmu.Core.Extensions; + +namespace Master.Services.Maintenance +{ + public class StatusManager : BackgroundService, IStatusManager + { + private static readonly TimeSpan Refresh = TimeSpan.FromSeconds(Convert.ToInt32(Environment.GetEnvironmentVariable("SERVICE_STATUS_REFRESH_IN_SECONDS") ?? "3")); + private static readonly TimeSpan Expiration = TimeSpan.FromSeconds(Convert.ToInt32(Environment.GetEnvironmentVariable("SERVICE_STATUS_EXPIRATION_IN_SECONDS") ?? "5")); + + private readonly Dictionary _alerts = new(); + private readonly IMessagePublisher _serviceDownPublisher; + private readonly Dictionary _serviceStatus = new(); + + public StatusManager(IMessagePublisher serviceDownPublisher) => _serviceDownPublisher = serviceDownPublisher; + + public void UpdateStatus(ServiceStatus service) + { + _serviceStatus[service.ServiceName] = service; + } + + public ServiceStatus GetServiceByName(string serviceName) => _serviceStatus.GetOrDefault(serviceName); + + public IReadOnlyList GetAllServicesStatus() => _serviceStatus.Values.ToList(); + + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + Log.Warn("[SERVICE_STATUS_MANAGER] Starting..."); + while (!stoppingToken.IsCancellationRequested) + { + var services = _serviceStatus.Values.ToList(); + Log.Debug($"[SERVICE_STATUS_MANAGER] Checking {services.Count} status..."); + + DateTime dateTime = DateTime.UtcNow; + foreach (ServiceStatus service in services) + { + if (service.LastUpdate + Expiration > dateTime) + { + Log.Debug($"[SERVICE_STATUS_MANAGER] {service.ServiceName} OK..."); + continue; + } + + // add alert + Log.Warn($"[SERVICE_STATUS_MANAGER] {service.ServiceName} is offline"); + + if (!_alerts.TryGetValue(service.ServiceName, out DateTime lastAlert)) + { + lastAlert = DateTime.MinValue; + } + + if (lastAlert + TimeSpan.FromMinutes(5) > dateTime) + { + continue; + } + + _alerts[service.ServiceName] = dateTime; + await _serviceDownPublisher.PublishAsync(new ServiceDownMessage + { + ServiceName = service.ServiceName, + LastUpdate = service.LastUpdate + }); + } + + await Task.Delay(Refresh, stoppingToken); + } + } + } +} \ No newline at end of file diff --git a/srcs/Master/Services/Maintenance/StatusRefreshMessageConsumer.cs b/srcs/Master/Services/Maintenance/StatusRefreshMessageConsumer.cs new file mode 100644 index 0000000..73e6b54 --- /dev/null +++ b/srcs/Master/Services/Maintenance/StatusRefreshMessageConsumer.cs @@ -0,0 +1,26 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; +using WingsEmu.Health; + +namespace Master.Services.Maintenance +{ + public class StatusRefreshMessageConsumer : IMessageConsumer + { + private readonly IStatusManager _statusManager; + + public StatusRefreshMessageConsumer(IStatusManager statusManager) => _statusManager = statusManager; + + public async Task HandleAsync(ServiceStatusUpdateMessage notification, CancellationToken token) => + _statusManager.UpdateStatus(new ServiceStatus + { + Status = notification.StatusType, + LastUpdate = notification.LastUpdate, + ServiceName = notification.ServiceName + }); + } +} \ No newline at end of file diff --git a/srcs/Master/Services/Sessions/EncryptionKeyFactory.cs b/srcs/Master/Services/Sessions/EncryptionKeyFactory.cs new file mode 100644 index 0000000..efba40e --- /dev/null +++ b/srcs/Master/Services/Sessions/EncryptionKeyFactory.cs @@ -0,0 +1,13 @@ +namespace WingsEmu.Master.Sessions +{ + public class EncryptionKeyFactory + { + private int _key; + + public int CreateEncryptionKey() + { + _key += 2; + return _key; + } + } +} \ No newline at end of file diff --git a/srcs/Master/Services/Sessions/ISessionManager.cs b/srcs/Master/Services/Sessions/ISessionManager.cs new file mode 100644 index 0000000..139002e --- /dev/null +++ b/srcs/Master/Services/Sessions/ISessionManager.cs @@ -0,0 +1,14 @@ +using System.Threading.Tasks; +using WingsAPI.Communication.Sessions.Model; + +namespace WingsEmu.Master.Sessions +{ + public interface ISessionManager + { + Task Create(Session session); + Task Update(Session session); + Task GetSessionByAccountName(string accountName); + Task GetSessionByAccountId(long accountId); + Task Pulse(Session session); + } +} \ No newline at end of file diff --git a/srcs/Master/Services/Sessions/RedisSessionManager.cs b/srcs/Master/Services/Sessions/RedisSessionManager.cs new file mode 100644 index 0000000..356e671 --- /dev/null +++ b/srcs/Master/Services/Sessions/RedisSessionManager.cs @@ -0,0 +1,105 @@ +using System; +using System.Threading.Tasks; +using Newtonsoft.Json; +using StackExchange.Redis; +using WingsAPI.Communication.Sessions.Model; + +namespace WingsEmu.Master.Sessions +{ + public class RedisSessionManager : ISessionManager + { + private const string SessionPrefix = "session"; + private const string SessionMappingPrefix = "session:mapping"; + private static readonly TimeSpan Ttl = TimeSpan.FromMinutes(4); + + private readonly IDatabase _db; + + public RedisSessionManager(IConnectionMultiplexer multiplexer) => _db = multiplexer.GetDatabase(0); + + public async Task Create(Session session) + { + bool exists = await _db.KeyExistsAsync(CreateSessionKey(session.Id)); + if (exists) + { + return false; + } + + await Set(session); + + return true; + } + + public async Task Update(Session session) + { + bool exists = await _db.KeyExistsAsync(CreateSessionKey(session.Id)); + if (!exists) + { + return false; + } + + await Set(session); + + return true; + } + + public async Task GetSessionByAccountName(string accountName) + { + string sessionKey = await _db.StringGetAsync(CreateAccountNameMappingKey(accountName)); + if (sessionKey is null) + { + return default; + } + + string serializedSession = await _db.StringGetAsync(sessionKey); + if (serializedSession is null) + { + return default; + } + + return JsonConvert.DeserializeObject(serializedSession); + } + + public async Task GetSessionByAccountId(long accountId) + { + string sessionKey = await _db.StringGetAsync(CreateAccountIdMappingKey(accountId)); + if (sessionKey is null) + { + return default; + } + + string serializedSession = await _db.StringGetAsync(sessionKey); + if (serializedSession is null) + { + return default; + } + + return JsonConvert.DeserializeObject(serializedSession); + } + + public async Task Pulse(Session session) + { + bool exists = await _db.KeyExistsAsync(CreateSessionKey(session.Id)); + if (!exists) + { + return false; + } + + await _db.KeyExpireAsync(CreateSessionKey(session.Id), Ttl); + await _db.KeyExpireAsync(CreateAccountIdMappingKey(session.AccountId), Ttl); + await _db.KeyExpireAsync(CreateAccountNameMappingKey(session.AccountName), Ttl); + + return true; + } + + private async Task Set(Session session) + { + await _db.StringSetAsync(CreateSessionKey(session.Id), JsonConvert.SerializeObject(session), Ttl); + await _db.StringSetAsync(CreateAccountIdMappingKey(session.AccountId), CreateSessionKey(session.Id), Ttl); + await _db.StringSetAsync(CreateAccountNameMappingKey(session.AccountName), CreateSessionKey(session.Id), Ttl); + } + + private static string CreateSessionKey(string sessionId) => $"{SessionPrefix}:{sessionId}"; + private static string CreateAccountIdMappingKey(long accountId) => $"{SessionMappingPrefix}:account-id:{accountId}"; + private static string CreateAccountNameMappingKey(string accountName) => $"{SessionMappingPrefix}:account-name:{accountName}"; + } +} \ No newline at end of file diff --git a/srcs/Master/Services/Sessions/SessionService.cs b/srcs/Master/Services/Sessions/SessionService.cs new file mode 100644 index 0000000..a403c78 --- /dev/null +++ b/srcs/Master/Services/Sessions/SessionService.cs @@ -0,0 +1,439 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Logging; +using WingsAPI.Communication; +using WingsAPI.Communication.Sessions; +using WingsAPI.Communication.Sessions.Model; +using WingsAPI.Communication.Sessions.Request; +using WingsAPI.Communication.Sessions.Response; + +namespace WingsEmu.Master.Sessions +{ + public class SessionService : ISessionService + { + private static readonly SemaphoreSlim _semaphoreSlim = new(1, 1); + private readonly EncryptionKeyFactory _encryptionKeyFactory; + private readonly ISessionManager _sessionManager; + + public SessionService(ISessionManager sessionManager, EncryptionKeyFactory encryptionKeyFactory) + { + _sessionManager = sessionManager; + _encryptionKeyFactory = encryptionKeyFactory; + } + + public async ValueTask CreateSession(CreateSessionRequest request) + { + await _semaphoreSlim.WaitAsync(); + try + { + Session existingSession = await _sessionManager.GetSessionByAccountId(request.AccountId); + if (existingSession is not null && existingSession.State != SessionState.Disconnected && existingSession.State != SessionState.ServerSelection) + { + Log.Debug($"[SESSION_SERVICE][CREATE_SESSION] A Session for account {request.AccountId} already exists"); + return new SessionResponse + { + ResponseType = RpcResponseType.GENERIC_SERVER_ERROR + }; + } + + var session = new Session + { + Id = Guid.NewGuid().ToString().Replace("-", ""), + IpAddress = request.IpAddress, + AccountId = request.AccountId, + AccountName = request.AccountName, + Authority = request.AuthorityType, + State = SessionState.Disconnected, + EncryptionKey = _encryptionKeyFactory.CreateEncryptionKey() + }; + + bool created = await _sessionManager.Create(session); + if (!created) + { + Log.Debug($"[SESSION_SERVICE][CREATE_SESSION] Failed to save session of account {session.AccountId} with session id {session.Id} into redis"); + return new SessionResponse + { + ResponseType = RpcResponseType.GENERIC_SERVER_ERROR + }; + } + + Log.Debug($"[SESSION_SERVICE][CREATE_SESSION] Successfully created session for account {session.AccountId} with session id {session.Id}"); + return new SessionResponse + { + ResponseType = RpcResponseType.SUCCESS, + Session = session + }; + } + finally + { + _semaphoreSlim.Release(); + } + } + + public async ValueTask GetSessionByAccountName(GetSessionByAccountNameRequest request) + { + await _semaphoreSlim.WaitAsync(); + try + { + Session session = await _sessionManager.GetSessionByAccountName(request.AccountName); + if (session is null) + { + return new SessionResponse + { + ResponseType = RpcResponseType.GENERIC_SERVER_ERROR + }; + } + + return new SessionResponse + { + ResponseType = RpcResponseType.SUCCESS, + Session = session + }; + } + finally + { + _semaphoreSlim.Release(); + } + } + + public async ValueTask GetSessionByAccountId(GetSessionByAccountIdRequest request) + { + await _semaphoreSlim.WaitAsync(); + try + { + Session session = await _sessionManager.GetSessionByAccountId(request.AccountId); + if (session is null) + { + Log.Debug($"[SESSION_SERVICE][GET_SESSION_BY_ACCOUNT_ID] Couldn't find session with account id: {request.AccountId}"); + return new SessionResponse + { + ResponseType = RpcResponseType.GENERIC_SERVER_ERROR + }; + } + + Log.Debug($"[SESSION_SERVICE][GET_SESSION_BY_ACCOUNT_ID] Successfully found a session with account id: {request.AccountId}"); + return new SessionResponse + { + ResponseType = RpcResponseType.SUCCESS, + Session = session + }; + } + finally + { + _semaphoreSlim.Release(); + } + } + + public async ValueTask ConnectToLoginServer(ConnectToLoginServerRequest request) + { + await _semaphoreSlim.WaitAsync(); + try + { + Session session = await _sessionManager.GetSessionByAccountId(request.AccountId); + if (session is null) + { + Log.Debug($"[SESSION_SERVICE][CONNECT_TO_LOGIN_SERVER] Can't connect account with ID {request.AccountId} to login server (No session found)"); + return new SessionResponse + { + ResponseType = RpcResponseType.GENERIC_SERVER_ERROR + }; + } + + if (session.State != SessionState.Disconnected && session.State != SessionState.CharacterSelection) + { + Log.Debug($"[SESSION_SERVICE][CONNECT_TO_LOGIN_SERVER] Can't session with ID {request.AccountId} to login server (Already connected)"); + return new SessionResponse + { + ResponseType = RpcResponseType.GENERIC_SERVER_ERROR + }; + } + + session.State = SessionState.ServerSelection; + session.HardwareId = request.HardwareId; + session.ClientVersion = request.ClientVersion; + + bool updated = await _sessionManager.Update(session); + if (!updated) + { + Log.Debug($"[SESSION_SERVICE][CONNECT_TO_LOGIN_SERVER] Failed to update session of {session.AccountId} into redis"); + return new SessionResponse + { + ResponseType = RpcResponseType.GENERIC_SERVER_ERROR + }; + } + + Log.Debug($"[SESSION_SERVICE][CONNECT_TO_LOGIN_SERVER] Successfully connected session of {session.AccountId} to login server"); + return new SessionResponse + { + ResponseType = RpcResponseType.SUCCESS, + Session = session + }; + } + finally + { + _semaphoreSlim.Release(); + } + } + + public async ValueTask Disconnect(DisconnectSessionRequest request) + { + await _semaphoreSlim.WaitAsync(); + try + { + Session session = await _sessionManager.GetSessionByAccountId(request.AccountId); + if (session is null) + { + Log.Debug($"[SESSION_SERVICE][DISCONNECT] Can't disconnect session of account {request.AccountId} (Couldn't find session)"); + return new SessionResponse + { + ResponseType = RpcResponseType.GENERIC_SERVER_ERROR + }; + } + + if (session.State == SessionState.CrossChannelAuthentication) + { + Log.Debug($"[SESSION_SERVICE][DISCONNECT] Can't disconnect session of account {request.AccountId} (CrossChannel or CharacterSelection)"); + return new SessionResponse + { + ResponseType = RpcResponseType.GENERIC_SERVER_ERROR + }; + } + + if (request.ForceDisconnect == false) + { + if (session.EncryptionKey != request.EncryptionKey) + { + Log.Debug($"[SESSION_SERVICE][DISCONNECT] Can't disconnect session of account {request.AccountId} (CrossChannel or CharacterSelection)"); + return new SessionResponse + { + ResponseType = RpcResponseType.GENERIC_SERVER_ERROR + }; + } + } + + session.State = SessionState.Disconnected; + session.CharacterId = 0; + session.ChannelId = 0; + session.ServerGroup = null; + + bool saved = await _sessionManager.Update(session); + if (!saved) + { + Log.Debug($"[SESSION_SERVICE][DISCONNECT] Failed to update session of account {request.AccountId}"); + return new SessionResponse + { + ResponseType = RpcResponseType.GENERIC_SERVER_ERROR + }; + } + + Log.Debug($"[SESSION_SERVICE][DISCONNECT] Successfully updated session of account {request.AccountId}"); + return new SessionResponse + { + ResponseType = RpcResponseType.SUCCESS + }; + } + finally + { + _semaphoreSlim.Release(); + } + } + + public async ValueTask ConnectCharacter(ConnectCharacterRequest request) + { + await _semaphoreSlim.WaitAsync(); + try + { + Session session = await _sessionManager.GetSessionByAccountId(request.AccountId); + if (session is null || session.State != SessionState.CharacterSelection || session.ChannelId != request.ChannelId) + { + Log.Debug($"[SESSION_SERVICE][CONNECT_CHARACTER] Can't connect character from session of account {request.AccountId} (Couldn't find session)"); + return new SessionResponse + { + ResponseType = RpcResponseType.GENERIC_SERVER_ERROR + }; + } + + session.CharacterId = request.CharacterId; + session.State = SessionState.InGame; + + bool updated = await _sessionManager.Update(session); + if (!updated) + { + Log.Debug($"[SESSION_SERVICE][CONNECT_CHARACTER] Failed to update session of account {request.AccountId}"); + return new SessionResponse + { + ResponseType = RpcResponseType.GENERIC_SERVER_ERROR + }; + } + + Log.Debug($"[SESSION_SERVICE][CONNECT_CHARACTER] Successfully update session of account {request.AccountId}"); + return new SessionResponse + { + ResponseType = RpcResponseType.SUCCESS, + Session = session + }; + } + finally + { + _semaphoreSlim.Release(); + } + } + + public async ValueTask ActivateCrossChannelAuthentication(ActivateCrossChannelAuthenticationRequest request) + { + await _semaphoreSlim.WaitAsync(); + try + { + Session session = await _sessionManager.GetSessionByAccountId(request.AccountId); + if (session is null || session.State != SessionState.InGame) + { + Log.Debug($"[SESSION_SERVICE][ACTIVE_CROSS_CHANNEL_AUTHENTICATION] Couldn't find session for account {request.AccountId} or account doesn't have correct state ({session?.State})"); + return new SessionResponse + { + ResponseType = RpcResponseType.GENERIC_SERVER_ERROR + }; + } + + session.State = SessionState.CrossChannelAuthentication; + session.AllowedCrossChannelId = request.ChannelId; + + bool updated = await _sessionManager.Update(session); + if (!updated) + { + Log.Debug($"[SESSION_SERVICE][ACTIVE_CROSS_CHANNEL_AUTHENTICATION] Failed to update session of account {request.AccountId}"); + return new SessionResponse + { + ResponseType = RpcResponseType.GENERIC_SERVER_ERROR + }; + } + + + Log.Debug($"[SESSION_SERVICE][ACTIVE_CROSS_CHANNEL_AUTHENTICATION] Successfully updated session of account {request.AccountId}"); + return new SessionResponse + { + ResponseType = RpcResponseType.SUCCESS, + Session = session + }; + } + finally + { + _semaphoreSlim.Release(); + } + } + + public async ValueTask Pulse(PulseRequest request) + { + await _semaphoreSlim.WaitAsync(); + try + { + Session session = await _sessionManager.GetSessionByAccountId(request.AccountId); + if (session is null || session.State != SessionState.InGame) + { + Log.Debug($"[SESSION_SERVICE][PULSE] Couldn't find session of account or incorrect state {request.AccountId} ({session?.State})"); + return new SessionResponse + { + ResponseType = RpcResponseType.GENERIC_SERVER_ERROR + }; + } + + bool pulsed = await _sessionManager.Pulse(session); + if (!pulsed) + { + Log.Debug($"[SESSION_SERVICE][PULSE] Failed to pulse session of account {request.AccountId}"); + return new SessionResponse + { + ResponseType = RpcResponseType.GENERIC_SERVER_ERROR + }; + } + + session.LastPulse = DateTime.UtcNow; + + bool updated = await _sessionManager.Update(session); + if (!updated) + { + Log.Debug($"[SESSION_SERVICE][PULSE] Failed to update session of account {request.AccountId} after pulse"); + return new SessionResponse + { + ResponseType = RpcResponseType.GENERIC_SERVER_ERROR + }; + } + + + Log.Debug($"[SESSION_SERVICE][PULSE] Successfully pulsed session of account {request.AccountId}"); + return new SessionResponse + { + ResponseType = RpcResponseType.SUCCESS, + Session = session + }; + } + finally + { + _semaphoreSlim.Release(); + } + } + + public async ValueTask ConnectToWorldServer(ConnectToWorldServerRequest request) + { + await _semaphoreSlim.WaitAsync(); + try + { + Session session = await _sessionManager.GetSessionByAccountId(request.AccountId); + if (session is null) + { + Log.Debug($"[SESSION_SERVICE][CONNECT_TO_WORLD_SERVER] Couldn't find session of account or incorrect state {request.AccountId}"); + return new SessionResponse + { + ResponseType = RpcResponseType.GENERIC_SERVER_ERROR + }; + } + + // Can't connect to world if not in server selection screen or cross channel + if (session.State != SessionState.ServerSelection && session.State != SessionState.CrossChannelAuthentication) + { + Log.Debug($"[SESSION_SERVICE][CONNECT_TO_WORLD_SERVER] Incorrect session state {session.State}"); + return new SessionResponse + { + ResponseType = RpcResponseType.GENERIC_SERVER_ERROR + }; + } + + if (session.State == SessionState.CrossChannelAuthentication && session.AllowedCrossChannelId != request.ChannelId) + { + Log.Debug($"[SESSION_SERVICE][CONNECT_TO_WORLD_SERVER] Incorrect cross channel ID {request.ChannelId} instead of {session.AllowedCrossChannelId}"); + return new SessionResponse + { + ResponseType = RpcResponseType.GENERIC_SERVER_ERROR + }; + } + + session.LastChannelId = session.ChannelId; + session.ServerGroup = request.ServerGroup; + session.ChannelId = request.ChannelId; + session.State = SessionState.CharacterSelection; + session.AllowedCrossChannelId = 0; + + bool updated = await _sessionManager.Update(session); + if (!updated) + { + Log.Debug($"[SESSION_SERVICE][CONNECT_TO_WORLD_SERVER] Failed to update session of account {session.AccountId}"); + return new SessionResponse + { + ResponseType = RpcResponseType.GENERIC_SERVER_ERROR + }; + } + + Log.Debug($"[SESSION_SERVICE][CONNECT_TO_WORLD_SERVER] Successfully updated session of account {session.AccountId}"); + return new SessionResponse + { + ResponseType = RpcResponseType.SUCCESS, + Session = session + }; + } + finally + { + _semaphoreSlim.Release(); + } + } + } +} \ No newline at end of file diff --git a/srcs/Master/Startup.cs b/srcs/Master/Startup.cs new file mode 100644 index 0000000..edb69ce --- /dev/null +++ b/srcs/Master/Startup.cs @@ -0,0 +1,123 @@ +using System; +using Master.Consumers; +using Master.Managers; +using Master.Proxies; +using Master.RecurrentJobs; +using Master.Services.Maintenance; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using PhoenixLib.DAL; +using PhoenixLib.DAL.Redis; +using PhoenixLib.Logging; +using PhoenixLib.Scheduler.ReactiveX; +using PhoenixLib.ServiceBus.Extensions; +using Plugin.Database; +using Plugin.Database.DB; +using Plugin.Database.Mapping; +using ProtoBuf.Grpc.Server; +using RandN; +using RandN.Compat; +using WingsAPI.Communication.ServerApi; +using WingsAPI.Communication.Services.Messages; +using WingsEmu.Game; +using WingsEmu.Health; +using WingsEmu.Master; +using WingsEmu.Master.Sessions; +using WingsEmu.Plugins.DistributedGameEvents.PlayerEvents; + +namespace Master +{ + public class RandomGenerator : IRandomGenerator + { + private static readonly Random Local = RandomShim.Create(SmallRng.Create()); + + public int RandomNumber(int min, int max) + { + if (min > max) + { + return RandomNumber(max, min); + } + + return min == max ? max : Local.Next(min, max); + } + + public int RandomNumber(int max) => RandomNumber(0, max); + + public int RandomNumber() => RandomNumber(0, 100); + } + + public class Startup + { + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) + { + services.AddScheduler(); + services.AddCron(); + services.AddPhoenixLogging(); + + if (bool.TryParse(Environment.GetEnvironmentVariable("WORLD_PULSE_SYSTEM") ?? "false", out bool pulseSystem) && pulseSystem) + { + services.AddHostedService(); + } + + services.AddSingleton(); + services.AddSingleton(s => RedisConfiguration.FromEnv()); + services.AddSingleton(s => s.GetRequiredService().GetConnectionMultiplexer()); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddCodeFirstGrpc(config => + { + config.MaxReceiveMessageSize = null; + config.MaxSendMessageSize = null; + config.EnableDetailedErrors = true; + }); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + services.AddMqttConfigurationFromEnv(); + services.AddMessagePublisher(); + services.AddMessagePublisher(); + + services.AddSingleton(); + services.AddHostedService(s => s.GetRequiredService() as StatusManager); + services.AddSingleton(); + services.AddMessagePublisher(); + services.AddMessageSubscriber(); + services.AddMessagePublisher(); + services.AddMessagePublisher(); + services.AddMessagePublisher(); + services.AddMessagePublisher(); + services.AddMessagePublisher(); + services.AddMessageSubscriber(); + services.AddMessageSubscriber(); + + new DatabasePlugin().AddDependencies(services); + services.AddTransient(typeof(IMapper<,>), typeof(MapsterMapper<,>)); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseRouting(); + + app.UseEndpoints(endpoints => + { + endpoints.MapGrpcService(); + endpoints.MapGrpcService(); + endpoints.MapGrpcService(); + endpoints.MapGrpcService(); + }); + } + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Auth.JWT/DependencyInjectionExtensions.cs b/srcs/PhoenixLib.Auth.JWT/DependencyInjectionExtensions.cs new file mode 100644 index 0000000..5e70d6a --- /dev/null +++ b/srcs/PhoenixLib.Auth.JWT/DependencyInjectionExtensions.cs @@ -0,0 +1,14 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace PhoenixLib.Auth.JWT +{ + public static class DependencyInjectionExtensions + { + public static void AddJwtFactoryFromEnv(this IServiceCollection services) + { + services.TryAddSingleton(new JwtTokenFactory(Environment.GetEnvironmentVariable("JWT_PRIVATE_KEY"))); + } + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Auth.JWT/HashingHelper.cs b/srcs/PhoenixLib.Auth.JWT/HashingHelper.cs new file mode 100644 index 0000000..b869f90 --- /dev/null +++ b/srcs/PhoenixLib.Auth.JWT/HashingHelper.cs @@ -0,0 +1,15 @@ +using System.Linq; +using System.Security.Cryptography; +using System.Text; + +namespace PhoenixLib.Auth.JWT +{ + internal static class HashingHelper + { + internal static string ToSha512(this string str) + { + using var hash = SHA512.Create(); + return string.Concat(hash.ComputeHash(Encoding.UTF8.GetBytes(str)).Select(item => item.ToString("x2"))); + } + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Auth.JWT/IJwtTokenFactory.cs b/srcs/PhoenixLib.Auth.JWT/IJwtTokenFactory.cs new file mode 100644 index 0000000..677349b --- /dev/null +++ b/srcs/PhoenixLib.Auth.JWT/IJwtTokenFactory.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using System.Security.Claims; +using Microsoft.IdentityModel.Tokens; + +namespace PhoenixLib.Auth.JWT +{ + public interface IJwtTokenFactory + { + string CreateJwtToken(); + string CreateJwtToken(params Claim[] claims); + IEnumerable ObtainClaimsFromJwtToken(string jwtToken, TokenValidationParameters validationParameters); + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Auth.JWT/JwtExtensions.cs b/srcs/PhoenixLib.Auth.JWT/JwtExtensions.cs new file mode 100644 index 0000000..d55d0b3 --- /dev/null +++ b/srcs/PhoenixLib.Auth.JWT/JwtExtensions.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; + +namespace PhoenixLib.Auth.JWT +{ + public static class JwtExtensions + { + public static bool EqualsTo(this Claim x, Claim y) + { + if (x == null || y == null || x == y) + { + return true; + } + + if (x.Type == y.Type && x.Value == y.Value) + { + return true; + } + + return false; + } + + public static bool ContainsAll(this IEnumerable values, IEnumerable expectedValues) + { + int initialAmountOfValues = expectedValues.Count(); + var equalValues = new HashSet(); + foreach (Claim value in values) + { + foreach (Claim value2 in expectedValues) + { + if (value.EqualsTo(value2)) + { + equalValues.Add(value2.ToString()); + } + + if (equalValues.Count >= initialAmountOfValues) + { + return true; + } + } + } + + return false; + } + + public static bool Contains(this IEnumerable values, Claim expectedValue) + { + foreach (Claim value in values) + { + if (value.EqualsTo(expectedValue)) + { + return true; + } + } + + return false; + } + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Auth.JWT/JwtTokenFactory.cs b/srcs/PhoenixLib.Auth.JWT/JwtTokenFactory.cs new file mode 100644 index 0000000..5600886 --- /dev/null +++ b/srcs/PhoenixLib.Auth.JWT/JwtTokenFactory.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using Microsoft.IdentityModel.Tokens; + +namespace PhoenixLib.Auth.JWT +{ + public class JwtTokenFactory : IJwtTokenFactory + { + private const string SigningKeyAlgorithm = SecurityAlgorithms.HmacSha256Signature; + private const string SigningKeyAlgorithmShortName = "HS256"; //Yeah, Idk how to obtain this, but it basically represents the algorithm we use. + private readonly string _audience; + private readonly string _issuer; + private readonly string _jwtPrivateKey; + private readonly TimeSpan _tokenLifeTime; + + public JwtTokenFactory(string jwtPrivateKey, string issuer = null, string audience = null, TimeSpan? tokenLifeTime = null) + { + _jwtPrivateKey = jwtPrivateKey; + _issuer = issuer ?? "https://noswings.com"; + _audience = audience ?? "https://noswings.com"; + _tokenLifeTime = tokenLifeTime ?? TimeSpan.FromHours(1); + } + + private SecurityKey GetSigningKey => new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtPrivateKey.ToSha512())); + + public string CreateJwtToken() => CreateJwtToken(Array.Empty()); + + public string CreateJwtToken(params Claim[] claims) + { + SecurityKey signinKey = GetSigningKey; + var handler = new JwtSecurityTokenHandler(); + + var claimsIdentity = new ClaimsIdentity(claims); + SecurityToken securityToken = handler.CreateToken(new SecurityTokenDescriptor + { + Subject = claimsIdentity, + Issuer = _issuer, + Audience = _audience, + Expires = DateTime.UtcNow + _tokenLifeTime, + SigningCredentials = new SigningCredentials(signinKey, SigningKeyAlgorithm) + }); + return handler.WriteToken(securityToken); + } + + public IEnumerable ObtainClaimsFromJwtToken(string jwtToken, TokenValidationParameters validationParameters) + { + validationParameters.ValidIssuer = _issuer; + validationParameters.ValidAudience = _audience; + validationParameters.ValidAlgorithms = new[] + { + SigningKeyAlgorithmShortName + }; + validationParameters.IssuerSigningKey = GetSigningKey; + if (validationParameters.ValidateLifetime) + { + validationParameters.LifetimeValidator = LifetimeValidator; + } + + var tokenHandler = new JwtSecurityTokenHandler(); + ClaimsPrincipal claimsPrincipal = tokenHandler.ValidateToken(jwtToken, validationParameters, out _); + + return claimsPrincipal.Claims; + } + + private static bool LifetimeValidator(DateTime? notBefore, DateTime? expires, SecurityToken securityToken, TokenValidationParameters validationParameters) + { + if (expires == null) + { + return false; + } + + return DateTime.UtcNow < expires; + } + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Auth.JWT/PhoenixLib.Auth.JWT.csproj b/srcs/PhoenixLib.Auth.JWT/PhoenixLib.Auth.JWT.csproj new file mode 100644 index 0000000..2a9719f --- /dev/null +++ b/srcs/PhoenixLib.Auth.JWT/PhoenixLib.Auth.JWT.csproj @@ -0,0 +1,14 @@ + + + + net5.0 + + + + + + + + + + diff --git a/srcs/PhoenixLib.Caching/ICachedRepository.cs b/srcs/PhoenixLib.Caching/ICachedRepository.cs new file mode 100644 index 0000000..45346b9 --- /dev/null +++ b/srcs/PhoenixLib.Caching/ICachedRepository.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace PhoenixLib.Caching +{ + public interface ICachedRepository + { + T Get(TKey id); + T Get(TKey id, string prefix); + + T GetOrSet(TKey id, Func fetchDelegate); + T GetOrSet(TKey id, Func fetchDelegate, TimeSpan timeToKeepInCache); + + Task GetOrSetAsync(TKey id, Func> fetchDelegate); + Task GetOrSetAsync(TKey id, Func> fetchDelegate, TimeSpan timeToKeepInCache); + + IReadOnlyList GetValues(IEnumerable keys); + IReadOnlyList GetValues(IEnumerable keys, string prefix); + + + void Set(TKey id, T value); + void Set(TKey id, T value, TimeSpan timeToKeepInCache); + void Set(TKey id, T value, string prefix); + void Set(TKey id, T value, string prefix, TimeSpan timeToKeepInCache); + + Task SetAsync(TKey id, Func> fetchDelegate); + Task SetAsync(TKey id, Func> fetchDelegate, TimeSpan timeToKeepInCache); + + void Remove(TKey id); + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Caching/IKeyValueCache.cs b/srcs/PhoenixLib.Caching/IKeyValueCache.cs new file mode 100644 index 0000000..eae31bf --- /dev/null +++ b/srcs/PhoenixLib.Caching/IKeyValueCache.cs @@ -0,0 +1,6 @@ +namespace PhoenixLib.Caching +{ + public interface IKeyValueCache : ICachedRepository + { + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Caching/ILongKeyCachedRepository.cs b/srcs/PhoenixLib.Caching/ILongKeyCachedRepository.cs new file mode 100644 index 0000000..8db0306 --- /dev/null +++ b/srcs/PhoenixLib.Caching/ILongKeyCachedRepository.cs @@ -0,0 +1,6 @@ +namespace PhoenixLib.Caching +{ + public interface ILongKeyCachedRepository : ICachedRepository + { + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Caching/IUuidKeyCachedRepository.cs b/srcs/PhoenixLib.Caching/IUuidKeyCachedRepository.cs new file mode 100644 index 0000000..1f70cb1 --- /dev/null +++ b/srcs/PhoenixLib.Caching/IUuidKeyCachedRepository.cs @@ -0,0 +1,8 @@ +using System; + +namespace PhoenixLib.Caching +{ + public interface IUuidKeyCachedRepository : ICachedRepository + { + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Caching/InMemoryCacheRepository.cs b/srcs/PhoenixLib.Caching/InMemoryCacheRepository.cs new file mode 100644 index 0000000..56efb7c --- /dev/null +++ b/srcs/PhoenixLib.Caching/InMemoryCacheRepository.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using CacheManager.Core; + +namespace PhoenixLib.Caching +{ + public class InMemoryCacheRepository : ILongKeyCachedRepository + { + private static readonly string Prefix = "repo:" + typeof(T).Name.ToLower(); + + private static readonly ICacheManager CacheManager = CacheFactory.Build(Prefix, + settings => { settings.WithSystemRuntimeCacheHandle(Prefix); }); + + public void Set(long id, T value) + { + Set(id, value, Prefix); + } + + public void Set(long id, T value, TimeSpan timeToKeepInCache) + { + Set(id, value, Prefix, timeToKeepInCache); + } + + public void Set(long id, T value, string prefix) + { + CacheManager.Put(ToKey(prefix, id), value); + } + + public void Set(long id, T value, string prefix, TimeSpan timeToKeepInCache) + { + CacheManager.Put(new CacheItem(ToKey(prefix, id), value, ExpirationMode.Sliding, timeToKeepInCache)); + } + + public async Task SetAsync(long id, Func> fetchDelegate) => Set(id, await fetchDelegate.Invoke(), Prefix); + + public async Task SetAsync(long id, Func> fetchDelegate, TimeSpan timeToKeepInCache) => + CacheManager.Put(new CacheItem(ToKey(id), await fetchDelegate.Invoke(), ExpirationMode.Sliding, timeToKeepInCache)); + + public void Remove(long id) + { + CacheManager.Remove(ToKey(id)); + } + + public T Get(long id) => Get(id, Prefix); + + public T Get(long id, string prefix) => CacheManager.Get(ToKey(prefix, id)); + + public T GetOrSet(long id, Func fetchDelegate) + { + CacheItem cacheItem = CacheManager.GetCacheItem(ToKey(id)); + if (cacheItem != null) + { + return cacheItem.Value; + } + + Set(id, fetchDelegate()); + return CacheManager.Get(ToKey(id)); + } + + public T GetOrSet(long id, Func fetchDelegate, TimeSpan timeToKeepInCache) + { + CacheItem cacheItem = CacheManager.GetCacheItem(ToKey(id)); + if (cacheItem != null) + { + return cacheItem.Value; + } + + Set(id, fetchDelegate(), timeToKeepInCache); + return CacheManager.Get(ToKey(id)); + } + + public async Task GetOrSetAsync(long id, Func> fetchDelegate) => CacheManager.GetOrAdd(ToKey(id), await fetchDelegate.Invoke()); + + public async Task GetOrSetAsync(long id, Func> fetchDelegate, TimeSpan timeToKeepInCache) + { + CacheItem cacheItem = CacheManager.GetCacheItem(ToKey(id)); + if (cacheItem != null) + { + return cacheItem.Value; + } + + await SetAsync(id, fetchDelegate, timeToKeepInCache); + return CacheManager.Get(ToKey(id)); + } + + public IReadOnlyList GetValues(IEnumerable keys) => GetValues(keys, Prefix); + + public IReadOnlyList GetValues(IEnumerable keys, string prefix) + { + return keys.Select(key => CacheManager.Get(ToKey(prefix, key))).Where(result => !result.Equals(default(T))) + .ToList(); + } + + private static string ToKey(long id) => ToKey(Prefix, id); + + private static string ToKey(string prefix, long id) => $"{prefix}:{id}"; + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Caching/InMemoryKeyValueCache.cs b/srcs/PhoenixLib.Caching/InMemoryKeyValueCache.cs new file mode 100644 index 0000000..3a1d77f --- /dev/null +++ b/srcs/PhoenixLib.Caching/InMemoryKeyValueCache.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using CacheManager.Core; + +namespace PhoenixLib.Caching +{ + public class InMemoryKeyValueCache : IKeyValueCache + { + private static readonly string Prefix = "kv:" + typeof(T).Name.ToLower(); + + private static readonly ICacheManager CacheManager = CacheFactory.Build(Prefix, + settings => { settings.WithSystemRuntimeCacheHandle(Prefix); }); + + public void Set(string id, T value) + { + Set(id, value, Prefix); + } + + public void Set(string id, T value, string prefix) + { + CacheManager.Put(ToKey(prefix, id), value); + } + + public void Set(string id, T value, TimeSpan timeToKeepInCache) + { + Set(id, value, Prefix, timeToKeepInCache); + } + + public void Set(string id, T value, string prefix, TimeSpan timeToKeepInCache) + { + CacheManager.Put(new CacheItem(ToKey(prefix, id), value, ExpirationMode.Sliding, timeToKeepInCache)); + } + + public async Task SetAsync(string id, Func> fetchDelegate) + { + T obj = await fetchDelegate.Invoke(); + Set(id, obj); + } + + public async Task SetAsync(string id, Func> fetchDelegate, TimeSpan timeToKeepInCache) + { + T obj = await fetchDelegate.Invoke(); + Set(id, obj, timeToKeepInCache); + } + + public void Remove(string id) + { + CacheManager.Remove(ToKey(id)); + } + + public T Get(string id) => Get(id, Prefix); + + public T Get(string id, string prefix) => CacheManager.Get(ToKey(prefix, id)); + + public T GetOrSet(string id, Func fetchDelegate) + { + CacheItem cacheItem = CacheManager.GetCacheItem(ToKey(id)); + if (cacheItem != null) + { + return cacheItem.Value; + } + + T obj = fetchDelegate(); + Set(id, obj); + return CacheManager.Get(ToKey(id)); + } + + public T GetOrSet(string id, Func fetchDelegate, TimeSpan timeToKeepInCache) + { + CacheItem cacheItem = CacheManager.GetCacheItem(ToKey(id)); + if (cacheItem != null) + { + return cacheItem.Value; + } + + T obj = fetchDelegate(); + Set(id, obj, timeToKeepInCache); + return CacheManager.Get(ToKey(id)); + } + + public async Task GetOrSetAsync(string id, Func> fetchDelegate) => CacheManager.GetOrAdd(ToKey(id), await fetchDelegate.Invoke()); + + public async Task GetOrSetAsync(string id, Func> fetchDelegate, TimeSpan timeToKeepInCache) + { + CacheItem cacheItem = CacheManager.GetCacheItem(ToKey(id)); + if (cacheItem != null) + { + return cacheItem.Value; + } + + await SetAsync(id, fetchDelegate, timeToKeepInCache); + return CacheManager.Get(ToKey(id)); + } + + public IReadOnlyList GetValues(IEnumerable keys) => GetValues(keys, Prefix); + + public IReadOnlyList GetValues(IEnumerable keys, string prefix) + { + return keys.Select(key => CacheManager.Get(ToKey(key))).Where(result => !result.Equals(default(T))).ToList(); + } + + private static string ToKey(string prefix, string id) => $"{prefix}:{id}"; + private static string ToKey(string id) => ToKey(Prefix, id); + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Caching/InMemoryUuidCacheRepository.cs b/srcs/PhoenixLib.Caching/InMemoryUuidCacheRepository.cs new file mode 100644 index 0000000..3340fb5 --- /dev/null +++ b/srcs/PhoenixLib.Caching/InMemoryUuidCacheRepository.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using CacheManager.Core; + +namespace PhoenixLib.Caching +{ + public class InMemoryUuidCacheRepository : IUuidKeyCachedRepository + { + private static readonly string Prefix = "repo:" + typeof(T).Name.ToLower(); + + private static readonly ICacheManager CacheManager = CacheFactory.Build(Prefix, + settings => { settings.WithSystemRuntimeCacheHandle(Prefix); }); + + public void Set(Guid id, T value) + { + Set(id, value, Prefix); + } + + public void Set(Guid id, T value, TimeSpan timeToKeepInCache) + { + Set(id, value, Prefix, timeToKeepInCache); + } + + public void Set(Guid id, T value, string prefix) + { + CacheManager.Put(ToKey(prefix, id), value); + } + + public void Set(Guid id, T value, string prefix, TimeSpan timeToKeepInCache) + { + CacheManager.Put(new CacheItem(ToKey(prefix, id), value, ExpirationMode.Sliding, timeToKeepInCache)); + } + + public async Task SetAsync(Guid id, Func> fetchDelegate) => Set(id, await fetchDelegate.Invoke(), Prefix); + + public async Task SetAsync(Guid id, Func> fetchDelegate, TimeSpan timeToKeepInCache) => + CacheManager.Put(new CacheItem(ToKey(id), await fetchDelegate.Invoke(), ExpirationMode.Sliding, timeToKeepInCache)); + + public void Remove(Guid id) + { + CacheManager.Remove(ToKey(id)); + } + + public T Get(Guid id) => Get(id, Prefix); + + public T Get(Guid id, string prefix) => CacheManager.Get(ToKey(prefix, id)); + + public T GetOrSet(Guid id, Func fetchDelegate) => CacheManager.GetOrAdd(ToKey(id), fetchDelegate()); + + public T GetOrSet(Guid id, Func fetchDelegate, TimeSpan timeToKeepInCache) + { + CacheItem cacheItem = CacheManager.GetCacheItem(ToKey(id)); + if (cacheItem != null) + { + return cacheItem.Value; + } + + Set(id, fetchDelegate(), timeToKeepInCache); + return CacheManager.Get(ToKey(id)); + } + + public async Task GetOrSetAsync(Guid id, Func> fetchDelegate) => CacheManager.GetOrAdd(ToKey(id), await fetchDelegate.Invoke()); + + public async Task GetOrSetAsync(Guid id, Func> fetchDelegate, TimeSpan timeToKeepInCache) + { + CacheItem cacheItem = CacheManager.GetCacheItem(ToKey(id)); + if (cacheItem != null) + { + return cacheItem.Value; + } + + await SetAsync(id, fetchDelegate, timeToKeepInCache); + return CacheManager.Get(ToKey(id)); + } + + public IReadOnlyList GetValues(IEnumerable keys) => GetValues(keys, Prefix); + + public IReadOnlyList GetValues(IEnumerable keys, string prefix) + { + return keys.Select(key => CacheManager.Get(ToKey(prefix, key))).Where(result => !result.Equals(default(T))) + .ToList(); + } + + private static string ToKey(Guid id) => ToKey(Prefix, id); + + private static string ToKey(string prefix, Guid id) => $"{prefix}:{id.ToString()}"; + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Caching/MemoryCacheHandle.cs b/srcs/PhoenixLib.Caching/MemoryCacheHandle.cs new file mode 100644 index 0000000..961c209 --- /dev/null +++ b/srcs/PhoenixLib.Caching/MemoryCacheHandle.cs @@ -0,0 +1,399 @@ +using System; +using System.Collections.Specialized; +using System.Globalization; +using System.Runtime.Caching; +using CacheManager.Core; +using CacheManager.Core.Internal; +using CacheManager.Core.Logging; +using CacheManager.Core.Utility; + +namespace PhoenixLib.Caching +{ + /// + /// Simple implementation for the . + /// + /// The type of the cache value. + /// + /// Although the MemoryCache doesn't support regions nor a RemoveAll/Clear method, we will + /// implement it via cache dependencies. + /// + public class MemoryCacheHandle : BaseCacheHandle + { + private const string DefaultName = "default"; + + // can be default or any other name + private readonly string _cacheName = string.Empty; + + private volatile MemoryCache _cache; + private string _instanceKey; + private int _instanceKeyLength; + + /// + /// Initializes a new instance of the class. + /// + /// The manager configuration. + /// The cache handle configuration. + /// The logger factory. + public MemoryCacheHandle(ICacheManagerConfiguration managerConfiguration, CacheHandleConfiguration configuration, ILoggerFactory loggerFactory) + : base(managerConfiguration, configuration) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(loggerFactory, nameof(loggerFactory)); + + Logger = loggerFactory.CreateLogger(this); + _cacheName = configuration.Name; + + _cache = _cacheName.ToUpper(CultureInfo.InvariantCulture).Equals(DefaultName.ToUpper(CultureInfo.InvariantCulture)) + ? MemoryCache.Default + : new MemoryCache(_cacheName); + + _instanceKey = Guid.NewGuid().ToString(); + _instanceKeyLength = _instanceKey.Length; + CreateInstanceToken(); + } + + /// + /// Gets the cache settings. + /// + /// The cache settings. + public NameValueCollection CacheSettings => GetSettings(_cache); + + /// + /// Gets the number of items the cache handle currently maintains. + /// + /// The count. + public override int Count => (int)_cache.GetCount(); + + /// + protected override ILogger Logger { get; } + + /// + /// Clears this cache, removing all items in the base cache and all regions. + /// + public override void Clear() + { + _cache.Remove(_instanceKey); + CreateInstanceToken(); + } + + /// + /// Clears the cache region, removing all items from the specified only. + /// + /// The cache region. + public override void ClearRegion(string region) => + _cache.Remove(GetRegionTokenKey(region)); + + /// + public override bool Exists(string key) => _cache.Contains(GetItemKey(key)); + + /// + public override bool Exists(string key, string region) + { + Guard.NotNullOrWhiteSpace(region, nameof(region)); + string fullKey = GetItemKey(key, region); + return _cache.Contains(fullKey); + } + + /// + /// Adds a value to the cache. + /// + /// The CacheItem to be added to the cache. + /// + /// true if the key was not already added to the cache, false otherwise. + /// + protected override bool AddInternalPrepared(CacheItem item) + { + string key = GetItemKey(item); + + if (_cache.Contains(key)) + { + return false; + } + + CacheItemPolicy policy = GetPolicy(item); + return _cache.Add(key, item, policy); + } + + /// + /// Gets a CacheItem for the specified key. + /// + /// The key being used to identify the item within the cache. + /// The CacheItem. + protected override CacheItem GetCacheItemInternal(string key) => GetCacheItemInternal(key, null); + + /// + /// Gets a CacheItem for the specified key. + /// + /// The key being used to identify the item within the cache. + /// The cache region. + /// The CacheItem. + protected override CacheItem GetCacheItemInternal(string key, string region) + { + string fullKey = GetItemKey(key, region); + + if (!(_cache.Get(fullKey) is CacheItem item)) + { + return null; + } + + // maybe the item is already expired because MemoryCache implements a default interval + // of 20 seconds! to check for expired items on each store, we do it on access to also + // reflect smaller time frames especially for sliding expiration... + // cache.Get eventually triggers eviction callback, but just in case... + if (item.IsExpired) + { + RemoveInternal(item.Key, item.Region); + TriggerCacheSpecificRemove(item.Key, item.Region, CacheItemRemovedReason.Expired, item.Value); + return null; + } + + if (item.ExpirationMode == ExpirationMode.Sliding) + { + // because we don't use UpdateCallback because of some multithreading issues lets + // try to simply reset the item by setting it again. + // item = this.GetItemExpiration(item); // done via base cache handle + _cache.Set(fullKey, item, GetPolicy(item)); + } + + return item; + } + + /// + /// Puts the into the cache. If the item exists it will get updated + /// with the new value. If the item doesn't exist, the item will be added to the cache. + /// + /// The CacheItem to be added to the cache. + protected override void PutInternalPrepared(CacheItem item) + { + string key = GetItemKey(item); + CacheItemPolicy policy = GetPolicy(item); + _cache.Set(key, item, policy); + } + + /// + /// Removes a value from the cache for the specified key. + /// + /// The key being used to identify the item within the cache. + /// + /// true if the key was found and removed from the cache, false otherwise. + /// + protected override bool RemoveInternal(string key) => RemoveInternal(key, null); + + /// + /// Removes a value from the cache for the specified key. + /// + /// The key being used to identify the item within the cache. + /// The cache region. + /// + /// true if the key was found and removed from the cache, false otherwise. + /// + protected override bool RemoveInternal(string key, string region) + { + string fullKey = GetItemKey(key, region); + object obj = _cache.Remove(fullKey); + + return obj != null; + } + + private static NameValueCollection GetSettings(MemoryCache instance) + { + var cacheCfg = new NameValueCollection + { + { "CacheMemoryLimitMegabytes", (instance.CacheMemoryLimit / 1024 / 1024).ToString(CultureInfo.InvariantCulture) }, + { "PhysicalMemoryLimitPercentage", instance.PhysicalMemoryLimit.ToString(CultureInfo.InvariantCulture) }, + { "PollingInterval", instance.PollingInterval.ToString() } + }; + + return cacheCfg; + } + + private void CreateInstanceToken() + { + // don't add a new key while we are disposing our instance + if (Disposing) + { + return; + } + + var instanceItem = new CacheItem(_instanceKey, _instanceKey); + var policy = new CacheItemPolicy + { + Priority = CacheItemPriority.NotRemovable, + RemovedCallback = InstanceTokenRemoved, + AbsoluteExpiration = ObjectCache.InfiniteAbsoluteExpiration, + SlidingExpiration = ObjectCache.NoSlidingExpiration + }; + + _cache.Add(instanceItem.Key, instanceItem, policy); + } + + private void CreateRegionToken(string region) + { + string key = GetRegionTokenKey(region); + + // add region token with dependency on our instance token, so that all regions get + // removed whenever the instance gets cleared. + var policy = new CacheItemPolicy + { + Priority = CacheItemPriority.NotRemovable, + AbsoluteExpiration = ObjectCache.InfiniteAbsoluteExpiration, + SlidingExpiration = ObjectCache.NoSlidingExpiration, + ChangeMonitors = { _cache.CreateCacheEntryChangeMonitor(new[] { _instanceKey }) } + }; + _cache.Add(key, region, policy); + } + + private CacheItemPolicy GetPolicy(CacheItem item) + { + string[] monitorKeys = { _instanceKey }; + + if (!string.IsNullOrWhiteSpace(item.Region)) + { + // this should be the only place to create the region token if it doesn't exist it + // might got removed by clearRegion but next time put or add gets called, the region + // should be re added... + string regionToken = GetRegionTokenKey(item.Region); + if (!_cache.Contains(regionToken)) + { + CreateRegionToken(item.Region); + } + + monitorKeys = new[] { _instanceKey, regionToken }; + } + + var policy = new CacheItemPolicy + { + Priority = CacheItemPriority.Default, + ChangeMonitors = { _cache.CreateCacheEntryChangeMonitor(monitorKeys) }, + AbsoluteExpiration = ObjectCache.InfiniteAbsoluteExpiration, + SlidingExpiration = ObjectCache.NoSlidingExpiration + }; + + switch (item.ExpirationMode) + { + case ExpirationMode.Absolute: + policy.AbsoluteExpiration = new DateTimeOffset(DateTime.UtcNow.Add(item.ExpirationTimeout)); + policy.RemovedCallback = ItemRemoved; + break; + case ExpirationMode.Sliding: + policy.SlidingExpiration = item.ExpirationTimeout; + policy.RemovedCallback = ItemRemoved; + + //// for some reason, we'll get issues with multithreading if we set this... + //// see http://stackoverflow.com/questions/21680429/why-does-memorycache-throw-nullreferenceexception + ////policy.UpdateCallback = new CacheEntryUpdateCallback(ItemUpdated); // must be set, otherwise sliding doesn't work at all. + break; + } + + item.LastAccessedUtc = DateTime.UtcNow; + + return policy; + } + + private string GetItemKey(CacheItem item) => GetItemKey(item?.Key, item?.Region); + + private string GetItemKey(string key, string region = null) + { + Guard.NotNullOrWhiteSpace(key, nameof(key)); + + if (string.IsNullOrWhiteSpace(region)) + { + return _instanceKey + ":" + key; + } + + // key without region + // :key + // key with region + // @: + // @6region:key + return string.Concat(_instanceKey, "@", region.Length, "@", region, ":", key); + } + + private string GetRegionTokenKey(string region) + { + string key = string.Concat(_instanceKey, "_", region); + return key; + } + + private void InstanceTokenRemoved(CacheEntryRemovedArguments arguments) + { + _instanceKey = Guid.NewGuid().ToString(); + _instanceKeyLength = _instanceKey.Length; + } + + private void ItemRemoved(CacheEntryRemovedArguments arguments) + { + string fullKey = arguments.CacheItem.Key; + if (string.IsNullOrWhiteSpace(fullKey)) + { + return; + } + + // ignore manual removes, stats will be updated already + if (arguments.RemovedReason == CacheEntryRemovedReason.Removed) + { + return; + } + + ParseKeyParts(_instanceKeyLength, fullKey, out bool isToken, out bool hasRegion, out string region, out string key); + + if (isToken) + { + return; + } + + if (hasRegion) + { + Stats.OnRemove(region); + } + else + { + Stats.OnRemove(); + } + + var item = arguments.CacheItem.Value as CacheItem; + object originalValue = null; + if (item != null) + { + originalValue = item.Value; + } + + switch (arguments.RemovedReason) + { + // trigger cachemanager's remove on evicted and expired items + case CacheEntryRemovedReason.Evicted: + case CacheEntryRemovedReason.CacheSpecificEviction: + TriggerCacheSpecificRemove(key, region, CacheItemRemovedReason.Evicted, originalValue); + break; + case CacheEntryRemovedReason.Expired: + TriggerCacheSpecificRemove(key, region, CacheItemRemovedReason.Expired, originalValue); + break; + } + } + + private static void ParseKeyParts(int instanceKeyLength, string fullKey, out bool isToken, out bool hasRegion, out string region, out string key) + { + string relevantKey = fullKey.Substring(instanceKeyLength); + isToken = relevantKey[0] == '_'; + hasRegion = false; + region = null; + key = null; + + if (isToken) + { + return; + } + + hasRegion = relevantKey[0] == '@'; + int regionLenEnd = hasRegion ? relevantKey.IndexOf('@', 1) : -1; + + int regionLen; + regionLen = hasRegion && regionLenEnd > 0 ? int.TryParse(relevantKey.Substring(1, regionLenEnd - 1), out regionLen) ? regionLen : 0 : 0; + hasRegion = hasRegion && regionLen > 0; + + string restKey = hasRegion ? relevantKey.Substring(regionLenEnd + 1) : relevantKey; + region = hasRegion ? restKey.Substring(0, regionLen) : null; + key = restKey.Substring(regionLen + 1); + } + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Caching/PhoenixLib.Caching.csproj b/srcs/PhoenixLib.Caching/PhoenixLib.Caching.csproj new file mode 100644 index 0000000..0dbddb1 --- /dev/null +++ b/srcs/PhoenixLib.Caching/PhoenixLib.Caching.csproj @@ -0,0 +1,12 @@ + + + + net5.0 + + + + + + + + diff --git a/srcs/PhoenixLib.Caching/RuntimeCachingBuilderExtensions.cs b/srcs/PhoenixLib.Caching/RuntimeCachingBuilderExtensions.cs new file mode 100644 index 0000000..f9a1a72 --- /dev/null +++ b/srcs/PhoenixLib.Caching/RuntimeCachingBuilderExtensions.cs @@ -0,0 +1,48 @@ +using System; +using CacheManager.Core; + +namespace PhoenixLib.Caching +{ + /// + /// Extensions for the configuration builder specific to System.Runtime.Caching cache handle. + /// + public static class RuntimeCachingBuilderExtensions + { + private const string DEFAULT_NAME = "default"; + + /// + /// Adds a using a . + /// The name of the cache instance will be 'default'. + /// + /// The builder part. + /// + /// Set this to true if this cache handle should be the source of the backplane. + /// This setting will be ignored if no backplane is configured. + /// + /// + /// The builder part. + /// + /// The builder part. + public static ConfigurationBuilderCacheHandlePart WithSystemRuntimeCacheHandle(this ConfigurationBuilderCachePart part, bool isBackplaneSource = false) + => part?.WithHandle(typeof(MemoryCacheHandle<>), DEFAULT_NAME, isBackplaneSource); + + /// + /// Adds a using a + /// instance with the given . + /// The named cache instance can be configured via app/web.config system.runtime.caching section. + /// + /// The builder part. + /// The name to be used for the cache instance. + /// + /// Set this to true if this cache handle should be the source of the backplane. + /// This setting will be ignored if no backplane is configured. + /// + /// + /// The builder part. + /// + /// If part is null. + /// Thrown if is null. + public static ConfigurationBuilderCacheHandlePart WithSystemRuntimeCacheHandle(this ConfigurationBuilderCachePart part, string instanceName, bool isBackplaneSource = false) + => part?.WithHandle(typeof(MemoryCacheHandle<>), instanceName, isBackplaneSource); + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Configuration/ConfigurationHelperConfig.cs b/srcs/PhoenixLib.Configuration/ConfigurationHelperConfig.cs new file mode 100644 index 0000000..38ac57f --- /dev/null +++ b/srcs/PhoenixLib.Configuration/ConfigurationHelperConfig.cs @@ -0,0 +1,7 @@ +namespace PhoenixLib.Configuration +{ + public class ConfigurationHelperConfig + { + public string ConfigurationDirectory { get; set; } = "config/"; + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Configuration/ConfigurationPathProvider.cs b/srcs/PhoenixLib.Configuration/ConfigurationPathProvider.cs new file mode 100644 index 0000000..6dc343b --- /dev/null +++ b/srcs/PhoenixLib.Configuration/ConfigurationPathProvider.cs @@ -0,0 +1,11 @@ +namespace PhoenixLib.Configuration +{ + public class ConfigurationPathProvider : IConfigurationPathProvider + { + private readonly string _path; + + public ConfigurationPathProvider(string path) => _path = path; + + public string GetConfigurationPath(string configBlobName) => _path + configBlobName; + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Configuration/DependencyInjectionExtensions.cs b/srcs/PhoenixLib.Configuration/DependencyInjectionExtensions.cs new file mode 100644 index 0000000..e32b4da --- /dev/null +++ b/srcs/PhoenixLib.Configuration/DependencyInjectionExtensions.cs @@ -0,0 +1,164 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using PhoenixLib.Logging; +using YamlDotNet.Core; +using YamlDotNet.Core.Events; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; + +namespace PhoenixLib.Configuration +{ + public class YamlNullableEnumTypeConverter : IYamlTypeConverter + { + public bool Accepts(Type type) => Nullable.GetUnderlyingType(type)?.IsEnum ?? false; + + public object ReadYaml(IParser parser, Type type) + { + type = Nullable.GetUnderlyingType(type) ?? throw new ArgumentException("Expected nullable enum type for ReadYaml"); + Scalar scalar = parser.Consume(); + + if (string.IsNullOrWhiteSpace(scalar.Value)) + { + return null; + } + + try + { + return Enum.Parse(type, scalar.Value); + } + catch (Exception ex) + { + throw new Exception($"Invalid value: \"{scalar.Value}\" for {type.Name}", ex); + } + } + + public void WriteYaml(IEmitter emitter, object value, Type type) + { + type = Nullable.GetUnderlyingType(type) ?? throw new ArgumentException("Expected nullable enum type for WriteYaml"); + + if (value == null) + { + return; + } + + string? toWrite = Enum.GetName(type, value) ?? throw new InvalidOperationException($"Invalid value {value} for enum: {type}"); + emitter.Emit(new Scalar(null, null, toWrite, ScalarStyle.Any, true, false)); + } + } + + public static class DependencyInjectionExtensions + { + public static void AddYamlConfigurationHelper(this IServiceCollection services, + Action configurationAction = null) + { + var config = new ConfigurationHelperConfig(); + + configurationAction?.Invoke(config); + + services.AddSingleton(UnderscoredNamingConvention.Instance); + services.AddSingleton(s => + new SerializerBuilder().WithNamingConvention(s.GetRequiredService()).WithTypeConverter(new YamlNullableEnumTypeConverter()).Build()); + services.AddSingleton(s => + new DeserializerBuilder().WithNamingConvention(s.GetRequiredService()).WithTypeConverter(new YamlNullableEnumTypeConverter()).Build()); + services.AddTransient(); + services.AddSingleton(new ConfigurationPathProvider(config.ConfigurationDirectory)); + } + + /// + /// The file will be named with underscore case with .yaml as its file extension + /// + /// + /// + /// + public static void AddConfigurationsFromDirectory(this IServiceCollection services, string path) + where T : class, new() + { + services.AddSingleton>(s => + s.GetRequiredService() + .GetConfigurations(s.GetService().GetConfigurationPath(path))); + } + + /// + /// The file will be named with underscore case with .yaml as its file extension + /// + /// + /// + /// + public static void AddFileConfiguration(this IServiceCollection services, string configName) + where T : class, new() + { + services.AddSingleton(s => + s.GetRequiredService().Load( + s.GetService().GetConfigurationPath(configName.ToUnderscoreCase() + ".yaml"), true)); + } + + /// + /// The file will be named with underscore case with .yaml as its file extension + /// + /// + /// + /// + public static void AddMultipleConfigurationOneFile(this IServiceCollection services, string configName) + where T : class, new() + { + services.AddSingleton>(s => + s.GetRequiredService().Load>( + s.GetService().GetConfigurationPath(configName.ToUnderscoreCase() + ".yaml"), true)); + } + + + /// + /// The file will be named with underscore case with .yaml as its file extension + /// + /// + /// + public static void AddFileConfiguration(this IServiceCollection services) where T : class, new() + { + services.AddSingleton(s => + s.GetRequiredService().Load( + s.GetService().GetConfigurationPath(typeof(T).Name.ToUnderscoreCase() + ".yaml"), + true)); + } + + + /// + /// The file will be named with underscore case with .yaml as its file extension + /// + /// + /// Default value in case the file does not exist + /// + public static void AddFileConfiguration(this IServiceCollection services, T defaultConfig) + where T : class, new() + { + services.AddSingleton(s => + s.GetRequiredService().Load( + s.GetService().GetConfigurationPath(typeof(T).Name.ToUnderscoreCase() + ".yaml"), + defaultConfig)); + } + + private static List GetConfigurations(this IConfigurationHelper helper, string path) + where T : class, new() + { + var configs = new List(); + if (!Directory.Exists(path)) + { + Directory.CreateDirectory(path); + return configs; + } + + foreach (string file in Directory.GetFiles(path, "*.yaml", SearchOption.AllDirectories).Concat(Directory.GetFiles(path, "*.yml", SearchOption.AllDirectories))) + { + var fileInfo = new FileInfo(file); + T config = helper.Load(file); + configs.Add(config); + + Log.Info($"[CONFIGURATION_HELPER] Loading configuration from {fileInfo.Name} as {typeof(T).Name}"); + } + + return configs; + } + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Configuration/IConfigurationHelper.cs b/srcs/PhoenixLib.Configuration/IConfigurationHelper.cs new file mode 100644 index 0000000..926d175 --- /dev/null +++ b/srcs/PhoenixLib.Configuration/IConfigurationHelper.cs @@ -0,0 +1,13 @@ +namespace PhoenixLib.Configuration +{ + /// + /// A file system based configuration helper + /// + public interface IConfigurationHelper + { + T Load(string path) where T : class, new(); + T Load(string path, bool createIfNotExists) where T : class, new(); + T Load(string path, T defaultValue) where T : class, new(); + void Save(string path, T value); + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Configuration/IConfigurationPathProvider.cs b/srcs/PhoenixLib.Configuration/IConfigurationPathProvider.cs new file mode 100644 index 0000000..b4054f0 --- /dev/null +++ b/srcs/PhoenixLib.Configuration/IConfigurationPathProvider.cs @@ -0,0 +1,7 @@ +namespace PhoenixLib.Configuration +{ + public interface IConfigurationPathProvider + { + string GetConfigurationPath(string configBlobName); + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Configuration/PhoenixLib.Configuration.csproj b/srcs/PhoenixLib.Configuration/PhoenixLib.Configuration.csproj new file mode 100644 index 0000000..bdeef80 --- /dev/null +++ b/srcs/PhoenixLib.Configuration/PhoenixLib.Configuration.csproj @@ -0,0 +1,18 @@ + + + + net5.0 + PhoenixLib.Configuration + + + + + + + + + + + + + diff --git a/srcs/PhoenixLib.Configuration/StringExtensions.cs b/srcs/PhoenixLib.Configuration/StringExtensions.cs new file mode 100644 index 0000000..a522525 --- /dev/null +++ b/srcs/PhoenixLib.Configuration/StringExtensions.cs @@ -0,0 +1,12 @@ +using System.Linq; + +namespace PhoenixLib.Configuration +{ + public static class StringExtensions + { + public static string ToUnderscoreCase(this string str) + { + return string.Concat(str.Select((x, i) => i > 0 && char.IsUpper(x) ? "_" + x : x.ToString())).ToLower(); + } + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Configuration/YamlConfigurationHelper.cs b/srcs/PhoenixLib.Configuration/YamlConfigurationHelper.cs new file mode 100644 index 0000000..d1206c1 --- /dev/null +++ b/srcs/PhoenixLib.Configuration/YamlConfigurationHelper.cs @@ -0,0 +1,84 @@ +using System.Diagnostics.CodeAnalysis; +using System.IO; +using PhoenixLib.Logging; +using YamlDotNet.Serialization; + +namespace PhoenixLib.Configuration +{ + /// + /// Yaml + /// + public class YamlConfigurationHelper : IConfigurationHelper + { + private readonly IDeserializer _deserializer; + private readonly ISerializer _serializer; + + public YamlConfigurationHelper(IDeserializer deserializer, ISerializer serializer) + { + _deserializer = deserializer; + _serializer = serializer; + } + + public T Load([NotNull] string path) where T : class, new() => Load(path, false); + + public T Load([NotNull] string path, bool createIfNotExists) where T : class, new() + { + if (createIfNotExists) + { + return Load(path, new T()); + } + + if (!File.Exists(path)) + { + throw new IOException(path); + } + + Log.Debug($"[CONFIGURATION_HELPER] Loading configuration {typeof(T).Name} from {path}..."); + + using FileStream stream = File.OpenRead(path); + + Log.Debug($"[CONFIGURATION_HELPER] Loading configuration {typeof(T).Name} from {path}..."); + + using var streamReader = new StreamReader(stream); + + T deserialized = _deserializer.Deserialize(streamReader); + Log.Debug($"[CONFIGURATION_HELPER] Configuration {typeof(T).Name} from {path} loaded !"); + + return deserialized; + } + + public T Load([NotNull] string path, T defaultValue) where T : class, new() + { + if (!File.Exists(path)) + { + Log.Debug($"[CONFIGURATION_HELPER] Configuration at {path}, does not exist, creating a new one..."); + Save(path, defaultValue); + } + + using FileStream stream = File.OpenRead(path); + + Log.Debug($"[CONFIGURATION_HELPER] Loading configuration {typeof(T).Name} from {path}..."); + + using var streamReader = new StreamReader(stream); + + T deserialized = _deserializer.Deserialize(streamReader); + + Log.Debug($"[CONFIGURATION_HELPER] Configuration {typeof(T).Name} from {path} loaded !"); + return deserialized; + } + + public void Save([NotNull] string path, T value) + { + if (!Directory.Exists(path)) + { + Log.Debug($"[CONFIGURATION_HELPER] Creating directory {path}..."); + Directory.CreateDirectory(Path.GetDirectoryName(path)); + } + + Log.Debug($"[CONFIGURATION_HELPER] Saving configuration {typeof(T).Name} into {path}..."); + string valueSerialized = _serializer.Serialize(value); + File.WriteAllText(path, valueSerialized); + Log.Debug($"[CONFIGURATION_HELPER] Configuration saved into {path} !"); + } + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.DAL.Abstractions/BaseUuidDto.cs b/srcs/PhoenixLib.DAL.Abstractions/BaseUuidDto.cs new file mode 100644 index 0000000..ce714de --- /dev/null +++ b/srcs/PhoenixLib.DAL.Abstractions/BaseUuidDto.cs @@ -0,0 +1,13 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace PhoenixLib.DAL +{ + public class BaseUuidDto : IUuidDto + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public Guid Id { get; set; } = Guid.NewGuid(); + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.DAL.Abstractions/IDto.cs b/srcs/PhoenixLib.DAL.Abstractions/IDto.cs new file mode 100644 index 0000000..6e2939f --- /dev/null +++ b/srcs/PhoenixLib.DAL.Abstractions/IDto.cs @@ -0,0 +1,13 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace PhoenixLib.DAL +{ + /// + /// A simple DTO + /// + public interface IDto + { + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.DAL.Abstractions/IGenericAsyncIntRepository.cs b/srcs/PhoenixLib.DAL.Abstractions/IGenericAsyncIntRepository.cs new file mode 100644 index 0000000..b214544 --- /dev/null +++ b/srcs/PhoenixLib.DAL.Abstractions/IGenericAsyncIntRepository.cs @@ -0,0 +1,12 @@ +namespace PhoenixLib.DAL +{ + /// + /// IGenericAsyncLongRepository permits to manage specialize an IGenericAsyncRepository for ILongDto objects (which are + /// objects with an long key Id) + /// + /// + public interface IGenericAsyncIntRepository : IGenericAsyncRepository + where TDto : class, IIntDto + { + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.DAL.Abstractions/IGenericAsyncLongRepository.cs b/srcs/PhoenixLib.DAL.Abstractions/IGenericAsyncLongRepository.cs new file mode 100644 index 0000000..2ab5715 --- /dev/null +++ b/srcs/PhoenixLib.DAL.Abstractions/IGenericAsyncLongRepository.cs @@ -0,0 +1,16 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace PhoenixLib.DAL +{ + /// + /// IGenericAsyncLongRepository permits to manage specialize an IGenericAsyncRepository for ILongDto objects (which are + /// objects with an long key Id) + /// + /// + public interface IGenericAsyncLongRepository : IGenericAsyncRepository + where TDto : class, ILongDto + { + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.DAL.Abstractions/IGenericAsyncRepository.cs b/srcs/PhoenixLib.DAL.Abstractions/IGenericAsyncRepository.cs new file mode 100644 index 0000000..ffa2474 --- /dev/null +++ b/srcs/PhoenixLib.DAL.Abstractions/IGenericAsyncRepository.cs @@ -0,0 +1,69 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace PhoenixLib.DAL +{ + /// + /// IGenericAsyncRepository permits to manage the data storage through async operations + /// + /// + /// + public interface IGenericAsyncRepository where TObject : class + { + /// + /// Asynchronously returns all objects that are stored in data storage + /// + /// + Task> GetAllAsync(); + + /// + /// Returns object by its id in data storage + /// + /// + /// + Task GetByIdAsync(TObjectId id); + + /// + /// Returns all objects with the given ids + /// + /// + /// + Task> GetByIdsAsync(IEnumerable ids); + + + /// + /// Asynchronously inserts or updates object parameter into data storage + /// Parameter's id will be set in this method + /// + /// + /// + Task SaveAsync(TObject obj); + + + /// + /// Asynchronously inserts or updates objects given in parameter into data storage + /// Parameter's id will be set in this method + /// + /// + /// + Task> SaveAsync(IReadOnlyList objs); + + /// + /// Asynchronously delete the object from the data storage with the given id + /// + /// + /// + Task DeleteByIdAsync(TObjectId id); + + /// + /// Asynchronously delete all the objects from the data storage with the given id + /// + /// + /// + Task DeleteByIdsAsync(IEnumerable ids); + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.DAL.Abstractions/IGenericAsyncUuidRepository.cs b/srcs/PhoenixLib.DAL.Abstractions/IGenericAsyncUuidRepository.cs new file mode 100644 index 0000000..81072e6 --- /dev/null +++ b/srcs/PhoenixLib.DAL.Abstractions/IGenericAsyncUuidRepository.cs @@ -0,0 +1,12 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; + +namespace PhoenixLib.DAL +{ + public interface IGenericAsyncUuidRepository : IGenericAsyncRepository where T : class, IUuidDto + { + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.DAL.Abstractions/IIntDto.cs b/srcs/PhoenixLib.DAL.Abstractions/IIntDto.cs new file mode 100644 index 0000000..faddca8 --- /dev/null +++ b/srcs/PhoenixLib.DAL.Abstractions/IIntDto.cs @@ -0,0 +1,9 @@ +namespace PhoenixLib.DAL +{ + /// + /// + public interface IIntDto : IDto + { + int Id { get; set; } + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.DAL.Abstractions/IKeyValueAsyncStorage.cs b/srcs/PhoenixLib.DAL.Abstractions/IKeyValueAsyncStorage.cs new file mode 100644 index 0000000..230bfed --- /dev/null +++ b/srcs/PhoenixLib.DAL.Abstractions/IKeyValueAsyncStorage.cs @@ -0,0 +1,70 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace PhoenixLib.DAL +{ + public interface IKeyValueAsyncStorage + where TKey : notnull + { + /// + /// Gets all the objects stored within the cache + /// + /// + Task> GetAllAsync(); + + /// + /// Clears cache + /// + /// + Task ClearAllAsync(); + + + /// + /// Gets all the objects that are contained in the given id enumerable + /// + /// + /// + Task> GetByIdsAsync(IEnumerable ids); + + /// + /// Gets the object with the given key from the cache + /// + /// + /// + Task GetByIdAsync(TKey id); + + /// + /// Registers the object that are contained + /// + /// + /// + /// + /// + Task RegisterAsync(TKey id, TObject obj, TimeSpan? lifeTime = null); + + /// + /// Asynchronously registers all the objects given as parameter assuming that these objects contains a key + /// + /// + /// + Task RegisterAsync(IEnumerable<(TKey, TObject)> objs, TimeSpan? lifeTime = null); + + /// + /// Asynchronously removes the object and returns it + /// + /// + Task RemoveAsync(TKey id); + + /// + /// Asynchronously removes the objects with the given keys + /// + /// + /// + Task> RemoveAsync(IEnumerable ids); + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.DAL.Abstractions/ILongDto.cs b/srcs/PhoenixLib.DAL.Abstractions/ILongDto.cs new file mode 100644 index 0000000..26070b4 --- /dev/null +++ b/srcs/PhoenixLib.DAL.Abstractions/ILongDto.cs @@ -0,0 +1,13 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace PhoenixLib.DAL +{ + /// + /// + public interface ILongDto : IDto + { + long Id { get; set; } + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.DAL.Abstractions/IMapper.cs b/srcs/PhoenixLib.DAL.Abstractions/IMapper.cs new file mode 100644 index 0000000..f982cde --- /dev/null +++ b/srcs/PhoenixLib.DAL.Abstractions/IMapper.cs @@ -0,0 +1,27 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; + +namespace PhoenixLib.DAL +{ + /// + /// IMapper facilitate mapping between an entity and a dto + /// + /// + /// + public interface IMapper + { + TEntity Map(TDto input); + List Map(List input); + IEnumerable Map(IEnumerable input); + IReadOnlyList Map(IReadOnlyList input); + TDto Map(TEntity input); + List Map(List input); + IEnumerable Map(IEnumerable input); + IReadOnlyList Map(IReadOnlyList input); + void Map(TDto input, TEntity output); + void Map(TEntity input, TDto output); + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.DAL.Abstractions/IUuidDto.cs b/srcs/PhoenixLib.DAL.Abstractions/IUuidDto.cs new file mode 100644 index 0000000..55a339b --- /dev/null +++ b/srcs/PhoenixLib.DAL.Abstractions/IUuidDto.cs @@ -0,0 +1,13 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; + +namespace PhoenixLib.DAL +{ + public interface IUuidDto : IDto + { + Guid Id { get; set; } + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.DAL.Abstractions/PhoenixLib.DAL.Abstractions.csproj b/srcs/PhoenixLib.DAL.Abstractions/PhoenixLib.DAL.Abstractions.csproj new file mode 100644 index 0000000..771832a --- /dev/null +++ b/srcs/PhoenixLib.DAL.Abstractions/PhoenixLib.DAL.Abstractions.csproj @@ -0,0 +1,14 @@ + + + + net5.0 + PhoenixLib.DAL + + + + + + + + + diff --git a/srcs/PhoenixLib.DAL.EFCore.PGSQL/Extensions/DatabaseClearExtensions.cs b/srcs/PhoenixLib.DAL.EFCore.PGSQL/Extensions/DatabaseClearExtensions.cs new file mode 100644 index 0000000..d3af01f --- /dev/null +++ b/srcs/PhoenixLib.DAL.EFCore.PGSQL/Extensions/DatabaseClearExtensions.cs @@ -0,0 +1,23 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.ComponentModel.DataAnnotations.Schema; +using System.Reflection; +using Microsoft.EntityFrameworkCore; + +namespace PhoenixLib.DAL.EFCore.PGSQL.Extensions +{ + public static class DatabaseClearExtensions + { + public static void ResetTable(this DbContext dbContext, DbSet dbSet) where T : class + { + dbSet.DeleteFromQuery(); + dbContext.Database.ExecuteSqlRaw(CleanTable()); + } + + + private static string CleanTable() => + @$"ALTER SEQUENCE ""{((TableAttribute)typeof(T).GetCustomAttribute(typeof(TableAttribute))).Schema}"".""{((TableAttribute)typeof(T).GetCustomAttribute(typeof(TableAttribute))).Name}_Id_seq"" RESTART;"; + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.DAL.EFCore.PGSQL/Extensions/ServiceCollectionExtensions.cs b/srcs/PhoenixLib.DAL.EFCore.PGSQL/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..b58aabd --- /dev/null +++ b/srcs/PhoenixLib.DAL.EFCore.PGSQL/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,91 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace PhoenixLib.DAL.EFCore.PGSQL.Extensions +{ + public static class ServiceCollectionExtensions + { + public static void TryAddStringRepository(this IServiceCollection services) + where TEntity : class, IStringKeyEntity, new() + where TDbContext : DbContext + { + services.TryAddTransient, GenericStringRepository>(); + } + + + public static void TryAddLongRepository(this IServiceCollection services) + where TEntity : class, ILongEntity, new() + where TDto : class, ILongDto, new() + where TDbContext : DbContext + { + services.TryAddLongRepository(); + services.TryAddTransient, GenericMappedLongRepository>(); + } + + public static void TryAddLongRepository(this IServiceCollection services) + where TEntity : class, ILongEntity, new() + where TDbContext : DbContext + { + services.TryAddTransient, GenericLongRepository>(); + } + + public static void TryAddOldMappedLongRepository(this IServiceCollection services) + where TEntity : class, ILongEntity, new() + where TDto : class, ILongDto, new() + where TDbContext : DbContext + { + services.TryAddTransient, GenericOldMappedLongRepository>(); + } + + + public static void TryAddUuidRepository(this IServiceCollection services) + where TEntity : class, IUuidEntity, new() + where TDto : class, IUuidDto, new() + where TDbContext : DbContext + { + services.TryAddUuidRepository(); + services.TryAddTransient, GenericMappedUuidRepository>(); + } + + public static void TryAddUuidRepository(this IServiceCollection services) + where TEntity : class, IUuidEntity, new() + where TDbContext : DbContext + { + services.TryAddTransient, GenericUuidRepository>(); + } + + public static void TryAddOldMappedUuidRepository(this IServiceCollection services) + where TEntity : class, IUuidEntity, new() + where TDto : class, IUuidDto, new() + where TDbContext : DbContext + { + services.TryAddTransient, GenericOldMappedUuidRepository>(); + } + + + public static void TryAddIntRepository(this IServiceCollection services) + where TEntity : class, IIntEntity, new() + where TDto : class, IIntDto, new() + where TDbContext : DbContext + { + services.TryAddIntRepository(); + services.TryAddTransient, GenericMappedIntRepository>(); + } + + public static void TryAddIntRepository(this IServiceCollection services) + where TEntity : class, IIntEntity, new() + where TDbContext : DbContext + { + services.TryAddTransient, GenericIntRepository>(); + } + + public static void TryAddOldMappedIntRepository(this IServiceCollection services) + where TEntity : class, IIntEntity, new() + where TDto : class, IIntDto, new() + where TDbContext : DbContext + { + services.TryAddTransient, GenericOldMappedIntRepository>(); + } + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.DAL.EFCore.PGSQL/GenericIntRepository.cs b/srcs/PhoenixLib.DAL.EFCore.PGSQL/GenericIntRepository.cs new file mode 100644 index 0000000..b0d483b --- /dev/null +++ b/srcs/PhoenixLib.DAL.EFCore.PGSQL/GenericIntRepository.cs @@ -0,0 +1,177 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace PhoenixLib.DAL.EFCore.PGSQL +{ + /// + /// GenericAsyncMappedRepository is an asynchronous Data Access Object + /// + /// + /// + /// + public sealed class GenericIntRepository : IGenericAsyncIntRepository + where TEntity : class, IIntEntity, new() + where TDbContext : DbContext + { + private static readonly string _typeName = typeof(TEntity).Name; + private readonly IDbContextFactory _contextFactory; + + private readonly ILogger> _logger; + + public GenericIntRepository(IDbContextFactory contextFactory, ILogger> logger) + { + _contextFactory = contextFactory; + _logger = logger; + } + + public async Task> GetAllAsync() + { + try + { + await using TDbContext context = _contextFactory.CreateDbContext(); + List tmp = await context.Set().ToListAsync(); + return tmp; + } + catch (Exception e) + { + _logger.LogError(e, $"GetAllAsync {_typeName}"); + throw; + } + } + + + public async Task GetByIdAsync(int id) + { + try + { + await using DbContext context = _contextFactory.CreateDbContext(); + TEntity tmp = await context.Set().FindAsync(id); + return tmp; + } + catch (Exception e) + { + _logger.LogError(e, $"GetByIdAsync {_typeName}"); + throw; + } + } + + public async Task> GetByIdsAsync(IEnumerable ids) + { + try + { + await using DbContext context = _contextFactory.CreateDbContext(); + List entities = await context.Set().Where(s => ids.Contains(s.Id)).ToListAsync(); + return entities; + } + catch (Exception e) + { + _logger.LogError(e, $"GetByIdsAsync {_typeName}"); + throw; + } + } + + public async Task SaveAsync(TEntity obj) + { + try + { + await using DbContext context = _contextFactory.CreateDbContext(); + await context.SingleMergeAsync(obj, operation => + { + operation.InsertKeepIdentity = true; + operation.IsCheckConstraintOnInsertDisabled = false; + }); + return obj; + } + catch (Exception e) + { + _logger.LogError(e, $"SaveAsync {_typeName}"); + throw; + } + } + + public async Task> SaveAsync(IReadOnlyList objs) + { + try + { + await using DbContext context = _contextFactory.CreateDbContext(); + await context.BulkMergeAsync(objs, operation => + { + operation.InsertKeepIdentity = true; + operation.IsCheckConstraintOnInsertDisabled = false; + }); + return objs; + } + catch (Exception e) + { + _logger.LogError(e, $"SaveAsync {_typeName}"); + throw; + } + } + + public async Task DeleteByIdAsync(int id) + { + try + { + await using DbContext context = _contextFactory.CreateDbContext(); + TEntity entity = await context.FindAsync(id); + if (entity == null) + { + return; + } + + context.Set().Remove(entity); + await context.SaveChangesAsync(); + } + catch (Exception e) + { + _logger.LogError(e, $"DeleteByIdAsync {_typeName}"); + throw; + } + } + + public async Task DeleteByIdsAsync(IEnumerable ids) + { + try + { + await using DbContext context = _contextFactory.CreateDbContext(); + foreach (int id in ids) + { + TEntity entity = await context.FindAsync(id); + if (entity == null) + { + continue; + } + + context.Set().Remove(entity); + } + + await context.SaveChangesAsync(); + } + catch (Exception e) + { + _logger.LogError(e, $"DeleteByIdsAsync {_typeName}"); + throw; + } + } + + public async Task> GetMatchingAsync(Expression> predicate) + { + try + { + await using DbContext context = _contextFactory.CreateDbContext(); + List entities = await context.Set().Where(predicate).ToListAsync(); + return entities; + } + catch (Exception e) + { + _logger.LogError(e, $"GetMatchingAsync {_typeName}"); + throw; + } + } + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.DAL.EFCore.PGSQL/GenericLongRepository.cs b/srcs/PhoenixLib.DAL.EFCore.PGSQL/GenericLongRepository.cs new file mode 100644 index 0000000..cfaf3c5 --- /dev/null +++ b/srcs/PhoenixLib.DAL.EFCore.PGSQL/GenericLongRepository.cs @@ -0,0 +1,167 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace PhoenixLib.DAL.EFCore.PGSQL +{ + /// + /// GenericAsyncMappedRepository is an asynchronous Data Access Object + /// + /// + /// + public sealed class GenericLongRepository : IGenericAsyncLongRepository + where TEntity : class, ILongEntity, new() + where TDbContext : DbContext + { + private static readonly string _typeName = typeof(TEntity).Name; + private readonly IDbContextFactory _contextFactory; + + private readonly ILogger> _logger; + + public GenericLongRepository(IDbContextFactory contextFactory, ILogger> logger) + { + _contextFactory = contextFactory; + _logger = logger; + } + + public async Task> GetAllAsync() + { + try + { + await using TDbContext context = _contextFactory.CreateDbContext(); + List tmp = await context.Set().ToListAsync(); + return tmp; + } + catch (Exception e) + { + _logger.LogError(e, $"GetAllAsync {_typeName}"); + throw; + } + } + + + public async Task GetByIdAsync(long id) + { + try + { + await using DbContext context = _contextFactory.CreateDbContext(); + TEntity tmp = await context.Set().FindAsync(id); + return tmp; + } + catch (Exception e) + { + _logger.LogError(e, $"GetByIdAsync {_typeName}"); + throw; + } + } + + public async Task> GetByIdsAsync(IEnumerable ids) + { + try + { + await using DbContext context = _contextFactory.CreateDbContext(); + List entities = await context.Set().Where(s => ids.Contains(s.Id)).ToListAsync(); + return entities; + } + catch (Exception e) + { + _logger.LogError(e, $"GetByIdsAsync {_typeName}"); + throw; + } + } + + public async Task SaveAsync(TEntity obj) + { + try + { + await using DbContext context = _contextFactory.CreateDbContext(); + + await context.SingleMergeAsync(obj, operation => + { + operation.InsertKeepIdentity = true; + operation.IsCheckConstraintOnInsertDisabled = false; + }); + + return obj; + } + catch (Exception e) + { + _logger.LogError(e, $"SaveAsync {_typeName}"); + throw; + } + } + + public async Task> SaveAsync(IReadOnlyList objs) + { + try + { + await using DbContext context = _contextFactory.CreateDbContext(); + + await context.BulkMergeAsync(objs, operation => + { + operation.InsertKeepIdentity = true; + operation.IsCheckConstraintOnInsertDisabled = false; + }); + return objs; + } + catch (Exception e) + { + _logger.LogError(e, $"SaveAsync {_typeName}"); + throw; + } + } + + public async Task DeleteByIdAsync(long id) + { + try + { + await using DbContext context = _contextFactory.CreateDbContext(); + TEntity entity = await context.FindAsync(id); + if (entity == null) + { + return; + } + + context.Set().Remove(entity); + await context.SaveChangesAsync(); + } + catch (Exception e) + { + _logger.LogError(e, $"DeleteByIdAsync {_typeName}"); + throw; + } + } + + public async Task DeleteByIdsAsync(IEnumerable ids) + { + try + { + await using DbContext context = _contextFactory.CreateDbContext(); + foreach (long id in ids) + { + TEntity entity = await context.FindAsync(id); + if (entity == null) + { + continue; + } + + context.Set().Remove(entity); + } + + await context.SaveChangesAsync(); + } + catch (Exception e) + { + _logger.LogError(e, $"DeleteByIdsAsync {_typeName}"); + throw; + } + } + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.DAL.EFCore.PGSQL/GenericMappedIntRepository.cs b/srcs/PhoenixLib.DAL.EFCore.PGSQL/GenericMappedIntRepository.cs new file mode 100644 index 0000000..7fd1d3f --- /dev/null +++ b/srcs/PhoenixLib.DAL.EFCore.PGSQL/GenericMappedIntRepository.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace PhoenixLib.DAL.EFCore.PGSQL +{ + /// + /// GenericAsyncMappedRepository is an asynchronous Data Access Object + /// + /// + /// + public sealed class GenericMappedIntRepository : IGenericAsyncIntRepository + where TDto : class, IIntDto, new() + where TEntity : class, IIntEntity, new() + { + private readonly IMapper _mapper; + private readonly IGenericAsyncIntRepository _repository; + + public GenericMappedIntRepository(IMapper mapper, IGenericAsyncIntRepository repository) + { + _mapper = mapper; + _repository = repository; + } + + public async Task> GetAllAsync() => _mapper.Map(await _repository.GetAllAsync()); + + public async Task GetByIdAsync(int id) => _mapper.Map(await _repository.GetByIdAsync(id)); + + public async Task> GetByIdsAsync(IEnumerable ids) => _mapper.Map(await _repository.GetByIdsAsync(ids)); + + public async Task SaveAsync(TDto obj) => _mapper.Map(await _repository.SaveAsync(_mapper.Map(obj))); + + public async Task> SaveAsync(IReadOnlyList objs) => _mapper.Map(await _repository.SaveAsync(_mapper.Map(objs))); + + public async Task DeleteByIdAsync(int id) + { + await _repository.DeleteByIdAsync(id); + } + + public async Task DeleteByIdsAsync(IEnumerable ids) + { + await _repository.DeleteByIdsAsync(ids); + } + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.DAL.EFCore.PGSQL/GenericMappedLongRepository.cs b/srcs/PhoenixLib.DAL.EFCore.PGSQL/GenericMappedLongRepository.cs new file mode 100644 index 0000000..e800d4e --- /dev/null +++ b/srcs/PhoenixLib.DAL.EFCore.PGSQL/GenericMappedLongRepository.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace PhoenixLib.DAL.EFCore.PGSQL +{ + /// + /// GenericAsyncMappedRepository is an asynchronous Data Access Object + /// + /// + /// + public sealed class GenericMappedLongRepository : IGenericAsyncLongRepository + where TDto : class, ILongDto, new() + where TEntity : class, ILongEntity, new() + { + private readonly IMapper _mapper; + private readonly IGenericAsyncLongRepository _repository; + + public GenericMappedLongRepository(IMapper mapper, IGenericAsyncLongRepository repository) + { + _mapper = mapper; + _repository = repository; + } + + public async Task> GetAllAsync() => _mapper.Map(await _repository.GetAllAsync()); + + public async Task GetByIdAsync(long id) => _mapper.Map(await _repository.GetByIdAsync(id)); + + public async Task> GetByIdsAsync(IEnumerable ids) => _mapper.Map(await _repository.GetByIdsAsync(ids)); + + public async Task SaveAsync(TDto obj) => _mapper.Map(await _repository.SaveAsync(_mapper.Map(obj))); + + public async Task> SaveAsync(IReadOnlyList objs) => _mapper.Map(await _repository.SaveAsync(_mapper.Map(objs))); + + public async Task DeleteByIdAsync(long id) + { + await _repository.DeleteByIdAsync(id); + } + + public async Task DeleteByIdsAsync(IEnumerable ids) + { + await _repository.DeleteByIdsAsync(ids); + } + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.DAL.EFCore.PGSQL/GenericMappedUuidRepository.cs b/srcs/PhoenixLib.DAL.EFCore.PGSQL/GenericMappedUuidRepository.cs new file mode 100644 index 0000000..6e6cd5f --- /dev/null +++ b/srcs/PhoenixLib.DAL.EFCore.PGSQL/GenericMappedUuidRepository.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace PhoenixLib.DAL.EFCore.PGSQL +{ + /// + /// GenericAsyncMappedRepository is an asynchronous Data Access Object + /// + /// + /// + public sealed class GenericMappedUuidRepository : IGenericAsyncUuidRepository + where TDto : class, IUuidDto, new() + where TEntity : class, IUuidEntity, new() + { + private readonly IMapper _mapper; + private readonly IGenericAsyncUuidRepository _repository; + + public GenericMappedUuidRepository(IMapper mapper, IGenericAsyncUuidRepository repository) + { + _mapper = mapper; + _repository = repository; + } + + public async Task> GetAllAsync() => _mapper.Map(await _repository.GetAllAsync()); + + public async Task GetByIdAsync(Guid id) => _mapper.Map(await _repository.GetByIdAsync(id)); + + public async Task> GetByIdsAsync(IEnumerable ids) => _mapper.Map(await _repository.GetByIdsAsync(ids)); + + public async Task SaveAsync(TDto obj) => _mapper.Map(await _repository.SaveAsync(_mapper.Map(obj))); + + public async Task> SaveAsync(IReadOnlyList objs) => _mapper.Map(await _repository.SaveAsync(_mapper.Map(objs))); + + public async Task DeleteByIdAsync(Guid id) + { + await _repository.DeleteByIdAsync(id); + } + + public async Task DeleteByIdsAsync(IEnumerable ids) + { + await _repository.DeleteByIdsAsync(ids); + } + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.DAL.EFCore.PGSQL/GenericOldMappedIntRepository.cs b/srcs/PhoenixLib.DAL.EFCore.PGSQL/GenericOldMappedIntRepository.cs new file mode 100644 index 0000000..8d632a1 --- /dev/null +++ b/srcs/PhoenixLib.DAL.EFCore.PGSQL/GenericOldMappedIntRepository.cs @@ -0,0 +1,156 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace PhoenixLib.DAL.EFCore.PGSQL +{ + /// + /// GenericAsyncMappedRepository is an asynchronous Data Access Object + /// + /// + /// + /// + [Obsolete("use GenericLongRepository instead")] + public sealed class GenericOldMappedIntRepository : IGenericAsyncIntRepository + where TDto : class, IIntDto, new() + where TEntity : class, IIntEntity, new() + where TDbContext : DbContext + { + private readonly IDbContextFactory _contextFactory; + private readonly ILogger> _logger; + private readonly IMapper _mapper; + + public GenericOldMappedIntRepository(IDbContextFactory contextFactory, + IMapper mapper, ILogger> logger) + { + _contextFactory = contextFactory; + _mapper = mapper; + _logger = logger; + } + + public async Task> GetAllAsync() + { + try + { + await using TDbContext context = _contextFactory.CreateDbContext(); + List tmp = await context.Set().ToListAsync(); + return _mapper.Map(tmp); + } + catch (Exception e) + { + _logger.LogError(e, "GetAllAsync"); + throw; + } + } + + + public async Task GetByIdAsync(int id) + { + try + { + await using DbContext context = _contextFactory.CreateDbContext(); + TEntity tmp = await context.Set().FindAsync(id); + return _mapper.Map(tmp); + } + catch (Exception e) + { + _logger.LogError(e, "GetByIdAsync"); + throw; + } + } + + public async Task> GetByIdsAsync(IEnumerable ids) + { + try + { + await using DbContext context = _contextFactory.CreateDbContext(); + List entities = await context.Set().Where(s => ids.Contains(s.Id)).ToListAsync(); + return _mapper.Map(entities); + } + catch (Exception e) + { + _logger.LogError(e, "GetByIdsAsync"); + throw; + } + } + + public async Task SaveAsync(TDto obj) + { + try + { + await using DbContext context = _contextFactory.CreateDbContext(); + TEntity entity = _mapper.Map(obj); + await context.SingleMergeAsync(entity, operation => + { + operation.InsertKeepIdentity = true; + operation.IsCheckConstraintOnInsertDisabled = false; + }); + return _mapper.Map(entity); + } + catch (Exception e) + { + _logger.LogError(e, "SaveAsync"); + throw; + } + } + + public async Task> SaveAsync(IReadOnlyList objs) + { + try + { + List entities = _mapper.Map(objs.ToList()); + await using (DbContext context = _contextFactory.CreateDbContext()) + { + await context.BulkMergeAsync(entities, operation => + { + operation.InsertKeepIdentity = true; + operation.IsCheckConstraintOnInsertDisabled = false; + }); + } + + return _mapper.Map(entities); + } + catch (Exception e) + { + _logger.LogError(e, $"SaveAsync {typeof(TEntity).Name}"); + return Enumerable.Empty(); + } + } + + public async Task DeleteByIdAsync(int id) + { + try + { + await using DbContext context = _contextFactory.CreateDbContext(); + var model = new TEntity { Id = id }; + context.Set().Attach(model); + context.Set().Remove(model); + await context.SaveChangesAsync(); + } + catch (Exception e) + { + _logger.LogError(e, "DeleteByIdAsync"); + throw; + } + } + + public async Task DeleteByIdsAsync(IEnumerable ids) + { + try + { + await using DbContext context = _contextFactory.CreateDbContext(); + var toDelete = ids.Select(s => new TEntity { Id = s }).ToList(); + context.Set().RemoveRange(toDelete); + await context.SaveChangesAsync(); + } + catch (Exception e) + { + _logger.LogError(e, "DeleteByIdsAsync"); + throw; + } + } + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.DAL.EFCore.PGSQL/GenericOldMappedLongRepository.cs b/srcs/PhoenixLib.DAL.EFCore.PGSQL/GenericOldMappedLongRepository.cs new file mode 100644 index 0000000..990bae5 --- /dev/null +++ b/srcs/PhoenixLib.DAL.EFCore.PGSQL/GenericOldMappedLongRepository.cs @@ -0,0 +1,158 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace PhoenixLib.DAL.EFCore.PGSQL +{ + /// + /// GenericAsyncMappedRepository is an asynchronous Data Access Object + /// + /// + /// + /// + [Obsolete("Use GenericMappedLongRepository now")] + public sealed class GenericOldMappedLongRepository : IGenericAsyncLongRepository + where TDto : class, ILongDto, new() + where TEntity : class, ILongEntity, new() + where TDbContext : DbContext + { + private readonly IDbContextFactory _contextFactory; + private readonly ILogger> _logger; + private readonly IMapper _mapper; + + public GenericOldMappedLongRepository(IDbContextFactory contextFactory, + IMapper mapper, ILogger> logger) + { + _contextFactory = contextFactory; + _mapper = mapper; + _logger = logger; + } + + public async Task> GetAllAsync() + { + try + { + await using TDbContext context = _contextFactory.CreateDbContext(); + List tmp = await context.Set().ToListAsync(); + return _mapper.Map(tmp); + } + catch (Exception e) + { + _logger.LogError(e, "GetAllAsync"); + throw; + } + } + + + public async Task GetByIdAsync(long id) + { + try + { + await using DbContext context = _contextFactory.CreateDbContext(); + TEntity tmp = await context.Set().FindAsync(id); + return _mapper.Map(tmp); + } + catch (Exception e) + { + _logger.LogError(e, "GetByIdAsync"); + throw; + } + } + + public async Task> GetByIdsAsync(IEnumerable ids) + { + try + { + await using DbContext context = _contextFactory.CreateDbContext(); + List entities = await context.Set().Where(s => ids.Contains(s.Id)).ToListAsync(); + return _mapper.Map(entities); + } + catch (Exception e) + { + _logger.LogError(e, "GetByIdsAsync"); + throw; + } + } + + public async Task SaveAsync(TDto obj) + { + try + { + await using DbContext context = _contextFactory.CreateDbContext(); + + TEntity entity = _mapper.Map(obj); + + await context.SingleMergeAsync(entity, operation => + { + operation.InsertKeepIdentity = true; + operation.IsCheckConstraintOnInsertDisabled = false; + }); + return _mapper.Map(entity); + } + catch (Exception e) + { + _logger.LogError(e, "SaveAsync"); + throw; + } + } + + public async Task> SaveAsync(IReadOnlyList objs) + { + try + { + List entities = _mapper.Map(objs.ToList()); + await using (DbContext context = _contextFactory.CreateDbContext()) + { + await context.BulkMergeAsync(entities, operation => + { + operation.InsertKeepIdentity = true; + operation.IsCheckConstraintOnInsertDisabled = false; + }); + } + + return _mapper.Map(entities); + } + catch (Exception e) + { + _logger.LogError(e, "SaveAsyncBulk"); + throw; + } + } + + public async Task DeleteByIdAsync(long id) + { + try + { + await using DbContext context = _contextFactory.CreateDbContext(); + var model = new TEntity { Id = id }; + context.Set().Attach(model); + context.Set().Remove(model); + await context.SaveChangesAsync(); + } + catch (Exception e) + { + _logger.LogError(e, "DeleteByIdsAsync"); + throw; + } + } + + public async Task DeleteByIdsAsync(IEnumerable ids) + { + try + { + await using DbContext context = _contextFactory.CreateDbContext(); + var toDelete = ids.Select(s => new TEntity { Id = s }).ToList(); + context.Set().RemoveRange(toDelete); + await context.SaveChangesAsync(); + } + catch (Exception e) + { + _logger.LogError(e, "DeleteByIdsAsync"); + throw; + } + } + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.DAL.EFCore.PGSQL/GenericOldMappedUuidRepository.cs b/srcs/PhoenixLib.DAL.EFCore.PGSQL/GenericOldMappedUuidRepository.cs new file mode 100644 index 0000000..ba4308e --- /dev/null +++ b/srcs/PhoenixLib.DAL.EFCore.PGSQL/GenericOldMappedUuidRepository.cs @@ -0,0 +1,169 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace PhoenixLib.DAL.EFCore.PGSQL +{ + /// + /// GenericAsyncMappedRepository is an asynchronous Data Access Object + /// + /// + /// + /// + [Obsolete("Use GenericMappedUuidRepository now")] + public class GenericOldMappedUuidRepository : IGenericAsyncUuidRepository + where TDto : class, IUuidDto, new() + where TEntity : class, IUuidEntity, new() + where TDbContext : DbContext + { + private readonly IDbContextFactory _contextFactory; + private readonly ILogger> _logger; + private readonly IMapper _mapper; + + public GenericOldMappedUuidRepository(IDbContextFactory contextFactory, IMapper mapper, ILogger> logger) + { + _contextFactory = contextFactory; + _mapper = mapper; + _logger = logger; + } + + public async Task> GetAllAsync() + { + try + { + await using DbContext context = _contextFactory.CreateDbContext(); + List tmp = await context.Set().ToListAsync(); + return _mapper.Map(tmp); + } + catch (Exception e) + { + _logger.LogError(e, "GetAllAsync"); + throw; + } + } + + + public async Task GetByIdAsync(Guid id) + { + try + { + await using DbContext context = _contextFactory.CreateDbContext(); + TEntity tmp = await context.Set().FirstOrDefaultAsync(s => s.Id == id); + return _mapper.Map(tmp); + } + catch (Exception e) + { + _logger.LogError(e, "GetByIdAsync"); + throw; + } + } + + public async Task> GetByIdsAsync(IEnumerable ids) + { + try + { + await using DbContext context = _contextFactory.CreateDbContext(); + List tmp = await context.Set().Where(s => ids.Contains(s.Id)).ToListAsync(); + return _mapper.Map(tmp); + } + catch (Exception e) + { + _logger.LogError(e, "GetByIdsAsync"); + throw; + } + } + + public async Task SaveAsync(TDto obj) + { + try + { + await using DbContext context = _contextFactory.CreateDbContext(); + TEntity entity = _mapper.Map(obj); + await context.SingleMergeAsync(obj, operation => + { + operation.InsertKeepIdentity = true; + operation.IsCheckConstraintOnInsertDisabled = false; + }); + return _mapper.Map(entity); + } + catch (Exception e) + { + _logger.LogError(e, "SaveAsync"); + throw; + } + } + + public async Task> SaveAsync(IReadOnlyList objs) + { + try + { + var entities = new List(_mapper.Map(objs)); + await using DbContext context = _contextFactory.CreateDbContext(); + await context.BulkMergeAsync(entities, operation => + { + operation.InsertKeepIdentity = true; + operation.IsCheckConstraintOnInsertDisabled = false; + }); + return _mapper.Map(entities); + } + catch (Exception e) + { + _logger.LogError(e, $"SaveAsync<{typeof(TEntity).Name}>"); + throw; + } + } + + public async Task DeleteByIdAsync(Guid id) + { + try + { + await using DbContext context = _contextFactory.CreateDbContext(); + TEntity entity = await context.FindAsync(id); + if (entity == null) + { + return; + } + + context.Set().Remove(entity); + await context.SaveChangesAsync(); + } + catch (Exception e) + { + _logger.LogError(e, "DeleteByIdAsync"); + throw; + } + } + + public async Task DeleteByIdsAsync(IEnumerable ids) + { + try + { + await using DbContext context = _contextFactory.CreateDbContext(); + foreach (Guid id in ids) + { + TEntity entity = await context.FindAsync(id); + if (entity == null) + { + continue; + } + + context.Set().Remove(entity); + } + + await context.SaveChangesAsync(); + } + catch (Exception e) + { + _logger.LogError(e, "DeleteByIdsAsync"); + throw; + } + } + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.DAL.EFCore.PGSQL/GenericStringRepository.cs b/srcs/PhoenixLib.DAL.EFCore.PGSQL/GenericStringRepository.cs new file mode 100644 index 0000000..091ce77 --- /dev/null +++ b/srcs/PhoenixLib.DAL.EFCore.PGSQL/GenericStringRepository.cs @@ -0,0 +1,158 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace PhoenixLib.DAL.EFCore.PGSQL +{ + /// + /// GenericAsyncMappedRepository is an asynchronous repository for a given Entity + /// + /// + /// + public class GenericStringRepository : IGenericStringRepository + where TEntity : class, IStringKeyEntity, new() + where TDbContext : DbContext + { + private readonly IDbContextFactory _contextFactory; + private readonly ILogger> _logger; + + public GenericStringRepository(IDbContextFactory contextFactory, ILogger> logger) + { + _contextFactory = contextFactory; + _logger = logger; + } + + public async Task> GetAllAsync() + { + try + { + await using DbContext context = _contextFactory.CreateDbContext(); + List tmp = await context.Set().ToListAsync(); + return tmp; + } + catch (Exception e) + { + _logger.LogError(e, "GetAllAsync"); + throw; + } + } + + + public async Task GetByIdAsync(string id) + { + try + { + await using DbContext context = _contextFactory.CreateDbContext(); + TEntity tmp = await context.Set().FindAsync(id); + return tmp; + } + catch (Exception e) + { + _logger.LogError(e, "GetByIdAsync"); + throw; + } + } + + public async Task> GetByIdsAsync(IEnumerable ids) + { + try + { + await using DbContext context = _contextFactory.CreateDbContext(); + List tmp = await context.Set().Where(s => ids.Contains(s.Id)).ToListAsync(); + return tmp; + } + catch (Exception e) + { + _logger.LogError(e, "GetByIdsAsync"); + throw; + } + } + + public async Task SaveAsync(TEntity obj) + { + try + { + await using DbContext context = _contextFactory.CreateDbContext(); + await context.SingleMergeAsync(obj, operation => + { + operation.InsertKeepIdentity = true; + operation.IsCheckConstraintOnInsertDisabled = false; + }); + return obj; + } + catch (Exception e) + { + _logger.LogError(e, "SaveAsync"); + throw; + } + } + + public async Task> SaveAsync(IReadOnlyList objs) + { + try + { + await using DbContext context = _contextFactory.CreateDbContext(); + await context.BulkMergeAsync(objs, operation => + { + operation.InsertKeepIdentity = true; + operation.IsCheckConstraintOnInsertDisabled = false; + }); + return objs; + } + catch (Exception e) + { + _logger.LogError(e, $"SaveAsync<{typeof(TEntity).Name}>"); + throw; + } + } + + public async Task DeleteByIdAsync(string id) + { + try + { + await using DbContext context = _contextFactory.CreateDbContext(); + TEntity entity = await context.FindAsync(id); + if (entity == null) + { + return; + } + + context.Set().Remove(entity); + await context.SaveChangesAsync(); + } + catch (Exception e) + { + _logger.LogError(e, "DeleteByIdAsync"); + throw; + } + } + + public async Task DeleteByIdsAsync(IEnumerable ids) + { + try + { + await using DbContext context = _contextFactory.CreateDbContext(); + foreach (string id in ids) + { + TEntity entity = await context.FindAsync(id); + if (entity == null) + { + continue; + } + + context.Set().Remove(entity); + } + + await context.SaveChangesAsync(); + } + catch (Exception e) + { + _logger.LogError(e, "DeleteByIdsAsync"); + throw; + } + } + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.DAL.EFCore.PGSQL/GenericUuidRepository.cs b/srcs/PhoenixLib.DAL.EFCore.PGSQL/GenericUuidRepository.cs new file mode 100644 index 0000000..787632c --- /dev/null +++ b/srcs/PhoenixLib.DAL.EFCore.PGSQL/GenericUuidRepository.cs @@ -0,0 +1,177 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace PhoenixLib.DAL.EFCore.PGSQL +{ + /// + /// GenericAsyncMappedRepository is an asynchronous Data Access Object + /// + /// + /// + public sealed class GenericUuidRepository : IGenericAsyncUuidRepository + where TEntity : class, IUuidEntity, new() + where TDbContext : DbContext + { + private static readonly string _typeName = typeof(TEntity).Name; + private readonly IDbContextFactory _contextFactory; + + private readonly ILogger> _logger; + + public GenericUuidRepository(IDbContextFactory contextFactory, ILogger> logger) + { + _contextFactory = contextFactory; + _logger = logger; + } + + public async Task> GetAllAsync() + { + try + { + await using TDbContext context = _contextFactory.CreateDbContext(); + List tmp = await context.Set().ToListAsync(); + return tmp; + } + catch (Exception e) + { + _logger.LogError(e, $"GetAllAsync {_typeName}"); + throw; + } + } + + + public async Task GetByIdAsync(Guid id) + { + try + { + await using DbContext context = _contextFactory.CreateDbContext(); + TEntity tmp = await context.Set().FindAsync(id); + return tmp; + } + catch (Exception e) + { + _logger.LogError(e, $"GetByIdAsync {_typeName}"); + throw; + } + } + + public async Task> GetByIdsAsync(IEnumerable ids) + { + try + { + await using DbContext context = _contextFactory.CreateDbContext(); + List entities = await context.Set().Where(s => ids.Contains(s.Id)).ToListAsync(); + return entities; + } + catch (Exception e) + { + _logger.LogError(e, $"GetByIdsAsync {_typeName}"); + throw; + } + } + + public async Task SaveAsync(TEntity obj) + { + try + { + await using DbContext context = _contextFactory.CreateDbContext(); + + // meh + if (obj.Id == Guid.Empty) + { + obj.Id = Guid.NewGuid(); + } + + await context.SingleMergeAsync(obj, operation => + { + operation.InsertKeepIdentity = true; + operation.IsCheckConstraintOnInsertDisabled = false; + }); + + return obj; + } + catch (Exception e) + { + _logger.LogError(e, $"SaveAsync {_typeName}"); + throw; + } + } + + public async Task> SaveAsync(IReadOnlyList objs) + { + try + { + foreach (TEntity obj in objs) + { + // meh + if (obj.Id == Guid.Empty) + { + obj.Id = Guid.NewGuid(); + } + } + + await using DbContext context = _contextFactory.CreateDbContext(); + await context.BulkMergeAsync(objs, operation => + { + operation.InsertKeepIdentity = true; + operation.IsCheckConstraintOnInsertDisabled = false; + }); + return objs; + } + catch (Exception e) + { + _logger.LogError(e, $"SaveAsync {_typeName}"); + throw; + } + } + + public async Task DeleteByIdAsync(Guid id) + { + try + { + await using DbContext context = _contextFactory.CreateDbContext(); + TEntity entity = await context.FindAsync(id); + if (entity == null) + { + return; + } + + context.Set().Remove(entity); + await context.SaveChangesAsync(); + } + catch (Exception e) + { + _logger.LogError(e, $"DeleteByIdAsync {_typeName}"); + throw; + } + } + + public async Task DeleteByIdsAsync(IEnumerable ids) + { + try + { + await using DbContext context = _contextFactory.CreateDbContext(); + foreach (Guid id in ids) + { + TEntity entity = await context.FindAsync(id); + if (entity == null) + { + continue; + } + + context.Set().Remove(entity); + } + + await context.SaveChangesAsync(); + } + catch (Exception e) + { + _logger.LogError(e, $"DeleteByIdsAsync {_typeName}"); + throw; + } + } + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.DAL.EFCore.PGSQL/IContextFactory.cs b/srcs/PhoenixLib.DAL.EFCore.PGSQL/IContextFactory.cs new file mode 100644 index 0000000..5086f3e --- /dev/null +++ b/srcs/PhoenixLib.DAL.EFCore.PGSQL/IContextFactory.cs @@ -0,0 +1,17 @@ +// WingsEmu +// +// Developed by NosWings Team + +using Microsoft.EntityFrameworkCore; + +namespace PhoenixLib.DAL.EFCore.PGSQL +{ + public interface IContextFactory where T : DbContext + { + /// + /// Instantiates a new + /// + /// + T CreateContext(); + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.DAL.EFCore.PGSQL/IGenericStringRepository.cs b/srcs/PhoenixLib.DAL.EFCore.PGSQL/IGenericStringRepository.cs new file mode 100644 index 0000000..4c78de1 --- /dev/null +++ b/srcs/PhoenixLib.DAL.EFCore.PGSQL/IGenericStringRepository.cs @@ -0,0 +1,7 @@ +namespace PhoenixLib.DAL.EFCore.PGSQL +{ + public interface IGenericStringRepository : IGenericAsyncRepository + where TEntity : class, IStringKeyEntity, new() + { + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.DAL.EFCore.PGSQL/IIntEntity.cs b/srcs/PhoenixLib.DAL.EFCore.PGSQL/IIntEntity.cs new file mode 100644 index 0000000..2a4a59a --- /dev/null +++ b/srcs/PhoenixLib.DAL.EFCore.PGSQL/IIntEntity.cs @@ -0,0 +1,6 @@ +namespace PhoenixLib.DAL.EFCore.PGSQL +{ + public interface IIntEntity : IIntDto + { + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.DAL.EFCore.PGSQL/ILongEntity.cs b/srcs/PhoenixLib.DAL.EFCore.PGSQL/ILongEntity.cs new file mode 100644 index 0000000..6c6ca50 --- /dev/null +++ b/srcs/PhoenixLib.DAL.EFCore.PGSQL/ILongEntity.cs @@ -0,0 +1,13 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace PhoenixLib.DAL.EFCore.PGSQL +{ + /// + /// An entity that has a long as ID + /// + public interface ILongEntity : ILongDto + { + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.DAL.EFCore.PGSQL/IStringKeyEntity.cs b/srcs/PhoenixLib.DAL.EFCore.PGSQL/IStringKeyEntity.cs new file mode 100644 index 0000000..d6c4f11 --- /dev/null +++ b/srcs/PhoenixLib.DAL.EFCore.PGSQL/IStringKeyEntity.cs @@ -0,0 +1,7 @@ +namespace PhoenixLib.DAL.EFCore.PGSQL +{ + public interface IStringKeyEntity + { + public string Id { get; set; } + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.DAL.EFCore.PGSQL/IUuidEntity.cs b/srcs/PhoenixLib.DAL.EFCore.PGSQL/IUuidEntity.cs new file mode 100644 index 0000000..8afa78b --- /dev/null +++ b/srcs/PhoenixLib.DAL.EFCore.PGSQL/IUuidEntity.cs @@ -0,0 +1,9 @@ +namespace PhoenixLib.DAL.EFCore.PGSQL +{ + /// + /// An entity that has a GUID as ID + /// + public interface IUuidEntity : IUuidDto + { + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.DAL.EFCore.PGSQL/PgSqlDatabaseConfiguration.cs b/srcs/PhoenixLib.DAL.EFCore.PGSQL/PgSqlDatabaseConfiguration.cs new file mode 100644 index 0000000..a0288f8 --- /dev/null +++ b/srcs/PhoenixLib.DAL.EFCore.PGSQL/PgSqlDatabaseConfiguration.cs @@ -0,0 +1,43 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using Microsoft.EntityFrameworkCore; + +namespace PhoenixLib.DAL.EFCore.PGSQL +{ + public class PgSqlDatabaseConfiguration where TDbContext : DbContext + { + public PgSqlDatabaseConfiguration(string ip, string username, string password, string database, int port) + { + Ip = ip; + Username = username; + Password = password; + Database = database; + Port = port; + } + + public string Ip { get; } + public string Username { get; } + public string Password { get; } + public string Database { get; } + public int Port { get; } + + public static PgSqlDatabaseConfiguration FromEnv() + { + string ip = Environment.GetEnvironmentVariable("POSTGRES_DATABASE_IP") ?? "localhost"; + string username = Environment.GetEnvironmentVariable("POSTGRES_DATABASE_USER") ?? "postgres"; + string password = Environment.GetEnvironmentVariable("POSTGRES_DATABASE_PASSWORD") ?? "postgres"; + string database = Environment.GetEnvironmentVariable("POSTGRES_DATABASE_NAME") ?? "posgtres"; + if (!ushort.TryParse(Environment.GetEnvironmentVariable("POSTGRES_DATABASE_PORT") ?? "5432", out ushort port)) + { + port = 5432; + } + + return new PgSqlDatabaseConfiguration(ip, username, password, database, port); + } + + public override string ToString() => $"Server={Ip};Port={Port};Database={Database};User Id={Username};Password={Password};"; + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.DAL.EFCore.PGSQL/PhoenixLib.DAL.EFCore.PGSQL.csproj b/srcs/PhoenixLib.DAL.EFCore.PGSQL/PhoenixLib.DAL.EFCore.PGSQL.csproj new file mode 100644 index 0000000..c55e4a5 --- /dev/null +++ b/srcs/PhoenixLib.DAL.EFCore.PGSQL/PhoenixLib.DAL.EFCore.PGSQL.csproj @@ -0,0 +1,18 @@ + + + + net5.0 + + + + + + + + + + + + + + diff --git a/srcs/PhoenixLib.DAL.MongoDB/MappedMongoRepository.cs b/srcs/PhoenixLib.DAL.MongoDB/MappedMongoRepository.cs new file mode 100644 index 0000000..f4e7518 --- /dev/null +++ b/srcs/PhoenixLib.DAL.MongoDB/MappedMongoRepository.cs @@ -0,0 +1,86 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Threading.Tasks; +using MongoDB.Driver; + +namespace PhoenixLib.DAL.MongoDB +{ + public sealed class MappedMongoRepository : IGenericAsyncLongRepository where TObject : class, ILongDto + { + private readonly IMongoCollection _collection; + + public MappedMongoRepository(MongoConfigurationBuilder builder, IMongoClient client) : this(builder.Build(), client) + { + } + + private MappedMongoRepository(MongoConfiguration conf, IMongoClient client) + { + IMongoDatabase database = client.GetDatabase(conf.DatabaseName); + _collection = database.GetCollection(typeof(TObject).Name); + } + + public async Task> GetAllAsync() => await (await _collection.FindAsync(FilterDefinition.Empty)).ToListAsync(); + + public async Task GetByIdAsync(long id) + { + return await (await _collection.FindAsync(o => o.Id == id)).FirstOrDefaultAsync(); + } + + public async Task> GetByIdsAsync(IEnumerable ids) + { + return await (await _collection.FindAsync(o => ids.Any(s => s == o.Id))).ToListAsync(); + } + + public async Task SaveAsync(TObject obj) + { + await _collection.FindOneAndReplaceAsync(o => o.Id == obj.Id, obj, new FindOneAndReplaceOptions + { + IsUpsert = true + }); + + return obj; + } + + async Task> IGenericAsyncRepository.SaveAsync(IReadOnlyList objs) + { + var bulks = objs.Select(obj => + new ReplaceOneModel(Builders.Filter.Where(s => s.Id == obj.Id), obj) + { IsUpsert = true }).ToList(); + + await _collection.BulkWriteAsync(bulks, new BulkWriteOptions + { + IsOrdered = true + }); + return new List(objs); + } + + + public async Task DeleteByIdAsync(long id) + { + await _collection.DeleteOneAsync(obj => obj.Id == id); + } + + public async Task DeleteByIdsAsync(IEnumerable ids) + { + await _collection.DeleteManyAsync(o => ids.Any(id => o.Id == id)); + } + + public async Task> GetAllMatchingAsync(Func predicate) + { + Expression> expr = o => predicate(o); + return await (await _collection.FindAsync(expr)).ToListAsync(); + } + + public async Task GetFirstMatchingOrDefaultAsync(Func predicate) + { + Expression> expr = o => predicate(o); + return await (await _collection.FindAsync(expr)).FirstOrDefaultAsync(); + } + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.DAL.MongoDB/MongoConfiguration.cs b/srcs/PhoenixLib.DAL.MongoDB/MongoConfiguration.cs new file mode 100644 index 0000000..427f19b --- /dev/null +++ b/srcs/PhoenixLib.DAL.MongoDB/MongoConfiguration.cs @@ -0,0 +1,16 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace PhoenixLib.DAL.MongoDB +{ + public class MongoConfiguration + { + public string Endpoint { get; set; } + + public short Port { get; set; } + public string DatabaseName { get; set; } + + public override string ToString() => $"mongodb://{Endpoint}:{Port}"; + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.DAL.MongoDB/MongoConfigurationBuilder.cs b/srcs/PhoenixLib.DAL.MongoDB/MongoConfigurationBuilder.cs new file mode 100644 index 0000000..ba78f86 --- /dev/null +++ b/srcs/PhoenixLib.DAL.MongoDB/MongoConfigurationBuilder.cs @@ -0,0 +1,39 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace PhoenixLib.DAL.MongoDB +{ + public class MongoConfigurationBuilder + { + private string _databaseName; + private string _endpoint; + private short _port; + + public MongoConfigurationBuilder ConnectTo(string ip) + { + _endpoint = ip; + return this; + } + + public MongoConfigurationBuilder WithDatabaseName(string databaseName) + { + _databaseName = databaseName; + return this; + } + + public MongoConfigurationBuilder WithPort(short port) + { + _port = port; + return this; + } + + public MongoConfiguration Build() => + new() + { + Endpoint = _endpoint, + Port = _port, + DatabaseName = _databaseName + }; + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.DAL.MongoDB/PhoenixLib.DAL.MongoDB.csproj b/srcs/PhoenixLib.DAL.MongoDB/PhoenixLib.DAL.MongoDB.csproj new file mode 100644 index 0000000..6bee6da --- /dev/null +++ b/srcs/PhoenixLib.DAL.MongoDB/PhoenixLib.DAL.MongoDB.csproj @@ -0,0 +1,18 @@ + + + + net5.0 + PhoenixLib.DAL.MongoDB + + + + + + + + + + + + + diff --git a/srcs/PhoenixLib.DAL.MongoDB/SynchronizedMongoRepository.cs b/srcs/PhoenixLib.DAL.MongoDB/SynchronizedMongoRepository.cs new file mode 100644 index 0000000..7f7ce52 --- /dev/null +++ b/srcs/PhoenixLib.DAL.MongoDB/SynchronizedMongoRepository.cs @@ -0,0 +1,63 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using MongoDB.Driver; + +namespace PhoenixLib.DAL.MongoDB +{ + public sealed class SynchronizedMongoRepository : IGenericAsyncUuidRepository where TObject : class, IUuidDto + { + private readonly IMongoCollection Collection; + + // protected readonly ILogger Log; + private readonly IMongoDatabase Database; + + public SynchronizedMongoRepository(MongoConfiguration conf) + { + var settings = MongoClientSettings.FromConnectionString(conf.ToString()); + var client = new MongoClient(settings); + Database = client.GetDatabase(conf.DatabaseName); + Collection = Database.GetCollection(typeof(TObject).Name); + } + + public async Task> GetAllAsync() => await (await Collection.FindAsync(FilterDefinition.Empty)).ToListAsync(); + + public async Task GetByIdAsync(Guid id) + { + return await (await Collection.FindAsync(o => o.Id == id)).FirstOrDefaultAsync(); + } + + public async Task> GetByIdsAsync(IEnumerable ids) + { + return await (await Collection.FindAsync(o => ids.Any(s => s == o.Id))).ToListAsync(); + } + + public async Task SaveAsync(TObject obj) + { + return await Collection.FindOneAndUpdateAsync(o => o.Id == obj.Id, new ObjectUpdateDefinition(obj)); + } + + public async Task> SaveAsync(IReadOnlyList objs) + { + // probably faster than FindOneAndUpdateOne for each object + await Collection.DeleteManyAsync(o => objs.Any(s => s.Id == o.Id)); + await Collection.InsertManyAsync(objs); + return objs.ToList(); + } + + public async Task DeleteByIdAsync(Guid id) + { + await Collection.DeleteOneAsync(obj => obj.Id == id); + } + + public async Task DeleteByIdsAsync(IEnumerable ids) + { + await Collection.DeleteManyAsync(o => ids.Any(id => o.Id == id)); + } + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.DAL.Redis/DependencyInjectionExtensions.cs b/srcs/PhoenixLib.DAL.Redis/DependencyInjectionExtensions.cs new file mode 100644 index 0000000..6260b11 --- /dev/null +++ b/srcs/PhoenixLib.DAL.Redis/DependencyInjectionExtensions.cs @@ -0,0 +1,69 @@ +using Foundatio.Caching; +using Foundatio.Serializer; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using StackExchange.Redis; + +namespace PhoenixLib.DAL.Redis +{ + public static class DependencyInjectionExtensions + { + public static IConnectionMultiplexer GetConnectionMultiplexer(this RedisConfiguration redisConfiguration) => + ConnectionMultiplexer.Connect(new ConfigurationOptions + { + Password = redisConfiguration.Password, + EndPoints = { $"{redisConfiguration.Host}:{redisConfiguration.Port}" } + }); + + /// + /// Register redis configuration from env + /// + /// + public static void TryAddConfigurationFromEnv(this IServiceCollection services) + { + services.TryAddSingleton(s => RedisConfiguration.FromEnv()); + } + + /// + /// Registers the Connection Multiplexer + /// + /// + internal static void TryAddConnectionMultiplexer(this IServiceCollection services) + { + services.TryAddSingleton(s => s.GetService().GetConnectionMultiplexer()); + } + + /// + /// Registers the Connection Multiplexer + /// + /// + public static void TryAddConnectionMultiplexerFromEnv(this IServiceCollection services) + { + services.TryAddConfigurationFromEnv(); + services.TryAddConnectionMultiplexer(); + } + + public static void TryAddRedisCacheClient(this IServiceCollection services) + { + services.TryAddSingleton(s => new RedisCacheClient(new RedisCacheClientOptions + { + ConnectionMultiplexer = s.GetRequiredService(), + Serializer = new JsonNetSerializer() + })); + services.TryAddSingleton(s => s.GetRequiredService()); + } + + /// + /// Registers a KeyValueStorage from Env + /// + /// + /// + /// + public static void TryAddRedisKeyValueStorageFromEnv(this IServiceCollection services) + { + services.TryAddConnectionMultiplexerFromEnv(); + services.TryAddRedisCacheClient(); + services.TryAddSingleton(typeof(IKeyValueAsyncStorage<,>), typeof(RedisGenericKeyValueAsyncStorage<,>)); + } + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.DAL.Redis/Locks/IExpirableLockService.cs b/srcs/PhoenixLib.DAL.Redis/Locks/IExpirableLockService.cs new file mode 100644 index 0000000..602ce66 --- /dev/null +++ b/srcs/PhoenixLib.DAL.Redis/Locks/IExpirableLockService.cs @@ -0,0 +1,46 @@ +using System; +using System.Threading.Tasks; + +namespace PhoenixLib.DAL.Redis.Locks +{ + public interface IExpirableLockService + { + /// + /// Sets a key without increment with a defined expiration date + /// + /// + /// + /// true if the key already exist + Task TryAddTemporaryLockAsync(string key, DateTime expirationDate); + + /// + /// Sets an incremental key with a defined maxValue, expires on the specified expirationDate + /// + /// + /// + /// + /// + Task<(bool, int newValue)> TryIncrementTemporaryLockCounter(string key, int maxValue, DateTime expirationDate); + + /// + /// Try getting the current value of the counter + /// + /// + /// + Task<(bool, int newValue)> TryGetTemporaryCounterValue(string key); + + /// + /// Removes a key if exists + /// + /// + /// true if the key exists and got removed + Task TryRemoveTemporaryLock(string key); + + /// + /// Checks if a key exists + /// + /// + /// true if the key exists + Task ExistsTemporaryLock(string key); + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.DAL.Redis/Locks/ILockService.cs b/srcs/PhoenixLib.DAL.Redis/Locks/ILockService.cs new file mode 100644 index 0000000..4e2a66e --- /dev/null +++ b/srcs/PhoenixLib.DAL.Redis/Locks/ILockService.cs @@ -0,0 +1,14 @@ +using System.Threading.Tasks; + +namespace PhoenixLib.DAL.Redis.Locks +{ + public interface ILockService + { + Task TryAcquireScopedLock(string key); + + Task IsLocked(string key); + + Task TryAcquireLock(string key); + Task TryFreeLock(string key); + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.DAL.Redis/Locks/IScopedLock.cs b/srcs/PhoenixLib.DAL.Redis/Locks/IScopedLock.cs new file mode 100644 index 0000000..44c7aa3 --- /dev/null +++ b/srcs/PhoenixLib.DAL.Redis/Locks/IScopedLock.cs @@ -0,0 +1,9 @@ +using System; + +namespace PhoenixLib.DAL.Redis.Locks +{ + public interface IScopedLock : IAsyncDisposable + { + bool IsAcquired { get; } + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.DAL.Redis/Locks/RedisCheckableLock.cs b/srcs/PhoenixLib.DAL.Redis/Locks/RedisCheckableLock.cs new file mode 100644 index 0000000..810dba3 --- /dev/null +++ b/srcs/PhoenixLib.DAL.Redis/Locks/RedisCheckableLock.cs @@ -0,0 +1,68 @@ +using System; +using System.Threading.Tasks; +using StackExchange.Redis; + +namespace PhoenixLib.DAL.Redis.Locks +{ + public class RedisCheckableLock : IExpirableLockService + { + private readonly IDatabase _database; + + public RedisCheckableLock(IConnectionMultiplexer multiplexer) => _database = multiplexer.GetDatabase(0); + + public async Task TryAddTemporaryLockAsync(string key, DateTime dateTime) + { + bool value = await _database.KeyExistsAsync(key); + if (value) + { + // key already exists + return false; + } + + long tmp = await _database.StringIncrementAsync(key); + TimeSpan expireIn = dateTime - DateTime.UtcNow; + await _database.KeyExpireAsync(key, expireIn); + + return true; + } + + public async Task<(bool, int newValue)> TryIncrementTemporaryLockCounter(string key, int maxValue, DateTime expirationDate) + { + RedisValueWithExpiry value = await _database.StringGetWithExpiryAsync(key); + if (!value.Value.HasValue) + { + int newValue = (int)await _database.StringIncrementAsync(key); + await _database.KeyExpireAsync(key, expirationDate); + return (true, newValue); + } + + int counter = int.Parse(value.Value); + if (counter >= maxValue) + { + // should not go above maxValue + return (false, counter); + } + + int tmp = (int)await _database.StringIncrementAsync(key); + await _database.KeyExpireAsync(key, expirationDate); + + return (true, tmp); + } + + public async Task<(bool, int newValue)> TryGetTemporaryCounterValue(string key) + { + RedisValueWithExpiry value = await _database.StringGetWithExpiryAsync(key); + if (!value.Value.HasValue) + { + return (false, 0); + } + + int counter = int.Parse(value.Value); + return (true, counter); + } + + public async Task TryRemoveTemporaryLock(string key) => await _database.KeyDeleteAsync(key); + + public async Task ExistsTemporaryLock(string key) => await _database.KeyExistsAsync(key); + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.DAL.Redis/Locks/RedisLockService.cs b/srcs/PhoenixLib.DAL.Redis/Locks/RedisLockService.cs new file mode 100644 index 0000000..9849f84 --- /dev/null +++ b/srcs/PhoenixLib.DAL.Redis/Locks/RedisLockService.cs @@ -0,0 +1,20 @@ +using System.Threading.Tasks; +using Foundatio.Caching; + +namespace PhoenixLib.DAL.Redis.Locks +{ + public class RedisLockService : ILockService + { + private readonly ICacheClient _database; + + public RedisLockService(ICacheClient database) => _database = database; + + public async Task TryAcquireScopedLock(string key) => new RedisScopedLock(key, this, await TryAcquireLock(key)); + + public async Task IsLocked(string key) => await _database.ExistsAsync(key); + + public Task TryAcquireLock(string key) => _database.AddAsync(key, 1.ToString()); + + public Task TryFreeLock(string key) => _database.RemoveAsync(key); + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.DAL.Redis/Locks/RedisScopedLock.cs b/srcs/PhoenixLib.DAL.Redis/Locks/RedisScopedLock.cs new file mode 100644 index 0000000..7a88497 --- /dev/null +++ b/srcs/PhoenixLib.DAL.Redis/Locks/RedisScopedLock.cs @@ -0,0 +1,29 @@ +using System.Threading.Tasks; + +namespace PhoenixLib.DAL.Redis.Locks +{ + public sealed class RedisScopedLock : IScopedLock + { + private readonly string _lockKey; + private readonly ILockService _lockService; + + public RedisScopedLock(string lockKey, ILockService lockService, bool isLockAcquired) + { + _lockKey = lockKey; + _lockService = lockService; + IsAcquired = isLockAcquired; + } + + public async ValueTask DisposeAsync() + { + if (!IsAcquired) + { + return; + } + + await _lockService.TryFreeLock(_lockKey); + } + + public bool IsAcquired { get; } + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.DAL.Redis/PhoenixLib.DAL.Redis.csproj b/srcs/PhoenixLib.DAL.Redis/PhoenixLib.DAL.Redis.csproj new file mode 100644 index 0000000..366e99a --- /dev/null +++ b/srcs/PhoenixLib.DAL.Redis/PhoenixLib.DAL.Redis.csproj @@ -0,0 +1,20 @@ + + + + net5.0 + PhoenixLib.DAL.Redis + + + + + + + + + + + + + + + diff --git a/srcs/PhoenixLib.DAL.Redis/RedisConfiguration.cs b/srcs/PhoenixLib.DAL.Redis/RedisConfiguration.cs new file mode 100644 index 0000000..75d5caf --- /dev/null +++ b/srcs/PhoenixLib.DAL.Redis/RedisConfiguration.cs @@ -0,0 +1,29 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; + +namespace PhoenixLib.DAL.Redis +{ + public class RedisConfiguration + { + public RedisConfiguration(string host, int port, string password) + { + Host = host; + Port = port; + Password = password; + } + + public string Host { get; } + public int Port { get; } + public string Password { get; } + + public static RedisConfiguration FromEnv() => + new( + Environment.GetEnvironmentVariable("REDIS_IP") ?? "localhost", + Convert.ToInt32(Environment.GetEnvironmentVariable("REDIS_PORT") ?? "6379"), + Environment.GetEnvironmentVariable("REDIS_PASSWORD") + ); + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.DAL.Redis/RedisGenericKeyValueAsyncStorage.cs b/srcs/PhoenixLib.DAL.Redis/RedisGenericKeyValueAsyncStorage.cs new file mode 100644 index 0000000..58ad282 --- /dev/null +++ b/srcs/PhoenixLib.DAL.Redis/RedisGenericKeyValueAsyncStorage.cs @@ -0,0 +1,88 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Foundatio.Caching; + +namespace PhoenixLib.DAL.Redis +{ + public sealed class RedisGenericKeyValueAsyncStorage : IKeyValueAsyncStorage + where TKey : notnull + { + private readonly ICacheClient _cacheClient; + private readonly string _dataPrefix; + private readonly string _keySetKey; + + public RedisGenericKeyValueAsyncStorage(ICacheClient multiplexer) : this(typeof(TObject).Name.ToLower(), multiplexer) + { + } + + private RedisGenericKeyValueAsyncStorage(string basePrefix, ICacheClient multiplexer) + { + _dataPrefix = "data:" + basePrefix + ':'; + _keySetKey = "keys:" + basePrefix; + _cacheClient = multiplexer; + } + + private Task>> KeySet => _cacheClient.GetListAsync(_keySetKey); + + public async Task> GetAllAsync() + { + return (await _cacheClient.GetAllAsync(await GetAllKeysAsync().ConfigureAwait(false)).ConfigureAwait(false)).Select(s => s.Value.Value); + } + + public async Task ClearAllAsync() + { + CacheValue> keys = await KeySet; + await _cacheClient.ListRemoveAsync(_keySetKey, keys); + await _cacheClient.RemoveAllAsync(keys.Value); + } + + + public async Task GetByIdAsync(TKey id) => (await _cacheClient.GetAsync(ToKey(id)).ConfigureAwait(false)).Value; + + public async Task> GetByIdsAsync(IEnumerable ids) + { + return (await _cacheClient.GetAllAsync(ids.Select(ToKey))).Select(s => s.Value.Value); + } + + public async Task RegisterAsync(TKey id, TObject obj, TimeSpan? lifeTime = null) + { + await RegisterKeyAsync(new[] { id }, lifeTime).ConfigureAwait(false); + await _cacheClient.SetAsync(ToKey(id), obj, lifeTime).ConfigureAwait(false); + } + + + public async Task RegisterAsync(IEnumerable<(TKey, TObject)> objs, TimeSpan? lifeTime = null) + { + await RegisterKeyAsync(objs.Select(s => s.Item1), lifeTime).ConfigureAwait(false); + await _cacheClient.SetAllAsync(objs.ToDictionary(s => ToKey(s.Item1), o => o), lifeTime).ConfigureAwait(false); + } + + public async Task RemoveAsync(TKey id) + { + await RemoveKeyAsync(new[] { id }).ConfigureAwait(false); + await _cacheClient.RemoveAsync(ToKey(id)).ConfigureAwait(false); + return default; + } + + public async Task> RemoveAsync(IEnumerable ids) + { + await RemoveKeyAsync(ids); + await _cacheClient.RemoveAllAsync(ids.Select(ToKey)); + return null; + } + + private async Task> GetAllKeysAsync() => (await KeySet).Value; + + private Task RegisterKeyAsync(IEnumerable keys, TimeSpan? lifeTime) => _cacheClient.ListAddAsync(_keySetKey, keys.Select(ToKey), lifeTime); + + private Task RemoveKeyAsync(IEnumerable keys) => _cacheClient.ListRemoveAsync(_keySetKey, keys.Select(ToKey)); + + private string ToKey(TKey id) => _dataPrefix + id; + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Events/AsyncEventPipeline.cs b/srcs/PhoenixLib.Events/AsyncEventPipeline.cs new file mode 100644 index 0000000..c4c6af6 --- /dev/null +++ b/srcs/PhoenixLib.Events/AsyncEventPipeline.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Logging; + +namespace PhoenixLib.Events +{ + internal class AsyncEventPipeline : IAsyncEventPipeline + { + private static readonly MethodInfo ProcessAsyncGenericMethodInfo = + typeof(AsyncEventPipeline).GetMethods(BindingFlags.Public | BindingFlags.Instance).FirstOrDefault(s => s.Name == nameof(ProcessEventAsync) && s.IsGenericMethod); + + private readonly IServiceProvider _serviceProvider; + + public AsyncEventPipeline(IServiceProvider serviceProvider) => _serviceProvider = serviceProvider; + + public async Task ProcessEventAsync(IAsyncEvent notification) + { + MethodInfo method = ProcessAsyncGenericMethodInfo.MakeGenericMethod(notification.GetType()); + await (Task)method.Invoke(this, new object[] { notification, CancellationToken.None }); + } + + public async Task ProcessEventAsync(T notification, CancellationToken cancellationToken = default) where T : IAsyncEvent + { + try + { + IEnumerable> handlers = _serviceProvider.GetServices>(); + foreach (IAsyncEventProcessor handler in handlers) + { + await handler.HandleAsync(notification, cancellationToken).ConfigureAwait(false); + } + } + catch (Exception e) + { + Log.Error("ProcessEventAsync", e); + } + } + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Events/ConvertedEventForwarder.cs b/srcs/PhoenixLib.Events/ConvertedEventForwarder.cs new file mode 100644 index 0000000..4abe522 --- /dev/null +++ b/srcs/PhoenixLib.Events/ConvertedEventForwarder.cs @@ -0,0 +1,29 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Threading; +using System.Threading.Tasks; + +namespace PhoenixLib.Events +{ + public class ConvertedEventForwarder : IAsyncEventProcessor + where TSource : IAsyncEvent + where TDest : IAsyncEvent + { + private readonly IConverter _converter; + private readonly IAsyncEventPipeline _eventPipeline; + + public ConvertedEventForwarder(IConverter converter, IAsyncEventPipeline eventPipeline) + { + _converter = converter; + _eventPipeline = eventPipeline; + } + + public async Task HandleAsync(TSource e, CancellationToken cancellation) + { + TDest dest = _converter.Convert(e); + await _eventPipeline.ProcessEventAsync(dest, cancellation); + } + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Events/DependencyInjection.cs b/srcs/PhoenixLib.Events/DependencyInjection.cs new file mode 100644 index 0000000..6e35536 --- /dev/null +++ b/srcs/PhoenixLib.Events/DependencyInjection.cs @@ -0,0 +1,41 @@ +using System; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using PhoenixLib.Events.Internal; +using PhoenixLib.Logging; + +namespace PhoenixLib.Events +{ + public static class DependencyInjection + { + public static void AddEventHandlersInAssembly(this IServiceCollection services) + { + Type[] types = typeof(T).Assembly.GetTypesImplementingInterface(typeof(IAsyncEventProcessor<>)); + foreach (Type handlerType in types) + { + services.AddTransient(handlerType); + Type handlerInterface = handlerType.GetInterfaces().First(s => s.IsGenericType && s.GetGenericTypeDefinition() == typeof(IAsyncEventProcessor<>)); + Log.Debug($"[EVENT_HANDLER] Added IEventHandler<{handlerInterface.GetGenericArguments()[0].Name}> : {handlerType}"); + services.AddTransient(handlerInterface, handlerType); + } + } + + public static void AddEventPipeline(this IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + } + + public static void AddEventProcessorsInAssembly(this IServiceCollection services) + { + Type[] types = typeof(T).Assembly.GetTypesImplementingInterface(typeof(IEventProcessor<>)); + foreach (Type handlerType in types) + { + services.AddTransient(handlerType); + Type handlerInterface = handlerType.GetInterfaces().First(s => s.IsGenericType && s.GetGenericTypeDefinition() == typeof(IEventProcessor<>)); + Log.Debug($"[EVENT_HANDLER] Added IEventProcessor<{handlerInterface.GetGenericArguments()[0].Name}> : {handlerType}"); + services.AddTransient(handlerInterface, handlerType); + } + } + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Events/EventPipeline.cs b/srcs/PhoenixLib.Events/EventPipeline.cs new file mode 100644 index 0000000..9864810 --- /dev/null +++ b/srcs/PhoenixLib.Events/EventPipeline.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using PhoenixLib.Logging; + +namespace PhoenixLib.Events +{ + internal class EventPipeline : IEventProcessorPipeline + { + private static readonly MethodInfo ProcessAsyncGenericMethodInfo = + typeof(EventPipeline).GetMethods(BindingFlags.Public | BindingFlags.Instance).FirstOrDefault(s => s.Name == nameof(ProcessEvent) && s.IsGenericMethod); + + private readonly IServiceProvider _serviceProvider; + + public EventPipeline(IServiceProvider serviceProvider) => _serviceProvider = serviceProvider; + + public void ProcessEvent(IEvent notification) + { + MethodInfo method = ProcessAsyncGenericMethodInfo.MakeGenericMethod(notification.GetType()); + method.Invoke(this, new object?[] { notification }); + } + + public void ProcessEvent(T eventNotification) where T : IEvent + { + IEnumerable> handlers = _serviceProvider.GetServices>(); + + try + { + foreach (IEventProcessor handler in handlers) + { + handler.Handle(eventNotification); + } + } + catch (Exception e) + { + Log.Error("ProcessEventAsync", e); + } + } + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Events/IAsyncEvent.cs b/srcs/PhoenixLib.Events/IAsyncEvent.cs new file mode 100644 index 0000000..ddb4c0e --- /dev/null +++ b/srcs/PhoenixLib.Events/IAsyncEvent.cs @@ -0,0 +1,13 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace PhoenixLib.Events +{ + /// + /// Interface for every type of event notification + /// + public interface IAsyncEvent + { + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Events/IAsyncEventPipeline.cs b/srcs/PhoenixLib.Events/IAsyncEventPipeline.cs new file mode 100644 index 0000000..14456dc --- /dev/null +++ b/srcs/PhoenixLib.Events/IAsyncEventPipeline.cs @@ -0,0 +1,22 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace PhoenixLib.Events +{ + public interface IAsyncEventPipeline + { + /// + /// + /// + /// + Task ProcessEventAsync(IAsyncEvent notification); + + /// + /// Asynchronously send a notification to handlers of type T + /// + /// Notification object + /// Optional cancellation token + /// A task that represents the publish operation. + Task ProcessEventAsync(T notification, CancellationToken cancellationToken = default) where T : IAsyncEvent; + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Events/IAsyncEventProcessor.cs b/srcs/PhoenixLib.Events/IAsyncEventProcessor.cs new file mode 100644 index 0000000..63e31ce --- /dev/null +++ b/srcs/PhoenixLib.Events/IAsyncEventProcessor.cs @@ -0,0 +1,18 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Threading; +using System.Threading.Tasks; + +namespace PhoenixLib.Events +{ + /// + /// Defines a handler for any type of notification + /// + public interface IAsyncEventProcessor + where T : IAsyncEvent + { + Task HandleAsync(T e, CancellationToken cancellation); + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Events/IConverter.cs b/srcs/PhoenixLib.Events/IConverter.cs new file mode 100644 index 0000000..32f17a1 --- /dev/null +++ b/srcs/PhoenixLib.Events/IConverter.cs @@ -0,0 +1,7 @@ +namespace PhoenixLib.Events +{ + public interface IConverter + { + TDestination Convert(TSource source); + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Events/IEvent.cs b/srcs/PhoenixLib.Events/IEvent.cs new file mode 100644 index 0000000..ceb609b --- /dev/null +++ b/srcs/PhoenixLib.Events/IEvent.cs @@ -0,0 +1,6 @@ +namespace PhoenixLib.Events +{ + public interface IEvent + { + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Events/IEventProcessor.cs b/srcs/PhoenixLib.Events/IEventProcessor.cs new file mode 100644 index 0000000..443105f --- /dev/null +++ b/srcs/PhoenixLib.Events/IEventProcessor.cs @@ -0,0 +1,8 @@ +namespace PhoenixLib.Events +{ + public interface IEventProcessor + where T : IEvent + { + void Handle(T e); + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Events/IEventProcessorPipeline.cs b/srcs/PhoenixLib.Events/IEventProcessorPipeline.cs new file mode 100644 index 0000000..de594cf --- /dev/null +++ b/srcs/PhoenixLib.Events/IEventProcessorPipeline.cs @@ -0,0 +1,8 @@ +namespace PhoenixLib.Events +{ + public interface IEventProcessorPipeline + { + void ProcessEvent(IEvent e); + void ProcessEvent(T e) where T : IEvent; + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Events/Internal/AssemblyExtensions.cs b/srcs/PhoenixLib.Events/Internal/AssemblyExtensions.cs new file mode 100644 index 0000000..82edd9c --- /dev/null +++ b/srcs/PhoenixLib.Events/Internal/AssemblyExtensions.cs @@ -0,0 +1,105 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace PhoenixLib.Events.Internal +{ + internal static class AssemblyExtensions + { + internal static bool IsSubclassOfRawGeneric(Type generic, Type toCheck) + { + if (generic == toCheck) + { + return false; + } + + while (toCheck != null && toCheck != typeof(object)) + { + Type cur = toCheck.IsGenericType ? toCheck.GetGenericTypeDefinition() : toCheck; + if (generic == cur) + { + return true; + } + + toCheck = toCheck.BaseType; + } + + return false; + } + + internal static Type[] GetTypesImplementingInterface(this Assembly assembly, params Type[] types) + { + var list = new List(); + foreach (Type type in types) + { + list.AddRange(assembly.GetTypesImplementingInterface(type)); + } + + return list.ToArray(); + } + + internal static Type[] GetTypesImplementingInterface(this Assembly assembly) => assembly.GetTypesImplementingInterface(typeof(T)); + + internal static Type[] GetTypesImplementingInterface(this Assembly assembly, Type type) + { + return assembly.GetTypes().Where(s => s.ImplementsInterface(type)).ToArray(); + } + + internal static bool ImplementsInterface(this Type type) => type.IsAssignableFrom(typeof(T)); + + + internal static bool ImplementsInterface(this Type type, Type interfaceType) + { + if (interfaceType.IsGenericType) + { + return type.IsAssignableToGenericType(interfaceType); + } + + return interfaceType.IsAssignableFrom(type); + } + + internal static bool IsAssignableToGenericType(this Type givenType, Type genericType) + { + Type[] interfaceTypes = givenType.GetInterfaces(); + + if (interfaceTypes.Any(it => it.IsGenericType && it.GetGenericTypeDefinition() == genericType)) + { + return true; + } + + if (givenType.IsGenericType && givenType.GetGenericTypeDefinition() == genericType) + { + return true; + } + + Type baseType = givenType.BaseType; + return baseType != null && IsAssignableToGenericType(baseType, genericType); + } + + internal static Type[] GetTypesImplementingGenericClass(this Assembly assembly, params Type[] types) + { + var list = new List(); + foreach (Type type in types) + { + list.AddRange(assembly.GetTypesImplementingGenericClass(type)); + } + + return list.ToArray(); + } + + internal static Type[] GetTypesImplementingGenericClass(this Assembly assembly, Type type) + { + return assembly.GetTypes().Where(s => IsSubclassOfRawGeneric(type, s)).ToArray(); + } + + internal static Type[] GetTypesDerivedFrom(this Assembly assembly) + { + return assembly.GetTypes().Where(s => s.IsSubclassOf(typeof(T))).ToArray(); + } + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Events/PhoenixLib.Events.csproj b/srcs/PhoenixLib.Events/PhoenixLib.Events.csproj new file mode 100644 index 0000000..302c37e --- /dev/null +++ b/srcs/PhoenixLib.Events/PhoenixLib.Events.csproj @@ -0,0 +1,15 @@ + + + + net5.0 + + + + + + + + + + + diff --git a/srcs/PhoenixLib.Events/ServiceProviderServiceExtensions.cs b/srcs/PhoenixLib.Events/ServiceProviderServiceExtensions.cs new file mode 100644 index 0000000..d46b0f5 --- /dev/null +++ b/srcs/PhoenixLib.Events/ServiceProviderServiceExtensions.cs @@ -0,0 +1,76 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Generic; + +namespace PhoenixLib.Events +{ + /// + /// Extension methods for getting services from an . + /// + internal static class ServiceProviderServiceExtensions + { + /// + /// Get service of type from the . + /// + /// The to retrieve the service object from. + /// An object that specifies the type of service object to get. + /// A service object of type . + /// There is no service of type . + internal static object GetRequiredService(this IServiceProvider provider, Type serviceType) + { + if (provider == null) + { + throw new ArgumentNullException(nameof(provider)); + } + + if (serviceType == null) + { + throw new ArgumentNullException(nameof(serviceType)); + } + + object service = provider.GetService(serviceType); + if (service == null) + { + throw new InvalidOperationException(nameof(serviceType)); + } + + return service; + } + + /// + /// Get service of type from the . + /// + /// The type of service object to get. + /// The to retrieve the service object from. + /// A service object of type . + /// There is no service of type . + internal static T GetRequiredService(this IServiceProvider provider) + { + if (provider == null) + { + throw new ArgumentNullException(nameof(provider)); + } + + return (T)provider.GetRequiredService(typeof(T)); + } + + /// + /// Get an enumeration of services of type from the . + /// + /// The type of service object to get. + /// The to retrieve the services from. + /// An enumeration of services of type . + internal static IEnumerable GetServices(this IServiceProvider provider) + { + if (provider == null) + { + throw new ArgumentNullException(nameof(provider)); + } + + return provider.GetRequiredService>(); + } + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Extensions/AssemblyExtensions.cs b/srcs/PhoenixLib.Extensions/AssemblyExtensions.cs new file mode 100644 index 0000000..c5c2065 --- /dev/null +++ b/srcs/PhoenixLib.Extensions/AssemblyExtensions.cs @@ -0,0 +1,105 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace PhoenixLib.Extensions +{ + public static class AssemblyExtensions + { + public static bool IsSubclassOfRawGeneric(Type generic, Type toCheck) + { + if (generic == toCheck) + { + return false; + } + + while (toCheck != null && toCheck != typeof(object)) + { + Type cur = toCheck.IsGenericType ? toCheck.GetGenericTypeDefinition() : toCheck; + if (generic == cur) + { + return true; + } + + toCheck = toCheck.BaseType; + } + + return false; + } + + public static Type[] GetTypesImplementingInterface(this Assembly assembly, params Type[] types) + { + var list = new List(); + foreach (Type type in types) + { + list.AddRange(assembly.GetTypesImplementingInterface(type)); + } + + return list.ToArray(); + } + + public static Type[] GetTypesImplementingInterface(this Assembly assembly) => assembly.GetTypesImplementingInterface(typeof(T)); + + public static Type[] GetTypesImplementingInterface(this Assembly assembly, Type type) + { + return assembly.GetTypes().Where(s => s.ImplementsInterface(type)).ToArray(); + } + + public static bool ImplementsInterface(this Type type) => type.IsAssignableFrom(typeof(T)); + + + public static bool ImplementsInterface(this Type type, Type interfaceType) + { + if (interfaceType.IsGenericType) + { + return type.IsAssignableToGenericType(interfaceType); + } + + return interfaceType.IsAssignableFrom(type); + } + + public static bool IsAssignableToGenericType(this Type givenType, Type genericType) + { + Type[] interfaceTypes = givenType.GetInterfaces(); + + if (interfaceTypes.Any(it => it.IsGenericType && it.GetGenericTypeDefinition() == genericType)) + { + return true; + } + + if (givenType.IsGenericType && givenType.GetGenericTypeDefinition() == genericType) + { + return true; + } + + Type baseType = givenType.BaseType; + return baseType != null && IsAssignableToGenericType(baseType, genericType); + } + + public static Type[] GetTypesImplementingGenericClass(this Assembly assembly, params Type[] types) + { + var list = new List(); + foreach (Type type in types) + { + list.AddRange(assembly.GetTypesImplementingGenericClass(type)); + } + + return list.ToArray(); + } + + public static Type[] GetTypesImplementingGenericClass(this Assembly assembly, Type type) + { + return assembly.GetTypes().Where(s => IsSubclassOfRawGeneric(type, s)).ToArray(); + } + + public static Type[] GetTypesDerivedFrom(this Assembly assembly) + { + return assembly.GetTypes().Where(s => s.IsSubclassOf(typeof(T))).ToArray(); + } + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Extensions/ByteArrayExtensions.cs b/srcs/PhoenixLib.Extensions/ByteArrayExtensions.cs new file mode 100644 index 0000000..2e61846 --- /dev/null +++ b/srcs/PhoenixLib.Extensions/ByteArrayExtensions.cs @@ -0,0 +1,19 @@ +namespace PhoenixLib.Extensions +{ + public static class ByteArrayExtensions + { + public static string ToHexString(this byte[] byteArray) + { + char[] c = new char[byteArray.Length * 2]; + for (int i = 0; i < byteArray.Length; ++i) + { + byte b = (byte)(byteArray[i] >> 4); + c[i * 2] = (char)(b > 9 ? b + 0x37 : b + 0x30); + b = (byte)(byteArray[i] & 0xF); + c[i * 2 + 1] = (char)(b > 9 ? b + 0x37 : b + 0x30); + } + + return new string(c); + } + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Extensions/PhoenixLib.Extensions.csproj b/srcs/PhoenixLib.Extensions/PhoenixLib.Extensions.csproj new file mode 100644 index 0000000..7339b18 --- /dev/null +++ b/srcs/PhoenixLib.Extensions/PhoenixLib.Extensions.csproj @@ -0,0 +1,9 @@ + + + + net5.0 + PhoenixLib.Extensions + + + + diff --git a/srcs/PhoenixLib.Extensions/StringExtensions.cs b/srcs/PhoenixLib.Extensions/StringExtensions.cs new file mode 100644 index 0000000..597e881 --- /dev/null +++ b/srcs/PhoenixLib.Extensions/StringExtensions.cs @@ -0,0 +1,19 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Linq; +using System.Security.Cryptography; +using System.Text; + +namespace PhoenixLib.Extensions +{ + public static class StringExtensions + { + public static string ToSha512(this string str) + { + using var hash = SHA512.Create(); + return string.Concat(hash.ComputeHash(Encoding.UTF8.GetBytes(str)).Select(item => item.ToString("x2"))); + } + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Logging/ILogger.cs b/srcs/PhoenixLib.Logging/ILogger.cs new file mode 100644 index 0000000..2ed577e --- /dev/null +++ b/srcs/PhoenixLib.Logging/ILogger.cs @@ -0,0 +1,28 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; + +namespace PhoenixLib.Logging +{ + public interface ILogger + { + void Debug(string msg); + void Debug(string msg, Exception ex); + void DebugFormat(string msg, params object[] objs); + + void Info(string msg); + void Info(string msg, Exception ex); + void InfoFormat(string msg, params object[] objs); + + void Warn(string msg); + void Warn(string msg, Exception ex); + void WarnFormat(string msg, params object[] objs); + + void Error(string msg, Exception ex); + void ErrorFormat(string msg, Exception ex, params object[] objs); + + void Fatal(string msg, Exception ex); + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Logging/Log.cs b/srcs/PhoenixLib.Logging/Log.cs new file mode 100644 index 0000000..52fbe60 --- /dev/null +++ b/srcs/PhoenixLib.Logging/Log.cs @@ -0,0 +1,70 @@ +using System; + +namespace PhoenixLib.Logging +{ + public static class Log + { + private static readonly ILogger _logger = new SerilogLogger(); + + public static void Debug(string msg) + { + _logger.Debug(msg); + } + + public static void Debug(string msg, Exception ex) + { + _logger.Debug(msg, ex); + } + + public static void Debug(string msg, params object[] objs) + { + _logger.DebugFormat(msg, objs); + } + + public static void Info(string msg) + { + _logger.Info(msg); + } + + public static void Info(string msg, Exception ex) + { + _logger.Info(msg, ex); + } + + public static void Info(string msg, params object[] objs) + { + _logger.InfoFormat(msg, objs); + } + + public static void Warn(string msg) + { + _logger.Warn(msg); + } + + + public static void Warn(string msg, Exception ex) + { + _logger.Warn(msg, ex); + } + + public static void WarnFormat(string msg, params object[] objs) + { + _logger.WarnFormat(msg, objs); + } + + public static void Error(string msg, Exception ex) + { + _logger.Error(msg, ex); + } + + public static void ErrorFormat(string msg, Exception ex, params object[] objs) + { + _logger.ErrorFormat(msg, ex, objs); + } + + public static void Fatal(string msg, Exception ex) + { + _logger.Fatal(msg, ex); + } + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Logging/LoggingExtensions.cs b/srcs/PhoenixLib.Logging/LoggingExtensions.cs new file mode 100644 index 0000000..086e577 --- /dev/null +++ b/srcs/PhoenixLib.Logging/LoggingExtensions.cs @@ -0,0 +1,28 @@ +// WingsEmu +// +// Developed by NosWings Team + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Serilog; + +namespace PhoenixLib.Logging +{ + public static class LoggingExtensions + { + public static void AddPhoenixLogging(this IServiceCollection services, LogLevel logLevel = LogLevel.Warning) + { + services.AddLogging(builder => + { + builder.AddPhoenixLogging(); + builder.AddFilter("Microsoft", LogLevel.Warning); + builder.AddFilter("Microsoft.EntityFrameworkCore.Database.Command", LogLevel.Warning); + }); + } + + public static void AddPhoenixLogging(this ILoggingBuilder logging, LogLevel logLevel = LogLevel.Debug) + { + logging.AddSerilog(SerilogLogger.CreateLogger(logLevel), true); + } + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Logging/PhoenixLib.Logging.csproj b/srcs/PhoenixLib.Logging/PhoenixLib.Logging.csproj new file mode 100644 index 0000000..da4ac17 --- /dev/null +++ b/srcs/PhoenixLib.Logging/PhoenixLib.Logging.csproj @@ -0,0 +1,16 @@ + + + + net5.0 + PhoenixLib.Logging + + + + + + + + + + + diff --git a/srcs/PhoenixLib.Logging/SerilogLogger.cs b/srcs/PhoenixLib.Logging/SerilogLogger.cs new file mode 100644 index 0000000..b4ac18b --- /dev/null +++ b/srcs/PhoenixLib.Logging/SerilogLogger.cs @@ -0,0 +1,128 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.ComponentModel; +using Microsoft.Extensions.Logging; +using Serilog; +using Serilog.Events; +using Serilog.Sinks.SystemConsole.Themes; + +namespace PhoenixLib.Logging +{ + public class SerilogLogger : ILogger + { + private readonly Serilog.ILogger _logger; + + public SerilogLogger() => _logger = CreateLogger(LogLevel.Information); + + public void Debug(string msg) + { + _logger.Debug(msg); + } + + public void Debug(string msg, Exception ex) + { + _logger.Debug(msg, ex); + } + + public void DebugFormat(string msg, params object[] objs) + { + _logger.Debug(msg, objs); + } + + public void Info(string msg) + { + _logger.Information(msg); + } + + public void Info(string msg, Exception ex) + { + _logger.Information(msg, ex); + } + + public void InfoFormat(string msg, params object[] objs) + { + _logger.Information(msg, objs); + } + + public void Warn(string msg) + { + _logger.Warning(msg); + } + + public void Warn(string msg, Exception ex) + { + _logger.Warning(msg, ex); + } + + public void WarnFormat(string msg, params object[] objs) + { + _logger.Warning(msg, objs); + } + + public void Error(string msg, Exception ex) + { + _logger.Error(ex, msg); + } + + public void ErrorFormat(string msg, Exception ex, params object[] objs) + { + _logger.Error(ex, msg, objs); + } + + public void Fatal(string msg, Exception ex) + { + _logger.Fatal(ex, msg); + } + + internal static Serilog.ILogger CreateLogger(LogLevel logEventLevel = LogLevel.Debug) + { + LoggerConfiguration config = new LoggerConfiguration() + .Enrich.WithThreadId() + .Enrich.FromLogContext() + .WriteTo.Console(theme: AnsiConsoleTheme.Code, + outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff}][{Level:u4}][Thread:{ThreadId}] {Message:lj} {NewLine}{Exception}"); + + string? logLevel = Environment.GetEnvironmentVariable("PHOENIX_LOG_LEVEL"); + + if (!string.IsNullOrEmpty(logLevel)) + { + config.MinimumLevel.Is(logLevel.ToUpper() switch + { + "DEBUG" => LogEventLevel.Debug, + "INFO" => LogEventLevel.Information, + "INFORMATION" => LogEventLevel.Information, + "WARN" => LogEventLevel.Warning, + "WARNING" => LogEventLevel.Warning, + "FATAL" => LogEventLevel.Fatal, + "ERROR" => LogEventLevel.Error, + _ => throw new InvalidEnumArgumentException("PHOENIX_LOG_LEVEL authorized : Debug / Info / Warning / Error / Fatal") + }); + } + else + { + config.MinimumLevel.Is(logEventLevel switch + { + LogLevel.Trace => LogEventLevel.Verbose, + LogLevel.Debug => LogEventLevel.Debug, + LogLevel.Information => LogEventLevel.Information, + LogLevel.Warning => LogEventLevel.Warning, + LogLevel.Error => LogEventLevel.Error, + LogLevel.Critical => LogEventLevel.Error, + LogLevel.None => LogEventLevel.Error, + _ => throw new ArgumentOutOfRangeException(nameof(logEventLevel), logEventLevel, null) + }); + } + + return config.CreateLogger(); + } + } + + public static class EnvironmentExtensions + { + public static bool IsFeatureActivated(string envVarName, bool defaultActivationState = false) => + bool.TryParse(Environment.GetEnvironmentVariable(envVarName) ?? defaultActivationState.ToString(), out bool isActivated) && isActivated; + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Messaging/Extensions/DependencyInjectionExtensions.cs b/srcs/PhoenixLib.Messaging/Extensions/DependencyInjectionExtensions.cs new file mode 100644 index 0000000..a0cf9cc --- /dev/null +++ b/srcs/PhoenixLib.Messaging/Extensions/DependencyInjectionExtensions.cs @@ -0,0 +1,69 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using PhoenixLib.ServiceBus.Internal.Routing; +using PhoenixLib.ServiceBus.MQTT; +using PhoenixLib.ServiceBus.Protocol; +using PhoenixLib.ServiceBus.Routing; + +namespace PhoenixLib.ServiceBus.Extensions +{ + public static class DependencyInjectionExtensions + { + internal static string ToLowerPure(this string str) + { + return string.Concat(str.Select(s => !char.IsLetterOrDigit(s) ? '/' : char.IsUpper(s) ? char.ToLower(s) : s)); + } + + + public static void AddMqttConfigurationFromEnv(this IServiceCollection services) + { + services.TryAddSingleton(s => new MqttConfiguration( + Environment.GetEnvironmentVariable("MQTT_BROKER_ADDRESS") ?? "localhost", + Environment.GetEnvironmentVariable("MQTT_BROKER_CLIENT_NAME") ?? "client-" + Guid.NewGuid(), + Convert.ToInt32(Environment.GetEnvironmentVariable("MQTT_BROKER_PORT") ?? "1883") + )); + } + + internal static void AddMessageService(this IServiceCollection services) + { + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + } + + internal static MessageTypeAttribute GetMessageAttributes() => typeof(T).GetMessageAttributes(); + + internal static void RegisterMessage(this IServiceCollection services) where T : IMessage + { + GetMessageAttributes(); + services.AddSingleton>(); + } + + public static void AddMessagePublisher(this IServiceCollection services) where T : IMessage + { + services.AddMessageService(); + + // adds the event publisher + services.TryAddSingleton, GenericMessagePublisher>(); + } + + public static void AddMessageSubscriber(this IServiceCollection services) + where TMessage : class, IMessage + where TMessageConsumer : class, IMessageConsumer + { + services.AddMessageService(); + services.RegisterMessage(); + + // Message -> event pipeline forwarder + services.AddSingleton, TMessageConsumer>(); + } + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Messaging/GenericSubscribedMessage.cs b/srcs/PhoenixLib.Messaging/GenericSubscribedMessage.cs new file mode 100644 index 0000000..aca18b0 --- /dev/null +++ b/srcs/PhoenixLib.Messaging/GenericSubscribedMessage.cs @@ -0,0 +1,10 @@ +using System; + +namespace PhoenixLib.ServiceBus +{ + internal class GenericSubscribedMessage : ISubscribedMessage + where T : IMessage + { + public Type Type => typeof(T); + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Messaging/IMessage.cs b/srcs/PhoenixLib.Messaging/IMessage.cs new file mode 100644 index 0000000..381c11d --- /dev/null +++ b/srcs/PhoenixLib.Messaging/IMessage.cs @@ -0,0 +1,10 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace PhoenixLib.ServiceBus +{ + public interface IMessage + { + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Messaging/IMessageConsumer.cs b/srcs/PhoenixLib.Messaging/IMessageConsumer.cs new file mode 100644 index 0000000..44dd003 --- /dev/null +++ b/srcs/PhoenixLib.Messaging/IMessageConsumer.cs @@ -0,0 +1,10 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace PhoenixLib.ServiceBus +{ + public interface IMessageConsumer + { + Task HandleAsync(T notification, CancellationToken token); + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Messaging/IMessagePublisher.cs b/srcs/PhoenixLib.Messaging/IMessagePublisher.cs new file mode 100644 index 0000000..10970e4 --- /dev/null +++ b/srcs/PhoenixLib.Messaging/IMessagePublisher.cs @@ -0,0 +1,17 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace PhoenixLib.ServiceBus +{ + public interface IMessagePublisher + where T : IMessage + { + /// + /// Publishes the given event + /// + /// + /// + /// + Task PublishAsync(T notification, CancellationToken token = default); + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Messaging/IMessagingService.cs b/srcs/PhoenixLib.Messaging/IMessagingService.cs new file mode 100644 index 0000000..1381b56 --- /dev/null +++ b/srcs/PhoenixLib.Messaging/IMessagingService.cs @@ -0,0 +1,18 @@ +using System; +using System.Threading.Tasks; + +namespace PhoenixLib.ServiceBus.MQTT +{ + public interface IMessagingService : IAsyncDisposable + { + /// + /// Should not be exposed but permits to send IMessage on the MessageQueue + /// + /// + /// + /// + Task SendAsync(T eventToSend) where T : IMessage; + + Task StartAsync(); + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Messaging/Internal/GenericMessagePublisher.cs b/srcs/PhoenixLib.Messaging/Internal/GenericMessagePublisher.cs new file mode 100644 index 0000000..a382315 --- /dev/null +++ b/srcs/PhoenixLib.Messaging/Internal/GenericMessagePublisher.cs @@ -0,0 +1,23 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus.MQTT; + +namespace PhoenixLib.ServiceBus +{ + internal sealed class GenericMessagePublisher : IMessagePublisher where T : IMessage + { + private readonly IMessagingService _publisher; + + public GenericMessagePublisher(IMessagingService publisher) => _publisher = publisher; + + public async Task PublishAsync(T notification, CancellationToken token = default) + { + if (token.IsCancellationRequested) + { + return; + } + + await _publisher.SendAsync(notification); + } + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Messaging/Internal/IServiceBusInstance.cs b/srcs/PhoenixLib.Messaging/Internal/IServiceBusInstance.cs new file mode 100644 index 0000000..37ca503 --- /dev/null +++ b/srcs/PhoenixLib.Messaging/Internal/IServiceBusInstance.cs @@ -0,0 +1,9 @@ +using System; + +namespace PhoenixLib.ServiceBus +{ + public interface IServiceBusInstance + { + Guid Id { get; } + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Messaging/Internal/MQTT/CloudEventsJsonMessageSerializer.cs b/srcs/PhoenixLib.Messaging/Internal/MQTT/CloudEventsJsonMessageSerializer.cs new file mode 100644 index 0000000..675a9b8 --- /dev/null +++ b/srcs/PhoenixLib.Messaging/Internal/MQTT/CloudEventsJsonMessageSerializer.cs @@ -0,0 +1,73 @@ +using System; +using System.Net.Mime; +using CloudNative.CloudEvents; +using CloudNative.CloudEvents.Mqtt; +using MQTTnet; +using MQTTnet.Protocol; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +using PhoenixLib.Logging; +using PhoenixLib.ServiceBus.Routing; + +namespace PhoenixLib.ServiceBus.Protocol +{ + internal class CloudEventsJsonMessageSerializer : IMessageSerializer + { + private static readonly JsonSerializerSettings _settings = new() + { + NullValueHandling = NullValueHandling.Ignore, + ContractResolver = new CamelCasePropertyNamesContractResolver() + }; + + private readonly IServiceBusInstance _busInstance; + private readonly IMessageRouter _messageRouter; + + public CloudEventsJsonMessageSerializer(IServiceBusInstance busInstance, IMessageRouter messageRouter) + { + _busInstance = busInstance; + _messageRouter = messageRouter; + } + + public MqttApplicationMessage ToMessage(T packet) where T : IMessage + { + IRoutingInformation routingInfos = _messageRouter.GetRoutingInformation(); + MqttApplicationMessage tmp = Create(routingInfos, packet, _busInstance.Id.ToString()); + return tmp; + } + + public (object obj, Type objType) FromMessage(MqttApplicationMessage message) + { + var container = message.ToCloudEvent(new JsonEventFormatter()); + if (container.Source.OriginalString.Contains(_busInstance.Id.ToString())) + { + Log.Debug("[SERVICE_BUS][SUBSCRIBER] Message received from myself"); + // should take a look to broker's ACL + // https://stackoverflow.com/questions/59565487/mqtt-message-subscription-all-except-me + return (null, null); + } + + if (!(container.Data is string eventContent)) + { + Log.Debug("container.Data as string == null"); + throw new ArgumentNullException(nameof(container.Data)); + } + + IRoutingInformation routingInformation = _messageRouter.GetRoutingInformation(container.Type); + + + Log.Debug($"[SERVICE_BUS][SUBSCRIBER] Message received from sender : {container.Source} topic {message.Topic}"); + object packet = JsonConvert.DeserializeObject(eventContent, routingInformation.ObjectType); + return (packet, routingInformation.ObjectType); + } + + private static MqttApplicationMessage Create(IRoutingInformation routingInformation, object content, string source) + { + var cloudEvent = new CloudEvent(CloudEventsSpecVersion.V1_0, routingInformation.EventType, new Uri("publisher:" + source), Guid.NewGuid().ToString(), DateTime.UtcNow) + { + DataContentType = new ContentType(MediaTypeNames.Application.Json), + Data = JsonConvert.SerializeObject(content, _settings) + }; + return new MqttCloudEventMessage(cloudEvent, new JsonEventFormatter()) { Topic = routingInformation.Topic, QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce }; + } + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Messaging/Internal/MQTT/IMessageSerializer.cs b/srcs/PhoenixLib.Messaging/Internal/MQTT/IMessageSerializer.cs new file mode 100644 index 0000000..b2c6447 --- /dev/null +++ b/srcs/PhoenixLib.Messaging/Internal/MQTT/IMessageSerializer.cs @@ -0,0 +1,11 @@ +using System; +using MQTTnet; + +namespace PhoenixLib.ServiceBus.Protocol +{ + internal interface IMessageSerializer + { + MqttApplicationMessage ToMessage(T packet) where T : IMessage; + (object obj, Type objType) FromMessage(MqttApplicationMessage messageApplicationMessage); + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Messaging/Internal/MQTT/MqttConfiguration.cs b/srcs/PhoenixLib.Messaging/Internal/MQTT/MqttConfiguration.cs new file mode 100644 index 0000000..7af5cd6 --- /dev/null +++ b/srcs/PhoenixLib.Messaging/Internal/MQTT/MqttConfiguration.cs @@ -0,0 +1,16 @@ +namespace PhoenixLib.ServiceBus.MQTT +{ + public class MqttConfiguration + { + public MqttConfiguration(string address, string clientName, int? port = null) + { + Address = address; + ClientName = clientName; + Port = port; + } + + public string Address { get; } + public int? Port { get; } + public string ClientName { get; } + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Messaging/Internal/MQTT/MqttMessagingService.cs b/srcs/PhoenixLib.Messaging/Internal/MQTT/MqttMessagingService.cs new file mode 100644 index 0000000..1756740 --- /dev/null +++ b/srcs/PhoenixLib.Messaging/Internal/MQTT/MqttMessagingService.cs @@ -0,0 +1,182 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using MQTTnet; +using MQTTnet.Client.Connecting; +using MQTTnet.Client.Options; +using MQTTnet.Diagnostics; +using MQTTnet.Extensions.ManagedClient; +using PhoenixLib.Logging; +using PhoenixLib.ServiceBus.Protocol; +using PhoenixLib.ServiceBus.Routing; + +namespace PhoenixLib.ServiceBus.MQTT +{ + internal sealed class MqttMessagingService : IMessagingService + { + private static readonly MethodInfo HandleMessageMethod = + typeof(MqttMessagingService).GetMethod(nameof(HandleMessageReceived), BindingFlags.Instance | BindingFlags.NonPublic); + + private readonly IManagedMqttClient _client; + private readonly ManagedMqttClientOptions _options; + private readonly IMessageSerializer _packetSerializer; + private readonly IServiceProvider _provider; + private readonly HashSet _queues; + private readonly IMessageRouter _router; + private readonly IServiceBusInstance _serviceBusInstance; + private TaskCompletionSource _clientConnectionReady; + + public MqttMessagingService(MqttConfiguration conf, IMessageRouter router, IServiceProvider provider, IServiceBusInstance serviceBusInstance, IMessageSerializer packetSerializer) + { + _router = router; + _provider = provider; + _serviceBusInstance = serviceBusInstance; + _packetSerializer = packetSerializer; + + + _client = new MqttFactory().CreateManagedMqttClient(new MqttNetLogger(conf.ClientName)); + _queues = new HashSet(); + _options = new ManagedMqttClientOptionsBuilder() + .WithAutoReconnectDelay(TimeSpan.FromSeconds(5)) + .WithClientOptions(new MqttClientOptionsBuilder() + .WithClientId($"{conf.ClientName}-{_serviceBusInstance.Id.ToString()}") + .WithTcpServer(conf.Address, conf.Port) + .Build()) + .Build(); + // event handlers + _client.UseApplicationMessageReceivedHandler(Client_OnMessage); + } + + private bool IsInitialized => _client.IsStarted; + + + public async Task SendAsync(T eventToSend) where T : IMessage + { + if (!IsInitialized) + { + await StartAsync(); + } + + Log.Debug($"[SERVICE_BUS][PUBLISHER] Sending<{typeof(T)}>..."); + await SendAsync(_packetSerializer.ToMessage(eventToSend)); + } + + public async Task StartAsync() + { + if (IsInitialized) + { + return; + } + + Log.Debug("[SERVICE_BUS][SUBSCRIBER] Starting..."); + _client.UseConnectedHandler(new MqttClientConnectedHandlerDelegate(WaitReadyAsync)); + _clientConnectionReady = new TaskCompletionSource(TimeSpan.FromSeconds(10)); + await _client.StartAsync(_options); + await _clientConnectionReady.Task; + if (_clientConnectionReady.Task.IsCanceled) + { + throw new Exception("Could not connect to MQTT broker within 10 seconds"); + } + + Log.Debug("[SERVICE_BUS][SUBSCRIBER] Started !"); + await SubscribeRegisteredEventsAsync(); + } + + public async ValueTask DisposeAsync() => _client.Dispose(); + + private async Task SendAsync(MqttApplicationMessage container) + { + IRoutingInformation infos = GetRoutingInformation(); + await _client.PublishAsync(container); + Log.Debug($"[SERVICE_BUS][PUBLISHER] Message sent from : {_client.Options.ClientOptions.ClientId} topic {infos.Topic}"); + } + + private async Task Client_OnMessage(MqttApplicationMessageReceivedEventArgs mqttEventArgs) + { + (object message, Type objType) = _packetSerializer.FromMessage(mqttEventArgs.ApplicationMessage); + + if (message == null || objType == null) + { + return; + } + + try + { + MethodInfo method = HandleMessageMethod.MakeGenericMethod(objType); + var task = (Task)method.Invoke(this, new[] { message }); + await task; + } + catch (Exception e) + { + Log.Error($"Client_OnMessage<{objType.Name}", e); + throw; + } + } + + private async Task HandleMessageReceived(T message) + { + try + { + IEnumerable> tmp = _provider.GetServices>(); + var cts = new CancellationTokenSource(); + foreach (IMessageConsumer subscriber in tmp) + { + await subscriber.HandleAsync(message, cts.Token); + } + } + catch (Exception e) + { + Log.Error($"HandleMessageReceived<{typeof(T).Name}", e); + throw; + } + } + + private async Task TrySubscribeAsync(IRoutingInformation infos) + { + if (_queues.Contains(infos.Topic)) + { + return; + } + + await _client.SubscribeAsync(infos.Topic); + _queues.Add(infos.Topic); + Log.Debug($"[SERVICE_BUS][SUBSCRIBER] Subscribed to topic : {infos.Topic} with eventType: {infos.EventType}"); + } + + private IRoutingInformation GetRoutingInformation() => GetRoutingInformation(typeof(T)); + + private IRoutingInformation GetRoutingInformation(Type type) + { + IRoutingInformation routingInfos = _router.GetRoutingInformation(type); + if (string.IsNullOrEmpty(routingInfos.Topic)) + { + throw new ArgumentException("routing information couldn't be retrieved"); + } + + return routingInfos; + } + + + private async Task SubscribeRegisteredEventsAsync() + { + IEnumerable subs = _provider.GetServices(); + + foreach (ISubscribedMessage sub in subs) + { + IRoutingInformation routingInfo = GetRoutingInformation(sub.Type); + await TrySubscribeAsync(routingInfo); + } + } + + private async Task WaitReadyAsync(MqttClientConnectedEventArgs onConnectedArgs) + { + if (onConnectedArgs.AuthenticateResult.ResultCode == MqttClientConnectResultCode.Success) + { + _clientConnectionReady.SetResult(true); + } + } + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Messaging/Internal/Routing/IMessageRouter.cs b/srcs/PhoenixLib.Messaging/Internal/Routing/IMessageRouter.cs new file mode 100644 index 0000000..3962580 --- /dev/null +++ b/srcs/PhoenixLib.Messaging/Internal/Routing/IMessageRouter.cs @@ -0,0 +1,28 @@ +using System; + +namespace PhoenixLib.ServiceBus.Routing +{ + public interface IMessageRouter + { + /// + /// Get the routing informations from the router + /// + /// + /// + IRoutingInformation GetRoutingInformation(); + + /// + /// Used for runtime possibilities + /// + /// + /// + IRoutingInformation GetRoutingInformation(Type type); + + /// + /// Gets the routing information + /// + /// + /// + IRoutingInformation GetRoutingInformation(string type); + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Messaging/Internal/Routing/IRoutingInformation.cs b/srcs/PhoenixLib.Messaging/Internal/Routing/IRoutingInformation.cs new file mode 100644 index 0000000..f011e90 --- /dev/null +++ b/srcs/PhoenixLib.Messaging/Internal/Routing/IRoutingInformation.cs @@ -0,0 +1,11 @@ +using System; + +namespace PhoenixLib.ServiceBus.Routing +{ + public interface IRoutingInformation + { + Type ObjectType { get; } + string Topic { get; } + string EventType { get; } + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Messaging/Internal/Routing/IRoutingInformationFactory.cs b/srcs/PhoenixLib.Messaging/Internal/Routing/IRoutingInformationFactory.cs new file mode 100644 index 0000000..a6aa256 --- /dev/null +++ b/srcs/PhoenixLib.Messaging/Internal/Routing/IRoutingInformationFactory.cs @@ -0,0 +1,9 @@ +using System; + +namespace PhoenixLib.ServiceBus.Routing +{ + public interface IRoutingInformationFactory + { + IRoutingInformation Create(Type type, string topic, string eventType); + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Messaging/Internal/Routing/ISubscribedMessage.cs b/srcs/PhoenixLib.Messaging/Internal/Routing/ISubscribedMessage.cs new file mode 100644 index 0000000..82a2df9 --- /dev/null +++ b/srcs/PhoenixLib.Messaging/Internal/Routing/ISubscribedMessage.cs @@ -0,0 +1,13 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; + +namespace PhoenixLib.ServiceBus +{ + internal interface ISubscribedMessage + { + Type Type { get; } + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Messaging/Internal/Routing/MessageExtensions.cs b/srcs/PhoenixLib.Messaging/Internal/Routing/MessageExtensions.cs new file mode 100644 index 0000000..a5f20d7 --- /dev/null +++ b/srcs/PhoenixLib.Messaging/Internal/Routing/MessageExtensions.cs @@ -0,0 +1,24 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Reflection; +using PhoenixLib.ServiceBus.Routing; + +namespace PhoenixLib.ServiceBus.Internal.Routing +{ + internal static class MessageExtensions + { + internal static MessageTypeAttribute GetMessageAttributes(this Type type) + { + MessageTypeAttribute messageTypeAttribute = type.GetCustomAttribute(); + if (messageTypeAttribute == null) + { + throw new ArgumentException($"{type} misses the attribute EventTypeAttribute on the class"); + } + + return messageTypeAttribute; + } + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Messaging/Internal/Routing/MessageRouter.cs b/srcs/PhoenixLib.Messaging/Internal/Routing/MessageRouter.cs new file mode 100644 index 0000000..f620605 --- /dev/null +++ b/srcs/PhoenixLib.Messaging/Internal/Routing/MessageRouter.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using PhoenixLib.Logging; +using PhoenixLib.ServiceBus.Internal.Routing; + +namespace PhoenixLib.ServiceBus.Routing +{ + public class MessageRouter : IMessageRouter + { + private static readonly Dictionary _infos = new(); + private static readonly Dictionary _infosByCloudEventType = new(); + private readonly IRoutingInformationFactory _routingInformationFactory; + + public MessageRouter(IRoutingInformationFactory routingInformationFactory) => + _routingInformationFactory = routingInformationFactory; + + public IRoutingInformation GetRoutingInformation() => GetRoutingInformation(typeof(T)); + + public IRoutingInformation GetRoutingInformation(Type type) => _infos.TryGetValue(type, out IRoutingInformation value) ? value : Register(type); + + public IRoutingInformation GetRoutingInformation(string type) => + _infosByCloudEventType.TryGetValue(type, out IRoutingInformation value) ? value : throw new ArgumentException($"type: {type} is not registered"); + + private IRoutingInformation Register(Type type) + { + Log.Debug($"Register: {type}"); + if (type == null) + { + throw new ArgumentNullException(nameof(type)); + } + + if (type.IsGenericType) + { + throw new ArgumentException("Generics are not yet supported"); + } + + MessageTypeAttribute eventType = type.GetMessageAttributes(); + if (string.IsNullOrEmpty(eventType.EventType)) + { + throw new ArgumentException($"{type} misses the attribute EventTypeAttribute on the class"); + } + + IRoutingInformation routingInfos = _routingInformationFactory.Create(type, eventType.EventType.Replace('.', '/'), eventType.EventType); + Register(type, routingInfos); + return routingInfos; + } + + private static void Register(Type type, IRoutingInformation routingInformation) + { + if (_infos.ContainsKey(type)) + { + return; + } + + _infos.Add(type, routingInformation); + _infosByCloudEventType[routingInformation.EventType] = routingInformation; + } + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Messaging/Internal/Routing/MessageTypeAttribute.cs b/srcs/PhoenixLib.Messaging/Internal/Routing/MessageTypeAttribute.cs new file mode 100644 index 0000000..912a4da --- /dev/null +++ b/srcs/PhoenixLib.Messaging/Internal/Routing/MessageTypeAttribute.cs @@ -0,0 +1,11 @@ +using System; + +namespace PhoenixLib.ServiceBus.Routing +{ + public class MessageTypeAttribute : Attribute + { + public MessageTypeAttribute(string eventType) => EventType = eventType; + + public string EventType { get; set; } + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Messaging/Internal/Routing/RoutingInformation.cs b/srcs/PhoenixLib.Messaging/Internal/Routing/RoutingInformation.cs new file mode 100644 index 0000000..8d58bf4 --- /dev/null +++ b/srcs/PhoenixLib.Messaging/Internal/Routing/RoutingInformation.cs @@ -0,0 +1,18 @@ +using System; + +namespace PhoenixLib.ServiceBus.Routing +{ + public class RoutingInformation : IRoutingInformation + { + public RoutingInformation(Type type, string topic, string eventType) + { + ObjectType = type; + Topic = topic; + EventType = eventType; + } + + public Type ObjectType { get; } + public string Topic { get; } + public string EventType { get; } + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Messaging/Internal/Routing/RoutingInformationFactory.cs b/srcs/PhoenixLib.Messaging/Internal/Routing/RoutingInformationFactory.cs new file mode 100644 index 0000000..bc00d28 --- /dev/null +++ b/srcs/PhoenixLib.Messaging/Internal/Routing/RoutingInformationFactory.cs @@ -0,0 +1,9 @@ +using System; + +namespace PhoenixLib.ServiceBus.Routing +{ + internal class RoutingInformationFactory : IRoutingInformationFactory + { + public IRoutingInformation Create(Type type, string topic, string eventType) => new RoutingInformation(type, topic, eventType); + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Messaging/Internal/ServiceBusInstance.cs b/srcs/PhoenixLib.Messaging/Internal/ServiceBusInstance.cs new file mode 100644 index 0000000..6242853 --- /dev/null +++ b/srcs/PhoenixLib.Messaging/Internal/ServiceBusInstance.cs @@ -0,0 +1,10 @@ +using System; + +namespace PhoenixLib.ServiceBus +{ + internal class ServiceBusInstance : IServiceBusInstance + { + private static readonly Guid _id = Guid.NewGuid(); + public Guid Id => _id; + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Messaging/PhoenixLib.Messaging.csproj b/srcs/PhoenixLib.Messaging/PhoenixLib.Messaging.csproj new file mode 100644 index 0000000..dd84857 --- /dev/null +++ b/srcs/PhoenixLib.Messaging/PhoenixLib.Messaging.csproj @@ -0,0 +1,22 @@ + + + + net5.0 + PhoenixLib.ServiceBus + + + + + + + + + + + + + + + + + diff --git a/srcs/PhoenixLib.Multilanguage/DependencyInjectionExtensions.cs b/srcs/PhoenixLib.Multilanguage/DependencyInjectionExtensions.cs new file mode 100644 index 0000000..b0cb7bb --- /dev/null +++ b/srcs/PhoenixLib.Multilanguage/DependencyInjectionExtensions.cs @@ -0,0 +1,20 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using Microsoft.Extensions.DependencyInjection; +using PhoenixLib.DAL.Redis; + +namespace PhoenixLib.MultiLanguage +{ + public static class DependencyInjectionExtensions + { + public static void TryAddEnumBasedMultilanguageService(this IServiceCollection services) + where T : struct, Enum + { + services.TryAddConnectionMultiplexerFromEnv(); + services.AddSingleton, GenericMultilanguageService>(); + } + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Multilanguage/GenericMultilanguageService.cs b/srcs/PhoenixLib.Multilanguage/GenericMultilanguageService.cs new file mode 100644 index 0000000..73db967 --- /dev/null +++ b/srcs/PhoenixLib.Multilanguage/GenericMultilanguageService.cs @@ -0,0 +1,104 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Foundatio.Caching; +using Foundatio.Serializer; +using StackExchange.Redis; + +namespace PhoenixLib.MultiLanguage +{ + public class GenericMultilanguageService : IEnumBasedLanguageService where T : struct, Enum + { + private readonly ICacheClient _cacheClient; + private readonly string _dataPrefixString; + private readonly string _keySetKeyString; + + public GenericMultilanguageService(IConnectionMultiplexer conf) + { + _dataPrefixString = $"data:{typeof(T).Name.ToLower()}:"; + _keySetKeyString = $"keys:{typeof(T).Name.ToLower()}:"; + _cacheClient = new RedisCacheClient(new RedisCacheClientOptions + { + ConnectionMultiplexer = conf, + Serializer = new JsonNetSerializer() + }); + } + + public string GetLanguage(T key, RegionLanguageType lang) => GetLanguageAsync(key, lang).ConfigureAwait(false).GetAwaiter().GetResult(); + + public async Task GetLanguageAsync(T key, RegionLanguageType lang) + { + CacheValue value = await _cacheClient.GetAsync(ToKey(key, lang)); + if (value.HasValue) + { + return value.Value; + } + + string newLanguageValue = key.ToString().ToUpper(); + await SetLanguageAsync(key, newLanguageValue, lang); + return newLanguageValue; + } + + public async Task> GetLanguageAsync(ICollection key, RegionLanguageType type) + { + CacheValue> set = await _cacheClient.GetListAsync(KeySetKeyString(type)).ConfigureAwait(false); + IDictionary> dico = await _cacheClient.GetAllAsync(set.Value).ConfigureAwait(false); + return dico.ToDictionary(s => Enum.Parse(s.Key.Substring(s.Key.LastIndexOf(':') + 1)), s => s.Value.Value); + } + + public void SetLanguage(T key, string value, RegionLanguageType lang) + { + SetLanguageAsync(key, value, lang).ConfigureAwait(false).GetAwaiter().GetResult(); + } + + public async Task SetLanguageAsync(T key, string value, RegionLanguageType lang) + { + await RegisterGameDialogKeyAsync(new[] { key }, lang).ConfigureAwait(false); + await _cacheClient.SetAsync(ToKey(key, lang), value).ConfigureAwait(false); + } + + public async Task SetLanguageAsync(IDictionary keyValues, RegionLanguageType type) + { + var dico = keyValues.ToDictionary(s => ToKey(s.Key, type), s => s.Value); + await RegisterGameDialogKeyAsync(keyValues.Keys.ToArray(), type); + await _cacheClient.SetAllAsync(dico); + } + + private static string LangSuffix(RegionLanguageType lang) => lang.ToString().ToLower(); + private string KeySetKeyString(RegionLanguageType lang) => _keySetKeyString + LangSuffix(lang); + private string ToKey(T id, RegionLanguageType lang) => _dataPrefixString + LangSuffix(lang) + ':' + id.ToString().ToUpper(); + private async Task>> KeySet(RegionLanguageType lang) => await _cacheClient.GetListAsync(KeySetKeyString(lang)); + private async Task> GetAllStringKeysAsync(RegionLanguageType lang) => (await KeySet(lang).ConfigureAwait(false)).Value; + + private async Task RegisterGameDialogKeyAsync(IEnumerable keys, RegionLanguageType lang) + { + await _cacheClient.ListAddAsync(_keySetKeyString + lang.ToString().ToLower(), keys.Select(s => ToKey(s, lang))); + } + + public void SetLanguage(Dictionary keyValues, RegionLanguageType lang) + { + SetLanguageAsync(keyValues, lang).ConfigureAwait(false).GetAwaiter().GetResult(); + } + + public async Task SetLanguageAsync(Dictionary keyValues, RegionLanguageType lang) + { + var dico = keyValues.ToDictionary(s => ToKey(s.Key, lang), s => s.Value); + await RegisterGameDialogKeyAsync(keyValues.Keys.ToArray(), lang); + await _cacheClient.SetAllAsync(dico); + } + + + public Dictionary GetLanguages(RegionLanguageType lang) + { + CacheValue> set = _cacheClient.GetListAsync(KeySetKeyString(lang)).ConfigureAwait(false).GetAwaiter().GetResult(); + IDictionary> dico = _cacheClient.GetAllAsync(set.Value).ConfigureAwait(false).GetAwaiter().GetResult(); + + return dico.ToDictionary(s => s.Key, s => s.Value.Value); + } + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Multilanguage/IEnumBasedLanguageService.cs b/srcs/PhoenixLib.Multilanguage/IEnumBasedLanguageService.cs new file mode 100644 index 0000000..34683c6 --- /dev/null +++ b/srcs/PhoenixLib.Multilanguage/IEnumBasedLanguageService.cs @@ -0,0 +1,16 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; + +namespace PhoenixLib.MultiLanguage +{ + /// + /// Permits multi language Key/value based on an enum + /// + public interface IEnumBasedLanguageService : ILanguageService + where T : Enum + { + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Multilanguage/ILanguageService.cs b/srcs/PhoenixLib.Multilanguage/ILanguageService.cs new file mode 100644 index 0000000..06c45f5 --- /dev/null +++ b/srcs/PhoenixLib.Multilanguage/ILanguageService.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace PhoenixLib.MultiLanguage +{ + public interface ILanguageService where T : notnull + { + /// + /// Will return the string by its key & region + /// Used for ChickenAPI mainly + /// + /// + /// + /// + string GetLanguage(T key, RegionLanguageType type); + + /// + /// Will return the string by its key & region + /// + /// + /// + /// + Task GetLanguageAsync(T key, RegionLanguageType type); + + /// + /// Will return the string by its key & region + /// + /// + /// + /// + Task> GetLanguageAsync(ICollection key, RegionLanguageType type); + + /// + /// Will register the key and value by its region type + /// + /// + /// + /// + void SetLanguage(T key, string value, RegionLanguageType type); + + /// + /// Will register the key and value by its region type + /// + /// + /// + /// + Task SetLanguageAsync(T key, string value, RegionLanguageType type); + + /// + /// Will register the key and value by its region type + /// + /// + /// + Task SetLanguageAsync(IDictionary keyValues, RegionLanguageType type); + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Multilanguage/IStringBasedLanguageService.cs b/srcs/PhoenixLib.Multilanguage/IStringBasedLanguageService.cs new file mode 100644 index 0000000..948b6ce --- /dev/null +++ b/srcs/PhoenixLib.Multilanguage/IStringBasedLanguageService.cs @@ -0,0 +1,10 @@ +namespace PhoenixLib.MultiLanguage +{ + /// + /// Permits multi language key/value based on a string key + /// + /// + public interface IStringBasedLanguageService : ILanguageService + { + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Multilanguage/PhoenixLib.Multilanguage.csproj b/srcs/PhoenixLib.Multilanguage/PhoenixLib.Multilanguage.csproj new file mode 100644 index 0000000..1cfa5d0 --- /dev/null +++ b/srcs/PhoenixLib.Multilanguage/PhoenixLib.Multilanguage.csproj @@ -0,0 +1,19 @@ + + + + net5.0 + PhoenixLib.MultiLanguage + + + + + + + + + + + + + + diff --git a/srcs/PhoenixLib.Multilanguage/RegionLanguageType.cs b/srcs/PhoenixLib.Multilanguage/RegionLanguageType.cs new file mode 100644 index 0000000..1a55c9d --- /dev/null +++ b/srcs/PhoenixLib.Multilanguage/RegionLanguageType.cs @@ -0,0 +1,19 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace PhoenixLib.MultiLanguage +{ + public enum RegionLanguageType + { + EN = 0, + DE = 1, + FR = 2, + IT = 3, + PL = 4, + ES = 5, + CZ = 6, + RU = 7, + TR = 8 + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Scheduler.ReactiveX/FuncTaskWorkerDisposable.cs b/srcs/PhoenixLib.Scheduler.ReactiveX/FuncTaskWorkerDisposable.cs new file mode 100644 index 0000000..281bc26 --- /dev/null +++ b/srcs/PhoenixLib.Scheduler.ReactiveX/FuncTaskWorkerDisposable.cs @@ -0,0 +1,35 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace PhoenixLib.Scheduler.ReactiveX +{ + public class FuncTaskWorkerDisposable : IDisposable + { + private readonly Func _action; + private readonly CancellationTokenSource _cts; + private readonly TimeSpan _interval; + + public FuncTaskWorkerDisposable(TimeSpan interval, Func action) + { + _interval = interval; + _action = action; + _cts = new CancellationTokenSource(); + Work(); + } + + public void Dispose() + { + _cts.Cancel(); + } + + private async Task Work() + { + while (!_cts.IsCancellationRequested) + { + await _action(); + await Task.Delay(_interval); + } + } + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Scheduler.ReactiveX/ObservableCron.cs b/srcs/PhoenixLib.Scheduler.ReactiveX/ObservableCron.cs new file mode 100644 index 0000000..dcce6e9 --- /dev/null +++ b/srcs/PhoenixLib.Scheduler.ReactiveX/ObservableCron.cs @@ -0,0 +1,12 @@ +using System; +using System.Threading.Tasks; + +namespace PhoenixLib.Scheduler.ReactiveX +{ + public class ObservableCron : ICron + { + public IDisposable Schedule(TimeSpan interval, Action callback) => new TaskWorkerDisposable(interval, callback); + + public IDisposable Schedule(TimeSpan interval, Func callback) => new FuncTaskWorkerDisposable(interval, callback); + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Scheduler.ReactiveX/ObservableScheduler.cs b/srcs/PhoenixLib.Scheduler.ReactiveX/ObservableScheduler.cs new file mode 100644 index 0000000..685a705 --- /dev/null +++ b/srcs/PhoenixLib.Scheduler.ReactiveX/ObservableScheduler.cs @@ -0,0 +1,58 @@ +using System; +using System.Reactive.Linq; +using System.Threading.Tasks; +using PhoenixLib.Logging; + +namespace PhoenixLib.Scheduler.ReactiveX +{ + public class ObservableScheduler : IScheduler + { + public IDisposable Schedule(TimeSpan delay, Action tmp) + { + return Observable.Timer(delay).Subscribe(s => + { + try + { + tmp(); + } + catch (Exception e) + { + Log.Error("[SCHEDULER]", e); + throw; + } + }); + } + + public IDisposable Schedule(TimeSpan delay, Func tmp) + { + return Observable.Timer(delay).Subscribe(async s => + { + try + { + await tmp(); + } + catch (Exception e) + { + Log.Error("[SCHEDULER]", e); + throw; + } + }); + } + + public IDisposable Schedule(TimeSpan delay, Action action) + { + return Observable.Timer(delay).Subscribe(async s => + { + try + { + action(null); + } + catch (Exception e) + { + Log.Error("[SCHEDULER]", e); + throw; + } + }); + } + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Scheduler.ReactiveX/PhoenixLib.Scheduler.ReactiveX.csproj b/srcs/PhoenixLib.Scheduler.ReactiveX/PhoenixLib.Scheduler.ReactiveX.csproj new file mode 100644 index 0000000..e2e10d8 --- /dev/null +++ b/srcs/PhoenixLib.Scheduler.ReactiveX/PhoenixLib.Scheduler.ReactiveX.csproj @@ -0,0 +1,19 @@ + + + + net5.0 + PhoenixLib.Scheduler.ReactiveX + + + + + + + + + + + + + + diff --git a/srcs/PhoenixLib.Scheduler.ReactiveX/SchedulerServiceCollectionExtensions.cs b/srcs/PhoenixLib.Scheduler.ReactiveX/SchedulerServiceCollectionExtensions.cs new file mode 100644 index 0000000..cc747eb --- /dev/null +++ b/srcs/PhoenixLib.Scheduler.ReactiveX/SchedulerServiceCollectionExtensions.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace PhoenixLib.Scheduler.ReactiveX +{ + public static class SchedulerServiceCollectionExtensions + { + public static void AddScheduler(this IServiceCollection services) + { + services.AddTransient(); + } + + public static void AddCron(this IServiceCollection services) + { + services.AddTransient(); + } + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Scheduler.ReactiveX/TaskWorkerDisposable.cs b/srcs/PhoenixLib.Scheduler.ReactiveX/TaskWorkerDisposable.cs new file mode 100644 index 0000000..4f99067 --- /dev/null +++ b/srcs/PhoenixLib.Scheduler.ReactiveX/TaskWorkerDisposable.cs @@ -0,0 +1,35 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace PhoenixLib.Scheduler.ReactiveX +{ + public class TaskWorkerDisposable : IDisposable + { + private readonly Action _action; + private readonly CancellationTokenSource _cts; + private readonly TimeSpan _interval; + + public TaskWorkerDisposable(TimeSpan interval, Action action) + { + _interval = interval; + _action = action; + _cts = new CancellationTokenSource(); + Work(); + } + + public void Dispose() + { + _cts.Cancel(); + } + + private async Task Work() + { + while (!_cts.IsCancellationRequested) + { + _action(); + await Task.Delay(_interval); + } + } + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Scheduler/ICron.cs b/srcs/PhoenixLib.Scheduler/ICron.cs new file mode 100644 index 0000000..0e37c10 --- /dev/null +++ b/srcs/PhoenixLib.Scheduler/ICron.cs @@ -0,0 +1,12 @@ +using System; +using System.Threading.Tasks; + +namespace PhoenixLib.Scheduler +{ + public interface ICron + { + IDisposable Schedule(TimeSpan interval, Action callback); + + IDisposable Schedule(TimeSpan interval, Func callback); + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Scheduler/ICronJobPool.cs b/srcs/PhoenixLib.Scheduler/ICronJobPool.cs new file mode 100644 index 0000000..a4f8255 --- /dev/null +++ b/srcs/PhoenixLib.Scheduler/ICronJobPool.cs @@ -0,0 +1,9 @@ +namespace PhoenixLib.Scheduler +{ + /// + /// Cron pools + /// + public interface ICronJobPool : IGenericCronPool + { + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Scheduler/IGenericCronPool.cs b/srcs/PhoenixLib.Scheduler/IGenericCronPool.cs new file mode 100644 index 0000000..f9fe925 --- /dev/null +++ b/srcs/PhoenixLib.Scheduler/IGenericCronPool.cs @@ -0,0 +1,64 @@ +using System; +using System.Linq.Expressions; +using System.Threading.Tasks; + +namespace PhoenixLib.Scheduler +{ + /// + /// This interface represents a cron scheduler. + /// A cron is a recurring job/task. + /// + /// + public interface IGenericCronPool + { + /// + /// Creates a new cron. + /// + /// Method to call when the cron is executed. + /// Time interval between each cron execution. + /// + void New(TId cronId, Expression method, Interval interval); + + /// + /// Creates a new cron. + /// + /// Method to call when the cron is executed. + /// Time interval between each cron execution. + /// + void New(TId cronId, Expression> method, Interval interval); + + + /// + /// Creates a new cron. + /// + /// Method to call when the cron is executed. + /// Time interval between each cron execution. + /// + void New(TId cronId, Expression> method, Interval interval); + + + /// + /// Creates a new cron. + /// + /// Method to call when the cron is executed. + /// Time interval between each cron execution. + /// + void New(TId cronId, Expression>> method, Interval interval); + + /// + /// Removes by id an existing cron. + /// May throw if the given cron id does not correspond to a valid cron. + /// + /// Id of the cron to remove. + void Remove(TId cronId); + + /// + /// Behaves similarly as 'Remove' but with a strictly defined + /// behavior and a performance penalty is the price to pay: + /// it checks if the given cron id is valid before trying to remove + /// the corresponding cron. + /// + /// + void RemoveIfExists(TId cronId); + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Scheduler/IGenericJobPool.cs b/srcs/PhoenixLib.Scheduler/IGenericJobPool.cs new file mode 100644 index 0000000..3e5f2f7 --- /dev/null +++ b/srcs/PhoenixLib.Scheduler/IGenericJobPool.cs @@ -0,0 +1,122 @@ +using System; +using System.Linq.Expressions; +using System.Threading.Tasks; + +namespace PhoenixLib.Scheduler +{ + /// + /// This interface represents a task/job scheduler. + /// + /// Type of the id that will be used to distinguish every job. + public interface IGenericJobPool + { + /// + /// Enqueues a new job. + /// + /// Method to call when the job is executed. + /// + TId Enqueue(Expression method); + + /// + /// Enqueues a new job. + /// + /// Method to call when the job is executed. + /// + TId Enqueue(Expression> method); + + /// + /// Enqueues a new job. + /// + /// + /// Method to call when the job is executed. + /// + TId Enqueue(Expression> method); + + /// + /// Enqueues a new job. + /// + /// + /// Method to call when the job is executed. + /// + TId Enqueue(Expression>> method); + + /// + /// Creates a new job that will be enqueued after a given delay. + /// + /// Method to call when the job is executed. + /// Delay before enqueuing the job. + /// + TId Schedule(Expression method, TimeSpan delay); + + /// + /// Creates a new job that will be enqueued after a given delay. + /// + /// Method to call when the job is executed. + /// Delay before enqueuing the job. + /// + TId Schedule(Expression> method, TimeSpan delay); + + /// + /// Creates a new job that will be enqueued after a given delay. + /// + /// Method to call when the job is executed. + /// Delay before enqueuing the job. + /// + TId Schedule(Expression> method, TimeSpan delay); + + /// + /// Creates a new job that will be enqueued after a given delay. + /// + /// Method to call when the job is executed. + /// Delay before enqueuing the job. + /// + TId Schedule(Expression> method, TimeSpan delay); + + /// + /// Creates a new job that will be enqueued after a given delay. + /// + /// Method to call when the job is executed. + /// Delay before enqueuing the job. + /// + TId Schedule(Expression>> method, TimeSpan delay); + + + /// + /// Creates a new job that will wait for parent job completion before to be enqueued. + /// + /// Job id of the parent. + /// Method to call when the job is executed. + /// + TId ContinueWith(TId parentJobId, Expression method); + + /// + /// Creates a new job that will wait for parent job completion before to be enqueued. + /// + /// Job id of the parent. + /// Method to call when the job is executed. + /// + TId ContinueWith(TId parentJobId, Expression> method); + + /// + /// Creates a new job that will wait for parent job completion before to be enqueued. + /// + /// Job id of the parent. + /// Method to call when the job is executed. + /// + TId ContinueWith(TId parentJobId, Expression> method); + + /// + /// Creates a new job that will wait for parent job completion before to be enqueued. + /// + /// Job id of the parent. + /// Method to call when the job is executed. + /// + TId ContinueWith(TId parentJobId, Expression>> method); + + /// + /// Deletes an existing job. + /// + /// Id of the job. + void Remove(TId jobId); + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Scheduler/IScheduledJob.cs b/srcs/PhoenixLib.Scheduler/IScheduledJob.cs new file mode 100644 index 0000000..44d9ade --- /dev/null +++ b/srcs/PhoenixLib.Scheduler/IScheduledJob.cs @@ -0,0 +1,9 @@ +namespace PhoenixLib.Scheduler +{ + /// + /// Default JobPool + /// + public interface IScheduledJob : IGenericJobPool + { + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Scheduler/IScheduler.cs b/srcs/PhoenixLib.Scheduler/IScheduler.cs new file mode 100644 index 0000000..42bb342 --- /dev/null +++ b/srcs/PhoenixLib.Scheduler/IScheduler.cs @@ -0,0 +1,12 @@ +using System; +using System.Threading.Tasks; + +namespace PhoenixLib.Scheduler +{ + public interface IScheduler + { + IDisposable Schedule(TimeSpan delay, Action action); + IDisposable Schedule(TimeSpan delay, Func action); + IDisposable Schedule(TimeSpan delay, Action action); + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Scheduler/Interval.cs b/srcs/PhoenixLib.Scheduler/Interval.cs new file mode 100644 index 0000000..140fb04 --- /dev/null +++ b/srcs/PhoenixLib.Scheduler/Interval.cs @@ -0,0 +1,55 @@ +using System; + +namespace PhoenixLib.Scheduler +{ + /// + /// This class wraps a time interval. + /// You can use this class to process some recurring operations with a delay. + /// The delay can be represented by this class. + /// + public class Interval + { + /// + /// Minute's value. + /// + public byte Minute { get; private set; } + + /// + /// Hour's value. + /// + public byte Hour { get; private set; } + + /// + /// Specifies how many minute is required by the interval to be completed. + /// + /// An integer withing [0; 59] range + /// + public Interval EveryMinute(byte minute) + { + if (minute > 59) + { + throw new InvalidOperationException("Minute should be between 0~59."); + } + + Minute = minute; + return this; + } + + + /// + /// Specifies how many hours is required by the interval to be completed. + /// + /// An integer withing [0; 23] range + /// + public Interval EveryHour(byte hour) + { + if (hour > 23) + { + throw new InvalidOperationException("Hour should be between 0~23."); + } + + Hour = hour; + return this; + } + } +} \ No newline at end of file diff --git a/srcs/PhoenixLib.Scheduler/PhoenixLib.Scheduler.csproj b/srcs/PhoenixLib.Scheduler/PhoenixLib.Scheduler.csproj new file mode 100644 index 0000000..be21832 --- /dev/null +++ b/srcs/PhoenixLib.Scheduler/PhoenixLib.Scheduler.csproj @@ -0,0 +1,8 @@ + + + + net5.0 + + + + diff --git a/srcs/PhoenixLib.Scheduler/TimePoint.cs b/srcs/PhoenixLib.Scheduler/TimePoint.cs new file mode 100644 index 0000000..ca40555 --- /dev/null +++ b/srcs/PhoenixLib.Scheduler/TimePoint.cs @@ -0,0 +1,54 @@ +using System; + +namespace PhoenixLib.Scheduler +{ + /// + /// Represents a given moment. + /// + public class TimePoint + { + /// + /// The hour value of the given moment. + /// + public byte Hour { get; private set; } + + /// + /// The minute value of the given moment. + /// + public byte Minute { get; private set; } + + /// + /// Specifies the hour of the given moment. + /// + /// The hour between 0 and 23 + /// A reference to the used TimePoint object. + public TimePoint AtHour(byte hour) + { + if (Hour > 23) + { + throw new ArgumentOutOfRangeException(nameof(hour), "Value must be between 0 and 23."); + } + + Hour = hour; + + return this; + } + + /// + /// Specifies the minute of the given moment. + /// + /// The minute between 0 and 59 + /// A reference to the used TimePoint object. + public TimePoint AtMinute(byte minute) + { + if (Minute > 59) + { + throw new ArgumentOutOfRangeException(nameof(minute), "Value must be between 0 and 59"); + } + + Minute = minute; + + return this; + } + } +} \ No newline at end of file diff --git a/srcs/Plugin.RainbowBattle/Command/RainbowBattleCommandModule.cs b/srcs/Plugin.RainbowBattle/Command/RainbowBattleCommandModule.cs new file mode 100644 index 0000000..f121093 --- /dev/null +++ b/srcs/Plugin.RainbowBattle/Command/RainbowBattleCommandModule.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using PhoenixLib.Events; +using Qmmands; +using WingsEmu.Commands.Checks; +using WingsEmu.Commands.Entities; +using WingsEmu.DTOs.Account; +using WingsEmu.Game.Networking; +using WingsEmu.Game.RainbowBattle.Event; + +namespace Plugin.RainbowBattle.Command +{ + [Name("Rainbow Battle")] + [Group("rainbowbattle", "rbb")] + [RequireAuthority(AuthorityType.Owner)] + public class RainbowBattleCommandModule : SaltyModuleBase + { + private readonly IAsyncEventPipeline _asyncEventPipeline; + + public RainbowBattleCommandModule(IAsyncEventPipeline asyncEventPipeline) => _asyncEventPipeline = asyncEventPipeline; + + [Command("start")] + public async Task Start() + { + await Context.Player.EmitEventAsync(new RainbowBattleStartEvent + { + RedTeam = new List + { + Context.Player + }, + BlueTeam = new List() + }); + + return new SaltyCommandResult(true); + } + + [Command("end")] + public async Task End() + { + if (!Context.Player.PlayerEntity.RainbowBattleComponent.IsInRainbowBattle) + { + return new SaltyCommandResult(false); + } + + await _asyncEventPipeline.ProcessEventAsync(new RainbowBattleEndEvent + { + RainbowBattleParty = Context.Player.PlayerEntity.RainbowBattleComponent.RainbowBattleParty + }); + return new SaltyCommandResult(true); + } + + [Command("register")] + public async Task Register() + { + await Context.Player.EmitEventAsync(new RainbowBattleStartRegisterEvent()); + + return new SaltyCommandResult(true); + } + } +} \ No newline at end of file diff --git a/srcs/Plugin.RainbowBattle/EventHandlers/RainbowBattleCaptureFlagEventHandler.cs b/srcs/Plugin.RainbowBattle/EventHandlers/RainbowBattleCaptureFlagEventHandler.cs new file mode 100644 index 0000000..07713b1 --- /dev/null +++ b/srcs/Plugin.RainbowBattle/EventHandlers/RainbowBattleCaptureFlagEventHandler.cs @@ -0,0 +1,205 @@ +using System; +using System.Collections.Concurrent; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Packets.Enums.Rainbow; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Game.RainbowBattle; +using WingsEmu.Game.RainbowBattle.Event; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.RainbowBattle.EventHandlers +{ + public class RainbowBattleCaptureFlagEventHandler : IAsyncEventProcessor + { + private readonly IDelayManager _delayManager; + private readonly IGameLanguageService _gameLanguageService; + private readonly RainbowBattleConfiguration _rainbowBattleConfiguration; + + public RainbowBattleCaptureFlagEventHandler(IDelayManager delayManager, IGameLanguageService gameLanguageService, RainbowBattleConfiguration rainbowBattleConfiguration) + { + _delayManager = delayManager; + _gameLanguageService = gameLanguageService; + _rainbowBattleConfiguration = rainbowBattleConfiguration; + } + + public async Task HandleAsync(RainbowBattleCaptureFlagEvent e, CancellationToken cancellation) + { + INpcEntity npc = e.NpcEntity; + IClientSession session = e.Sender; + + if (npc.RainbowFlag == null) + { + return; + } + + RainbowBattleParty rainbowBattleParty = session.PlayerEntity.RainbowBattleComponent.RainbowBattleParty; + if (rainbowBattleParty == null) + { + return; + } + + if (!rainbowBattleParty.Started) + { + return; + } + + if (rainbowBattleParty.FinishTime != null) + { + return; + } + + if (session.PlayerEntity.RainbowBattleComponent.IsFrozen) + { + return; + } + + int distance = session.PlayerEntity.Position.GetDistance(npc.Position); + if (distance > 5) + { + return; + } + + RainbowBattleTeamType? flagTeam = npc.RainbowFlag.FlagTeamType switch + { + RainbowBattleFlagTeamType.Red => RainbowBattleTeamType.Red, + RainbowBattleFlagTeamType.Blue => RainbowBattleTeamType.Blue, + _ => null + }; + + RainbowBattleTeamType playerTeam = session.PlayerEntity.RainbowBattleComponent.Team; + + if (flagTeam == playerTeam) + { + session.SendChatMessage(session.GetLanguage(GameDialogKey.RAINBOW_BATTLE_CHATMESSAGE_ALREADY_CAPTURED), ChatMessageColorType.Yellow); + return; + } + + if (npc.RainbowFlag.RainbowBattleLastTakeOver.AddSeconds(_rainbowBattleConfiguration.DelayBetweenCapture) > DateTime.UtcNow) + { + int cooldown = (int)(npc.RainbowFlag.RainbowBattleLastTakeOver.AddSeconds(_rainbowBattleConfiguration.DelayBetweenCapture) - DateTime.UtcNow).TotalSeconds; + session.SendChatMessage(session.GetLanguageFormat(GameDialogKey.RAINBOW_BATTLE_CHATMESSAGE_CAPTURE_COOLDOWN, cooldown), ChatMessageColorType.Yellow); + return; + } + + if (!e.IsConfirm) + { + await session.PlayerEntity.RemoveInvisibility(); + DateTime waitUntil = await _delayManager.RegisterAction(session.PlayerEntity, DelayedActionType.RainbowBattleCaptureFlag); + session.SendDelay((int)(waitUntil - DateTime.UtcNow).TotalMilliseconds, GuriType.IceBreaker, $"guri 504 {npc.Id}"); + return; + } + + if (!await _delayManager.CanPerformAction(session.PlayerEntity, DelayedActionType.RainbowBattleCaptureFlag)) + { + return; + } + + await _delayManager.CompleteAction(session.PlayerEntity, DelayedActionType.RainbowBattleCaptureFlag); + + // If flag was red and player is in blue team + if (flagTeam != null && flagTeam != playerTeam) + { + ConcurrentDictionary flags = flagTeam == RainbowBattleTeamType.Red ? rainbowBattleParty.RedFlags : rainbowBattleParty.BlueFlags; + + byte flagsCounter = (byte)(flags.TryGetValue(npc.RainbowFlag.FlagType, out byte count) ? count : 0); + if (flagsCounter > 0) + { + flagsCounter--; + flags[npc.RainbowFlag.FlagType] = flagsCounter; + } + } + + int flagPoints = (byte)npc.RainbowFlag.FlagType * 3; + npc.RainbowFlag.RainbowBattleLastTakeOver = DateTime.UtcNow; + + switch (playerTeam) + { + case RainbowBattleTeamType.Red: + + byte redFlags = (byte)(rainbowBattleParty.RedFlags.TryGetValue(npc.RainbowFlag.FlagType, out byte redCount) ? redCount : 0); + redFlags++; + rainbowBattleParty.RedFlags[npc.RainbowFlag.FlagType] = redFlags; + + npc.RainbowFlag.FlagTeamType = RainbowBattleFlagTeamType.Red; + + rainbowBattleParty.IncreaseRedPoints(flagPoints); + rainbowBattleParty.IncreaseBluePoints(-flagPoints); + + break; + case RainbowBattleTeamType.Blue: + + byte blueFlags = (byte)(rainbowBattleParty.BlueFlags.TryGetValue(npc.RainbowFlag.FlagType, out byte blueCount) ? blueCount : 0); + blueFlags++; + rainbowBattleParty.BlueFlags[npc.RainbowFlag.FlagType] = blueFlags; + + npc.RainbowFlag.FlagTeamType = RainbowBattleFlagTeamType.Blue; + + rainbowBattleParty.IncreaseBluePoints(flagPoints); + rainbowBattleParty.IncreaseRedPoints(-flagPoints); + + break; + } + + npc.MapInstance.Broadcast(npc.GenerateFlagPacket()); + + EffectType effectType = npc.RainbowFlag.FlagTeamType switch + { + RainbowBattleFlagTeamType.None => EffectType.NoneFlag, + RainbowBattleFlagTeamType.Red => EffectType.RedFlag, + RainbowBattleFlagTeamType.Blue => EffectType.BlueFlag, + _ => EffectType.NoneFlag + }; + + npc.MapInstance.Broadcast(npc.GenerateEffectPacket(effectType)); + + foreach (IClientSession red in rainbowBattleParty.RedTeam) + { + string npcName = _gameLanguageService.GetNpcMonsterName(npc, red); + + if (playerTeam == RainbowBattleTeamType.Red) + { + red.SendMsg(red.GetLanguageFormat(GameDialogKey.RAINBOW_BATTLE_MESSAGE_ALLY_CAPTURED, session.PlayerEntity.Name, npcName), MsgMessageType.Middle); + red.SendChatMessage(red.GetLanguageFormat(GameDialogKey.RAINBOW_BATTLE_MESSAGE_ALLY_CAPTURED, session.PlayerEntity.Name, npcName), ChatMessageColorType.Green); + } + else + { + red.SendMsg(red.GetLanguageFormat(GameDialogKey.RAINBOW_BATTLE_MESSAGE_BLUE_TEAM_FLAG_CAPTURED, npcName), MsgMessageType.Middle); + red.SendChatMessage(red.GetLanguageFormat(GameDialogKey.RAINBOW_BATTLE_MESSAGE_BLUE_TEAM_FLAG_CAPTURED, npcName), ChatMessageColorType.Red); + } + } + + foreach (IClientSession blue in rainbowBattleParty.BlueTeam) + { + string npcName = _gameLanguageService.GetNpcMonsterName(npc, blue); + + if (playerTeam == RainbowBattleTeamType.Blue) + { + blue.SendMsg(blue.GetLanguageFormat(GameDialogKey.RAINBOW_BATTLE_MESSAGE_ALLY_CAPTURED, session.PlayerEntity.Name, npcName), MsgMessageType.Middle); + blue.SendChatMessage(blue.GetLanguageFormat(GameDialogKey.RAINBOW_BATTLE_MESSAGE_ALLY_CAPTURED, session.PlayerEntity.Name, npcName), ChatMessageColorType.Green); + } + else + { + blue.SendMsg(blue.GetLanguageFormat(GameDialogKey.RAINBOW_BATTLE_MESSAGE_RED_TEAM_FLAG_CAPTURED, npcName), MsgMessageType.Middle); + blue.SendChatMessage(blue.GetLanguageFormat(GameDialogKey.RAINBOW_BATTLE_MESSAGE_RED_TEAM_FLAG_CAPTURED, npcName), ChatMessageColorType.Red); + } + } + + session.PlayerEntity.RainbowBattleComponent.ActivityPoints += _rainbowBattleConfiguration.CaptureActivityPoints; + + await session.EmitEventAsync(new RainbowBattleRefreshScoreEvent + { + RainbowBattleParty = rainbowBattleParty + }); + } + } +} \ No newline at end of file diff --git a/srcs/Plugin.RainbowBattle/EventHandlers/RainbowBattleDestroyEventHandler.cs b/srcs/Plugin.RainbowBattle/EventHandlers/RainbowBattleDestroyEventHandler.cs new file mode 100644 index 0000000..22285ab --- /dev/null +++ b/srcs/Plugin.RainbowBattle/EventHandlers/RainbowBattleDestroyEventHandler.cs @@ -0,0 +1,39 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Networking; +using WingsEmu.Game.RainbowBattle; +using WingsEmu.Game.RainbowBattle.Event; + +namespace Plugin.RainbowBattle.EventHandlers +{ + public class RainbowBattleDestroyEventHandler : IAsyncEventProcessor + { + private readonly IMapManager _mapManager; + private readonly IRainbowBattleManager _rainbowBattleManager; + + public RainbowBattleDestroyEventHandler(IRainbowBattleManager rainbowBattleManager, IMapManager mapManager) + { + _rainbowBattleManager = rainbowBattleManager; + _mapManager = mapManager; + } + + public async Task HandleAsync(RainbowBattleDestroyEvent e, CancellationToken cancellation) + { + RainbowBattleParty rainbowBattleParty = e.RainbowBattleParty; + _rainbowBattleManager.RemoveRainbowBattle(rainbowBattleParty); + + IMapInstance mapInstance = rainbowBattleParty.MapInstance; + foreach (IClientSession session in mapInstance.Sessions) + { + await session.EmitEventAsync(new RainbowBattleLeaveEvent + { + AddLeaverBuster = false + }); + } + + _mapManager.RemoveMapInstance(mapInstance.Id); + } + } +} \ No newline at end of file diff --git a/srcs/Plugin.RainbowBattle/EventHandlers/RainbowBattleEndEventHandler.cs b/srcs/Plugin.RainbowBattle/EventHandlers/RainbowBattleEndEventHandler.cs new file mode 100644 index 0000000..4f4c461 --- /dev/null +++ b/srcs/Plugin.RainbowBattle/EventHandlers/RainbowBattleEndEventHandler.cs @@ -0,0 +1,274 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.DAL.Redis.Locks; +using PhoenixLib.Events; +using Plugin.FamilyImpl.Achievements; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsAPI.Packets.Enums; +using WingsAPI.Packets.Enums.Rainbow; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Families; +using WingsEmu.Game.Items; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Raids; +using WingsEmu.Game.RainbowBattle; +using WingsEmu.Game.RainbowBattle.Event; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.RainbowBattle.EventHandlers +{ + public class RainbowBattleEndEventHandler : IAsyncEventProcessor + { + private readonly IExpirableLockService _expirableLockService; + private readonly IFamilyAchievementManager _familyAchievementManager; + private readonly IFamilyMissionManager _familyMissionManager; + private readonly IGameItemInstanceFactory _gameItemInstanceFactory; + private readonly RainbowBattleConfiguration _rainbowBattleConfiguration; + + public RainbowBattleEndEventHandler(IFamilyMissionManager familyMissionManager, RainbowBattleConfiguration rainbowBattleConfiguration, + IGameItemInstanceFactory gameItemInstanceFactory, IFamilyAchievementManager familyAchievementManager, IExpirableLockService expirableLockService) + { + _familyMissionManager = familyMissionManager; + _rainbowBattleConfiguration = rainbowBattleConfiguration; + _gameItemInstanceFactory = gameItemInstanceFactory; + _familyAchievementManager = familyAchievementManager; + _expirableLockService = expirableLockService; + } + + public async Task HandleAsync(RainbowBattleEndEvent e, CancellationToken cancellation) + { + RainbowBattleParty rainbowBattleParty = e.RainbowBattleParty; + rainbowBattleParty.FinishTime = DateTime.UtcNow.AddSeconds(15); + + RainbowBattleTeamType? winnerTeam = null; + if (rainbowBattleParty.BluePoints > rainbowBattleParty.RedPoints) + { + winnerTeam = RainbowBattleTeamType.Blue; + } + else if (rainbowBattleParty.RedPoints > rainbowBattleParty.BluePoints) + { + winnerTeam = RainbowBattleTeamType.Red; + } + + switch (winnerTeam) + { + case RainbowBattleTeamType.Red: + await ProcessWin(rainbowBattleParty, RainbowBattleTeamType.Red); + await ProcessLose(rainbowBattleParty, RainbowBattleTeamType.Blue); + + IClientSession redDummy = rainbowBattleParty.RedTeam.FirstOrDefault(); + if (redDummy != null) + { + await redDummy.EmitEventAsync(new RainbowBattleWonEvent + { + Id = rainbowBattleParty.Id, + Players = rainbowBattleParty.RedTeam.Select(x => x.PlayerEntity.Id).ToArray() + }); + } + + IClientSession blueDummy = rainbowBattleParty.BlueTeam.FirstOrDefault(); + if (blueDummy != null) + { + await blueDummy.EmitEventAsync(new RainbowBattleLoseEvent + { + Id = rainbowBattleParty.Id, + Players = rainbowBattleParty.BlueTeam.Select(x => x.PlayerEntity.Id).ToArray() + }); + } + + return; + case RainbowBattleTeamType.Blue: + await ProcessWin(rainbowBattleParty, RainbowBattleTeamType.Blue); + await ProcessLose(rainbowBattleParty, RainbowBattleTeamType.Red); + + redDummy = rainbowBattleParty.RedTeam.FirstOrDefault(); + if (redDummy != null) + { + await redDummy.EmitEventAsync(new RainbowBattleLoseEvent + { + Players = rainbowBattleParty.RedTeam.Select(x => x.PlayerEntity.Id).ToArray() + }); + } + + blueDummy = rainbowBattleParty.BlueTeam.FirstOrDefault(); + if (blueDummy != null) + { + await blueDummy.EmitEventAsync(new RainbowBattleWonEvent + { + Players = rainbowBattleParty.BlueTeam.Select(x => x.PlayerEntity.Id).ToArray() + }); + } + + return; + case null: + await ProcessLose(rainbowBattleParty, RainbowBattleTeamType.Blue); + await ProcessLose(rainbowBattleParty, RainbowBattleTeamType.Red); + + IClientSession dummy = rainbowBattleParty.RedTeam.FirstOrDefault(); + if (dummy != null) + { + await dummy.EmitEventAsync(new RainbowBattleTieEvent + { + RedTeam = rainbowBattleParty.RedTeam.Select(x => x.PlayerEntity.Id).ToArray(), + BlueTeam = rainbowBattleParty.BlueTeam.Select(x => x.PlayerEntity.Id).ToArray() + }); + } + + return; + } + } + + private async Task ProcessWin(RainbowBattleParty rainbowBattleParty, RainbowBattleTeamType team) + { + IReadOnlyList members = team == RainbowBattleTeamType.Red ? rainbowBattleParty.RedTeam : rainbowBattleParty.BlueTeam; + string removeClock = RainbowBattleExtensions.GenerateRainbowTime(RainbowTimeType.End); + + short neededPoints = _rainbowBattleConfiguration.NeededActivityPoints; + + foreach (IClientSession member in members) + { + member.SendMsg(member.GetLanguage(GameDialogKey.RAINBOW_BATTLE_MESSAGE_YOU_WON), MsgMessageType.Middle); + member.SendChatMessage(member.GetLanguage(GameDialogKey.RAINBOW_BATTLE_MESSAGE_YOU_WON), ChatMessageColorType.Yellow); + member.SendEmptyRaidBoss(); + member.SendPacket(removeClock); + + if (member.PlayerEntity.RainbowBattleComponent.ActivityPoints < neededPoints) + { + member.SendChatMessage(member.GetLanguage(GameDialogKey.RAINBOW_BATTLE_CHATMESSAGE_NOT_ENOUGH_ACTIVITY_POINTS), ChatMessageColorType.Red); + continue; + } + + if (member.PlayerEntity.RainbowBattleLeaverBusterDto is { RewardPenalty: > 0 }) + { + member.PlayerEntity.RainbowBattleLeaverBusterDto.RewardPenalty -= 1; + + if (member.PlayerEntity.RainbowBattleLeaverBusterDto.RewardPenalty == 0) + { + member.SendChatMessageNoPlayer(member.GetLanguage(GameDialogKey.RAINBOW_BATTLE_CHATMESSAGE_PENALTY_NEXT_REWARD), ChatMessageColorType.IntenseRed); + } + else + { + member.SendChatMessageNoPlayer(member.GetLanguageFormat(GameDialogKey.RAINBOW_BATTLE_CHATMESSAGE_PENALTY_LEFT, + member.PlayerEntity.RainbowBattleLeaverBusterDto.RewardPenalty), ChatMessageColorType.IntenseRed); + } + + continue; + } + + await ProcessCoins(member, true); + await member.EmitEventAsync(new GenerateReputationEvent + { + Amount = _rainbowBattleConfiguration.ReputationMultiplier * member.PlayerEntity.Level, + SendMessage = true + }); + + ProcessFamilyWinMission(member); + await ProcessFamilyAchievement(member, true); + ProcessFamilyMission(member); + } + } + + private async Task ProcessLose(RainbowBattleParty rainbowBattleParty, RainbowBattleTeamType team) + { + IReadOnlyList members = team == RainbowBattleTeamType.Red ? rainbowBattleParty.RedTeam : rainbowBattleParty.BlueTeam; + string removeClock = RainbowBattleExtensions.GenerateRainbowTime(RainbowTimeType.End); + + short neededPoints = _rainbowBattleConfiguration.NeededActivityPoints; + + foreach (IClientSession member in members) + { + member.SendMsg(member.GetLanguage(GameDialogKey.RAINBOW_BATTLE_MESSAGE_YOU_LOSE), MsgMessageType.Middle); + member.SendChatMessage(member.GetLanguage(GameDialogKey.RAINBOW_BATTLE_MESSAGE_YOU_LOSE), ChatMessageColorType.Yellow); + member.SendRaidUiPacket(RaidType.Cuby, RaidWindowType.MISSION_FAIL); + member.SendPacket(removeClock); + + if (member.PlayerEntity.RainbowBattleComponent.ActivityPoints < neededPoints) + { + member.SendChatMessage(member.GetLanguage(GameDialogKey.RAINBOW_BATTLE_CHATMESSAGE_NOT_ENOUGH_ACTIVITY_POINTS), ChatMessageColorType.Red); + continue; + } + + if (member.PlayerEntity.RainbowBattleLeaverBusterDto is { RewardPenalty: > 0 }) + { + member.PlayerEntity.RainbowBattleLeaverBusterDto.RewardPenalty -= 1; + + if (member.PlayerEntity.RainbowBattleLeaverBusterDto.RewardPenalty == 0) + { + member.SendChatMessageNoPlayer(member.GetLanguage(GameDialogKey.RAINBOW_BATTLE_CHATMESSAGE_PENALTY_NEXT_REWARD), ChatMessageColorType.IntenseRed); + } + else + { + member.SendChatMessageNoPlayer(member.GetLanguageFormat(GameDialogKey.RAINBOW_BATTLE_CHATMESSAGE_PENALTY_LEFT, + member.PlayerEntity.RainbowBattleLeaverBusterDto.RewardPenalty), ChatMessageColorType.IntenseRed); + } + + continue; + } + + await ProcessCoins(member, false); + await ProcessFamilyAchievement(member, false); + ProcessFamilyMission(member); + } + } + + private async Task ProcessFamilyAchievement(IClientSession session, bool isWin) + { + if (!session.PlayerEntity.IsInFamily()) + { + return; + } + + IFamily family = session.PlayerEntity.Family; + + if (await _expirableLockService.TryAddTemporaryLockAsync($"game:locks:family:rainbowbattle:character:{session.PlayerEntity.Id}", DateTime.UtcNow.Date.AddDays(1))) + { + _familyAchievementManager.IncrementFamilyAchievement(family.Id, (short)FamilyAchievementsVnum.COMPLETE_10_RAINBOW_BATTLE); + } + + if (!isWin) + { + return; + } + + if (!await _expirableLockService.TryAddTemporaryLockAsync($"game:locks:family:rainbowbattle-win:character:{session.PlayerEntity.Id}", DateTime.UtcNow.Date.AddDays(1))) + { + return; + } + + _familyAchievementManager.IncrementFamilyAchievement(family.Id, (short)FamilyAchievementsVnum.WIN_5_RAINBOW_BATTLE); + } + + private async Task ProcessCoins(IClientSession session, bool isWinner) + { + GameItemInstance coin = _gameItemInstanceFactory.CreateItem((short)ItemVnums.RAINBOW_COIN, isWinner ? 3 : 1); + await session.AddNewItemToInventory(coin, true, sendGiftIsFull: true); + } + + private void ProcessFamilyMission(IClientSession session) + { + if (!session.PlayerEntity.IsInFamily()) + { + return; + } + + _familyMissionManager.IncrementFamilyMission(session.PlayerEntity.Family.Id, session.PlayerEntity.Id, (int)FamilyMissionVnums.DAILY_COMPLETE_10_RAINBOW_BATTLE); + } + + private void ProcessFamilyWinMission(IClientSession session) + { + if (!session.PlayerEntity.IsInFamily()) + { + return; + } + + _familyMissionManager.IncrementFamilyMission(session.PlayerEntity.Family.Id, session.PlayerEntity.Id, (int)FamilyMissionVnums.DAILY_WIN_5_RAINBOW_BATTLE); + } + } +} \ No newline at end of file diff --git a/srcs/Plugin.RainbowBattle/EventHandlers/RainbowBattleFreezeEventHandler.cs b/srcs/Plugin.RainbowBattle/EventHandlers/RainbowBattleFreezeEventHandler.cs new file mode 100644 index 0000000..bdf04e1 --- /dev/null +++ b/srcs/Plugin.RainbowBattle/EventHandlers/RainbowBattleFreezeEventHandler.cs @@ -0,0 +1,127 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Packets.Enums.Rainbow; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Entities.Extensions; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Networking; +using WingsEmu.Game.RainbowBattle; +using WingsEmu.Game.RainbowBattle.Event; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.RainbowBattle.EventHandlers +{ + public class RainbowBattleFreezeEventHandler : IAsyncEventProcessor + { + private readonly RainbowBattleConfiguration _rainbowBattleConfiguration; + + public RainbowBattleFreezeEventHandler(RainbowBattleConfiguration rainbowBattleConfiguration) => _rainbowBattleConfiguration = rainbowBattleConfiguration; + + public async Task HandleAsync(RainbowBattleFreezeEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + IBattleEntity killer = e.Killer; + + RainbowBattleParty rainbowParty = session.PlayerEntity.RainbowBattleComponent.RainbowBattleParty; + if (rainbowParty == null) + { + return; + } + + session.PlayerEntity.RainbowBattleComponent.IsFrozen = true; + session.PlayerEntity.RainbowBattleComponent.FrozenTime = DateTime.UtcNow.AddSeconds(_rainbowBattleConfiguration.SecondsBeingFrozen); + session.SendCondPacket(); + session.BroadcastEffect(EffectType.Frozen); + await session.PlayerEntity.RemoveNegativeBuffs(100); + + session.PlayerEntity.Hp = session.PlayerEntity.MaxHp; + session.PlayerEntity.Mp = session.PlayerEntity.MaxMp; + session.RefreshStat(); + + foreach (IMateEntity mate in session.PlayerEntity.MateComponent.TeamMembers()) + { + session.PlayerEntity.MapInstance.RemoveMate(mate); + session.PlayerEntity.MapInstance.Broadcast(mate.GenerateOut()); + } + + IReadOnlyList members = session.PlayerEntity.RainbowBattleComponent.Team == RainbowBattleTeamType.Red ? rainbowParty.RedTeam : rainbowParty.BlueTeam; + foreach (IClientSession member in members) + { + member.SendMsg(member.GetLanguageFormat(GameDialogKey.RAINBOW_BATTLE_SHOUTMESSAGE_FROZEN, session.PlayerEntity.Name), MsgMessageType.Middle); + } + + session.PlayerEntity.RainbowBattleComponent.Deaths++; + session.PlayerEntity.RainbowBattleComponent.ActivityPoints += _rainbowBattleConfiguration.DeathActivityPoints; + + IPlayerEntity playerKiller = killer switch + { + IPlayerEntity playerEntity => playerEntity, + IMateEntity mateEntity => mateEntity.Owner, + IMonsterEntity monsterEntity => monsterEntity.SummonerType != null && monsterEntity.SummonerId != null && monsterEntity.SummonerType == VisualType.Player + ? monsterEntity.MapInstance.GetCharacterById(monsterEntity.SummonerId.Value) + : null, + _ => null + }; + + if (playerKiller?.RainbowBattleComponent.RainbowBattleParty == null) + { + return; + } + + playerKiller.RainbowBattleComponent.Kills++; + playerKiller.RainbowBattleComponent.ActivityPoints += _rainbowBattleConfiguration.KillActivityPoints; + + IReadOnlyList playerMembers = playerKiller.RainbowBattleComponent.Team == RainbowBattleTeamType.Red + ? playerKiller.RainbowBattleComponent.RainbowBattleParty.RedTeam + : playerKiller.RainbowBattleComponent.RainbowBattleParty.BlueTeam; + + string memberList = playerKiller.RainbowBattleComponent.RainbowBattleParty.GenerateRainbowBattleWidget(playerKiller.RainbowBattleComponent.Team); + foreach (IClientSession member in playerMembers) + { + member.SendPacket(memberList); + } + + await session.EmitEventAsync(new RainbowBattleFrozenEvent + { + Id = playerKiller.RainbowBattleComponent.RainbowBattleParty.Id, + Killer = new RainbowBattlePlayerDump + { + CharacterId = playerKiller.Id, + Level = playerKiller.Level, + Class = playerKiller.Class, + Specialist = playerKiller.Specialist, + TotalFireResistance = playerKiller.FireResistance, + TotalWaterResistance = playerKiller.WaterResistance, + TotalLightResistance = playerKiller.LightResistance, + TotalDarkResistance = playerKiller.DarkResistance, + FairyLevel = playerKiller.Fairy?.ElementRate + playerKiller.Fairy?.GameItem.ElementRate, + Score = playerKiller.RainbowBattleComponent.ActivityPoints, + Team = playerKiller.RainbowBattleComponent.Team.ToString() + }, + Killed = new RainbowBattlePlayerDump + { + CharacterId = session.PlayerEntity.Id, + Level = session.PlayerEntity.Level, + Class = session.PlayerEntity.Class, + Specialist = session.PlayerEntity.Specialist, + TotalFireResistance = session.PlayerEntity.FireResistance, + TotalWaterResistance = session.PlayerEntity.WaterResistance, + TotalLightResistance = session.PlayerEntity.LightResistance, + TotalDarkResistance = session.PlayerEntity.DarkResistance, + FairyLevel = session.PlayerEntity.Fairy?.ElementRate + session.PlayerEntity.Fairy?.GameItem.ElementRate, + Score = session.PlayerEntity.RainbowBattleComponent.ActivityPoints, + Team = session.PlayerEntity.RainbowBattleComponent.Team.ToString() + } + }); + } + } +} \ No newline at end of file diff --git a/srcs/Plugin.RainbowBattle/EventHandlers/RainbowBattleLeaveEventHandler.cs b/srcs/Plugin.RainbowBattle/EventHandlers/RainbowBattleLeaveEventHandler.cs new file mode 100644 index 0000000..a421058 --- /dev/null +++ b/srcs/Plugin.RainbowBattle/EventHandlers/RainbowBattleLeaveEventHandler.cs @@ -0,0 +1,88 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Data.Character; +using WingsAPI.Packets.Enums.Rainbow; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Game.RainbowBattle; +using WingsEmu.Game.RainbowBattle.Event; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.RainbowBattle.EventHandlers +{ + public class RainbowBattleLeaveEventHandler : IAsyncEventProcessor + { + public async Task HandleAsync(RainbowBattleLeaveEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + RainbowBattleParty rainbowBattleParty = session.PlayerEntity.RainbowBattleComponent.RainbowBattleParty; + if (rainbowBattleParty == null) + { + return; + } + + if (e.CheckIfFinished && rainbowBattleParty.FinishTime == null) + { + return; + } + + RainbowBattleTeamType team = session.PlayerEntity.RainbowBattleComponent.Team; + IReadOnlyList members = team == RainbowBattleTeamType.Red ? rainbowBattleParty.RedTeam : rainbowBattleParty.BlueTeam; + + switch (team) + { + case RainbowBattleTeamType.Red: + session.PlayerEntity.RainbowBattleComponent.RainbowBattleParty.RemoveRedPlayer(session); + break; + case RainbowBattleTeamType.Blue: + session.PlayerEntity.RainbowBattleComponent.RainbowBattleParty.RemoveBluePlayer(session); + break; + } + + string membersPacket = rainbowBattleParty.GenerateRainbowMembers(team); + string memberList = rainbowBattleParty.GenerateRainbowBattleWidget(team); + string rainbowScore = rainbowBattleParty.GenerateRainbowScore(team); + + foreach (IClientSession member in members) + { + if (e.SendMessage) + { + member.SendMsg(member.GetLanguageFormat(GameDialogKey.RAINBOW_BATTLE_MESSAGE_PLAYER_LEFT, session.PlayerEntity.Name), MsgMessageType.Middle); + member.SendChatMessage(member.GetLanguageFormat(GameDialogKey.RAINBOW_BATTLE_MESSAGE_PLAYER_LEFT, session.PlayerEntity.Name), ChatMessageColorType.Yellow); + } + + member.SendPacket(membersPacket); + member.SendPacket(memberList); + member.SendPacket(rainbowScore); + } + + session.SendPacket(RainbowBattleExtensions.GenerateRainbowTime(RainbowTimeType.End)); + session.SendPacket(RainbowBattleExtensions.GenerateRainBowEnter(false)); + session.SendPacket(RainbowBattleExtensions.GenerateRainBowExit()); + + if (e.AddLeaverBuster && rainbowBattleParty.FinishTime == null) + { + ProcessLeaverBuster(session); + } + + session.PlayerEntity.RainbowBattleComponent.RemoveRainbowBattle(); + session.ChangeToLastBaseMap(); + } + + private void ProcessLeaverBuster(IClientSession session) + { + session.PlayerEntity.RainbowBattleLeaverBusterDto ??= new RainbowBattleLeaverBusterDto(); + RainbowBattleLeaverBusterDto leaver = session.PlayerEntity.RainbowBattleLeaverBusterDto; + if (leaver.Exits < 3) // Only 3 times per month + { + leaver.Exits++; + return; + } + + leaver.RewardPenalty++; + } + } +} \ No newline at end of file diff --git a/srcs/Plugin.RainbowBattle/EventHandlers/RainbowBattleLeaverBusterRefreshEventHandler.cs b/srcs/Plugin.RainbowBattle/EventHandlers/RainbowBattleLeaverBusterRefreshEventHandler.cs new file mode 100644 index 0000000..0d0ed81 --- /dev/null +++ b/srcs/Plugin.RainbowBattle/EventHandlers/RainbowBattleLeaverBusterRefreshEventHandler.cs @@ -0,0 +1,33 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.DAL.Redis.Locks; +using PhoenixLib.Events; +using WingsAPI.Data.Character; +using WingsEmu.Game.Networking; +using WingsEmu.Game.RainbowBattle.Event; + +namespace Plugin.RainbowBattle.EventHandlers +{ + public class RainbowBattleLeaverBusterRefreshEventHandler : IAsyncEventProcessor + { + private readonly IExpirableLockService _lock; + + public RainbowBattleLeaverBusterRefreshEventHandler(IExpirableLockService @lock) => _lock = @lock; + + public async Task HandleAsync(RainbowBattleLeaverBusterRefreshEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + DateTime nextMonth = DateTime.UtcNow.Date.AddMonths(1).AddDays(-DateTime.UtcNow.Date.Day + 1); + bool canRefresh = await _lock.TryAddTemporaryLockAsync($"game:locks:rainbow-battle-leaver-buster:{session.PlayerEntity.Id}", nextMonth); + + if (!e.Force && !canRefresh) + { + return; + } + + session.PlayerEntity.RainbowBattleLeaverBusterDto ??= new RainbowBattleLeaverBusterDto(); + session.PlayerEntity.RainbowBattleLeaverBusterDto.Exits = 0; + } + } +} \ No newline at end of file diff --git a/srcs/Plugin.RainbowBattle/EventHandlers/RainbowBattleProcessActivityPointsEventHandler.cs b/srcs/Plugin.RainbowBattle/EventHandlers/RainbowBattleProcessActivityPointsEventHandler.cs new file mode 100644 index 0000000..9146df0 --- /dev/null +++ b/srcs/Plugin.RainbowBattle/EventHandlers/RainbowBattleProcessActivityPointsEventHandler.cs @@ -0,0 +1,45 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Networking; +using WingsEmu.Game.RainbowBattle; +using WingsEmu.Game.RainbowBattle.Event; + +namespace Plugin.RainbowBattle.EventHandlers +{ + public class RainbowBattleProcessActivityPointsEventHandler : IAsyncEventProcessor + { + private readonly RainbowBattleConfiguration _rainbowBattleConfiguration; + + public RainbowBattleProcessActivityPointsEventHandler(RainbowBattleConfiguration rainbowBattleConfiguration) => _rainbowBattleConfiguration = rainbowBattleConfiguration; + + public async Task HandleAsync(RainbowBattleProcessActivityPointsEvent e, CancellationToken cancellation) + { + RainbowBattleParty rainbowBattle = e.RainbowBattleParty; + + if (rainbowBattle?.MapInstance == null) + { + return; + } + + DateTime now = DateTime.UtcNow; + short pointsToAdd = _rainbowBattleConfiguration.WalkingActivityPoints; + foreach (IClientSession session in rainbowBattle.MapInstance.Sessions) + { + if (!session.PlayerEntity.RainbowBattleComponent.IsInRainbowBattle) + { + continue; + } + + if (session.PlayerEntity.LastWalk?.WalkTimeStart.AddSeconds(59) < now) + { + continue; + } + + session.PlayerEntity.RainbowBattleComponent.ActivityPoints += pointsToAdd; + } + } + } +} \ No newline at end of file diff --git a/srcs/Plugin.RainbowBattle/EventHandlers/RainbowBattleProcessFlagPointsEventHandler.cs b/srcs/Plugin.RainbowBattle/EventHandlers/RainbowBattleProcessFlagPointsEventHandler.cs new file mode 100644 index 0000000..f909d7e --- /dev/null +++ b/srcs/Plugin.RainbowBattle/EventHandlers/RainbowBattleProcessFlagPointsEventHandler.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Packets.Enums.Rainbow; +using WingsEmu.Game.RainbowBattle; +using WingsEmu.Game.RainbowBattle.Event; + +namespace Plugin.RainbowBattle.EventHandlers +{ + public class RainbowBattleProcessFlagPointsEventHandler : IAsyncEventProcessor + { + private readonly IAsyncEventPipeline _asyncEventPipeline; + + public RainbowBattleProcessFlagPointsEventHandler(IAsyncEventPipeline asyncEventPipeline) => _asyncEventPipeline = asyncEventPipeline; + + public async Task HandleAsync(RainbowBattleProcessFlagPointsEvent e, CancellationToken cancellation) + { + RainbowBattleParty rainbowParty = e.RainbowBattleParty; + + IReadOnlyDictionary redFlags = rainbowParty.RedFlags; + IReadOnlyDictionary blueFlags = rainbowParty.BlueFlags; + + int redPointsToAdd = 0; + int bluePointsToAdd = 0; + + foreach ((RainbowBattleFlagType flagType, byte count) in redFlags) + { + byte flagPoints = (byte)flagType; + int counter = flagPoints * count; + redPointsToAdd += counter; + } + + foreach ((RainbowBattleFlagType flagType, byte count) in blueFlags) + { + byte flagPoints = (byte)flagType; + int counter = flagPoints * count; + bluePointsToAdd += counter; + } + + rainbowParty.IncreaseRedPoints(redPointsToAdd); + rainbowParty.IncreaseBluePoints(bluePointsToAdd); + + await _asyncEventPipeline.ProcessEventAsync(new RainbowBattleRefreshScoreEvent + { + RainbowBattleParty = rainbowParty + }); + } + } +} \ No newline at end of file diff --git a/srcs/Plugin.RainbowBattle/EventHandlers/RainbowBattleProcessLifeEventHandler.cs b/srcs/Plugin.RainbowBattle/EventHandlers/RainbowBattleProcessLifeEventHandler.cs new file mode 100644 index 0000000..2836133 --- /dev/null +++ b/srcs/Plugin.RainbowBattle/EventHandlers/RainbowBattleProcessLifeEventHandler.cs @@ -0,0 +1,31 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Packets.Enums.Rainbow; +using WingsEmu.Game.Networking; +using WingsEmu.Game.RainbowBattle; +using WingsEmu.Game.RainbowBattle.Event; + +namespace Plugin.RainbowBattle.EventHandlers +{ + public class RainbowBattleProcessLifeEventHandler : IAsyncEventProcessor + { + public async Task HandleAsync(RainbowBattleProcessLifeEvent e, CancellationToken cancellation) + { + RainbowBattleParty rainbowBattleParty = e.RainbowBattleParty; + + string redTeam = rainbowBattleParty.GenerateRainbowBattleLive(RainbowBattleTeamType.Red); + string blueTeam = rainbowBattleParty.GenerateRainbowBattleLive(RainbowBattleTeamType.Blue); + + foreach (IClientSession session in rainbowBattleParty.RedTeam) + { + session.SendPacket(redTeam); + } + + foreach (IClientSession session in rainbowBattleParty.BlueTeam) + { + session.SendPacket(blueTeam); + } + } + } +} \ No newline at end of file diff --git a/srcs/Plugin.RainbowBattle/EventHandlers/RainbowBattleRefreshScoreEventHandler.cs b/srcs/Plugin.RainbowBattle/EventHandlers/RainbowBattleRefreshScoreEventHandler.cs new file mode 100644 index 0000000..26cae1e --- /dev/null +++ b/srcs/Plugin.RainbowBattle/EventHandlers/RainbowBattleRefreshScoreEventHandler.cs @@ -0,0 +1,46 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Packets.Enums.Rainbow; +using WingsEmu.Game.Networking; +using WingsEmu.Game.RainbowBattle; +using WingsEmu.Game.RainbowBattle.Event; + +namespace Plugin.RainbowBattle.EventHandlers +{ + public class RainbowBattleRefreshScoreEventHandler : IAsyncEventProcessor + { + public async Task HandleAsync(RainbowBattleRefreshScoreEvent e, CancellationToken cancellation) + { + RainbowBattleParty rainbowBattleParty = e.RainbowBattleParty; + + if (rainbowBattleParty == null) + { + return; + } + + if (!rainbowBattleParty.Started) + { + return; + } + + if (rainbowBattleParty.FinishTime != null) + { + return; + } + + string redTeam = rainbowBattleParty.GenerateRainbowScore(RainbowBattleTeamType.Red); + string blueTeam = rainbowBattleParty.GenerateRainbowScore(RainbowBattleTeamType.Blue); + + foreach (IClientSession red in rainbowBattleParty.RedTeam) + { + red.SendPacket(redTeam); + } + + foreach (IClientSession blue in rainbowBattleParty.BlueTeam) + { + blue.SendPacket(blueTeam); + } + } + } +} \ No newline at end of file diff --git a/srcs/Plugin.RainbowBattle/EventHandlers/RainbowBattleStartEventHandler.cs b/srcs/Plugin.RainbowBattle/EventHandlers/RainbowBattleStartEventHandler.cs new file mode 100644 index 0000000..5a88af6 --- /dev/null +++ b/srcs/Plugin.RainbowBattle/EventHandlers/RainbowBattleStartEventHandler.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using Plugin.RainbowBattle.Managers; +using WingsAPI.Packets.Enums.Rainbow; +using WingsEmu.Game; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Networking; +using WingsEmu.Game.RainbowBattle; +using WingsEmu.Game.RainbowBattle.Event; +using WingsEmu.Game.Skills; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.RainbowBattle.EventHandlers +{ + public class RainbowBattleStartEventHandler : IAsyncEventProcessor + { + private readonly RainbowBattleConfiguration _rainbowBattleConfiguration; + private readonly IRainbowBattleManager _rainbowBattleManager; + private readonly IRainbowFactory _rainbowFactory; + private readonly IRandomGenerator _randomGenerator; + + public RainbowBattleStartEventHandler(IRainbowFactory rainbowFactory, IRainbowBattleManager rainbowBattleManager, + RainbowBattleConfiguration rainbowBattleConfiguration, IRandomGenerator randomGenerator) + { + _rainbowFactory = rainbowFactory; + _rainbowBattleManager = rainbowBattleManager; + _rainbowBattleConfiguration = rainbowBattleConfiguration; + _randomGenerator = randomGenerator; + } + + public async Task HandleAsync(RainbowBattleStartEvent e, CancellationToken cancellation) + { + RainbowBattleParty rainbowBattleParty = await _rainbowFactory.CreateRainbowBattle(e.RedTeam, e.BlueTeam); + if (rainbowBattleParty == null) + { + return; + } + + if (!_rainbowBattleManager.IsActive) + { + _rainbowBattleManager.IsActive = true; + } + + _rainbowBattleManager.AddRainbowBattle(rainbowBattleParty); + + await HandleStart(rainbowBattleParty, RainbowBattleTeamType.Red); + await HandleStart(rainbowBattleParty, RainbowBattleTeamType.Blue); + } + + private async Task HandleStart(RainbowBattleParty rainbowBattleParty, RainbowBattleTeamType teamType) + { + string score = rainbowBattleParty.GenerateRainbowScore(teamType); + string timeEnter = RainbowBattleExtensions.GenerateRainbowTime(RainbowTimeType.Enter); + string timeStart = RainbowBattleExtensions.GenerateRainbowTime(RainbowTimeType.Start, (short?)(rainbowBattleParty.EndTime - DateTime.UtcNow).TotalSeconds); + string rainbowEnter = RainbowBattleExtensions.GenerateRainBowEnter(true); + + string membersPacket = rainbowBattleParty.GenerateRainbowMembers(teamType); + string memberList = rainbowBattleParty.GenerateRainbowBattleWidget(teamType); + + IReadOnlyList members = teamType == RainbowBattleTeamType.Red ? rainbowBattleParty.RedTeam : rainbowBattleParty.BlueTeam; + + IMapInstance mapInstance = rainbowBattleParty.MapInstance; + GameDialogKey gameDialogKey = teamType == RainbowBattleTeamType.Red ? GameDialogKey.RAINBOW_BATTLE_SHOUTMESSAGE_RED_TEAM : GameDialogKey.RAINBOW_BATTLE_SHOUTMESSAGE_BLUE_TEAM; + + foreach (IClientSession member in members) + { + member.PlayerEntity.RainbowBattleComponent.SetRainbowBattle(rainbowBattleParty, teamType); + + member.PlayerEntity.Hp = member.PlayerEntity.MaxHp; + member.PlayerEntity.Mp = member.PlayerEntity.MaxMp; + + await member.PlayerEntity.RemovePositiveBuffs(100); + await member.PlayerEntity.RemoveNegativeBuffs(100); + await member.EmitEventAsync(new RemoveVehicleEvent()); + + member.PlayerEntity.ClearSkillCooldowns(); + foreach (IBattleEntitySkill skill in member.PlayerEntity.Skills) + { + skill.LastUse = DateTime.MinValue; + member.SendSkillCooldownReset(skill.Skill.CastId); + } + + short randomX; + short randomY; + + switch (teamType) + { + case RainbowBattleTeamType.Red: + randomX = (short)_randomGenerator.RandomNumber(_rainbowBattleConfiguration.RedStartX, _rainbowBattleConfiguration.RedEndX + 1); + randomY = (short)_randomGenerator.RandomNumber(_rainbowBattleConfiguration.RedStartY, _rainbowBattleConfiguration.RedEndY + 1); + + if (mapInstance.IsBlockedZone(randomX, randomY)) + { + randomX = 0; + randomY = 34; + } + + break; + case RainbowBattleTeamType.Blue: + + randomX = (short)_randomGenerator.RandomNumber(_rainbowBattleConfiguration.BlueStartX, _rainbowBattleConfiguration.BlueEndX + 1); + randomY = (short)_randomGenerator.RandomNumber(_rainbowBattleConfiguration.BlueStartY, _rainbowBattleConfiguration.BlueEndY + 1); + + if (mapInstance.IsBlockedZone(randomX, randomY)) + { + randomX = 117; + randomY = 42; + } + + break; + default: + member.PlayerEntity.RainbowBattleComponent.RemoveRainbowBattle(); + continue; + } + + member.ChangeMap(mapInstance, randomX, randomY); + + member.SendPacket(score); + member.SendPacket(timeEnter); + member.SendPacket(timeStart); + member.SendPacket(membersPacket); + member.SendPacket(rainbowEnter); + member.SendPacket(memberList); + member.BroadcastRainbowTeamType(); + member.SendMsg(member.GetLanguage(gameDialogKey), MsgMessageType.Middle); + await member.EmitEventAsync(new RainbowBattleJoinEvent()); + } + } + } +} \ No newline at end of file diff --git a/srcs/Plugin.RainbowBattle/EventHandlers/RainbowBattleStartProcessRegistrationEventHandler.cs b/srcs/Plugin.RainbowBattle/EventHandlers/RainbowBattleStartProcessRegistrationEventHandler.cs new file mode 100644 index 0000000..8fb9b18 --- /dev/null +++ b/srcs/Plugin.RainbowBattle/EventHandlers/RainbowBattleStartProcessRegistrationEventHandler.cs @@ -0,0 +1,193 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Core; +using WingsEmu.Core.Extensions; +using WingsEmu.Game; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Game.RainbowBattle; +using WingsEmu.Game.RainbowBattle.Event; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.RainbowBattle.EventHandlers +{ + public class RainbowBattleStartProcessRegistrationEventHandler : IAsyncEventProcessor + { + private readonly IAsyncEventPipeline _asyncEventPipeline; + private readonly RainbowBattleConfiguration _rainbowBattleConfiguration; + private readonly IRainbowBattleManager _rainbowBattleManager; + private readonly IRandomGenerator _randomGenerator; + private readonly ISessionManager _sessionManager; + + public RainbowBattleStartProcessRegistrationEventHandler(ISessionManager sessionManager, IRainbowBattleManager rainbowBattleManager, RainbowBattleConfiguration rainbowBattleConfiguration, + IAsyncEventPipeline asyncEventPipeline, IRandomGenerator randomGenerator) + { + _sessionManager = sessionManager; + _rainbowBattleManager = rainbowBattleManager; + _rainbowBattleConfiguration = rainbowBattleConfiguration; + _asyncEventPipeline = asyncEventPipeline; + _randomGenerator = randomGenerator; + } + + public async Task HandleAsync(RainbowBattleStartProcessRegistrationEvent e, CancellationToken cancellation) + { + _rainbowBattleManager.DisableBattleRainbowRegistration(); + IEnumerable registeredPlayers = _rainbowBattleManager.RegisteredPlayers.ToArray(); + _rainbowBattleManager.ClearRegisteredPlayers(); + + _sessionManager.Broadcast(x => x.GenerateEsfPacket(0)); + + // Check if player can join to Rainbow Battle + HashSet sessions = new(); + foreach (long playerId in registeredPlayers) + { + IClientSession session = _sessionManager.GetSessionByCharacterId(playerId); + if (session == null) + { + continue; + } + + session.SendBsInfoPacket(BsInfoType.CloseWindow, GameType.RainbowBattle, 0, QueueWindow.WaitForEntry); + if (!session.CanJoinToRainbowBattle()) + { + continue; + } + + sessions.Add(session); + } + + List> levelRanges = _rainbowBattleConfiguration.LevelRange; + + var dictionary = new Dictionary, HashSet>(); + var refusedSessions = new HashSet(); + + // Check if player is in level range + foreach (IClientSession session in sessions) + { + byte level = session.PlayerEntity.Level; + Range? levelRange = levelRanges.FirstOrDefault(x => level >= x.Minimum && level <= x.Maximum); + if (levelRange == null) + { + refusedSessions.Add(session); + continue; + } + + if (!dictionary.TryGetValue(levelRange, out HashSet list)) + { + list = new HashSet(); + dictionary[levelRange] = list; + } + + list.Add(session); + } + + foreach (IClientSession refusedSession in refusedSessions) + { + refusedSession.SendMsg(refusedSession.GetLanguage(GameDialogKey.RAINBOW_BATTLE_MESSAGE_LOW_LEVEL), MsgMessageType.Middle); + refusedSession.SendChatMessage(refusedSession.GetLanguage(GameDialogKey.RAINBOW_BATTLE_MESSAGE_LOW_LEVEL), ChatMessageColorType.Red); + } + + List teams = new(); + var notEnoughPlayers = new HashSet(); + + foreach (HashSet sessionList in dictionary.Values) // 100 players + { + if (sessionList.Count < _rainbowBattleConfiguration.MinimumPlayers) + { + foreach (IClientSession session in sessionList) + { + notEnoughPlayers.Add(session); + } + + continue; + } + + var teamsList = sessionList.Split(_rainbowBattleConfiguration.MaximumPlayers).ToList(); + + if (teamsList.Count > 1) + { + var getLastList = teamsList[^1].ToList(); + var getPreviousList = teamsList[^2].ToList(); + + if (getLastList.Count <= 14) + { + int x = getLastList.Count + getPreviousList.Count; // 10 + 30 + int half = x / 2; // 40 / 2 = 20 + int toRemove = half - getLastList.Count; // 20 - 10 = 10 + IClientSession[] previousSession = getPreviousList.TakeLast(toRemove).ToArray(); + getLastList.AddRange(previousSession); + foreach (IClientSession session in previousSession) + { + getPreviousList.Remove(session); + } + + teamsList[^1] = getLastList; + teamsList[^2] = getPreviousList; + } + } + + // split it into 30, 30, 30, 10 + // if getLastList.Count <= 14, take previous list and split in half + // 30 + 10 = 40 + // split in half = 20/20 + foreach (IEnumerable members in teamsList) + { + var membersList = members.ToList(); + + var redTeam = new List(15); + var blueTeam = new List(15); + int randomNumber = _randomGenerator.RandomNumber(0, 2); + + for (int i = 0; i < membersList.Count; i++) + { + IClientSession member = membersList[i]; + int modulo = (randomNumber + i) % 2; + switch (modulo) + { + case 0: + redTeam.Add(member); + break; + case 1: + blueTeam.Add(member); + break; + } + } + + teams.Add(new RainbowTeam + { + RedTeam = redTeam, + BlueTeam = blueTeam + }); + } + } + + foreach (IClientSession session in notEnoughPlayers) + { + session.SendMsg(session.GetLanguage(GameDialogKey.RAINBOW_BATTLE_MESSAGE_NOT_ENOUGH_PLAYERS), MsgMessageType.Middle); + session.SendChatMessage(session.GetLanguage(GameDialogKey.RAINBOW_BATTLE_MESSAGE_NOT_ENOUGH_PLAYERS), ChatMessageColorType.Red); + } + + foreach (RainbowTeam team in teams) + { + await _asyncEventPipeline.ProcessEventAsync(new RainbowBattleStartEvent + { + RedTeam = team.RedTeam, + BlueTeam = team.BlueTeam + }); + } + } + + private class RainbowTeam + { + public List RedTeam { get; init; } + public List BlueTeam { get; init; } + } + } +} \ No newline at end of file diff --git a/srcs/Plugin.RainbowBattle/EventHandlers/RainbowBattleStartRegisterEventHandler.cs b/srcs/Plugin.RainbowBattle/EventHandlers/RainbowBattleStartRegisterEventHandler.cs new file mode 100644 index 0000000..98dc684 --- /dev/null +++ b/srcs/Plugin.RainbowBattle/EventHandlers/RainbowBattleStartRegisterEventHandler.cs @@ -0,0 +1,40 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking.Broadcasting; +using WingsEmu.Game.RainbowBattle; +using WingsEmu.Game.RainbowBattle.Event; +using WingsEmu.Packets.Enums; + +namespace Plugin.RainbowBattle.EventHandlers +{ + public class RainbowBattleStartRegisterEventHandler : IAsyncEventProcessor + { + private readonly IRainbowBattleManager _rainbowBattleManager; + private readonly ISessionManager _sessionManager; + + public RainbowBattleStartRegisterEventHandler(IRainbowBattleManager rainbowBattleManager, ISessionManager sessionManager) + { + _rainbowBattleManager = rainbowBattleManager; + _sessionManager = sessionManager; + } + + public async Task HandleAsync(RainbowBattleStartRegisterEvent e, CancellationToken cancellation) + { + if (_rainbowBattleManager.IsRegistrationActive) + { + return; + } + + _rainbowBattleManager.RainbowBattleProcessTime = null; + _rainbowBattleManager.EnableBattleRainbowRegistration(); + + _sessionManager.Broadcast(x => x.GenerateEventAsk(QnamlType.RainbowBattle, "guri 503", + x.GetLanguageFormat(GameDialogKey.GAMEEVENT_DIALOG_ASK_PARTICIPATE, x.GetLanguage(GameDialogKey.RAINBOW_BATTLE_EVENT_NAME))), + new InBaseMapBroadcast(), new NotMutedBroadcast()); + } + } +} \ No newline at end of file diff --git a/srcs/Plugin.RainbowBattle/EventHandlers/RainbowBattleUnfreezeEventHandler.cs b/srcs/Plugin.RainbowBattle/EventHandlers/RainbowBattleUnfreezeEventHandler.cs new file mode 100644 index 0000000..d53eebd --- /dev/null +++ b/srcs/Plugin.RainbowBattle/EventHandlers/RainbowBattleUnfreezeEventHandler.cs @@ -0,0 +1,167 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.Families; +using WingsAPI.Game.Extensions.Groups; +using WingsAPI.Packets.Enums.Rainbow; +using WingsEmu.Game; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage.Configuration; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Networking.Broadcasting; +using WingsEmu.Game.RainbowBattle; +using WingsEmu.Game.RainbowBattle.Event; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.RainbowBattle.EventHandlers +{ + public class RainbowBattleUnfreezeEventHandler : IAsyncEventProcessor + { + private readonly IBuffFactory _buffFactory; + private readonly IGameLanguageService _gameLanguageService; + private readonly RainbowBattleConfiguration _rainbowBattleConfiguration; + private readonly IRandomGenerator _randomGenerator; + private readonly IRankingManager _rankingManager; + private readonly IReputationConfiguration _reputationConfiguration; + private readonly ISpPartnerConfiguration _spPartner; + + public RainbowBattleUnfreezeEventHandler(IGameLanguageService gameLanguageService, ISpPartnerConfiguration spPartner, RainbowBattleConfiguration rainbowBattleConfiguration, + IRandomGenerator randomGenerator, IBuffFactory buffFactory, IReputationConfiguration reputationConfiguration, IRankingManager rankingManager) + { + _gameLanguageService = gameLanguageService; + _spPartner = spPartner; + _rainbowBattleConfiguration = rainbowBattleConfiguration; + _randomGenerator = randomGenerator; + _buffFactory = buffFactory; + _reputationConfiguration = reputationConfiguration; + _rankingManager = rankingManager; + } + + public async Task HandleAsync(RainbowBattleUnfreezeEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + IPlayerEntity target = e.Unfreezer; + + RainbowBattleParty rainbowBattleParty = session.PlayerEntity.RainbowBattleComponent.RainbowBattleParty; + IMapInstance mapInstance = rainbowBattleParty?.MapInstance; + if (rainbowBattleParty == null || mapInstance == null) + { + return; + } + + if (!session.PlayerEntity.RainbowBattleComponent.IsFrozen) + { + return; + } + + if (target != null) + { + if (!target.RainbowBattleComponent.IsInRainbowBattle) + { + return; + } + + if (target.RainbowBattleComponent.Team != session.PlayerEntity.RainbowBattleComponent.Team) + { + return; + } + + if (target.RainbowBattleComponent.IsFrozen) + { + return; + } + + if (target.Position.GetDistance(session.PlayerEntity.Position) > 5) + { + return; + } + } + + session.PlayerEntity.Hp = session.PlayerEntity.MaxHp; + session.PlayerEntity.Mp = session.PlayerEntity.MaxMp; + session.PlayerEntity.RainbowBattleComponent.IsFrozen = false; + session.PlayerEntity.RainbowBattleComponent.FrozenTime = null; + await session.PlayerEntity.RemoveNegativeBuffs(100); + session.SendCondPacket(); + session.RefreshStat(); + + foreach (IMateEntity mate in session.PlayerEntity.MateComponent.TeamMembers()) + { + session.PlayerEntity.MapInstance.AddMate(mate); + } + + if ((session.PlayerEntity.RainbowBattleComponent.Deaths % 3) == 0) + { + Buff angry = _buffFactory.CreateBuff((short)BuffVnums.ANGRY, session.PlayerEntity); + await session.PlayerEntity.AddBuffAsync(angry); + } + + session.BroadcastIn(_reputationConfiguration, _rankingManager.TopReputation, new ExceptSessionBroadcast(session)); + session.BroadcastGidx(session.PlayerEntity.Family, _gameLanguageService); + session.BroadcastRainbowTeamType(); + session.BroadcastInTeamMembers(_gameLanguageService, _spPartner); + session.RefreshParty(_spPartner); + + if (target != null) + { + IReadOnlyList members = session.PlayerEntity.RainbowBattleComponent.Team == RainbowBattleTeamType.Red ? rainbowBattleParty.RedTeam : rainbowBattleParty.BlueTeam; + + foreach (IClientSession member in members) + { + member.SendMsg(member.GetLanguageFormat(GameDialogKey.RAINBOW_BATTLE_SHOUTMESSAGE_UNFROZEN, session.PlayerEntity.Name), MsgMessageType.Middle); + } + + target.RainbowBattleComponent.ActivityPoints += _rainbowBattleConfiguration.UnfreezeActivityPoints; + return; + } + + // It's by time so move frozen player to start position / main flag + + Buff buff = _buffFactory.CreateBuff((short)BuffVnums.INVICIBLE_IN_PVP, session.PlayerEntity, BuffFlag.NORMAL); + await session.PlayerEntity.AddBuffAsync(buff); + + switch (session.PlayerEntity.RainbowBattleComponent.Team) + { + case RainbowBattleTeamType.Red: + + short randomX = (short)_randomGenerator.RandomNumber(_rainbowBattleConfiguration.RedStartX, _rainbowBattleConfiguration.RedEndX + 1); + short randomY = (short)_randomGenerator.RandomNumber(_rainbowBattleConfiguration.RedStartY, _rainbowBattleConfiguration.RedEndY + 1); + + if (mapInstance.IsBlockedZone(randomX, randomY)) + { + randomX = 0; + randomY = 34; + } + + session.PlayerEntity.TeleportOnMap(randomX, randomY, true); + + break; + case RainbowBattleTeamType.Blue: + + randomX = (short)_randomGenerator.RandomNumber(_rainbowBattleConfiguration.BlueStartX, _rainbowBattleConfiguration.BlueEndX + 1); + randomY = (short)_randomGenerator.RandomNumber(_rainbowBattleConfiguration.BlueStartY, _rainbowBattleConfiguration.BlueEndY + 1); + + if (mapInstance.IsBlockedZone(randomX, randomY)) + { + randomX = 117; + randomY = 42; + } + + session.PlayerEntity.TeleportOnMap(randomX, randomY, true); + + break; + } + } + } +} \ No newline at end of file diff --git a/srcs/Plugin.RainbowBattle/EventHandlers/RainbowBattleUnfreezeProcessEventHandler.cs b/srcs/Plugin.RainbowBattle/EventHandlers/RainbowBattleUnfreezeProcessEventHandler.cs new file mode 100644 index 0000000..eafbcd2 --- /dev/null +++ b/srcs/Plugin.RainbowBattle/EventHandlers/RainbowBattleUnfreezeProcessEventHandler.cs @@ -0,0 +1,41 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Networking; +using WingsEmu.Game.RainbowBattle; +using WingsEmu.Game.RainbowBattle.Event; + +namespace Plugin.RainbowBattle.EventHandlers +{ + public class RainbowBattleUnfreezeProcessEventHandler : IAsyncEventProcessor + { + public async Task HandleAsync(RainbowBattleUnfreezeProcessEvent e, CancellationToken cancellation) + { + RainbowBattleParty rainbowParty = e.RainbowBattleParty; + + DateTime now = DateTime.UtcNow; + foreach (IClientSession session in rainbowParty.MapInstance.Sessions) + { + if (!session.PlayerEntity.RainbowBattleComponent.IsFrozen) + { + continue; + } + + if (!session.PlayerEntity.RainbowBattleComponent.FrozenTime.HasValue) + { + // isFrozen and Frozen time is null? Unfreeze player + await session.EmitEventAsync(new RainbowBattleUnfreezeEvent()); + continue; + } + + if (session.PlayerEntity.RainbowBattleComponent.FrozenTime > now) + { + continue; + } + + await session.EmitEventAsync(new RainbowBattleUnfreezeEvent()); + } + } + } +} \ No newline at end of file diff --git a/srcs/Plugin.RainbowBattle/Managers/IRainbowFactory.cs b/srcs/Plugin.RainbowBattle/Managers/IRainbowFactory.cs new file mode 100644 index 0000000..12ea6a6 --- /dev/null +++ b/srcs/Plugin.RainbowBattle/Managers/IRainbowFactory.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using WingsAPI.Packets.Enums.Rainbow; +using WingsEmu.Game._enum; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Entities.Event; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Networking; +using WingsEmu.Game.RainbowBattle; + +namespace Plugin.RainbowBattle.Managers +{ + public interface IRainbowFactory + { + Task CreateRainbowBattle(List redTeam, List blueTeam); + } + + public class RainbowFactory : IRainbowFactory + { + private readonly IMapManager _mapManager; + private readonly INpcEntityFactory _npcEntityFactory; + private readonly RainbowBattleConfiguration _rainbowBattleConfiguration; + + public RainbowFactory(IMapManager mapManager, INpcEntityFactory npcEntityFactory, RainbowBattleConfiguration rainbowBattleConfiguration) + { + _mapManager = mapManager; + _npcEntityFactory = npcEntityFactory; + _rainbowBattleConfiguration = rainbowBattleConfiguration; + } + + public async Task CreateRainbowBattle(List redTeam, List blueTeam) + { + IMapInstance mapInstance = _mapManager.GenerateMapInstanceByMapId(_rainbowBattleConfiguration.MapId, MapInstanceType.RainbowBattle); + if (mapInstance == null) + { + return null; + } + + foreach (FlagPosition position in _rainbowBattleConfiguration.MainFlags) + { + INpcEntity mainFlag = _npcEntityFactory.CreateNpc((short)MonsterVnum.BIG_FLAG, mapInstance, null, new NpcAdditionalData + { + RainbowFlag = new RainBowFlag + { + FlagType = RainbowBattleFlagType.Big, + FlagTeamType = RainbowBattleFlagTeamType.None + } + }); + + await mainFlag.EmitEventAsync(new MapJoinNpcEntityEvent(mainFlag, position.X, position.Y)); + } + + foreach (FlagPosition position in _rainbowBattleConfiguration.MediumFlags) + { + INpcEntity mediumFlag = _npcEntityFactory.CreateNpc((short)MonsterVnum.MEDIUM_FLAG, mapInstance, null, new NpcAdditionalData + { + RainbowFlag = new RainBowFlag + { + FlagType = RainbowBattleFlagType.Medium, + FlagTeamType = RainbowBattleFlagTeamType.None + } + }); + + await mediumFlag.EmitEventAsync(new MapJoinNpcEntityEvent(mediumFlag, position.X, position.Y)); + } + + foreach (FlagPosition position in _rainbowBattleConfiguration.SmallFlags) + { + INpcEntity smallFlag = _npcEntityFactory.CreateNpc((short)MonsterVnum.SMALL_FLAG, mapInstance, null, new NpcAdditionalData + { + RainbowFlag = new RainBowFlag + { + FlagType = RainbowBattleFlagType.Small, + FlagTeamType = RainbowBattleFlagTeamType.None + } + }); + + await smallFlag.EmitEventAsync(new MapJoinNpcEntityEvent(smallFlag, position.X, position.Y)); + } + + mapInstance.Initialize(DateTime.UtcNow.AddSeconds(-1)); + + var rainbowParty = new RainbowBattleParty(redTeam, blueTeam) + { + MapInstance = mapInstance + }; + return rainbowParty; + } + } +} \ No newline at end of file diff --git a/srcs/Plugin.RainbowBattle/Managers/RainbowBattleLeaverBusterResetMessageConsumer.cs b/srcs/Plugin.RainbowBattle/Managers/RainbowBattleLeaverBusterResetMessageConsumer.cs new file mode 100644 index 0000000..e6d1460 --- /dev/null +++ b/srcs/Plugin.RainbowBattle/Managers/RainbowBattleLeaverBusterResetMessageConsumer.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; +using WingsAPI.Communication.RainbowBattle; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Game.RainbowBattle.Event; + +namespace Plugin.RainbowBattle.Managers +{ + public class RainbowBattleLeaverBusterResetMessageConsumer : IMessageConsumer + { + private readonly ISessionManager _sessionManager; + + public RainbowBattleLeaverBusterResetMessageConsumer(ISessionManager sessionManager) => _sessionManager = sessionManager; + + public async Task HandleAsync(RainbowBattleLeaverBusterResetMessage notification, CancellationToken token) + { + IReadOnlyList sessions = _sessionManager.Sessions; + foreach (IClientSession session in sessions) + { + await session.EmitEventAsync(new RainbowBattleLeaverBusterRefreshEvent + { + Force = notification.Force + }); + } + } + } +} \ No newline at end of file diff --git a/srcs/Plugin.RainbowBattle/Managers/RainbowBattleStartMessageConsumer.cs b/srcs/Plugin.RainbowBattle/Managers/RainbowBattleStartMessageConsumer.cs new file mode 100644 index 0000000..f2c4487 --- /dev/null +++ b/srcs/Plugin.RainbowBattle/Managers/RainbowBattleStartMessageConsumer.cs @@ -0,0 +1,21 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; +using WingsAPI.Communication.RainbowBattle; +using WingsEmu.Game.RainbowBattle; + +namespace Plugin.RainbowBattle.Managers +{ + public class RainbowBattleStartMessageConsumer : IMessageConsumer + { + private readonly IRainbowBattleManager _rainbowBattleManager; + + public RainbowBattleStartMessageConsumer(IRainbowBattleManager rainbowBattleManager) => _rainbowBattleManager = rainbowBattleManager; + + public async Task HandleAsync(RainbowBattleStartMessage notification, CancellationToken token) + { + _rainbowBattleManager.RainbowBattleProcessTime = DateTime.UtcNow; + } + } +} \ No newline at end of file diff --git a/srcs/Plugin.RainbowBattle/Plugin.RainbowBattle.csproj b/srcs/Plugin.RainbowBattle/Plugin.RainbowBattle.csproj new file mode 100644 index 0000000..9024b42 --- /dev/null +++ b/srcs/Plugin.RainbowBattle/Plugin.RainbowBattle.csproj @@ -0,0 +1,17 @@ + + + + net5.0 + + + + + + + + + + + + + diff --git a/srcs/Plugin.RainbowBattle/RainbowBattlePluginCore.cs b/srcs/Plugin.RainbowBattle/RainbowBattlePluginCore.cs new file mode 100644 index 0000000..218db71 --- /dev/null +++ b/srcs/Plugin.RainbowBattle/RainbowBattlePluginCore.cs @@ -0,0 +1,48 @@ +using Microsoft.Extensions.DependencyInjection; +using PhoenixLib.Events; +using PhoenixLib.ServiceBus.Extensions; +using Plugin.RainbowBattle.Command; +using Plugin.RainbowBattle.Managers; +using Plugin.RainbowBattle.RecurrentJob; +using WingsAPI.Communication.RainbowBattle; +using WingsAPI.Communication.ServerApi.Protocol; +using WingsAPI.Plugins; +using WingsEmu.Commands.Interfaces; +using WingsEmu.Game.RainbowBattle; + +namespace Plugin.RainbowBattle +{ + public class RainbowBattlePluginCore : IGameServerPlugin + { + public string Name => nameof(RainbowBattlePluginCore); + + public void AddDependencies(IServiceCollection services, GameServerLoader gameServer) + { + services.AddEventHandlersInAssembly(); + services.AddSingleton(); + services.AddMessageSubscriber(); + services.AddMessageSubscriber(); + services.AddSingleton(); + if (gameServer.Type == GameChannelType.ACT_4) + { + return; + } + + services.AddHostedService(); + } + } + + public class RainbowBattlePlugin : IGamePlugin + { + private readonly ICommandContainer _commandContainer; + + public RainbowBattlePlugin(ICommandContainer commandContainer) => _commandContainer = commandContainer; + + public string Name => nameof(RainbowBattlePlugin); + + public void OnLoad() + { + _commandContainer.AddModule(); + } + } +} \ No newline at end of file diff --git a/srcs/Plugin.RainbowBattle/RecurrentJob/RainbowBattleSystem.cs b/srcs/Plugin.RainbowBattle/RecurrentJob/RainbowBattleSystem.cs new file mode 100644 index 0000000..d89c97c --- /dev/null +++ b/srcs/Plugin.RainbowBattle/RecurrentJob/RainbowBattleSystem.cs @@ -0,0 +1,294 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using PhoenixLib.Events; +using PhoenixLib.Logging; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Game.RainbowBattle; +using WingsEmu.Game.RainbowBattle.Event; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.RainbowBattle.RecurrentJob +{ + public class RainbowBattleSystem : BackgroundService + { + private static readonly TimeSpan Interval = TimeSpan.FromSeconds(1); + private static readonly TimeSpan Start = TimeSpan.FromMinutes(5); + + private static List<(TimeSpan, int, TimeType)> _times = new(); + private readonly IAsyncEventPipeline _eventPipeline; + private readonly IRainbowBattleManager _rainbowBattleManager; + private readonly ISessionManager _sessionManager; + + public RainbowBattleSystem(IAsyncEventPipeline eventPipeline, IRainbowBattleManager rainbowBattleManager, ISessionManager sessionManager) + { + _eventPipeline = eventPipeline; + _rainbowBattleManager = rainbowBattleManager; + _sessionManager = sessionManager; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + Log.Info("[RAINBOW_SYSTEM] Start Rainbow Battle system..."); + + _times = _rainbowBattleManager.Warnings.ToList(); + + while (!stoppingToken.IsCancellationRequested) + { + try + { + await MainProcess(); + } + catch (Exception e) + { + Log.Error("[RAINBOW_SYSTEM] ", e); + } + + await Task.Delay(Interval, stoppingToken); + } + } + + private async Task MainProcess() + { + DateTime dateNow = DateTime.UtcNow; + switch (_rainbowBattleManager.IsRegistrationActive) + { + case true: + await ProcessRegistration(dateNow); + return; + case false when !_rainbowBattleManager.IsActive: + ProcessTime(dateNow); + await ProcessStart(dateNow); + return; + } + + if (_rainbowBattleManager.IsActive && !_rainbowBattleManager.RainbowBattleParties.Any()) + { + _rainbowBattleManager.IsActive = false; + _times.Clear(); + _times = _rainbowBattleManager.Warnings.ToList(); + return; + } + + foreach (RainbowBattleParty rainbowBattleParty in _rainbowBattleManager.RainbowBattleParties) + { + await ProcessRainbowBattle(rainbowBattleParty, dateNow); + } + } + + private async Task ProcessStart(DateTime dateNow) + { + if (_rainbowBattleManager.RainbowBattleProcessTime is null) + { + return; + } + + if (dateNow < _rainbowBattleManager.RainbowBattleProcessTime + Start) + { + return; + } + + _times.Clear(); + _times = _rainbowBattleManager.Warnings.ToList(); + await _eventPipeline.ProcessEventAsync(new RainbowBattleStartRegisterEvent()); + } + + private void ProcessTime(DateTime dateNow) + { + if (_rainbowBattleManager.RainbowBattleProcessTime is null) + { + return; + } + + if (_times.Count < 1) + { + return; + } + + (TimeSpan, int, TimeType) warning = _times.OrderBy(x => x.Item1).First(); + + if (dateNow < _rainbowBattleManager.RainbowBattleProcessTime + warning.Item1) + { + return; + } + + _times.Remove(warning); + + GameDialogKey gameDialogKey = warning.Item3 == TimeType.SECONDS ? GameDialogKey.GAMEEVENT_SHOUTMESSAGE_PREPARATION_SECONDS : GameDialogKey.GAMEEVENT_SHOUTMESSAGE_PREPARATION_MINUTES; + + _sessionManager.Broadcast(x => + { + string rainbowBattleName = x.GetLanguage(GameDialogKey.RAINBOW_BATTLE_EVENT_NAME); + return x.GenerateMsgPacket(x.GetLanguageFormat(gameDialogKey, rainbowBattleName, warning.Item2), MsgMessageType.Middle); + }); + + _sessionManager.Broadcast(x => + { + string rainbowBattleName = x.GetLanguage(GameDialogKey.RAINBOW_BATTLE_EVENT_NAME); + return x.GenerateMsgPacket(x.GetLanguageFormat(gameDialogKey, rainbowBattleName, warning.Item2), MsgMessageType.BottomCard); + }); + } + + private async Task ProcessRegistration(DateTime dateTime) + { + if (_rainbowBattleManager.RegistrationStartTime > dateTime) + { + return; + } + + await _eventPipeline.ProcessEventAsync(new RainbowBattleStartProcessRegistrationEvent()); + } + + private async Task ProcessRainbowBattle(RainbowBattleParty rainbowBattleParty, DateTime dateTime) + { + ProcessStartGame(rainbowBattleParty, dateTime); + await ProcessFrozenMembers(rainbowBattleParty); + await TryEndRainbowBattle(rainbowBattleParty, dateTime); + await TryDestroyRainbowBattle(rainbowBattleParty, dateTime); + await ProcessTeamPoints(rainbowBattleParty, dateTime); + await ProcessActivityTeamPoints(rainbowBattleParty, dateTime); + await ProcessMembersLife(rainbowBattleParty, dateTime); + } + + private async Task ProcessActivityTeamPoints(RainbowBattleParty rainbowBattleParty, DateTime dateTime) + { + if (!rainbowBattleParty.Started || rainbowBattleParty.FinishTime != null) + { + return; + } + + if (rainbowBattleParty.LastActivityPointsTeamAdd > dateTime) + { + return; + } + + rainbowBattleParty.LastActivityPointsTeamAdd = dateTime.AddSeconds(59); + await _eventPipeline.ProcessEventAsync(new RainbowBattleProcessActivityPointsEvent + { + RainbowBattleParty = rainbowBattleParty + }); + } + + private async Task ProcessFrozenMembers(RainbowBattleParty rainbowBattleParty) + { + if (!rainbowBattleParty.Started) + { + return; + } + + await _eventPipeline.ProcessEventAsync(new RainbowBattleUnfreezeProcessEvent + { + RainbowBattleParty = rainbowBattleParty + }); + } + + private async Task ProcessTeamPoints(RainbowBattleParty rainbowBattleParty, DateTime dateTime) + { + if (!rainbowBattleParty.Started || rainbowBattleParty.FinishTime != null) + { + return; + } + + if (rainbowBattleParty.LastPointsTeamAdd > dateTime) + { + return; + } + + rainbowBattleParty.LastPointsTeamAdd = dateTime.AddSeconds(30); + await _eventPipeline.ProcessEventAsync(new RainbowBattleProcessFlagPointsEvent + { + RainbowBattleParty = rainbowBattleParty + }); + } + + private async Task TryEndRainbowBattle(RainbowBattleParty rainbowBattleParty, DateTime dateTime) + { + if (!rainbowBattleParty.Started || rainbowBattleParty.EndTime > dateTime || rainbowBattleParty.FinishTime != null) + { + return; + } + + await _eventPipeline.ProcessEventAsync(new RainbowBattleEndEvent + { + RainbowBattleParty = rainbowBattleParty + }); + } + + private async Task TryDestroyRainbowBattle(RainbowBattleParty rainbowBattleParty, DateTime dateTime) + { + if (rainbowBattleParty.MapInstance != null && rainbowBattleParty.MapInstance.Sessions.Count < 1) + { + Log.Warn("[RAINBOW_SYSTEM] Destroying Rainbow Battle instance"); + await _eventPipeline.ProcessEventAsync(new RainbowBattleDestroyEvent + { + RainbowBattleParty = rainbowBattleParty + }); + return; + } + + if (!rainbowBattleParty.Started || rainbowBattleParty.FinishTime == null) + { + return; + } + + if (rainbowBattleParty.FinishTime > dateTime) + { + return; + } + + Log.Warn("[RAINBOW_SYSTEM] Destroying Rainbow Battle instance"); + await _eventPipeline.ProcessEventAsync(new RainbowBattleDestroyEvent + { + RainbowBattleParty = rainbowBattleParty + }); + } + + private async Task ProcessMembersLife(RainbowBattleParty rainbowBattleParty, DateTime dateTime) + { + if (!rainbowBattleParty.Started || rainbowBattleParty.FinishTime != null) + { + return; + } + + if (rainbowBattleParty.LastMembersLife > dateTime) + { + return; + } + + rainbowBattleParty.LastMembersLife = dateTime.AddSeconds(2); + await _eventPipeline.ProcessEventAsync(new RainbowBattleProcessLifeEvent + { + RainbowBattleParty = rainbowBattleParty + }); + } + + private void ProcessStartGame(RainbowBattleParty rainbowBattleParty, in DateTime time) + { + if (rainbowBattleParty.Started) + { + return; + } + + if (rainbowBattleParty.StartTime.AddSeconds(5) > time) + { + return; + } + + rainbowBattleParty.Started = true; + + foreach (IClientSession session in rainbowBattleParty.MapInstance.Sessions) + { + session.SendCondPacket(); + } + + rainbowBattleParty.MapInstance.Broadcast(x => x.GenerateMsgPacket(x.GetLanguage(GameDialogKey.RAINBOW_BATTLE_SHOUTMESSAGE_START), MsgMessageType.Middle)); + } + } +} \ No newline at end of file diff --git a/srcs/RelationServer/Consumer/RelationCharacterConnectMessageConsumer.cs b/srcs/RelationServer/Consumer/RelationCharacterConnectMessageConsumer.cs new file mode 100644 index 0000000..5e67755 --- /dev/null +++ b/srcs/RelationServer/Consumer/RelationCharacterConnectMessageConsumer.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; +using WingsEmu.DTOs.Relations; +using WingsEmu.Plugins.DistributedGameEvents.PlayerEvents; +using WingsEmu.Plugins.DistributedGameEvents.Relation; + +namespace RelationServer.Consumer +{ + public class RelationCharacterConnectMessageConsumer : IMessageConsumer + { + private readonly IMessagePublisher _messagePublisher; + private readonly ICharacterRelationDAO _relationDao; + + public RelationCharacterConnectMessageConsumer(ICharacterRelationDAO relationDao, IMessagePublisher messagePublisher) + { + _relationDao = relationDao; + _messagePublisher = messagePublisher; + } + + public async Task HandleAsync(PlayerConnectedOnChannelMessage notification, CancellationToken token) + { + long characterId = notification.CharacterId; + List relations = await _relationDao.LoadRelationsByCharacterIdAsync(characterId); + + if (!relations.Any()) + { + return; + } + + await _messagePublisher.PublishAsync(new RelationCharacterJoinMessage + { + CharacterId = characterId, + CharacterName = notification.CharacterName, + Relations = relations + }); + } + } +} \ No newline at end of file diff --git a/srcs/RelationServer/Consumer/RelationCharacterDisconnectMessageConsumer.cs b/srcs/RelationServer/Consumer/RelationCharacterDisconnectMessageConsumer.cs new file mode 100644 index 0000000..9f854bc --- /dev/null +++ b/srcs/RelationServer/Consumer/RelationCharacterDisconnectMessageConsumer.cs @@ -0,0 +1,24 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; +using WingsEmu.Plugins.DistributedGameEvents.PlayerEvents; +using WingsEmu.Plugins.DistributedGameEvents.Relation; + +namespace RelationServer.Consumer +{ + public class RelationCharacterDisconnectMessageConsumer : IMessageConsumer + { + private readonly IMessagePublisher _messagePublisher; + + public RelationCharacterDisconnectMessageConsumer(IMessagePublisher messagePublisher) => _messagePublisher = messagePublisher; + + public async Task HandleAsync(PlayerDisconnectedChannelMessage notification, CancellationToken token) + { + await _messagePublisher.PublishAsync(new RelationCharacterLeaveMessage + { + CharacterId = notification.CharacterId, + CharacterName = notification.CharacterName + }); + } + } +} \ No newline at end of file diff --git a/srcs/RelationServer/DockerGracefulStopService.cs b/srcs/RelationServer/DockerGracefulStopService.cs new file mode 100644 index 0000000..970c21c --- /dev/null +++ b/srcs/RelationServer/DockerGracefulStopService.cs @@ -0,0 +1,54 @@ +using System; +using System.Runtime.Loader; +using System.Threading; +using PhoenixLib.Logging; + +namespace RelationServer +{ + public class DockerGracefulStopService : IDisposable + { + private readonly CancellationTokenSource _cancellationTokenSource; + private readonly ManualResetEventSlim _stoppedEvent; + + public DockerGracefulStopService() + { + _cancellationTokenSource = new CancellationTokenSource(); + _stoppedEvent = new ManualResetEventSlim(); + + // SIGINT + Console.CancelKeyPress += (sender, eventArgs) => + { + Log.Error("[GRACEFUL_SHUTDOWN] SIGINT received", new Exception("[GRACEFUL_SHUTDOWN] SIGINT received")); + GracefulStop(_cancellationTokenSource, _stoppedEvent); + }; + + // SIGTERM + AssemblyLoadContext.Default.Unloading += context => + { + Log.Error("[GRACEFUL_SHUTDOWN] SIGTERM received", new Exception("[GRACEFUL_SHUTDOWN] SIGTERM received")); + GracefulStop(_cancellationTokenSource, _stoppedEvent); + }; + + AppDomain.CurrentDomain.UnhandledException += (sender, args) => + { + Log.Error("UnhandledException", args.ExceptionObject as Exception); + GracefulStop(_cancellationTokenSource, _stoppedEvent); + }; + } + + public CancellationToken CancellationToken => _cancellationTokenSource.Token; + + public void Dispose() + { + _stoppedEvent.Set(); + } + + private static void GracefulStop(CancellationTokenSource cancellationTokenSource, ManualResetEventSlim stoppedEvent) + { + Log.Info("DockerGracefulStopService Stopping service"); + cancellationTokenSource.Cancel(); + stoppedEvent.Wait(); + Log.Info("DockerGracefulStopService Stop finished"); + } + } +} \ No newline at end of file diff --git a/srcs/RelationServer/Program.cs b/srcs/RelationServer/Program.cs new file mode 100644 index 0000000..c437d2a --- /dev/null +++ b/srcs/RelationServer/Program.cs @@ -0,0 +1,77 @@ +using System; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using PhoenixLib.Logging; +using PhoenixLib.ServiceBus.MQTT; +using Plugin.Database.Mapping; +using ProtoBuf.Grpc.Client; + +namespace RelationServer +{ + public class Program + { + public static async Task Main(string[] args) + { + PrintHeader(); + NonGameMappingRules.InitializeMapping(); + GrpcClientFactory.AllowUnencryptedHttp2 = true; + using var stopService = new DockerGracefulStopService(); + using IHost host = CreateHostBuilder(args).Build(); + { + await host.StartAsync(); + IMessagingService messagingService = host.Services.GetRequiredService(); + await messagingService.StartAsync(); + Log.Info("RelationServer is working..."); + IServiceProvider services = host.Services; + await host.WaitForShutdownAsync(stopService.CancellationToken); + await messagingService.DisposeAsync(); + } + } + + private static void PrintHeader() + { + Console.Title = "WingsEmu - Master"; + const string text = @" +██╗ ██╗██╗███╗ ██╗ ██████╗ ███████╗███████╗███╗ ███╗██╗ ██╗ +██║ ██║██║████╗ ██║██╔════╝ ██╔════╝██╔════╝████╗ ████║██║ ██║ +██║ █╗ ██║██║██╔██╗ ██║██║ ███╗███████╗█████╗ ██╔████╔██║██║ ██║ +██║███╗██║██║██║╚██╗██║██║ ██║╚════██║██╔══╝ ██║╚██╔╝██║██║ ██║ +╚███╔███╔╝██║██║ ╚████║╚██████╔╝███████║███████╗██║ ╚═╝ ██║╚██████╔╝ + ╚══╝╚══╝ ╚═╝╚═╝ ╚═══╝ ╚═════╝ ╚══════╝╚══════╝╚═╝ ╚═╝ ╚═════╝ + +██████╗ ███████╗██╗ █████╗ ████████╗██╗ ██████╗ ███╗ ██╗ ███████╗███████╗██████╗ ██╗ ██╗███████╗██████╗ +██╔══██╗██╔════╝██║ ██╔══██╗╚══██╔══╝██║██╔═══██╗████╗ ██║ ██╔════╝██╔════╝██╔══██╗██║ ██║██╔════╝██╔══██╗ +██████╔╝█████╗ ██║ ███████║ ██║ ██║██║ ██║██╔██╗ ██║█████╗███████╗█████╗ ██████╔╝██║ ██║█████╗ ██████╔╝ +██╔══██╗██╔══╝ ██║ ██╔══██║ ██║ ██║██║ ██║██║╚██╗██║╚════╝╚════██║██╔══╝ ██╔══██╗╚██╗ ██╔╝██╔══╝ ██╔══██╗ +██║ ██║███████╗███████╗██║ ██║ ██║ ██║╚██████╔╝██║ ╚████║ ███████║███████╗██║ ██║ ╚████╔╝ ███████╗██║ ██║ +╚═╝ ╚═╝╚══════╝╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝ ╚══════╝╚══════╝╚═╝ ╚═╝ ╚═══╝ ╚══════╝╚═╝ ╚═╝ +"; + string separator = new('=', Console.WindowWidth); + string logo = text.Split('\n').Select(s => string.Format("{0," + (Console.WindowWidth / 2 + s.Length / 2) + "}\n", s)) + .Aggregate("", (current, i) => current + i); + + Console.ForegroundColor = ConsoleColor.Cyan; + Console.WriteLine(separator + logo + $"Version: {Assembly.GetExecutingAssembly().GetName().Version}\n" + separator); + Console.ForegroundColor = ConsoleColor.White; + } + + private static IHostBuilder CreateHostBuilder(string[] args) + { + IHostBuilder host = Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.ConfigureKestrel(s => + { + s.ListenAnyIP(short.Parse(Environment.GetEnvironmentVariable("RELATION_SERVER_PORT") ?? "21111"), options => { options.Protocols = HttpProtocols.Http2; }); + }); + webBuilder.UseStartup(); + }); + return host; + } + } +} \ No newline at end of file diff --git a/srcs/RelationServer/RelationServer.csproj b/srcs/RelationServer/RelationServer.csproj new file mode 100644 index 0000000..2dad148 --- /dev/null +++ b/srcs/RelationServer/RelationServer.csproj @@ -0,0 +1,24 @@ + + + + net5.0 + ..\..\dist\relation-server\ + false + + + + + + + + + + + + + + + + + + diff --git a/srcs/RelationServer/Services/RelationService.cs b/srcs/RelationServer/Services/RelationService.cs new file mode 100644 index 0000000..b6730d6 --- /dev/null +++ b/srcs/RelationServer/Services/RelationService.cs @@ -0,0 +1,226 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using PhoenixLib.Logging; +using PhoenixLib.ServiceBus; +using WingsAPI.Communication; +using WingsAPI.Communication.Relation; +using WingsAPI.Data.Character; +using WingsEmu.DTOs.Relations; +using WingsEmu.Packets.Enums.Relations; +using WingsEmu.Plugins.DistributedGameEvents.Relation; + +namespace RelationServer.Services +{ + public class RelationService : IRelationService + { + private readonly ICharacterDAO _characterDao; + private readonly IMessagePublisher _messagePublisher; + private readonly IMessagePublisher _messagePublisherRemove; + private readonly ICharacterRelationDAO _relationDao; + + public RelationService(ICharacterRelationDAO relationDao, ICharacterDAO characterDao, IMessagePublisher messagePublisher, + IMessagePublisher messagePublisherRemove) + { + _relationDao = relationDao; + _characterDao = characterDao; + _messagePublisher = messagePublisher; + _messagePublisherRemove = messagePublisherRemove; + } + + public async Task AddRelationAsync(RelationAddRequest request) + { + long characterId = request.CharacterId; + long targetId = request.TargetId; + CharacterRelationType relationType = request.RelationType; + string characterName = request.CharacterName; + + CharacterDTO target; + try + { + target = _characterDao.GetById(targetId); + } + catch (Exception e) + { + Log.Error("TargetAddRelationAsync", e); + return new RelationAddResponse + { + ResponseType = RpcResponseType.UNKNOWN_ERROR + }; + } + + if (target == null) + { + return new RelationAddResponse + { + ResponseType = RpcResponseType.UNKNOWN_ERROR + }; + } + + var senderRelation = new CharacterRelationDTO + { + CharacterId = characterId, + RelatedCharacterId = targetId, + RelationType = relationType, + RelatedName = target.Name + }; + + CharacterRelationDTO targetRelation = null; + + if (relationType != CharacterRelationType.Blocked) + { + targetRelation = new CharacterRelationDTO + { + CharacterId = targetId, + RelatedCharacterId = characterId, + RelationType = relationType, + RelatedName = characterName + }; + } + + try + { + await _relationDao.SaveRelationsByCharacterIdAsync(characterId, senderRelation); + } + catch (Exception e) + { + Log.Error("AddRelationAsync", e); + return new RelationAddResponse + { + ResponseType = RpcResponseType.UNKNOWN_ERROR + }; + } + + if (targetRelation == null) + { + await _messagePublisher.PublishAsync(new RelationCharacterAddMessage + { + SenderRelation = senderRelation, + TargetRelation = null + }); + + return new RelationAddResponse + { + ResponseType = RpcResponseType.SUCCESS, + SenderRelation = senderRelation, + TargetRelation = null + }; + } + + try + { + await _relationDao.SaveRelationsByCharacterIdAsync(targetId, targetRelation); + } + catch (Exception e) + { + Log.Error("AddRelationAsync", e); + return new RelationAddResponse + { + ResponseType = RpcResponseType.UNKNOWN_ERROR + }; + } + + await _messagePublisher.PublishAsync(new RelationCharacterAddMessage + { + SenderRelation = senderRelation, + TargetRelation = targetRelation + }); + + return new RelationAddResponse + { + ResponseType = RpcResponseType.SUCCESS, + SenderRelation = senderRelation, + TargetRelation = targetRelation + }; + } + + public async Task GetRelationsByIdAsync(RelationGetAllRequest request) + { + List dtos; + try + { + dtos = await _relationDao.LoadRelationsByCharacterIdAsync(request.CharacterId); + } + catch (Exception e) + { + Log.Error("[RELATION_SERVICE] Unexpected error: ", e); + return new RelationGetAllResponse + { + ResponseType = RpcResponseType.GENERIC_SERVER_ERROR + }; + } + + return new RelationGetAllResponse + { + ResponseType = RpcResponseType.SUCCESS, + CharacterRelationDtos = dtos + }; + } + + public async Task RemoveRelationAsync(RelationRemoveRequest request) + { + long characterId = request.CharacterId; + long targetId = request.TargetId; + CharacterRelationType relationType = request.RelationType; + CharacterRelationDTO senderRelation = await _relationDao.GetRelationByCharacterIdAsync(characterId, targetId); + + if (senderRelation == null) + { + return new BasicRpcResponse + { + ResponseType = RpcResponseType.UNKNOWN_ERROR + }; + } + + if (senderRelation.RelationType != relationType) + { + return new BasicRpcResponse + { + ResponseType = RpcResponseType.UNKNOWN_ERROR + }; + } + + try + { + await _relationDao.RemoveRelationAsync(senderRelation); + } + catch (Exception e) + { + Log.Error("RemoveRelationAsync", e); + return new BasicRpcResponse + { + ResponseType = RpcResponseType.UNKNOWN_ERROR + }; + } + + if (relationType != CharacterRelationType.Blocked) + { + CharacterRelationDTO targetRelation = await _relationDao.GetRelationByCharacterIdAsync(targetId, characterId); + try + { + await _relationDao.RemoveRelationAsync(targetRelation); + } + catch (Exception e) + { + Log.Error("RemoveRelationAsync", e); + return new BasicRpcResponse + { + ResponseType = RpcResponseType.UNKNOWN_ERROR + }; + } + } + + await _messagePublisherRemove.PublishAsync(new RelationCharacterRemoveMessage + { + RelationType = relationType, + CharacterId = characterId, + TargetId = targetId + }); + + return new BasicRpcResponse + { + ResponseType = RpcResponseType.SUCCESS + }; + } + } +} \ No newline at end of file diff --git a/srcs/RelationServer/Startup.cs b/srcs/RelationServer/Startup.cs new file mode 100644 index 0000000..617a23f --- /dev/null +++ b/srcs/RelationServer/Startup.cs @@ -0,0 +1,61 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using PhoenixLib.Events; +using PhoenixLib.Logging; +using PhoenixLib.ServiceBus.Extensions; +using Plugin.Database; +using ProtoBuf.Grpc.Server; +using RelationServer.Consumer; +using RelationServer.Services; +using WingsEmu.Health.Extensions; +using WingsEmu.Plugins.DistributedGameEvents.PlayerEvents; +using WingsEmu.Plugins.DistributedGameEvents.Relation; + +namespace RelationServer +{ + public class Startup + { + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) + { + services.AddMqttConfigurationFromEnv(); + services.AddEventPipeline(); + services.AddEventHandlersInAssembly(); + services.AddMaintenanceMode(); + services.AddPhoenixLogging(); + new DatabasePlugin().AddDependencies(services); + + services.AddSingleton(); + + services.AddCodeFirstGrpc(config => + { + config.MaxReceiveMessageSize = null; + config.MaxSendMessageSize = null; + config.EnableDetailedErrors = true; + }); + + services.AddMessagePublisher(); + services.AddMessagePublisher(); + services.AddMessagePublisher(); + services.AddMessagePublisher(); + services.AddMessageSubscriber(); + services.AddMessageSubscriber(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseRouting(); + + app.UseEndpoints(endpoints => { endpoints.MapGrpcService(); }); + } + } +} \ No newline at end of file diff --git a/srcs/Scheduler/Configs/InstantBattleStartFileConfiguration.cs b/srcs/Scheduler/Configs/InstantBattleStartFileConfiguration.cs new file mode 100644 index 0000000..26020d9 --- /dev/null +++ b/srcs/Scheduler/Configs/InstantBattleStartFileConfiguration.cs @@ -0,0 +1,11 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.ClusterScheduler.Configs +{ + public class InstantBattleStartFileConfiguration + { + public string CronExpression { get; set; } + } +} \ No newline at end of file diff --git a/srcs/Scheduler/DockerGracefulStopService.cs b/srcs/Scheduler/DockerGracefulStopService.cs new file mode 100644 index 0000000..f9e0069 --- /dev/null +++ b/srcs/Scheduler/DockerGracefulStopService.cs @@ -0,0 +1,54 @@ +using System; +using System.Runtime.Loader; +using System.Threading; +using PhoenixLib.Logging; + +namespace WingsEmu.ClusterCommunicator +{ + public class DockerGracefulStopService : IDisposable + { + private readonly CancellationTokenSource _cancellationTokenSource; + private readonly ManualResetEventSlim _stoppedEvent; + + public DockerGracefulStopService() + { + _cancellationTokenSource = new CancellationTokenSource(); + _stoppedEvent = new ManualResetEventSlim(); + + // SIGINT + Console.CancelKeyPress += (sender, eventArgs) => + { + Log.Error("[GRACEFUL_SHUTDOWN] SIGINT received", new Exception("[GRACEFUL_SHUTDOWN] SIGINT received")); + GracefulStop(_cancellationTokenSource, _stoppedEvent); + }; + + // SIGTERM + AssemblyLoadContext.Default.Unloading += context => + { + Log.Error("[GRACEFUL_SHUTDOWN] SIGTERM received", new Exception("[GRACEFUL_SHUTDOWN] SIGTERM received")); + GracefulStop(_cancellationTokenSource, _stoppedEvent); + }; + + AppDomain.CurrentDomain.UnhandledException += (sender, args) => + { + Log.Error("UnhandledException", args.ExceptionObject as Exception); + GracefulStop(_cancellationTokenSource, _stoppedEvent); + }; + } + + public CancellationToken CancellationToken => _cancellationTokenSource.Token; + + public void Dispose() + { + _stoppedEvent.Set(); + } + + private static void GracefulStop(CancellationTokenSource cancellationTokenSource, ManualResetEventSlim stoppedEvent) + { + Log.Info("DockerGracefulStopService Stopping service"); + cancellationTokenSource.Cancel(); + stoppedEvent.Wait(); + Log.Info("DockerGracefulStopService Stop finished"); + } + } +} \ No newline at end of file diff --git a/srcs/Scheduler/HangfireJobActivator.cs b/srcs/Scheduler/HangfireJobActivator.cs new file mode 100644 index 0000000..e73c21f --- /dev/null +++ b/srcs/Scheduler/HangfireJobActivator.cs @@ -0,0 +1,14 @@ +using System; +using Hangfire; + +namespace WingsEmu.ClusterScheduler +{ + public class HangfireJobActivator : JobActivator + { + private readonly IServiceProvider _serviceProvider; + + public HangfireJobActivator(IServiceProvider serviceProvider) => _serviceProvider = serviceProvider; + + public override object ActivateJob(Type type) => _serviceProvider.GetService(type); + } +} \ No newline at end of file diff --git a/srcs/Scheduler/Program.cs b/srcs/Scheduler/Program.cs new file mode 100644 index 0000000..9af356a --- /dev/null +++ b/srcs/Scheduler/Program.cs @@ -0,0 +1,72 @@ +using System; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using PhoenixLib.ServiceBus.MQTT; +using WingsEmu.ClusterCommunicator; + +namespace WingsEmu.ClusterScheduler +{ + public class Program + { + public static async Task Main(string[] args) + { + PrintHeader(); + using var stopService = new DockerGracefulStopService(); + using IHost host = CreateHostBuilder(args).Build(); + { + await host.StartAsync(); + IMessagingService messagingService = host.Services.GetRequiredService(); + await messagingService.StartAsync(); + + // warmup + + + await host.WaitForShutdownAsync(stopService.CancellationToken); + await messagingService.DisposeAsync(); + } + } + + // Additional configuration is required to successfully run gRPC on macOS. + // For instructions on how to configure Kestrel and gRPC clients on macOS, visit https://go.microsoft.com/fwlink/?linkid=2099682 + private static IHostBuilder CreateHostBuilder(string[] args) + { + IHostBuilder host = Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.ConfigureKestrel(s => { s.ListenAnyIP(25000); }); + webBuilder.UseStartup(); + }); + return host; + } + + private static void PrintHeader() + { + const string text = @" +██╗ ██╗██╗███╗ ██╗ ██████╗ ███████╗███████╗███╗ ███╗██╗ ██╗ +██║ ██║██║████╗ ██║██╔════╝ ██╔════╝██╔════╝████╗ ████║██║ ██║ +██║ █╗ ██║██║██╔██╗ ██║██║ ███╗███████╗█████╗ ██╔████╔██║██║ ██║ +██║███╗██║██║██║╚██╗██║██║ ██║╚════██║██╔══╝ ██║╚██╔╝██║██║ ██║ +╚███╔███╔╝██║██║ ╚████║╚██████╔╝███████║███████╗██║ ╚═╝ ██║╚██████╔╝ + ╚══╝╚══╝ ╚═╝╚═╝ ╚═══╝ ╚═════╝ ╚══════╝╚══════╝╚═╝ ╚═╝ ╚═════╝ + +███████╗ ██████╗██╗ ██╗███████╗██████╗ ██╗ ██╗██╗ ███████╗██████╗ ███████╗███████╗██████╗ ██╗ ██╗███████╗██████╗ +██╔════╝██╔════╝██║ ██║██╔════╝██╔══██╗██║ ██║██║ ██╔════╝██╔══██╗ ██╔════╝██╔════╝██╔══██╗██║ ██║██╔════╝██╔══██╗ +███████╗██║ ███████║█████╗ ██║ ██║██║ ██║██║ █████╗ ██████╔╝█████╗███████╗█████╗ ██████╔╝██║ ██║█████╗ ██████╔╝ +╚════██║██║ ██╔══██║██╔══╝ ██║ ██║██║ ██║██║ ██╔══╝ ██╔══██╗╚════╝╚════██║██╔══╝ ██╔══██╗╚██╗ ██╔╝██╔══╝ ██╔══██╗ +███████║╚██████╗██║ ██║███████╗██████╔╝╚██████╔╝███████╗███████╗██║ ██║ ███████║███████╗██║ ██║ ╚████╔╝ ███████╗██║ ██║ +╚══════╝ ╚═════╝╚═╝ ╚═╝╚══════╝╚═════╝ ╚═════╝ ╚══════╝╚══════╝╚═╝ ╚═╝ ╚══════╝╚══════╝╚═╝ ╚═╝ ╚═══╝ ╚══════╝╚═╝ ╚═╝ +"; + string separator = new('=', Console.WindowWidth); + string logo = text.Split('\n').Select(s => string.Format("{0," + (Console.WindowWidth / 2 + s.Length / 2) + "}\n", s)) + .Aggregate("", (current, i) => current + i); + + Console.ForegroundColor = ConsoleColor.Cyan; + Console.WriteLine(separator + logo + $"Version: {Assembly.GetExecutingAssembly().GetName().Version}\n" + separator); + Console.ForegroundColor = ConsoleColor.White; + } + } +} \ No newline at end of file diff --git a/srcs/Scheduler/Ranking/RankingRefreshEvent.cs b/srcs/Scheduler/Ranking/RankingRefreshEvent.cs new file mode 100644 index 0000000..5bc4f3c --- /dev/null +++ b/srcs/Scheduler/Ranking/RankingRefreshEvent.cs @@ -0,0 +1,8 @@ +using PhoenixLib.Events; + +namespace WingsEmu.ClusterScheduler.Ranking +{ + public class RankingRefreshEvent : IAsyncEvent + { + } +} \ No newline at end of file diff --git a/srcs/Scheduler/Ranking/RankingRefreshEventHandler.cs b/srcs/Scheduler/Ranking/RankingRefreshEventHandler.cs new file mode 100644 index 0000000..88d9709 --- /dev/null +++ b/srcs/Scheduler/Ranking/RankingRefreshEventHandler.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using PhoenixLib.Logging; +using PhoenixLib.ServiceBus; +using WingsAPI.Communication; +using WingsAPI.Communication.DbServer.CharacterService; +using WingsAPI.Communication.Player; +using WingsAPI.Data.Character; + +namespace WingsEmu.ClusterScheduler.Ranking +{ + public class RankingRefreshEventHandler : IAsyncEventProcessor + { + private readonly ICharacterService _characterService; + private readonly IMessagePublisher _message; + + public RankingRefreshEventHandler(IMessagePublisher message, ICharacterService characterService) + { + _message = message; + _characterService = characterService; + } + + public async Task HandleAsync(RankingRefreshEvent e, CancellationToken cancellation) + { + CharacterRefreshRankingResponse response = null; + + try + { + response = await _characterService.RefreshRanking(new EmptyRpcRequest()); + } + catch (Exception ex) + { + Log.Error("[RANKING_REFRESH] Unexpected error: ", ex); + } + + if (response?.ResponseType != RpcResponseType.SUCCESS) + { + Log.Warn("[RANKING_REFRESH] Issue while trying to refresh the Ranking"); + return; + } + + await _message.PublishAsync(new RankingRefreshMessage + { + TopCompliment = response.TopCompliment ?? new List(), + TopPoints = response.TopPoints ?? new List(), + TopReputation = response.TopReputation ?? new List() + }); + } + } +} \ No newline at end of file diff --git a/srcs/Scheduler/Scheduler.csproj b/srcs/Scheduler/Scheduler.csproj new file mode 100644 index 0000000..6d0a95b --- /dev/null +++ b/srcs/Scheduler/Scheduler.csproj @@ -0,0 +1,32 @@ + + + + net5.0 + ..\..\dist\scheduler\ + false + WingsEmu.ClusterScheduler + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/srcs/Scheduler/Service/ComplimentsMonthlyRefreshCronScheduler.cs b/srcs/Scheduler/Service/ComplimentsMonthlyRefreshCronScheduler.cs new file mode 100644 index 0000000..1123ade --- /dev/null +++ b/srcs/Scheduler/Service/ComplimentsMonthlyRefreshCronScheduler.cs @@ -0,0 +1,23 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Hangfire; +using Microsoft.Extensions.Hosting; +using PhoenixLib.ServiceBus; +using WingsAPI.Communication.Compliments; + +namespace WingsEmu.ClusterScheduler.Service +{ + public class ComplimentsMonthlyRefreshCronScheduler : BackgroundService + { + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + RecurringJob.AddOrUpdate>( + "compliments-monthly-refresh", + s => s.PublishAsync(new ComplimentsMonthlyRefreshMessage(), CancellationToken.None), + "1 0 1 * *", // hardcoded cron for the moment (every first of the month at 12am) + TimeZoneInfo.Utc + ); + } + } +} \ No newline at end of file diff --git a/srcs/Scheduler/Service/FamilyMissionsResetCronScheduler.cs b/srcs/Scheduler/Service/FamilyMissionsResetCronScheduler.cs new file mode 100644 index 0000000..c0c4d51 --- /dev/null +++ b/srcs/Scheduler/Service/FamilyMissionsResetCronScheduler.cs @@ -0,0 +1,23 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Hangfire; +using Microsoft.Extensions.Hosting; +using PhoenixLib.ServiceBus; +using WingsAPI.Communication.Families; + +namespace WingsEmu.ClusterScheduler.Service +{ + public class FamilyMissionsResetCronScheduler : BackgroundService + { + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + RecurringJob.AddOrUpdate>( + "family-missions-reset", + s => s.PublishAsync(new FamilyMissionsResetMessage(), CancellationToken.None), + "1 0 * * *", + TimeZoneInfo.Utc + ); + } + } +} \ No newline at end of file diff --git a/srcs/Scheduler/Service/InstantBattleCronScheduler.cs b/srcs/Scheduler/Service/InstantBattleCronScheduler.cs new file mode 100644 index 0000000..32fcefa --- /dev/null +++ b/srcs/Scheduler/Service/InstantBattleCronScheduler.cs @@ -0,0 +1,23 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Hangfire; +using Microsoft.Extensions.Hosting; +using PhoenixLib.ServiceBus; +using WingsAPI.Communication.InstantBattle; + +namespace WingsEmu.ClusterScheduler.Service +{ + public class InstantBattleCronScheduler : BackgroundService + { + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + RecurringJob.AddOrUpdate>( + "instant-battle", + s => s.PublishAsync(new InstantBattleStartMessage(), CancellationToken.None), + "55 1-23/2 * * *", // hardcoded cron for the moment + TimeZoneInfo.Utc + ); + } + } +} \ No newline at end of file diff --git a/srcs/Scheduler/Service/MinigameProductionRefreshCronScheduler.cs b/srcs/Scheduler/Service/MinigameProductionRefreshCronScheduler.cs new file mode 100644 index 0000000..06dbf75 --- /dev/null +++ b/srcs/Scheduler/Service/MinigameProductionRefreshCronScheduler.cs @@ -0,0 +1,23 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Hangfire; +using Microsoft.Extensions.Hosting; +using PhoenixLib.ServiceBus; +using WingsAPI.Communication.Miniland; + +namespace WingsEmu.ClusterScheduler.Service +{ + public class MinigameProductionRefreshCronScheduler : BackgroundService + { + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + RecurringJob.AddOrUpdate>( + "minigame-refresh-free-production-points", + s => s.PublishAsync(new MinigameRefreshProductionPointsMessage(), CancellationToken.None), + "1 0 * * *", // hardcoded cron for the moment + TimeZoneInfo.Utc + ); + } + } +} \ No newline at end of file diff --git a/srcs/Scheduler/Service/QuestDailyRefreshCronScheduler.cs b/srcs/Scheduler/Service/QuestDailyRefreshCronScheduler.cs new file mode 100644 index 0000000..aca21ba --- /dev/null +++ b/srcs/Scheduler/Service/QuestDailyRefreshCronScheduler.cs @@ -0,0 +1,26 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Hangfire; +using Microsoft.Extensions.Hosting; +using PhoenixLib.ServiceBus; +using WingsAPI.Communication.Quests; + +namespace WingsEmu.ClusterScheduler.Service +{ + public class QuestDailyRefreshCronScheduler : BackgroundService + { + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + RecurringJob.AddOrUpdate>( + "quest-daily-refresh", + s => s.PublishAsync(new QuestDailyRefreshMessage + { + Force = true + }, CancellationToken.None), + "1 0 * * *", // hardcoded cron for the moment (every day at 12am) + TimeZoneInfo.Utc + ); + } + } +} \ No newline at end of file diff --git a/srcs/Scheduler/Service/RaidRestrictionRefreshCronScheduler.cs b/srcs/Scheduler/Service/RaidRestrictionRefreshCronScheduler.cs new file mode 100644 index 0000000..52d58f3 --- /dev/null +++ b/srcs/Scheduler/Service/RaidRestrictionRefreshCronScheduler.cs @@ -0,0 +1,23 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Hangfire; +using Microsoft.Extensions.Hosting; +using PhoenixLib.ServiceBus; +using WingsAPI.Communication.Raid; + +namespace WingsEmu.ClusterScheduler.Service +{ + public class RaidRestrictionRefreshCronScheduler : BackgroundService + { + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + RecurringJob.AddOrUpdate>( + "raid-restriction-refresh", + s => s.PublishAsync(new RaidRestrictionRefreshMessage(), CancellationToken.None), + "1 0 * * *", // hardcoded cron for the moment + TimeZoneInfo.Utc + ); + } + } +} \ No newline at end of file diff --git a/srcs/Scheduler/Service/RainbowBattleCronScheduler.cs b/srcs/Scheduler/Service/RainbowBattleCronScheduler.cs new file mode 100644 index 0000000..2be504d --- /dev/null +++ b/srcs/Scheduler/Service/RainbowBattleCronScheduler.cs @@ -0,0 +1,23 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Hangfire; +using Microsoft.Extensions.Hosting; +using PhoenixLib.ServiceBus; +using WingsAPI.Communication.RainbowBattle; + +namespace WingsEmu.ClusterScheduler.Service +{ + public class RainbowBattleCronScheduler : BackgroundService + { + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + RecurringJob.AddOrUpdate>( + "rainbow-battle", + s => s.PublishAsync(new RainbowBattleStartMessage(), CancellationToken.None), + "55 */2 * * *", // hardcoded cron for the moment + TimeZoneInfo.Utc + ); + } + } +} \ No newline at end of file diff --git a/srcs/Scheduler/Service/RainbowBattleLeaverBusterCronScheduler.cs b/srcs/Scheduler/Service/RainbowBattleLeaverBusterCronScheduler.cs new file mode 100644 index 0000000..5cd26bf --- /dev/null +++ b/srcs/Scheduler/Service/RainbowBattleLeaverBusterCronScheduler.cs @@ -0,0 +1,26 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Hangfire; +using Microsoft.Extensions.Hosting; +using PhoenixLib.ServiceBus; +using WingsAPI.Communication.RainbowBattle; + +namespace WingsEmu.ClusterScheduler.Service +{ + public class RainbowBattleLeaverBusterCronScheduler : BackgroundService + { + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + RecurringJob.AddOrUpdate>( + "rainbow-battle-leaver-buster-reset", + s => s.PublishAsync(new RainbowBattleLeaverBusterResetMessage + { + Force = true + }, CancellationToken.None), + "1 0 1 * *", + TimeZoneInfo.Utc + ); + } + } +} \ No newline at end of file diff --git a/srcs/Scheduler/Service/RankingRefreshCronScheduler.cs b/srcs/Scheduler/Service/RankingRefreshCronScheduler.cs new file mode 100644 index 0000000..2590515 --- /dev/null +++ b/srcs/Scheduler/Service/RankingRefreshCronScheduler.cs @@ -0,0 +1,23 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Hangfire; +using Microsoft.Extensions.Hosting; +using PhoenixLib.Events; +using WingsEmu.ClusterScheduler.Ranking; + +namespace WingsEmu.ClusterScheduler.Service +{ + public class RankingRefreshCronScheduler : BackgroundService + { + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + RecurringJob.AddOrUpdate( + "ranking-refresh", + s => s.ProcessEventAsync(new RankingRefreshEvent(), CancellationToken.None), + "1 0 * * *", + TimeZoneInfo.Utc + ); + } + } +} \ No newline at end of file diff --git a/srcs/Scheduler/Service/SpecialistPointsRefreshCronScheduler.cs b/srcs/Scheduler/Service/SpecialistPointsRefreshCronScheduler.cs new file mode 100644 index 0000000..36ec9a0 --- /dev/null +++ b/srcs/Scheduler/Service/SpecialistPointsRefreshCronScheduler.cs @@ -0,0 +1,23 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Hangfire; +using Microsoft.Extensions.Hosting; +using PhoenixLib.ServiceBus; +using WingsAPI.Communication.Player; + +namespace WingsEmu.ClusterScheduler.Service +{ + public class SpecialistPointsRefreshCronScheduler : BackgroundService + { + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + RecurringJob.AddOrUpdate>( + "specialist-points-refresh", + s => s.PublishAsync(new SpecialistPointsRefreshMessage(), CancellationToken.None), + "1 0 * * *", // hardcoded cron for the moment + TimeZoneInfo.Utc + ); + } + } +} \ No newline at end of file diff --git a/srcs/Scheduler/Startup.cs b/srcs/Scheduler/Startup.cs new file mode 100644 index 0000000..9c6b2d2 --- /dev/null +++ b/srcs/Scheduler/Startup.cs @@ -0,0 +1,111 @@ +using System; +using Hangfire; +using Hangfire.MemoryStorage; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using PhoenixLib.DAL.Redis; +using PhoenixLib.Events; +using PhoenixLib.Logging; +using PhoenixLib.ServiceBus.Extensions; +using Plugin.Database; +using WingsAPI.Communication.InstantBattle; +using WingsAPI.Communication.Player; +using WingsAPI.Communication.RainbowBattle; +using WingsAPI.Plugins; +using WingsAPI.Plugins.Exceptions; +using WingsEmu.ClusterScheduler.Service; +using WingsEmu.ClusterScheduler.Utility; +using WingsEmu.Communication.gRPC.Extensions; +using WingsEmu.Game; +using WingsEmu.Health.Extensions; +using WingsEmu.Plugins.DistributedGameEvents; +using WingsEmu.Plugins.DistributedGameEvents.BotMessages; + +namespace WingsEmu.ClusterScheduler +{ + public class Startup + { + private static ServiceProvider GetPluginsProvider() + { + var pluginBuilder = new ServiceCollection(); + pluginBuilder.AddTransient(); + return pluginBuilder.BuildServiceProvider(); + } + + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) + { + new DatabasePlugin().AddDependencies(services); + using ServiceProvider plugins = GetPluginsProvider(); + foreach (IDependencyInjectorPlugin plugin in plugins.GetServices()) + { + try + { + Log.Debug($"[PLUGIN_LOADER] Loading generic plugin {plugin.Name}..."); + plugin.AddDependencies(services); + } + catch (PluginException e) + { + Log.Error($"{plugin.Name} : plugin.OnLoad", e); + } + } + + services.AddPhoenixLogging(); + services.AddEventPipeline(); + services.AddEventHandlersInAssembly(); + services.AddSingleton(); + + services.AddHangfire(configuration => + { + configuration.UseColouredConsoleLogProvider(); + configuration.UseMemoryStorage(); + }); + services.AddMaintenanceMode(); + //services.AddFileConfiguration(); + services.AddMqttConfigurationFromEnv(); + services.AddMessagePublisher(); + services.AddMessagePublisher(); + services.AddMessagePublisher(); + services.AddMessagePublisher(); + services.AddMessagePublisher(); + + services.TryAddConnectionMultiplexerFromEnv(); + + services.AddHangfireServer(); + services.AddGrpcDbServerServiceClient(); + + /* + * WARM UP SERVICES + */ + services.AddHostedService(); + services.AddHostedService(); + services.AddHostedService(); + services.AddHostedService(); + services.AddHostedService(); + services.AddHostedService(); + services.AddHostedService(); + services.AddHostedService(); + services.AddHostedService(); + services.AddHostedService(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IServiceProvider serviceProvider) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + GlobalConfiguration.Configuration.UseActivator(new HangfireJobActivator(serviceProvider)); + + app.UseRouting(); + + app.UseHangfireServer(); + app.UseHangfireDashboard(); + } + } +} \ No newline at end of file diff --git a/srcs/Scheduler/Utility/RandomGenerator.cs b/srcs/Scheduler/Utility/RandomGenerator.cs new file mode 100644 index 0000000..e2dd91d --- /dev/null +++ b/srcs/Scheduler/Utility/RandomGenerator.cs @@ -0,0 +1,26 @@ +using System; +using RandN; +using RandN.Compat; +using WingsEmu.Game; + +namespace WingsEmu.ClusterScheduler.Utility +{ + public class RandomGenerator : IRandomGenerator + { + private static readonly Random Local = RandomShim.Create(SmallRng.Create()); + + public int RandomNumber(int min, int max) + { + if (min > max) + { + return RandomNumber(max, min); + } + + return min == max ? max : Local.Next(min, max); + } + + public int RandomNumber(int max) => RandomNumber(0, max); + + public int RandomNumber() => RandomNumber(0, 100); + } +} \ No newline at end of file diff --git a/srcs/Toolkit/CommandHandlers/CheckTranslationsCommandHandler.cs b/srcs/Toolkit/CommandHandlers/CheckTranslationsCommandHandler.cs new file mode 100644 index 0000000..6af6629 --- /dev/null +++ b/srcs/Toolkit/CommandHandlers/CheckTranslationsCommandHandler.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using PhoenixLib.Logging; +using PhoenixLib.MultiLanguage; +using Toolkit.Commands; +using WingsEmu.Game._i18n; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; + +namespace Toolkit.CommandHandlers; + +public class CheckTranslationsCommandHandler +{ + private static readonly INamingConvention __NamingConvention = UnderscoredNamingConvention.Instance; + private static readonly ISerializer __Serializer = new SerializerBuilder().WithNamingConvention(__NamingConvention).Build(); + private static readonly IDeserializer __Deserializer = new DeserializerBuilder().WithNamingConvention(__NamingConvention).Build(); + + public static async Task HandleAsync(CheckTranslationsCommand command) + { + Log.Info($"Checking translations file in {command.InputPath}"); + Dictionary english = null; + foreach (RegionLanguageType i in Enum.GetValues(typeof(RegionLanguageType))) + { + if (i == RegionLanguageType.RU) + { + continue; + } + + string languageType = i.ToString().ToLowerInvariant(); + var newTmp = new Dictionary(); + + string languageDirectory = Path.Combine(command.InputPath, $"{languageType}"); + foreach (string translationFile in Directory.GetFiles(languageDirectory, "*.yml").Concat(Directory.GetFiles(languageDirectory, "*.yaml"))) + { + try + { + string fileContent = await File.ReadAllTextAsync(translationFile); + IDeserializer deserializer = __Deserializer; + Dictionary tmp = deserializer.Deserialize>(fileContent); + + foreach ((string s, string value) in tmp) + { + if (string.IsNullOrEmpty(value)) + { + continue; + } + + if (value == $"#{s}" && english != null && english.TryGetValue(s, out string translated)) + { + newTmp[s] = translated; + continue; + } + + newTmp[s] = value; + } + + if (i == RegionLanguageType.EN) + { + english = newTmp; + } + + foreach (object enumValue in Enum.GetValues(typeof(GameDialogKey))) + { + var enm = (GameDialogKey)enumValue; + if (!newTmp.ContainsKey(enm.ToString())) + { + Log.Warn($"{translationFile}: {enm.ToString()} is missing"); + continue; + } + + newTmp[enm.ToString()] = $"#{enm.ToString()}"; + } + + var toSerialize = newTmp + .OrderBy(s => s.Key.ToString()) + .ToDictionary(s => s.Key.ToString(), s => s.Value); + + if (i == RegionLanguageType.EN) + { + english = toSerialize; + } + + ISerializer serializer = __Serializer; + string content = serializer.Serialize(toSerialize); + string outputFile = $"{command.OutputPath}/{languageType.ToLowerInvariant()}/game-dialog-key.yaml"; + Log.Info($"Updating translation in {outputFile}"); + await File.WriteAllTextAsync(outputFile, content); + } + catch (Exception e) + { + Log.Error($"[RESOURCE_LOADER] {translationFile} {languageType}", e); + } + } + } + + return 0; + } +} \ No newline at end of file diff --git a/srcs/Toolkit/CommandHandlers/CreateAccountCommandHandler.cs b/srcs/Toolkit/CommandHandlers/CreateAccountCommandHandler.cs new file mode 100644 index 0000000..e4c4187 --- /dev/null +++ b/srcs/Toolkit/CommandHandlers/CreateAccountCommandHandler.cs @@ -0,0 +1,93 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using dotenv.net; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using PhoenixLib.Extensions; +using PhoenixLib.Logging; +using Plugin.Database; +using Plugin.Database.DB; +using Plugin.Database.Entities.Account; +using Toolkit.Commands; +using WingsAPI.Plugins; +using WingsAPI.Plugins.Exceptions; +using WingsEmu.DTOs.Account; + +namespace Toolkit.CommandHandlers; + +public class CreateAccountCommandHandler +{ + private static ServiceProvider BuildCoreContainer(CreateAccountCommand command) + { + DotEnv.Load(new DotEnvOptions(true, new[] { command.EnvFile })); + var pluginBuilder = new ServiceCollection(); + pluginBuilder.AddTransient(); + ServiceProvider container = pluginBuilder.BuildServiceProvider(); + + var coreBuilder = new ServiceCollection(); + foreach (IDependencyInjectorPlugin plugin in container.GetServices()) + { + try + { + Log.Debug($"[PLUGIN_LOADER] Loading generic plugin {plugin.Name}..."); + plugin.AddDependencies(coreBuilder); + } + catch (PluginException e) + { + Log.Error("[PLUGIN_LOADER] Add dependencies", e); + } + } + + coreBuilder.AddLogging(builder => + { + builder.ClearProviders(); + builder.AddConsole(); + builder.AddFilter("Microsoft", LogLevel.Warning); + builder.AddFilter("Microsoft.EntityFrameworkCore.Database.Command", LogLevel.Warning); + }); + + return coreBuilder.BuildServiceProvider(); + } + + public static async Task HandleAsync(CreateAccountCommand command) + { + await using ServiceProvider coreContainer = BuildCoreContainer(command); + try + { + IDbContextFactory factory = coreContainer.GetRequiredService>(); + await using GameContext context = factory.CreateDbContext(); + await context.Database.MigrateAsync(); + if (context.Account.Any()) + { + Log.Info("[DEFAULT ACCOUNT] Accounts were already present!"); + return 0; + } + + context.Account.Add(new AccountEntity + { + Authority = AuthorityType.Root, + Language = AccountLanguage.EN, + Name = "admin", + Password = "test".ToSha512() + }); + + context.Account.Add(new AccountEntity + { + Authority = AuthorityType.Root, + Language = AccountLanguage.EN, + Name = "test", + Password = "test".ToSha512() + }); + await context.SaveChangesAsync(); + Log.Info("[DEFAULT ACCOUNT] Accounts created!"); + return 0; + } + catch (Exception e) + { + Log.Error("[DEFAULT ACCOUNT] Error", e); + return 1; + } + } +} \ No newline at end of file diff --git a/srcs/Toolkit/CommandHandlers/GenerateTranslationsCommandHandler.cs b/srcs/Toolkit/CommandHandlers/GenerateTranslationsCommandHandler.cs new file mode 100644 index 0000000..81a23e2 --- /dev/null +++ b/srcs/Toolkit/CommandHandlers/GenerateTranslationsCommandHandler.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using PhoenixLib.Logging; +using PhoenixLib.MultiLanguage; +using Toolkit.Commands; +using WingsEmu.Game._i18n; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; + +namespace Toolkit.CommandHandlers; + +public class GenerateTranslationsCommandHandler +{ + private static readonly INamingConvention __NamingConvention = UnderscoredNamingConvention.Instance; + private static readonly ISerializer __Serializer = new SerializerBuilder().WithNamingConvention(__NamingConvention).Build(); + private static readonly IDeserializer __Deserializer = new DeserializerBuilder().WithNamingConvention(__NamingConvention).Build(); + + public static async Task HandleAsync(GenerateTranslationsCommand command) + { + Log.Info($"Updating translations file in {command.InputPath}"); + Dictionary english = null; + foreach (RegionLanguageType i in Enum.GetValues(typeof(RegionLanguageType))) + { + if (i == RegionLanguageType.RU) + { + continue; + } + + var newTmp = new Dictionary(); + + foreach (GameDialogKey enumValue in Enum.GetValues(typeof(GameDialogKey))) + { + GameDialogKey enm = enumValue; + string notTranslated = $"#{enm.ToString()}"; + if (newTmp.ContainsKey(enm)) + { + continue; + } + + + newTmp[enm] = notTranslated; + } + + string languageType = i.ToString(); + string fileContent = await File.ReadAllTextAsync($"{command.InputPath}/{languageType.ToLowerInvariant()}/game-dialog-key.yaml"); + IDeserializer deserializer = __Deserializer; + Dictionary tmp = deserializer.Deserialize>(fileContent); + + foreach ((string s, string value) in tmp) + { + if (!Enum.TryParse(s, out GameDialogKey key)) + { + continue; + } + + if (string.IsNullOrEmpty(value)) + { + continue; + } + + if (value == $"#{s}" && english != null && english.TryGetValue(s, out string translated)) + { + newTmp[key] = translated; + continue; + } + + newTmp[key] = value; + } + + var toSerialize = newTmp + .OrderBy(s => s.Key.ToString()) + .ToDictionary(s => s.Key.ToString(), s => s.Value); + + if (i == RegionLanguageType.EN) + { + english = toSerialize; + } + + ISerializer serializer = __Serializer; + string content = serializer.Serialize(toSerialize); + string outputFile = $"{command.OutputPath}/{languageType.ToLowerInvariant()}/game-dialog-key.yaml"; + Log.Info($"Updating translation in {outputFile}"); + await File.WriteAllTextAsync(outputFile, content); + } + + return 0; + } +} \ No newline at end of file diff --git a/srcs/Toolkit/Commands/CheckTranslationsCommand.cs b/srcs/Toolkit/Commands/CheckTranslationsCommand.cs new file mode 100644 index 0000000..6f69087 --- /dev/null +++ b/srcs/Toolkit/Commands/CheckTranslationsCommand.cs @@ -0,0 +1,13 @@ +using CommandLine; + +namespace Toolkit.Commands; + +[Verb("check-translations", HelpText = "Manage the language to update those")] +public class CheckTranslationsCommand +{ + [Option('i', "input", Required = true, HelpText = "Input paths from where you want to parse the files")] + public string InputPath { get; set; } + + [Option('o', "output", HelpText = "The output you wants to generate your parser")] + public string OutputPath { get; set; } +} \ No newline at end of file diff --git a/srcs/Toolkit/Commands/CreateAccountCommand.cs b/srcs/Toolkit/Commands/CreateAccountCommand.cs new file mode 100644 index 0000000..84135bc --- /dev/null +++ b/srcs/Toolkit/Commands/CreateAccountCommand.cs @@ -0,0 +1,10 @@ +using CommandLine; + +namespace Toolkit.Commands; + +[Verb("create-accounts", HelpText = "Manage the language to update those")] +public class CreateAccountCommand +{ + [Option('e', "env", Required = false, HelpText = "Input paths from where you want to parse the files", Default = "toolkit.env")] + public string EnvFile { get; set; } +} \ No newline at end of file diff --git a/srcs/Toolkit/Commands/GenerateTranslationsCommand.cs b/srcs/Toolkit/Commands/GenerateTranslationsCommand.cs new file mode 100644 index 0000000..da7d57c --- /dev/null +++ b/srcs/Toolkit/Commands/GenerateTranslationsCommand.cs @@ -0,0 +1,16 @@ +using CommandLine; + +namespace Toolkit.Commands; + +[Verb("translations", HelpText = "Manage the language to update those")] +public class GenerateTranslationsCommand +{ + [Option('l', "language", Default = "all", HelpText = "Language that will be used")] + public string Language { get; set; } + + [Option('i', "input", Required = true, HelpText = "Input paths from where you want to parse the files")] + public string InputPath { get; set; } + + [Option('o', "output", HelpText = "The output you wants to generate your parser")] + public string OutputPath { get; set; } +} \ No newline at end of file diff --git a/srcs/Toolkit/Extensions/RegionLanguageTypeExtensions.cs b/srcs/Toolkit/Extensions/RegionLanguageTypeExtensions.cs new file mode 100644 index 0000000..dfb713b --- /dev/null +++ b/srcs/Toolkit/Extensions/RegionLanguageTypeExtensions.cs @@ -0,0 +1,35 @@ +// WingsEmu +// +// Developed by NosWings Team + +using PhoenixLib.MultiLanguage; + +namespace Toolkit.Extensions; + +public static class RegionLanguageTypeExtensions +{ + public static string ToNostaleRegionKey(this RegionLanguageType type) + { + switch (type) + { + case RegionLanguageType.FR: + return "fr"; + case RegionLanguageType.EN: + return "uk"; + case RegionLanguageType.DE: + return "de"; + case RegionLanguageType.PL: + return "pl"; + case RegionLanguageType.IT: + return "it"; + case RegionLanguageType.ES: + return "es"; + case RegionLanguageType.CZ: + return "cz"; + case RegionLanguageType.TR: + return "tr"; + default: + return "uk"; + } + } +} \ No newline at end of file diff --git a/srcs/Toolkit/Program.cs b/srcs/Toolkit/Program.cs new file mode 100644 index 0000000..ba86775 --- /dev/null +++ b/srcs/Toolkit/Program.cs @@ -0,0 +1,33 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Text; +using System.Threading.Tasks; +using CommandLine; +using Plugin.Database.Mapping; +using Toolkit.CommandHandlers; +using Toolkit.Commands; + +namespace Toolkit; + +public class Program +{ + #region Methods + + public static async Task Main(string[] args) + { + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + NonGameMappingRules.InitializeMapping(); + return await Parser.Default + .ParseArguments(args) + .MapResult( + async (GenerateTranslationsCommand command) => await GenerateTranslationsCommandHandler.HandleAsync(command), + async (CreateAccountCommand command) => await CreateAccountCommandHandler.HandleAsync(command), + async (CheckTranslationsCommand command) => await CheckTranslationsCommandHandler.HandleAsync(command), + errs => Task.FromResult(1) + ); + } + + #endregion +} \ No newline at end of file diff --git a/srcs/Toolkit/Toolkit.csproj b/srcs/Toolkit/Toolkit.csproj new file mode 100644 index 0000000..2665e97 --- /dev/null +++ b/srcs/Toolkit/Toolkit.csproj @@ -0,0 +1,27 @@ + + + + Exe + net5.0 + false + latest + ..\..\dist\toolkit + + + + + + + + + + + + + + + + + + + diff --git a/srcs/TranslationsServer/DockerGracefulStopService.cs b/srcs/TranslationsServer/DockerGracefulStopService.cs new file mode 100644 index 0000000..e17438a --- /dev/null +++ b/srcs/TranslationsServer/DockerGracefulStopService.cs @@ -0,0 +1,54 @@ +using System; +using System.Runtime.Loader; +using System.Threading; +using PhoenixLib.Logging; + +namespace TranslationServer +{ + public class DockerGracefulStopService : IDisposable + { + private readonly CancellationTokenSource _cancellationTokenSource; + private readonly ManualResetEventSlim _stoppedEvent; + + public DockerGracefulStopService() + { + _cancellationTokenSource = new CancellationTokenSource(); + _stoppedEvent = new ManualResetEventSlim(); + + // SIGINT + Console.CancelKeyPress += (sender, eventArgs) => + { + Log.Error("[GRACEFUL_SHUTDOWN] SIGINT received", new Exception("[GRACEFUL_SHUTDOWN] SIGINT received")); + GracefulStop(_cancellationTokenSource, _stoppedEvent); + }; + + // SIGTERM + AssemblyLoadContext.Default.Unloading += context => + { + Log.Error("[GRACEFUL_SHUTDOWN] SIGTERM received", new Exception("[GRACEFUL_SHUTDOWN] SIGTERM received")); + GracefulStop(_cancellationTokenSource, _stoppedEvent); + }; + + AppDomain.CurrentDomain.UnhandledException += (sender, args) => + { + Log.Error("UnhandledException", args.ExceptionObject as Exception); + GracefulStop(_cancellationTokenSource, _stoppedEvent); + }; + } + + public CancellationToken CancellationToken => _cancellationTokenSource.Token; + + public void Dispose() + { + _stoppedEvent.Set(); + } + + private static void GracefulStop(CancellationTokenSource cancellationTokenSource, ManualResetEventSlim stoppedEvent) + { + Log.Info("DockerGracefulStopService Stopping service"); + cancellationTokenSource.Cancel(); + stoppedEvent.Wait(); + Log.Info("DockerGracefulStopService Stop finished"); + } + } +} \ No newline at end of file diff --git a/srcs/TranslationsServer/Loader/BannedNamesConfiguration.cs b/srcs/TranslationsServer/Loader/BannedNamesConfiguration.cs new file mode 100644 index 0000000..3806c98 --- /dev/null +++ b/srcs/TranslationsServer/Loader/BannedNamesConfiguration.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace TranslationServer.Loader +{ + public class BannedNamesConfiguration + { + public List BannedNames { get; set; } + } +} \ No newline at end of file diff --git a/srcs/TranslationsServer/Loader/GenericTranslationFileLoader.cs b/srcs/TranslationsServer/Loader/GenericTranslationFileLoader.cs new file mode 100644 index 0000000..77ca3c6 --- /dev/null +++ b/srcs/TranslationsServer/Loader/GenericTranslationFileLoader.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using PhoenixLib.Logging; +using PhoenixLib.MultiLanguage; +using WingsAPI.Data.GameData; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; + +namespace TranslationServer.Loader +{ + public class GenericTranslationFileLoader : IResourceLoader + { + private static readonly INamingConvention __NamingConvention = UnderscoredNamingConvention.Instance; + private static readonly IDeserializer __Deserializer = new DeserializerBuilder().WithNamingConvention(__NamingConvention).Build(); + + + private readonly TranslationsFileLoaderOptions _options; + private readonly List _translations = new(); + + public GenericTranslationFileLoader(TranslationsFileLoaderOptions options) => _options = options; + + public async Task> LoadAsync() + { + if (_translations.Any()) + { + return _translations; + } + + Dictionary english = null; + foreach (RegionLanguageType i in Enum.GetValues(typeof(RegionLanguageType))) + { + if (i == RegionLanguageType.RU) + { + continue; + } + + string languageType = i.ToString().ToLowerInvariant(); + var newTmp = new Dictionary(); + + string languageDirectory = Path.Combine(_options.TranslationsPath, $"{languageType}"); + foreach (string translationFile in Directory.GetFiles(languageDirectory, "*.yml").Concat(Directory.GetFiles(languageDirectory, "*.yaml"))) + { + try + { + string fileContent = await File.ReadAllTextAsync(translationFile); + IDeserializer deserializer = __Deserializer; + Dictionary tmp = deserializer.Deserialize>(fileContent); + + foreach ((string s, string value) in tmp) + { + if (string.IsNullOrEmpty(value)) + { + continue; + } + + if (value == $"#{s}" && english != null && english.TryGetValue(s, out string translated)) + { + newTmp[s] = translated; + continue; + } + + newTmp[s] = value; + } + + if (i == RegionLanguageType.EN) + { + english = newTmp; + } + + _translations.AddRange(newTmp.Select(s => new GenericTranslationDto + { + Key = s.Key, + Value = s.Value, + Language = i + })); + } + catch (Exception e) + { + Log.Error($"[RESOURCE_LOADER] {translationFile} {languageType}", e); + } + } + } + + Log.Info($"[RESOURCE_LOADER] {_translations.Count.ToString()} translations loaded"); + return _translations; + } + } +} \ No newline at end of file diff --git a/srcs/TranslationsServer/Loader/TranslationsFileLoaderOptions.cs b/srcs/TranslationsServer/Loader/TranslationsFileLoaderOptions.cs new file mode 100644 index 0000000..7f4ef9c --- /dev/null +++ b/srcs/TranslationsServer/Loader/TranslationsFileLoaderOptions.cs @@ -0,0 +1,7 @@ +namespace TranslationServer.Loader +{ + public class TranslationsFileLoaderOptions + { + public string TranslationsPath { get; set; } + } +} \ No newline at end of file diff --git a/srcs/TranslationsServer/Program.cs b/srcs/TranslationsServer/Program.cs new file mode 100644 index 0000000..559dfbd --- /dev/null +++ b/srcs/TranslationsServer/Program.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using PhoenixLib.Logging; +using ProtoBuf.Grpc.Client; +using WingsAPI.Data.GameData; + +namespace TranslationServer +{ + public class Program + { + public static async Task Main(string[] args) + { + PrintHeader(); + GrpcClientFactory.AllowUnencryptedHttp2 = true; + using var stopService = new DockerGracefulStopService(); + using IHost host = CreateHostBuilder(args).Build(); + { + IServiceProvider services = host.Services; + IResourceLoader loader = services.GetRequiredService>(); + await loader.LoadAsync(); + + Log.Warn("Starting host..."); + await host.StartAsync(); + // + // IMessagingService messagingService = host.Services.GetRequiredService(); + // await messagingService.StartAsync(); + + await host.WaitForShutdownAsync(stopService.CancellationToken); + // await messagingService.DisposeAsync(); + } + } + + private static void PrintHeader() + { + const string text = @" +██╗ ██╗██╗███╗ ██╗ ██████╗ ███████╗███████╗███╗ ███╗██╗ ██╗ +██║ ██║██║████╗ ██║██╔════╝ ██╔════╝██╔════╝████╗ ████║██║ ██║ +██║ █╗ ██║██║██╔██╗ ██║██║ ███╗███████╗█████╗ ██╔████╔██║██║ ██║ +██║███╗██║██║██║╚██╗██║██║ ██║╚════██║██╔══╝ ██║╚██╔╝██║██║ ██║ +╚███╔███╔╝██║██║ ╚████║╚██████╔╝███████║███████╗██║ ╚═╝ ██║╚██████╔╝ + ╚══╝╚══╝ ╚═╝╚═╝ ╚═══╝ ╚═════╝ ╚══════╝╚══════╝╚═╝ ╚═╝ ╚═════╝ + +████████╗██████╗ █████╗ ███╗ ██╗███████╗██╗ █████╗ ████████╗██╗ ██████╗ ███╗ ██╗ +╚══██╔══╝██╔══██╗██╔══██╗████╗ ██║██╔════╝██║ ██╔══██╗╚══██╔══╝██║██╔═══██╗████╗ ██║ + ██║ ██████╔╝███████║██╔██╗ ██║███████╗██║ ███████║ ██║ ██║██║ ██║██╔██╗ ██║ + ██║ ██╔══██╗██╔══██║██║╚██╗██║╚════██║██║ ██╔══██║ ██║ ██║██║ ██║██║╚██╗██║ + ██║ ██║ ██║██║ ██║██║ ╚████║███████║███████╗██║ ██║ ██║ ██║╚██████╔╝██║ ╚████║ + ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝ + +███████╗███████╗██████╗ ██╗ ██╗███████╗██████╗ +██╔════╝██╔════╝██╔══██╗██║ ██║██╔════╝██╔══██╗ +███████╗█████╗ ██████╔╝██║ ██║█████╗ ██████╔╝ +╚════██║██╔══╝ ██╔══██╗╚██╗ ██╔╝██╔══╝ ██╔══██╗ +███████║███████╗██║ ██║ ╚████╔╝ ███████╗██║ ██║ +╚══════╝╚══════╝╚═╝ ╚═╝ ╚═══╝ ╚══════╝╚═╝ ╚═╝ + +"; + string separator = new('=', Console.WindowWidth); + string logo = text.Split('\n').Select(s => string.Format("{0," + (Console.WindowWidth / 2 + s.Length / 2) + "}\n", s)) + .Aggregate("", (current, i) => current + i); + Console.ForegroundColor = ConsoleColor.Cyan; + Console.WriteLine(separator + logo + $"Version: {Assembly.GetExecutingAssembly().GetName().Version}\n" + separator); + Console.ForegroundColor = ConsoleColor.White; + } + + // Additional configuration is required to successfully run gRPC on macOS. + // For instructions on how to configure Kestrel and gRPC clients on macOS, visit https://go.microsoft.com/fwlink/?linkid=2099682 + private static IHostBuilder CreateHostBuilder(string[] args) + { + IHostBuilder host = Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.ConfigureKestrel(s => + { + s.ListenAnyIP(short.Parse(Environment.GetEnvironmentVariable("TRANSLATION_SERVER_PORT") ?? "19999"), options => { options.Protocols = HttpProtocols.Http2; }); + }); + webBuilder.UseStartup(); + }); + return host; + } + } +} \ No newline at end of file diff --git a/srcs/TranslationsServer/Services/GrpcGameLanguageService.cs b/srcs/TranslationsServer/Services/GrpcGameLanguageService.cs new file mode 100644 index 0000000..0417085 --- /dev/null +++ b/srcs/TranslationsServer/Services/GrpcGameLanguageService.cs @@ -0,0 +1,40 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using System.Threading.Tasks; +using TranslationServer.Loader; +using WingsAPI.Communication; +using WingsAPI.Communication.Translations; +using WingsAPI.Data.GameData; + +namespace TranslationServer.Services +{ + public class GrpcGameLanguageService : ITranslationService + { + private readonly BannedNamesConfiguration _bannedNamesConfiguration; + private readonly IResourceLoader _loader; + + public GrpcGameLanguageService(IResourceLoader loader, BannedNamesConfiguration bannedNamesConfiguration) + { + _loader = loader; + _bannedNamesConfiguration = bannedNamesConfiguration; + } + + public async Task GetTranslations(EmptyRpcRequest rpcRequest) + { + IReadOnlyList tmp = await _loader.LoadAsync(); + return new GetTranslationsResponse + { + Translations = tmp + }; + } + + public async Task GetForbiddenWords(EmptyRpcRequest rpcRequest) => + new GetForbiddenWordsResponse + { + ForbiddenWords = _bannedNamesConfiguration.BannedNames + }; + } +} \ No newline at end of file diff --git a/srcs/TranslationsServer/Startup.cs b/srcs/TranslationsServer/Startup.cs new file mode 100644 index 0000000..9f14abe --- /dev/null +++ b/srcs/TranslationsServer/Startup.cs @@ -0,0 +1,58 @@ +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using PhoenixLib.Configuration; +using PhoenixLib.ServiceBus.Extensions; +using ProtoBuf.Grpc.Server; +using TranslationServer.Loader; +using TranslationServer.Services; +using WingsAPI.Communication.Translations; +using WingsAPI.Data.GameData; +using WingsEmu.Health.Extensions; + +namespace TranslationServer +{ + public class Startup + { + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) + { + services.AddMqttConfigurationFromEnv(); + services.AddMaintenanceMode(); + + services.AddCodeFirstGrpc(config => + { + config.MaxReceiveMessageSize = null; + config.MaxSendMessageSize = null; + config.EnableDetailedErrors = true; + }); + + services.AddSingleton(s => new TranslationsFileLoaderOptions + { + TranslationsPath = Environment.GetEnvironmentVariable("TRANSLATIONS_PATH") ?? "translations" + }); + services.AddSingleton(); + services.AddSingleton, GenericTranslationFileLoader>(); + services.AddSingleton(); + + services.AddYamlConfigurationHelper(); + services.AddFileConfiguration("banned_names_configuration"); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseRouting(); + + app.UseEndpoints(endpoints => { endpoints.MapGrpcService(); }); + } + } +} \ No newline at end of file diff --git a/srcs/TranslationsServer/TranslationsServer.csproj b/srcs/TranslationsServer/TranslationsServer.csproj new file mode 100644 index 0000000..5b86013 --- /dev/null +++ b/srcs/TranslationsServer/TranslationsServer.csproj @@ -0,0 +1,25 @@ + + + + net5.0 + ..\..\dist\translation-server\ + false + TranslationServer + + + + + + + + + + + + + + + + + + diff --git a/srcs/WingsAPI.Commands/Checks/RequireAuthorityAttribute.cs b/srcs/WingsAPI.Commands/Checks/RequireAuthorityAttribute.cs new file mode 100644 index 0000000..ef4b369 --- /dev/null +++ b/srcs/WingsAPI.Commands/Checks/RequireAuthorityAttribute.cs @@ -0,0 +1,42 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Threading.Tasks; +using Qmmands; +using WingsEmu.Commands.Entities; +using WingsEmu.DTOs.Account; + +namespace WingsEmu.Commands.Checks +{ + public sealed class RequireAuthorityAttribute : CheckAttribute + { + public RequireAuthorityAttribute(AuthorityType authority) => Authority = authority; + + /// + /// This represents the Authority level required to execute a command. + /// + public AuthorityType Authority { get; } + + /// + /// + /// This is a check (pre-condition) before trying to execute a command that needs to pass this check. + /// + /// Context of the command. It needs to be castable to a WingsEmuIngameCommandContext in our case. + /// + public override ValueTask CheckAsync(CommandContext context) + { + if (context is not WingsEmuIngameCommandContext ctx) + { + return new ValueTask(new CheckResult("Invalid context. This is *very* bad. Please report this.")); + } + + if (ctx.Player?.Account is not null && ctx.Player.Account.Authority < Authority) + { + return new ValueTask(new CheckResult("You (at least) need to be a " + Authority + " in order to execute that command.")); + } + + return new ValueTask(CheckResult.Successful); + } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Commands/CommandGlobalExecutorWrapper.cs b/srcs/WingsAPI.Commands/CommandGlobalExecutorWrapper.cs new file mode 100644 index 0000000..f154f39 --- /dev/null +++ b/srcs/WingsAPI.Commands/CommandGlobalExecutorWrapper.cs @@ -0,0 +1,28 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Threading.Tasks; +using WingsEmu.Commands.Interfaces; +using WingsEmu.Game.Commands; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Commands +{ + public class CommandGlobalExecutorWrapper : IGlobalCommandExecutor + { + private readonly ICommandContainer _commandContainer; + + public CommandGlobalExecutorWrapper(ICommandContainer commandContainer) => _commandContainer = commandContainer; + + public void HandleCommand(string command, IClientSession sender, string prefix) + { + HandleCommandAsync(command, sender, prefix).ConfigureAwait(false).GetAwaiter().GetResult(); + } + + public async Task HandleCommandAsync(string command, IClientSession sender, string prefix) + { + await _commandContainer.HandleMessageAsync(command, sender, prefix); + } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Commands/CommandHandler.cs b/srcs/WingsAPI.Commands/CommandHandler.cs new file mode 100644 index 0000000..e6193e5 --- /dev/null +++ b/srcs/WingsAPI.Commands/CommandHandler.cs @@ -0,0 +1,263 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using PhoenixLib.Logging; +using Qmmands; +using WingsEmu.Commands.Checks; +using WingsEmu.Commands.Entities; +using WingsEmu.Commands.Interfaces; +using WingsEmu.DTOs.Account; +using WingsEmu.Game; +using WingsEmu.Game.Commands; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Commands +{ + /* todo: find a better way to deal with TAP in world and here. + * handle errors correctly and return them to the user ingame. + */ + public class CommandHandler : ICommandContainer, IGlobalCommandExecutor + { + private readonly CommandService _commands; + + /// + /// This class should be instanciated with our Container. + /// + public CommandHandler(IServiceProvider provider) + { + _commands = new CommandService(new CommandServiceConfiguration + { + StringComparison = StringComparison.OrdinalIgnoreCase + }); + + _commands.CommandExecuted += _commands_CommandExecuted; + _commands.CommandExecutionFailed += _commands_CommandErrored; + + Services = provider; + } + + public IServiceProvider Services { get; } + + public void AddModule() where T : SaltyModuleBase + { + Log.Info($"[ADD_MODULE] {typeof(T).Name}"); + _commands.AddModule(); + + IReadOnlyList readOnlyList = _commands.GetAllModules().FirstOrDefault(s => s.Type == typeof(T))?.Commands; + if (readOnlyList != null) + { + foreach (Command command in readOnlyList) + { + Log.Info($"[ADD_COMMAND] {command}"); + } + } + } + + public void RemoveModule() where T : SaltyModuleBase + { + Module module = _commands.GetAllModules().FirstOrDefault(s => s.Type == typeof(T)); + + if (module is null) + { + throw new ArgumentException("The given module is not registered in the command container."); + } + + _commands.RemoveModule(module); + } + + public Module[] GetModulesByName(string name, bool caseSensitive = true) + { + return _commands.GetAllModules().Where(x => caseSensitive ? x.Name == name : x.Name.Equals(name, StringComparison.OrdinalIgnoreCase)).ToArray(); + } + + public Command[] GetCommandsByName(string name, bool caseSensitive = true) + { + return _commands.GetAllCommands().Where(x => caseSensitive ? x.Name == name : x.Name.Equals(name, StringComparison.OrdinalIgnoreCase)).ToArray(); + } + + public void AddTypeParser(TypeParser typeParser) + { + _commands.AddTypeParser(typeParser); + Log.Info($"[ADD_TYPE_PARSER] {typeParser.GetType().Name}"); + } + + /// + /// + /// This is where every message from the InGame tchat starting with our prefix will arrive. + /// In our case, the parameter message represents the raw message sent by the user. + /// The parameter of type object would represent the instance of the entity that invoked the command. + /// That method could be called on each messages sent in the in-game tchat. We will just check that it starts with our + /// prefix ($). + /// Then we will create a Context that will propagate onto the command. + /// The CommandService will try to parse the message and find a command with the parsed arguments and will perform some + /// checks, if necessary. + /// + /// It represents the already parsed command with its parameters. + /// It represents the instance of the entity that performed the action of sending a message. + /// + public async Task HandleMessageAsync(string message, object entity, string prefix) + { + if (entity is not IClientSession player) + { + return; + } + + if (!player.HasSelectedCharacter) + { + return; + } + + if (!player.HasCurrentMapInstance) + { + return; + } + + var ctx = new WingsEmuIngameCommandContext(message, player, prefix, _commands, Services); + + IResult result = await _commands.ExecuteAsync(ctx.Input, ctx); + if (result.IsSuccessful) + { + ctx.Command = (result as CommandResult)?.Command; + var authorityAttribute = (RequireAuthorityAttribute)ctx.Command?.Module?.Checks.FirstOrDefault(check => check is RequireAuthorityAttribute); + if (authorityAttribute == null || authorityAttribute.Authority < AuthorityType.GameMaster) + { + ctx.Player.PlayerEntity.Session.EmitEvent(new PlayerCommandEvent + { + Command = ctx.Message + }); + return; + } + + ctx.Player.PlayerEntity.Session.EmitEvent(new GmCommandEvent + { + Command = ctx.Message, + PlayerAuthority = ctx.Player.Account.Authority, + CommandAuthority = authorityAttribute.Authority + }); + return; + } + + await HandleErrorAsync(result, ctx); + } + + public void HandleCommand(string command, IClientSession sender, string prefix) + { + HandleCommandAsync(command, sender, prefix).ConfigureAwait(false).GetAwaiter().GetResult(); + } + + public async Task HandleCommandAsync(string command, IClientSession sender, string prefix) + { + await HandleMessageAsync(command, sender, prefix); + } + + /// + /// This event is being invoked when the excecuted of a command threw an exception. + /// Error results are handled by the result of CommandService#ExecuteAsync. + /// + /// Result with its associated exception. + /// It represents the context. Must be casted to our custom context (WingsEmuIngameCommandContext) + /// + /// + private Task _commands_CommandErrored(CommandExecutionFailedEventArgs e) + { + switch (e.Result.Exception) + { + default: + Log.Debug($"{e.Result.Exception.GetType()} occured.\nError message: {e.Result.Exception.Message}.\nStack trace: {e.Result.Exception.StackTrace}"); + break; + } + + return Task.CompletedTask; + } + + /// + /// This event is being invoked when the execution of a command is over. When the command returned a result. + /// It could be a custom result that we can cast from our instance of CommandResult. + /// + /// It represents the command that has been executed. + /// + /// It represents the returned result. It can an 'empty' result when the command returned a Task, or a + /// custom result. + /// + /// It represents the context. Must be casted to our custom context (WingsEmuIngameCommandContext) + /// + private async Task _commands_CommandExecuted(CommandExecutedEventArgs e) + { + if (e.Context is not WingsEmuIngameCommandContext ctx) + { + Log.Debug($"Command context: {e.Context.GetType()}. This is bad. Please report this."); + return; + } + + Log.Debug($"The command {e.Context.Command.Name} (from player {ctx.Player.PlayerEntity.Name} [{ctx.Player.PlayerEntity.Id}]) has successfully been executed."); + + if (e.Result is SaltyCommandResult res && !string.IsNullOrWhiteSpace(res.Message)) + { + ctx.Player.SendChatMessage("[COMMAND] " + res.Message, e.Result.IsSuccessful ? ChatMessageColorType.Green : ChatMessageColorType.Red); + } + } + + /// + /// This is being called when the CommandService returned an unsuccessfull result. + /// + /// This represents the generic result returned by the command service. We'll check what was wrong. + /// This represents our context for this result. + private async Task HandleErrorAsync(IResult result, WingsEmuIngameCommandContext ctx) + { + Log.Debug($"An error occured: {result}"); + + var errorBuilder = new StringBuilder(); + bool help = false; + + switch (result) + { + case ChecksFailedResult ex: + ctx.Command = ex.Command; + Log.Debug("Some checks have failed: " + string.Join("\n", ex.FailedChecks.Select(x => x.Result))); + break; + case TypeParseFailedResult ex: + ctx.Command = ex.Parameter.Command; + errorBuilder.Append(ex.FailureReason); + help = true; + break; + case CommandNotFoundResult ex: + errorBuilder.Append($"The command was not found: {ctx.Input}"); + break; + case ArgumentParseFailedResult ex: + ctx.Command = ex.Command; + errorBuilder.Append(ex.Command.Parameters == null ? "Too many arguments." : $"The argument for the parameter {ex.Command.Name} was invalid."); + help = true; + break; + case SaltyCommandResult ex: + ctx.Command = ex.Command; + errorBuilder.Append($"{ctx.Command.Name}: {ex.Message}"); + break; + case OverloadsFailedResult ex: + ctx.Command = ex.FailedOverloads.Select(x => x.Key).FirstOrDefault(); + Log.Debug($"Every overload failed: {string.Join("\n", ex.FailedOverloads.Select(x => x.Value.FailureReason))}"); + errorBuilder.Append("Your command syntax was wrong."); + help = true; + break; + } + + if (errorBuilder.Length == 0) + { + return; + } + + ctx.Player.SendChatMessage(errorBuilder.ToString(), ChatMessageColorType.Red); + if (help) + { + await _commands.ExecuteAsync($"help {ctx.Command.FullAliases[0]}", ctx); + } + } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Commands/Entities/SaltyCommandResult.cs b/srcs/WingsAPI.Commands/Entities/SaltyCommandResult.cs new file mode 100644 index 0000000..ebdde95 --- /dev/null +++ b/srcs/WingsAPI.Commands/Entities/SaltyCommandResult.cs @@ -0,0 +1,20 @@ +// WingsEmu +// +// Developed by NosWings Team + +using Qmmands; + +namespace WingsEmu.Commands.Entities +{ + public sealed class SaltyCommandResult : CommandResult + { + public SaltyCommandResult(bool isSuccessful, string message = null) + { + IsSuccessful = isSuccessful; + Message = message; + } + + public override bool IsSuccessful { get; } + public string Message { get; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Commands/Entities/SaltyModuleBase.cs b/srcs/WingsAPI.Commands/Entities/SaltyModuleBase.cs new file mode 100644 index 0000000..560f978 --- /dev/null +++ b/srcs/WingsAPI.Commands/Entities/SaltyModuleBase.cs @@ -0,0 +1,12 @@ +// WingsEmu +// +// Developed by NosWings Team + +using Qmmands; + +namespace WingsEmu.Commands.Entities +{ + public class SaltyModuleBase : ModuleBase + { + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Commands/Entities/WingsEmuIngameCommandContext.cs b/srcs/WingsAPI.Commands/Entities/WingsEmuIngameCommandContext.cs new file mode 100644 index 0000000..20ba16d --- /dev/null +++ b/srcs/WingsAPI.Commands/Entities/WingsEmuIngameCommandContext.cs @@ -0,0 +1,36 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using Qmmands; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Commands.Entities +{ + public sealed class WingsEmuIngameCommandContext : CommandContext + { + public WingsEmuIngameCommandContext(string message, IClientSession sender, string prefix, CommandService cmds, IServiceProvider services) : base(services) + { + CommandService = cmds; + + Message = message; + Player = sender; + Prefix = prefix; + + int pos = message.IndexOf(prefix, StringComparison.Ordinal) + 1; + Input = message.Substring(pos); + } + + public CommandService CommandService { get; } + + public Command Command { get; set; } + + public string Message { get; set; } + + public string Prefix { get; set; } + public IClientSession Player { get; set; } + + public string Input { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Commands/Interfaces/ICommandContainer.cs b/srcs/WingsAPI.Commands/Interfaces/ICommandContainer.cs new file mode 100644 index 0000000..3606e85 --- /dev/null +++ b/srcs/WingsAPI.Commands/Interfaces/ICommandContainer.cs @@ -0,0 +1,88 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Threading.Tasks; +using Qmmands; +using WingsEmu.Commands.Entities; + +namespace WingsEmu.Commands.Interfaces +{ + public interface ICommandContainer + { + /// + /// Represents the Qmmands Container for DependencyInjection + /// + IServiceProvider Services { get; } + + /// + /// Asynchronously adds a module to CommandContainer. + /// + void AddModule() where T : SaltyModuleBase; + + /// + /// Asynchronously removes a module from the CommandContainer. + /// + /// s + void RemoveModule() where T : SaltyModuleBase; + + /// + /// Returns a module by its name; + /// + /// + /// Name of the module. If the module has a Name attribute, the name will be the value of that + /// attribute. If it doesn't, the name is the class name. + /// + /// Case sensitive. + /// + Module[] GetModulesByName(string name, bool caseSensitive = true); + + /// + /// Returns a command by its name. + /// + /// Name of the command. + /// Case sensitive. + /// + Command[] GetCommandsByName(string name, bool caseSensitive = true); + + /* + /// + /// Removes a command by its name. + /// + /// Name of the command. + /// Case sensitive. + /// + Task RemoveCommandsAsync(string commandName, bool caseSensitive = true); + + /// + /// Removes the specified commands from the CommandContainer. + /// As it doesn't internaly exist, we will find the module of this command and rebuilt it without that command. + /// + /// Commands to remove. + /// + Task RemoveCommandsAsync(Command[] command); + + /// + /// Removes the specified command from the CommandContainer. + /// As it doesn't internaly exist, we will find the module of this command and rebuilt it without that command. + /// + /// Command to remove. + /// + Task RemoveCommandAsync(Command command); + */ + + /// + /// Adds a typeparser to the CommandContainer. + /// + /// Instance of the TypeParser to add. + void AddTypeParser(TypeParser typeParser); + + /// + /// Method which will parse the message and try to execute the command. + /// + /// Raw message to parse. + /// Entity that sent the message. + Task HandleMessageAsync(string message, object entity, string prefix); + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Commands/TypeParsers/ItemTypeParser.cs b/srcs/WingsAPI.Commands/TypeParsers/ItemTypeParser.cs new file mode 100644 index 0000000..a7e8ed1 --- /dev/null +++ b/srcs/WingsAPI.Commands/TypeParsers/ItemTypeParser.cs @@ -0,0 +1,28 @@ +using System.Threading.Tasks; +using Qmmands; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers.StaticData; + +namespace WingsEmu.Commands.TypeParsers +{ + public sealed class ItemTypeParser : TypeParser + { + private readonly IItemsManager _itemManager; + + public ItemTypeParser(IItemsManager itemsManager) => _itemManager = itemsManager; + + public override ValueTask> ParseAsync(Parameter parameter, string value, CommandContext context) + { + if (!short.TryParse(value, out short itemVNum)) + { + return new ValueTask>(new TypeParserResult($"The given Item ID was invalid. ({value})")); + } + + IGameItem gameItem = _itemManager.GetItem(itemVNum); + + return gameItem is null + ? new ValueTask>(new TypeParserResult($"There is no Item with ID#{itemVNum}")) + : new ValueTask>(new TypeParserResult(gameItem)); + } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Commands/TypeParsers/MapInstanceTypeParser.cs b/srcs/WingsAPI.Commands/TypeParsers/MapInstanceTypeParser.cs new file mode 100644 index 0000000..9526b36 --- /dev/null +++ b/srcs/WingsAPI.Commands/TypeParsers/MapInstanceTypeParser.cs @@ -0,0 +1,29 @@ +using System; +using System.Threading.Tasks; +using Qmmands; +using WingsEmu.Game.Maps; + +namespace WingsEmu.Commands.TypeParsers +{ + public sealed class MapInstanceTypeParser : TypeParser + { + private readonly IMapManager _mapManager; + + public MapInstanceTypeParser(IMapManager mapManager) => _mapManager = mapManager; + + public override ValueTask> ParseAsync(Parameter param, string value, CommandContext context) + { + if (!short.TryParse(value, out short mapId)) + { + return new ValueTask>(new TypeParserResult($"The given map ID was invalid. ({value})")); + } + + Guid mapGuid = _mapManager.GetBaseMapInstanceIdByMapId(mapId); + IMapInstance map = _mapManager.GetMapInstance(mapGuid); + + return map is null + ? new ValueTask>(new TypeParserResult($"A map with ID#{mapId} doesn't exist.")) + : new ValueTask>(new TypeParserResult(map)); + } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Commands/TypeParsers/PlayerEntityTypeParser.cs b/srcs/WingsAPI.Commands/TypeParsers/PlayerEntityTypeParser.cs new file mode 100644 index 0000000..c8576ee --- /dev/null +++ b/srcs/WingsAPI.Commands/TypeParsers/PlayerEntityTypeParser.cs @@ -0,0 +1,27 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Threading.Tasks; +using Qmmands; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Commands.TypeParsers +{ + public sealed class PlayerEntityTypeParser : TypeParser + { + private readonly ISessionManager _sessionManager; + + public PlayerEntityTypeParser(ISessionManager sessionManager) => _sessionManager = sessionManager; + + public override ValueTask> ParseAsync(Parameter param, string value, CommandContext context) + { + IClientSession player = _sessionManager.GetSessionByCharacterName(value); + + return player is null + ? new ValueTask>(new TypeParserResult($"Player {value} is not connected or doesn't exist.")) + : new ValueTask>(new TypeParserResult(player)); + } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Commands/WingsAPI.Commands.csproj b/srcs/WingsAPI.Commands/WingsAPI.Commands.csproj new file mode 100644 index 0000000..2e71cb4 --- /dev/null +++ b/srcs/WingsAPI.Commands/WingsAPI.Commands.csproj @@ -0,0 +1,17 @@ + + + + net5.0 + WingsEmu.Commands + + + + + + + + + + + + diff --git a/srcs/WingsAPI.Communication/Auth/AuthorizedClientVersionDto.cs b/srcs/WingsAPI.Communication/Auth/AuthorizedClientVersionDto.cs new file mode 100644 index 0000000..8f21bc3 --- /dev/null +++ b/srcs/WingsAPI.Communication/Auth/AuthorizedClientVersionDto.cs @@ -0,0 +1,19 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using PhoenixLib.DAL; + +namespace WingsAPI.Communication.Auth +{ + public class AuthorizedClientVersionDto : ILongDto + { + public string ClientVersion { get; set; } + + public string ExecutableHash { get; set; } + + public string DllHash { get; set; } + + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long Id { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Auth/BlacklistedHwidDto.cs b/srcs/WingsAPI.Communication/Auth/BlacklistedHwidDto.cs new file mode 100644 index 0000000..80f5cf2 --- /dev/null +++ b/srcs/WingsAPI.Communication/Auth/BlacklistedHwidDto.cs @@ -0,0 +1,20 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.ComponentModel.DataAnnotations; + +namespace WingsAPI.Communication.Auth +{ + public class BlacklistedHwidDto + { + [Required] + public string HardwareId { get; set; } + + [Required] + public string Comment { get; set; } + + [Required] + public string Judge { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Auth/IHardwareIdService.cs b/srcs/WingsAPI.Communication/Auth/IHardwareIdService.cs new file mode 100644 index 0000000..da42cf3 --- /dev/null +++ b/srcs/WingsAPI.Communication/Auth/IHardwareIdService.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace WingsAPI.Communication.Auth +{ + public interface IHardwareIdService + { + Task SynchronizeWithDbAsync(IEnumerable dtos); + Task CanLogin(string hardwareId); + Task RegisterHardwareId(string hardwareId); + Task UnregisterHardwareId(string hardwareId); + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/BasicRpcResponse.cs b/srcs/WingsAPI.Communication/BasicRpcResponse.cs new file mode 100644 index 0000000..88d3287 --- /dev/null +++ b/srcs/WingsAPI.Communication/BasicRpcResponse.cs @@ -0,0 +1,11 @@ +using ProtoBuf; + +namespace WingsAPI.Communication +{ + [ProtoContract] + public class BasicRpcResponse + { + [ProtoMember(1)] + public RpcResponseType ResponseType { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Bazaar/BazaarAddItemRequest.cs b/srcs/WingsAPI.Communication/Bazaar/BazaarAddItemRequest.cs new file mode 100644 index 0000000..3cf2bac --- /dev/null +++ b/srcs/WingsAPI.Communication/Bazaar/BazaarAddItemRequest.cs @@ -0,0 +1,24 @@ +using ProtoBuf; +using WingsAPI.Data.Bazaar; + +namespace WingsAPI.Communication.Bazaar +{ + [ProtoContract] + public class BazaarAddItemRequest + { + [ProtoMember(1)] + public int ChannelId { get; set; } + + [ProtoMember(2)] + public BazaarItemDTO BazaarItemDto { get; set; } + + [ProtoMember(4)] + public string OwnerName { get; set; } + + [ProtoMember(5)] + public long SunkGold { get; set; } + + [ProtoMember(6)] + public int MaximumListedItems { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Bazaar/BazaarBuyItemRequest.cs b/srcs/WingsAPI.Communication/Bazaar/BazaarBuyItemRequest.cs new file mode 100644 index 0000000..2114b0d --- /dev/null +++ b/srcs/WingsAPI.Communication/Bazaar/BazaarBuyItemRequest.cs @@ -0,0 +1,23 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.Bazaar +{ + [ProtoContract] + public class BazaarBuyItemRequest + { + [ProtoMember(1)] + public int ChannelId { get; set; } + + [ProtoMember(2)] + public long BazaarItemId { get; set; } + + [ProtoMember(3)] + public long BuyerCharacterId { get; set; } + + [ProtoMember(4)] + public short Amount { get; set; } + + [ProtoMember(5)] + public long PricePerItem { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Bazaar/BazaarChangeItemPriceRequest.cs b/srcs/WingsAPI.Communication/Bazaar/BazaarChangeItemPriceRequest.cs new file mode 100644 index 0000000..1e35df0 --- /dev/null +++ b/srcs/WingsAPI.Communication/Bazaar/BazaarChangeItemPriceRequest.cs @@ -0,0 +1,27 @@ +using ProtoBuf; +using WingsAPI.Data.Bazaar; + +namespace WingsAPI.Communication.Bazaar +{ + [ProtoContract] + public class BazaarChangeItemPriceRequest + { + [ProtoMember(1)] + public int ChannelId { get; set; } + + [ProtoMember(2)] + public BazaarItemDTO BazaarItemDto { get; set; } + + [ProtoMember(3)] + public long ChangerCharacterId { get; init; } + + [ProtoMember(4)] + public long NewPrice { get; set; } + + [ProtoMember(5)] + public long NewSaleFee { get; set; } + + [ProtoMember(6)] + public long SunkGold { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Bazaar/BazaarGetItemByIdRequest.cs b/srcs/WingsAPI.Communication/Bazaar/BazaarGetItemByIdRequest.cs new file mode 100644 index 0000000..ef1cbd7 --- /dev/null +++ b/srcs/WingsAPI.Communication/Bazaar/BazaarGetItemByIdRequest.cs @@ -0,0 +1,11 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.Bazaar +{ + [ProtoContract] + public class BazaarGetItemByIdRequest + { + [ProtoMember(1)] + public long BazaarItemId { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Bazaar/BazaarGetItemsByCharIdRequest.cs b/srcs/WingsAPI.Communication/Bazaar/BazaarGetItemsByCharIdRequest.cs new file mode 100644 index 0000000..b2e0091 --- /dev/null +++ b/srcs/WingsAPI.Communication/Bazaar/BazaarGetItemsByCharIdRequest.cs @@ -0,0 +1,11 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.Bazaar +{ + [ProtoContract] + public class BazaarGetItemsByCharIdRequest + { + [ProtoMember(1)] + public long CharacterId { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Bazaar/BazaarGetItemsByCharIdResponse.cs b/srcs/WingsAPI.Communication/Bazaar/BazaarGetItemsByCharIdResponse.cs new file mode 100644 index 0000000..4d7c6cb --- /dev/null +++ b/srcs/WingsAPI.Communication/Bazaar/BazaarGetItemsByCharIdResponse.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using ProtoBuf; +using WingsAPI.Data.Bazaar; + +namespace WingsAPI.Communication.Bazaar +{ + [ProtoContract] + public class BazaarGetItemsByCharIdResponse + { + [ProtoMember(1)] + public RpcResponseType ResponseType { get; set; } + + [ProtoMember(2)] + public ICollection BazaarItems { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Bazaar/BazaarItemResponse.cs b/srcs/WingsAPI.Communication/Bazaar/BazaarItemResponse.cs new file mode 100644 index 0000000..237eeae --- /dev/null +++ b/srcs/WingsAPI.Communication/Bazaar/BazaarItemResponse.cs @@ -0,0 +1,15 @@ +using ProtoBuf; +using WingsAPI.Data.Bazaar; + +namespace WingsAPI.Communication.Bazaar +{ + [ProtoContract] + public class BazaarItemResponse + { + [ProtoMember(1)] + public RpcResponseType ResponseType { get; set; } + + [ProtoMember(2)] + public BazaarItemDTO BazaarItemDto { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Bazaar/BazaarRemoveItemRequest.cs b/srcs/WingsAPI.Communication/Bazaar/BazaarRemoveItemRequest.cs new file mode 100644 index 0000000..47cc3b3 --- /dev/null +++ b/srcs/WingsAPI.Communication/Bazaar/BazaarRemoveItemRequest.cs @@ -0,0 +1,18 @@ +using ProtoBuf; +using WingsAPI.Data.Bazaar; + +namespace WingsAPI.Communication.Bazaar +{ + [ProtoContract] + public class BazaarRemoveItemRequest + { + [ProtoMember(1)] + public int ChannelId { get; init; } + + [ProtoMember(2)] + public BazaarItemDTO BazaarItemDto { get; init; } + + [ProtoMember(3)] + public long RequesterCharacterId { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Bazaar/BazaarRemoveItemsByCharIdRequest.cs b/srcs/WingsAPI.Communication/Bazaar/BazaarRemoveItemsByCharIdRequest.cs new file mode 100644 index 0000000..e8b6486 --- /dev/null +++ b/srcs/WingsAPI.Communication/Bazaar/BazaarRemoveItemsByCharIdRequest.cs @@ -0,0 +1,11 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.Bazaar +{ + [ProtoContract] + public class BazaarRemoveItemsByCharIdRequest + { + [ProtoMember(1)] + public long CharacterId { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Bazaar/BazaarRemoveItemsByCharIdResponse.cs b/srcs/WingsAPI.Communication/Bazaar/BazaarRemoveItemsByCharIdResponse.cs new file mode 100644 index 0000000..8e24d81 --- /dev/null +++ b/srcs/WingsAPI.Communication/Bazaar/BazaarRemoveItemsByCharIdResponse.cs @@ -0,0 +1,11 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.Bazaar +{ + [ProtoContract] + public class BazaarRemoveItemsByCharIdResponse + { + [ProtoMember(1)] + public RpcResponseType ResponseType { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Bazaar/BazaarSearchBazaarItemsRequest.cs b/srcs/WingsAPI.Communication/Bazaar/BazaarSearchBazaarItemsRequest.cs new file mode 100644 index 0000000..c941262 --- /dev/null +++ b/srcs/WingsAPI.Communication/Bazaar/BazaarSearchBazaarItemsRequest.cs @@ -0,0 +1,11 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.Bazaar +{ + [ProtoContract] + public class BazaarSearchBazaarItemsRequest + { + [ProtoMember(1)] + public BazaarSearchContext BazaarSearchContext { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Bazaar/BazaarSearchBazaarItemsResponse.cs b/srcs/WingsAPI.Communication/Bazaar/BazaarSearchBazaarItemsResponse.cs new file mode 100644 index 0000000..da6b862 --- /dev/null +++ b/srcs/WingsAPI.Communication/Bazaar/BazaarSearchBazaarItemsResponse.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using ProtoBuf; +using WingsAPI.Data.Bazaar; + +namespace WingsAPI.Communication.Bazaar +{ + [ProtoContract] + public class BazaarSearchBazaarItemsResponse + { + [ProtoMember(1)] + public RpcResponseType ResponseType { get; set; } + + [ProtoMember(2)] + public IReadOnlyCollection BazaarItemDtos { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Bazaar/BazaarSearchContext.cs b/srcs/WingsAPI.Communication/Bazaar/BazaarSearchContext.cs new file mode 100644 index 0000000..a235acd --- /dev/null +++ b/srcs/WingsAPI.Communication/Bazaar/BazaarSearchContext.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using ProtoBuf; +using WingsAPI.Packets.Enums.Bazaar.Filter; + +namespace WingsAPI.Communication.Bazaar +{ + [ProtoContract] + public class BazaarSearchContext + { + [ProtoMember(1)] + public int Index { get; set; } + + [ProtoMember(2)] + public BazaarCategoryFilterType CategoryFilterType { get; set; } + + [ProtoMember(3)] + public byte SubTypeFilter { get; set; } + + [ProtoMember(4)] + public BazaarLevelFilterType LevelFilter { get; set; } + + [ProtoMember(5)] + public BazaarRarityFilterType RareFilter { get; set; } + + [ProtoMember(6)] + public BazaarUpgradeFilterType UpgradeFilter { get; set; } + + [ProtoMember(7)] + public BazaarSortFilterType OrderFilter { get; set; } + + [ProtoMember(8)] + public IReadOnlyCollection ItemVNumFilter { get; set; } + + [ProtoMember(9)] + public int AmountOfItemsPerIndex { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Bazaar/IBazaarService.cs b/srcs/WingsAPI.Communication/Bazaar/IBazaarService.cs new file mode 100644 index 0000000..083404b --- /dev/null +++ b/srcs/WingsAPI.Communication/Bazaar/IBazaarService.cs @@ -0,0 +1,62 @@ +using System.Collections.Generic; +using System.ServiceModel; +using System.Threading.Tasks; +using ProtoBuf; + +namespace WingsAPI.Communication.Bazaar +{ + [ServiceContract] + public interface IBazaarService + { + [OperationContract] + ValueTask GetBazaarItemById(BazaarGetItemByIdRequest request); + + [OperationContract] + ValueTask AddItemToBazaar(BazaarAddItemRequest request); + + [OperationContract] + ValueTask RemoveItemFromBazaar(BazaarRemoveItemRequest request); + + [OperationContract] + ValueTask ChangeItemPriceFromBazaar(BazaarChangeItemPriceRequest request); + + [OperationContract] + ValueTask GetItemsByCharacterIdFromBazaar(BazaarGetItemsByCharIdRequest request); + + [OperationContract] + ValueTask RemoveItemsByCharacterIdFromBazaar(BazaarRemoveItemsByCharIdRequest request); + + [OperationContract] + ValueTask SearchBazaarItems(BazaarSearchBazaarItemsRequest request); + + [OperationContract] + ValueTask BuyItemFromBazaar(BazaarBuyItemRequest request); + + [OperationContract] + ValueTask UnlistItemsFromBazaarWithVnumAsync(UnlistItemFromBazaarRequest request); + + [OperationContract] + ValueTask UnlistCharacterItemsFromBazaarAsync(UnlistCharacterItemsFromBazaarRequest request); + } + + [ProtoContract] + public class UnlistItemFromBazaarRequest + { + [ProtoMember(1)] + public List Vnum { get; set; } + } + + [ProtoContract] + public class UnlistCharacterItemsFromBazaarRequest + { + [ProtoMember(1)] + public int Id { get; set; } + } + + [ProtoContract] + public class UnlistItemFromBazaarResponse + { + [ProtoMember(1)] + public int UnlistedItems { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Compliments/ComplimentsMonthlyRefreshMessage.cs b/srcs/WingsAPI.Communication/Compliments/ComplimentsMonthlyRefreshMessage.cs new file mode 100644 index 0000000..47a472a --- /dev/null +++ b/srcs/WingsAPI.Communication/Compliments/ComplimentsMonthlyRefreshMessage.cs @@ -0,0 +1,11 @@ +using PhoenixLib.ServiceBus; +using PhoenixLib.ServiceBus.Routing; + +namespace WingsAPI.Communication.Compliments +{ + [MessageType("compliments.refresh.monthly")] + public class ComplimentsMonthlyRefreshMessage : IMessage + { + public bool Force { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/DbServer/AccountService/AccountBanGetRequest.cs b/srcs/WingsAPI.Communication/DbServer/AccountService/AccountBanGetRequest.cs new file mode 100644 index 0000000..92afd0f --- /dev/null +++ b/srcs/WingsAPI.Communication/DbServer/AccountService/AccountBanGetRequest.cs @@ -0,0 +1,11 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.DbServer.AccountService +{ + [ProtoContract] + public class AccountBanGetRequest + { + [ProtoMember(1)] + public long AccountId { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/DbServer/AccountService/AccountBanGetResponse.cs b/srcs/WingsAPI.Communication/DbServer/AccountService/AccountBanGetResponse.cs new file mode 100644 index 0000000..db910d0 --- /dev/null +++ b/srcs/WingsAPI.Communication/DbServer/AccountService/AccountBanGetResponse.cs @@ -0,0 +1,15 @@ +using ProtoBuf; +using WingsAPI.Data.Account; + +namespace WingsAPI.Communication.DbServer.AccountService +{ + [ProtoContract] + public class AccountBanGetResponse + { + [ProtoMember(1)] + public RpcResponseType ResponseType { get; init; } + + [ProtoMember(2)] + public AccountBanDto AccountBanDto { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/DbServer/AccountService/AccountBanSaveRequest.cs b/srcs/WingsAPI.Communication/DbServer/AccountService/AccountBanSaveRequest.cs new file mode 100644 index 0000000..902ddbf --- /dev/null +++ b/srcs/WingsAPI.Communication/DbServer/AccountService/AccountBanSaveRequest.cs @@ -0,0 +1,12 @@ +using ProtoBuf; +using WingsAPI.Data.Account; + +namespace WingsAPI.Communication.DbServer.AccountService +{ + [ProtoContract] + public class AccountBanSaveRequest + { + [ProtoMember(1)] + public AccountBanDto AccountBanDto { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/DbServer/AccountService/AccountBanSaveResponse.cs b/srcs/WingsAPI.Communication/DbServer/AccountService/AccountBanSaveResponse.cs new file mode 100644 index 0000000..e09c45d --- /dev/null +++ b/srcs/WingsAPI.Communication/DbServer/AccountService/AccountBanSaveResponse.cs @@ -0,0 +1,15 @@ +using ProtoBuf; +using WingsAPI.Data.Account; + +namespace WingsAPI.Communication.DbServer.AccountService +{ + [ProtoContract] + public class AccountBanSaveResponse + { + [ProtoMember(1)] + public RpcResponseType ResponseType { get; init; } + + [ProtoMember(2)] + public AccountBanDto AccountBanDto { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/DbServer/AccountService/AccountLoadByIdRequest.cs b/srcs/WingsAPI.Communication/DbServer/AccountService/AccountLoadByIdRequest.cs new file mode 100644 index 0000000..2dabfd9 --- /dev/null +++ b/srcs/WingsAPI.Communication/DbServer/AccountService/AccountLoadByIdRequest.cs @@ -0,0 +1,11 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.DbServer.AccountService +{ + [ProtoContract] + public class AccountLoadByIdRequest + { + [ProtoMember(1)] + public long AccountId { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/DbServer/AccountService/AccountLoadByNameRequest.cs b/srcs/WingsAPI.Communication/DbServer/AccountService/AccountLoadByNameRequest.cs new file mode 100644 index 0000000..072a51f --- /dev/null +++ b/srcs/WingsAPI.Communication/DbServer/AccountService/AccountLoadByNameRequest.cs @@ -0,0 +1,11 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.DbServer.AccountService +{ + [ProtoContract] + public class AccountLoadByNameRequest + { + [ProtoMember(1)] + public string Name { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/DbServer/AccountService/AccountLoadResponse.cs b/srcs/WingsAPI.Communication/DbServer/AccountService/AccountLoadResponse.cs new file mode 100644 index 0000000..784c349 --- /dev/null +++ b/srcs/WingsAPI.Communication/DbServer/AccountService/AccountLoadResponse.cs @@ -0,0 +1,15 @@ +using ProtoBuf; +using WingsAPI.Data.Account; + +namespace WingsAPI.Communication.DbServer.AccountService +{ + [ProtoContract] + public class AccountLoadResponse + { + [ProtoMember(1)] + public RpcResponseType ResponseType { get; init; } + + [ProtoMember(2)] + public AccountDTO AccountDto { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/DbServer/AccountService/AccountPenaltyGetAllResponse.cs b/srcs/WingsAPI.Communication/DbServer/AccountService/AccountPenaltyGetAllResponse.cs new file mode 100644 index 0000000..1f9a552 --- /dev/null +++ b/srcs/WingsAPI.Communication/DbServer/AccountService/AccountPenaltyGetAllResponse.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using ProtoBuf; +using WingsAPI.Data.Account; + +namespace WingsAPI.Communication.DbServer.AccountService +{ + [ProtoContract] + public class AccountPenaltyGetAllResponse + { + [ProtoMember(1)] + public RpcResponseType ResponseType { get; init; } + + [ProtoMember(2)] + public List AccountPenaltyDtos { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/DbServer/AccountService/AccountPenaltyGetRequest.cs b/srcs/WingsAPI.Communication/DbServer/AccountService/AccountPenaltyGetRequest.cs new file mode 100644 index 0000000..d4a9d6a --- /dev/null +++ b/srcs/WingsAPI.Communication/DbServer/AccountService/AccountPenaltyGetRequest.cs @@ -0,0 +1,11 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.DbServer.AccountService +{ + [ProtoContract] + public class AccountPenaltyGetRequest + { + [ProtoMember(1)] + public long AccountId { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/DbServer/AccountService/AccountPenaltyMultiSaveRequest.cs b/srcs/WingsAPI.Communication/DbServer/AccountService/AccountPenaltyMultiSaveRequest.cs new file mode 100644 index 0000000..3ee36d7 --- /dev/null +++ b/srcs/WingsAPI.Communication/DbServer/AccountService/AccountPenaltyMultiSaveRequest.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using ProtoBuf; +using WingsAPI.Data.Account; + +namespace WingsAPI.Communication.DbServer.AccountService +{ + [ProtoContract] + public class AccountPenaltyMultiSaveRequest + { + [ProtoMember(1)] + public IReadOnlyList AccountPenaltyDtos { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/DbServer/AccountService/AccountPenaltyMultiSaveResponse.cs b/srcs/WingsAPI.Communication/DbServer/AccountService/AccountPenaltyMultiSaveResponse.cs new file mode 100644 index 0000000..2882717 --- /dev/null +++ b/srcs/WingsAPI.Communication/DbServer/AccountService/AccountPenaltyMultiSaveResponse.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using ProtoBuf; +using WingsAPI.Data.Account; + +namespace WingsAPI.Communication.DbServer.AccountService +{ + [ProtoContract] + public class AccountPenaltyMultiSaveResponse + { + [ProtoMember(1)] + public RpcResponseType ResponseType { get; init; } + + [ProtoMember(2)] + public IEnumerable AccountPenaltyDtos { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/DbServer/AccountService/AccountSaveRequest.cs b/srcs/WingsAPI.Communication/DbServer/AccountService/AccountSaveRequest.cs new file mode 100644 index 0000000..9ddb3af --- /dev/null +++ b/srcs/WingsAPI.Communication/DbServer/AccountService/AccountSaveRequest.cs @@ -0,0 +1,12 @@ +using ProtoBuf; +using WingsAPI.Data.Account; + +namespace WingsAPI.Communication.DbServer.AccountService +{ + [ProtoContract] + public class AccountSaveRequest + { + [ProtoMember(1)] + public AccountDTO AccountDto { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/DbServer/AccountService/AccountSaveResponse.cs b/srcs/WingsAPI.Communication/DbServer/AccountService/AccountSaveResponse.cs new file mode 100644 index 0000000..d1a8754 --- /dev/null +++ b/srcs/WingsAPI.Communication/DbServer/AccountService/AccountSaveResponse.cs @@ -0,0 +1,15 @@ +using ProtoBuf; +using WingsAPI.Data.Account; + +namespace WingsAPI.Communication.DbServer.AccountService +{ + [ProtoContract] + public class AccountSaveResponse + { + [ProtoMember(1)] + public RpcResponseType ResponseType { get; init; } + + [ProtoMember(2)] + public AccountDTO AccountDto { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/DbServer/AccountService/IAccountService.cs b/srcs/WingsAPI.Communication/DbServer/AccountService/IAccountService.cs new file mode 100644 index 0000000..bd383d2 --- /dev/null +++ b/srcs/WingsAPI.Communication/DbServer/AccountService/IAccountService.cs @@ -0,0 +1,39 @@ +using System.ServiceModel; +using System.Threading.Tasks; + +namespace WingsAPI.Communication.DbServer.AccountService +{ + [ServiceContract] + public interface IAccountService + { + /* + * AccountDTO + */ + [OperationContract] + Task LoadAccountByName(AccountLoadByNameRequest request); + + [OperationContract] + Task LoadAccountById(AccountLoadByIdRequest request); + + [OperationContract] + Task SaveAccount(AccountSaveRequest request); + + /* + * AccountBanDTO + */ + [OperationContract] + Task GetAccountBan(AccountBanGetRequest request); + + [OperationContract] + Task SaveAccountBan(AccountBanSaveRequest request); + + /* + * AccountPenaltyDTO + */ + [OperationContract] + Task GetAccountPenalties(AccountPenaltyGetRequest request); + + [OperationContract] + Task SaveAccountPenalties(AccountPenaltyMultiSaveRequest request); + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/DbServer/CharacterService/CharacterGetTopResponse.cs b/srcs/WingsAPI.Communication/DbServer/CharacterService/CharacterGetTopResponse.cs new file mode 100644 index 0000000..df1e546 --- /dev/null +++ b/srcs/WingsAPI.Communication/DbServer/CharacterService/CharacterGetTopResponse.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using ProtoBuf; +using WingsAPI.Data.Character; + +namespace WingsAPI.Communication.DbServer.CharacterService +{ + [ProtoContract] + public class CharacterGetTopResponse + { + [ProtoMember(1)] + public RpcResponseType ResponseType { get; init; } + + [ProtoMember(2)] + public IReadOnlyList Top { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/DbServer/CharacterService/CharacterRefreshRankingResponse.cs b/srcs/WingsAPI.Communication/DbServer/CharacterService/CharacterRefreshRankingResponse.cs new file mode 100644 index 0000000..56371ab --- /dev/null +++ b/srcs/WingsAPI.Communication/DbServer/CharacterService/CharacterRefreshRankingResponse.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using ProtoBuf; +using WingsAPI.Data.Character; + +namespace WingsAPI.Communication.DbServer.CharacterService +{ + [ProtoContract] + public class CharacterRefreshRankingResponse + { + [ProtoMember(1)] + public RpcResponseType ResponseType { get; init; } + + [ProtoMember(2)] + public IReadOnlyList TopCompliment { get; init; } + + [ProtoMember(3)] + public IReadOnlyList TopPoints { get; init; } + + [ProtoMember(4)] + public IReadOnlyList TopReputation { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/DbServer/CharacterService/DbServerDeleteCharacterRequest.cs b/srcs/WingsAPI.Communication/DbServer/CharacterService/DbServerDeleteCharacterRequest.cs new file mode 100644 index 0000000..5d78086 --- /dev/null +++ b/srcs/WingsAPI.Communication/DbServer/CharacterService/DbServerDeleteCharacterRequest.cs @@ -0,0 +1,12 @@ +using ProtoBuf; +using WingsAPI.Data.Character; + +namespace WingsAPI.Communication.DbServer.CharacterService +{ + [ProtoContract] + public class DbServerDeleteCharacterRequest + { + [ProtoMember(1)] + public CharacterDTO CharacterDto { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/DbServer/CharacterService/DbServerDeleteCharacterResponse.cs b/srcs/WingsAPI.Communication/DbServer/CharacterService/DbServerDeleteCharacterResponse.cs new file mode 100644 index 0000000..9b772c6 --- /dev/null +++ b/srcs/WingsAPI.Communication/DbServer/CharacterService/DbServerDeleteCharacterResponse.cs @@ -0,0 +1,11 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.DbServer.CharacterService +{ + [ProtoContract] + public class DbServerDeleteCharacterResponse + { + [ProtoMember(1)] + public RpcResponseType RpcResponseType { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/DbServer/CharacterService/DbServerFlushCharacterSavesRequest.cs b/srcs/WingsAPI.Communication/DbServer/CharacterService/DbServerFlushCharacterSavesRequest.cs new file mode 100644 index 0000000..509908c --- /dev/null +++ b/srcs/WingsAPI.Communication/DbServer/CharacterService/DbServerFlushCharacterSavesRequest.cs @@ -0,0 +1,9 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.DbServer.CharacterService +{ + [ProtoContract] + public class DbServerFlushCharacterSavesRequest + { + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/DbServer/CharacterService/DbServerFlushCharacterSavesResponse.cs b/srcs/WingsAPI.Communication/DbServer/CharacterService/DbServerFlushCharacterSavesResponse.cs new file mode 100644 index 0000000..069cc54 --- /dev/null +++ b/srcs/WingsAPI.Communication/DbServer/CharacterService/DbServerFlushCharacterSavesResponse.cs @@ -0,0 +1,11 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.DbServer.CharacterService +{ + [ProtoContract] + public class DbServerFlushCharacterSavesResponse + { + [ProtoMember(1)] + public RpcResponseType RpcResponseType { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/DbServer/CharacterService/DbServerGetCharacterByIdRequest.cs b/srcs/WingsAPI.Communication/DbServer/CharacterService/DbServerGetCharacterByIdRequest.cs new file mode 100644 index 0000000..0d6dd10 --- /dev/null +++ b/srcs/WingsAPI.Communication/DbServer/CharacterService/DbServerGetCharacterByIdRequest.cs @@ -0,0 +1,11 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.DbServer.CharacterService +{ + [ProtoContract] + public class DbServerGetCharacterByIdRequest + { + [ProtoMember(1)] + public long CharacterId { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/DbServer/CharacterService/DbServerGetCharacterFromSlotRequest.cs b/srcs/WingsAPI.Communication/DbServer/CharacterService/DbServerGetCharacterFromSlotRequest.cs new file mode 100644 index 0000000..3c1f9ed --- /dev/null +++ b/srcs/WingsAPI.Communication/DbServer/CharacterService/DbServerGetCharacterFromSlotRequest.cs @@ -0,0 +1,14 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.DbServer.CharacterService +{ + [ProtoContract] + public class DbServerGetCharacterFromSlotRequest + { + [ProtoMember(1)] + public long AccountId { get; set; } + + [ProtoMember(2)] + public byte Slot { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/DbServer/CharacterService/DbServerGetCharacterRequestByName.cs b/srcs/WingsAPI.Communication/DbServer/CharacterService/DbServerGetCharacterRequestByName.cs new file mode 100644 index 0000000..f5e4df4 --- /dev/null +++ b/srcs/WingsAPI.Communication/DbServer/CharacterService/DbServerGetCharacterRequestByName.cs @@ -0,0 +1,11 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.DbServer.CharacterService +{ + [ProtoContract] + public class DbServerGetCharacterRequestByName + { + [ProtoMember(1)] + public string CharacterName { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/DbServer/CharacterService/DbServerGetCharacterResponse.cs b/srcs/WingsAPI.Communication/DbServer/CharacterService/DbServerGetCharacterResponse.cs new file mode 100644 index 0000000..2f0c112 --- /dev/null +++ b/srcs/WingsAPI.Communication/DbServer/CharacterService/DbServerGetCharacterResponse.cs @@ -0,0 +1,15 @@ +using ProtoBuf; +using WingsAPI.Data.Character; + +namespace WingsAPI.Communication.DbServer.CharacterService +{ + [ProtoContract] + public class DbServerGetCharacterResponse + { + [ProtoMember(1)] + public RpcResponseType RpcResponseType { get; set; } + + [ProtoMember(2)] + public CharacterDTO CharacterDto { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/DbServer/CharacterService/DbServerGetCharactersRequest.cs b/srcs/WingsAPI.Communication/DbServer/CharacterService/DbServerGetCharactersRequest.cs new file mode 100644 index 0000000..fcef125 --- /dev/null +++ b/srcs/WingsAPI.Communication/DbServer/CharacterService/DbServerGetCharactersRequest.cs @@ -0,0 +1,11 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.DbServer.CharacterService +{ + [ProtoContract] + public class DbServerGetCharactersRequest + { + [ProtoMember(1)] + public long AccountId { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/DbServer/CharacterService/DbServerGetCharactersResponse.cs b/srcs/WingsAPI.Communication/DbServer/CharacterService/DbServerGetCharactersResponse.cs new file mode 100644 index 0000000..38a8210 --- /dev/null +++ b/srcs/WingsAPI.Communication/DbServer/CharacterService/DbServerGetCharactersResponse.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using ProtoBuf; +using WingsAPI.Data.Character; + +namespace WingsAPI.Communication.DbServer.CharacterService +{ + [ProtoContract] + public class DbServerGetCharactersResponse + { + [ProtoMember(1)] + public RpcResponseType RpcResponseType { get; set; } + + [ProtoMember(2)] + public IEnumerable Characters { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/DbServer/CharacterService/DbServerSaveCharacterRequest.cs b/srcs/WingsAPI.Communication/DbServer/CharacterService/DbServerSaveCharacterRequest.cs new file mode 100644 index 0000000..7f1329f --- /dev/null +++ b/srcs/WingsAPI.Communication/DbServer/CharacterService/DbServerSaveCharacterRequest.cs @@ -0,0 +1,15 @@ +using ProtoBuf; +using WingsAPI.Data.Character; + +namespace WingsAPI.Communication.DbServer.CharacterService +{ + [ProtoContract] + public class DbServerSaveCharacterRequest + { + [ProtoMember(1)] + public CharacterDTO Character { get; set; } + + [ProtoMember(2)] + public bool IgnoreSlotCheck { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/DbServer/CharacterService/DbServerSaveCharacterResponse.cs b/srcs/WingsAPI.Communication/DbServer/CharacterService/DbServerSaveCharacterResponse.cs new file mode 100644 index 0000000..f87a2f8 --- /dev/null +++ b/srcs/WingsAPI.Communication/DbServer/CharacterService/DbServerSaveCharacterResponse.cs @@ -0,0 +1,15 @@ +using ProtoBuf; +using WingsAPI.Data.Character; + +namespace WingsAPI.Communication.DbServer.CharacterService +{ + [ProtoContract] + public class DbServerSaveCharacterResponse + { + [ProtoMember(1)] + public RpcResponseType RpcResponseType { get; set; } + + [ProtoMember(2)] + public CharacterDTO Character { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/DbServer/CharacterService/DbServerSaveCharactersRequest.cs b/srcs/WingsAPI.Communication/DbServer/CharacterService/DbServerSaveCharactersRequest.cs new file mode 100644 index 0000000..4e8b60f --- /dev/null +++ b/srcs/WingsAPI.Communication/DbServer/CharacterService/DbServerSaveCharactersRequest.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using ProtoBuf; +using WingsAPI.Data.Character; + +namespace WingsAPI.Communication.DbServer.CharacterService +{ + [ProtoContract] + public class DbServerSaveCharactersRequest + { + [ProtoMember(1)] + public List Characters { get; set; } + + [ProtoMember(2)] + public DateTime Date { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/DbServer/CharacterService/DbServerSaveCharactersResponse.cs b/srcs/WingsAPI.Communication/DbServer/CharacterService/DbServerSaveCharactersResponse.cs new file mode 100644 index 0000000..eadaa08 --- /dev/null +++ b/srcs/WingsAPI.Communication/DbServer/CharacterService/DbServerSaveCharactersResponse.cs @@ -0,0 +1,11 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.DbServer.CharacterService +{ + [ProtoContract] + public class DbServerSaveCharactersResponse + { + [ProtoMember(1)] + public RpcResponseType RpcResponseType { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/DbServer/CharacterService/ICharacterService.cs b/srcs/WingsAPI.Communication/DbServer/CharacterService/ICharacterService.cs new file mode 100644 index 0000000..fc8c924 --- /dev/null +++ b/srcs/WingsAPI.Communication/DbServer/CharacterService/ICharacterService.cs @@ -0,0 +1,57 @@ +using System.ServiceModel; +using System.Threading.Tasks; + +namespace WingsAPI.Communication.DbServer.CharacterService +{ + [ServiceContract] + public interface ICharacterService + { + /* + * CharacterSaves + */ + [OperationContract] + Task SaveCharacters(DbServerSaveCharactersRequest request); + + [OperationContract] + Task SaveCharacter(DbServerSaveCharacterRequest request); + + [OperationContract] + Task CreateCharacter(DbServerSaveCharacterRequest request); + + [OperationContract] + Task GetCharacters(DbServerGetCharactersRequest request); + + [OperationContract] + Task GetCharacterBySlot(DbServerGetCharacterFromSlotRequest fromSlotRequest); + + [OperationContract] + Task GetCharacterById(DbServerGetCharacterByIdRequest fromSlotRequest); + + [OperationContract] + Task GetCharacterByName(DbServerGetCharacterRequestByName request); + + [OperationContract] + Task FlushCharacterSaves(DbServerFlushCharacterSavesRequest request); + + [OperationContract] + Task DeleteCharacter(DbServerDeleteCharacterRequest request); + + [OperationContract] + Task ForceRemoveCharacterFromCache(DbServerGetCharacterRequestByName request); + + /* + * Ranking + */ + [OperationContract] + ValueTask GetTopCompliment(EmptyRpcRequest request); + + [OperationContract] + ValueTask GetTopPoints(EmptyRpcRequest request); + + [OperationContract] + ValueTask GetTopReputation(EmptyRpcRequest request); + + [OperationContract] + ValueTask RefreshRanking(EmptyRpcRequest request); + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/DbServer/TimeSpaceService/ITimeSpaceService.cs b/srcs/WingsAPI.Communication/DbServer/TimeSpaceService/ITimeSpaceService.cs new file mode 100644 index 0000000..5032908 --- /dev/null +++ b/srcs/WingsAPI.Communication/DbServer/TimeSpaceService/ITimeSpaceService.cs @@ -0,0 +1,18 @@ +using System.ServiceModel; +using System.Threading.Tasks; + +namespace WingsAPI.Communication.DbServer.TimeSpaceService +{ + [ServiceContract] + public interface ITimeSpaceService + { + [OperationContract] + ValueTask IsNewRecord(TimeSpaceIsNewRecordRequest request); + + [OperationContract] + ValueTask SetNewRecord(TimeSpaceNewRecordRequest request); + + [OperationContract] + ValueTask GetTimeSpaceRecord(TimeSpaceRecordRequest request); + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/DbServer/TimeSpaceService/TimeSpaceIsNewRecordRequest.cs b/srcs/WingsAPI.Communication/DbServer/TimeSpaceService/TimeSpaceIsNewRecordRequest.cs new file mode 100644 index 0000000..ab3ade2 --- /dev/null +++ b/srcs/WingsAPI.Communication/DbServer/TimeSpaceService/TimeSpaceIsNewRecordRequest.cs @@ -0,0 +1,14 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.DbServer.TimeSpaceService +{ + [ProtoContract] + public class TimeSpaceIsNewRecordRequest + { + [ProtoMember(1)] + public long TimeSpaceId { get; init; } + + [ProtoMember(2)] + public long Record { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/DbServer/TimeSpaceService/TimeSpaceIsNewRecordResponse.cs b/srcs/WingsAPI.Communication/DbServer/TimeSpaceService/TimeSpaceIsNewRecordResponse.cs new file mode 100644 index 0000000..073a3c5 --- /dev/null +++ b/srcs/WingsAPI.Communication/DbServer/TimeSpaceService/TimeSpaceIsNewRecordResponse.cs @@ -0,0 +1,11 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.DbServer.TimeSpaceService +{ + [ProtoContract] + public class TimeSpaceIsNewRecordResponse + { + [ProtoMember(1)] + public bool IsNewRecord { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/DbServer/TimeSpaceService/TimeSpaceNewRecordRequest.cs b/srcs/WingsAPI.Communication/DbServer/TimeSpaceService/TimeSpaceNewRecordRequest.cs new file mode 100644 index 0000000..0daf194 --- /dev/null +++ b/srcs/WingsAPI.Communication/DbServer/TimeSpaceService/TimeSpaceNewRecordRequest.cs @@ -0,0 +1,12 @@ +using ProtoBuf; +using WingsAPI.Data.TimeSpace; + +namespace WingsAPI.Communication.DbServer.TimeSpaceService +{ + [ProtoContract] + public class TimeSpaceNewRecordRequest + { + [ProtoMember(1)] + public TimeSpaceRecordDto TimeSpaceRecordDto { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/DbServer/TimeSpaceService/TimeSpaceRecordRequest.cs b/srcs/WingsAPI.Communication/DbServer/TimeSpaceService/TimeSpaceRecordRequest.cs new file mode 100644 index 0000000..4f80900 --- /dev/null +++ b/srcs/WingsAPI.Communication/DbServer/TimeSpaceService/TimeSpaceRecordRequest.cs @@ -0,0 +1,11 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.DbServer.TimeSpaceService +{ + [ProtoContract] + public class TimeSpaceRecordRequest + { + [ProtoMember(1)] + public long TimeSpaceId { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/DbServer/TimeSpaceService/TimeSpaceRecordResponse.cs b/srcs/WingsAPI.Communication/DbServer/TimeSpaceService/TimeSpaceRecordResponse.cs new file mode 100644 index 0000000..cb72655 --- /dev/null +++ b/srcs/WingsAPI.Communication/DbServer/TimeSpaceService/TimeSpaceRecordResponse.cs @@ -0,0 +1,12 @@ +using ProtoBuf; +using WingsAPI.Data.TimeSpace; + +namespace WingsAPI.Communication.DbServer.TimeSpaceService +{ + [ProtoContract] + public class TimeSpaceRecordResponse + { + [ProtoMember(1)] + public TimeSpaceRecordDto TimeSpaceRecordDto { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/DbServer/WarehouseService/AccountWarehouseAddItemRequest.cs b/srcs/WingsAPI.Communication/DbServer/WarehouseService/AccountWarehouseAddItemRequest.cs new file mode 100644 index 0000000..2efb03a --- /dev/null +++ b/srcs/WingsAPI.Communication/DbServer/WarehouseService/AccountWarehouseAddItemRequest.cs @@ -0,0 +1,12 @@ +using ProtoBuf; +using WingsAPI.Data.Account; + +namespace WingsAPI.Communication.DbServer.WarehouseService +{ + [ProtoContract] + public class AccountWarehouseAddItemRequest + { + [ProtoMember(1)] + public AccountWarehouseItemDto Item { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/DbServer/WarehouseService/AccountWarehouseAddItemResponse.cs b/srcs/WingsAPI.Communication/DbServer/WarehouseService/AccountWarehouseAddItemResponse.cs new file mode 100644 index 0000000..f4762e2 --- /dev/null +++ b/srcs/WingsAPI.Communication/DbServer/WarehouseService/AccountWarehouseAddItemResponse.cs @@ -0,0 +1,15 @@ +using ProtoBuf; +using WingsAPI.Data.Account; + +namespace WingsAPI.Communication.DbServer.WarehouseService +{ + [ProtoContract] + public class AccountWarehouseAddItemResponse + { + [ProtoMember(1)] + public RpcResponseType ResponseType { get; set; } + + [ProtoMember(2)] + public AccountWarehouseItemDto Item { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/DbServer/WarehouseService/AccountWarehouseGetItemRequest.cs b/srcs/WingsAPI.Communication/DbServer/WarehouseService/AccountWarehouseGetItemRequest.cs new file mode 100644 index 0000000..c7b7af4 --- /dev/null +++ b/srcs/WingsAPI.Communication/DbServer/WarehouseService/AccountWarehouseGetItemRequest.cs @@ -0,0 +1,14 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.DbServer.WarehouseService +{ + [ProtoContract] + public class AccountWarehouseGetItemRequest + { + [ProtoMember(1)] + public long AccountId { get; set; } + + [ProtoMember(2)] + public short Slot { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/DbServer/WarehouseService/AccountWarehouseGetItemResponse.cs b/srcs/WingsAPI.Communication/DbServer/WarehouseService/AccountWarehouseGetItemResponse.cs new file mode 100644 index 0000000..ce1dcdb --- /dev/null +++ b/srcs/WingsAPI.Communication/DbServer/WarehouseService/AccountWarehouseGetItemResponse.cs @@ -0,0 +1,15 @@ +using ProtoBuf; +using WingsAPI.Data.Account; + +namespace WingsAPI.Communication.DbServer.WarehouseService +{ + [ProtoContract] + public class AccountWarehouseGetItemResponse + { + [ProtoMember(1)] + public RpcResponseType ResponseType { get; set; } + + [ProtoMember(2)] + public AccountWarehouseItemDto Item { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/DbServer/WarehouseService/AccountWarehouseGetItemsRequest.cs b/srcs/WingsAPI.Communication/DbServer/WarehouseService/AccountWarehouseGetItemsRequest.cs new file mode 100644 index 0000000..5b6c341 --- /dev/null +++ b/srcs/WingsAPI.Communication/DbServer/WarehouseService/AccountWarehouseGetItemsRequest.cs @@ -0,0 +1,11 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.DbServer.WarehouseService +{ + [ProtoContract] + public class AccountWarehouseGetItemsRequest + { + [ProtoMember(1)] + public long AccountId { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/DbServer/WarehouseService/AccountWarehouseGetItemsResponse.cs b/srcs/WingsAPI.Communication/DbServer/WarehouseService/AccountWarehouseGetItemsResponse.cs new file mode 100644 index 0000000..d762a7b --- /dev/null +++ b/srcs/WingsAPI.Communication/DbServer/WarehouseService/AccountWarehouseGetItemsResponse.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using ProtoBuf; +using WingsAPI.Data.Account; + +namespace WingsAPI.Communication.DbServer.WarehouseService +{ + [ProtoContract] + public class AccountWarehouseGetItemsResponse + { + [ProtoMember(1)] + public RpcResponseType ResponseType { get; set; } + + [ProtoMember(2)] + public IEnumerable Items { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/DbServer/WarehouseService/AccountWarehouseMoveItemRequest.cs b/srcs/WingsAPI.Communication/DbServer/WarehouseService/AccountWarehouseMoveItemRequest.cs new file mode 100644 index 0000000..408a380 --- /dev/null +++ b/srcs/WingsAPI.Communication/DbServer/WarehouseService/AccountWarehouseMoveItemRequest.cs @@ -0,0 +1,18 @@ +using ProtoBuf; +using WingsAPI.Data.Account; + +namespace WingsAPI.Communication.DbServer.WarehouseService +{ + [ProtoContract] + public class AccountWarehouseMoveItemRequest + { + [ProtoMember(1)] + public AccountWarehouseItemDto WarehouseItemDtoToMove { get; set; } + + [ProtoMember(2)] + public int Amount { get; set; } + + [ProtoMember(3)] + public short NewSlot { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/DbServer/WarehouseService/AccountWarehouseMoveItemResponse.cs b/srcs/WingsAPI.Communication/DbServer/WarehouseService/AccountWarehouseMoveItemResponse.cs new file mode 100644 index 0000000..d9569bd --- /dev/null +++ b/srcs/WingsAPI.Communication/DbServer/WarehouseService/AccountWarehouseMoveItemResponse.cs @@ -0,0 +1,18 @@ +using ProtoBuf; +using WingsAPI.Data.Account; + +namespace WingsAPI.Communication.DbServer.WarehouseService +{ + [ProtoContract] + public class AccountWarehouseMoveItemResponse + { + [ProtoMember(1)] + public RpcResponseType ResponseType { get; set; } + + [ProtoMember(2)] + public AccountWarehouseItemDto OldItem { get; set; } + + [ProtoMember(3)] + public AccountWarehouseItemDto NewItem { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/DbServer/WarehouseService/AccountWarehouseWithdrawItemRequest.cs b/srcs/WingsAPI.Communication/DbServer/WarehouseService/AccountWarehouseWithdrawItemRequest.cs new file mode 100644 index 0000000..1215e74 --- /dev/null +++ b/srcs/WingsAPI.Communication/DbServer/WarehouseService/AccountWarehouseWithdrawItemRequest.cs @@ -0,0 +1,15 @@ +using ProtoBuf; +using WingsAPI.Data.Account; + +namespace WingsAPI.Communication.DbServer.WarehouseService +{ + [ProtoContract] + public class AccountWarehouseWithdrawItemRequest + { + [ProtoMember(1)] + public AccountWarehouseItemDto ItemToWithdraw { get; set; } + + [ProtoMember(2)] + public int Amount { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/DbServer/WarehouseService/AccountWarehouseWithdrawItemResponse.cs b/srcs/WingsAPI.Communication/DbServer/WarehouseService/AccountWarehouseWithdrawItemResponse.cs new file mode 100644 index 0000000..abbb2fb --- /dev/null +++ b/srcs/WingsAPI.Communication/DbServer/WarehouseService/AccountWarehouseWithdrawItemResponse.cs @@ -0,0 +1,19 @@ +using ProtoBuf; +using WingsAPI.Data.Account; +using WingsEmu.DTOs.Items; + +namespace WingsAPI.Communication.DbServer.WarehouseService +{ + [ProtoContract] + public class AccountWarehouseWithdrawItemResponse + { + [ProtoMember(1)] + public RpcResponseType ResponseType { get; set; } + + [ProtoMember(2)] + public AccountWarehouseItemDto UpdatedItem { get; set; } + + [ProtoMember(3)] + public ItemInstanceDTO WithdrawnItem { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/DbServer/WarehouseService/IAccountWarehouseService.cs b/srcs/WingsAPI.Communication/DbServer/WarehouseService/IAccountWarehouseService.cs new file mode 100644 index 0000000..51e7cd9 --- /dev/null +++ b/srcs/WingsAPI.Communication/DbServer/WarehouseService/IAccountWarehouseService.cs @@ -0,0 +1,24 @@ +using System.ServiceModel; +using System.Threading.Tasks; + +namespace WingsAPI.Communication.DbServer.WarehouseService +{ + [ServiceContract] + public interface IAccountWarehouseService + { + [OperationContract] + ValueTask GetItems(AccountWarehouseGetItemsRequest request); + + [OperationContract] + ValueTask GetItem(AccountWarehouseGetItemRequest request); + + [OperationContract] + ValueTask AddItem(AccountWarehouseAddItemRequest request); + + [OperationContract] + ValueTask WithdrawItem(AccountWarehouseWithdrawItemRequest request); + + [OperationContract] + ValueTask MoveItem(AccountWarehouseMoveItemRequest request); + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/EmptyResponse.cs b/srcs/WingsAPI.Communication/EmptyResponse.cs new file mode 100644 index 0000000..4915b00 --- /dev/null +++ b/srcs/WingsAPI.Communication/EmptyResponse.cs @@ -0,0 +1,9 @@ +using ProtoBuf; + +namespace WingsAPI.Communication +{ + [ProtoContract] + public class EmptyResponse + { + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/EmptyRpcRequest.cs b/srcs/WingsAPI.Communication/EmptyRpcRequest.cs new file mode 100644 index 0000000..a9b5790 --- /dev/null +++ b/srcs/WingsAPI.Communication/EmptyRpcRequest.cs @@ -0,0 +1,9 @@ +using ProtoBuf; + +namespace WingsAPI.Communication +{ + [ProtoContract] + public class EmptyRpcRequest + { + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Families/FamilyAddMemberRequest.cs b/srcs/WingsAPI.Communication/Families/FamilyAddMemberRequest.cs new file mode 100644 index 0000000..00c4e24 --- /dev/null +++ b/srcs/WingsAPI.Communication/Families/FamilyAddMemberRequest.cs @@ -0,0 +1,18 @@ +using ProtoBuf; +using WingsAPI.Data.Families; + +namespace WingsAPI.Communication.Families +{ + [ProtoContract] + public class FamilyAddMemberRequest + { + [ProtoMember(1)] + public FamilyMembershipDto Member { get; set; } + + [ProtoMember(2)] + public string Nickname { get; set; } + + [ProtoMember(3)] + public long SenderId { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Families/FamilyAddMemberResponse.cs b/srcs/WingsAPI.Communication/Families/FamilyAddMemberResponse.cs new file mode 100644 index 0000000..e9e061c --- /dev/null +++ b/srcs/WingsAPI.Communication/Families/FamilyAddMemberResponse.cs @@ -0,0 +1,12 @@ +using ProtoBuf; +using WingsAPI.Data.Families; + +namespace WingsAPI.Communication.Families +{ + [ProtoContract] + public class FamilyAddMemberResponse + { + [ProtoMember(1)] + public FamilyMembershipDto Member { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Families/FamilyChangeAuthorityRequest.cs b/srcs/WingsAPI.Communication/Families/FamilyChangeAuthorityRequest.cs new file mode 100644 index 0000000..8fb8184 --- /dev/null +++ b/srcs/WingsAPI.Communication/Families/FamilyChangeAuthorityRequest.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using ProtoBuf; + +namespace WingsAPI.Communication.Families +{ + [ProtoContract] + public class FamilyChangeAuthorityRequest + { + [ProtoMember(1)] + public List FamilyMembers { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Families/FamilyChangeContainer.cs b/srcs/WingsAPI.Communication/Families/FamilyChangeContainer.cs new file mode 100644 index 0000000..7b25748 --- /dev/null +++ b/srcs/WingsAPI.Communication/Families/FamilyChangeContainer.cs @@ -0,0 +1,15 @@ +using ProtoBuf; +using WingsEmu.Packets.Enums.Families; + +namespace WingsAPI.Communication.Families +{ + [ProtoContract] + public class FamilyChangeContainer + { + [ProtoMember(1)] + public long CharacterId { get; set; } + + [ProtoMember(2)] + public FamilyAuthority RequestedFamilyAuthority { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Families/FamilyChangeFactionRequest.cs b/srcs/WingsAPI.Communication/Families/FamilyChangeFactionRequest.cs new file mode 100644 index 0000000..9e3ea58 --- /dev/null +++ b/srcs/WingsAPI.Communication/Families/FamilyChangeFactionRequest.cs @@ -0,0 +1,15 @@ +using ProtoBuf; +using WingsEmu.Packets.Enums; + +namespace WingsAPI.Communication.Families +{ + [ProtoContract] + public class FamilyChangeFactionRequest + { + [ProtoMember(1)] + public long FamilyId { get; init; } + + [ProtoMember(2)] + public FactionType NewFaction { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Families/FamilyChangeFactionResponse.cs b/srcs/WingsAPI.Communication/Families/FamilyChangeFactionResponse.cs new file mode 100644 index 0000000..08b9b79 --- /dev/null +++ b/srcs/WingsAPI.Communication/Families/FamilyChangeFactionResponse.cs @@ -0,0 +1,11 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.Families +{ + [ProtoContract] + public class FamilyChangeFactionResponse + { + [ProtoMember(1)] + public FamilyChangeFactionResponseType Status { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Families/FamilyChangeFactionResponseType.cs b/srcs/WingsAPI.Communication/Families/FamilyChangeFactionResponseType.cs new file mode 100644 index 0000000..28ec589 --- /dev/null +++ b/srcs/WingsAPI.Communication/Families/FamilyChangeFactionResponseType.cs @@ -0,0 +1,10 @@ +namespace WingsAPI.Communication.Families +{ + public enum FamilyChangeFactionResponseType + { + GENERIC_ERROR, + ALREADY_THAT_FACTION, + UNDER_COOLDOWN, + SUCCESS + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Families/FamilyChangeTitleRequest.cs b/srcs/WingsAPI.Communication/Families/FamilyChangeTitleRequest.cs new file mode 100644 index 0000000..7e2c9cd --- /dev/null +++ b/srcs/WingsAPI.Communication/Families/FamilyChangeTitleRequest.cs @@ -0,0 +1,15 @@ +using ProtoBuf; +using WingsEmu.Packets.Enums.Families; + +namespace WingsAPI.Communication.Families +{ + [ProtoContract] + public class FamilyChangeTitleRequest + { + [ProtoMember(1)] + public long CharacterId { get; set; } + + [ProtoMember(2)] + public FamilyTitle RequestedFamilyTitle { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Families/FamilyCreateRequest.cs b/srcs/WingsAPI.Communication/Families/FamilyCreateRequest.cs new file mode 100644 index 0000000..279f6e9 --- /dev/null +++ b/srcs/WingsAPI.Communication/Families/FamilyCreateRequest.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using ProtoBuf; +using WingsAPI.Data.Families; + +namespace WingsAPI.Communication.Families +{ + [ProtoContract] + public class FamilyCreateRequest + { + [ProtoMember(1)] + public string Name { get; set; } + + [ProtoMember(2)] + public byte Level { get; set; } + + [ProtoMember(3)] + public byte MembershipCapacity { get; set; } + + [ProtoMember(4)] + public byte Faction { get; set; } + + [ProtoMember(5)] + public List Members { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Families/FamilyCreateResponse.cs b/srcs/WingsAPI.Communication/Families/FamilyCreateResponse.cs new file mode 100644 index 0000000..6d082c7 --- /dev/null +++ b/srcs/WingsAPI.Communication/Families/FamilyCreateResponse.cs @@ -0,0 +1,15 @@ +using ProtoBuf; +using WingsAPI.Data.Families; + +namespace WingsAPI.Communication.Families +{ + [ProtoContract] + public class FamilyCreateResponse + { + [ProtoMember(1)] + public FamilyCreateResponseType Status { get; set; } + + [ProtoMember(2)] + public FamilyDTO Family { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Families/FamilyCreateResponseType.cs b/srcs/WingsAPI.Communication/Families/FamilyCreateResponseType.cs new file mode 100644 index 0000000..970e2c1 --- /dev/null +++ b/srcs/WingsAPI.Communication/Families/FamilyCreateResponseType.cs @@ -0,0 +1,14 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.Families +{ + [ProtoContract] + public enum FamilyCreateResponseType + { + GENERIC_ERROR, + + NAME_ALREADY_TAKEN, + + SUCCESS + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Families/FamilyDisbandRequest.cs b/srcs/WingsAPI.Communication/Families/FamilyDisbandRequest.cs new file mode 100644 index 0000000..0d8efb3 --- /dev/null +++ b/srcs/WingsAPI.Communication/Families/FamilyDisbandRequest.cs @@ -0,0 +1,11 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.Families +{ + [ProtoContract] + public class FamilyDisbandRequest + { + [ProtoMember(1)] + public long FamilyId { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Families/FamilyIdRequest.cs b/srcs/WingsAPI.Communication/Families/FamilyIdRequest.cs new file mode 100644 index 0000000..e125a7c --- /dev/null +++ b/srcs/WingsAPI.Communication/Families/FamilyIdRequest.cs @@ -0,0 +1,11 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.Families +{ + [ProtoContract] + public class FamilyIdRequest + { + [ProtoMember(1)] + public long FamilyId { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Families/FamilyIdResponse.cs b/srcs/WingsAPI.Communication/Families/FamilyIdResponse.cs new file mode 100644 index 0000000..54d3511 --- /dev/null +++ b/srcs/WingsAPI.Communication/Families/FamilyIdResponse.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using ProtoBuf; +using WingsAPI.Data.Families; + +namespace WingsAPI.Communication.Families +{ + [ProtoContract] + public class FamilyIdResponse + { + [ProtoMember(1)] + public FamilyDTO Family { get; set; } + + [ProtoMember(2)] + public List Members { get; set; } + + [ProtoMember(3)] + public List Logs { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Families/FamilyInvitation.cs b/srcs/WingsAPI.Communication/Families/FamilyInvitation.cs new file mode 100644 index 0000000..c26df23 --- /dev/null +++ b/srcs/WingsAPI.Communication/Families/FamilyInvitation.cs @@ -0,0 +1,17 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.Families +{ + [ProtoContract] + public class FamilyInvitation + { + [ProtoMember(1)] + public long SenderId { get; set; } + + [ProtoMember(2)] + public long SenderFamilyId { get; set; } + + [ProtoMember(3)] + public long TargetId { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Families/FamilyInvitationContainsResponse.cs b/srcs/WingsAPI.Communication/Families/FamilyInvitationContainsResponse.cs new file mode 100644 index 0000000..2c6f4eb --- /dev/null +++ b/srcs/WingsAPI.Communication/Families/FamilyInvitationContainsResponse.cs @@ -0,0 +1,11 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.Families +{ + [ProtoContract] + public class FamilyInvitationContainsResponse + { + [ProtoMember(1)] + public bool IsContains { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Families/FamilyInvitationGetResponse.cs b/srcs/WingsAPI.Communication/Families/FamilyInvitationGetResponse.cs new file mode 100644 index 0000000..47ccc9c --- /dev/null +++ b/srcs/WingsAPI.Communication/Families/FamilyInvitationGetResponse.cs @@ -0,0 +1,11 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.Families +{ + [ProtoContract] + public class FamilyInvitationGetResponse + { + [ProtoMember(1)] + public FamilyInvitation Invitation { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Families/FamilyInvitationRemoveRequest.cs b/srcs/WingsAPI.Communication/Families/FamilyInvitationRemoveRequest.cs new file mode 100644 index 0000000..4f56598 --- /dev/null +++ b/srcs/WingsAPI.Communication/Families/FamilyInvitationRemoveRequest.cs @@ -0,0 +1,11 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.Families +{ + [ProtoContract] + public class FamilyInvitationRemoveRequest + { + [ProtoMember(1)] + public long SenderId { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Families/FamilyInvitationRequest.cs b/srcs/WingsAPI.Communication/Families/FamilyInvitationRequest.cs new file mode 100644 index 0000000..6c26559 --- /dev/null +++ b/srcs/WingsAPI.Communication/Families/FamilyInvitationRequest.cs @@ -0,0 +1,14 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.Families +{ + [ProtoContract] + public class FamilyInvitationRequest + { + [ProtoMember(1)] + public long SenderId { get; set; } + + [ProtoMember(2)] + public long TargetId { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Families/FamilyInvitationSaveRequest.cs b/srcs/WingsAPI.Communication/Families/FamilyInvitationSaveRequest.cs new file mode 100644 index 0000000..21a021c --- /dev/null +++ b/srcs/WingsAPI.Communication/Families/FamilyInvitationSaveRequest.cs @@ -0,0 +1,11 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.Families +{ + [ProtoContract] + public class FamilyInvitationSaveRequest + { + [ProtoMember(1)] + public FamilyInvitation Invitation { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Families/FamilyListMembersResponse.cs b/srcs/WingsAPI.Communication/Families/FamilyListMembersResponse.cs new file mode 100644 index 0000000..922a2a5 --- /dev/null +++ b/srcs/WingsAPI.Communication/Families/FamilyListMembersResponse.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using ProtoBuf; +using WingsAPI.Data.Families; + +namespace WingsAPI.Communication.Families +{ + [ProtoContract] + public class FamilyListMembersResponse + { + [ProtoMember(1)] + public List Members { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Families/FamilyMemberDisconnectedRequest.cs b/srcs/WingsAPI.Communication/Families/FamilyMemberDisconnectedRequest.cs new file mode 100644 index 0000000..82c3b31 --- /dev/null +++ b/srcs/WingsAPI.Communication/Families/FamilyMemberDisconnectedRequest.cs @@ -0,0 +1,15 @@ +using System; +using ProtoBuf; + +namespace WingsAPI.Communication.Families +{ + [ProtoContract] + public class FamilyMemberDisconnectedRequest + { + [ProtoMember(1)] + public long CharacterId { get; set; } + + [ProtoMember(2)] + public DateTime DisconnectionTime { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Families/FamilyMissionsResetMessage.cs b/srcs/WingsAPI.Communication/Families/FamilyMissionsResetMessage.cs new file mode 100644 index 0000000..655fa9c --- /dev/null +++ b/srcs/WingsAPI.Communication/Families/FamilyMissionsResetMessage.cs @@ -0,0 +1,10 @@ +using PhoenixLib.ServiceBus; +using PhoenixLib.ServiceBus.Routing; + +namespace WingsAPI.Communication.Families +{ + [MessageType("family.missions.reset")] + public class FamilyMissionsResetMessage : IMessage + { + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Families/FamilyRemoveMemberByCharIdRequest.cs b/srcs/WingsAPI.Communication/Families/FamilyRemoveMemberByCharIdRequest.cs new file mode 100644 index 0000000..fdf4092 --- /dev/null +++ b/srcs/WingsAPI.Communication/Families/FamilyRemoveMemberByCharIdRequest.cs @@ -0,0 +1,11 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.Families +{ + [ProtoContract] + public class FamilyRemoveMemberByCharIdRequest + { + [ProtoMember(1)] + public long CharacterId { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Families/FamilyRemoveMemberRequest.cs b/srcs/WingsAPI.Communication/Families/FamilyRemoveMemberRequest.cs new file mode 100644 index 0000000..eda7eaf --- /dev/null +++ b/srcs/WingsAPI.Communication/Families/FamilyRemoveMemberRequest.cs @@ -0,0 +1,14 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.Families +{ + [ProtoContract] + public class FamilyRemoveMemberRequest + { + [ProtoMember(1)] + public long CharacterId { get; set; } + + [ProtoMember(2)] + public long FamilyId { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Families/FamilySettingsRequest.cs b/srcs/WingsAPI.Communication/Families/FamilySettingsRequest.cs new file mode 100644 index 0000000..0f11c9f --- /dev/null +++ b/srcs/WingsAPI.Communication/Families/FamilySettingsRequest.cs @@ -0,0 +1,22 @@ +using ProtoBuf; +using WingsAPI.Packets.Enums.Families; +using WingsEmu.Packets.Enums.Families; + +namespace WingsAPI.Communication.Families +{ + [ProtoContract] + public class FamilySettingsRequest + { + [ProtoMember(1)] + public long FamilyId { get; set; } + + [ProtoMember(2)] + public FamilyAuthority Authority { get; set; } + + [ProtoMember(3)] + public FamilyActionType FamilyActionType { get; set; } + + [ProtoMember(4)] + public byte Value { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Families/FamilyUpgradeAddResponseType.cs b/srcs/WingsAPI.Communication/Families/FamilyUpgradeAddResponseType.cs new file mode 100644 index 0000000..bd69264 --- /dev/null +++ b/srcs/WingsAPI.Communication/Families/FamilyUpgradeAddResponseType.cs @@ -0,0 +1,11 @@ +namespace WingsAPI.Communication.Families +{ + public enum FamilyUpgradeAddResponseType + { + UNKNOWN_ERROR, + SUCCESS, + GENERIC_SERVER_ERROR, + MAINTENANCE_MODE, + UPGRADE_ALREADY_UNLOCKED + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Families/FamilyUpgradeRequest.cs b/srcs/WingsAPI.Communication/Families/FamilyUpgradeRequest.cs new file mode 100644 index 0000000..a577214 --- /dev/null +++ b/srcs/WingsAPI.Communication/Families/FamilyUpgradeRequest.cs @@ -0,0 +1,21 @@ +using ProtoBuf; +using WingsAPI.Data.Families; + +namespace WingsAPI.Communication.Families +{ + [ProtoContract] + public class FamilyUpgradeRequest + { + [ProtoMember(1)] + public long FamilyId { get; set; } + + [ProtoMember(2)] + public int UpgradeId { get; set; } + + [ProtoMember(3)] + public FamilyUpgradeType FamilyUpgradeType { get; set; } + + [ProtoMember(4)] + public short Value { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Families/FamilyUpgradeResponse.cs b/srcs/WingsAPI.Communication/Families/FamilyUpgradeResponse.cs new file mode 100644 index 0000000..f648abc --- /dev/null +++ b/srcs/WingsAPI.Communication/Families/FamilyUpgradeResponse.cs @@ -0,0 +1,11 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.Families +{ + [ProtoContract] + public class FamilyUpgradeResponse + { + [ProtoMember(1)] + public FamilyUpgradeAddResponseType ResponseType { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Families/IFamilyInvitationService.cs b/srcs/WingsAPI.Communication/Families/IFamilyInvitationService.cs new file mode 100644 index 0000000..f296018 --- /dev/null +++ b/srcs/WingsAPI.Communication/Families/IFamilyInvitationService.cs @@ -0,0 +1,21 @@ +using System.ServiceModel; +using System.Threading.Tasks; + +namespace WingsAPI.Communication.Families +{ + [ServiceContract] + public interface IFamilyInvitationService + { + [OperationContract] + ValueTask SaveFamilyInvitationAsync(FamilyInvitationSaveRequest request); + + [OperationContract] + ValueTask ContainsFamilyInvitationAsync(FamilyInvitationRequest request); + + [OperationContract] + ValueTask GetFamilyInvitationAsync(FamilyInvitationRequest request); + + [OperationContract] + ValueTask RemoveFamilyInvitationAsync(FamilyInvitationRemoveRequest request); + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Families/IFamilyService.cs b/srcs/WingsAPI.Communication/Families/IFamilyService.cs new file mode 100644 index 0000000..2d33360 --- /dev/null +++ b/srcs/WingsAPI.Communication/Families/IFamilyService.cs @@ -0,0 +1,65 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.ServiceModel; +using System.Threading.Tasks; + +namespace WingsAPI.Communication.Families +{ + [ServiceContract] + public interface IFamilyService + { + /// + /// + /// + /// + [OperationContract] + ValueTask CreateFamilyAsync(FamilyCreateRequest name); + + [OperationContract] + ValueTask DisbandFamilyAsync(FamilyDisbandRequest request); + + [OperationContract] + ValueTask ChangeAuthorityByIdAsync(FamilyChangeAuthorityRequest request); + + [OperationContract] + ValueTask ChangeFactionByIdAsync(FamilyChangeFactionRequest request); + + [OperationContract] + ValueTask ChangeTitleByIdAsync(FamilyChangeTitleRequest request); + + [OperationContract] + ValueTask TryAddFamilyUpgrade(FamilyUpgradeRequest request); + + [OperationContract] + ValueTask AddMemberToFamilyAsync(FamilyAddMemberRequest request); + + [OperationContract] + ValueTask MemberDisconnectedAsync(FamilyMemberDisconnectedRequest request); + + [OperationContract] + ValueTask RemoveMemberToFamilyAsync(FamilyRemoveMemberRequest request); + + [OperationContract] + ValueTask RemoveMemberByCharIdAsync(FamilyRemoveMemberByCharIdRequest request); + + [OperationContract] + ValueTask GetFamilyByIdAsync(FamilyIdRequest req); + + [OperationContract] + ValueTask GetFamilyMembersByFamilyId(FamilyIdRequest req); + + [OperationContract] + ValueTask GetMembershipByCharacterIdAsync(MembershipRequest req); + + [OperationContract] + ValueTask CanPerformTodayMessageAsync(MembershipTodayRequest req); + + [OperationContract] + ValueTask UpdateFamilySettingsAsync(FamilySettingsRequest request); + + [OperationContract] + ValueTask ResetFamilyMissions(); + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Families/MembershipRequest.cs b/srcs/WingsAPI.Communication/Families/MembershipRequest.cs new file mode 100644 index 0000000..1e2b4aa --- /dev/null +++ b/srcs/WingsAPI.Communication/Families/MembershipRequest.cs @@ -0,0 +1,11 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.Families +{ + [ProtoContract] + public class MembershipRequest + { + [ProtoMember(1)] + public long CharacterId { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Families/MembershipResponse.cs b/srcs/WingsAPI.Communication/Families/MembershipResponse.cs new file mode 100644 index 0000000..1e72b1c --- /dev/null +++ b/srcs/WingsAPI.Communication/Families/MembershipResponse.cs @@ -0,0 +1,12 @@ +using ProtoBuf; +using WingsAPI.Data.Families; + +namespace WingsAPI.Communication.Families +{ + [ProtoContract] + public class MembershipResponse + { + [ProtoMember(1)] + public FamilyMembershipDto Membership { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Families/MembershipTodayRequest.cs b/srcs/WingsAPI.Communication/Families/MembershipTodayRequest.cs new file mode 100644 index 0000000..1c98deb --- /dev/null +++ b/srcs/WingsAPI.Communication/Families/MembershipTodayRequest.cs @@ -0,0 +1,14 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.Families +{ + [ProtoContract] + public class MembershipTodayRequest + { + [ProtoMember(1)] + public long CharacterId { get; set; } + + [ProtoMember(2)] + public string CharacterName { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Families/MembershipTodayResponse.cs b/srcs/WingsAPI.Communication/Families/MembershipTodayResponse.cs new file mode 100644 index 0000000..dd5dccc --- /dev/null +++ b/srcs/WingsAPI.Communication/Families/MembershipTodayResponse.cs @@ -0,0 +1,11 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.Families +{ + [ProtoContract] + public class MembershipTodayResponse + { + [ProtoMember(1)] + public bool CanPerformAction { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Families/Warehouse/FamilyWarehouseAddItemRequest.cs b/srcs/WingsAPI.Communication/Families/Warehouse/FamilyWarehouseAddItemRequest.cs new file mode 100644 index 0000000..6ef73fc --- /dev/null +++ b/srcs/WingsAPI.Communication/Families/Warehouse/FamilyWarehouseAddItemRequest.cs @@ -0,0 +1,18 @@ +using ProtoBuf; +using WingsAPI.Data.Families; + +namespace WingsAPI.Communication.Families.Warehouse +{ + [ProtoContract] + public class FamilyWarehouseAddItemRequest + { + [ProtoMember(1)] + public FamilyWarehouseItemDto Item { get; set; } + + [ProtoMember(2)] + public long? CharacterId { get; set; } + + [ProtoMember(3)] + public string CharacterName { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Families/Warehouse/FamilyWarehouseAddItemResponse.cs b/srcs/WingsAPI.Communication/Families/Warehouse/FamilyWarehouseAddItemResponse.cs new file mode 100644 index 0000000..c760f1e --- /dev/null +++ b/srcs/WingsAPI.Communication/Families/Warehouse/FamilyWarehouseAddItemResponse.cs @@ -0,0 +1,15 @@ +using ProtoBuf; +using WingsAPI.Data.Families; + +namespace WingsAPI.Communication.Families.Warehouse +{ + [ProtoContract] + public class FamilyWarehouseAddItemResponse + { + [ProtoMember(1)] + public RpcResponseType ResponseType { get; set; } + + [ProtoMember(2)] + public FamilyWarehouseItemDto Item { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Families/Warehouse/FamilyWarehouseGetItemRequest.cs b/srcs/WingsAPI.Communication/Families/Warehouse/FamilyWarehouseGetItemRequest.cs new file mode 100644 index 0000000..7a206f5 --- /dev/null +++ b/srcs/WingsAPI.Communication/Families/Warehouse/FamilyWarehouseGetItemRequest.cs @@ -0,0 +1,17 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.Families.Warehouse +{ + [ProtoContract] + public class FamilyWarehouseGetItemRequest + { + [ProtoMember(1)] + public long FamilyId { get; set; } + + [ProtoMember(2)] + public long? CharacterId { get; set; } + + [ProtoMember(3)] + public short Slot { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Families/Warehouse/FamilyWarehouseGetItemResponse.cs b/srcs/WingsAPI.Communication/Families/Warehouse/FamilyWarehouseGetItemResponse.cs new file mode 100644 index 0000000..a102fbd --- /dev/null +++ b/srcs/WingsAPI.Communication/Families/Warehouse/FamilyWarehouseGetItemResponse.cs @@ -0,0 +1,15 @@ +using ProtoBuf; +using WingsAPI.Data.Families; + +namespace WingsAPI.Communication.Families.Warehouse +{ + [ProtoContract] + public class FamilyWarehouseGetItemResponse + { + [ProtoMember(1)] + public RpcResponseType ResponseType { get; set; } + + [ProtoMember(2)] + public FamilyWarehouseItemDto Item { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Families/Warehouse/FamilyWarehouseGetItemsRequest.cs b/srcs/WingsAPI.Communication/Families/Warehouse/FamilyWarehouseGetItemsRequest.cs new file mode 100644 index 0000000..79cc42f --- /dev/null +++ b/srcs/WingsAPI.Communication/Families/Warehouse/FamilyWarehouseGetItemsRequest.cs @@ -0,0 +1,14 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.Families.Warehouse +{ + [ProtoContract] + public class FamilyWarehouseGetItemsRequest + { + [ProtoMember(1)] + public long FamilyId { get; set; } + + [ProtoMember(2)] + public long? CharacterId { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Families/Warehouse/FamilyWarehouseGetItemsResponse.cs b/srcs/WingsAPI.Communication/Families/Warehouse/FamilyWarehouseGetItemsResponse.cs new file mode 100644 index 0000000..dc4665d --- /dev/null +++ b/srcs/WingsAPI.Communication/Families/Warehouse/FamilyWarehouseGetItemsResponse.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using ProtoBuf; +using WingsAPI.Data.Families; + +namespace WingsAPI.Communication.Families.Warehouse +{ + [ProtoContract] + public class FamilyWarehouseGetItemsResponse + { + [ProtoMember(1)] + public RpcResponseType ResponseType { get; set; } + + [ProtoMember(2)] + public IEnumerable Items { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Families/Warehouse/FamilyWarehouseGetLogsRequest.cs b/srcs/WingsAPI.Communication/Families/Warehouse/FamilyWarehouseGetLogsRequest.cs new file mode 100644 index 0000000..77fcaaa --- /dev/null +++ b/srcs/WingsAPI.Communication/Families/Warehouse/FamilyWarehouseGetLogsRequest.cs @@ -0,0 +1,14 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.Families.Warehouse +{ + [ProtoContract] + public class FamilyWarehouseGetLogsRequest + { + [ProtoMember(1)] + public long FamilyId { get; set; } + + [ProtoMember(2)] + public long? CharacterId { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Families/Warehouse/FamilyWarehouseGetLogsResponse.cs b/srcs/WingsAPI.Communication/Families/Warehouse/FamilyWarehouseGetLogsResponse.cs new file mode 100644 index 0000000..def4452 --- /dev/null +++ b/srcs/WingsAPI.Communication/Families/Warehouse/FamilyWarehouseGetLogsResponse.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using ProtoBuf; +using WingsAPI.Data.Families; + +namespace WingsAPI.Communication.Families.Warehouse +{ + [ProtoContract] + public class FamilyWarehouseGetLogsResponse + { + [ProtoMember(1)] + public RpcResponseType ResponseType { get; set; } + + [ProtoMember(2)] + public IList Logs { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Families/Warehouse/FamilyWarehouseMoveItemRequest.cs b/srcs/WingsAPI.Communication/Families/Warehouse/FamilyWarehouseMoveItemRequest.cs new file mode 100644 index 0000000..e30d776 --- /dev/null +++ b/srcs/WingsAPI.Communication/Families/Warehouse/FamilyWarehouseMoveItemRequest.cs @@ -0,0 +1,21 @@ +using ProtoBuf; +using WingsAPI.Data.Families; + +namespace WingsAPI.Communication.Families.Warehouse +{ + [ProtoContract] + public class FamilyWarehouseMoveItemRequest + { + [ProtoMember(1)] + public FamilyWarehouseItemDto WarehouseItemDtoToMove { get; set; } + + [ProtoMember(2)] + public long? CharacterId { get; set; } + + [ProtoMember(3)] + public int Amount { get; set; } + + [ProtoMember(4)] + public short NewSlot { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Families/Warehouse/FamilyWarehouseMoveItemResponse.cs b/srcs/WingsAPI.Communication/Families/Warehouse/FamilyWarehouseMoveItemResponse.cs new file mode 100644 index 0000000..6b5fa00 --- /dev/null +++ b/srcs/WingsAPI.Communication/Families/Warehouse/FamilyWarehouseMoveItemResponse.cs @@ -0,0 +1,18 @@ +using ProtoBuf; +using WingsAPI.Data.Families; + +namespace WingsAPI.Communication.Families.Warehouse +{ + [ProtoContract] + public class FamilyWarehouseMoveItemResponse + { + [ProtoMember(1)] + public RpcResponseType ResponseType { get; set; } + + [ProtoMember(2)] + public FamilyWarehouseItemDto OldItem { get; set; } + + [ProtoMember(3)] + public FamilyWarehouseItemDto NewItem { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Families/Warehouse/FamilyWarehouseWithdrawItemRequest.cs b/srcs/WingsAPI.Communication/Families/Warehouse/FamilyWarehouseWithdrawItemRequest.cs new file mode 100644 index 0000000..1ceef07 --- /dev/null +++ b/srcs/WingsAPI.Communication/Families/Warehouse/FamilyWarehouseWithdrawItemRequest.cs @@ -0,0 +1,21 @@ +using ProtoBuf; +using WingsAPI.Data.Families; + +namespace WingsAPI.Communication.Families.Warehouse +{ + [ProtoContract] + public class FamilyWarehouseWithdrawItemRequest + { + [ProtoMember(1)] + public FamilyWarehouseItemDto ItemToWithdraw { get; set; } + + [ProtoMember(2)] + public long? CharacterId { get; set; } + + [ProtoMember(3)] + public int Amount { get; set; } + + [ProtoMember(4)] + public string CharacterName { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Families/Warehouse/FamilyWarehouseWithdrawItemResponse.cs b/srcs/WingsAPI.Communication/Families/Warehouse/FamilyWarehouseWithdrawItemResponse.cs new file mode 100644 index 0000000..b96ba11 --- /dev/null +++ b/srcs/WingsAPI.Communication/Families/Warehouse/FamilyWarehouseWithdrawItemResponse.cs @@ -0,0 +1,19 @@ +using ProtoBuf; +using WingsAPI.Data.Families; +using WingsEmu.DTOs.Items; + +namespace WingsAPI.Communication.Families.Warehouse +{ + [ProtoContract] + public class FamilyWarehouseWithdrawItemResponse + { + [ProtoMember(1)] + public RpcResponseType ResponseType { get; set; } + + [ProtoMember(2)] + public FamilyWarehouseItemDto UpdatedItem { get; set; } + + [ProtoMember(3)] + public ItemInstanceDTO WithdrawnItem { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Families/Warehouse/IFamilyWarehouseService.cs b/srcs/WingsAPI.Communication/Families/Warehouse/IFamilyWarehouseService.cs new file mode 100644 index 0000000..355cca0 --- /dev/null +++ b/srcs/WingsAPI.Communication/Families/Warehouse/IFamilyWarehouseService.cs @@ -0,0 +1,27 @@ +using System.ServiceModel; +using System.Threading.Tasks; + +namespace WingsAPI.Communication.Families.Warehouse +{ + [ServiceContract] + public interface IFamilyWarehouseService + { + [OperationContract] + ValueTask GetLogs(FamilyWarehouseGetLogsRequest request); + + [OperationContract] + ValueTask GetItems(FamilyWarehouseGetItemsRequest request); + + [OperationContract] + ValueTask GetItem(FamilyWarehouseGetItemRequest request); + + [OperationContract] + ValueTask AddItem(FamilyWarehouseAddItemRequest request); + + [OperationContract] + ValueTask WithdrawItem(FamilyWarehouseWithdrawItemRequest request); + + [OperationContract] + ValueTask MoveItem(FamilyWarehouseMoveItemRequest request); + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/InstantBattle/InstantBattleStartMessage.cs b/srcs/WingsAPI.Communication/InstantBattle/InstantBattleStartMessage.cs new file mode 100644 index 0000000..2dea33f --- /dev/null +++ b/srcs/WingsAPI.Communication/InstantBattle/InstantBattleStartMessage.cs @@ -0,0 +1,15 @@ +// WingsEmu +// +// Developed by NosWings Team + +using PhoenixLib.ServiceBus; +using PhoenixLib.ServiceBus.Routing; + +namespace WingsAPI.Communication.InstantBattle +{ + [MessageType("game.instant-battle.start")] + public class InstantBattleStartMessage : IMessage + { + public bool HasNoDelay { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Mail/CreateMailBatchRequest.cs b/srcs/WingsAPI.Communication/Mail/CreateMailBatchRequest.cs new file mode 100644 index 0000000..167b152 --- /dev/null +++ b/srcs/WingsAPI.Communication/Mail/CreateMailBatchRequest.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using ProtoBuf; +using WingsEmu.DTOs.Mails; + +namespace WingsAPI.Communication.Mail +{ + [ProtoContract] + public class CreateMailBatchRequest + { + [ProtoMember(1)] + public List Mails { get; set; } + + [ProtoMember(2)] + public bool Bufferized { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Mail/CreateMailBatchResponse.cs b/srcs/WingsAPI.Communication/Mail/CreateMailBatchResponse.cs new file mode 100644 index 0000000..1dbb92e --- /dev/null +++ b/srcs/WingsAPI.Communication/Mail/CreateMailBatchResponse.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using ProtoBuf; +using WingsEmu.DTOs.Mails; + +namespace WingsAPI.Communication.Mail +{ + [ProtoContract] + public class CreateMailBatchResponse + { + [ProtoMember(1)] + public RpcResponseType Status { get; set; } + + [ProtoMember(2)] + public IEnumerable Mail { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Mail/CreateMailRequest.cs b/srcs/WingsAPI.Communication/Mail/CreateMailRequest.cs new file mode 100644 index 0000000..e1c4557 --- /dev/null +++ b/srcs/WingsAPI.Communication/Mail/CreateMailRequest.cs @@ -0,0 +1,22 @@ +using ProtoBuf; +using WingsEmu.DTOs.Items; +using WingsEmu.DTOs.Mails; + +namespace WingsAPI.Communication.Mail +{ + [ProtoContract] + public class CreateMailRequest + { + [ProtoMember(1)] + public string SenderName { get; set; } + + [ProtoMember(2)] + public long ReceiverId { get; set; } + + [ProtoMember(3)] + public MailGiftType MailGiftType { get; set; } + + [ProtoMember(4)] + public ItemInstanceDTO ItemInstance { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Mail/CreateMailResponse.cs b/srcs/WingsAPI.Communication/Mail/CreateMailResponse.cs new file mode 100644 index 0000000..2af7846 --- /dev/null +++ b/srcs/WingsAPI.Communication/Mail/CreateMailResponse.cs @@ -0,0 +1,15 @@ +using ProtoBuf; +using WingsEmu.DTOs.Mails; + +namespace WingsAPI.Communication.Mail +{ + [ProtoContract] + public class CreateMailResponse + { + [ProtoMember(1)] + public RpcResponseType Status { get; set; } + + [ProtoMember(2)] + public CharacterMailDto Mail { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Mail/CreateNoteRequest.cs b/srcs/WingsAPI.Communication/Mail/CreateNoteRequest.cs new file mode 100644 index 0000000..2ce4181 --- /dev/null +++ b/srcs/WingsAPI.Communication/Mail/CreateNoteRequest.cs @@ -0,0 +1,43 @@ +using ProtoBuf; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Character; + +namespace WingsAPI.Communication.Mail +{ + [ProtoContract] + public class CreateNoteRequest + { + [ProtoMember(1)] + public long SenderId { get; set; } + + [ProtoMember(2)] + public string SenderName { get; set; } + + [ProtoMember(3)] + public long ReceiverId { get; set; } + + [ProtoMember(4)] + public string ReceiverName { get; set; } + + [ProtoMember(5)] + public string Title { get; set; } + + [ProtoMember(6)] + public string Message { get; set; } + + [ProtoMember(7)] + public string EquipmentPackets { get; set; } + + [ProtoMember(8)] + public GenderType SenderGender { get; set; } + + [ProtoMember(9)] + public ClassType SenderClass { get; set; } + + [ProtoMember(10)] + public HairColorType SenderHairColor { get; set; } + + [ProtoMember(11)] + public HairStyleType SenderHairStyle { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Mail/CreateNoteResponse.cs b/srcs/WingsAPI.Communication/Mail/CreateNoteResponse.cs new file mode 100644 index 0000000..0ea8a1e --- /dev/null +++ b/srcs/WingsAPI.Communication/Mail/CreateNoteResponse.cs @@ -0,0 +1,18 @@ +using ProtoBuf; +using WingsEmu.DTOs.Mails; + +namespace WingsAPI.Communication.Mail +{ + [ProtoContract] + public class CreateNoteResponse + { + [ProtoMember(1)] + public CharacterNoteDto SenderNote { get; set; } + + [ProtoMember(2)] + public CharacterNoteDto ReceiverNote { get; set; } + + [ProtoMember(3)] + public RpcResponseType Status { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Mail/GetMailsRequest.cs b/srcs/WingsAPI.Communication/Mail/GetMailsRequest.cs new file mode 100644 index 0000000..eae064e --- /dev/null +++ b/srcs/WingsAPI.Communication/Mail/GetMailsRequest.cs @@ -0,0 +1,11 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.Mail +{ + [ProtoContract] + public class GetMailsRequest + { + [ProtoMember(1)] + public long CharacterId { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Mail/GetMailsResponse.cs b/srcs/WingsAPI.Communication/Mail/GetMailsResponse.cs new file mode 100644 index 0000000..ad562a3 --- /dev/null +++ b/srcs/WingsAPI.Communication/Mail/GetMailsResponse.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using ProtoBuf; +using WingsEmu.DTOs.Mails; + +namespace WingsAPI.Communication.Mail +{ + [ProtoContract] + public class GetMailsResponse + { + [ProtoMember(1)] + public IEnumerable CharacterMailsDto { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Mail/IMailService.cs b/srcs/WingsAPI.Communication/Mail/IMailService.cs new file mode 100644 index 0000000..2f3ab16 --- /dev/null +++ b/srcs/WingsAPI.Communication/Mail/IMailService.cs @@ -0,0 +1,21 @@ +using System.ServiceModel; +using System.Threading.Tasks; + +namespace WingsAPI.Communication.Mail +{ + [ServiceContract] + public interface IMailService + { + [OperationContract] + ValueTask CreateMailAsync(CreateMailRequest request); + + [OperationContract] + Task CreateMailBatchAsync(CreateMailBatchRequest request); + + [OperationContract] + ValueTask RemoveMailAsync(RemoveMailRequest request); + + [OperationContract] + ValueTask GetMailsByCharacterId(GetMailsRequest request); + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Mail/INoteService.cs b/srcs/WingsAPI.Communication/Mail/INoteService.cs new file mode 100644 index 0000000..6724e78 --- /dev/null +++ b/srcs/WingsAPI.Communication/Mail/INoteService.cs @@ -0,0 +1,18 @@ +using System.ServiceModel; +using System.Threading.Tasks; + +namespace WingsAPI.Communication.Mail +{ + [ServiceContract] + public interface INoteService + { + [OperationContract] + ValueTask CreateNoteAsync(CreateNoteRequest request); + + [OperationContract] + ValueTask OpenNoteAsync(OpenNoteRequest request); + + [OperationContract] + ValueTask RemoveNoteAsync(RemoveNoteRequest request); + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Mail/OpenNoteRequest.cs b/srcs/WingsAPI.Communication/Mail/OpenNoteRequest.cs new file mode 100644 index 0000000..58d9463 --- /dev/null +++ b/srcs/WingsAPI.Communication/Mail/OpenNoteRequest.cs @@ -0,0 +1,11 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.Mail +{ + [ProtoContract] + public class OpenNoteRequest + { + [ProtoMember(1)] + public long NoteId { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Mail/RemoveMailRequest.cs b/srcs/WingsAPI.Communication/Mail/RemoveMailRequest.cs new file mode 100644 index 0000000..6ac6926 --- /dev/null +++ b/srcs/WingsAPI.Communication/Mail/RemoveMailRequest.cs @@ -0,0 +1,14 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.Mail +{ + [ProtoContract] + public class RemoveMailRequest + { + [ProtoMember(1)] + public long CharacterId { get; set; } + + [ProtoMember(2)] + public long MailId { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Mail/RemoveNoteRequest.cs b/srcs/WingsAPI.Communication/Mail/RemoveNoteRequest.cs new file mode 100644 index 0000000..a539247 --- /dev/null +++ b/srcs/WingsAPI.Communication/Mail/RemoveNoteRequest.cs @@ -0,0 +1,11 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.Mail +{ + [ProtoContract] + public class RemoveNoteRequest + { + [ProtoMember(1)] + public long NoteId { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Miniland/MinigameRefreshProductionPointsMessage.cs b/srcs/WingsAPI.Communication/Miniland/MinigameRefreshProductionPointsMessage.cs new file mode 100644 index 0000000..6e2238f --- /dev/null +++ b/srcs/WingsAPI.Communication/Miniland/MinigameRefreshProductionPointsMessage.cs @@ -0,0 +1,15 @@ +// WingsEmu +// +// Developed by NosWings Team + +using PhoenixLib.ServiceBus; +using PhoenixLib.ServiceBus.Routing; + +namespace WingsAPI.Communication.Miniland +{ + [MessageType("minigame.refresh-production.daily")] + public class MinigameRefreshProductionPointsMessage : IMessage + { + public bool Force { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Player/ClusterCharacterByChannelIdRequest.cs b/srcs/WingsAPI.Communication/Player/ClusterCharacterByChannelIdRequest.cs new file mode 100644 index 0000000..79ea1b8 --- /dev/null +++ b/srcs/WingsAPI.Communication/Player/ClusterCharacterByChannelIdRequest.cs @@ -0,0 +1,11 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.Player +{ + [ProtoContract] + public class ClusterCharacterByChannelIdRequest + { + [ProtoMember(1)] + public byte ChannelId { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Player/ClusterCharacterByIdRequest.cs b/srcs/WingsAPI.Communication/Player/ClusterCharacterByIdRequest.cs new file mode 100644 index 0000000..c6372f8 --- /dev/null +++ b/srcs/WingsAPI.Communication/Player/ClusterCharacterByIdRequest.cs @@ -0,0 +1,11 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.Player +{ + [ProtoContract] + public class ClusterCharacterByIdRequest + { + [ProtoMember(1)] + public long CharacterId { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Player/ClusterCharacterByNameRequest.cs b/srcs/WingsAPI.Communication/Player/ClusterCharacterByNameRequest.cs new file mode 100644 index 0000000..40b1b30 --- /dev/null +++ b/srcs/WingsAPI.Communication/Player/ClusterCharacterByNameRequest.cs @@ -0,0 +1,11 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.Player +{ + [ProtoContract] + public class ClusterCharacterByNameRequest + { + [ProtoMember(1)] + public string CharacterName { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Player/ClusterCharacterGetMultipleResponse.cs b/srcs/WingsAPI.Communication/Player/ClusterCharacterGetMultipleResponse.cs new file mode 100644 index 0000000..90dd7f4 --- /dev/null +++ b/srcs/WingsAPI.Communication/Player/ClusterCharacterGetMultipleResponse.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using ProtoBuf; + +namespace WingsAPI.Communication.Player +{ + [ProtoContract] + public class ClusterCharacterGetMultipleResponse + { + [ProtoMember(1)] + public RpcResponseType ResponseType { get; init; } + + [ProtoMember(2)] + public IReadOnlyList ClusterCharacterInfo { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Player/ClusterCharacterGetSortedResponse.cs b/srcs/WingsAPI.Communication/Player/ClusterCharacterGetSortedResponse.cs new file mode 100644 index 0000000..c4ce922 --- /dev/null +++ b/srcs/WingsAPI.Communication/Player/ClusterCharacterGetSortedResponse.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using ProtoBuf; + +namespace WingsAPI.Communication.Player +{ + [ProtoContract] + public class ClusterCharacterGetSortedResponse + { + [ProtoMember(1)] + public RpcResponseType ResponseType { get; init; } + + [ProtoMember(2)] + public IReadOnlyCollection>> CharactersByChannel { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Player/ClusterCharacterInfo.cs b/srcs/WingsAPI.Communication/Player/ClusterCharacterInfo.cs new file mode 100644 index 0000000..f0e4798 --- /dev/null +++ b/srcs/WingsAPI.Communication/Player/ClusterCharacterInfo.cs @@ -0,0 +1,40 @@ +// WingsEmu +// +// Developed by NosWings Team + +using ProtoBuf; +using WingsEmu.Packets.Enums.Character; + +namespace WingsAPI.Communication.Player +{ + [ProtoContract] + public class ClusterCharacterInfo + { + [ProtoMember(1)] + public long Id { get; set; } + + [ProtoMember(2)] + public string Name { get; set; } + + [ProtoMember(3)] + public GenderType Gender { get; set; } + + [ProtoMember(4)] + public ClassType Class { get; set; } + + [ProtoMember(5)] + public byte Level { get; set; } + + [ProtoMember(6)] + public byte HeroLevel { get; set; } + + [ProtoMember(7)] + public int? MorphId { get; set; } + + [ProtoMember(8)] + public byte? ChannelId { get; set; } + + [ProtoMember(9)] + public string HardwareId { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Player/ClusterCharacterResponse.cs b/srcs/WingsAPI.Communication/Player/ClusterCharacterResponse.cs new file mode 100644 index 0000000..33234b4 --- /dev/null +++ b/srcs/WingsAPI.Communication/Player/ClusterCharacterResponse.cs @@ -0,0 +1,14 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.Player +{ + [ProtoContract] + public class ClusterCharacterResponse + { + [ProtoMember(1)] + public RpcResponseType ResponseType { get; init; } + + [ProtoMember(2)] + public ClusterCharacterInfo ClusterCharacterInfo { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Player/IClusterCharacterService.cs b/srcs/WingsAPI.Communication/Player/IClusterCharacterService.cs new file mode 100644 index 0000000..b9e783e --- /dev/null +++ b/srcs/WingsAPI.Communication/Player/IClusterCharacterService.cs @@ -0,0 +1,24 @@ +using System.ServiceModel; +using System.Threading.Tasks; + +namespace WingsAPI.Communication.Player +{ + [ServiceContract] + public interface IClusterCharacterService + { + [OperationContract] + ValueTask GetCharacterById(ClusterCharacterByIdRequest request); + + [OperationContract] + ValueTask GetCharacterByName(ClusterCharacterByNameRequest request); + + [OperationContract] + ValueTask GetCharactersByChannelId(ClusterCharacterByChannelIdRequest request); + + [OperationContract] + ValueTask GetCharactersSortedByChannel(EmptyRpcRequest request); + + [OperationContract] + ValueTask GetAllCharacters(EmptyRpcRequest request); + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Player/RankingRefreshMessage.cs b/srcs/WingsAPI.Communication/Player/RankingRefreshMessage.cs new file mode 100644 index 0000000..ad16127 --- /dev/null +++ b/srcs/WingsAPI.Communication/Player/RankingRefreshMessage.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using PhoenixLib.ServiceBus; +using PhoenixLib.ServiceBus.Routing; +using WingsAPI.Data.Character; + +namespace WingsAPI.Communication.Player +{ + [MessageType("ranking.refresh")] + public class RankingRefreshMessage : IMessage + { + public IReadOnlyList TopReputation { get; init; } + public IReadOnlyList TopCompliment { get; init; } + public IReadOnlyList TopPoints { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Player/SpecialistPointsRefreshMessage.cs b/srcs/WingsAPI.Communication/Player/SpecialistPointsRefreshMessage.cs new file mode 100644 index 0000000..d28659f --- /dev/null +++ b/srcs/WingsAPI.Communication/Player/SpecialistPointsRefreshMessage.cs @@ -0,0 +1,11 @@ +using PhoenixLib.ServiceBus; +using PhoenixLib.ServiceBus.Routing; + +namespace WingsAPI.Communication.Player +{ + [MessageType("specialist.points.refresh")] + public class SpecialistPointsRefreshMessage : IMessage + { + public bool Force { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Punishment/PlayerKickMessage.cs b/srcs/WingsAPI.Communication/Punishment/PlayerKickMessage.cs new file mode 100644 index 0000000..939ad3f --- /dev/null +++ b/srcs/WingsAPI.Communication/Punishment/PlayerKickMessage.cs @@ -0,0 +1,12 @@ +using PhoenixLib.ServiceBus; +using PhoenixLib.ServiceBus.Routing; + +namespace WingsAPI.Communication.Punishment +{ + [MessageType("player.kick")] + public class PlayerKickMessage : IMessage + { + public long? PlayerId { get; init; } + public string PlayerName { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Quests/QuestDailyRefreshMessage.cs b/srcs/WingsAPI.Communication/Quests/QuestDailyRefreshMessage.cs new file mode 100644 index 0000000..6b02e82 --- /dev/null +++ b/srcs/WingsAPI.Communication/Quests/QuestDailyRefreshMessage.cs @@ -0,0 +1,11 @@ +using PhoenixLib.ServiceBus; +using PhoenixLib.ServiceBus.Routing; + +namespace WingsAPI.Communication.Quests +{ + [MessageType("quest.refresh.daily")] + public class QuestDailyRefreshMessage : IMessage + { + public bool Force { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Raid/RaidRestrictionRefreshMessage.cs b/srcs/WingsAPI.Communication/Raid/RaidRestrictionRefreshMessage.cs new file mode 100644 index 0000000..1d325e2 --- /dev/null +++ b/srcs/WingsAPI.Communication/Raid/RaidRestrictionRefreshMessage.cs @@ -0,0 +1,10 @@ +using PhoenixLib.ServiceBus; +using PhoenixLib.ServiceBus.Routing; + +namespace WingsAPI.Communication.Raid +{ + [MessageType("raid.restriction-refresh")] + public class RaidRestrictionRefreshMessage : IMessage + { + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/RainbowBattle/RainbowBattleLeaverBusterResetMessage.cs b/srcs/WingsAPI.Communication/RainbowBattle/RainbowBattleLeaverBusterResetMessage.cs new file mode 100644 index 0000000..47d7277 --- /dev/null +++ b/srcs/WingsAPI.Communication/RainbowBattle/RainbowBattleLeaverBusterResetMessage.cs @@ -0,0 +1,11 @@ +using PhoenixLib.ServiceBus; +using PhoenixLib.ServiceBus.Routing; + +namespace WingsAPI.Communication.RainbowBattle +{ + [MessageType("rainbow-battle.leaver-buster.reset")] + public class RainbowBattleLeaverBusterResetMessage : IMessage + { + public bool Force { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/RainbowBattle/RainbowBattleStartMessage.cs b/srcs/WingsAPI.Communication/RainbowBattle/RainbowBattleStartMessage.cs new file mode 100644 index 0000000..6ce39d0 --- /dev/null +++ b/srcs/WingsAPI.Communication/RainbowBattle/RainbowBattleStartMessage.cs @@ -0,0 +1,10 @@ +using PhoenixLib.ServiceBus; +using PhoenixLib.ServiceBus.Routing; + +namespace WingsAPI.Communication.RainbowBattle +{ + [MessageType("rainbow-battle.start")] + public class RainbowBattleStartMessage : IMessage + { + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Relation/IRelationService.cs b/srcs/WingsAPI.Communication/Relation/IRelationService.cs new file mode 100644 index 0000000..9fdfff5 --- /dev/null +++ b/srcs/WingsAPI.Communication/Relation/IRelationService.cs @@ -0,0 +1,18 @@ +using System.ServiceModel; +using System.Threading.Tasks; + +namespace WingsAPI.Communication.Relation +{ + [ServiceContract] + public interface IRelationService + { + [OperationContract] + Task AddRelationAsync(RelationAddRequest request); + + [OperationContract] + Task GetRelationsByIdAsync(RelationGetAllRequest request); + + [OperationContract] + Task RemoveRelationAsync(RelationRemoveRequest request); + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Relation/RelationAddRequest.cs b/srcs/WingsAPI.Communication/Relation/RelationAddRequest.cs new file mode 100644 index 0000000..d9580d8 --- /dev/null +++ b/srcs/WingsAPI.Communication/Relation/RelationAddRequest.cs @@ -0,0 +1,21 @@ +using ProtoBuf; +using WingsEmu.Packets.Enums.Relations; + +namespace WingsAPI.Communication.Relation +{ + [ProtoContract] + public class RelationAddRequest + { + [ProtoMember(1)] + public long CharacterId { get; set; } + + [ProtoMember(2)] + public long TargetId { get; set; } + + [ProtoMember(3)] + public CharacterRelationType RelationType { get; set; } + + [ProtoMember(5)] + public string CharacterName { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Relation/RelationAddResponse.cs b/srcs/WingsAPI.Communication/Relation/RelationAddResponse.cs new file mode 100644 index 0000000..e38be68 --- /dev/null +++ b/srcs/WingsAPI.Communication/Relation/RelationAddResponse.cs @@ -0,0 +1,18 @@ +using ProtoBuf; +using WingsEmu.DTOs.Relations; + +namespace WingsAPI.Communication.Relation +{ + [ProtoContract] + public class RelationAddResponse + { + [ProtoMember(1)] + public CharacterRelationDTO SenderRelation { get; set; } + + [ProtoMember(2)] + public CharacterRelationDTO TargetRelation { get; set; } + + [ProtoMember(3)] + public RpcResponseType ResponseType { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Relation/RelationGetAllRequest.cs b/srcs/WingsAPI.Communication/Relation/RelationGetAllRequest.cs new file mode 100644 index 0000000..1cf35f2 --- /dev/null +++ b/srcs/WingsAPI.Communication/Relation/RelationGetAllRequest.cs @@ -0,0 +1,11 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.Relation +{ + [ProtoContract] + public class RelationGetAllRequest + { + [ProtoMember(1)] + public long CharacterId { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Relation/RelationGetAllResponse.cs b/srcs/WingsAPI.Communication/Relation/RelationGetAllResponse.cs new file mode 100644 index 0000000..6c3b1d0 --- /dev/null +++ b/srcs/WingsAPI.Communication/Relation/RelationGetAllResponse.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using ProtoBuf; +using WingsEmu.DTOs.Relations; + +namespace WingsAPI.Communication.Relation +{ + [ProtoContract] + public class RelationGetAllResponse + { + [ProtoMember(1)] + public RpcResponseType ResponseType { get; init; } + + [ProtoMember(2)] + public IReadOnlyList CharacterRelationDtos { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Relation/RelationRemoveRequest.cs b/srcs/WingsAPI.Communication/Relation/RelationRemoveRequest.cs new file mode 100644 index 0000000..3ff5fa8 --- /dev/null +++ b/srcs/WingsAPI.Communication/Relation/RelationRemoveRequest.cs @@ -0,0 +1,18 @@ +using ProtoBuf; +using WingsEmu.Packets.Enums.Relations; + +namespace WingsAPI.Communication.Relation +{ + [ProtoContract] + public class RelationRemoveRequest + { + [ProtoMember(1)] + public long CharacterId { get; set; } + + [ProtoMember(2)] + public long TargetId { get; set; } + + [ProtoMember(3)] + public CharacterRelationType RelationType { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/RpcResponseType.cs b/srcs/WingsAPI.Communication/RpcResponseType.cs new file mode 100644 index 0000000..0ec4a81 --- /dev/null +++ b/srcs/WingsAPI.Communication/RpcResponseType.cs @@ -0,0 +1,13 @@ +using ProtoBuf; + +namespace WingsAPI.Communication +{ + [ProtoContract] + public enum RpcResponseType + { + UNKNOWN_ERROR, + SUCCESS, + GENERIC_SERVER_ERROR, + MAINTENANCE_MODE + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/ServerApi/IServerApiService.cs b/srcs/WingsAPI.Communication/ServerApi/IServerApiService.cs new file mode 100644 index 0000000..24f2de2 --- /dev/null +++ b/srcs/WingsAPI.Communication/ServerApi/IServerApiService.cs @@ -0,0 +1,40 @@ +using System.ServiceModel; +using System.Threading.Tasks; +using WingsAPI.Communication.ServerApi.Protocol; + +namespace WingsAPI.Communication.ServerApi +{ + [ServiceContract] + public interface IServerApiService + { + [OperationContract] + ValueTask IsMasterOnline(EmptyRpcRequest request); + + /* + * World + */ + [OperationContract] + ValueTask RegisterWorldServer(RegisterWorldServerRequest request); + + [OperationContract] + ValueTask PulseWorldServer(PulseWorldServerRequest request); + + [OperationContract] + ValueTask UnregisterWorldServer(UnregisterWorldServerRequest request); + + [OperationContract] + ValueTask SetWorldServerVisibility(SetWorldServerVisibilityRequest request); + + [OperationContract] + ValueTask RetrieveRegisteredWorldServers(RetrieveRegisteredWorldServersRequest request); + + [OperationContract] + Task RetrieveAllGameServers(EmptyRpcRequest request); + + [OperationContract] + ValueTask GetChannelInfo(GetChannelInfoRequest request); + + [OperationContract] + ValueTask GetAct4ChannelInfo(GetAct4ChannelInfoRequest request); + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/ServerApi/Protocol/GameChannelType.cs b/srcs/WingsAPI.Communication/ServerApi/Protocol/GameChannelType.cs new file mode 100644 index 0000000..3476e5b --- /dev/null +++ b/srcs/WingsAPI.Communication/ServerApi/Protocol/GameChannelType.cs @@ -0,0 +1,8 @@ +namespace WingsAPI.Communication.ServerApi.Protocol +{ + public enum GameChannelType + { + PVE_NORMAL, + ACT_4 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/ServerApi/Protocol/GetAct4ChannelInfoRequest.cs b/srcs/WingsAPI.Communication/ServerApi/Protocol/GetAct4ChannelInfoRequest.cs new file mode 100644 index 0000000..55eef21 --- /dev/null +++ b/srcs/WingsAPI.Communication/ServerApi/Protocol/GetAct4ChannelInfoRequest.cs @@ -0,0 +1,11 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.ServerApi.Protocol +{ + [ProtoContract] + public class GetAct4ChannelInfoRequest + { + [ProtoMember(1)] + public string WorldGroup { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/ServerApi/Protocol/GetChannelInfoRequest.cs b/srcs/WingsAPI.Communication/ServerApi/Protocol/GetChannelInfoRequest.cs new file mode 100644 index 0000000..1b2228f --- /dev/null +++ b/srcs/WingsAPI.Communication/ServerApi/Protocol/GetChannelInfoRequest.cs @@ -0,0 +1,14 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.ServerApi.Protocol +{ + [ProtoContract] + public class GetChannelInfoRequest + { + [ProtoMember(1)] + public string WorldGroup { get; init; } + + [ProtoMember(2)] + public int ChannelId { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/ServerApi/Protocol/GetChannelInfoResponse.cs b/srcs/WingsAPI.Communication/ServerApi/Protocol/GetChannelInfoResponse.cs new file mode 100644 index 0000000..ff87d64 --- /dev/null +++ b/srcs/WingsAPI.Communication/ServerApi/Protocol/GetChannelInfoResponse.cs @@ -0,0 +1,14 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.ServerApi.Protocol +{ + [ProtoContract] + public class GetChannelInfoResponse + { + [ProtoMember(1)] + public RpcResponseType ResponseType { get; init; } + + [ProtoMember(2)] + public SerializableGameServer GameServer { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/ServerApi/Protocol/PulseWorldServerRequest.cs b/srcs/WingsAPI.Communication/ServerApi/Protocol/PulseWorldServerRequest.cs new file mode 100644 index 0000000..5289a9c --- /dev/null +++ b/srcs/WingsAPI.Communication/ServerApi/Protocol/PulseWorldServerRequest.cs @@ -0,0 +1,18 @@ +using System; +using ProtoBuf; + +namespace WingsAPI.Communication.ServerApi.Protocol +{ + [ProtoContract] + public class PulseWorldServerRequest + { + [ProtoMember(1)] + public int ChannelId { get; init; } + + [ProtoMember(2)] + public int SessionsCount { get; init; } + + [ProtoMember(3)] + public DateTime StartDate { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/ServerApi/Protocol/RegisterLoginResponse.cs b/srcs/WingsAPI.Communication/ServerApi/Protocol/RegisterLoginResponse.cs new file mode 100644 index 0000000..9be23c7 --- /dev/null +++ b/srcs/WingsAPI.Communication/ServerApi/Protocol/RegisterLoginResponse.cs @@ -0,0 +1,16 @@ +using System; + +namespace WingsAPI.Communication.ServerApi.Protocol +{ + public class RegisterLoginResponse + { + public enum ResponseType + { + Success, + Fail + } + + public ResponseType Type { get; set; } + public Guid Id { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/ServerApi/Protocol/RegisterWorldRequest.cs b/srcs/WingsAPI.Communication/ServerApi/Protocol/RegisterWorldRequest.cs new file mode 100644 index 0000000..9d2c4d9 --- /dev/null +++ b/srcs/WingsAPI.Communication/ServerApi/Protocol/RegisterWorldRequest.cs @@ -0,0 +1,6 @@ +namespace WingsAPI.Communication.ServerApi.Protocol +{ + public class RegisterWorldRequest + { + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/ServerApi/Protocol/RegisterWorldResponse.cs b/srcs/WingsAPI.Communication/ServerApi/Protocol/RegisterWorldResponse.cs new file mode 100644 index 0000000..ab2174e --- /dev/null +++ b/srcs/WingsAPI.Communication/ServerApi/Protocol/RegisterWorldResponse.cs @@ -0,0 +1,6 @@ +namespace WingsAPI.Communication.ServerApi.Protocol +{ + public class RegisterWorldResponse + { + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/ServerApi/Protocol/RegisterWorldServerRequest.cs b/srcs/WingsAPI.Communication/ServerApi/Protocol/RegisterWorldServerRequest.cs new file mode 100644 index 0000000..91cd1ac --- /dev/null +++ b/srcs/WingsAPI.Communication/ServerApi/Protocol/RegisterWorldServerRequest.cs @@ -0,0 +1,11 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.ServerApi.Protocol +{ + [ProtoContract] + public class RegisterWorldServerRequest + { + [ProtoMember(1)] + public SerializableGameServer GameServer { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/ServerApi/Protocol/RetrieveRegisteredWorldServersRequest.cs b/srcs/WingsAPI.Communication/ServerApi/Protocol/RetrieveRegisteredWorldServersRequest.cs new file mode 100644 index 0000000..7b29f42 --- /dev/null +++ b/srcs/WingsAPI.Communication/ServerApi/Protocol/RetrieveRegisteredWorldServersRequest.cs @@ -0,0 +1,12 @@ +using ProtoBuf; +using WingsEmu.DTOs.Account; + +namespace WingsAPI.Communication.ServerApi.Protocol +{ + [ProtoContract] + public class RetrieveRegisteredWorldServersRequest + { + [ProtoMember(1)] + public AuthorityType RequesterAuthority { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/ServerApi/Protocol/RetrieveRegisteredWorldServersResponse.cs b/srcs/WingsAPI.Communication/ServerApi/Protocol/RetrieveRegisteredWorldServersResponse.cs new file mode 100644 index 0000000..21991bb --- /dev/null +++ b/srcs/WingsAPI.Communication/ServerApi/Protocol/RetrieveRegisteredWorldServersResponse.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using ProtoBuf; + +namespace WingsAPI.Communication.ServerApi.Protocol +{ + [ProtoContract] + public class RetrieveRegisteredWorldServersResponse + { + [ProtoMember(1)] + public List WorldServers { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/ServerApi/Protocol/SerializableWorldServer.cs b/srcs/WingsAPI.Communication/ServerApi/Protocol/SerializableWorldServer.cs new file mode 100644 index 0000000..0ac38bc --- /dev/null +++ b/srcs/WingsAPI.Communication/ServerApi/Protocol/SerializableWorldServer.cs @@ -0,0 +1,41 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using ProtoBuf; +using WingsEmu.DTOs.Account; + +namespace WingsAPI.Communication.ServerApi.Protocol +{ + [ProtoContract] + public class SerializableGameServer + { + [ProtoMember(1)] + public GameChannelType ChannelType { get; set; } + + [ProtoMember(2)] + public string WorldGroup { get; set; } + + [ProtoMember(3)] + public int ChannelId { get; set; } + + [ProtoMember(4)] + public int AccountLimit { get; set; } + + [ProtoMember(5)] + public string EndPointIp { get; set; } + + [ProtoMember(6)] + public int EndPointPort { get; set; } + + [ProtoMember(7)] + public int SessionCount { get; set; } + + [ProtoMember(8)] + public AuthorityType Authority { get; set; } + + [ProtoMember(9)] + public DateTime RegistrationDate { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/ServerApi/Protocol/SetMaintenanceRequest.cs b/srcs/WingsAPI.Communication/ServerApi/Protocol/SetMaintenanceRequest.cs new file mode 100644 index 0000000..79980af --- /dev/null +++ b/srcs/WingsAPI.Communication/ServerApi/Protocol/SetMaintenanceRequest.cs @@ -0,0 +1,11 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.ServerApi.Protocol +{ + [ProtoContract] + public class SetMaintenanceRequest + { + [ProtoMember(1)] + public bool Maintenance { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/ServerApi/Protocol/SetWorldServerVisibilityRequest.cs b/srcs/WingsAPI.Communication/ServerApi/Protocol/SetWorldServerVisibilityRequest.cs new file mode 100644 index 0000000..44a1c59 --- /dev/null +++ b/srcs/WingsAPI.Communication/ServerApi/Protocol/SetWorldServerVisibilityRequest.cs @@ -0,0 +1,21 @@ +using ProtoBuf; +using WingsEmu.DTOs.Account; + +namespace WingsAPI.Communication.ServerApi.Protocol +{ + [ProtoContract] + public class SetWorldServerVisibilityRequest + { + [ProtoMember(1)] + public int ChannelId { get; init; } + + [ProtoMember(2)] + public string WorldGroup { get; init; } + + /// + /// Being able to see this server requires this AuthorityType or higher + /// + [ProtoMember(3)] + public AuthorityType AuthorityRequired { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/ServerApi/Protocol/ShutdownRequest.cs b/srcs/WingsAPI.Communication/ServerApi/Protocol/ShutdownRequest.cs new file mode 100644 index 0000000..6ff3b7e --- /dev/null +++ b/srcs/WingsAPI.Communication/ServerApi/Protocol/ShutdownRequest.cs @@ -0,0 +1,11 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.ServerApi.Protocol +{ + [ProtoContract] + public class ShutdownRequest + { + [ProtoMember(1)] + public string WorldGroup { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/ServerApi/Protocol/UnregisterWorldServerRequest.cs b/srcs/WingsAPI.Communication/ServerApi/Protocol/UnregisterWorldServerRequest.cs new file mode 100644 index 0000000..cb1aba3 --- /dev/null +++ b/srcs/WingsAPI.Communication/ServerApi/Protocol/UnregisterWorldServerRequest.cs @@ -0,0 +1,11 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.ServerApi.Protocol +{ + [ProtoContract] + public class UnregisterWorldServerRequest + { + [ProtoMember(1)] + public int ChannelId { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/ServerApi/WorldServerShutdownMessage.cs b/srcs/WingsAPI.Communication/ServerApi/WorldServerShutdownMessage.cs new file mode 100644 index 0000000..59dc057 --- /dev/null +++ b/srcs/WingsAPI.Communication/ServerApi/WorldServerShutdownMessage.cs @@ -0,0 +1,11 @@ +using PhoenixLib.ServiceBus; +using PhoenixLib.ServiceBus.Routing; + +namespace WingsAPI.Communication.ServerApi +{ + [MessageType("worldserver.shutdown")] + public class WorldServerShutdownMessage : IMessage + { + public int ChannelId { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Services/IClusterStatusService.cs b/srcs/WingsAPI.Communication/Services/IClusterStatusService.cs new file mode 100644 index 0000000..f2d50c7 --- /dev/null +++ b/srcs/WingsAPI.Communication/Services/IClusterStatusService.cs @@ -0,0 +1,35 @@ +using System.ServiceModel; +using System.Threading.Tasks; +using WingsAPI.Communication.Services.Requests; +using WingsAPI.Communication.Services.Responses; + +namespace WingsAPI.Communication.Services +{ + [ServiceContract] + public interface IClusterStatusService + { + [OperationContract] + Task GetAllServicesStatus(EmptyRpcRequest req); + + [OperationContract] + Task GetServiceStatusByNameAsync(ServiceBasicRequest req); + + [OperationContract] + Task EnableMaintenanceMode(ServiceBasicRequest req); + + [OperationContract] + Task DisableMaintenanceMode(ServiceBasicRequest req); + + [OperationContract] + Task ScheduleGeneralMaintenance(ServiceScheduleGeneralMaintenanceRequest maintenanceRequest); + + [OperationContract] + Task UnscheduleGeneralMaintenance(EmptyRpcRequest emptyRpcRequest); + + [OperationContract] + Task ExecuteGeneralEmergencyMaintenance(ServiceExecuteGeneralEmergencyMaintenanceRequest shutdownRequest); + + [OperationContract] + Task LiftGeneralMaintenance(EmptyRpcRequest shutdownRequest); + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Services/Messages/ServiceDownMessage.cs b/srcs/WingsAPI.Communication/Services/Messages/ServiceDownMessage.cs new file mode 100644 index 0000000..0a3dfe2 --- /dev/null +++ b/srcs/WingsAPI.Communication/Services/Messages/ServiceDownMessage.cs @@ -0,0 +1,13 @@ +using System; +using PhoenixLib.ServiceBus; +using PhoenixLib.ServiceBus.Routing; + +namespace WingsAPI.Communication.Services.Messages +{ + [MessageType("service.status.down")] + public class ServiceDownMessage : IMessage + { + public string ServiceName { get; init; } + public DateTime LastUpdate { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Services/Messages/ServiceFlushAllMessage.cs b/srcs/WingsAPI.Communication/Services/Messages/ServiceFlushAllMessage.cs new file mode 100644 index 0000000..0931ed0 --- /dev/null +++ b/srcs/WingsAPI.Communication/Services/Messages/ServiceFlushAllMessage.cs @@ -0,0 +1,13 @@ +using PhoenixLib.ServiceBus; +using PhoenixLib.ServiceBus.Routing; + +namespace WingsAPI.Communication.Services.Messages +{ + [MessageType("service.order.flushall")] + public class ServiceFlushAllMessage : IMessage + { + public bool IsGlobal { get; init; } + + public string TargetedService { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Services/Messages/ServiceKickAllMessage.cs b/srcs/WingsAPI.Communication/Services/Messages/ServiceKickAllMessage.cs new file mode 100644 index 0000000..56eaafe --- /dev/null +++ b/srcs/WingsAPI.Communication/Services/Messages/ServiceKickAllMessage.cs @@ -0,0 +1,13 @@ +using PhoenixLib.ServiceBus; +using PhoenixLib.ServiceBus.Routing; + +namespace WingsAPI.Communication.Services.Messages +{ + [MessageType("service.order.kickall")] + public class ServiceKickAllMessage : IMessage + { + public bool IsGlobal { get; init; } + + public string TargetedService { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Services/Messages/ServiceMaintenanceNotificationMessage.cs b/srcs/WingsAPI.Communication/Services/Messages/ServiceMaintenanceNotificationMessage.cs new file mode 100644 index 0000000..60a4c97 --- /dev/null +++ b/srcs/WingsAPI.Communication/Services/Messages/ServiceMaintenanceNotificationMessage.cs @@ -0,0 +1,24 @@ +using System; +using PhoenixLib.ServiceBus; +using PhoenixLib.ServiceBus.Routing; + +namespace WingsAPI.Communication.Services.Messages +{ + [MessageType("service.notify.maintenance")] + public class ServiceMaintenanceNotificationMessage : IMessage + { + public ServiceMaintenanceNotificationType NotificationType { get; init; } + public string Reason { get; init; } + public TimeSpan TimeLeft { get; init; } + } + + public enum ServiceMaintenanceNotificationType + { + Rescheduled, + ScheduleStopped, + ScheduleWarning, + Executed, + EmergencyExecuted, + Lifted + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Services/Requests/ServiceBasicRequest.cs b/srcs/WingsAPI.Communication/Services/Requests/ServiceBasicRequest.cs new file mode 100644 index 0000000..44c636d --- /dev/null +++ b/srcs/WingsAPI.Communication/Services/Requests/ServiceBasicRequest.cs @@ -0,0 +1,11 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.Services.Requests +{ + [ProtoContract] + public class ServiceBasicRequest + { + [ProtoMember(1)] + public string ServiceName { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Services/Requests/ServiceExecuteGeneralEmergencyMaintenanceRequest.cs b/srcs/WingsAPI.Communication/Services/Requests/ServiceExecuteGeneralEmergencyMaintenanceRequest.cs new file mode 100644 index 0000000..737e1e0 --- /dev/null +++ b/srcs/WingsAPI.Communication/Services/Requests/ServiceExecuteGeneralEmergencyMaintenanceRequest.cs @@ -0,0 +1,11 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.Services.Requests +{ + [ProtoContract] + public class ServiceExecuteGeneralEmergencyMaintenanceRequest + { + [ProtoMember(1)] + public string Reason { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Services/Requests/ServiceScheduleGeneralMaintenanceRequest.cs b/srcs/WingsAPI.Communication/Services/Requests/ServiceScheduleGeneralMaintenanceRequest.cs new file mode 100644 index 0000000..42ec4c2 --- /dev/null +++ b/srcs/WingsAPI.Communication/Services/Requests/ServiceScheduleGeneralMaintenanceRequest.cs @@ -0,0 +1,15 @@ +using System; +using ProtoBuf; + +namespace WingsAPI.Communication.Services.Requests +{ + [ProtoContract] + public class ServiceScheduleGeneralMaintenanceRequest + { + [ProtoMember(1)] + public TimeSpan ShutdownTimeSpan { get; init; } + + [ProtoMember(2)] + public string Reason { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Services/Responses/ServiceGetAllResponse.cs b/srcs/WingsAPI.Communication/Services/Responses/ServiceGetAllResponse.cs new file mode 100644 index 0000000..98188ec --- /dev/null +++ b/srcs/WingsAPI.Communication/Services/Responses/ServiceGetAllResponse.cs @@ -0,0 +1,16 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using ProtoBuf; + +namespace WingsAPI.Communication.Services.Responses +{ + [ProtoContract] + public class ServiceGetAllResponse + { + [ProtoMember(1)] + public List Services { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Services/Responses/ServiceGetStatusByNameResponse.cs b/srcs/WingsAPI.Communication/Services/Responses/ServiceGetStatusByNameResponse.cs new file mode 100644 index 0000000..f02d544 --- /dev/null +++ b/srcs/WingsAPI.Communication/Services/Responses/ServiceGetStatusByNameResponse.cs @@ -0,0 +1,14 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.Services.Responses +{ + [ProtoContract] + public class ServiceGetStatusByNameResponse + { + [ProtoMember(1)] + public RpcResponseType ResponseType { get; init; } + + [ProtoMember(2)] + public Service Service { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Services/Service.cs b/srcs/WingsAPI.Communication/Services/Service.cs new file mode 100644 index 0000000..6575c55 --- /dev/null +++ b/srcs/WingsAPI.Communication/Services/Service.cs @@ -0,0 +1,18 @@ +using System; +using ProtoBuf; + +namespace WingsAPI.Communication.Services +{ + [ProtoContract] + public class Service + { + [ProtoMember(1)] + public string Id { get; set; } + + [ProtoMember(2)] + public ServiceHealthStatus Status { get; set; } + + [ProtoMember(3)] + public DateTime LastUpdate { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Services/ServiceHealthStatus.cs b/srcs/WingsAPI.Communication/Services/ServiceHealthStatus.cs new file mode 100644 index 0000000..78aa5a2 --- /dev/null +++ b/srcs/WingsAPI.Communication/Services/ServiceHealthStatus.cs @@ -0,0 +1,9 @@ +namespace WingsAPI.Communication.Services +{ + public enum ServiceHealthStatus + { + OFFLINE, + ONLINE, + MAINTENANCE + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Sessions/ISessionService.cs b/srcs/WingsAPI.Communication/Sessions/ISessionService.cs new file mode 100644 index 0000000..d857f1b --- /dev/null +++ b/srcs/WingsAPI.Communication/Sessions/ISessionService.cs @@ -0,0 +1,38 @@ +using System.ServiceModel; +using System.Threading.Tasks; +using WingsAPI.Communication.Sessions.Request; +using WingsAPI.Communication.Sessions.Response; + +namespace WingsAPI.Communication.Sessions +{ + [ServiceContract] + public interface ISessionService + { + [OperationContract] + ValueTask CreateSession(CreateSessionRequest request); + + [OperationContract] + ValueTask GetSessionByAccountName(GetSessionByAccountNameRequest request); + + [OperationContract] + ValueTask GetSessionByAccountId(GetSessionByAccountIdRequest request); + + [OperationContract] + ValueTask ConnectToLoginServer(ConnectToLoginServerRequest request); + + [OperationContract] + ValueTask ConnectToWorldServer(ConnectToWorldServerRequest request); + + [OperationContract] + ValueTask Disconnect(DisconnectSessionRequest request); + + [OperationContract] + ValueTask ConnectCharacter(ConnectCharacterRequest request); + + [OperationContract] + ValueTask ActivateCrossChannelAuthentication(ActivateCrossChannelAuthenticationRequest request); + + [OperationContract] + ValueTask Pulse(PulseRequest request); + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Sessions/Model/Session.cs b/srcs/WingsAPI.Communication/Sessions/Model/Session.cs new file mode 100644 index 0000000..f6c9391 --- /dev/null +++ b/srcs/WingsAPI.Communication/Sessions/Model/Session.cs @@ -0,0 +1,55 @@ +using System; +using ProtoBuf; +using WingsEmu.DTOs.Account; + +namespace WingsAPI.Communication.Sessions.Model +{ + [ProtoContract] + public class Session + { + [ProtoMember(1)] + public string Id { get; init; } + + [ProtoMember(2)] + public AuthorityType Authority { get; init; } + + [ProtoMember(3)] + public long AccountId { get; init; } + + [ProtoMember(4)] + public string IpAddress { get; init; } + + [ProtoMember(5)] + public string AccountName { get; init; } + + [ProtoMember(6)] + public SessionState State { get; set; } + + [ProtoMember(7)] + public string HardwareId { get; set; } + + [ProtoMember(8)] + public string ClientVersion { get; set; } + + [ProtoMember(9)] + public int EncryptionKey { get; set; } + + [ProtoMember(10)] + public DateTime LastPulse { get; set; } + + [ProtoMember(11)] + public long CharacterId { get; set; } + + [ProtoMember(12)] + public int ChannelId { get; set; } + + [ProtoMember(13)] + public int LastChannelId { get; set; } + + [ProtoMember(14)] + public string ServerGroup { get; set; } + + [ProtoMember(15)] + public long AllowedCrossChannelId { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Sessions/Model/SessionState.cs b/srcs/WingsAPI.Communication/Sessions/Model/SessionState.cs new file mode 100644 index 0000000..38d8fb2 --- /dev/null +++ b/srcs/WingsAPI.Communication/Sessions/Model/SessionState.cs @@ -0,0 +1,11 @@ +namespace WingsAPI.Communication.Sessions.Model +{ + public enum SessionState + { + Disconnected, + ServerSelection, + CrossChannelAuthentication, + CharacterSelection, + InGame + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Sessions/Request/ActivateCrossChannelAuthenticationRequest.cs b/srcs/WingsAPI.Communication/Sessions/Request/ActivateCrossChannelAuthenticationRequest.cs new file mode 100644 index 0000000..b5cc998 --- /dev/null +++ b/srcs/WingsAPI.Communication/Sessions/Request/ActivateCrossChannelAuthenticationRequest.cs @@ -0,0 +1,14 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.Sessions.Request +{ + [ProtoContract] + public class ActivateCrossChannelAuthenticationRequest + { + [ProtoMember(1)] + public long AccountId { get; init; } + + [ProtoMember(2)] + public long ChannelId { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Sessions/Request/ConnectCharacterRequest.cs b/srcs/WingsAPI.Communication/Sessions/Request/ConnectCharacterRequest.cs new file mode 100644 index 0000000..8c96702 --- /dev/null +++ b/srcs/WingsAPI.Communication/Sessions/Request/ConnectCharacterRequest.cs @@ -0,0 +1,17 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.Sessions.Request +{ + [ProtoContract] + public class ConnectCharacterRequest + { + [ProtoMember(1)] + public long AccountId { get; init; } + + [ProtoMember(2)] + public int ChannelId { get; init; } + + [ProtoMember(3)] + public long CharacterId { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Sessions/Request/ConnectToLoginServerRequest.cs b/srcs/WingsAPI.Communication/Sessions/Request/ConnectToLoginServerRequest.cs new file mode 100644 index 0000000..64bed30 --- /dev/null +++ b/srcs/WingsAPI.Communication/Sessions/Request/ConnectToLoginServerRequest.cs @@ -0,0 +1,17 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.Sessions.Request +{ + [ProtoContract] + public class ConnectToLoginServerRequest + { + [ProtoMember(1)] + public long AccountId { get; init; } + + [ProtoMember(2)] + public string HardwareId { get; init; } + + [ProtoMember(3)] + public string ClientVersion { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Sessions/Request/ConnectToWorldServerRequest.cs b/srcs/WingsAPI.Communication/Sessions/Request/ConnectToWorldServerRequest.cs new file mode 100644 index 0000000..f0655e2 --- /dev/null +++ b/srcs/WingsAPI.Communication/Sessions/Request/ConnectToWorldServerRequest.cs @@ -0,0 +1,17 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.Sessions.Request +{ + [ProtoContract] + public class ConnectToWorldServerRequest + { + [ProtoMember(1)] + public long AccountId { get; init; } + + [ProtoMember(2)] + public string ServerGroup { get; init; } + + [ProtoMember(3)] + public int ChannelId { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Sessions/Request/CreateSessionRequest.cs b/srcs/WingsAPI.Communication/Sessions/Request/CreateSessionRequest.cs new file mode 100644 index 0000000..241ba5b --- /dev/null +++ b/srcs/WingsAPI.Communication/Sessions/Request/CreateSessionRequest.cs @@ -0,0 +1,21 @@ +using ProtoBuf; +using WingsEmu.DTOs.Account; + +namespace WingsAPI.Communication.Sessions.Request +{ + [ProtoContract] + public class CreateSessionRequest + { + [ProtoMember(1)] + public long AccountId { get; init; } + + [ProtoMember(2)] + public string AccountName { get; init; } + + [ProtoMember(3)] + public AuthorityType AuthorityType { get; init; } + + [ProtoMember(4)] + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Sessions/Request/DisconnectSessionRequest.cs b/srcs/WingsAPI.Communication/Sessions/Request/DisconnectSessionRequest.cs new file mode 100644 index 0000000..0f958bf --- /dev/null +++ b/srcs/WingsAPI.Communication/Sessions/Request/DisconnectSessionRequest.cs @@ -0,0 +1,17 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.Sessions.Request +{ + [ProtoContract] + public class DisconnectSessionRequest + { + [ProtoMember(1)] + public long AccountId { get; init; } + + [ProtoMember(2)] + public long EncryptionKey { get; init; } + + [ProtoMember(3)] + public bool ForceDisconnect { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Sessions/Request/GetSessionByAccountIdRequest.cs b/srcs/WingsAPI.Communication/Sessions/Request/GetSessionByAccountIdRequest.cs new file mode 100644 index 0000000..55f8770 --- /dev/null +++ b/srcs/WingsAPI.Communication/Sessions/Request/GetSessionByAccountIdRequest.cs @@ -0,0 +1,11 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.Sessions.Request +{ + [ProtoContract] + public class GetSessionByAccountIdRequest + { + [ProtoMember(1)] + public long AccountId { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Sessions/Request/GetSessionByAccountNameRequest.cs b/srcs/WingsAPI.Communication/Sessions/Request/GetSessionByAccountNameRequest.cs new file mode 100644 index 0000000..f6bb0cb --- /dev/null +++ b/srcs/WingsAPI.Communication/Sessions/Request/GetSessionByAccountNameRequest.cs @@ -0,0 +1,11 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.Sessions.Request +{ + [ProtoContract] + public class GetSessionByAccountNameRequest + { + [ProtoMember(1)] + public string AccountName { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Sessions/Request/GetSessionByIdRequest.cs b/srcs/WingsAPI.Communication/Sessions/Request/GetSessionByIdRequest.cs new file mode 100644 index 0000000..b8121ef --- /dev/null +++ b/srcs/WingsAPI.Communication/Sessions/Request/GetSessionByIdRequest.cs @@ -0,0 +1,11 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.Sessions.Request +{ + [ProtoContract] + public class GetSessionByIdRequest + { + [ProtoMember(1)] + public string SessionId { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Sessions/Request/PulseRequest.cs b/srcs/WingsAPI.Communication/Sessions/Request/PulseRequest.cs new file mode 100644 index 0000000..68f265e --- /dev/null +++ b/srcs/WingsAPI.Communication/Sessions/Request/PulseRequest.cs @@ -0,0 +1,11 @@ +using ProtoBuf; + +namespace WingsAPI.Communication.Sessions.Request +{ + [ProtoContract] + public class PulseRequest + { + [ProtoMember(1)] + public long AccountId { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Sessions/Response/SessionResponse.cs b/srcs/WingsAPI.Communication/Sessions/Response/SessionResponse.cs new file mode 100644 index 0000000..2128536 --- /dev/null +++ b/srcs/WingsAPI.Communication/Sessions/Response/SessionResponse.cs @@ -0,0 +1,15 @@ +using ProtoBuf; +using WingsAPI.Communication.Sessions.Model; + +namespace WingsAPI.Communication.Sessions.Response +{ + [ProtoContract] + public class SessionResponse + { + [ProtoMember(1)] + public RpcResponseType ResponseType { get; init; } + + [ProtoMember(2)] + public Session Session { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Translations/TranslationService.cs b/srcs/WingsAPI.Communication/Translations/TranslationService.cs new file mode 100644 index 0000000..2d69beb --- /dev/null +++ b/srcs/WingsAPI.Communication/Translations/TranslationService.cs @@ -0,0 +1,37 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using System.ServiceModel; +using System.Threading.Tasks; +using ProtoBuf; +using WingsAPI.Data.GameData; + +namespace WingsAPI.Communication.Translations +{ + [ServiceContract] + public interface ITranslationService + { + [OperationContract] + Task GetTranslations(EmptyRpcRequest rpcRequest); + + [OperationContract] + Task GetForbiddenWords(EmptyRpcRequest rpcRequest); + } + + [ProtoContract] + public class GetTranslationsResponse + { + [ProtoMember(1)] + public IReadOnlyList Translations { get; set; } + } + + + [ProtoContract] + public class GetForbiddenWordsResponse + { + [ProtoMember(1)] + public IReadOnlyList ForbiddenWords { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/Translations/TranslationsRefreshMessage.cs b/srcs/WingsAPI.Communication/Translations/TranslationsRefreshMessage.cs new file mode 100644 index 0000000..6ac00a7 --- /dev/null +++ b/srcs/WingsAPI.Communication/Translations/TranslationsRefreshMessage.cs @@ -0,0 +1,11 @@ +using PhoenixLib.ServiceBus; +using PhoenixLib.ServiceBus.Routing; + +namespace WingsAPI.Communication.Translations +{ + [MessageType("translations.refresh")] + public class TranslationsRefreshMessage : IMessage + { + public bool IsFullReload { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Communication/WingsAPI.Communication.csproj b/srcs/WingsAPI.Communication/WingsAPI.Communication.csproj new file mode 100644 index 0000000..35b27cf --- /dev/null +++ b/srcs/WingsAPI.Communication/WingsAPI.Communication.csproj @@ -0,0 +1,20 @@ + + + + net5.0 + WingsAPI.Communication + + + + + + + + + + + + + + + diff --git a/srcs/WingsAPI.Data/Account/AccountBanDto.cs b/srcs/WingsAPI.Data/Account/AccountBanDto.cs new file mode 100644 index 0000000..e027114 --- /dev/null +++ b/srcs/WingsAPI.Data/Account/AccountBanDto.cs @@ -0,0 +1,33 @@ +using System; +using PhoenixLib.DAL; +using ProtoBuf; + +namespace WingsAPI.Data.Account; + +[ProtoContract] +public class AccountBanDto : ILongDto +{ + [ProtoMember(2)] + public long AccountId { get; set; } + + [ProtoMember(3)] + public string JudgeName { get; set; } + + [ProtoMember(4)] + public string TargetName { get; set; } + + [ProtoMember(5)] + public DateTime Start { get; set; } + + [ProtoMember(6)] + public DateTime? End { get; set; } + + [ProtoMember(7)] + public string Reason { get; set; } + + [ProtoMember(8)] + public string UnlockReason { get; set; } + + [ProtoMember(1)] + public long Id { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Account/AccountDTO.cs b/srcs/WingsAPI.Data/Account/AccountDTO.cs new file mode 100644 index 0000000..85060f6 --- /dev/null +++ b/srcs/WingsAPI.Data/Account/AccountDTO.cs @@ -0,0 +1,42 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using PhoenixLib.DAL; +using ProtoBuf; +using WingsEmu.DTOs.Account; + +namespace WingsAPI.Data.Account; + +[ProtoContract] +public class AccountDTO : ILongDto +{ + [ProtoMember(2)] + public Guid MasterAccountId { get; set; } + + [ProtoMember(3)] + public AuthorityType Authority { get; set; } + + [ProtoMember(4)] + public AccountLanguage Language { get; set; } + + [ProtoMember(5)] + public long BankMoney { get; set; } + + [ProtoMember(6)] + public bool IsPrimaryAccount { get; set; } + + [ProtoMember(7)] + public string Name { get; set; } + + [ProtoMember(8)] + public string Password { get; set; } + + [ProtoMember(1)] + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long Id { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Account/AccountLanguage.cs b/srcs/WingsAPI.Data/Account/AccountLanguage.cs new file mode 100644 index 0000000..392fe09 --- /dev/null +++ b/srcs/WingsAPI.Data/Account/AccountLanguage.cs @@ -0,0 +1,13 @@ +namespace WingsEmu.DTOs.Account; + +public enum AccountLanguage +{ + EN, + FR, + DE, + PL, + IT, + ES, + CZ, + TR +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Account/AccountPenaltyDto.cs b/srcs/WingsAPI.Data/Account/AccountPenaltyDto.cs new file mode 100644 index 0000000..9afafbc --- /dev/null +++ b/srcs/WingsAPI.Data/Account/AccountPenaltyDto.cs @@ -0,0 +1,37 @@ +using System; +using PhoenixLib.DAL; +using ProtoBuf; +using WingsEmu.Packets.Enums; + +namespace WingsAPI.Data.Account; + +[ProtoContract] +public class AccountPenaltyDto : ILongDto +{ + [ProtoMember(2)] + public long AccountId { get; set; } + + [ProtoMember(3)] + public string JudgeName { get; set; } + + [ProtoMember(4)] + public string TargetName { get; set; } + + [ProtoMember(5)] + public DateTime Start { get; set; } + + [ProtoMember(6)] + public int? RemainingTime { get; set; } + + [ProtoMember(7)] + public PenaltyType PenaltyType { get; set; } + + [ProtoMember(8)] + public string Reason { get; set; } + + [ProtoMember(9)] + public string UnlockReason { get; set; } + + [ProtoMember(1)] + public long Id { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Account/AccountWarehouseItemDto.cs b/srcs/WingsAPI.Data/Account/AccountWarehouseItemDto.cs new file mode 100644 index 0000000..e8ef2ca --- /dev/null +++ b/srcs/WingsAPI.Data/Account/AccountWarehouseItemDto.cs @@ -0,0 +1,17 @@ +using ProtoBuf; +using WingsEmu.DTOs.Items; + +namespace WingsAPI.Data.Account; + +[ProtoContract] +public class AccountWarehouseItemDto +{ + [ProtoMember(1)] + public long AccountId { get; set; } + + [ProtoMember(2)] + public short Slot { get; set; } + + [ProtoMember(3)] + public ItemInstanceDTO ItemInstance { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Account/AuthorityType.cs b/srcs/WingsAPI.Data/Account/AuthorityType.cs new file mode 100644 index 0000000..a4d290b --- /dev/null +++ b/srcs/WingsAPI.Data/Account/AuthorityType.cs @@ -0,0 +1,33 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.DTOs.Account; + +public enum AuthorityType : short +{ + Closed = -3, + Banned = -2, + Unconfirmed = -1, + User = 0, + Vip = 1, + VipPlus = 3, + VipPlusPlus = 5, + Donator = 10, + DonatorPlus = 15, + DonatorPlusPlus = 20, + Moderator = 25, + BetaGameTester = 30, + GameMaster = 40, + SuperGameMaster = 500, + + + CommunityManager = 900, + + GameAdmin = 1000, // everything??? + + Owner = 1337, // everything except giving rights & some Remote + + + Root = 30000 // everything +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Account/IAccountBanDao.cs b/srcs/WingsAPI.Data/Account/IAccountBanDao.cs new file mode 100644 index 0000000..7842cbf --- /dev/null +++ b/srcs/WingsAPI.Data/Account/IAccountBanDao.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using PhoenixLib.DAL; + +namespace WingsAPI.Data.Account; + +public interface IAccountBanDao : IGenericAsyncLongRepository +{ + Task FindAccountBan(long accountId); + Task> GetAccountBans(long accountId); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Account/IAccountDAO.cs b/srcs/WingsAPI.Data/Account/IAccountDAO.cs new file mode 100644 index 0000000..738613e --- /dev/null +++ b/srcs/WingsAPI.Data/Account/IAccountDAO.cs @@ -0,0 +1,18 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using PhoenixLib.DAL; + +namespace WingsAPI.Data.Account; + +public interface IAccountDAO : IGenericAsyncLongRepository +{ + AccountDTO LoadByName(string name); + + Task GetByNameAsync(string name); + Task> LoadByMasterAccountIdAsync(Guid masterAccountId); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Account/IAccountPenaltyDao.cs b/srcs/WingsAPI.Data/Account/IAccountPenaltyDao.cs new file mode 100644 index 0000000..14131bf --- /dev/null +++ b/srcs/WingsAPI.Data/Account/IAccountPenaltyDao.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using PhoenixLib.DAL; + +namespace WingsAPI.Data.Account; + +public interface IAccountPenaltyDao : IGenericAsyncLongRepository +{ + Task> GetPenaltiesByAccountId(long accountId); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/ActDesc/ActDescDTO.cs b/srcs/WingsAPI.Data/ActDesc/ActDescDTO.cs new file mode 100644 index 0000000..dc8e086 --- /dev/null +++ b/srcs/WingsAPI.Data/ActDesc/ActDescDTO.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace WingsAPI.Data.ActDesc; + +public class ActDescDTO +{ + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public byte Act { get; set; } + + public byte SubAct { get; set; } + public byte TsAmount { get; set; } + public string ActName { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/BCards/BCardDTO.cs b/srcs/WingsAPI.Data/BCards/BCardDTO.cs new file mode 100644 index 0000000..85a872b --- /dev/null +++ b/srcs/WingsAPI.Data/BCards/BCardDTO.cs @@ -0,0 +1,47 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using WingsEmu.Game._enum; + +namespace WingsEmu.DTOs.BCards; + +public class BCardDTO +{ + public Guid Id { get; set; } = Guid.NewGuid(); + + public byte SubType { get; set; } + + public short Type { get; set; } + + public int FirstData { get; set; } + + public int SecondData { get; set; } + + public int ProcChance { get; set; } + + public byte? TickPeriod { get; set; } + + public byte CastType { get; set; } + + public BCardScalingType FirstDataScalingType { get; set; } + + public BCardScalingType SecondDataScalingType { get; set; } + + public bool? IsSecondBCardExecution { get; set; } + + public int? CardId { get; set; } + + public int? ItemVNum { get; set; } + + public int? SkillVNum { get; set; } + + public int? NpcMonsterVNum { get; set; } + + public BCardNpcMonsterTriggerType? TriggerType { get; set; } + + public BCardNpcTriggerType? NpcTriggerType { get; set; } + + public bool IsMonsterMode { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/BCards/BCardNpcMonsterTriggerType.cs b/srcs/WingsAPI.Data/BCards/BCardNpcMonsterTriggerType.cs new file mode 100644 index 0000000..007457d --- /dev/null +++ b/srcs/WingsAPI.Data/BCards/BCardNpcMonsterTriggerType.cs @@ -0,0 +1,8 @@ +namespace WingsEmu.DTOs.BCards; + +public enum BCardNpcMonsterTriggerType +{ + NONE = 0, // should always trigger + ON_FIRST_ATTACK = 1, // triggers only on first hit + ON_DEATH = 2 // triggers on npcMonster's death +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/BCards/BCardNpcTriggerType.cs b/srcs/WingsAPI.Data/BCards/BCardNpcTriggerType.cs new file mode 100644 index 0000000..ecfb9c2 --- /dev/null +++ b/srcs/WingsAPI.Data/BCards/BCardNpcTriggerType.cs @@ -0,0 +1,7 @@ +namespace WingsEmu.DTOs.BCards; + +public enum BCardNpcTriggerType +{ + ON_ATTACK = 0, + ON_DEFENSE = 1 +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Bazaar/BazaarItemDTO.cs b/srcs/WingsAPI.Data/Bazaar/BazaarItemDTO.cs new file mode 100644 index 0000000..681e92d --- /dev/null +++ b/srcs/WingsAPI.Data/Bazaar/BazaarItemDTO.cs @@ -0,0 +1,51 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using PhoenixLib.DAL; +using ProtoBuf; +using WingsEmu.DTOs.Items; + +namespace WingsAPI.Data.Bazaar; + +[ProtoContract] +public class BazaarItemDTO : ILongDto +{ + [ProtoMember(2)] + public long CharacterId { get; set; } + + [ProtoMember(3)] + public ItemInstanceDTO ItemInstance { get; set; } + + [ProtoMember(4)] + public int Amount { get; set; } + + [ProtoMember(5)] + public int SoldAmount { get; set; } + + [ProtoMember(6)] + public bool IsPackage { get; set; } + + [ProtoMember(7)] + public bool UsedMedal { get; set; } + + [ProtoMember(8)] + public long PricePerItem { get; set; } + + [ProtoMember(9)] + public long SaleFee { get; set; } + + [ProtoMember(10)] + public DateTime ExpiryDate { get; set; } + + [ProtoMember(11)] + public short DayExpiryAmount { get; set; } + + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + [ProtoMember(1)] + public long Id { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Bazaar/IBazaarItemDAO.cs b/srcs/WingsAPI.Data/Bazaar/IBazaarItemDAO.cs new file mode 100644 index 0000000..3eeefeb --- /dev/null +++ b/srcs/WingsAPI.Data/Bazaar/IBazaarItemDAO.cs @@ -0,0 +1,16 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using System.Threading.Tasks; +using PhoenixLib.DAL; +using WingsAPI.Data.Bazaar; + +namespace WingsEmu.DTOs.Bazaar; + +public interface IBazaarItemDAO : IGenericAsyncLongRepository +{ + Task> GetAllNonDeletedBazaarItems(); + Task> GetBazaarItemsByCharacterId(long characterId); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Bonus/CharacterStaticBonusDto.cs b/srcs/WingsAPI.Data/Bonus/CharacterStaticBonusDto.cs new file mode 100644 index 0000000..d9dffa1 --- /dev/null +++ b/srcs/WingsAPI.Data/Bonus/CharacterStaticBonusDto.cs @@ -0,0 +1,21 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using ProtoBuf; + +namespace WingsEmu.DTOs.Bonus; + +[ProtoContract] +public class CharacterStaticBonusDto +{ + [ProtoMember(1)] + public DateTime? DateEnd { get; set; } + + [ProtoMember(2)] + public StaticBonusType StaticBonusType { get; set; } + + [ProtoMember(3)] + public int ItemVnum { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Bonus/StaticBonusType.cs b/srcs/WingsAPI.Data/Bonus/StaticBonusType.cs new file mode 100644 index 0000000..5b563cf --- /dev/null +++ b/srcs/WingsAPI.Data/Bonus/StaticBonusType.cs @@ -0,0 +1,16 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.DTOs.Bonus; + +public enum StaticBonusType : byte +{ + BazaarMedalGold = 0, + BazaarMedalSilver = 1, + Backpack = 2, + PetBasket = 3, + PartnerBackpack = 4, + InventoryExpansion = 5, + CuarryBankMedal = 6 +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Buffs/CardDTO.cs b/srcs/WingsAPI.Data/Buffs/CardDTO.cs new file mode 100644 index 0000000..5c7ab1f --- /dev/null +++ b/srcs/WingsAPI.Data/Buffs/CardDTO.cs @@ -0,0 +1,41 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using PhoenixLib.DAL; +using WingsEmu.DTOs.BCards; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.DTOs.Buffs; + +public class CardDTO : IIntDto +{ + public int Duration { get; set; } + + public int EffectId { get; set; } + + public int GroupId { get; set; } + + public byte Level { get; set; } + + public string Name { get; set; } + + public short TimeoutBuff { get; set; } + + public int BuffType { get; set; } + + public byte TimeoutBuffChance { get; set; } + + public int SecondBCardsDelay { get; set; } + + public BuffCategory BuffCategory { get; set; } + + public byte BuffPartnerLevel { get; set; } + + public bool IsConstEffect { get; set; } + + public byte ElementType { get; set; } + public List Bcards { get; set; } = new(); + public int Id { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Buffs/CharacterStaticBuffDto.cs b/srcs/WingsAPI.Data/Buffs/CharacterStaticBuffDto.cs new file mode 100644 index 0000000..eee8856 --- /dev/null +++ b/srcs/WingsAPI.Data/Buffs/CharacterStaticBuffDto.cs @@ -0,0 +1,20 @@ +// WingsEmu +// +// Developed by NosWings Team + +using ProtoBuf; + +namespace WingsEmu.DTOs.Buffs; + +[ProtoContract] +public class CharacterStaticBuffDto +{ + [ProtoMember(1)] + public long CharacterId { get; set; } + + [ProtoMember(2)] + public int RemainingTime { get; set; } + + [ProtoMember(3)] + public int CardId { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Character/CharacterDTO.cs b/srcs/WingsAPI.Data/Character/CharacterDTO.cs new file mode 100644 index 0000000..bf3f47a --- /dev/null +++ b/srcs/WingsAPI.Data/Character/CharacterDTO.cs @@ -0,0 +1,278 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using PhoenixLib.DAL; +using ProtoBuf; +using WingsAPI.Data.Miniland; +using WingsAPI.Packets.Enums; +using WingsEmu.DTOs.Bonus; +using WingsEmu.DTOs.Buffs; +using WingsEmu.DTOs.Inventory; +using WingsEmu.DTOs.Mates; +using WingsEmu.DTOs.Quests; +using WingsEmu.DTOs.Quicklist; +using WingsEmu.DTOs.Respawns; +using WingsEmu.DTOs.Skills; +using WingsEmu.DTOs.Titles; +using WingsEmu.Game._enum; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Character; + +namespace WingsAPI.Data.Character; + +[ProtoContract] +public class CharacterDTO : ILongDto +{ + [ProtoMember(2)] + public long AccountId { get; set; } + + [ProtoMember(3)] + public int Act4Dead { get; set; } + + [ProtoMember(4)] + public int Act4Kill { get; set; } + + [ProtoMember(5)] + public int Act4Points { get; set; } + + [ProtoMember(6)] + public int ArenaWinner { get; set; } + + [ProtoMember(7)] + [MaxLength(255)] + public string Biography { get; set; } + + [ProtoMember(8)] + public bool BuffBlocked { get; set; } + + [ProtoMember(9)] + public ClassType Class { get; set; } + + [ProtoMember(10)] + public short Compliment { get; set; } + + [ProtoMember(11)] + public float Dignity { get; set; } + + [ProtoMember(12)] + public bool EmoticonsBlocked { get; set; } + + [ProtoMember(13)] + public bool ExchangeBlocked { get; set; } + + [ProtoMember(14)] + public FactionType Faction { get; set; } + + [ProtoMember(15)] + public bool FamilyRequestBlocked { get; set; } + + [ProtoMember(16)] + public bool FriendRequestBlocked { get; set; } + + [ProtoMember(17)] + public GenderType Gender { get; set; } + + [ProtoMember(18)] + public long Gold { get; set; } + + [ProtoMember(19)] + public bool GroupRequestBlocked { get; set; } + + [ProtoMember(20)] + public HairColorType HairColor { get; set; } + + [ProtoMember(21)] + public HairStyleType HairStyle { get; set; } + + [ProtoMember(22)] + public bool HeroChatBlocked { get; set; } + + [ProtoMember(23)] + public byte HeroLevel { get; set; } + + [ProtoMember(24)] + public long HeroXp { get; set; } + + [ProtoMember(25)] + public int Hp { get; set; } + + [ProtoMember(26)] + public bool HpBlocked { get; set; } + + [ProtoMember(27)] + public bool IsPetAutoRelive { get; set; } + + [ProtoMember(28)] + public bool IsPartnerAutoRelive { get; set; } + + [ProtoMember(29)] + public byte JobLevel { get; set; } + + [ProtoMember(30)] + public long JobLevelXp { get; set; } + + [ProtoMember(31)] + public byte Level { get; set; } + + [ProtoMember(32)] + public long LevelXp { get; set; } + + [ProtoMember(33)] + public int MapId { get; set; } + + [ProtoMember(34)] + public short MapX { get; set; } + + [ProtoMember(35)] + public short MapY { get; set; } + + [ProtoMember(36)] + public int MasterPoints { get; set; } + + [ProtoMember(37)] + public int MasterTicket { get; set; } + + [ProtoMember(38)] + public byte MaxPetCount { get; set; } + + [ProtoMember(39)] + public byte MaxPartnerCount { get; set; } + + [ProtoMember(40)] + public bool MinilandInviteBlocked { get; set; } + + [ProtoMember(41)] + [MaxLength(255)] + public string MinilandMessage { get; set; } + + [ProtoMember(42)] + public short MinilandPoint { get; set; } + + [ProtoMember(43)] + public MinilandState MinilandState { get; set; } + + [ProtoMember(44)] + public bool MouseAimLock { get; set; } + + [ProtoMember(45)] + public int Mp { get; set; } + + [ProtoMember(46)] + [MaxLength(25)] + public string Prefix { get; set; } + + [ProtoMember(47)] + [MaxLength(30)] + public string Name { get; set; } + + [ProtoMember(48)] + public bool QuickGetUp { get; set; } + + [ProtoMember(49)] + public bool HideHat { get; set; } + + [ProtoMember(50)] + public bool UiBlocked { get; set; } + + [ProtoMember(51)] + public long RagePoint { get; set; } + + [ProtoMember(52)] + public long Reput { get; set; } + + [ProtoMember(53)] + public byte Slot { get; set; } + + [ProtoMember(54)] + public int SpPointsBonus { get; set; } + + [ProtoMember(55)] + public int SpPointsBasic { get; set; } + + [ProtoMember(56)] + public int TalentLose { get; set; } + + [ProtoMember(57)] + public int TalentSurrender { get; set; } + + [ProtoMember(58)] + public int TalentWin { get; set; } + + [ProtoMember(59)] + public bool WhisperBlocked { get; set; } + + [ProtoMember(60)] + public List PartnerInventory { get; set; } = new(); + + [ProtoMember(61)] + public List NosMates { get; set; } = new(); + + [ProtoMember(62)] + public List PartnerWarehouse { get; set; } = new(); + + [ProtoMember(63)] + public List Bonus { get; set; } = new(); + + [ProtoMember(64)] + public List StaticBuffs { get; set; } = new(); + + [ProtoMember(65)] + public List Quicklist { get; set; } = new(); + + [ProtoMember(66)] + public List LearnedSkills { get; set; } = new(); + + [ProtoMember(67)] + public List Titles { get; set; } = new(); + + [ProtoMember(68)] + public List CompletedScripts { get; set; } = new(); + + [ProtoMember(69)] + public List CompletedPeriodicQuests { get; set; } = new(); + + [ProtoMember(70)] + public List ActiveQuests { get; set; } = new(); + + [ProtoMember(71)] + public List MinilandObjects { get; set; } = new(); + + [ProtoMember(72)] + public RespawnType RespawnType { get; set; } + + [ProtoMember(73)] + public CharacterReturnDto ReturnPoint { get; set; } + + [ProtoMember(74)] + public List Inventory { get; set; } = new(); + + [ProtoMember(75)] + public List EquippedStuffs { get; set; } = new(); + + [ProtoMember(76)] + public CharacterLifetimeStatsDto LifetimeStats { get; set; } = new(); + + [ProtoMember(77)] + public List CompletedQuests { get; set; } = new(); + + [ProtoMember(78)] + public HashSet CompletedTimeSpaces { get; set; } = new(); + + [ProtoMember(79)] + public CharacterRaidRestrictionDto RaidRestrictionDto { get; set; } = new(); + + [ProtoMember(80)] + public Act5RespawnType Act5RespawnType { get; set; } + + [ProtoMember(81)] + public RainbowBattleLeaverBusterDto RainbowBattleLeaverBusterDto { get; set; } = new(); + + [ProtoMember(1)] + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long Id { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Character/CharacterLifetimeStatsDto.cs b/srcs/WingsAPI.Data/Character/CharacterLifetimeStatsDto.cs new file mode 100644 index 0000000..63c625c --- /dev/null +++ b/srcs/WingsAPI.Data/Character/CharacterLifetimeStatsDto.cs @@ -0,0 +1,107 @@ +using System; +using ProtoBuf; + +namespace WingsAPI.Data.Character; + +[ProtoContract] +public class CharacterLifetimeStatsDto +{ + /* + * BATTLE + */ + [ProtoMember(1)] + public long TotalMonstersKilled { get; set; } + + [ProtoMember(2)] + public long TotalPlayersKilled { get; set; } + + [ProtoMember(3)] + public long TotalDeathsByMonster { get; set; } + + [ProtoMember(4)] + public long TotalDeathsByPlayer { get; set; } + + [ProtoMember(5)] + public long TotalSkillsCasted { get; set; } + + [ProtoMember(6)] + public long TotalDamageDealt { get; set; } + + /* + * FARMING + */ + [ProtoMember(7)] + public long TotalRaidsWon { get; set; } + + [ProtoMember(8)] + public long TotalRaidsLost { get; set; } + + [ProtoMember(9)] + public long TotalTimespacesWon { get; set; } + + [ProtoMember(10)] + public long TotalTimespacesLost { get; set; } + + /* + * EVENTS + */ + [ProtoMember(11)] + public long TotalInstantBattleWon { get; set; } + + [ProtoMember(12)] + public long TotalIcebreakerWon { get; set; } + + /* + * ECONOMY + */ + [ProtoMember(13)] + public long TotalGoldSpent { get; set; } + + [ProtoMember(14)] + public long TotalGoldSpentInBazaarItems { get; set; } + + [ProtoMember(15)] + public long TotalGoldSpentInBazaarFees { get; set; } + + [ProtoMember(16)] + public long TotalGoldDropped { get; set; } + + [ProtoMember(17)] + public long TotalGoldEarnedInBazaarItems { get; set; } + + [ProtoMember(18)] + public long TotalGoldSpentInNpcShop { get; set; } + + /* + * ITEMS + */ + [ProtoMember(19)] + public long TotalItemsUsed { get; set; } + + [ProtoMember(20)] + public long TotalPotionsUsed { get; set; } + + [ProtoMember(21)] + public long TotalSnacksUsed { get; set; } + + [ProtoMember(22)] + public long TotalFoodUsed { get; set; } + + /* + * OTHER + */ + [ProtoMember(23)] + public long TotalMinilandVisits { get; set; } + + [ProtoMember(24)] + public TimeSpan TotalTimeOnline { get; set; } + + /* + * ARENA + */ + [ProtoMember(25)] + public long TotalArenaDeaths { get; set; } + + [ProtoMember(26)] + public long TotalArenaKills { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Character/CharacterRaidRestrictionDto.cs b/srcs/WingsAPI.Data/Character/CharacterRaidRestrictionDto.cs new file mode 100644 index 0000000..8d7272b --- /dev/null +++ b/srcs/WingsAPI.Data/Character/CharacterRaidRestrictionDto.cs @@ -0,0 +1,13 @@ +using ProtoBuf; + +namespace WingsAPI.Data.Character; + +[ProtoContract] +public class CharacterRaidRestrictionDto +{ + [ProtoMember(1)] + public byte LordDraco { get; set; } + + [ProtoMember(2)] + public byte Glacerus { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Character/ICharacterDAO.cs b/srcs/WingsAPI.Data/Character/ICharacterDAO.cs new file mode 100644 index 0000000..6f35606 --- /dev/null +++ b/srcs/WingsAPI.Data/Character/ICharacterDAO.cs @@ -0,0 +1,45 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using PhoenixLib.DAL; +using WingsEmu.DTOs.Enums; +using WingsEmu.Packets.Enums.Character; + +namespace WingsAPI.Data.Character; + +public interface ICharacterDAO : IGenericAsyncLongRepository +{ + Task DeleteByPrimaryKey(long accountId, byte characterSlot); + + Task> GetTopCompliment(int top = 30); + + Task> GetTopPoints(int top = 30); + + Task> GetTopReputation(int top = 43); + + IEnumerable LoadByAccount(long accountId); + + Task> LoadByAccountAsync(long accountId); + + CharacterDTO GetById(long characterId); + + Task LoadByNameAsync(string name); + + CharacterDTO LoadBySlot(long accountId, byte slot); + + Task LoadBySlotAsync(long accountId, byte slot); + + IEnumerable LoadAllCharactersByAccount(long accountId); + + Task> GetAllCharactersByMasterAccountIdAsync(Guid accountId); + + Task> GetTopByLevelAsync(int number); + + Task> GetTopLevelByClassTypeAsync(ClassType classType, int number); + + Task> GetClassesCountAsync(); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Character/RainbowBattleLeaverBusterDto.cs b/srcs/WingsAPI.Data/Character/RainbowBattleLeaverBusterDto.cs new file mode 100644 index 0000000..8448f3d --- /dev/null +++ b/srcs/WingsAPI.Data/Character/RainbowBattleLeaverBusterDto.cs @@ -0,0 +1,19 @@ +using ProtoBuf; + +namespace WingsAPI.Data.Character; + +[ProtoContract] +public class RainbowBattleLeaverBusterDto +{ + /// + /// The number of times a player has left the Rainbow Battle + /// + [ProtoMember(1)] + public byte Exits { get; set; } + + /// + /// The amount of prizes the player will not get + /// + [ProtoMember(2)] + public short RewardPenalty { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Drops/DropDTO.cs b/srcs/WingsAPI.Data/Drops/DropDTO.cs new file mode 100644 index 0000000..f8a0a2b --- /dev/null +++ b/srcs/WingsAPI.Data/Drops/DropDTO.cs @@ -0,0 +1,26 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using PhoenixLib.DAL; + +namespace WingsAPI.Data.Drops; + +public class DropDTO : IIntDto +{ + public int Amount { get; set; } + + public int DropChance { get; set; } + + public int ItemVNum { get; set; } + public int? MapId { get; set; } + public int? MonsterVNum { get; set; } + public int? RaceType { get; set; } + public int? RaceSubType { get; set; } + + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Enums/DeleteResult.cs b/srcs/WingsAPI.Data/Enums/DeleteResult.cs new file mode 100644 index 0000000..207b115 --- /dev/null +++ b/srcs/WingsAPI.Data/Enums/DeleteResult.cs @@ -0,0 +1,12 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.DTOs.Enums; + +public enum DeleteResult : byte +{ + Unknown = 0, + Deleted = 1, + Error = 2 +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Exchanges/LogPlayerExchangeItemInfo.cs b/srcs/WingsAPI.Data/Exchanges/LogPlayerExchangeItemInfo.cs new file mode 100644 index 0000000..cb45127 --- /dev/null +++ b/srcs/WingsAPI.Data/Exchanges/LogPlayerExchangeItemInfo.cs @@ -0,0 +1,10 @@ +using WingsEmu.DTOs.Items; + +namespace WingsAPI.Data.Exchanges; + +public class LogPlayerExchangeItemInfo +{ + public short Amount { get; init; } + public ItemInstanceDTO ItemInstance { get; init; } + public byte Slot { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Families/FamilyAchievementCompletionDto.cs b/srcs/WingsAPI.Data/Families/FamilyAchievementCompletionDto.cs new file mode 100644 index 0000000..4e4db86 --- /dev/null +++ b/srcs/WingsAPI.Data/Families/FamilyAchievementCompletionDto.cs @@ -0,0 +1,14 @@ +using System; +using ProtoBuf; + +namespace WingsAPI.Data.Families; + +[ProtoContract] +public class FamilyAchievementCompletionDto +{ + [ProtoMember(1)] + public int Id { get; set; } + + [ProtoMember(2)] + public DateTime CompletionDate { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Families/FamilyAchievementProgressDto.cs b/srcs/WingsAPI.Data/Families/FamilyAchievementProgressDto.cs new file mode 100644 index 0000000..9e403c2 --- /dev/null +++ b/srcs/WingsAPI.Data/Families/FamilyAchievementProgressDto.cs @@ -0,0 +1,13 @@ +using ProtoBuf; + +namespace WingsAPI.Data.Families; + +[ProtoContract] +public class FamilyAchievementProgressDto +{ + [ProtoMember(1)] + public int Id { get; set; } + + [ProtoMember(2)] + public int Count { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Families/FamilyAchievementsDto.cs b/srcs/WingsAPI.Data/Families/FamilyAchievementsDto.cs new file mode 100644 index 0000000..a46b1e6 --- /dev/null +++ b/srcs/WingsAPI.Data/Families/FamilyAchievementsDto.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using ProtoBuf; + +namespace WingsAPI.Data.Families; + +[ProtoContract] +public class FamilyAchievementsDto +{ + [ProtoMember(1)] + public Dictionary Achievements { get; set; } + + [ProtoMember(2)] + public Dictionary Progress { get; set; } +} + +[ProtoContract] +public class FamilyMissionsDto +{ + [ProtoMember(1)] + public Dictionary Missions { get; set; } +} + +[ProtoContract] +public class FamilyMissionDto +{ + [ProtoMember(1)] + public int Id { get; set; } + + [ProtoMember(2)] + public int Count { get; set; } + + [ProtoMember(3)] + public int CompletionCount { get; set; } + + [ProtoMember(4)] + public DateTime? CompletionDate { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Families/FamilyDTO.cs b/srcs/WingsAPI.Data/Families/FamilyDTO.cs new file mode 100644 index 0000000..fa0f171 --- /dev/null +++ b/srcs/WingsAPI.Data/Families/FamilyDTO.cs @@ -0,0 +1,72 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using PhoenixLib.DAL; +using ProtoBuf; +using WingsAPI.Packets.Enums.Families; +using WingsEmu.Packets.Enums.Character; + +namespace WingsAPI.Data.Families; + +[ProtoContract] +public class FamilyDTO : ILongDto +{ + [ProtoMember(2)] + [MinLength(3)] + [MaxLength(20)] + public string Name { get; set; } + + [ProtoMember(3)] + public byte Level { get; set; } + + [ProtoMember(4)] + public long Experience { get; set; } + + [ProtoMember(5)] + public byte Faction { get; set; } + + [ProtoMember(6)] + public GenderType HeadGender { get; set; } + + [ProtoMember(9)] + [MaxLength(50)] + public string Message { get; set; } + + [ProtoMember(10)] + public FamilyWarehouseAuthorityType AssistantWarehouseAuthorityType { get; set; } + + [ProtoMember(11)] + public FamilyWarehouseAuthorityType MemberWarehouseAuthorityType { get; set; } + + [ProtoMember(12)] + public bool AssistantCanGetHistory { get; set; } + + [ProtoMember(13)] + public bool AssistantCanInvite { get; set; } + + [ProtoMember(14)] + public bool AssistantCanNotice { get; set; } + + [ProtoMember(15)] + public bool AssistantCanShout { get; set; } + + [ProtoMember(16)] + public bool MemberCanGetHistory { get; set; } + + [ProtoMember(17)] + public FamilyUpgradeDto Upgrades { get; set; } + + [ProtoMember(18)] + public FamilyAchievementsDto Achievements { get; set; } + + [ProtoMember(19)] + public FamilyMissionsDto Missions { get; set; } + + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + [ProtoMember(1)] + public long Id { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Families/FamilyLogDTO.cs b/srcs/WingsAPI.Data/Families/FamilyLogDTO.cs new file mode 100644 index 0000000..4f5930a --- /dev/null +++ b/srcs/WingsAPI.Data/Families/FamilyLogDTO.cs @@ -0,0 +1,38 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using PhoenixLib.DAL; +using ProtoBuf; +using WingsEmu.Packets.Enums.Families; + +namespace WingsAPI.Data.Families; + +[ProtoContract] +public class FamilyLogDto : ILongDto +{ + [ProtoMember(2)] + public long FamilyId { get; set; } + + [ProtoMember(3)] + public FamilyLogType FamilyLogType { get; set; } + + [ProtoMember(4)] + public DateTime Timestamp { get; set; } + + [ProtoMember(5)] + public string Actor { get; set; } + + [ProtoMember(6)] + public string Argument1 { get; set; } + + [ProtoMember(7)] + public string Argument2 { get; set; } + + [ProtoMember(8)] + public string Argument3 { get; set; } + + [ProtoMember(1)] + public long Id { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Families/FamilyMembershipDto.cs b/srcs/WingsAPI.Data/Families/FamilyMembershipDto.cs new file mode 100644 index 0000000..04b5460 --- /dev/null +++ b/srcs/WingsAPI.Data/Families/FamilyMembershipDto.cs @@ -0,0 +1,46 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using PhoenixLib.DAL; +using ProtoBuf; +using WingsEmu.Packets.Enums.Families; + +namespace WingsAPI.Data.Families; + +[ProtoContract] +public class FamilyMembershipDto : ILongDto +{ + [ProtoMember(2)] + public long FamilyId { get; set; } + + [ProtoMember(3)] + public long CharacterId { get; set; } + + [ProtoMember(4)] + public FamilyAuthority Authority { get; set; } + + [ProtoMember(5)] + [MaxLength(50)] + public string DailyMessage { get; set; } + + [ProtoMember(6)] + public long Experience { get; set; } + + [ProtoMember(7)] + public FamilyTitle Title { get; set; } + + [ProtoMember(8)] + public DateTime JoinDate { get; set; } + + [ProtoMember(9)] + public DateTime LastOnlineDate { get; set; } + + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + [ProtoMember(1)] + public long Id { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Families/FamilyUpgradeDto.cs b/srcs/WingsAPI.Data/Families/FamilyUpgradeDto.cs new file mode 100644 index 0000000..2b2539c --- /dev/null +++ b/srcs/WingsAPI.Data/Families/FamilyUpgradeDto.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using ProtoBuf; + +namespace WingsAPI.Data.Families; + +[ProtoContract] +public class FamilyUpgradeDto +{ + [ProtoMember(1)] + public HashSet UpgradesBought { get; set; } = new(); + + [ProtoMember(2)] + public Dictionary UpgradeValues { get; set; } = new(); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Families/FamilyUpgradeState.cs b/srcs/WingsAPI.Data/Families/FamilyUpgradeState.cs new file mode 100644 index 0000000..acdb0c7 --- /dev/null +++ b/srcs/WingsAPI.Data/Families/FamilyUpgradeState.cs @@ -0,0 +1,8 @@ +namespace WingsAPI.Data.Families; + +public enum FamilyUpgradeState +{ + PASSIVE, + ACTIVATABLE, + ALREADY_USED_TODAY +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Families/FamilyUpgradeType.cs b/srcs/WingsAPI.Data/Families/FamilyUpgradeType.cs new file mode 100644 index 0000000..9678b30 --- /dev/null +++ b/srcs/WingsAPI.Data/Families/FamilyUpgradeType.cs @@ -0,0 +1,17 @@ +namespace WingsAPI.Data.Families; + +public enum FamilyUpgradeType +{ + INCREASE_ATTACK_DEFENSE = 0, + INCREASE_FOOD_SNACK_REGEN = 1, + DECREASE_SHIP_TP_COST = 2, + INCREASE_POTION_REGEN = 3, + INCREASE_FAMILY_WAREHOUSE = 4, + INCREASE_FAMILY_MEMBERS_LIMIT = 5, + + + FIRE_RESISTANCE, + WATER_RESISTANCE, + DARK_RESISTANCE, + LIGHT_RESISTANCE +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Families/FamilyWarehouseItemDto.cs b/srcs/WingsAPI.Data/Families/FamilyWarehouseItemDto.cs new file mode 100644 index 0000000..b199077 --- /dev/null +++ b/srcs/WingsAPI.Data/Families/FamilyWarehouseItemDto.cs @@ -0,0 +1,17 @@ +using ProtoBuf; +using WingsEmu.DTOs.Items; + +namespace WingsAPI.Data.Families; + +[ProtoContract] +public class FamilyWarehouseItemDto +{ + [ProtoMember(1)] + public long FamilyId { get; set; } + + [ProtoMember(2)] + public short Slot { get; set; } + + [ProtoMember(3)] + public ItemInstanceDTO ItemInstance { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Families/FamilyWarehouseLogEntryDto.cs b/srcs/WingsAPI.Data/Families/FamilyWarehouseLogEntryDto.cs new file mode 100644 index 0000000..398367b --- /dev/null +++ b/srcs/WingsAPI.Data/Families/FamilyWarehouseLogEntryDto.cs @@ -0,0 +1,32 @@ +using System; +using ProtoBuf; + +namespace WingsAPI.Data.Families; + +[ProtoContract] +public class FamilyWarehouseLogEntryDto +{ + [ProtoMember(1)] + public long CharacterId { get; set; } + + [ProtoMember(2)] + public DateTime DateOfLog { get; set; } + + [ProtoMember(3)] + public FamilyWarehouseLogEntryType Type { get; set; } + + [ProtoMember(4)] + public int ItemVnum { get; set; } + + [ProtoMember(5)] + public int Amount { get; set; } + + [ProtoMember(6)] + public string CharacterName { get; set; } +} + +public enum FamilyWarehouseLogEntryType +{ + List = 0, + Withdraw = 1 +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Families/IFamilyDAO.cs b/srcs/WingsAPI.Data/Families/IFamilyDAO.cs new file mode 100644 index 0000000..070e796 --- /dev/null +++ b/srcs/WingsAPI.Data/Families/IFamilyDAO.cs @@ -0,0 +1,13 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Threading.Tasks; +using PhoenixLib.DAL; + +namespace WingsAPI.Data.Families; + +public interface IFamilyDAO : IGenericAsyncLongRepository +{ + Task GetByNameAsync(string reqName); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Families/IFamilyLogDAO.cs b/srcs/WingsAPI.Data/Families/IFamilyLogDAO.cs new file mode 100644 index 0000000..d630c74 --- /dev/null +++ b/srcs/WingsAPI.Data/Families/IFamilyLogDAO.cs @@ -0,0 +1,19 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using System.Threading.Tasks; +using PhoenixLib.DAL; + +namespace WingsAPI.Data.Families; + +public interface IFamilyLogDAO : IGenericAsyncLongRepository +{ + /// + /// Gets a list of 200 logs ordered by DateTime + /// + /// + /// + Task> GetLogsByFamilyIdAsync(long familyId); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Families/IFamilyMembershipDao.cs b/srcs/WingsAPI.Data/Families/IFamilyMembershipDao.cs new file mode 100644 index 0000000..09071ca --- /dev/null +++ b/srcs/WingsAPI.Data/Families/IFamilyMembershipDao.cs @@ -0,0 +1,16 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using System.Threading.Tasks; +using PhoenixLib.DAL; + +namespace WingsAPI.Data.Families; + +public interface IFamilyMembershipDao : IGenericAsyncLongRepository +{ + Task GetByCharacterIdAsync(long characterId); + + Task> GetByFamilyIdAsync(long familyId); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Families/IFamilyWarehouseItemDAO.cs b/srcs/WingsAPI.Data/Families/IFamilyWarehouseItemDAO.cs new file mode 100644 index 0000000..52cbc4d --- /dev/null +++ b/srcs/WingsAPI.Data/Families/IFamilyWarehouseItemDAO.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace WingsAPI.Data.Families; + +public interface IFamilyWarehouseItemDao +{ + Task SaveAsync(IReadOnlyList objs); + Task DeleteAsync(IEnumerable objs); + Task> GetByFamilyIdAsync(long familyId); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Families/IFamilyWarehouseLogDao.cs b/srcs/WingsAPI.Data/Families/IFamilyWarehouseLogDao.cs new file mode 100644 index 0000000..373292b --- /dev/null +++ b/srcs/WingsAPI.Data/Families/IFamilyWarehouseLogDao.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace WingsAPI.Data.Families; + +public interface IFamilyWarehouseLogDao +{ + Task SaveAsync(long familyId, IEnumerable objs); + Task> GetByFamilyIdAsync(long familyId); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/GameData/IResourceLoader.cs b/srcs/WingsAPI.Data/GameData/IResourceLoader.cs new file mode 100644 index 0000000..4dbe859 --- /dev/null +++ b/srcs/WingsAPI.Data/GameData/IResourceLoader.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using PhoenixLib.MultiLanguage; +using ProtoBuf; +using WingsEmu.Game._i18n; + +namespace WingsAPI.Data.GameData; + +public interface IResourceLoader +{ + Task> LoadAsync(); +} + +public class GameDataTranslationDto +{ + public GameDataType DataType { get; set; } + public RegionLanguageType Language { get; set; } + public string Key { get; set; } + public string Value { get; set; } +} + +[ProtoContract] +public class GenericTranslationDto +{ + [ProtoMember(1)] + public RegionLanguageType Language { get; set; } + + [ProtoMember(2)] + public string Key { get; set; } + + [ProtoMember(3)] + public string Value { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Inventory/CharacterInventoryItemDto.cs b/srcs/WingsAPI.Data/Inventory/CharacterInventoryItemDto.cs new file mode 100644 index 0000000..a58f678 --- /dev/null +++ b/srcs/WingsAPI.Data/Inventory/CharacterInventoryItemDto.cs @@ -0,0 +1,32 @@ +// WingsEmu +// +// Developed by NosWings Team + +using ProtoBuf; +using WingsEmu.DTOs.Items; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.DTOs.Inventory; + +/// +/// Composite object +/// Character + Slot are unique +/// +[ProtoContract] +public class CharacterInventoryItemDto +{ + [ProtoMember(1)] + public long CharacterId { get; set; } + + [ProtoMember(2)] + public short Slot { get; set; } + + [ProtoMember(3)] + public InventoryType InventoryType { get; set; } + + [ProtoMember(4)] + public bool IsEquipped { get; set; } + + [ProtoMember(5)] + public ItemInstanceDTO ItemInstance { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Inventory/CharacterPartnerInventoryItemDto.cs b/srcs/WingsAPI.Data/Inventory/CharacterPartnerInventoryItemDto.cs new file mode 100644 index 0000000..0ee01b6 --- /dev/null +++ b/srcs/WingsAPI.Data/Inventory/CharacterPartnerInventoryItemDto.cs @@ -0,0 +1,17 @@ +using ProtoBuf; +using WingsEmu.DTOs.Items; + +namespace WingsEmu.DTOs.Inventory; + +/// +/// Composite object +/// +[ProtoContract] +public class CharacterPartnerInventoryItemDto +{ + [ProtoMember(1)] + public short PartnerSlot { get; set; } + + [ProtoMember(2)] + public ItemInstanceDTO ItemInstance { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Items/EquipmentOptionDTO.cs b/srcs/WingsAPI.Data/Items/EquipmentOptionDTO.cs new file mode 100644 index 0000000..f00db3d --- /dev/null +++ b/srcs/WingsAPI.Data/Items/EquipmentOptionDTO.cs @@ -0,0 +1,29 @@ +// WingsEmu +// +// Developed by NosWings Team + +using ProtoBuf; + +namespace WingsEmu.DTOs.Items; + +[ProtoContract] +public class EquipmentOptionDTO +{ + [ProtoMember(1)] + public EquipmentOptionType EquipmentOptionType { get; set; } + + [ProtoMember(2)] + public int EffectVnum { get; set; } + + [ProtoMember(3)] + public int Level { get; set; } + + [ProtoMember(4)] + public byte Type { get; set; } + + [ProtoMember(5)] + public int Value { get; set; } + + [ProtoMember(6)] + public byte Weight { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Items/EquipmentOptionType.cs b/srcs/WingsAPI.Data/Items/EquipmentOptionType.cs new file mode 100644 index 0000000..f4ea979 --- /dev/null +++ b/srcs/WingsAPI.Data/Items/EquipmentOptionType.cs @@ -0,0 +1,10 @@ +namespace WingsEmu.DTOs.Items; + +public enum EquipmentOptionType +{ + NONE, + JEWELS, + WEAPON_SHELL, + ARMOR_SHELL, + RUNE +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Items/ItemDTO.cs b/srcs/WingsAPI.Data/Items/ItemDTO.cs new file mode 100644 index 0000000..1dab81d --- /dev/null +++ b/srcs/WingsAPI.Data/Items/ItemDTO.cs @@ -0,0 +1,192 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using PhoenixLib.DAL; +using WingsAPI.Packets.Enums.Shells; +using WingsEmu.DTOs.BCards; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Battle; + +namespace WingsEmu.DTOs.Items; + +public class ItemDTO : IIntDto +{ + public byte BasicUpgrade { get; set; } + + public byte CellonLvl { get; set; } + + public byte Class { get; set; } + + public short CloseDefence { get; set; } + + public byte Color { get; set; } + + public short Concentrate { get; set; } + + public sbyte CriticalLuckRate { get; set; } + + public short CriticalRate { get; set; } + + public short DamageMaximum { get; set; } + + public short DamageMinimum { get; set; } + + public byte DarkElement { get; set; } + + public short DarkResistance { get; set; } + + public short DefenceDodge { get; set; } + + public short DistanceDefence { get; set; } + + public short DistanceDefenceDodge { get; set; } + + public short Effect { get; set; } + + public int EffectValue { get; set; } + + public byte Element { get; set; } + + public short ElementRate { get; set; } + + public EquipmentType EquipmentSlot { get; set; } + + public byte FireElement { get; set; } + + public short FireResistance { get; set; } + + public byte Height { get; set; } + + public short HitRate { get; set; } + + public short Hp { get; set; } + + public short HpRegeneration { get; set; } + + public bool IsMinilandActionable { get; set; } + + public bool IsColorable { get; set; } + + public bool IsTimeSpaceRewardBox { get; set; } + + public bool ShowDescriptionOnHover { get; set; } + + public bool Flag3 { get; set; } + + public bool FollowMouseOnUse { get; set; } + + public bool ShowSomethingOnHover { get; set; } + + public bool PlaySoundOnPickup { get; set; } + + public bool Flag7 { get; set; } + + public bool IsLimited { get; set; } + + public bool IsConsumable { get; set; } + + public bool IsDroppable { get; set; } + + public bool IsHeroic { get; set; } + + public bool ShowWarningOnUse { get; set; } + + public bool IsWarehouse { get; set; } + + public bool IsSoldable { get; set; } + + public bool IsTradable { get; set; } + + public byte ItemSubType { get; set; } + + public ItemType ItemType { get; set; } + + public long ItemValidTime { get; set; } + + public byte LevelJobMinimum { get; set; } + + public byte LevelMinimum { get; set; } + + public byte LightElement { get; set; } + + public short LightResistance { get; set; } + + public short MagicDefence { get; set; } + + public byte MaxCellon { get; set; } + + public byte MaxCellonLvl { get; set; } + + public short MaxElementRate { get; set; } + + public byte MaximumAmmo { get; set; } + + public int MinilandObjectPoint { get; set; } + + public short MoreHp { get; set; } + + public short MoreMp { get; set; } + + public short Morph { get; set; } + + public short Mp { get; set; } + + public short MpRegeneration { get; set; } + + public string Name { get; set; } + + public long Price { get; set; } + + public byte ReputationMinimum { get; set; } + + public long ReputPrice { get; set; } + + public byte Sex { get; set; } + + public byte Speed { get; set; } + + public byte SpPointsUsage { get; set; } + + public InventoryType Type { get; set; } + + public short WaitDelay { get; set; } + + public byte WaterElement { get; set; } + + public short WaterResistance { get; set; } + + public byte Width { get; set; } + + public AttackType AttackType { get; set; } + + public bool UseReputationAsPrice { get; set; } + + public byte PartnerClass { get; set; } + + public bool IsPartnerSpecialist { get; set; } + + public byte SpMorphId { get; set; } + + public short ItemLeftType { get; set; } + + public int LeftUsages { get; set; } + + public int IconId { get; set; } + + public short ShellMinimumLevel { get; set; } + + public short ShellMaximumLevel { get; set; } + + public ShellType ShellType { get; set; } + + public int[] Data { get; set; } + public List BCards { get; set; } = new(); + + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public int Id { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Items/ItemInstanceDTO.cs b/srcs/WingsAPI.Data/Items/ItemInstanceDTO.cs new file mode 100644 index 0000000..644293a --- /dev/null +++ b/srcs/WingsAPI.Data/Items/ItemInstanceDTO.cs @@ -0,0 +1,181 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Generic; +using PhoenixLib.DAL; +using ProtoBuf; +using WingsEmu.DTOs.Skills; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.DTOs.Items; + +[ProtoContract] +public class ItemInstanceDTO : IDto +{ + [ProtoMember(1)] + public int Amount { get; set; } + + [ProtoMember(2)] + public long? BoundCharacterId { get; set; } + + [ProtoMember(3)] + public short Design { get; set; } + + [ProtoMember(4)] + public int DurabilityPoint { get; set; } + + [ProtoMember(5)] + public DateTime? ItemDeleteTime { get; set; } + + [ProtoMember(6)] + public int ItemVNum { get; set; } + + [ProtoMember(7)] + public short Rarity { get; set; } + + [ProtoMember(8)] + public byte Upgrade { get; set; } + + [ProtoMember(9)] + public byte Ammo { get; set; } + + [ProtoMember(10)] + public byte Cellon { get; set; } + + [ProtoMember(16)] + public short DarkResistance { get; set; } + + [ProtoMember(17)] + public short ElementRate { get; set; } + + [ProtoMember(18)] + public short FireResistance { get; set; } + + [ProtoMember(19)] + public bool IsEmpty { get; set; } + + [ProtoMember(20)] + public bool IsFixed { get; set; } + + [ProtoMember(21)] + public short LightResistance { get; set; } + + [ProtoMember(22)] + public short? ShellRarity { get; set; } + + [ProtoMember(23)] + public short WaterResistance { get; set; } + + [ProtoMember(24)] + public long Xp { get; set; } + + [ProtoMember(25)] + public byte Agility { get; set; } + + [ProtoMember(26)] + public bool PartnerSkill1 { get; set; } + + [ProtoMember(27)] + public bool PartnerSkill2 { get; set; } + + [ProtoMember(28)] + public bool PartnerSkill3 { get; set; } + + [ProtoMember(29)] + public byte SkillRank1 { get; set; } + + [ProtoMember(30)] + public byte SkillRank2 { get; set; } + + [ProtoMember(31)] + public byte SkillRank3 { get; set; } + + [ProtoMember(32)] + public List EquipmentOptions { get; set; } + + [ProtoMember(33)] + public short SlDamage { get; set; } + + [ProtoMember(34)] + public short SlDefence { get; set; } + + [ProtoMember(35)] + public short SlElement { get; set; } + + [ProtoMember(36)] + public short SlHP { get; set; } + + [ProtoMember(37)] + public byte SpDamage { get; set; } + + [ProtoMember(38)] + public byte SpDark { get; set; } + + [ProtoMember(39)] + public byte SpDefence { get; set; } + + [ProtoMember(40)] + public byte SpElement { get; set; } + + [ProtoMember(41)] + public byte SpFire { get; set; } + + [ProtoMember(42)] + public byte SpHP { get; set; } + + [ProtoMember(43)] + public byte SpLevel { get; set; } + + [ProtoMember(44)] + public byte SpLight { get; set; } + + [ProtoMember(45)] + public byte SpStoneUpgrade { get; set; } + + [ProtoMember(46)] + public byte SpWater { get; set; } + + [ProtoMember(47)] + public List PartnerSkills { get; set; } + + [ProtoMember(48)] + public int? HoldingVNum { get; set; } + + [ProtoMember(49)] + public MateType? MateType { get; set; } + + [ProtoMember(50)] + public bool IsLimitedMatePearl { get; set; } + + [ProtoMember(80)] + public ItemInstanceType Type { get; set; } + + [ProtoMember(81)] + public int WeaponMinDamageAdditionalValue { get; set; } + + [ProtoMember(82)] + public int WeaponMaxDamageAdditionalValue { get; set; } + + [ProtoMember(83)] + public int WeaponHitRateAdditionalValue { get; set; } + + [ProtoMember(84)] + public int ArmorDodgeAdditionalValue { get; set; } + + [ProtoMember(85)] + public int ArmorRangeAdditionalValue { get; set; } + + [ProtoMember(86)] + public int ArmorMagicAdditionalValue { get; set; } + + [ProtoMember(87)] + public int ArmorMeleeAdditionalValue { get; set; } + + [ProtoMember(90)] + public Guid? SerialTracker { get; set; } + + [ProtoMember(100)] + public int? OriginalItemVnum { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Items/ItemInstanceType.cs b/srcs/WingsAPI.Data/Items/ItemInstanceType.cs new file mode 100644 index 0000000..6d817b3 --- /dev/null +++ b/srcs/WingsAPI.Data/Items/ItemInstanceType.cs @@ -0,0 +1,9 @@ +namespace WingsEmu.DTOs.Items; + +public enum ItemInstanceType +{ + NORMAL_ITEM, + WearableInstance, + SpecialistInstance, + BoxInstance +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Mails/AccountMailDto.cs b/srcs/WingsAPI.Data/Mails/AccountMailDto.cs new file mode 100644 index 0000000..fce2e8c --- /dev/null +++ b/srcs/WingsAPI.Data/Mails/AccountMailDto.cs @@ -0,0 +1,21 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using PhoenixLib.DAL; + +namespace WingsEmu.DTOs.Mails; + +public class AccountMailDto : ILongDto +{ + public long AccountId { get; set; } + + public DateTime Date { get; set; } + + public int ItemVnum { get; set; } + + public short Amount { get; set; } + + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long Id { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Mails/CharacterMailDTO.cs b/srcs/WingsAPI.Data/Mails/CharacterMailDTO.cs new file mode 100644 index 0000000..74babf1 --- /dev/null +++ b/srcs/WingsAPI.Data/Mails/CharacterMailDTO.cs @@ -0,0 +1,36 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using PhoenixLib.DAL; +using ProtoBuf; +using WingsEmu.DTOs.Items; + +namespace WingsEmu.DTOs.Mails; + +[ProtoContract] +public class CharacterMailDto : ILongDto +{ + [ProtoMember(2)] + public DateTime Date { get; set; } + + [ProtoMember(3)] + public string SenderName { get; set; } + + [ProtoMember(4)] + public long ReceiverId { get; set; } + + [ProtoMember(5)] + public MailGiftType MailGiftType { get; set; } + + [ProtoMember(6)] + public ItemInstanceDTO ItemInstance { get; set; } + + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + [ProtoMember(1)] + public long Id { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Mails/CharacterNoteDto.cs b/srcs/WingsAPI.Data/Mails/CharacterNoteDto.cs new file mode 100644 index 0000000..6fe153c --- /dev/null +++ b/srcs/WingsAPI.Data/Mails/CharacterNoteDto.cs @@ -0,0 +1,60 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using PhoenixLib.DAL; +using ProtoBuf; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Character; + +namespace WingsEmu.DTOs.Mails; + +[ProtoContract] +public class CharacterNoteDto : ILongDto +{ + [ProtoMember(2)] + public DateTime Date { get; set; } + + [ProtoMember(3)] + public long SenderId { get; set; } + + [ProtoMember(4)] + public long ReceiverId { get; set; } + + [ProtoMember(5)] + public string Title { get; set; } + + [ProtoMember(6)] + public string Message { get; set; } + + [ProtoMember(7)] + public string EquipmentPackets { get; set; } + + [ProtoMember(8)] + public bool IsSenderCopy { get; set; } + + [ProtoMember(9)] + public bool IsOpened { get; set; } + + [ProtoMember(10)] + public GenderType SenderGender { get; set; } + + [ProtoMember(11)] + public ClassType SenderClass { get; set; } + + [ProtoMember(12)] + public HairColorType SenderHairColor { get; set; } + + [ProtoMember(13)] + public HairStyleType SenderHairStyle { get; set; } + + [ProtoMember(14)] + public string SenderName { get; set; } + + [ProtoMember(15)] + public string ReceiverName { get; set; } + + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + [ProtoMember(1)] + public long Id { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Mails/IAccountMailDao.cs b/srcs/WingsAPI.Data/Mails/IAccountMailDao.cs new file mode 100644 index 0000000..07e7a3b --- /dev/null +++ b/srcs/WingsAPI.Data/Mails/IAccountMailDao.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using PhoenixLib.DAL; + +namespace WingsEmu.DTOs.Mails; + +public interface IAccountMailDao : IGenericAsyncLongRepository +{ + Task> GetByAccountIdAsync(long accountId); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Mails/ICharacterMailDao.cs b/srcs/WingsAPI.Data/Mails/ICharacterMailDao.cs new file mode 100644 index 0000000..8b18053 --- /dev/null +++ b/srcs/WingsAPI.Data/Mails/ICharacterMailDao.cs @@ -0,0 +1,14 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using System.Threading.Tasks; +using PhoenixLib.DAL; + +namespace WingsEmu.DTOs.Mails; + +public interface ICharacterMailDao : IGenericAsyncLongRepository +{ + Task> GetByCharacterIdAsync(long characterId); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Mails/ICharacterNoteDao.cs b/srcs/WingsAPI.Data/Mails/ICharacterNoteDao.cs new file mode 100644 index 0000000..336cf07 --- /dev/null +++ b/srcs/WingsAPI.Data/Mails/ICharacterNoteDao.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using PhoenixLib.DAL; + +namespace WingsEmu.DTOs.Mails; + +public interface ICharacterNoteDao : IGenericAsyncLongRepository +{ + Task> GetByCharacterIdAsync(long characterId); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Mails/MailGiftType.cs b/srcs/WingsAPI.Data/Mails/MailGiftType.cs new file mode 100644 index 0000000..a01b915 --- /dev/null +++ b/srcs/WingsAPI.Data/Mails/MailGiftType.cs @@ -0,0 +1,7 @@ +namespace WingsEmu.DTOs.Mails; + +public enum MailGiftType +{ + ItemShop = 1, + Normal = 4 +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Maps/MapDataDTO.cs b/srcs/WingsAPI.Data/Maps/MapDataDTO.cs new file mode 100644 index 0000000..4923d33 --- /dev/null +++ b/srcs/WingsAPI.Data/Maps/MapDataDTO.cs @@ -0,0 +1,16 @@ +// WingsEmu +// +// Developed by NosWings Team + +using PhoenixLib.DAL; + +namespace WingsEmu.DTOs.Maps; + +public class MapDataDTO : IIntDto +{ + public short Height { get; set; } + public short Width { get; set; } + public byte[] Grid { get; set; } + public string Name { get; set; } + public int Id { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Maps/MapFlags.cs b/srcs/WingsAPI.Data/Maps/MapFlags.cs new file mode 100644 index 0000000..2af1c0f --- /dev/null +++ b/srcs/WingsAPI.Data/Maps/MapFlags.cs @@ -0,0 +1,42 @@ +namespace WingsEmu.DTOs.Maps; + +public enum MapFlags +{ + /* ACTS */ + ACT_1, + ACT_2, + ACT_3, + ACT_4, + ACT_5_1, + ACT_5_2, + ACT_6_1, + ACT_6_2, + ACT_7, + + /* FACTION */ + ANGEL_SIDE = 30, + DEMON_SIDE, + + NOSVILLE = 40, + PORT_ALVEUS, + + /* TYPES */ + IS_BASE_MAP = 50, + IS_MINILAND_MAP, + + /* FLAGS */ + HAS_PVP_ENABLED = 100, + HAS_PVP_FACTION_ENABLED, + HAS_PVP_FAMILY_ENABLED, + HAS_USER_SHOPS_DISABLED, + HAS_DROP_DIRECTLY_IN_INVENTORY_ENABLED, + HAS_CHAMPION_EXPERIENCE_ENABLED, + HAS_SEALED_VESSELS_DISABLED, + HAS_RAID_TEAM_SUMMON_STONE_ENABLED, + HAS_PVE_REPUTATION_ENABLED, + HAS_SIGNPOSTS_ENABLED, + + /* DEBUFFS */ + HAS_IMMUNITY_ON_MAP_CHANGE_ENABLED = 200, + HAS_BURNING_SWORD_ENABLED +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Maps/MapMonsterDTO.cs b/srcs/WingsAPI.Data/Maps/MapMonsterDTO.cs new file mode 100644 index 0000000..0f3fd1c --- /dev/null +++ b/srcs/WingsAPI.Data/Maps/MapMonsterDTO.cs @@ -0,0 +1,27 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using PhoenixLib.DAL; + +namespace WingsEmu.DTOs.Maps; + +public class MapMonsterDTO : IIntDto +{ + public bool IsMoving { get; set; } + + public int MapId { get; set; } + + public short MapX { get; set; } + + public short MapY { get; set; } + + public int MonsterVNum { get; set; } + public byte Direction { get; set; } + + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Maps/MapNpcDTO.cs b/srcs/WingsAPI.Data/Maps/MapNpcDTO.cs new file mode 100644 index 0000000..3c2aa08 --- /dev/null +++ b/srcs/WingsAPI.Data/Maps/MapNpcDTO.cs @@ -0,0 +1,32 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using PhoenixLib.DAL; + +namespace WingsEmu.DTOs.Maps; + +public class MapNpcDTO : IIntDto +{ + public short Dialog { get; set; } + public int? QuestDialog { get; set; } + public short Effect { get; set; } + public short EffectDelay { get; set; } + public bool IsDisabled { get; set; } + public bool IsMoving { get; set; } + public bool IsSitting { get; set; } + public int MapId { get; set; } + public short MapX { get; set; } + public short MapY { get; set; } + public int NpcVNum { get; set; } + public byte Direction { get; set; } + public bool CanAttack { get; set; } + public bool HasGodMode { get; set; } + public string CustomName { get; set; } + + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public int Id { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Maps/ServerMapDto.cs b/srcs/WingsAPI.Data/Maps/ServerMapDto.cs new file mode 100644 index 0000000..c08225a --- /dev/null +++ b/srcs/WingsAPI.Data/Maps/ServerMapDto.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using PhoenixLib.DAL; + +namespace WingsEmu.DTOs.Maps; + +public class ServerMapDto : IIntDto +{ + public int MapVnum { get; set; } + + public int NameId { get; set; } + + public int MusicId { get; set; } + + public int AmbientId { get; set; } + + public List Flags { get; set; } + + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public int Id { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Mates/MateDTO.cs b/srcs/WingsAPI.Data/Mates/MateDTO.cs new file mode 100644 index 0000000..a36dd73 --- /dev/null +++ b/srcs/WingsAPI.Data/Mates/MateDTO.cs @@ -0,0 +1,82 @@ +// WingsEmu +// +// Developed by NosWings Team + +using PhoenixLib.DAL; +using ProtoBuf; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.DTOs.Mates; + +[ProtoContract] +public class MateDTO : ILongDto +{ + [ProtoMember(2)] + public byte Attack { get; set; } + + [ProtoMember(3)] + public bool CanPickUp { get; set; } + + [ProtoMember(4)] + public long CharacterId { get; set; } + + [ProtoMember(5)] + public byte Defence { get; set; } + + [ProtoMember(6)] + public byte Direction { get; set; } + + [ProtoMember(7)] + public long Experience { get; set; } + + [ProtoMember(8)] + public int Hp { get; set; } + + [ProtoMember(9)] + public bool IsSummonable { get; set; } + + [ProtoMember(10)] + public bool IsTeamMember { get; set; } + + [ProtoMember(11)] + public byte Level { get; set; } + + [ProtoMember(12)] + public short Loyalty { get; set; } + + [ProtoMember(13)] + public short MapX { get; set; } + + [ProtoMember(14)] + public short MapY { get; set; } + + [ProtoMember(15)] + public MateType MateType { get; set; } + + [ProtoMember(16)] + public int Mp { get; set; } + + [ProtoMember(17)] + public string MateName { get; set; } + + [ProtoMember(18)] + public int NpcMonsterVNum { get; set; } + + [ProtoMember(19)] + public short Skin { get; set; } + + [ProtoMember(20)] + public byte PetSlot { get; set; } + + [ProtoMember(21)] + public short MinilandX { get; set; } + + [ProtoMember(22)] + public short MinilandY { get; set; } + + [ProtoMember(23)] + public bool IsLimited { get; set; } + + [ProtoMember(1)] + public long Id { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Miniland/CharacterMinilandObjectDto.cs b/srcs/WingsAPI.Data/Miniland/CharacterMinilandObjectDto.cs new file mode 100644 index 0000000..0a869cc --- /dev/null +++ b/srcs/WingsAPI.Data/Miniland/CharacterMinilandObjectDto.cs @@ -0,0 +1,39 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using ProtoBuf; + +namespace WingsAPI.Data.Miniland; + +[ProtoContract] +public class CharacterMinilandObjectDto +{ + [ProtoMember(1)] + public Guid Id { get; set; } + + [ProtoMember(2)] + public short InventorySlot { get; set; } + + [ProtoMember(3)] + public byte Level1BoxAmount { get; set; } + + [ProtoMember(4)] + public byte Level2BoxAmount { get; set; } + + [ProtoMember(5)] + public byte Level3BoxAmount { get; set; } + + [ProtoMember(6)] + public byte Level4BoxAmount { get; set; } + + [ProtoMember(7)] + public byte Level5BoxAmount { get; set; } + + [ProtoMember(8)] + public short MapX { get; set; } + + [ProtoMember(9)] + public short MapY { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/NpcMonster/NpcMonsterDTO.cs b/srcs/WingsAPI.Data/NpcMonster/NpcMonsterDTO.cs new file mode 100644 index 0000000..961fd2e --- /dev/null +++ b/srcs/WingsAPI.Data/NpcMonster/NpcMonsterDTO.cs @@ -0,0 +1,210 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using PhoenixLib.DAL; +using WingsAPI.Data.Drops; +using WingsEmu.DTOs.BCards; +using WingsEmu.DTOs.Skills; +using WingsEmu.Packets.Enums.Battle; + +namespace WingsEmu.DTOs.NpcMonster; + +public class NpcMonsterDto : IIntDto +{ + public short AmountRequired { get; set; } + + public AttackType AttackType { get; set; } + + public byte AttackUpgrade { get; set; } + + public byte BasicCastTime { get; set; } + + public short BasicCooldown { get; set; } + + public byte BasicRange { get; set; } + + public short BasicDashSpeed { get; set; } + + public short AttackEffect { get; set; } + + public short PermanentEffect { get; set; } + + public short DeathEffect { get; set; } + + public short BasicHitChance { get; set; } + + public short CloseDefence { get; set; } + + public short Concentrate { get; set; } + + public short CriticalChance { get; set; } + + public short CriticalRate { get; set; } + + public int DamageMaximum { get; set; } + + public int DamageMinimum { get; set; } + + public short DarkResistance { get; set; } + + public short DefenceDodge { get; set; } + + public byte DefenceUpgrade { get; set; } + + public short DistanceDefence { get; set; } + + public short DistanceDefenceDodge { get; set; } + + public byte Element { get; set; } + + public short ElementRate { get; set; } + + public short FireResistance { get; set; } + + public int HostilityType { get; set; } + + public int GroupAttack { get; set; } + + public int JobXp { get; set; } + + public byte Level { get; set; } + + public short LightResistance { get; set; } + + public short MagicDefence { get; set; } + + public int MaxHp { get; set; } + + public int MaxMp { get; set; } + + public string Name { get; set; } + + public byte NoticeRange { get; set; } + + public byte Race { get; set; } + + public byte RaceType { get; set; } + + public int RespawnTime { get; set; } + + public byte Speed { get; set; } + + public short VNumRequired { get; set; } + + public short WaterResistance { get; set; } + + public int Xp { get; set; } + + public bool IsPercent { get; set; } + + public int TakeDamages { get; set; } + + public int GiveDamagePercentage { get; set; } + + public int IconId { get; set; } + + public int SpawnMobOrColor { get; set; } + + public int SpriteSize { get; set; } + + public int CellSize { get; set; } + + public short MeleeHpFactor { get; set; } + + public short RangeDodgeFactor { get; set; } + + public short MagicMpFactor { get; set; } + + public bool CanWalk { get; set; } + + public bool CanBeCollected { get; set; } + + public bool CanBeDebuffed { get; set; } + + public bool CanBeCaught { get; set; } + + public bool DisappearAfterSeconds { get; set; } + + public bool DisappearAfterHitting { get; set; } + + public bool HasMode { get; set; } + + public bool DisappearAfterSecondsMana { get; set; } + + public bool OnDefenseOnlyOnce { get; set; } + + public bool CanRegenMp { get; set; } + + public bool CanBePushed { get; set; } + + public bool DamagedOnlyLastJajamaruSkill { get; set; } + + public sbyte MinimumAttackRange { get; set; } + + public byte WeaponLevel { get; set; } + + public byte ArmorLevel { get; set; } + + public byte WinfoValue { get; set; } + + public int CleanDamageMin { get; set; } + + public int CleanDamageMax { get; set; } + + public int CleanHitRate { get; set; } + + public int CleanMeleeDefence { get; set; } + + public int CleanRangeDefence { get; set; } + + public int CleanMagicDefence { get; set; } + + public int CleanDodge { get; set; } + + public int CleanHp { get; set; } + + public int CleanMp { get; set; } + + public byte MaxTries { get; set; } + + public short CollectionCooldown { get; set; } + + public byte CollectionDanceTime { get; set; } + + public bool TeleportRemoveFromInventory { get; set; } + + public bool ModeIsHpTriggered { get; set; } + + public byte ModeLimiterType { get; set; } + + public short ModeRangeTreshold { get; set; } + + public short ModeCModeVnum { get; set; } + + public short ModeHpTresholdOrItemVnum { get; set; } + + public short MidgardDamage { get; set; } + + public bool HasDash { get; set; } + + public int BaseXp { get; set; } + + public int BaseJobXp { get; set; } + + public bool DropToInventory { get; set; } + public List Drops { get; set; } = new(); + public List Skills { get; set; } = new(); + public List BCards { get; set; } = new(); + public List ModeBCards { get; set; } = new(); + + /// + /// VNUM in client files + /// + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public int Id { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Quests/CharacterQuestCompletedDto.cs b/srcs/WingsAPI.Data/Quests/CharacterQuestCompletedDto.cs new file mode 100644 index 0000000..d1e3f2e --- /dev/null +++ b/srcs/WingsAPI.Data/Quests/CharacterQuestCompletedDto.cs @@ -0,0 +1,9 @@ +using System; + +namespace WingsEmu.DTOs.Quests; + +public class CharacterQuestCompletedDto +{ + public int QuestId { get; set; } + public DateTime CompletionDate { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Quests/CharacterQuestDto.cs b/srcs/WingsAPI.Data/Quests/CharacterQuestDto.cs new file mode 100644 index 0000000..e60d258 --- /dev/null +++ b/srcs/WingsAPI.Data/Quests/CharacterQuestDto.cs @@ -0,0 +1,21 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using ProtoBuf; + +namespace WingsEmu.DTOs.Quests; + +[ProtoContract] +public class CharacterQuestDto +{ + [ProtoMember(1)] + public int QuestId { get; set; } + + [ProtoMember(2)] + public QuestSlotType SlotType { get; set; } + + [ProtoMember(3)] + public Dictionary ObjectiveAmount { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Quests/CharacterQuestGeneratedReward.cs b/srcs/WingsAPI.Data/Quests/CharacterQuestGeneratedReward.cs new file mode 100644 index 0000000..310bbdc --- /dev/null +++ b/srcs/WingsAPI.Data/Quests/CharacterQuestGeneratedReward.cs @@ -0,0 +1,7 @@ +namespace WingsEmu.DTOs.Quests; + +public class CharacterQuestGeneratedReward +{ + public int ItemVnum { get; set; } + public int Amount { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Quests/CharacterQuestObjectiveDto.cs b/srcs/WingsAPI.Data/Quests/CharacterQuestObjectiveDto.cs new file mode 100644 index 0000000..e83a967 --- /dev/null +++ b/srcs/WingsAPI.Data/Quests/CharacterQuestObjectiveDto.cs @@ -0,0 +1,13 @@ +using ProtoBuf; + +namespace WingsEmu.DTOs.Quests; + +[ProtoContract] +public class CharacterQuestObjectiveDto +{ + [ProtoMember(1)] + public int CurrentAmount { get; set; } + + [ProtoMember(2)] + public int RequiredAmount { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Quests/CompletedScriptsDto.cs b/srcs/WingsAPI.Data/Quests/CompletedScriptsDto.cs new file mode 100644 index 0000000..be39216 --- /dev/null +++ b/srcs/WingsAPI.Data/Quests/CompletedScriptsDto.cs @@ -0,0 +1,17 @@ +using System; +using ProtoBuf; + +namespace WingsEmu.DTOs.Quests; + +[ProtoContract] +public class CompletedScriptsDto +{ + [ProtoMember(1)] + public int ScriptId { get; set; } + + [ProtoMember(2)] + public int ScriptIndex { get; set; } + + [ProtoMember(3)] + public DateTime CompletedDate { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Quests/ITutorialDao.cs b/srcs/WingsAPI.Data/Quests/ITutorialDao.cs new file mode 100644 index 0000000..7a62dcb --- /dev/null +++ b/srcs/WingsAPI.Data/Quests/ITutorialDao.cs @@ -0,0 +1,7 @@ +using PhoenixLib.DAL; + +namespace WingsEmu.DTOs.Quests; + +public interface ITutorialDao : IGenericAsyncIntRepository +{ +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Quests/QuestDto.cs b/srcs/WingsAPI.Data/Quests/QuestDto.cs new file mode 100644 index 0000000..5279409 --- /dev/null +++ b/srcs/WingsAPI.Data/Quests/QuestDto.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using PhoenixLib.DAL; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.DTOs.Quests; + +public class QuestDto : IIntDto +{ + public string Name { get; set; } + public string Description { get; set; } + public bool AutoFinish { get; set; } + public int DialogStarting { get; set; } + public int DialogFinish { get; set; } + public int DialogDuring { get; set; } + public byte MinLevel { get; set; } + public byte MaxLevel { get; set; } + public int NextQuestId { get; set; } + public QuestType QuestType { get; set; } + public int RequiredQuestId { get; set; } + public int TalkerVnum { get; set; } + public short TargetMapId { get; set; } + public short TargetMapX { get; set; } + public short TargetMapY { get; set; } + public int Unknown1 { get; set; } + public bool IsBlue { get; set; } + public List Prizes { get; set; } = new(); + public List Objectives { get; set; } = new(); + + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public int Id { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Quests/QuestNpcDto.cs b/srcs/WingsAPI.Data/Quests/QuestNpcDto.cs new file mode 100644 index 0000000..fde5029 --- /dev/null +++ b/srcs/WingsAPI.Data/Quests/QuestNpcDto.cs @@ -0,0 +1,20 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using PhoenixLib.DAL; + +namespace WingsEmu.DTOs.Quests; + +public class QuestNpcDto : IIntDto +{ + public int? QuestId { get; set; } + public short NpcVnum { get; set; } + public short Level { get; set; } + public short StartingScript { get; set; } + public short RequiredCompletedScript { get; set; } + public bool IsMainQuest { get; set; } + public short MapId { get; set; } + + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Quests/QuestObjectiveDto.cs b/srcs/WingsAPI.Data/Quests/QuestObjectiveDto.cs new file mode 100644 index 0000000..f678f27 --- /dev/null +++ b/srcs/WingsAPI.Data/Quests/QuestObjectiveDto.cs @@ -0,0 +1,19 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using PhoenixLib.DAL; + +namespace WingsEmu.DTOs.Quests; + +public class QuestObjectiveDto : IIntDto +{ + public int QuestId { get; set; } + public int Data0 { get; set; } + public int Data1 { get; set; } + public int Data2 { get; set; } + public int Data3 { get; set; } + public byte ObjectiveIndex { get; set; } + + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Quests/QuestPrizeDto.cs b/srcs/WingsAPI.Data/Quests/QuestPrizeDto.cs new file mode 100644 index 0000000..94baf54 --- /dev/null +++ b/srcs/WingsAPI.Data/Quests/QuestPrizeDto.cs @@ -0,0 +1,24 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using PhoenixLib.DAL; + +namespace WingsEmu.DTOs.Quests; + +public class QuestPrizeDto : IIntDto +{ + public int QuestId { get; set; } + public byte RewardType { get; set; } + public int Data0 { get; set; } + public int Data1 { get; set; } + public int Data2 { get; set; } + public int Data3 { get; set; } + public int Data4 { get; set; } + + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Quests/QuestSlotType.cs b/srcs/WingsAPI.Data/Quests/QuestSlotType.cs new file mode 100644 index 0000000..044ec33 --- /dev/null +++ b/srcs/WingsAPI.Data/Quests/QuestSlotType.cs @@ -0,0 +1,8 @@ +namespace WingsEmu.DTOs.Quests; + +public enum QuestSlotType +{ + MAIN, + SECONDARY, // Blue + GENERAL // Yellow +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Quests/TutorialActionType.cs b/srcs/WingsAPI.Data/Quests/TutorialActionType.cs new file mode 100644 index 0000000..b5bd580 --- /dev/null +++ b/srcs/WingsAPI.Data/Quests/TutorialActionType.cs @@ -0,0 +1,16 @@ +namespace WingsEmu.DTOs.Quests; + +public enum TutorialActionType +{ + NONE, + TALK, + START_QUEST, + WEB_DISPLAY, + WAIT_FOR_QUEST_COMPLETION, + OPEN_WINDOW, + WAIT_FOR_REWARDS_CLAIM, + RUN, + DELAY, + SHOW_TARGET, + REMOVE_TARGET +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Quests/TutorialDto.cs b/srcs/WingsAPI.Data/Quests/TutorialDto.cs new file mode 100644 index 0000000..8c4fc44 --- /dev/null +++ b/srcs/WingsAPI.Data/Quests/TutorialDto.cs @@ -0,0 +1,20 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using PhoenixLib.DAL; + +namespace WingsEmu.DTOs.Quests; + +public class TutorialDto : IIntDto +{ + public int Data { get; set; } + public int ScriptId { get; set; } + public int ScriptIndex { get; set; } + public TutorialActionType Type { get; set; } + + /// + /// Generated by parser + /// + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Quicklist/CharacterQuicklistEntryDto.cs b/srcs/WingsAPI.Data/Quicklist/CharacterQuicklistEntryDto.cs new file mode 100644 index 0000000..9151b1e --- /dev/null +++ b/srcs/WingsAPI.Data/Quicklist/CharacterQuicklistEntryDto.cs @@ -0,0 +1,45 @@ +// WingsEmu +// +// Developed by NosWings Team + +using ProtoBuf; + +namespace WingsEmu.DTOs.Quicklist; + +[ProtoContract] +public class CharacterQuicklistEntryDto +{ + [ProtoMember(1)] + public short Morph { get; set; } + + [ProtoMember(2)] + public short InvSlotOrSkillSlotOrSkillVnum { get; set; } + + [ProtoMember(3)] + public short QuicklistTab { get; set; } + + [ProtoMember(4)] + public short QuicklistSlot { get; set; } + + /// + /// SkillTabs: + /// 0 => passive + /// 1 => skills + /// 2 => upgraded skills + /// 3 => motion/emotes + /// + [ProtoMember(5)] + public short InventoryTypeOrSkillTab { get; set; } + + [ProtoMember(6)] + public QuicklistType Type { get; set; } + + [ProtoMember(7)] + public short? SkillVnum { get; set; } +} + +public enum QuicklistType +{ + ITEM = 0, + SKILLS = 1 +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Recipes/RecipeDTO.cs b/srcs/WingsAPI.Data/Recipes/RecipeDTO.cs new file mode 100644 index 0000000..769b913 --- /dev/null +++ b/srcs/WingsAPI.Data/Recipes/RecipeDTO.cs @@ -0,0 +1,24 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using PhoenixLib.DAL; + +namespace WingsEmu.DTOs.Recipes; + +public class RecipeDTO : IIntDto +{ + public int Amount { get; set; } + public int? ProducerMapNpcId { get; set; } + public int ProducedItemVnum { get; set; } + public int? ProducerItemVnum { get; set; } + public int? ProducerNpcVnum { get; set; } + public List Items { get; set; } + + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Recipes/RecipeItemDTO.cs b/srcs/WingsAPI.Data/Recipes/RecipeItemDTO.cs new file mode 100644 index 0000000..b0e7f65 --- /dev/null +++ b/srcs/WingsAPI.Data/Recipes/RecipeItemDTO.cs @@ -0,0 +1,14 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.DTOs.Recipes; + +public class RecipeItemDTO +{ + public short Amount { get; set; } + + public short Slot { get; set; } + + public short ItemVNum { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Relations/CharacterRelationDTO.cs b/srcs/WingsAPI.Data/Relations/CharacterRelationDTO.cs new file mode 100644 index 0000000..6109dc7 --- /dev/null +++ b/srcs/WingsAPI.Data/Relations/CharacterRelationDTO.cs @@ -0,0 +1,24 @@ +// WingsEmu +// +// Developed by NosWings Team + +using ProtoBuf; +using WingsEmu.Packets.Enums.Relations; + +namespace WingsEmu.DTOs.Relations; + +[ProtoContract] +public class CharacterRelationDTO +{ + [ProtoMember(1)] + public long CharacterId { get; set; } + + [ProtoMember(2)] + public long RelatedCharacterId { get; set; } + + [ProtoMember(3)] + public string RelatedName { get; set; } + + [ProtoMember(4)] + public CharacterRelationType RelationType { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Relations/ICharacterRelationDAO.cs b/srcs/WingsAPI.Data/Relations/ICharacterRelationDAO.cs new file mode 100644 index 0000000..60b42a8 --- /dev/null +++ b/srcs/WingsAPI.Data/Relations/ICharacterRelationDAO.cs @@ -0,0 +1,16 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace WingsEmu.DTOs.Relations; + +public interface ICharacterRelationDAO +{ + Task GetRelationByCharacterIdAsync(long characterId, long targetId); + Task SaveRelationsByCharacterIdAsync(long characterId, CharacterRelationDTO relations); + Task> LoadRelationsByCharacterIdAsync(long characterId); + Task RemoveRelationAsync(CharacterRelationDTO relation); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Respawns/CharacterReturnDto.cs b/srcs/WingsAPI.Data/Respawns/CharacterReturnDto.cs new file mode 100644 index 0000000..91f3e0d --- /dev/null +++ b/srcs/WingsAPI.Data/Respawns/CharacterReturnDto.cs @@ -0,0 +1,16 @@ +using ProtoBuf; + +namespace WingsEmu.DTOs.Respawns; + +[ProtoContract] +public class CharacterReturnDto +{ + [ProtoMember(1)] + public short MapId { get; set; } + + [ProtoMember(2)] + public short MapX { get; set; } + + [ProtoMember(3)] + public short MapY { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/ServerDatas/ItemBoxDto.cs b/srcs/WingsAPI.Data/ServerDatas/ItemBoxDto.cs new file mode 100644 index 0000000..c986f76 --- /dev/null +++ b/srcs/WingsAPI.Data/ServerDatas/ItemBoxDto.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using PhoenixLib.DAL; + +namespace WingsEmu.DTOs.ServerDatas; + +public class ItemBoxDto : IIntDto +{ + public ItemBoxType ItemBoxType { get; set; } + + /// + /// If not set => 1 + /// + public int? MinimumRewards { get; set; } + + /// + /// If not set => 1 + /// + public int? MaximumRewards { get; set; } + + public bool ShowsRaidBoxPanelOnOpen { get; set; } + + public List Items { get; set; } + + /// + /// ItemVnum + /// + public int Id { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/ServerDatas/ItemBoxItemDto.cs b/srcs/WingsAPI.Data/ServerDatas/ItemBoxItemDto.cs new file mode 100644 index 0000000..16b507a --- /dev/null +++ b/srcs/WingsAPI.Data/ServerDatas/ItemBoxItemDto.cs @@ -0,0 +1,18 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.DTOs.ServerDatas; + +public class ItemBoxItemDto +{ + public short Probability { get; set; } + + public short MinimumOriginalItemRare { get; set; } + public short MaximumOriginalItemRare { get; set; } + + public short ItemGeneratedAmount { get; set; } + public int ItemGeneratedVNum { get; set; } + public bool ItemGeneratedRandomRarity { get; set; } + public byte ItemGeneratedUpgrade { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/ServerDatas/ItemBoxType.cs b/srcs/WingsAPI.Data/ServerDatas/ItemBoxType.cs new file mode 100644 index 0000000..4ac0ccd --- /dev/null +++ b/srcs/WingsAPI.Data/ServerDatas/ItemBoxType.cs @@ -0,0 +1,7 @@ +namespace WingsEmu.DTOs.ServerDatas; + +public enum ItemBoxType +{ + BUNDLE, // a bundle of items + RANDOM_PICK +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/ServerDatas/PortalDTO.cs b/srcs/WingsAPI.Data/ServerDatas/PortalDTO.cs new file mode 100644 index 0000000..d43a725 --- /dev/null +++ b/srcs/WingsAPI.Data/ServerDatas/PortalDTO.cs @@ -0,0 +1,36 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using PhoenixLib.DAL; + +namespace WingsEmu.DTOs.ServerDatas; + +public class PortalDTO : IIntDto +{ + public int DestinationMapId { get; set; } + + public short DestinationX { get; set; } + + public short DestinationY { get; set; } + + public bool IsDisabled { get; set; } + + public int SourceMapId { get; set; } + + public short SourceX { get; set; } + + public short SourceY { get; set; } + + public short Type { get; set; } + + public short? RaidType { get; set; } + + public short? MapNameId { get; set; } + + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/ServerDatas/TeleporterDTO.cs b/srcs/WingsAPI.Data/ServerDatas/TeleporterDTO.cs new file mode 100644 index 0000000..5680487 --- /dev/null +++ b/srcs/WingsAPI.Data/ServerDatas/TeleporterDTO.cs @@ -0,0 +1,29 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using PhoenixLib.DAL; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.DTOs.ServerDatas; + +public class TeleporterDTO : IIntDto +{ + public short Index { get; set; } + + public TeleporterType Type { get; set; } + + public int MapId { get; set; } + + public int MapNpcId { get; set; } + + public short MapX { get; set; } + + public short MapY { get; set; } + + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Shops/ShopDTO.cs b/srcs/WingsAPI.Data/Shops/ShopDTO.cs new file mode 100644 index 0000000..dcd7027 --- /dev/null +++ b/srcs/WingsAPI.Data/Shops/ShopDTO.cs @@ -0,0 +1,22 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using WingsEmu.DTOs.Shops; + +namespace WingsAPI.Data.Shops; + +public class ShopDTO +{ + public int MapNpcId { get; set; } + + public byte MenuType { get; set; } + + public string Name { get; set; } + + public byte ShopType { get; set; } + + public List Skills { get; set; } + public List Items { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Shops/ShopItemDTO.cs b/srcs/WingsAPI.Data/Shops/ShopItemDTO.cs new file mode 100644 index 0000000..fa4d923 --- /dev/null +++ b/srcs/WingsAPI.Data/Shops/ShopItemDTO.cs @@ -0,0 +1,22 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.DTOs.Shops; + +public class ShopItemDTO +{ + public short Slot { get; set; } + + public byte Color { get; set; } + + public int ItemVNum { get; set; } + + public short Rare { get; set; } + + public byte Type { get; set; } + + public byte Upgrade { get; set; } + + public int? Price { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Shops/ShopSkillDTO.cs b/srcs/WingsAPI.Data/Shops/ShopSkillDTO.cs new file mode 100644 index 0000000..c5940bc --- /dev/null +++ b/srcs/WingsAPI.Data/Shops/ShopSkillDTO.cs @@ -0,0 +1,14 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.DTOs.Shops; + +public class ShopSkillDTO +{ + public short SkillVNum { get; set; } + + public short Slot { get; set; } + + public byte Type { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Skills/CharacterSkillDTO.cs b/srcs/WingsAPI.Data/Skills/CharacterSkillDTO.cs new file mode 100644 index 0000000..4bb1365 --- /dev/null +++ b/srcs/WingsAPI.Data/Skills/CharacterSkillDTO.cs @@ -0,0 +1,14 @@ +// WingsEmu +// +// Developed by NosWings Team + +using ProtoBuf; + +namespace WingsEmu.DTOs.Skills; + +[ProtoContract] +public class CharacterSkillDTO +{ + [ProtoMember(1)] + public int SkillVNum { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Skills/ComboDTO.cs b/srcs/WingsAPI.Data/Skills/ComboDTO.cs new file mode 100644 index 0000000..02fb577 --- /dev/null +++ b/srcs/WingsAPI.Data/Skills/ComboDTO.cs @@ -0,0 +1,21 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using PhoenixLib.DAL; + +namespace WingsEmu.DTOs.Skills; + +public class ComboDTO : IIntDto +{ + public short Animation { get; set; } + public short Effect { get; set; } + public short Hit { get; set; } + public int SkillVNum { get; set; } + + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public int Id { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Skills/NpcMonsterSkillDTO.cs b/srcs/WingsAPI.Data/Skills/NpcMonsterSkillDTO.cs new file mode 100644 index 0000000..212d80d --- /dev/null +++ b/srcs/WingsAPI.Data/Skills/NpcMonsterSkillDTO.cs @@ -0,0 +1,29 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using PhoenixLib.DAL; + +namespace WingsEmu.DTOs.Skills; + +public class NpcMonsterSkillDTO : IIntDto +{ + public int NpcMonsterVNum { get; set; } + + public short Rate { get; set; } + + public short SkillVNum { get; set; } + + /// + /// Tells whether or not the skill is considered as a basic attack + /// + public bool IsBasicAttack { get; set; } + + public bool IsIgnoringHitChance { get; set; } + + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Skills/PartnerSkillDTO.cs b/srcs/WingsAPI.Data/Skills/PartnerSkillDTO.cs new file mode 100644 index 0000000..bc7d069 --- /dev/null +++ b/srcs/WingsAPI.Data/Skills/PartnerSkillDTO.cs @@ -0,0 +1,16 @@ +using ProtoBuf; + +namespace WingsEmu.DTOs.Skills; + +[ProtoContract] +public class PartnerSkillDTO +{ + [ProtoMember(1)] + public int SkillId { get; set; } + + [ProtoMember(2)] + public int Rank { get; set; } + + [ProtoMember(3)] + public byte Slot { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Skills/SkillDTO.cs b/srcs/WingsAPI.Data/Skills/SkillDTO.cs new file mode 100644 index 0000000..28c9e7c --- /dev/null +++ b/srcs/WingsAPI.Data/Skills/SkillDTO.cs @@ -0,0 +1,84 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using PhoenixLib.DAL; +using WingsEmu.DTOs.BCards; +using WingsEmu.Packets.Enums.Battle; + +namespace WingsEmu.DTOs.Skills; + +public class SkillDTO : IIntDto +{ + public short DashSpeed { get; set; } + public short SuAnimation { get; set; } + + public short CtAnimation { get; set; } + + public short SuEffect { get; set; } + + public short CtEffect { get; set; } + + public short CastId { get; set; } + + public short CastTime { get; set; } + + public byte Class { get; set; } + + public short Cooldown { get; set; } + + public byte CPCost { get; set; } + + public short Duration { get; set; } + + public byte Element { get; set; } + + public TargetHitType HitType { get; set; } + + public short ItemVNum { get; set; } + + public byte Level { get; set; } + + public byte LevelMinimum { get; set; } + + public byte MinimumAdventurerLevel { get; set; } + + public byte MinimumArcherLevel { get; set; } + + public byte MinimumMagicianLevel { get; set; } + + public byte MinimumSwordmanLevel { get; set; } + + public short MpCost { get; set; } + + public string Name { get; set; } + + public int Price { get; set; } + + public byte Range { get; set; } + + public SkillType SkillType { get; set; } + + public short AoERange { get; set; } + + public TargetType TargetType { get; set; } + + public AttackType AttackType { get; set; } + + /// + /// If == 2 (SkillUpgrade) - UpgradeSkill = Parent Upgrade Skill Vnum + /// + public short UpgradeSkill { get; set; } + + public short UpgradeType { get; set; } + + public int SpecialCost { get; set; } + + public TargetAffectedEntities TargetAffectedEntities { get; set; } + + public bool IsUsingSecondWeapon { get; set; } + public List BCards { get; set; } = new(); + public List Combos { get; set; } = new(); + public int Id { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Skills/SkillType.cs b/srcs/WingsAPI.Data/Skills/SkillType.cs new file mode 100644 index 0000000..9222869 --- /dev/null +++ b/srcs/WingsAPI.Data/Skills/SkillType.cs @@ -0,0 +1,11 @@ +namespace WingsEmu.DTOs.Skills; + +public enum SkillType : byte +{ + Passive = 0, + NormalPlayerSkill = 1, + SkillUpgrade = 2, + ActionOrEmote = 3, + MonsterSkill = 4, + PartnerSkill = 5 +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Skills/TargetAffectedEntities.cs b/srcs/WingsAPI.Data/Skills/TargetAffectedEntities.cs new file mode 100644 index 0000000..5f5a382 --- /dev/null +++ b/srcs/WingsAPI.Data/Skills/TargetAffectedEntities.cs @@ -0,0 +1,9 @@ +namespace WingsEmu.DTOs.Skills; + +public enum TargetAffectedEntities +{ + Enemies = 0, + DebuffForEnemies = 1, + BuffForAllies = 2, + None = 3 // don't use su, cancel needs to be sent and uses some guri +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Skills/TargetType.cs b/srcs/WingsAPI.Data/Skills/TargetType.cs new file mode 100644 index 0000000..2a7d7fd --- /dev/null +++ b/srcs/WingsAPI.Data/Skills/TargetType.cs @@ -0,0 +1,9 @@ +namespace WingsEmu.DTOs.Skills; + +public enum TargetType +{ + Target = 0, // e.g. basic attack + Self = 1, // e.g. mage regeneration aura + SelfOrTarget = 2, // e.g. holy heal + NonTarget = 3 // e.g. scout tp +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/TimeSpace/ITimeSpaceRecordDao.cs b/srcs/WingsAPI.Data/TimeSpace/ITimeSpaceRecordDao.cs new file mode 100644 index 0000000..f24e21f --- /dev/null +++ b/srcs/WingsAPI.Data/TimeSpace/ITimeSpaceRecordDao.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace WingsAPI.Data.TimeSpace; + +public interface ITimeSpaceRecordDao +{ + Task GetRecordById(long timeSpaceId); + Task SaveRecord(TimeSpaceRecordDto recordDto); + Task> GetAllRecords(); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/TimeSpace/TimeSpaceRecordDto.cs b/srcs/WingsAPI.Data/TimeSpace/TimeSpaceRecordDto.cs new file mode 100644 index 0000000..5219f7f --- /dev/null +++ b/srcs/WingsAPI.Data/TimeSpace/TimeSpaceRecordDto.cs @@ -0,0 +1,24 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using ProtoBuf; + +namespace WingsAPI.Data.TimeSpace; + +[ProtoContract] +public class TimeSpaceRecordDto +{ + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.None)] + [ProtoMember(1)] + public long TimeSpaceId { get; set; } + + [ProtoMember(2)] + public string CharacterName { get; set; } + + [ProtoMember(3)] + public long Record { get; set; } + + [ProtoMember(4)] + public DateTime Date { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Titles/CharacterTitleDto.cs b/srcs/WingsAPI.Data/Titles/CharacterTitleDto.cs new file mode 100644 index 0000000..b37247e --- /dev/null +++ b/srcs/WingsAPI.Data/Titles/CharacterTitleDto.cs @@ -0,0 +1,23 @@ +// WingsEmu +// +// Developed by NosWings Team + +using ProtoBuf; + +namespace WingsEmu.DTOs.Titles; + +[ProtoContract] +public class CharacterTitleDto +{ + [ProtoMember(1)] + public int ItemVnum { get; set; } + + [ProtoMember(2)] + public int TitleId { get; set; } + + [ProtoMember(3)] + public bool IsVisible { get; set; } + + [ProtoMember(4)] + public bool IsEquipped { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Titles/TitleDto.cs b/srcs/WingsAPI.Data/Titles/TitleDto.cs new file mode 100644 index 0000000..c7e34c7 --- /dev/null +++ b/srcs/WingsAPI.Data/Titles/TitleDto.cs @@ -0,0 +1,22 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.ComponentModel.DataAnnotations; +using PhoenixLib.DAL; + +namespace WingsEmu.DTOs.Titles; + +/// +/// Titles +/// +public class TitleDto : IDto +{ + [Key] + public long Id { get; set; } + + /// + /// i18n key + /// + public string Name { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Translations/GameDataType.cs b/srcs/WingsAPI.Data/Translations/GameDataType.cs new file mode 100644 index 0000000..dd081bd --- /dev/null +++ b/srcs/WingsAPI.Data/Translations/GameDataType.cs @@ -0,0 +1,12 @@ +namespace WingsEmu.Game._i18n; + +public enum GameDataType +{ + Item, + NpcMonster, + Skill, + Card, + Map, + QuestName, + QuestDescription +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Warehouse/IAccountWarehouseItemDao.cs b/srcs/WingsAPI.Data/Warehouse/IAccountWarehouseItemDao.cs new file mode 100644 index 0000000..8df657c --- /dev/null +++ b/srcs/WingsAPI.Data/Warehouse/IAccountWarehouseItemDao.cs @@ -0,0 +1,16 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using System.Threading.Tasks; +using WingsAPI.Data.Account; + +namespace WingsAPI.Data.Warehouse; + +public interface IAccountWarehouseItemDao +{ + Task SaveAsync(IReadOnlyList objs); + Task DeleteAsync(IEnumerable objs); + Task> GetByAccountIdAsync(long accountId); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/Warehouse/PartnerWarehouseItemDto.cs b/srcs/WingsAPI.Data/Warehouse/PartnerWarehouseItemDto.cs new file mode 100644 index 0000000..a982164 --- /dev/null +++ b/srcs/WingsAPI.Data/Warehouse/PartnerWarehouseItemDto.cs @@ -0,0 +1,14 @@ +using ProtoBuf; +using WingsEmu.DTOs.Items; + +namespace WingsEmu.DTOs.Inventory; + +[ProtoContract] +public class PartnerWarehouseItemDto +{ + [ProtoMember(1)] + public short Slot { get; set; } + + [ProtoMember(2)] + public ItemInstanceDTO ItemInstance { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Data/WingsAPI.Data.csproj b/srcs/WingsAPI.Data/WingsAPI.Data.csproj new file mode 100644 index 0000000..e71d7f7 --- /dev/null +++ b/srcs/WingsAPI.Data/WingsAPI.Data.csproj @@ -0,0 +1,22 @@ + + + + net5.0 + latest + 2.0.0 + 2.0.0 + WingsAPI.Data + + + + + + + + + + + + + + diff --git a/srcs/WingsAPI.Game.Extensions/Arena/ArenaExtensions.cs b/srcs/WingsAPI.Game.Extensions/Arena/ArenaExtensions.cs new file mode 100644 index 0000000..fd42693 --- /dev/null +++ b/srcs/WingsAPI.Game.Extensions/Arena/ArenaExtensions.cs @@ -0,0 +1,7 @@ +namespace WingsAPI.Game.Extensions.Arena +{ + public static class ArenaExtensions + { + public static long GetArenaEntryPrice(bool familyArena) => familyArena ? 1000 : 500; + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game.Extensions/Bazaar/BazaarExtensions.cs b/srcs/WingsAPI.Game.Extensions/Bazaar/BazaarExtensions.cs new file mode 100644 index 0000000..28213cb --- /dev/null +++ b/srcs/WingsAPI.Game.Extensions/Bazaar/BazaarExtensions.cs @@ -0,0 +1,45 @@ +using System; +using WingsAPI.Data.Bazaar; +using WingsAPI.Packets.Enums.Bazaar; + +namespace WingsAPI.Game.Extensions.Bazaar +{ + public static class BazaarExtensions + { + public static BazaarListedItemType GetBazaarItemStatus(this BazaarItemDTO item) + { + if (item.ExpiryDate > DateTime.UtcNow) + { + return (item.Amount - item.SoldAmount) == 0 ? BazaarListedItemType.Sold : BazaarListedItemType.ForSale; + } + + return (item.Amount - item.SoldAmount) != 0 ? BazaarListedItemType.DeadlineExpired : BazaarListedItemType.Sold; + } + + public static bool PriceOrAmountExceeds(bool hasMedal, long pricePerItem, int amount) + => pricePerItem < 1 || amount is > 999 or < 1 || (!hasMedal ? pricePerItem > 1_000_000 || pricePerItem * amount > 100_000_000 : pricePerItem * amount > 1_000_000_000); + + public static long NormalTax(long price) + { + return price switch + { + <= 100_000 => 500, + >= 20_000_000 => 100_000, + _ => (long)Math.Floor(price * 0.005) + }; + } + + public static long MedalTax(long price, short days) + { + double multiplier = days * 0.0005; + long rawResult = (long)((price - price % 2000) * multiplier); + + return rawResult switch + { + < 50 => 50, + > 10_000 => 10_000, + _ => rawResult + }; + } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game.Extensions/CharacterExtensions/FriendsExtensions.cs b/srcs/WingsAPI.Game.Extensions/CharacterExtensions/FriendsExtensions.cs new file mode 100644 index 0000000..7dc3981 --- /dev/null +++ b/srcs/WingsAPI.Game.Extensions/CharacterExtensions/FriendsExtensions.cs @@ -0,0 +1,13 @@ +using WingsEmu.Game.Networking; + +namespace WingsAPI.Game.Extensions.CharacterExtensions +{ + public static class FriendsExtensions + { + public static string GenerateFriendOnlineInfo(this IClientSession session, long characterId, string characterName, bool isOnline) + => $"finfo {characterId}.{(isOnline ? 1 : 0)}.{characterName}"; + + public static void SendFriendOnlineInfo(this IClientSession session, long characterId, string characterName, bool isOnline) => + session.SendPacket(session.GenerateFriendOnlineInfo(characterId, characterName, isOnline)); + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game.Extensions/CharacterExtensions/ManagerExtensions.cs b/srcs/WingsAPI.Game.Extensions/CharacterExtensions/ManagerExtensions.cs new file mode 100644 index 0000000..0781e13 --- /dev/null +++ b/srcs/WingsAPI.Game.Extensions/CharacterExtensions/ManagerExtensions.cs @@ -0,0 +1,18 @@ +using WingsEmu.Game.Battle; +using WingsEmu.Game.Characters; + +namespace WingsAPI.Game.Extensions.CharacterExtensions +{ + public static class ManagerExtensions + { + public static void RemoveMeditation(this IPlayerEntity character, IMeditationManager meditationManager) + { + if (!meditationManager.HasMeditation(character)) + { + return; + } + + meditationManager.RemoveAllMeditation(character); + } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game.Extensions/CharacterExtensions/PlayerEntityExtensions.cs b/srcs/WingsAPI.Game.Extensions/CharacterExtensions/PlayerEntityExtensions.cs new file mode 100644 index 0000000..a9471b5 --- /dev/null +++ b/srcs/WingsAPI.Game.Extensions/CharacterExtensions/PlayerEntityExtensions.cs @@ -0,0 +1,24 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsEmu.Game.Helpers; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Networking; + +namespace WingsAPI.Game.Extensions.CharacterExtensions +{ + public static class PlayerEntityExtensions + { + public static Location GetLocation(this IClientSession playerEntity) + { + Position position = playerEntity.PlayerEntity.Position; + if (playerEntity.CurrentMapInstance is null) + { + return new Location(playerEntity.PlayerEntity.MapId, position.X, position.Y); + } + + return new Location(playerEntity.CurrentMapInstance.MapId, position.X, position.Y); + } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game.Extensions/Families/FamilyEventExtensions.cs b/srcs/WingsAPI.Game.Extensions/Families/FamilyEventExtensions.cs new file mode 100644 index 0000000..fd0fe4e --- /dev/null +++ b/srcs/WingsAPI.Game.Extensions/Families/FamilyEventExtensions.cs @@ -0,0 +1,48 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Threading.Tasks; +using WingsAPI.Data.Families; +using WingsEmu.Game.Families.Enum; +using WingsEmu.Game.Families.Event; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums.Families; + +namespace WingsAPI.Game.Extensions.Families +{ + public static class FamilyEventExtensions + { + public static async Task FamilyAddLogAsync(this IClientSession session, FamilyLogType familyLogType, string actor, string firstArg = null, string secondArg = null, string thirdArg = null) + { + if (!session.PlayerEntity.IsInFamily()) + { + return; + } + + var familyLog = new FamilyLogDto + { + FamilyId = session.PlayerEntity.Family.Id, + FamilyLogType = familyLogType, + Actor = actor, + Argument1 = firstArg, + Argument2 = secondArg, + Argument3 = thirdArg, + Timestamp = DateTime.UtcNow + }; + + await session.EmitEventAsync(new FamilyAddLogEvent(familyLog)); + } + + public static async Task FamilyAddExperience(this IClientSession session, int experience, FamXpObtainedFromType type) + { + if (!session.PlayerEntity.IsInFamily()) + { + return; + } + + await session.EmitEventAsync(new FamilyAddExperienceEvent(experience, type)); + } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game.Extensions/Families/FamilyPacketExtensions.cs b/srcs/WingsAPI.Game.Extensions/Families/FamilyPacketExtensions.cs new file mode 100644 index 0000000..21d2544 --- /dev/null +++ b/srcs/WingsAPI.Game.Extensions/Families/FamilyPacketExtensions.cs @@ -0,0 +1,622 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using PhoenixLib.MultiLanguage; +using WingsAPI.Data.Families; +using WingsEmu.Core; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Families; +using WingsEmu.Game.Families.Configuration; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums.Chat; +using WingsEmu.Packets.Enums.Families; + +namespace WingsAPI.Game.Extensions.Families +{ + public static class FamilyPacketExtensions + { + #region Send Packets + + public static void SendFmpPacket(this IFamily family, ISessionManager sessionManager, IItemsManager itemsManager) + { + if (family == null) + { + return; + } + + string packet = family.GenerateFmpPacket(itemsManager); + foreach (FamilyMembership member in family.Members) + { + IClientSession session = sessionManager.GetSessionByCharacterId(member.CharacterId); + + session?.SendPacket(packet); + } + } + + + public static void SendFmpPacket(this IClientSession session, IItemsManager itemsManager) + { + IFamily family = session.PlayerEntity.Family; + if (family == null) + { + return; + } + + session.SendPacket(family.GenerateFmpPacket(itemsManager)); + } + + public static string GenerateFmpPacket(this IFamily family, IItemsManager itemsManager) + { + var stringBuilder = new StringBuilder("fmp"); + + Dictionary toShow = new(); + + if (family?.Upgrades == null) + { + return stringBuilder.ToString(); + } + + foreach ((int upgradeId, FamilyUpgrade upgrade) in family.Upgrades) + { + IGameItem item = itemsManager.GetItem(upgradeId); + if (item == null) + { + continue; + } + + // Get the highest upgradeLevel by FamilyUpgradeType + FamilyUpgradeType type = Enum.Parse(item.Data[2].ToString()); + byte upgradeLevel = (byte)item.Data[3]; + + if (!toShow.TryGetValue(type, out (byte level, FamilyUpgrade familyUpgrade) familyUpgrade)) + { + familyUpgrade = (upgradeLevel, upgrade); + toShow[type] = familyUpgrade; + continue; + } + + if (upgradeLevel <= familyUpgrade.level) + { + continue; + } + + familyUpgrade = (upgradeLevel, upgrade); + toShow[type] = familyUpgrade; + } + + foreach ((FamilyUpgradeType _, (byte _, FamilyUpgrade familyUpgrade)) in toShow) + { + stringBuilder.AppendFormat(" {0}|{1}", familyUpgrade.Id, (byte)familyUpgrade.State); + } + + return stringBuilder.ToString(); + } + + public static void SendFmiPacket(this IFamily family, ISessionManager sessionManager) + { + if (family == null) + { + return; + } + + string packet = family.GenerateFmiPacket(); + + foreach (FamilyMembership member in family.Members) + { + IClientSession session = sessionManager.GetSessionByCharacterId(member.CharacterId); + + session?.SendPacket(packet); + } + } + + public static void SendFmiPacket(this IClientSession session) + { + IFamily family = session.PlayerEntity.Family; + if (family == null) + { + return; + } + + session.SendPacket(family.GenerateFmiPacket()); + } + + public static string GenerateFmiPacket(this IFamily family) + { + var stringBuilder = new StringBuilder("fmi"); + + // iterate over mission config & check level restrictions + DateTime resetDate = DateTime.UtcNow.Date; + foreach ((int missionId, FamilyMissionDto upgrade) in family.Mission) + { + // progressType => 1 = Completed + // progressType => 2 = InProgress + int progressType = upgrade.CompletionDate.HasValue && upgrade.CompletionDate > resetDate ? 1 : 2; + string thirdArgument = progressType == 1 && upgrade.CompletionDate.HasValue ? upgrade.CompletionDate.Value.ToString("yyyyMMdd") : upgrade.Count.ToString(); + stringBuilder.AppendFormat(" 0|{0}|{1}|{2}|{3}", missionId, progressType, thirdArgument, upgrade.CompletionCount); + } + + foreach ((int achievementId, FamilyAchievementProgressDto upgrade) in family.AchievementProgress) + { + stringBuilder.AppendFormat(" 1|{0}|2|{1}|0", achievementId, upgrade.Count); + } + + foreach ((int achievementId, FamilyAchievementCompletionDto upgrade) in family.Achievements) + { + stringBuilder.AppendFormat(" 1|{0}|1|{1:yyyyMMdd}|0", achievementId, upgrade.CompletionDate); + } + + return stringBuilder.ToString(); + } + + public static void SendFamilyLevelUpMessageToMembers(Family family, ISessionManager sessionManager, IGameLanguageService languageService, FamilyConfiguration familyConfiguration) + { + if (family == null) + { + return; + } + + foreach (FamilyMembership member in family.Members) + { + IClientSession session = sessionManager.GetSessionByCharacterId(member.CharacterId); + if (session == null) + { + continue; + } + + session.RefreshFamilyInfo(family, familyConfiguration); + session.BroadcastGidx(family, languageService); + session.SendMsg(languageService.GetLanguageFormat(GameDialogKey.FAMILY_SHOUTMESSAGE_LEVEL_UP, session.UserLanguage, family.Level.ToString()), MsgMessageType.Middle); + } + } + + public static void SendFamilyNoticeMessage(Family family, ISessionManager sessionManager, FamilyConfiguration familyConfiguration) + { + if (family == null) + { + return; + } + + string message = family.Message; + if (string.IsNullOrEmpty(message)) + { + return; + } + + foreach (FamilyMembership member in family.Members) + { + IClientSession session = sessionManager.GetSessionByCharacterId(member.CharacterId); + session?.SendInfo("--- Family Message ---\n" + message); + session?.RefreshFamilyInfo(family, familyConfiguration); + } + } + + public static void SendFamilyInfoToMembers(Family family, ISessionManager sessionManager, FamilyConfiguration familyConfiguration) + { + if (family == null) + { + return; + } + + foreach (FamilyMembership member in family.Members.ToArray()) + { + IClientSession session = sessionManager.GetSessionByCharacterId(member.CharacterId); + session?.RefreshFamilyInfo(family, familyConfiguration); + } + } + + public static void SendFamilyMembersAuthorityToMembers(Family family, ISessionManager sessionManager, FamilyConfiguration familyConfiguration) + { + if (family == null) + { + return; + } + + IReadOnlyList packets = null; + + foreach (FamilyMembership member in family.Members) + { + IClientSession session = sessionManager.GetSessionByCharacterId(member.CharacterId); + + if (session == null) + { + continue; + } + + packets ??= new List + { + GenerateFamilyMembersPacket(sessionManager, family), + GenerateFamilyMembersMessagesPacket(family) + }; + + session.RefreshFamilyInfo(family, familyConfiguration); + session.SendPackets(packets); + } + } + + public static void SendFamilyMembersInfoToMembers(IFamily family, ISessionManager sessionManager, FamilyConfiguration familyConfiguration) + { + if (family == null) + { + return; + } + + IReadOnlyList packets = null; + + foreach (FamilyMembership member in family.Members.ToArray()) + { + IClientSession session = sessionManager.GetSessionByCharacterId(member.CharacterId); + + if (session == null) + { + continue; + } + + packets ??= new List + { + GenerateFamilyMembersPacket(sessionManager, family), + GenerateFamilyMembersMessagesPacket(family), + GenerateFamilyMembersExpPacket(family) + }; + + session.RefreshFamilyInfo(family, familyConfiguration); + session.SendPackets(packets); + } + } + + public static void SendOnlineStatusToMembers(this IFamily family, ISessionManager sessionManager, long characterId, bool connecting, IGameLanguageService gameLanguage) + { + string packet = null; + + FamilyMembership familyMember = family.Members.ToArray().FirstOrDefault(x => x.CharacterId == characterId); + + if (familyMember == null) + { + return; + } + + foreach (FamilyMembership member in family.Members.ToArray()) + { + IClientSession session = sessionManager.GetSessionByCharacterId(member.CharacterId); + + if (session == null) + { + continue; + } + + packet ??= GenerateFamilyMemberConnectionStatus(characterId, connecting); + session.SendPacket(packet); + + if (!connecting) + { + continue; + } + + string authority = gameLanguage.GetLanguage(familyMember.Authority.GetMemberLanguageKey(), session.UserLanguage); + session.SendInformationChatMessage(gameLanguage.GetLanguageFormat(GameDialogKey.FAMILY_CHATMESSAGE_CHARACTER_LOGGED_IN, session.UserLanguage, familyMember.Character.Name, authority)); + } + } + + public static void SendMembersExpToMembers(IFamily family, ISessionManager sessionManager) + { + if (family == null) + { + return; + } + + string packet = null; + + foreach (FamilyMembership member in family.Members) + { + IClientSession session = sessionManager.GetSessionByCharacterId(member.CharacterId); + + if (session == null) + { + continue; + } + + packet ??= GenerateFamilyMembersExpPacket(family); + session.SendPacket(packet); + } + } + + public static void SendMembersDailyMessages(IFamily family, ISessionManager sessionManager) + { + if (family == null) + { + return; + } + + string packet = null; + + foreach (FamilyMembership member in family.Members) + { + IClientSession session = sessionManager.GetSessionByCharacterId(member.CharacterId); + + if (session == null) + { + continue; + } + + packet ??= GenerateFamilyMembersMessagesPacket(family); + session.SendPacket(packet); + } + } + + public static void SendFamilyLogsToMembers(IFamily family, ISessionManager sessionManager) + { + if (family == null) + { + return; + } + + string packet = GenerateFamilyHistoryUpdate(); + + foreach (FamilyMembership member in family.Members) + { + IClientSession session = sessionManager.GetSessionByCharacterId(member.CharacterId); + session?.SendPacket(packet); + } + } + + public static void SendFamilyLogsToMember(this IClientSession session, IFamily family) + { + session.SendPackets(GenerateGhisPackets(family)); + } + + public static void SendResetFamilyInterface(this IClientSession session) + { + session.SendPacket(GenerateRestartFamilyInterface()); + } + + public static void RefreshFamilyMembers(this IClientSession session, ISessionManager sessionManager, IFamily family) + { + if (family == null) + { + session.SendEmptyFamilyMembers(); + return; + } + + session.SendPacket(GenerateFamilyMembersPacket(sessionManager, family)); + } + + + public static void RefreshFamilyMembersMessages(this IClientSession session, IFamily family) + { + if (family == null) + { + session.SendEmptyFamilyMessages(); + } + + session.SendPacket(GenerateFamilyMembersMessagesPacket(family)); + } + + + public static void RefreshFamilyMembersExp(this IClientSession session, IFamily family) + { + if (family == null) + { + session.SendEmptyFamilyExp(); + return; + } + + session.SendPacket(GenerateFamilyMembersExpPacket(family)); + } + + public static void RefreshFamilyInfo(this IClientSession session, IFamily family, FamilyConfiguration familyConfiguration) + { + if (family == null) + { + session.SendEmptyFamilyInfo(); + return; + } + + session.SendPacket(session.GenerateFamilyInfoPacket(family, familyConfiguration)); + } + + public static void BroadcastGidx(this IClientSession session, IFamily family, IGameLanguageService gameLanguageService) + => session.CurrentMapInstance?.Broadcast(x => session.GenerateGidxPacket(gameLanguageService, family, x.UserLanguage)); + + public static void SendGidxPacket(this IClientSession session, IFamily family, IGameLanguageService gameLanguage) => + session.SendPacket(session.GenerateGidxPacket(gameLanguage, family, session.UserLanguage)); + + public static void SendTargetGidxPacket(this IClientSession session, IClientSession target, IFamily family, IGameLanguageService gameLanguageService) + => session.SendPacket(target.GenerateGidxPacket(gameLanguageService, family, session.UserLanguage)); + + public static void SendEmptyFamilyInfo(this IClientSession session) => session.SendPacket("ginfo"); + + public static void SendEmptyFamilyMembers(this IClientSession session) => session.SendPacket("gmbr 0"); + + public static void SendEmptyFamilyMessages(this IClientSession session) => session.SendPacket("gmsg"); + + public static void SendEmptyFamilyExp(this IClientSession session) => session.SendPacket("gexp"); + + #endregion + + #region Generate Packets + + public static string GenerateGidxPacket(this IClientSession session, IGameLanguageService gameLanguage, IFamily family, RegionLanguageType type) => + family == null + ? $"gidx 1 {session.PlayerEntity.Id} -1 - 0" + : $"gidx 1 {session.PlayerEntity.Id} {family.Id}.-1 {family.Name}({gameLanguage.GetLanguage(session.PlayerEntity.GetFamilyAuthority().GetMemberLanguageKey(), type)}) {family.Level} 0|0|0"; + + public static IReadOnlyCollection GenerateGhisPackets(IFamily family) + { + var list = new List(); + int index = 0; + DateTime currentTime = DateTime.UtcNow; + list.Add(GenerateGhisPacket(true, currentTime, family.Logs, ref index)); + + while (index < family.Logs.Count) + { + list.Add(GenerateGhisPacket(false, currentTime, family.Logs, ref index)); + } + + return list; + } + + public static string GenerateGhisPacket(bool initial, DateTime currentTime, IReadOnlyList logs, ref int index) + { + int initialIndexValue = index; + var packet = new StringBuilder("ghis"); + + if (initial) + { + packet.Append(" 0"); + } + + for (; index < logs.Count && index < initialIndexValue + 55; index++) + { + FamilyLogDto log = logs[index]; + packet.Append(GenerateGhisSubPacket(log, currentTime)); + } + + return packet.ToString(); + } + + public static string GenerateGhisSubPacket(FamilyLogDto log, DateTime currentTime) + { + string packet = $" {(byte)log.FamilyLogType}|"; + + switch (log.FamilyLogType) + { + case FamilyLogType.DailyMessage: + packet += $"{log.Actor}|{(log.Argument1 == null ? string.Empty : log.Argument1.Replace(' ', (char)0xB))}"; // memberName|message - Quarry|Hey + break; + case FamilyLogType.RaidWon: + packet += $"{log.Actor}"; // Act4RaidType + break; + case FamilyLogType.RainbowBattle: + packet += $"{log.Actor}|-"; // EnemyFamilyName - not used anymore because of the event rework + break; + case FamilyLogType.FamilyXP: + packet += $"{log.Actor}|{log.Argument1 ?? string.Empty}"; // memberName|memberXp - Quarry|50000 + break; + case FamilyLogType.FamilyLevelUp: + packet += $"{log.Actor}"; // FamilyLevel + break; + case FamilyLogType.LevelUp: + packet += $"{log.Actor}|{log.Argument1 ?? string.Empty}"; // memberName|memberLevel - Quarry|60 + break; + case FamilyLogType.ItemUpgraded: + packet += $"{log.Actor}|{log.Argument1 ?? string.Empty}|{log.Argument2 ?? string.Empty}"; // memberName|itemVnum|itemUpgrade - Quarry|1|10 + break; + case FamilyLogType.RightChanged: + packet += + $"{log.Actor}|{log.Argument1 ?? string.Empty}|{log.Argument2 ?? string.Empty}|{log.Argument3 ?? string.Empty}"; // memberName|FamilyAuthorityType|FamilyActionType|value - Quarry|2|1|1 + break; + case FamilyLogType.AuthorityChanged: + packet += $"{log.Actor}|{log.Argument1 ?? string.Empty}|{log.Argument2 ?? string.Empty}"; // memberName|FamilyAuthorityType|targetMemberName - Blowa|2|Quarry + break; + case FamilyLogType.MemberLeave: + packet += $"{log.Actor}"; // memberName + break; + case FamilyLogType.MemberJoin: + packet += $"{log.Actor}|{log.Argument1 ?? string.Empty}"; // memberName|targetMemberName - Blowa|Quarry + break; + case FamilyLogType.FamilyMission: + packet += $"{log.Actor}"; // FamilyMissionItemVnum + break; + case FamilyLogType.FamilyAchievement: + packet += $"{log.Actor}"; // FamilyAchievementItemVnum + break; + case FamilyLogType.HeroLevelUp: + packet += $"{log.Actor}|{log.Argument1 ?? string.Empty}"; // memberName|HeroLevel - Quarry|60 + break; + case FamilyLogType.MemberUsedItem: + packet += $"{log.Actor}|{log.Argument1 ?? string.Empty}"; // memberName|ItemVnum - Quarry|5060 - used only for FXP Boosters + break; + } + + return $"{packet}|{Math.Floor((currentTime - log.Timestamp).TotalHours).ToString(CultureInfo.InvariantCulture)}"; + } + + public static string GenerateFamilyMemberConnectionStatus(long characterId, bool connecting) => $"gcon {characterId.ToString()}|{(connecting ? 1 : 0).ToString()}"; + + public static string GenerateRestartFamilyInterface() => "fclear"; + + public static string GenerateFamilyMembersPacket(ISessionManager sessionManager, IFamily family) + { + var packet = new StringBuilder("gmbr 0"); + DateTime actualTime = DateTime.UtcNow; + foreach (FamilyMembership member in family.Members.OrderBy(x => x.JoinDate)) + { + bool isOnline = sessionManager.IsOnline(member.CharacterId); + packet.Append($" {member.CharacterId.ToString()}|{member.JoinDate:yyMMddHH)}|{member.Character.Name}|{member.Character.Level.ToString()}|{((byte)member.Character.Class).ToString()}" + + $"|{((byte)member.Authority).ToString()}|{((byte)member.Title).ToString()}|{(isOnline ? 1 : 0).ToString()}|{member.Character.HeroLevel.ToString()}" + + $"|{(isOnline ? -1 : Math.Floor((actualTime - member.LastOnlineDate).TotalHours)).ToString(CultureInfo.InvariantCulture)}"); + } + + return packet.ToString(); + } + + public static string GenerateFamilyMembersMessagesPacket(IFamily family) + { + var packet = new StringBuilder("gmsg"); + foreach (FamilyMembership member in family.Members) + { + if (string.IsNullOrEmpty(member.DailyMessage)) + { + continue; + } + + packet.Append($" {member.CharacterId.ToString()}|{member.DailyMessage.Replace(" ", " ")}"); + } + + return packet.ToString(); + } + + public static string GenerateFamilyMembersExpPacket(IFamily family) + { + var packet = new StringBuilder("gexp"); + foreach (FamilyMembership member in family.Members.ToArray()) + { + packet.Append($" {member.CharacterId.ToString()}|{member.Experience.ToString()}"); + } + + return packet.ToString(); + } + + public static string GenerateFamilyInfoPacket(this IClientSession session, IFamily family, FamilyConfiguration familyConfiguration) + { + if (family?.Head == null || session.PlayerEntity.Family == null) + { + return "ginfo"; + } + + Range levelInfo = familyConfiguration.GetRangeByFamilyXp(family.Experience); + bool hasLevelInfo = levelInfo != null; + long refinedMinXp = hasLevelInfo ? family.Experience - levelInfo.Minimum : 0; + long refinedMaxXp = hasLevelInfo ? levelInfo.Maximum - levelInfo.Minimum : 0; + + return + "ginfo " + + $"{family.Name} " + + $"{family.Head.Character.Name} " + + $"{((byte)family.HeadGender).ToString()} " + + $"{family.Level.ToString()} " + + $"{refinedMinXp.ToString()} " + + $"{refinedMaxXp.ToString()} " + + $"{family.Members.Count.ToString()} " + + $"{family.GetMaximumMembershipCapacity().ToString()} " + + $"{((byte)session.PlayerEntity.GetFamilyAuthority()).ToString()} " + + $"{(family.AssistantCanInvite ? 1 : 0).ToString()} " + + $"{(family.AssistantCanNotice ? 1 : 0).ToString()} " + + $"{(family.AssistantCanShout ? 1 : 0).ToString()} " + + $"{(family.AssistantCanGetHistory ? 1 : 0).ToString()} " + + $"{((byte)family.AssistantWarehouseAuthorityType).ToString()} " + + $"{(family.MemberCanGetHistory ? 1 : 0).ToString()} " + + $"{((byte)family.MemberWarehouseAuthorityType).ToString()} " + + $"{family.Message?.Replace(' ', '^') ?? string.Empty}"; + } + + public static string GenerateFamilyHistoryUpdate() => "fhis_stc"; + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game.Extensions/Families/FamilyWarehouseExtensions.cs b/srcs/WingsAPI.Game.Extensions/Families/FamilyWarehouseExtensions.cs new file mode 100644 index 0000000..f21794e --- /dev/null +++ b/srcs/WingsAPI.Game.Extensions/Families/FamilyWarehouseExtensions.cs @@ -0,0 +1,56 @@ +using WingsAPI.Data.Families; +using WingsAPI.Packets.Enums.Families; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums.Families; + +namespace WingsAPI.Game.Extensions.Families +{ + public static class FamilyWarehouseExtensions + { + public static bool CheckLogHistoryPermission(this IClientSession session) + { + return session.PlayerEntity.GetFamilyAuthority() switch + { + FamilyAuthority.Keeper => session.PlayerEntity.Family.AssistantCanGetHistory, + FamilyAuthority.Member => session.PlayerEntity.Family.MemberCanGetHistory, + _ => true + }; + } + + public static bool CheckLogHistoryPermission(this FamilyMembershipDto membershipDto, FamilyDTO family) + { + return membershipDto.Authority switch + { + FamilyAuthority.Keeper => family.AssistantCanGetHistory, + FamilyAuthority.Member => family.MemberCanGetHistory, + _ => true + }; + } + + public static bool CheckPutWithdrawPermission(this IClientSession session, FamilyWarehouseAuthorityType authorityRequested) + { + FamilyWarehouseAuthorityType memberAuthority = session.PlayerEntity.GetFamilyAuthority() switch + { + FamilyAuthority.Member => session.PlayerEntity.Family.MemberWarehouseAuthorityType, + FamilyAuthority.Keeper => session.PlayerEntity.Family.AssistantWarehouseAuthorityType, + _ => FamilyWarehouseAuthorityType.PutAndWithdraw + }; + + return memberAuthority.CheckPutWithdrawPermission(authorityRequested); + } + + public static bool CheckPutWithdrawPermission(this FamilyMembershipDto membershipDto, FamilyDTO family, FamilyWarehouseAuthorityType authorityRequested) + { + FamilyWarehouseAuthorityType memberAuthority = membershipDto.Authority switch + { + FamilyAuthority.Member => family.MemberWarehouseAuthorityType, + FamilyAuthority.Keeper => family.AssistantWarehouseAuthorityType, + _ => FamilyWarehouseAuthorityType.PutAndWithdraw + }; + + return memberAuthority.CheckPutWithdrawPermission(authorityRequested); + } + + public static bool CheckPutWithdrawPermission(this FamilyWarehouseAuthorityType memberAuthority, FamilyWarehouseAuthorityType authorityRequested) => authorityRequested <= memberAuthority; + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game.Extensions/Families/FamilyWarehousePacketExtension.cs b/srcs/WingsAPI.Game.Extensions/Families/FamilyWarehousePacketExtension.cs new file mode 100644 index 0000000..508c016 --- /dev/null +++ b/srcs/WingsAPI.Game.Extensions/Families/FamilyWarehousePacketExtension.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text; +using WingsAPI.Data.Families; +using WingsAPI.Game.Extensions.Warehouse; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Networking; + +namespace WingsAPI.Game.Extensions.Families +{ + public static class FamilyWarehousePacketExtension + { + public static void SendFamilyWarehouseLogs(this IClientSession session, IList logs) + { + session.SendPacket(session.GenerateFamilyWarehouseLogs(logs)); + } + + public static void SendFamilyWarehouseItems(this IClientSession session, IItemsManager itemsManager, int warehouseSize, IEnumerable items) => + session.SendPacket(session.GenerateFStashAll(itemsManager, warehouseSize, items)); + + public static void SendFamilyWarehouseAddItem(this IClientSession session, IItemsManager itemsManager, FamilyWarehouseItemDto item) + { + session.SendPacket(item.GenerateFStashAdd(itemsManager)); + } + + public static void SendFamilyWarehouseRemoveItem(this IClientSession session, FamilyWarehouseItemDto item) + { + session.SendPacket(session.GenerateFStashRemove(item)); + } + + public static void SendFamilyWarehouseRemoveItem(this IClientSession session, short slot) + { + session.SendPacket(session.GenerateFStashRemove(slot)); + } + + private static string GenerateFStashPacketContent(this FamilyWarehouseItemDto warehouseItem, IItemsManager itemsManager) => + WarehousePacketExtensions.GenerateStashSubPacket(itemsManager, warehouseItem.ItemInstance, warehouseItem.Slot); + + public static string GenerateFStashAll(this IClientSession session, IItemsManager itemsManager, int warehouseSize, IEnumerable items) + { + var packet = new StringBuilder($"f_stash_all {warehouseSize.ToString()}"); + foreach (FamilyWarehouseItemDto item in items) + { + packet.Append(' '); + packet.Append(item.GenerateFStashPacketContent(itemsManager)); + } + + return packet.ToString(); + } + + public static string GenerateFStashAdd(this FamilyWarehouseItemDto item, IItemsManager itemsManager) => $"f_stash {item.GenerateFStashPacketContent(itemsManager)}"; + + public static string GenerateFStashRemove(this IClientSession session, FamilyWarehouseItemDto item) => session.GenerateFStashRemove(item.Slot); + + public static string GenerateFStashRemove(this IClientSession session, short slot) => $"f_stash {slot.ToString()}.-1.0.0.0"; + + public static string GenerateFamilyWarehouseLogs(this IClientSession session, IList logs) + { + StringBuilder packet = new("fslog_stc -1"); + DateTime currentTime = DateTime.UtcNow; + + for (int i = logs.Count - 1; i >= 0; i--) + { + FamilyWarehouseLogEntryDto log = logs[i]; + packet.AppendFormat(" {0}|{1}|{2}|{3}|{4}", ((byte)log.Type).ToString(), log.CharacterName, log.ItemVnum.ToString(), log.Amount.ToString(), + Math.Floor((currentTime - log.DateOfLog).TotalHours).ToString(CultureInfo.InvariantCulture)); + } + + return packet.ToString(); + } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game.Extensions/Groups/GroupPacketExtensions.cs b/srcs/WingsAPI.Game.Extensions/Groups/GroupPacketExtensions.cs new file mode 100644 index 0000000..7a401e3 --- /dev/null +++ b/srcs/WingsAPI.Game.Extensions/Groups/GroupPacketExtensions.cs @@ -0,0 +1,193 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage.Configuration; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Groups; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Networking; + +namespace WingsAPI.Game.Extensions.Groups +{ + public static class GroupPacketExtensions + { + public static List GeneratePartyUiPackets(this IClientSession session) + { + var str = new List(); + + IReadOnlyList groupMembers = session.PlayerEntity.GetGroup().Members; + + // entwell mixing pets slots & group slots :/ + int groupSlotPetOffset = 3; + + for (int i = 0; i < groupMembers.Count; i++) + { + IPlayerEntity member = groupMembers[i]; + + if (member.Id == session.PlayerEntity.Id) + { + continue; + } + + byte groupSlot = (byte)(i + groupSlotPetOffset); + str.Add( + $"pst 1 {member.Id} {groupSlot.ToString()} {member.GetHpPercentage()} {member.GetMpPercentage()} {member.Hp} {member.Mp} {(byte)member.Class} {(byte)member.Gender} {(member.UseSp ? member.Morph : 0)}{member.BuffComponent.GetAllBuffs().Aggregate(string.Empty, (current, buff) => current + $" {buff.CardId}.{buff.CasterLevel}")}"); + } + + return str; + } + + public static void RefreshPartyUi(this IClientSession session) + { + if (!session.PlayerEntity.IsInGroup()) + { + return; + } + + session.SendPackets(session.GeneratePartyUiPackets()); + } + + public static byte GetGroupSlotIndex(this IClientSession session) + { + if (!session.PlayerEntity.IsInGroup()) + { + return 0; + } + + // offset of mates + 1 (players start at 0 in their group index) + byte groupSlot = 3; + IReadOnlyList groupMembers = session.PlayerEntity.GetGroup().Members; + for (int i = 0; i < groupMembers.Count; i++) + { + IPlayerEntity member = groupMembers[i]; + + if (member.Id != session.PlayerEntity.Id) + { + continue; + } + + groupSlot = (byte)(i + 3); + } + + return groupSlot; + } + + public static string GeneratePInitPacket(this IClientSession session, IGameLanguageService gameLanguage, ISpPartnerConfiguration spPartnerConfiguration) + { + IReadOnlyList mates = session.PlayerEntity.MateComponent.GetMates(); + int allyCount = 0; + + string str = string.Empty; + if (mates != null) + { + // FUCK GF I18N SOOOOO MUCH ༼ つ ◕_◕ ༽つ + foreach (IMateEntity mate in mates.Where(s => s.IsTeamMember).OrderBy(s => s.MateType)) + { + if (mate.Specialist != null && mate.IsUsingSp) + { + allyCount++; + + GameDialogKey? key = Enum.TryParse(spPartnerConfiguration.GetByMorph(mate.Specialist.GameItem.Morph)?.Name, out GameDialogKey gameDialogKey) ? gameDialogKey : null; + string specialistName = key.HasValue + ? gameLanguage.GetLanguage(key.Value, session.UserLanguage) + : gameLanguage.GetLanguage(GameDataType.NpcMonster, mate.Name, session.UserLanguage); + int specialistMorph = mate.Specialist.GameItem.Morph; + str += $" 2|{mate.Id}|{(int)mate.MateType}|{mate.Level}|{specialistName.Replace(' ', '^')}|-1|{specialistMorph}|1|0|-1"; + continue; + } + + allyCount++; + string mateName = string.IsNullOrEmpty(mate.MateName) || mate.Name == mate.MateName + ? gameLanguage.GetLanguage(GameDataType.NpcMonster, mate.Name, session.UserLanguage) + : mate.MateName; + int morph = mate.MonsterVNum; + str += $" 2|{mate.Id}|{(int)mate.MateType}|{mate.Level}|{mateName.Replace(' ', '^')}|-1|{morph}|-1|0|-1"; + } + } + + if (!session.PlayerEntity.IsInGroup()) + { + return $"pinit {allyCount}{str}"; + } + + PlayerGroup group = session.PlayerEntity.GetGroup(); + IReadOnlyList grpMembers = group.Members; + foreach (IPlayerEntity member in grpMembers) + { + allyCount++; + str += + $" 1|{member.Id}|{member.Session.GetGroupSlotIndex()}|{member.Level}|{member.Name}|{group.GroupId}|{(byte)member.Gender}|{(byte)member.Class}|{(member.UseSp ? member.Morph : 0)}|{member.HeroLevel}|0"; + } + + return $"pinit {allyCount}{str}"; + } + + public static void RefreshParty(this IClientSession session, ISpPartnerConfiguration partnerConfiguration) + => session.SendPacket(session.GeneratePInitPacket(StaticGameLanguageService.Instance, partnerConfiguration)); + + public static string GeneratePidx(this IClientSession session) + { + const string header = "pidx"; + string packet; + + if (!session.PlayerEntity.IsInGroup()) + { + packet = $" -1 1.{session.PlayerEntity.Id}"; + return header + packet; + } + + PlayerGroup grp = session.PlayerEntity.GetGroup(); + packet = $" {grp.GroupId}"; + + foreach (IPlayerEntity member in grp.Members) + { + packet += $" 1.{member.Id}"; + } + + return header + packet; + } + + public static void BroadcastPidx(this IClientSession session) => session.Broadcast(session.GeneratePidx()); + + public static void RefreshGroupLevelUi(this IClientSession session, ISpPartnerConfiguration spPartnerConfiguration) + { + if (!session.PlayerEntity.IsInGroup()) + { + return; + } + + foreach (IPlayerEntity member in session.PlayerEntity.GetGroup().Members) + { + member.Session.RefreshParty(spPartnerConfiguration); + } + } + + public static bool IsMemberWith(this IPlayerEntity character, long id, ISessionManager sessionManager) + { + if (!character.IsInGroup()) + { + return false; + } + + IClientSession target = sessionManager.GetSessionByCharacterId(id); + + if (target == null) + { + return false; + } + + if (!target.PlayerEntity.IsInGroup()) + { + return false; + } + + PlayerGroup characterGroup = character.GetGroup(); + PlayerGroup targetGroup = target.PlayerEntity.GetGroup(); + + return characterGroup.GroupId == targetGroup.GroupId; + } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game.Extensions/Inventory/InventoryPacketExtensions.cs b/srcs/WingsAPI.Game.Extensions/Inventory/InventoryPacketExtensions.cs new file mode 100644 index 0000000..0095a63 --- /dev/null +++ b/srcs/WingsAPI.Game.Extensions/Inventory/InventoryPacketExtensions.cs @@ -0,0 +1,10 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsAPI.Game.Extensions.Inventory +{ + public static class InventoryPacketExtensions + { + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game.Extensions/ItemExtension/BuyExtension.cs b/srcs/WingsAPI.Game.Extensions/ItemExtension/BuyExtension.cs new file mode 100644 index 0000000..fd1a16a --- /dev/null +++ b/srcs/WingsAPI.Game.Extensions/ItemExtension/BuyExtension.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using System.Text; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Shops; +using WingsEmu.Packets.Enums; + +namespace WingsAPI.Game.Extensions.ItemExtension +{ + public static class BuyExtension + { + public static string GenerateSellList(this IClientSession session, long gold, short slot, short amount, short sellAmount) => $"sell_list {gold} {slot}.{amount}.{sellAmount}"; + + public static void SendSellList(this IClientSession session, long gold, short slot, short amount, short sellAmount) => + session.SendPacket(session.GenerateSellList(gold, slot, amount, sellAmount)); + + public static void SendShopContent(this IClientSession receiverSession, long ownerCharacterId, IEnumerable items) + { + var packetToSend = new StringBuilder($"n_inv 1 {ownerCharacterId.ToString()} 0 0"); + + foreach (ShopPlayerItem item in items) + { + packetToSend.Append(item.GenerateShopContentSubPacket()); + } + + packetToSend.Append( + " -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1"); + + receiverSession.SendPacket(packetToSend.ToString()); + } + + private static string GenerateShopContentSubPacket(this ShopPlayerItem shopPlayerItem) + { + if (shopPlayerItem == null) + { + return " -1"; + } + + if (shopPlayerItem.InventoryItem.InventoryType == InventoryType.Equipment) + { + return $" {((byte)shopPlayerItem.InventoryItem.InventoryType).ToString()}.{shopPlayerItem.ShopSlot.ToString()}.{shopPlayerItem.InventoryItem.ItemInstance.ItemVNum.ToString()}" + + $".{shopPlayerItem.InventoryItem.ItemInstance.Rarity.ToString()}.{shopPlayerItem.InventoryItem.ItemInstance.Upgrade.ToString()}.{shopPlayerItem.PricePerUnit.ToString()}" + + $".{shopPlayerItem.InventoryItem.ItemInstance.GetRunesCount().ToString()}."; + } + + return $" {((byte)shopPlayerItem.InventoryItem.InventoryType).ToString()}.{shopPlayerItem.ShopSlot.ToString()}.{shopPlayerItem.InventoryItem.ItemInstance.ItemVNum.ToString()}" + + $".{shopPlayerItem.SellAmount.ToString()}.{shopPlayerItem.PricePerUnit.ToString()}.-1.-1.-1"; + } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game.Extensions/ItemExtension/ExchangeExtension.cs b/srcs/WingsAPI.Game.Extensions/ItemExtension/ExchangeExtension.cs new file mode 100644 index 0000000..60e469f --- /dev/null +++ b/srcs/WingsAPI.Game.Extensions/ItemExtension/ExchangeExtension.cs @@ -0,0 +1,25 @@ +using System.Threading.Tasks; +using WingsEmu.Game.Exchange.Event; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace WingsAPI.Game.Extensions.ItemExtension.Inventory +{ + public static class ExchangeExtension + { + public static async Task CloseExchange(this IClientSession session, ExcCloseType type = ExcCloseType.Failed) + => await session.EmitEventAsync(new ExchangeCloseEvent + { + Type = type + }); + + public static string GenerateEmptyExchangeWindow(this IClientSession session, long targetId) => $"exc_list 1 {targetId} -1"; + public static void SendEmptyExchangeWindow(this IClientSession session, long targetId) => session.SendPacket(session.GenerateEmptyExchangeWindow(targetId)); + + public static string GenerateExchangeWindow(this IClientSession session, long targetId, int gold, long bankGold, string itemsPackets) + => $"exc_list 1 {targetId} {gold} {bankGold / 1000} {(itemsPackets == string.Empty ? "-1" : itemsPackets)}"; + + public static void SendExchangeWindow(this IClientSession session, long targetId, int gold, long bankGold, string itemsPackets) + => session.SendPacket(session.GenerateExchangeWindow(targetId, gold, bankGold, itemsPackets)); + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game.Extensions/ItemExtension/InventoryExtension.cs b/srcs/WingsAPI.Game.Extensions/ItemExtension/InventoryExtension.cs new file mode 100644 index 0000000..1ab159b --- /dev/null +++ b/srcs/WingsAPI.Game.Extensions/ItemExtension/InventoryExtension.cs @@ -0,0 +1,37 @@ +using System.Threading.Tasks; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Inventory.Event; +using WingsEmu.Game.Items; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsAPI.Game.Extensions.ItemExtension.Inventory +{ + public static class InventoryExtension + { + public static int CountMissingItems(this IClientSession session, int itemVNum, short amount) => amount - session.PlayerEntity.CountItemWithVnum(itemVNum); + + public static bool HasEnoughGold(this IClientSession session, long gold) => session.PlayerEntity.Gold >= gold; + + public static async Task AddNewItemToInventory(this IClientSession session, GameItemInstance itemInstance, bool showMessage = false, + ChatMessageColorType colorType = ChatMessageColorType.Green, bool sendGiftIsFull = false, MessageErrorType errorType = MessageErrorType.Chat, short? slot = null, + InventoryType? type = null, bool isByMovePacket = false) + { + var inventoryItem = new InventoryItem + { + InventoryType = type ?? itemInstance.GameItem.Type, + IsEquipped = false, + ItemInstance = itemInstance, + CharacterId = session.PlayerEntity.Id, + Slot = slot ?? session.PlayerEntity.GetNextInventorySlot(type ?? itemInstance.GameItem.Type) + }; + await session.EmitEventAsync(new InventoryAddItemEvent(inventoryItem, showMessage, colorType, sendGiftIsFull, errorType, slot, type, isByMovePacket)); + return inventoryItem; + } + + public static async Task RemoveItemFromInventory(this IClientSession session, int itemVnum = 1, short amount = 1, bool isEquiped = false, InventoryItem item = null, bool sendPackets = true) + => await session.EmitEventAsync(new InventoryRemoveItemEvent(itemVnum, amount, isEquiped, item, sendPackets)); + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game.Extensions/ItemExtension/ItemExtension.cs b/srcs/WingsAPI.Game.Extensions/ItemExtension/ItemExtension.cs new file mode 100644 index 0000000..fbb77d3 --- /dev/null +++ b/srcs/WingsAPI.Game.Extensions/ItemExtension/ItemExtension.cs @@ -0,0 +1,27 @@ +using PhoenixLib.MultiLanguage; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Items; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace WingsAPI.Game.Extensions.ItemExtension.Item +{ + public static class ItemExtension + { + public static bool IsTimeSpaceStone(this IGameItem item) => item.Data[0] == 900; + + public static bool IsTimeSpaceChest(this IGameItem gameItem) => gameItem.Data[0] == 4; + + public static string GetItemName(this IGameItem gameItem, IGameLanguageService gameLanguage, RegionLanguageType regionLanguageType) + => gameLanguage.GetLanguage(GameDataType.Item, gameItem.Name, regionLanguageType); + + public static bool ShouldSendAmuletPacket(this IClientSession session, EquipmentType type) => + type != EquipmentType.CostumeHat && type != EquipmentType.CostumeSuit && type != EquipmentType.WeaponSkin; + + public static InventoryItem CreateInventoryItem(this IGameItemInstanceFactory instanceFactory, int vnum) => new() + { + ItemInstance = instanceFactory.CreateItem(vnum) + }; + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game.Extensions/ManagerExtensions.cs b/srcs/WingsAPI.Game.Extensions/ManagerExtensions.cs new file mode 100644 index 0000000..22ac25d --- /dev/null +++ b/srcs/WingsAPI.Game.Extensions/ManagerExtensions.cs @@ -0,0 +1,18 @@ +using WingsAPI.Communication; +using WingsEmu.Game._enum; + +namespace WingsAPI.Game.Extensions +{ + public static class ManagerExtensions + { + public static ManagerResponseType ToManagerType(this RpcResponseType responseType) + { + return responseType switch + { + RpcResponseType.MAINTENANCE_MODE => ManagerResponseType.Maintenance, + RpcResponseType.SUCCESS => ManagerResponseType.Success, + _ => ManagerResponseType.Error + }; + } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game.Extensions/MinilandExtensions/MinigameExtensions.cs b/srcs/WingsAPI.Game.Extensions/MinilandExtensions/MinigameExtensions.cs new file mode 100644 index 0000000..e88ff7b --- /dev/null +++ b/srcs/WingsAPI.Game.Extensions/MinilandExtensions/MinigameExtensions.cs @@ -0,0 +1,45 @@ +using System; +using WingsEmu.Game.Configurations.Miniland; +using WingsEmu.Game.Networking; + +namespace WingsAPI.Game.Extensions.MinilandExtensions +{ + public static class MinigameExtensions + { + /// + /// Returns True if the MinigamePoints was removed or False if it wasn't. + /// + /// + /// + /// + /// + public static bool RemoveMinigamePoints(this IClientSession session, short minigamePoints, MinigameConfiguration minigameConfiguration) + { + short minigamePointsToRemove = Math.Abs(minigamePoints); + if (minigamePointsToRemove > session.PlayerEntity.MinilandPoint) + { + return false; + } + + session.PlayerEntity.MinilandPoint -= minigamePointsToRemove; + session.SendMinigamePoints(minigameConfiguration); + return true; + } + + public static void AddMinigamePoints(this IClientSession session, short minigamePoints, MinigameConfiguration minigameConfiguration) + { + short minigamePointsToRemove = Math.Abs(minigamePoints); + + if (minigameConfiguration.Configuration.MaxmimumMinigamePoints < minigamePointsToRemove + session.PlayerEntity.MinilandPoint) + { + session.PlayerEntity.MinilandPoint = (short)minigameConfiguration.Configuration.MaxmimumMinigamePoints; + } + else + { + session.PlayerEntity.MinilandPoint += minigamePointsToRemove; + } + + session.SendMinigamePoints(minigameConfiguration); + } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game.Extensions/MinilandExtensions/MinigamePacketExtensions.cs b/srcs/WingsAPI.Game.Extensions/MinilandExtensions/MinigamePacketExtensions.cs new file mode 100644 index 0000000..78f1aa9 --- /dev/null +++ b/srcs/WingsAPI.Game.Extensions/MinilandExtensions/MinigamePacketExtensions.cs @@ -0,0 +1,85 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text; +using WingsEmu.Game.Configurations.Miniland; +using WingsEmu.Game.Miniland; +using WingsEmu.Game.Networking; + +namespace WingsAPI.Game.Extensions.MinilandExtensions +{ + public static class MinigamePacketExtensions + { + public static string GenerateMloPmg(this IClientSession session, MapDesignObject mlobj, IEnumerable itemsToShow, MinigameConfiguration minigameConfiguration) + { + var initialString = new StringBuilder( + $"mlo_pmg {mlobj.InventoryItem.ItemInstance.ItemVNum.ToString()} {session.PlayerEntity.MinilandPoint.ToString()} {(mlobj.ShowMinigameDurabilityWarning(minigameConfiguration) ? 1 : 0).ToString()}" + + $" {(mlobj.IsMinigameInventoryRewardsFull(minigameConfiguration) ? 1 : 0).ToString()}"); + + var itemsToShowList = itemsToShow.ToList(); + + for (int i = itemsToShowList.Count; i < 14; i++) + { + itemsToShowList.Add(new MinigameReward()); + } + + foreach (MinigameReward itemToShow in itemsToShowList) + { + initialString.Append($" {itemToShow.Vnum.ToString()} {itemToShow.Amount.ToString()}"); + } + + return initialString.ToString(); + } + + public static string GenerateMloMg(this IClientSession session, MapDesignObject mlobj, MinigameConfiguration minigameConfiguration) => + $"mlo_mg {mlobj.InventoryItem.ItemInstance.ItemVNum.ToString()} {session.PlayerEntity.MinilandPoint.ToString()} {(mlobj.ShowMinigameDurabilityWarning(minigameConfiguration) ? 1 : 0).ToString()}" + + $" {(mlobj.IsMinigameInventoryRewardsFull(minigameConfiguration) ? 1 : 0).ToString()} {mlobj.InventoryItem.ItemInstance.DurabilityPoint.ToString()} {mlobj.InventoryItem.ItemInstance.GameItem.MinilandObjectPoint.ToString()}"; + + public static string GenerateMinigamePoints(this IClientSession session, MinigameConfiguration minigameConfiguration) + => $"mlpt {session.PlayerEntity.MinilandPoint.ToString()} {minigameConfiguration.Configuration.MinigamePointsCostPerMinigame.ToString()}"; + + public static string GenerateMinigameReward(int vnum, int amount) => $"mlo_rw {vnum.ToString()} {amount.ToString()}"; + public static string GenerateMinigameRewardLevel(RewardLevel rewardLevel) => $"mlo_lv {((sbyte)rewardLevel).ToString()}"; + + public static string GenerateMinigameInfo(this MapDesignObject mapObj, IClientSession session, + MinigameConfiguration minigameConfiguration, MinigameScoresHolder minigameScoresHolder) + { + IOrderedEnumerable scores = minigameScoresHolder.Scores.OrderBy(x => x.ScoreRange.Minimum); + + var packetBase = new StringBuilder( + $"mlo_info {(session.CurrentMapInstance.Id == session.PlayerEntity.Miniland.Id ? 1 : 0).ToString()} {mapObj.InventoryItem.ItemInstance.ItemVNum.ToString()}" + + $" {mapObj.InventorySlot.ToString()} {session.PlayerEntity.MinilandPoint.ToString()}" + + $" {(mapObj.ShowMinigameDurabilityWarning(minigameConfiguration) ? 1 : 0).ToString()}" + + $" {(mapObj.IsMinigameInventoryRewardsFull(minigameConfiguration) ? 1 : 0).ToString()}"); + + foreach (ScoreHolder score in scores) + { + packetBase.Append($" {score.ScoreRange.Minimum.ToString()} {score.ScoreRange.Maximum.ToString()}"); + } + + return packetBase.ToString(); + } + + public static string GenerateMinilandObject(this MapDesignObject mapObj, bool deleted) => + $"mlobj {(deleted ? 0 : 1).ToString()} {mapObj.InventoryItem.Slot.ToString()} {mapObj.MapX.ToString()} {mapObj.MapY.ToString()}" + + $" {mapObj.InventoryItem.ItemInstance.GameItem.Width.ToString()} {mapObj.InventoryItem.ItemInstance.GameItem.Height.ToString()} 0 {mapObj.InventoryItem.ItemInstance.DurabilityPoint.ToString()}" + + $" 0 {(mapObj.InventoryItem.ItemInstance.GameItem.IsWarehouse ? 1 : 0).ToString()}"; + + public static string GenerateEffect(this MapDesignObject mapObj, bool removed) => + $"eff_g {(mapObj.InventoryItem.ItemInstance.GameItem?.EffectValue ?? mapObj.InventoryItem.ItemInstance.Design).ToString()}" + + $" {mapObj.MapX:00}{mapObj.MapY:00}" + + $" {mapObj.MapX.ToString()} {mapObj.MapY.ToString()} {(removed ? 1 : 0).ToString()}"; + + public static void SendMinilandYieldInfo(this IClientSession session, MapDesignObject mapObject, IEnumerable minigameRewards, MinigameConfiguration minigameConfiguration) + => session.SendPacket(session.GenerateMloPmg(mapObject, minigameRewards, minigameConfiguration)); + + public static void SendMinilandDurabilityInfo(this IClientSession session, MapDesignObject mapObject, MinigameConfiguration minigameConfiguration) + => session.SendPacket(session.GenerateMloMg(mapObject, minigameConfiguration)); + + public static void SendMinigamePoints(this IClientSession session, MinigameConfiguration minigameConfiguration) => session.SendPacket(session.GenerateMinigamePoints(minigameConfiguration)); + public static void SendMinigameReward(this IClientSession session, int vnum, int amount) => session.SendPacket(GenerateMinigameReward(vnum, amount)); + public static void SendMinigameRewardLevel(this IClientSession session, RewardLevel rewardLevel) => session.SendPacket(GenerateMinigameRewardLevel(rewardLevel)); + + public static void SendMinigameInfo(this IClientSession session, MapDesignObject mapObj, MinigameConfiguration minigameConfiguration, MinigameScoresHolder minigameScoresHolder) + => session.SendPacket(mapObj.GenerateMinigameInfo(session, minigameConfiguration, minigameScoresHolder)); + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game.Extensions/MinilandExtensions/MinigameUtilitiesExtensions.cs b/srcs/WingsAPI.Game.Extensions/MinilandExtensions/MinigameUtilitiesExtensions.cs new file mode 100644 index 0000000..4ebe973 --- /dev/null +++ b/srcs/WingsAPI.Game.Extensions/MinilandExtensions/MinigameUtilitiesExtensions.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using WingsEmu.Game._enum; +using WingsEmu.Game.Configurations.Miniland; +using WingsEmu.Game.Miniland; + +namespace WingsAPI.Game.Extensions.MinilandExtensions +{ + public static class MinigameUtilitiesExtensions + { + public static bool IsMinigameInventoryRewardsFull(this MapDesignObject mapObj, MinigameConfiguration minigameConfiguration) => + mapObj.Level1BoxAmount == minigameConfiguration.Configuration.MinigameMaximumRewards || + mapObj.Level2BoxAmount == minigameConfiguration.Configuration.MinigameMaximumRewards || + mapObj.Level3BoxAmount == minigameConfiguration.Configuration.MinigameMaximumRewards || + mapObj.Level4BoxAmount == minigameConfiguration.Configuration.MinigameMaximumRewards || + mapObj.Level5BoxAmount == minigameConfiguration.Configuration.MinigameMaximumRewards; + + public static bool ShowMinigameDurabilityWarning(this MapDesignObject mapObj, MinigameConfiguration minigameConfiguration) => + mapObj.InventoryItem.ItemInstance.DurabilityPoint < minigameConfiguration.Configuration.DurabilityWarning; + + public static IEnumerable GetYieldRewardEnumerable(this MapDesignObject mapObject) => + new List + { + new() + { + Vnum = mapObject.Level1BoxAmount < 1 ? 0 : (int)ItemVnums.MINIGAME_REWARD_CHEST_1, + Amount = mapObject.Level1BoxAmount + }, + new() + { + Vnum = mapObject.Level2BoxAmount < 1 ? 0 : (int)ItemVnums.MINIGAME_REWARD_CHEST_2, + Amount = mapObject.Level2BoxAmount + }, + new() + { + Vnum = mapObject.Level3BoxAmount < 1 ? 0 : (int)ItemVnums.MINIGAME_REWARD_CHEST_3, + Amount = mapObject.Level3BoxAmount + }, + new() + { + Vnum = mapObject.Level4BoxAmount < 1 ? 0 : (int)ItemVnums.MINIGAME_REWARD_CHEST_4, + Amount = mapObject.Level4BoxAmount + }, + new() + { + Vnum = mapObject.Level5BoxAmount < 1 ? 0 : (int)ItemVnums.MINIGAME_REWARD_CHEST_5, + Amount = mapObject.Level5BoxAmount + } + }; + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game.Extensions/MinilandExtensions/MinilandPacketExtensions.cs b/srcs/WingsAPI.Game.Extensions/MinilandExtensions/MinilandPacketExtensions.cs new file mode 100644 index 0000000..5161fcd --- /dev/null +++ b/srcs/WingsAPI.Game.Extensions/MinilandExtensions/MinilandPacketExtensions.cs @@ -0,0 +1,34 @@ +using WingsEmu.Game._i18n; +using WingsEmu.Game.Configurations.Miniland; +using WingsEmu.Game.Miniland; +using WingsEmu.Game.Networking; + +namespace WingsAPI.Game.Extensions.MinilandExtensions +{ + public static class MinilandPacketExtensions + { + public static string GenerateMinilandPrivateInformation(this IClientSession session, IMinilandManager minilandManager, IGameLanguageService languageService) + { + int visitCount = minilandManager.GetMinilandVisitCounter(session.PlayerEntity.Id).ConfigureAwait(false).GetAwaiter().GetResult(); + Miniland miniland = minilandManager.GetMinilandConfiguration(session.PlayerEntity.Miniland); + return $"mlinfo {miniland.MapItemVnum.ToString()} {session.PlayerEntity.MinilandPoint.ToString()} 100 {visitCount}" + + $" {session.PlayerEntity.LifetimeStats.TotalMinilandVisits} {minilandManager.GetMinilandMaximumCapacity(session.PlayerEntity.Id).ToString()} {((byte)session.PlayerEntity.MinilandState).ToString()} {miniland.MapItemVnum.ToString()}" + + $" {session.GetMinilandSerializedMessage(languageService)}"; + } + + public static string GenerateMinilandPublicInformation(this IClientSession session, IMinilandManager minilandManager, IGameLanguageService languageService) + { + IClientSession minilandOwner = minilandManager.GetSessionByMiniland(session.CurrentMapInstance); + Miniland miniland = minilandManager.GetMinilandConfiguration(minilandOwner.PlayerEntity.Miniland); + int visitCount = minilandManager.GetMinilandVisitCounter(minilandOwner.PlayerEntity.Id).ConfigureAwait(false).GetAwaiter().GetResult(); + return $"mlinfobr {miniland.MapItemVnum.ToString()} {minilandOwner.PlayerEntity.Name} {visitCount}" + + $" {minilandOwner.PlayerEntity.LifetimeStats.TotalMinilandVisits} {minilandManager.GetMinilandMaximumCapacity(minilandOwner.PlayerEntity.Id).ToString()} {minilandOwner.GetMinilandSerializedMessage(languageService)}"; + } + + public static void SendMinilandPrivateInformation(this IClientSession session, IMinilandManager minilandManager, IGameLanguageService languageService) => + session.SendPacket(session.GenerateMinilandPrivateInformation(minilandManager, languageService)); + + public static void SendMinilandPublicInformation(this IClientSession session, IMinilandManager minilandManager, IGameLanguageService languageService) => + session.SendPacket(session.GenerateMinilandPublicInformation(minilandManager, languageService)); + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game.Extensions/MinilandExtensions/MinilandUtilitiesExtensions.cs b/srcs/WingsAPI.Game.Extensions/MinilandExtensions/MinilandUtilitiesExtensions.cs new file mode 100644 index 0000000..5e05f0d --- /dev/null +++ b/srcs/WingsAPI.Game.Extensions/MinilandExtensions/MinilandUtilitiesExtensions.cs @@ -0,0 +1,14 @@ +using WingsEmu.Game._i18n; +using WingsEmu.Game.Networking; + +namespace WingsAPI.Game.Extensions.MinilandExtensions +{ + public static class MinilandUtilitiesExtensions + { + public static string GetMinilandSerializedMessage(this IClientSession session, IGameLanguageService languageService) => + session.GetMinilandCleanMessage(languageService).Replace(' ', '^'); + + public static string GetMinilandCleanMessage(this IClientSession session, IGameLanguageService languageService) => + session.PlayerEntity.MinilandMessage == string.Empty ? languageService.GetLanguage(GameDialogKey.MINILAND_WELCOME_MESSAGE, session.UserLanguage) : session.PlayerEntity.MinilandMessage; + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game.Extensions/PacketGeneration/DelayPacketsExtensions.cs b/srcs/WingsAPI.Game.Extensions/PacketGeneration/DelayPacketsExtensions.cs new file mode 100644 index 0000000..99c6983 --- /dev/null +++ b/srcs/WingsAPI.Game.Extensions/PacketGeneration/DelayPacketsExtensions.cs @@ -0,0 +1,13 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsEmu.Game.Networking; + +namespace WingsAPI.Game.Extensions.PacketGeneration +{ + public static class DelayPacketsExtensions + { + public static string GenerateDelayPacket(this IClientSession session, int delay, int type, string argument) => $"delay {delay} {type} {argument}"; + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game.Extensions/PacketGeneration/InfoPacketsExtensions.cs b/srcs/WingsAPI.Game.Extensions/PacketGeneration/InfoPacketsExtensions.cs new file mode 100644 index 0000000..977cd42 --- /dev/null +++ b/srcs/WingsAPI.Game.Extensions/PacketGeneration/InfoPacketsExtensions.cs @@ -0,0 +1,17 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsAPI.Game.Extensions.PacketGeneration +{ + public static class InfoPacketsExtensions + { + public static string GenerateSayPacket(this IClientSession session, string msg, ChatMessageColorType color) => + $"say {(byte)session.PlayerEntity.Type} {session.PlayerEntity.Id} {(byte)color} {msg}"; + + public static string GenerateInfoPacket(this IClientSession session, string message) => $"info {message}"; + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game.Extensions/PacketGeneration/LoginPacketsExtensions.cs b/srcs/WingsAPI.Game.Extensions/PacketGeneration/LoginPacketsExtensions.cs new file mode 100644 index 0000000..3d0e96f --- /dev/null +++ b/srcs/WingsAPI.Game.Extensions/PacketGeneration/LoginPacketsExtensions.cs @@ -0,0 +1,14 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace WingsAPI.Game.Extensions.PacketGeneration +{ + public static class LoginPacketsExtensions + { + public static string GenerateFailcPacket(this IClientSession session, LoginFailType failType) => $"failc {(short)failType}"; + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game.Extensions/PacketGeneration/MailPacketsExtensions.cs b/srcs/WingsAPI.Game.Extensions/PacketGeneration/MailPacketsExtensions.cs new file mode 100644 index 0000000..85aabc0 --- /dev/null +++ b/srcs/WingsAPI.Game.Extensions/PacketGeneration/MailPacketsExtensions.cs @@ -0,0 +1,91 @@ +using System.Collections.Generic; +using System.Linq; +using WingsEmu.Game.Mails; +using WingsEmu.Game.Networking; + +namespace WingsAPI.Game.Extensions.PacketGeneration +{ + public static class MailPacketsExtensions + { + /* Generate Packets */ + + public static string GeneratePost(this IClientSession session, CharacterNote characterNote, byte type) => + $"post 1 {type} {characterNote.NoteSlot} 0 {(characterNote.IsOpened ? 1 : 0)} {characterNote.Date:yyMMddHHmm} " + + $"{(type == 2 ? characterNote.ReceiverName : characterNote.SenderName)} {characterNote.Title}"; + + public static string GeneratePostMessage(this IClientSession session, CharacterNote characterNote, byte type) => + $"post 5 {type} {characterNote.NoteSlot} 0 0 {(byte)characterNote.SenderClass} " + + $"{(byte)characterNote.SenderGender} -1 {(byte)characterNote.SenderHairStyle} {(byte)characterNote.SenderHairColor} {characterNote.EquipmentPackets} " + + $"{characterNote.SenderName} {characterNote.Title.Replace(' ', (char)0xB)} {characterNote.Message.Replace(' ', (char)0xB)}"; + + public static string GenerateParcel(this IClientSession session, CharacterMail characterMail) => + $"parcel 1 1 {characterMail.MailSlot} {(byte)characterMail.MailGiftType} 0 " + + $"{characterMail.Date:yyMMddHHmm} {characterMail.SenderName} {characterMail.ItemInstance.ItemVNum} " + + $"{characterMail.ItemInstance.Amount} {(byte)characterMail.ItemInstance.GameItem.Type}"; + + public static string GenerateParcelDelete(this IClientSession session, byte type, long mailId) => $"parcel {type} 1 {mailId}"; + + public static string GenerateNoteDelete(this IClientSession session, long noteId, bool isSenderCopy) => $"post 2 {(isSenderCopy ? 2 : 1)} {noteId}"; + + /* Send Packets */ + + public static void SendPost(this IClientSession session, CharacterNote characterNote, byte type) => + session.SendPacket(session.GeneratePost(characterNote, type)); + + public static void SendPostMessage(this IClientSession session, CharacterNote characterNote, byte type) => + session.SendPacket(session.GeneratePostMessage(characterNote, type)); + + public static void SendParcel(this IClientSession session, CharacterMail characterMail) => session.SendPacket(session.GenerateParcel(characterMail)); + + public static void SendParcelDelete(this IClientSession session, byte type, long mailId) => session.SendPacket(session.GenerateParcelDelete(type, mailId)); + + public static void SendNoteDelete(this IClientSession session, long noteId, bool isSenderCopy) => session.SendPacket(session.GenerateNoteDelete(noteId, isSenderCopy)); + + public static void SendMailPacket(this IClientSession session, CharacterNote characterNote) + { + if (!characterNote.IsSenderCopy && characterNote.ReceiverId == session.PlayerEntity.Id) + { + session.SendPost(characterNote, 1); + return; + } + + session.SendPost(characterNote, 2); + } + + public static byte GetNextMailSlot(this IClientSession session) + { + byte slot; + IEnumerable mails = session.PlayerEntity.MailNoteComponent.GetMails().ToArray(); + for (slot = 0; slot < mails.Count(); slot++) + { + CharacterMail mail = session.PlayerEntity.MailNoteComponent.GetMail(slot); + if (mail != null) + { + continue; + } + + break; + } + + return slot; + } + + public static byte GetNextNoteSlot(this IClientSession session, bool isSenderCopy) + { + byte slot; + CharacterNote[] notesCopy = session.PlayerEntity.MailNoteComponent.GetNotes().Where(x => x.IsSenderCopy).ToArray(); + for (slot = 0; slot < notesCopy.Length; slot++) + { + CharacterNote mail = session.PlayerEntity.MailNoteComponent.GetNote(slot, isSenderCopy); + if (mail != null) + { + continue; + } + + break; + } + + return slot; + } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game.Extensions/PacketGeneration/MapExtensions.cs b/srcs/WingsAPI.Game.Extensions/PacketGeneration/MapExtensions.cs new file mode 100644 index 0000000..010bfb4 --- /dev/null +++ b/srcs/WingsAPI.Game.Extensions/PacketGeneration/MapExtensions.cs @@ -0,0 +1,76 @@ +using System; +using System.Linq; +using PhoenixLib.Scheduler; +using WingsEmu.Game; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Extensions.Mates; +using WingsEmu.Game.Maps; +using WingsEmu.Packets.Enums; + +namespace WingsAPI.Game.Extensions.PacketGeneration +{ + public static class MapExtensions + { + public static string GenerateDrop(this MapItem mapItem) => + $"drop {mapItem.ItemVNum} {mapItem.TransportId} {mapItem.PositionX} {mapItem.PositionY} {mapItem.Amount} {(mapItem.IsQuest ? 1 : 0)} {(mapItem is MonsterMapItem monsterMapItem ? monsterMapItem.OwnerId ?? -1 : -1)}"; + + public static string GenerateMapDesignObjects(this IMapInstance map) => map.MapDesignObjects.Aggregate("mltobj", (current, mp) => current + + $" {mp.InventoryItem.ItemInstance.ItemVNum.ToString()}.{mp.InventoryItem.Slot.ToString()}.{mp.MapX.ToString()}.{mp.MapY.ToString()}"); + + public static void BroadcastDrop(this MapItem mapItem) => mapItem.MapInstance.Broadcast(mapItem.GenerateDrop()); + + public static void AddPortalToMap(this IMapInstance mapInstance, IPortalEntity portal, IScheduler scheduler = null, int timeInSeconds = 0, bool isTemporary = false) + { + mapInstance.Portals.Add(portal); + mapInstance.Broadcast(portal.GenerateGp()); + if (isTemporary) + { + scheduler?.Schedule(TimeSpan.FromSeconds(timeInSeconds), o => { mapInstance.DeletePortal(portal); }); + } + } + + public static void DeletePortal(this IMapInstance mapInstance, IPortalEntity portal) + { + mapInstance.Portals.Remove(portal); + mapInstance.MapClear(); + } + + public static IPortalEntity GetClosestPortal(this IMapInstance mapInstance, short posX, short posY, PortalType portalType = PortalType.TSNormal) + { + return mapInstance.Portals.Where(x => x.Type == portalType).OrderBy(x => Math.Abs(x.PositionX - posX)).ThenBy(x => Math.Abs(x.PositionY - posY)).FirstOrDefault(); + } + + public static void MapClear(this IMapInstance mapInstance, bool onlyItemsAndPortals = false) + { + mapInstance.Broadcast(mapInstance.GenerateMapClear()); + mapInstance.Broadcast(mapInstance.GetEntitiesOnMapPackets(onlyItemsAndPortals)); + } + + public static void BroadcastTimeSpacePartnerInfo(this IMapInstance mapInstance) + { + if (mapInstance.MapInstanceType != MapInstanceType.TimeSpaceInstance) + { + return; + } + + foreach (INpcEntity npc in mapInstance.GetAliveNpcs(x => x.CharacterPartnerId.HasValue)) + { + if (!npc.CharacterPartnerId.HasValue) + { + continue; + } + + IPlayerEntity player = mapInstance.GetCharacterById(npc.CharacterPartnerId.Value); + if (player == null) + { + continue; + } + + player.Session.SendMateControl(npc); + player.Session.SendCondMate(npc); + } + } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game.Extensions/PacketGeneration/ModalPacketsExtensions.cs b/srcs/WingsAPI.Game.Extensions/PacketGeneration/ModalPacketsExtensions.cs new file mode 100644 index 0000000..88b6db3 --- /dev/null +++ b/srcs/WingsAPI.Game.Extensions/PacketGeneration/ModalPacketsExtensions.cs @@ -0,0 +1,19 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsAPI.Packets.Enums; +using WingsEmu.Game.Networking; + +namespace WingsAPI.Game.Extensions.PacketGeneration +{ + public static class ModalPacketsExtensions + { + public static string GenerateModalPacket(this IClientSession session, string message, ModalType type) => $"modal {(byte)type} {message}"; + + public static void SendModal(this IClientSession session, string message, ModalType type) + { + session.SendPacket(session.GenerateModalPacket(message, type)); + } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game.Extensions/PacketGeneration/NosBazaarPacketsExtensions.cs b/srcs/WingsAPI.Game.Extensions/PacketGeneration/NosBazaarPacketsExtensions.cs new file mode 100644 index 0000000..7e5b912 --- /dev/null +++ b/srcs/WingsAPI.Game.Extensions/PacketGeneration/NosBazaarPacketsExtensions.cs @@ -0,0 +1,127 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Generic; +using System.Linq; +using WingsAPI.Game.Extensions.Bazaar; +using WingsAPI.Packets.Enums.Bazaar; +using WingsEmu.DTOs.Items; +using WingsEmu.Game.Algorithm; +using WingsEmu.Game.Bazaar; +using WingsEmu.Game.Bazaar.Configuration; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Networking; + +namespace WingsAPI.Game.Extensions.PacketGeneration +{ + public static class NosBazaarPacketsExtensions + { + public static string GenerateBazaarItemsPacket(this IClientSession session) => "rc_blist"; + + public static string GenerateRcScalc(bool returned, long pricePerUnit, int soldAmount, int totalAmount, long taxes, long totalProfit, long itemVNum) + => $"rc_scalc {(returned ? 1 : 0).ToString()} {pricePerUnit.ToString()} {soldAmount.ToString()} {totalAmount.ToString()} {taxes.ToString()} {totalProfit.ToString()} {itemVNum.ToString()}"; + //rc_scalc 1 233 0 1 0 0 2038 + + public static string GenerateRcBuy(bool returned, long itemVNum, string seller, short amount, long pricePerUnit, long upgrade, long rarity) + => $"rc_buy {(returned ? 1 : 0).ToString()} {itemVNum.ToString()} {seller} {amount.ToString()} {pricePerUnit.ToString()} 0 {upgrade.ToString()} {rarity.ToString()}"; + + public static string GenerateCharacterBazaarItemsPacket(ushort index, IReadOnlyCollection items, BazaarListedItemType filter, IItemsManager itemsManager, + ICharacterAlgorithm characterAlgorithm, BazaarConfiguration bazaarConfiguration) + { + const string header = "rc_slist"; + if (items == null || !items.Any()) + { + return $"{header} 0"; + } + + string itemsPacket = string.Empty; + int ignoreCounter = index * bazaarConfiguration.ItemsPerIndex; + int sendCounter = 0; + foreach (BazaarItem bazaarItem in items.OrderByDescending(i => i.BazaarItemDto.Id)) + { + BazaarListedItemType itemStatus = bazaarItem.BazaarItemDto.GetBazaarItemStatus(); + if (filter != BazaarListedItemType.All && itemStatus != filter) + { + continue; + } + + if (ignoreCounter > 0) + { + ignoreCounter--; + continue; + } + + if (sendCounter >= bazaarConfiguration.ItemsPerIndex) + { + break; + } + + sendCounter++; + + string minutesPassed = (itemStatus == BazaarListedItemType.DeadlineExpired || (bazaarItem.BazaarItemDto.Amount - bazaarItem.BazaarItemDto.SoldAmount) == 0 + ? -1 + : (int)(bazaarItem.BazaarItemDto.ExpiryDate - DateTime.UtcNow).TotalMinutes).ToString(); + + GameItemInstance itemInstance = bazaarItem.Item.Type != ItemInstanceType.NORMAL_ITEM ? bazaarItem.Item : null; + string eqPacket = itemInstance == null ? string.Empty : itemInstance.GenerateEInfo(itemsManager, characterAlgorithm).Replace("e_info ", string.Empty).Replace(" ", "^"); + itemsPacket += + $" {bazaarItem.BazaarItemDto.Id.ToString()}|{bazaarItem.BazaarItemDto.CharacterId.ToString()}|{bazaarItem.Item.ItemVNum.ToString()}|{bazaarItem.BazaarItemDto.SoldAmount.ToString()}" + + $"|{bazaarItem.BazaarItemDto.Amount.ToString()}|{(bazaarItem.BazaarItemDto.IsPackage ? 1 : 0).ToString()}|{bazaarItem.BazaarItemDto.PricePerItem.ToString()}|{((byte)itemStatus).ToString()}" + + $"|{minutesPassed}|{(bazaarItem.BazaarItemDto.UsedMedal ? 1 : 0).ToString()}|0|{bazaarItem.Item.Rarity.ToString()}|{bazaarItem.Item.Upgrade.ToString()}" + + $"|{(itemInstance?.GetInternalRunesCount() ?? 0).ToString()}|0|{eqPacket}"; + } + + return $"rc_slist {index.ToString()}{itemsPacket}"; //Space removed cause it will be added by the generation of the packet + } + + public static string GenerateSearchResponseBazaarItemsPacket(int index, IReadOnlyCollection items, IItemsManager itemsManager, ICharacterAlgorithm characterAlgorithm, + BazaarConfiguration bazaarConfiguration) + { + const string header = "rc_blist"; + if (items == null || !items.Any()) + { + return $"{header} 0"; + } + + string itemsPacket = string.Empty; + foreach (BazaarItem bazaarItem in items) + { + string minutesPassed = ((int)(bazaarItem.BazaarItemDto.ExpiryDate - DateTime.UtcNow).TotalMinutes).ToString(); + + GameItemInstance itemInstance = bazaarItem.Item.Type != ItemInstanceType.NORMAL_ITEM ? bazaarItem.Item : null; + string eqPacket = itemInstance == null ? string.Empty : itemInstance.GenerateEInfo(itemsManager, characterAlgorithm).Replace("e_info ", string.Empty).Replace(" ", "^"); + + itemsPacket += $" {bazaarItem.BazaarItemDto.Id.ToString()}|{bazaarItem.BazaarItemDto.CharacterId.ToString()}|{bazaarItem.OwnerName}|{bazaarItem.Item.ItemVNum.ToString()}" + + $"|{(bazaarItem.BazaarItemDto.Amount - bazaarItem.BazaarItemDto.SoldAmount).ToString()}|{(bazaarItem.BazaarItemDto.IsPackage ? 1 : 0).ToString()}" + + $"|{bazaarItem.BazaarItemDto.PricePerItem.ToString()}|{minutesPassed}|2|0|{bazaarItem.Item.Rarity.ToString()}|{bazaarItem.Item.Upgrade.ToString()}" + + $"|{itemInstance?.GetRunesCount().ToString()}|0|{eqPacket}"; + } + + return $"rc_blist {index.ToString()}{itemsPacket}"; //Space removed cause it will be added by the generation of the packet + } + + public static void SendBazaarItems(this IClientSession session) => session.SendPacket(session.GenerateBazaarItemsPacket()); + + public static void SendSearchResponseBazaarItems(this IClientSession session, int index, IReadOnlyCollection items, IItemsManager itemsManager, + ICharacterAlgorithm characterAlgorithm, BazaarConfiguration bazaarConfiguration) + => session.SendPacket(GenerateSearchResponseBazaarItemsPacket(index, items, itemsManager, characterAlgorithm, bazaarConfiguration)); + + public static void SendCharacterListedBazaarItems(this IClientSession session, ushort index, IReadOnlyCollection items, BazaarListedItemType filter, IItemsManager itemsManager, + ICharacterAlgorithm characterAlgorithm, BazaarConfiguration bazaarConfiguration) + => session.SendPacket(GenerateCharacterBazaarItemsPacket(index, items, filter, itemsManager, characterAlgorithm, bazaarConfiguration)); + + public static void SendBazaarResponseItemRemove(this IClientSession session, bool returned, long pricePerUnit, int soldAmount, int totalAmount, long taxes, long totalProfit, long itemVNum) + { + session.SendPacket(GenerateRcScalc(returned, pricePerUnit, soldAmount, totalAmount, taxes, totalProfit, itemVNum)); + } + + public static void SendBazaarResponseItemBuy(this IClientSession session, bool returned, long itemVNum, string seller, short amount, long pricePerUnit, long upgrade, long rarity) + { + session.SendPacket(GenerateRcBuy(returned, itemVNum, seller, amount, pricePerUnit, upgrade, rarity)); + } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game.Extensions/PacketGeneration/NpcPacketsExtensions.cs b/srcs/WingsAPI.Game.Extensions/PacketGeneration/NpcPacketsExtensions.cs new file mode 100644 index 0000000..7851d1c --- /dev/null +++ b/srcs/WingsAPI.Game.Extensions/PacketGeneration/NpcPacketsExtensions.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using System.Linq; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Quests; + +namespace WingsAPI.Game.Extensions.PacketGeneration +{ + public static class NpcPacketsExtensions + { + public static void SendNpcDialog(this IClientSession session, INpcEntity npcEntity) => session.SendPacket(session.GenerateNpcDialog(npcEntity)); + public static void SendNpcQuestDialog(this IClientSession session, INpcEntity npcEntity) => session.SendPacket(GenerateNpcQuestDialog(npcEntity)); + + public static string GenerateNpcDialog(this IClientSession session, INpcEntity npcEntity) + { + IReadOnlyCollection specialDialogQuests = + session.PlayerEntity.GetCurrentQuests().Where(s => s.Quest.TalkerVnum == npcEntity.NpcVNum && s.Quest.DialogDuring != -1).ToList(); + int dialogId = specialDialogQuests.Any() ? specialDialogQuests.First().Quest.DialogDuring : npcEntity.Dialog; + return $"npc_req 2 {npcEntity.Id.ToString()} {dialogId}"; + } + + public static string GenerateNpcQuestDialog(this INpcEntity npcEntity) => $"npc_req 2 {npcEntity.Id.ToString()} {npcEntity.QuestDialog.ToString()}"; + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game.Extensions/Quests/QuestExtensions.cs b/srcs/WingsAPI.Game.Extensions/Quests/QuestExtensions.cs new file mode 100644 index 0000000..da3112c --- /dev/null +++ b/srcs/WingsAPI.Game.Extensions/Quests/QuestExtensions.cs @@ -0,0 +1,551 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using PhoenixLib.Logging; +using WingsEmu.DTOs.Quests; +using WingsEmu.Game._enum; +using WingsEmu.Game.Algorithm; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Quests; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Character; + +namespace WingsAPI.Game.Extensions.Quests +{ + public static class QuestExtensions + { + private static readonly string _b64SqstIndex = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz{}"; + + + private static readonly QuestType[] DialogQuestTypes = + { + QuestType.DIALOG, + QuestType.DIALOG_2, + QuestType.DELIVER_ITEM_TO_NPC, + QuestType.GIVE_ITEM_TO_NPC, + QuestType.GIVE_ITEM_TO_NPC_2, + QuestType.GIVE_NPC_GOLD, + QuestType.DIALOG_WHILE_WEARING, + QuestType.DIALOG_WHILE_HAVING_ITEM + }; + + public static string GenerateTargetQuest(this IClientSession session, short x, short y, short mapId, int questId) => $"target {x} {y} {mapId} {questId}"; + public static string GenerateTargetOffQuest(this IClientSession session, short x, short y, short mapId, int questId) => $"targetoff {x} {y} {mapId} {questId}"; + + public static string GenerateQnpcPacket(this IClientSession session, short level, short npcVnum, short mapId) => + $"qnpc {npcVnum}|{mapId}|{level} -1|-1|-1 -1|-1|-1 -1|-1|-1 -1|-1|-1 -1|-1|-1 -1|-1|-1 -1|-1|-1 -1|-1|-1 -1|-1|-1"; + + public static string GenerateQrPacket(this IClientSession session, CharacterQuest quest, IReadOnlyCollection rndRewards, QuestsRatesConfiguration questRates) + { + List prizes = quest.Quest.Prizes; + if (prizes == null) + { + return ""; + } + + var qrPacket = new StringBuilder("qr "); + foreach (QuestPrizeDto prize in prizes) + { + switch (prize.RewardType) + { + case (byte)QuestRewardType.Reput: + case (byte)QuestRewardType.Exp: + case (byte)QuestRewardType.SecondExp: + case (byte)QuestRewardType.ThirdExp: + case (byte)QuestRewardType.JobExp: + case (byte)QuestRewardType.SecondJobExp: + qrPacket.Append($"{prize.RewardType.ToString()} 0 {prize.Data0.ToString()} "); + break; + case (byte)QuestRewardType.Gold: + qrPacket.Append($"{prize.RewardType.ToString()} 0 {prize.Data0 * questRates.GoldRate} "); + break; + case (byte)QuestRewardType.AllRewards: + qrPacket.Append(prize.Data0 == -1 ? "" : $"{prize.RewardType.ToString()} {prize.Data0.ToString()} 1 "); + qrPacket.Append(prize.Data1 == -1 ? "" : $"{prize.RewardType.ToString()} {prize.Data1.ToString()} 1 "); + qrPacket.Append(prize.Data2 == -1 ? "" : $"{prize.RewardType.ToString()} {prize.Data2.ToString()} 1 "); + qrPacket.Append(prize.Data3 == -1 ? "" : $"{prize.RewardType.ToString()} {prize.Data3.ToString()} 1 "); + break; + case (byte)QuestRewardType.SecondGold: + qrPacket.Append($"{prize.RewardType.ToString()} 0 {prize.Data0 * questRates.BaseGold * questRates.GoldRate} "); + break; + case (byte)QuestRewardType.ThirdGold: + qrPacket.Append($"{prize.RewardType.ToString()} 0 {prize.Data0 * quest.ObjectiveAmount.Sum(s => s.Value.RequiredAmount) * session.PlayerEntity.Level * questRates.GoldRate} "); + break; + case (byte)QuestRewardType.Unknow: + case (byte)QuestRewardType.ItemsDependingOnClass: + switch (session.PlayerEntity.Class) + { + case ClassType.Swordman: + qrPacket.Append(prize.Data0 == -1 ? "" : $"{prize.RewardType.ToString()} {prize.Data0.ToString()} {prize.Data4.ToString()} "); + break; + case ClassType.Archer: + qrPacket.Append(prize.Data1 == -1 ? "" : $"{prize.RewardType.ToString()} {prize.Data1.ToString()} {prize.Data4.ToString()} "); + break; + case ClassType.Magician: + qrPacket.Append(prize.Data2 == -1 ? "" : $"{prize.RewardType.ToString()} {prize.Data2.ToString()} {prize.Data4.ToString()} "); + break; + default: + qrPacket.Append(prize.Data3 == -1 ? "" : $"{prize.RewardType.ToString()} {prize.Data3.ToString()} {prize.Data4.ToString()} "); + break; + } + + break; + } + } + + foreach (CharacterQuestGeneratedReward rndReward in rndRewards) + { + qrPacket.Append($"{(byte)QuestRewardType.RandomReward} {rndReward.ItemVnum} {rndReward.Amount} "); + } + + for (int i = prizes.Count(s => s.RewardType != (byte)QuestRewardType.RandomReward) + rndRewards.Count; i < 4; i++) + { + qrPacket.Append("0 0 0 "); + } + + qrPacket.Append(quest.QuestId); + return qrPacket.ToString(); + } + + public static string GenerateQuestList(this IClientSession session, IQuestManager questManager, int? questToShowInfo = null) + { + IEnumerable quests = session.PlayerEntity.GetCurrentQuests(); + string header = "qstlist"; + if (quests == null) + { + return header; + } + + var packet = new StringBuilder(); + foreach (CharacterQuest quest in quests) + { + packet.Append($" {session.GenerateQuestData(quest, questManager, qstlistCall: true, questToShowInfo: questToShowInfo)}"); + } + + int soundFlowerEmptySlot = session.GetEmptyQuestSlot(QuestSlotType.GENERAL, true); + for (int i = 0; i < session.PlayerEntity.GetPendingSoundFlowerQuests(); i++) + { + packet.Append($" {soundFlowerEmptySlot + i}.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0"); + } + + return $"{header}{packet}"; + } + + public static string GenerateQuestProgress(this IClientSession session, IQuestManager questManager, int questVnum) + { + CharacterQuest currentQuest = session.PlayerEntity.GetCurrentQuest(questVnum); + return currentQuest == null ? string.Empty : $"qsti {session.GenerateQuestData(currentQuest, questManager, session.PlayerEntity.IsQuestCompleted(currentQuest))}"; + } + + private static string GenerateQuestData(this IClientSession session, CharacterQuest characterQuest, IQuestManager questManager, bool isFinished = false, bool qstlistCall = false, + int? questToShowInfo = null) + { + int questSlot = session.GetQuestSlot(characterQuest); + if (questSlot == -1) + { + Log.Debug($"The slot for quest {characterQuest.QuestId} was not found."); + return ""; + } + + IReadOnlyList objectives; + bool statusAdded; + var data = new StringBuilder($"{questSlot}.{characterQuest.QuestId}"); + + switch (characterQuest.SlotType) + { + case QuestSlotType.MAIN: + data.Append($".{characterQuest.QuestId}"); + break; + case QuestSlotType.GENERAL: + IReadOnlyCollection questlines = questManager.GetQuestlines(characterQuest.QuestId); + data.Append($".{(questlines.Any() ? questlines.First() : characterQuest.QuestId)}"); + break; + case QuestSlotType.SECONDARY: + data.Append(".0"); + break; + } + + data.Append($".{(byte)characterQuest.Quest.QuestType}"); + + switch (characterQuest.Quest.QuestType) + { + case QuestType.WIN_RAID_AND_TALK_TO_NPC: + case QuestType.DELIVER_ITEM_TO_NPC: + case QuestType.KILL_MONSTER_BY_VNUM: + case QuestType.GIVE_ITEM_TO_NPC: + case QuestType.GIVE_ITEM_TO_NPC_2: + case QuestType.DIALOG_WHILE_WEARING: + case QuestType.DIALOG_WHILE_HAVING_ITEM: + case QuestType.USE_ITEM_ON_TARGET: + case QuestType.CAPTURE_WITHOUT_KEEPING: + case QuestType.CAPTURE_AND_KEEP: + case QuestType.DROP_IN_TIMESPACE: + case QuestType.COMPLETE_TIMESPACE: + case QuestType.CRAFT_WITHOUT_KEEPING: + case QuestType.DROP_CHANCE: + case QuestType.DROP_CHANCE_2: + case QuestType.COLLECT: + case QuestType.DROP_HARDCODED: + case QuestType.KILL_X_MOBS_SOUND_FLOWER: + statusAdded = false; + objectives = characterQuest.Quest.Objectives; + foreach (QuestObjectiveDto objective in objectives) + { + CharacterQuestObjectiveDto questObjectiveDto = characterQuest.ObjectiveAmount[objective.ObjectiveIndex]; + data.Append($".{questObjectiveDto.CurrentAmount}.{questObjectiveDto.RequiredAmount}"); + + if (!statusAdded) + { + data.Append($".{(isFinished ? 1 : 0)}"); + statusAdded = true; + } + } + + for (int i = objectives.Count; i < 5; i++) + { + data.Append(".0.0"); + } + + break; + + case QuestType.DIALOG: + case QuestType.DIALOG_2: + case QuestType.NOTHING: + case QuestType.GO_TO_MAP: + case QuestType.GIVE_NPC_GOLD: + data.Append($".0.0.{(isFinished ? 1 : 0)}.0.0.0.0.0.0.0.0"); + break; + case QuestType.KILL_PLAYER_IN_REGION: + case QuestType.DIE_X_TIMES: + case QuestType.EARN_REPUTATION: + case QuestType.COMPLETE_TIMESPACE_WITH_ATLEAST_X_POINTS: + break; + } + + if (qstlistCall) + { + data.Append(characterQuest.QuestId != questToShowInfo ? ".0" : $".{(isFinished ? 0 : 1)}"); + } + else + { + data.Append(".0"); + } + + return data.ToString(); + } + + public static bool IsQuestCompleted(this IPlayerEntity player, CharacterQuest characterQuest) + { + if (characterQuest == null) + { + return false; + } + + IEnumerable objectives = characterQuest.Quest.Objectives; + switch (characterQuest.Quest.QuestType) + { + case QuestType.DELIVER_ITEM_TO_NPC: + case QuestType.GIVE_ITEM_TO_NPC: + case QuestType.GIVE_ITEM_TO_NPC_2: + case QuestType.KILL_MONSTER_BY_VNUM: + case QuestType.CAPTURE_WITHOUT_KEEPING: + case QuestType.CAPTURE_AND_KEEP: + case QuestType.CRAFT_WITHOUT_KEEPING: + case QuestType.DROP_CHANCE: + case QuestType.DROP_CHANCE_2: + case QuestType.DROP_IN_TIMESPACE: + case QuestType.COLLECT: + case QuestType.DROP_HARDCODED: + case QuestType.COMPLETE_TIMESPACE_WITH_ATLEAST_X_POINTS: + case QuestType.GIVE_NPC_GOLD: + case QuestType.COMPLETE_TIMESPACE: + case QuestType.KILL_X_MOBS_SOUND_FLOWER: + return !objectives.Any(s => characterQuest.ObjectiveAmount[s.ObjectiveIndex].CurrentAmount < characterQuest.ObjectiveAmount[s.ObjectiveIndex].RequiredAmount); + case QuestType.GO_TO_MAP: + return player.Position.IsInAoeZone(new Position(characterQuest.Quest.TargetMapX, characterQuest.Quest.TargetMapY), 2); + case QuestType.USE_ITEM_ON_TARGET: + return !objectives.Any(s => characterQuest.ObjectiveAmount[s.ObjectiveIndex].CurrentAmount <= 0); + case QuestType.DIALOG_WHILE_HAVING_ITEM: + return !objectives.Any(s => player.CountItemWithVnum(s.Data1) < characterQuest.ObjectiveAmount[s.ObjectiveIndex].RequiredAmount); + case QuestType.DIALOG: + case QuestType.DIALOG_2: + case QuestType.DIALOG_WHILE_WEARING: + if ((characterQuest.QuestId == (int)QuestsVnums.TALK_WEARING_KOVOLT_MASK_1 || characterQuest.QuestId == (int)QuestsVnums.TALK_WEARING_KOVOLT_MASK_2) + && player.Class != ClassType.Adventurer) // Kovolt Mask + { + return true; + } + + return !objectives.Any(s => characterQuest.ObjectiveAmount[s.ObjectiveIndex].CurrentAmount <= 0); + case QuestType.NOTHING: + return true; + + // Not used for now + case QuestType.WIN_RAID_AND_TALK_TO_NPC: + case QuestType.DIE_X_TIMES: + case QuestType.EARN_REPUTATION: + case QuestType.KILL_PLAYER_IN_REGION: + break; + } + + return false; + } + + public static string GenerateSqstPacket(this IClientSession session, IQuestManager questManager, int sqstIndex) + { + IReadOnlyCollection blueAlertQuests = questManager.GetNpcBlueAlertQuests().ToList(); + IReadOnlyCollection completedBlueAlertQuests = session.PlayerEntity.GetCompletedQuests().Where(s => blueAlertQuests.Any(t => t.QuestId == s.QuestId)).ToList(); + var sb = new StringBuilder(); + sb.Append($"sqst {sqstIndex} "); + for (byte packetIndex = 0; packetIndex < 250; packetIndex++) + { + byte[] binary = { 0, 0, 0, 0, 0, 0 }; + for (byte bitIndex = 0; bitIndex < 6; bitIndex++) + { + int questId = packetIndex * 6 + bitIndex + 1500 * sqstIndex; + if (completedBlueAlertQuests.Any(s => s.QuestId == questId)) + { + if (bitIndex == 0) + { + binary[bitIndex] = 1; + } + else + { + binary[^bitIndex] = 1; + } + } + } + + string binaryString = string.Join("", binary); + sb.Append(_b64SqstIndex.ElementAt(Convert.ToInt16(binaryString, 2))); + } + + return sb.ToString(); + } + + public static void SendSqstPackets(this IClientSession session, IQuestManager questManager) + { + var packets = new List(); + int sqstPacketsAmount = (int)Math.Ceiling(questManager.GetNpcBlueAlertQuests().Max(s => s.QuestId ?? 0) / 1500.0); + for (int i = 0; i < sqstPacketsAmount; i++) + { + packets.Add(session.GenerateSqstPacket(questManager, i)); + } + + session.SendPackets(packets); + } + + public static void SendSqstPacket(this IClientSession session, IQuestManager questManager, int sqstIndex) => session.SendPacket(session.GenerateSqstPacket(questManager, sqstIndex)); + + public static void UpdateQuestSqstPacket(this IClientSession session, IQuestManager questManager, int questVnum) + { + int sqstIndex = (int)Math.Floor(questVnum / 1500.0); + session.SendPacket(session.GenerateSqstPacket(questManager, sqstIndex)); + } + + public static void SendQuestsTargets(this IClientSession session) + { + IReadOnlyCollection characterQuests = session.PlayerEntity.GetCurrentQuests().Where(s => s.Quest.TargetMapId != 0).ToList(); + if (!characterQuests.Any()) + { + return; + } + + foreach (CharacterQuest characterQuest in characterQuests) + { + QuestDto quest = characterQuest.Quest; + session.SendTargetQuest(quest.TargetMapX, quest.TargetMapY, quest.TargetMapId, quest.Id); + } + } + + public static void DeleteQuestTarget(this IClientSession session, CharacterQuest characterQuest) + { + if (characterQuest.Quest.TargetMapId == 0) + { + return; + } + + session.SendTargetOffQuest(characterQuest.Quest.TargetMapX, characterQuest.Quest.TargetMapY, characterQuest.Quest.TargetMapId, characterQuest.QuestId); + } + + public static void SendTargetQuest(this IClientSession session, short x, short y, short mapId, int questId) => session.SendPacket(session.GenerateTargetQuest(x, y, mapId, questId)); + public static void SendTargetOffQuest(this IClientSession session, short x, short y, short mapId, int questId) => session.SendPacket(session.GenerateTargetOffQuest(x, y, mapId, questId)); + + public static void RefreshQuestList(this IClientSession session, IQuestManager questManager, int? questToShowInfo) => + session.SendPacket(session.GenerateQuestList(questManager, questToShowInfo)); + + public static void RefreshQuestProgress(this IClientSession session, IQuestManager questManager, int questId) => session.SendPacket(session.GenerateQuestProgress(questManager, questId)); + + public static void SendQrPacket(this IClientSession session, CharacterQuest quest, IReadOnlyCollection rndRewards, QuestsRatesConfiguration questRates) => + session.SendPacket(session.GenerateQrPacket(quest, rndRewards, questRates)); + + public static void SendQnpcPacket(this IClientSession session, short level, short npcVnum, short mapId) => session.SendPacket(session.GenerateQnpcPacket(level, npcVnum, mapId)); + public static CharacterQuest GetQuestById(this IPlayerEntity character, int questId) => character.GetCurrentQuest(questId); + public static CharacterQuest GetQuestByActionSlot(this IClientSession session, int action, int slot) => action == 1 ? session.PlayerEntity.GetCurrentQuest(slot) : session.GetQuestBySlot(slot); + + public static CharacterQuest GetQuestBySlot(this IClientSession session, int slot) + { + CharacterQuest quest; + + if (slot < 0 || slot > 10) + { + Log.Debug($"[ERROR] PACKET QT: Invalid slot index: {slot.ToString()}"); + return null; + } + + if (slot < 5) + { + quest = session.PlayerEntity.GetCurrentQuests().Where(s => s.SlotType == QuestSlotType.GENERAL).ElementAtOrDefault(slot); + } + else if (slot == 5) + { + quest = session.PlayerEntity.GetCurrentQuests().Where(s => s.SlotType == QuestSlotType.MAIN).ElementAtOrDefault(0); + } + else + { + slot -= 6; + quest = session.PlayerEntity.GetCurrentQuests().Where(s => s.SlotType == QuestSlotType.SECONDARY).ElementAtOrDefault(slot); + } + + return quest; + } + + public static long GetLevelXpPercentage(this ICharacterAlgorithm characterAlgorithm, short percentage, short level) => + Convert.ToInt64(characterAlgorithm.GetLevelXp(level) * (percentage / 100.0)); + + public static long GetSpecialistJobXpPercentage(this ICharacterAlgorithm characterAlgorithm, short percentage, short level, bool isFunSpecialist) => + Convert.ToInt64(characterAlgorithm.GetSpecialistJobXp(level, isFunSpecialist) * (percentage / 100.0)); + + public static long GetHeroLevelXpPercentage(this ICharacterAlgorithm characterAlgorithm, short percentage, short level) => + Convert.ToInt64(characterAlgorithm.GetHeroLevelXp(level) * (percentage / 100.0)); + + public static long GetJobXpPercentage(this ICharacterAlgorithm characterAlgorithm, short percentage, short level) => Convert.ToInt64(characterAlgorithm.GetJobXp(level) * (percentage / 100.0)); + + public static int GetEmptyQuestSlot(this IClientSession session, QuestSlotType slotType, bool isSoundFlower = false) + { + IReadOnlyCollection quests = session.PlayerEntity.GetCurrentQuests().Where(s => s.SlotType == slotType).ToList(); + int currentGeneralQuestsCount = quests.Count + session.PlayerEntity.GetPendingSoundFlowerQuests(); + int slot = slotType switch + { + QuestSlotType.GENERAL => currentGeneralQuestsCount > (isSoundFlower ? 5 : 4) ? -1 : quests.Count, + QuestSlotType.MAIN => !quests.Any() ? 5 : -1, + QuestSlotType.SECONDARY => quests.Count > 4 ? -1 : quests.Count + 6, + _ => -1 + }; + return slot; + } + + public static int GetQuestSlot(this IClientSession session, CharacterQuest characterQuest) + { + var quests = session.PlayerEntity.GetCurrentQuests().Where(s => s.SlotType == characterQuest.SlotType).ToList(); + return characterQuest.SlotType switch + { + QuestSlotType.GENERAL => quests.IndexOf(characterQuest), + QuestSlotType.MAIN => 5, + QuestSlotType.SECONDARY => quests.IndexOf(characterQuest) + 6, + _ => -1 + }; + } + + public static bool HasAlreadyQuestOrQuestline(this IClientSession session, QuestDto quest, IQuestManager questManager, INpcRunTypeQuestsConfiguration npcRunTypeQuestsConfiguration) + { + return session.PlayerEntity.HasQuestWithId(quest.Id) + || session.PlayerEntity.GetCurrentQuests().SelectMany(s => questManager.GetQuestlines(s.QuestId)).Intersect(questManager.GetQuestlines(quest.Id)).Any() + || session.HasRunningPeriodicQuest(quest, npcRunTypeQuestsConfiguration); + } + + public static async Task HasCompletedPeriodicQuest(this IClientSession session, QuestDto quest, IQuestManager questManager, INpcRunTypeQuestsConfiguration npcRunTypeQuestsConfiguration, + IPeriodicQuestsConfiguration periodicQuestsConfiguration) + { + IEnumerable completedQuests = session.PlayerEntity.GetCompletedPeriodicQuests(); + PeriodicQuestSet periodicQuestSet = periodicQuestsConfiguration.GetPeriodicQuestSetByQuestId(quest.Id); + if (periodicQuestSet == null) + { + return completedQuests.Any(s => npcRunTypeQuestsConfiguration.HaveTheSameNpcRunType(s.QuestId, quest.Id)); + } + + bool couldAddKey = periodicQuestSet.PerNoswingsAccount is true + ? await questManager.TryTakeDailyQuest(session.Account.MasterAccountId, periodicQuestSet.Id) + : await questManager.TryTakeDailyQuest(session.PlayerEntity.Id, periodicQuestSet.Id); + return completedQuests.Any(s => npcRunTypeQuestsConfiguration.HaveTheSameNpcRunType(s.QuestId, quest.Id)) || !couldAddKey; + } + + public static bool HasRunningPeriodicQuest(this IClientSession session, QuestDto quest, INpcRunTypeQuestsConfiguration npcRunTypeQuestsConfiguration) + { + IEnumerable activeQuests = session.PlayerEntity.GetCurrentQuests(); + return activeQuests.Any(s => npcRunTypeQuestsConfiguration.HaveTheSameNpcRunType(s.QuestId, quest.Id)); + } + + public static PeriodicQuestSet GetPeriodicQuestSet(this QuestDto quest, IQuestManager questManager, IPeriodicQuestsConfiguration periodicQuestsConfiguration) + { + PeriodicQuestSet periodicQuestSet = periodicQuestsConfiguration.GetDailyQuests().FirstOrDefault(s => s.QuestVnums.Any(t => questManager.GetQuestById(t).NextQuestId == quest.Id)); + return periodicQuestSet; + } + + public static bool ShowNpcDialog(this IClientSession session, INpcEntity npcEntity, IQuestManager questManager) + { + IEnumerable characterQuests = session.PlayerEntity.GetCurrentQuestsByTypes(DialogQuestTypes).ToArray(); + + if (!characterQuests.Any()) + { + return true; + } + + foreach (CharacterQuest characterQuest in characterQuests) + { + if (questManager.IsNpcBlueAlertQuest(characterQuest.QuestId) && (characterQuest.Quest.QuestType == QuestType.DIALOG || characterQuest.Quest.QuestType == QuestType.DIALOG_2)) + { + continue; + } + + foreach (QuestObjectiveDto objective in characterQuest.Quest.Objectives) + { + if (npcEntity.NpcVNum != (characterQuest.Quest.QuestType is QuestType.DELIVER_ITEM_TO_NPC ? objective.Data1 : objective.Data0)) + { + continue; + } + + CharacterQuestObjectiveDto questObjectiveDto = characterQuest.ObjectiveAmount[objective.ObjectiveIndex]; + switch (characterQuest.Quest.QuestType) + { + case QuestType.DELIVER_ITEM_TO_NPC: + case QuestType.GIVE_ITEM_TO_NPC_2: + case QuestType.GIVE_ITEM_TO_NPC: + int amountLeft = questObjectiveDto.RequiredAmount - questObjectiveDto.CurrentAmount; + if (amountLeft == 0) + { + continue; + } + + int amountInPossession = session.PlayerEntity.CountItemWithVnum(characterQuest.Quest.QuestType is QuestType.DELIVER_ITEM_TO_NPC ? objective.Data0 : objective.Data1); + if (amountInPossession == 0) + { + continue; + } + + return false; + case QuestType.GIVE_NPC_GOLD: + int totalGoldToGive = questObjectiveDto.RequiredAmount; + if (session.PlayerEntity.Gold < totalGoldToGive) + { + continue; + } + + return false; + case QuestType.DIALOG: + case QuestType.DIALOG_2: + return false; + } + } + } + + return true; + } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game.Extensions/Quests/QuestScriptExtensions.cs b/srcs/WingsAPI.Game.Extensions/Quests/QuestScriptExtensions.cs new file mode 100644 index 0000000..b83bf36 --- /dev/null +++ b/srcs/WingsAPI.Game.Extensions/Quests/QuestScriptExtensions.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using System.Linq; +using WingsEmu.DTOs.Quests; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Quests; + +namespace WingsAPI.Game.Extensions.Quests +{ + public static class QuestScriptExtensions + { + public static string GenerateNextQuestScriptPacket(this IClientSession session, CharacterQuest quest, IQuestManager questManager) + { + IReadOnlyCollection scripts = questManager.GetScriptsTutorialByType(TutorialActionType.WAIT_FOR_REWARDS_CLAIM); + + TutorialDto actualScript = scripts.FirstOrDefault(s => s.Data == quest.QuestId); + if (actualScript == null) + { + return string.Empty; + } + + TutorialDto nextScript = questManager.GetScriptTutorialById(actualScript.Id + 1); + if (nextScript == null || actualScript.ScriptId != nextScript.ScriptId) // End of a script ID. Gotta pick quest from NPC + { + return string.Empty; + } + + return $"script {nextScript.ScriptId} {nextScript.ScriptIndex}"; + } + + public static string GenerateScriptPacket(this IClientSession session, int scriptId, int index) => $"script {scriptId.ToString()} {index.ToString()}"; + + public static void SendNextQuestScriptPacket(this IClientSession session, CharacterQuest quest, IQuestManager questManager) => + session.SendPacket(session.GenerateNextQuestScriptPacket(quest, questManager)); + + public static void SendScriptPacket(this IClientSession session, int scriptId, int index) => session.SendPacket(session.GenerateScriptPacket(scriptId, index)); + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game.Extensions/Quicklist/QuicklistPacketExtensions.cs b/srcs/WingsAPI.Game.Extensions/Quicklist/QuicklistPacketExtensions.cs new file mode 100644 index 0000000..6e73c76 --- /dev/null +++ b/srcs/WingsAPI.Game.Extensions/Quicklist/QuicklistPacketExtensions.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using System.Text; +using WingsEmu.DTOs.Quicklist; +using WingsEmu.Game.Networking; + +namespace WingsAPI.Game.Extensions.Quicklist +{ + public static class QuicklistPacketExtensions + { + public static void RefreshQuicklist(this IClientSession session) => session.SendPackets(session.GenerateQuicklist()); + + + public static IEnumerable GenerateQuicklist(this IClientSession session) + { + // to rework + StringBuilder[] pktQs = { new("qslot 0"), new("qslot 1") }; + + int morphId = session.PlayerEntity.UseSp ? session.PlayerEntity.Specialist?.GameItem.Morph ?? 0 : 0; + + for (short i = 0; i < 30; i++) + { + for (short j = 0; j < 2; j++) + { + IReadOnlyList tmp = session.PlayerEntity.QuicklistComponent.GetQuicklistByTab(j, morphId); + + if (tmp?[i] == null) + { + pktQs[j].Append(" 0.-1.-1"); + continue; + } + + CharacterQuicklistEntryDto qi = tmp[i]; + pktQs[j].Append($" {(byte?)qi?.Type ?? 0}.{qi?.InventoryTypeOrSkillTab ?? -1}.{qi?.InvSlotOrSkillSlotOrSkillVnum.ToString() ?? "-1"}"); + } + } + + return new[] { pktQs[0].ToString(), pktQs[1].ToString() }; + } + + public static void SendQuicklistSlot(this IClientSession session, CharacterQuicklistEntryDto entry, short? skillCastId = null) + { + session.SendPacket(session.GenerateQsetPacket(entry, skillCastId)); + } + + public static string GenerateQsetPacket(this IClientSession session, CharacterQuicklistEntryDto quicklistEntryDto, short? skillCastId = null) => + $"qset {quicklistEntryDto.QuicklistTab} {quicklistEntryDto.QuicklistSlot} {(byte)quicklistEntryDto.Type}.{quicklistEntryDto.InventoryTypeOrSkillTab}.{skillCastId ?? quicklistEntryDto.InvSlotOrSkillSlotOrSkillVnum}.0"; + + public static void SendEmptyQuicklistSlot(this IClientSession session, short tab, short slot) + { + session.SendPacket(session.GenerateEmptyQsetPacket(tab, slot)); + } + + public static string GenerateEmptyQsetPacket(this IClientSession session, short tab, short slot) => $"qset {tab} {slot} 0.-1.-1.0"; + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game.Extensions/RelationsExtensions/RelationsExtensions.cs b/srcs/WingsAPI.Game.Extensions/RelationsExtensions/RelationsExtensions.cs new file mode 100644 index 0000000..11390e9 --- /dev/null +++ b/srcs/WingsAPI.Game.Extensions/RelationsExtensions/RelationsExtensions.cs @@ -0,0 +1,28 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Threading.Tasks; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Relations; +using WingsEmu.Packets.Enums.Relations; + +namespace WingsAPI.Game.Extensions.RelationsExtensions +{ + public static class RelationsExtensions + { + public static async Task AddRelationAsync(this IClientSession session, long targetCharacterId, CharacterRelationType type) + { + await session.EmitEventAsync(new AddRelationEvent(targetCharacterId, type)); + } + + public static async Task RemoveRelationAsync(this IClientSession session, long targetCharacterId, CharacterRelationType type) + { + await session.EmitEventAsync(new RemoveRelationEvent(targetCharacterId, type)); + } + + public static string GenerateFriendMessage(this IClientSession session, long targetId, string message) => $"talk {targetId} {message}"; + + public static void SendFriendMessage(this IClientSession session, long targetId, string message) => session.SendPacket(session.GenerateFriendMessage(targetId, message)); + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game.Extensions/Skills/LearningSkillsExtensions.cs b/srcs/WingsAPI.Game.Extensions/Skills/LearningSkillsExtensions.cs new file mode 100644 index 0000000..53dd207 --- /dev/null +++ b/srcs/WingsAPI.Game.Extensions/Skills/LearningSkillsExtensions.cs @@ -0,0 +1,134 @@ +using System; +using System.Collections.Concurrent; +using System.Linq; +using WingsEmu.DTOs.Skills; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Skills; +using WingsEmu.Packets.Enums.Character; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsAPI.Game.Extensions.Quicklist +{ + public static class LearningSkillsExtensions + { + public static void LearnAdventurerSkill(this IClientSession session, ISkillsManager skillsManager, IGameLanguageService gameLanguage) + { + bool usingSp = session.PlayerEntity.UseSp && session.PlayerEntity.Specialist != null; + if (session.PlayerEntity.Class != (byte)ClassType.Adventurer) + { + if (!usingSp) + { + session.RefreshSkillList(); + session.RefreshQuicklist(); + + session.PlayerEntity.ClearSkillCooldowns(); + foreach (IBattleEntitySkill skill in session.PlayerEntity.Skills) + { + skill.LastUse = DateTime.MinValue; + } + } + + return; + } + + bool newSkill = false; + for (int skillVnum = 200; skillVnum <= 210; skillVnum++) + { + if (skillVnum == 209) + { + skillVnum++; + } + + SkillDTO skinfo = skillsManager.GetSkill((short)skillVnum); + if (skinfo.Class != 0 || session.PlayerEntity.JobLevel < skinfo.LevelMinimum) + { + continue; + } + + int vnum = skillVnum; + if (session.PlayerEntity.CharacterSkills.Any(s => s.Value.SkillVNum == vnum)) + { + continue; + } + + newSkill = true; + var newAdventurerSkill = new CharacterSkill { SkillVNum = (short)skillVnum }; + session.PlayerEntity.CharacterSkills[skillVnum] = newAdventurerSkill; + + if (usingSp) + { + continue; + } + + session.PlayerEntity.Skills.Add(newAdventurerSkill); + } + + if (newSkill == false && !usingSp) + { + session.RefreshSkillList(); + session.RefreshQuicklist(); + + session.PlayerEntity.ClearSkillCooldowns(); + foreach (IBattleEntitySkill skill in session.PlayerEntity.Skills) + { + skill.LastUse = DateTime.MinValue; + } + + return; + } + + if (usingSp) + { + return; + } + + session.SendMsg(gameLanguage.GetLanguage(GameDialogKey.SKILL_SHOUTMESSAGE_LEARNED, session.UserLanguage), MsgMessageType.Middle); + session.RefreshSkillList(); + session.RefreshQuicklist(); + + session.PlayerEntity.ClearSkillCooldowns(); + foreach (IBattleEntitySkill skill in session.PlayerEntity.Skills) + { + skill.LastUse = DateTime.MinValue; + } + } + + public static void LearnSpSkill(this IClientSession session, ISkillsManager skillsManager, IGameLanguageService gameLanguage) + { + byte skillSpCount = (byte)session.PlayerEntity.SkillsSp.Count; + session.PlayerEntity.SkillsSp = new ConcurrentDictionary(); + + foreach (SkillDTO ski in skillsManager.GetSkills()) + { + if (!session.PlayerEntity.Specialist.IsSpSkill(ski)) + { + continue; + } + + var newSkill = new CharacterSkill { SkillVNum = ski.Id }; + + session.PlayerEntity.SkillsSp[ski.Id] = newSkill; + session.PlayerEntity.Skills.Add(newSkill); + } + + session.PlayerEntity.ClearSkillCooldowns(); + foreach (IBattleEntitySkill skill in session.PlayerEntity.Skills) + { + skill.LastUse = DateTime.MinValue; + } + + session.RefreshSkillList(); + session.RefreshQuicklist(); + + if (session.PlayerEntity.SkillsSp.Count == skillSpCount) + { + return; + } + + session.SendMsg(gameLanguage.GetLanguage(GameDialogKey.SKILL_SHOUTMESSAGE_LEARNED, session.UserLanguage), MsgMessageType.Middle); + } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game.Extensions/Warehouse/WarehousePacketExtensions.cs b/srcs/WingsAPI.Game.Extensions/Warehouse/WarehousePacketExtensions.cs new file mode 100644 index 0000000..dfe291f --- /dev/null +++ b/srcs/WingsAPI.Game.Extensions/Warehouse/WarehousePacketExtensions.cs @@ -0,0 +1,133 @@ +using System.Collections.Generic; +using System.Text; +using WingsAPI.Data.Account; +using WingsEmu.DTOs.Items; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Warehouse; +using WingsEmu.Packets.Enums; + +namespace WingsAPI.Game.Extensions.Warehouse +{ + public static class WarehousePacketExtensions + { + #region Warehouse + + public static string GenerateStashAll(this IClientSession session, IItemsManager itemsManager, int warehouseSize, IEnumerable items) + { + var packet = new StringBuilder($"stash_all {warehouseSize.ToString()}"); + foreach (AccountWarehouseItemDto item in items) + { + packet.Append(' '); + packet.Append(item.GenerateStashPacketContent(itemsManager)); + } + + return packet.ToString(); + } + + public static void SendStashDynamicItemUpdate(this IClientSession session, IItemsManager itemsManager, AccountWarehouseItemDto item, int slot) + { + if (item == null) + { + session.SendWarehouseRemovePacket(slot); + } + else + { + session.SendStashPacket(itemsManager, item); + } + } + + public static string GenerateStashPacketContent(this AccountWarehouseItemDto warehouseItem, IItemsManager itemsManager) => + GenerateStashSubPacket(itemsManager, warehouseItem.ItemInstance, warehouseItem.Slot); + + public static void SendWarehouseStashAll(this IClientSession session, IItemsManager itemsManager, int warehouseSize, IEnumerable items) + { + session.SendPacket(session.GenerateStashAll(itemsManager, warehouseSize, items)); + } + + public static string GenerateEmptyStashPacket(this IClientSession session, int slot) => $"stash {slot.ToString()}.-1.0.0.0"; + + public static void SendWarehouseRemovePacket(this IClientSession session, int slot) => session.SendPacket(session.GenerateEmptyStashPacket(slot)); + + public static string GenerateStashPacket(this AccountWarehouseItemDto item, IItemsManager itemsManager) => $"stash {item.GenerateStashPacketContent(itemsManager)}"; + + public static void SendStashPacket(this IClientSession session, IItemsManager itemsManager, AccountWarehouseItemDto item) => session.SendPacket(item.GenerateStashPacket(itemsManager)); + + public static string GenerateStashSubPacket(IItemsManager itemsManager, ItemInstanceDTO itemInstanceDto, short slot) + { + IGameItem gameItem = itemsManager.GetItem(itemInstanceDto.ItemVNum); + + return GenerateStashSubPacket(itemInstanceDto, gameItem.Type, slot); + } + + public static string GenerateStashSubPacket(ItemInstanceDTO itemInstanceDto, InventoryType inventoryType, short slot) + { + var packet = new StringBuilder(); + + packet.AppendFormat("{0}.{1}.{2}.", slot.ToString(), itemInstanceDto.ItemVNum.ToString(), ((byte)inventoryType).ToString()); + switch (inventoryType) + { + case InventoryType.Equipment: + packet.AppendFormat("{0}.{1}.{2}.{3}", itemInstanceDto.Amount.ToString(), itemInstanceDto.Rarity.ToString(), itemInstanceDto.Upgrade.ToString(), + itemInstanceDto.GetRunesCount().ToString()); + break; + + case InventoryType.Specialist: + packet.AppendFormat("{0}.0.0", itemInstanceDto.Upgrade.ToString()); + break; + + default: + packet.AppendFormat("{0}.0.0.0", itemInstanceDto.Amount.ToString()); + break; + } + + return packet.ToString(); + } + + public static string GenerateStashSubPacket(GameItemInstance itemInstance, short slot) => GenerateStashSubPacket(itemInstance, itemInstance.GameItem.Type, slot); + + private static string GenerateStashPacketContent(this WarehouseItem warehouseItem) => GenerateStashSubPacket(warehouseItem.ItemInstance, warehouseItem.Slot); + + #endregion + + #region PartnerWarehouse + + public static void SendAddPartnerWarehouseItem(this IClientSession session, PartnerWarehouseItem item) => session.SendPacket(session.GenerateAddPartnerWarehouseItem(item)); + + public static void SendRemovePartnerWarehouseItem(this IClientSession session, short slot) => session.SendPacket(session.GenerateRemovePartnerWarehouseItem(slot)); + + private static string GeneratePStashPacketContent(this PartnerWarehouseItem warehouseItem) => GenerateStashSubPacket(warehouseItem.ItemInstance, warehouseItem.Slot); + + public static void RefreshPartnerWarehouseItems(this IClientSession session) + { + session.SendPacket(session.GeneratePStashAll()); + session.PlayerEntity.IsPartnerWarehouseOpen = true; + } + + public static string GeneratePStashAll(this IClientSession session) + { + var header = new StringBuilder($"pstash_all {session.PlayerEntity.GetPartnerWarehouseSlots()}"); + IEnumerable items = session.PlayerEntity.PartnerWarehouseItems(); + foreach (PartnerWarehouseItem item in items) + { + if (item == null) + { + continue; + } + + header.Append(' '); + header.Append(item.GeneratePStashPacketContent()); + } + + return header.ToString(); + } + + public static string GenerateAddPartnerWarehouseItem(this IClientSession session, PartnerWarehouseItem item) => $"pstash {item.GeneratePStashPacketContent()}"; + + public static string GenerateRemovePartnerWarehouseItem(this IClientSession session, short slot) => $"pstash {slot.ToString()}.-1.0.0.0"; + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game.Extensions/WingsAPI.Game.Extensions.csproj b/srcs/WingsAPI.Game.Extensions/WingsAPI.Game.Extensions.csproj new file mode 100644 index 0000000..770aabb --- /dev/null +++ b/srcs/WingsAPI.Game.Extensions/WingsAPI.Game.Extensions.csproj @@ -0,0 +1,15 @@ + + + + net5.0 + WingsAPI.Game.Extensions + + + + + + + + + + diff --git a/srcs/WingsAPI.Game/Account.cs b/srcs/WingsAPI.Game/Account.cs new file mode 100644 index 0000000..758624d --- /dev/null +++ b/srcs/WingsAPI.Game/Account.cs @@ -0,0 +1,59 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Generic; +using PhoenixLib.MultiLanguage; +using WingsAPI.Data.Account; +using WingsEmu.DTOs.Account; + +namespace WingsEmu.Game; + +public static class AccountExtensions +{ + public static RegionLanguageType ToRegionLanguageType(this AccountLanguage lang) + { + return lang switch + { + AccountLanguage.EN => RegionLanguageType.EN, + AccountLanguage.FR => RegionLanguageType.FR, + AccountLanguage.DE => RegionLanguageType.DE, + AccountLanguage.PL => RegionLanguageType.PL, + AccountLanguage.IT => RegionLanguageType.IT, + AccountLanguage.ES => RegionLanguageType.ES, + AccountLanguage.CZ => RegionLanguageType.CZ, + AccountLanguage.TR => RegionLanguageType.TR, + _ => throw new ArgumentOutOfRangeException(nameof(lang), lang, null) + }; + } + + public static AccountLanguage ToAccountLanguage(this RegionLanguageType lang) + { + return lang switch + { + RegionLanguageType.EN => AccountLanguage.EN, + RegionLanguageType.FR => AccountLanguage.FR, + RegionLanguageType.DE => AccountLanguage.DE, + RegionLanguageType.PL => AccountLanguage.PL, + RegionLanguageType.IT => AccountLanguage.IT, + RegionLanguageType.ES => AccountLanguage.ES, + RegionLanguageType.CZ => AccountLanguage.CZ, + RegionLanguageType.TR => AccountLanguage.TR, + _ => throw new ArgumentOutOfRangeException(nameof(lang), lang, null) + }; + } +} + +public class Account : AccountDTO +{ + public List Logs { get; set; } = new(); + + public void ChangeLanguage(RegionLanguageType lang) + { + Language = lang.ToAccountLanguage(); + LangChanged?.Invoke(this, lang); + } + + public event EventHandler LangChanged; +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Act4/Configuration/Act4Configuration.cs b/srcs/WingsAPI.Game/Act4/Configuration/Act4Configuration.cs new file mode 100644 index 0000000..01449e8 --- /dev/null +++ b/srcs/WingsAPI.Game/Act4/Configuration/Act4Configuration.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using WingsEmu.Core; + +namespace WingsEmu.Game.Act4.Configuration; + +public class Act4Configuration +{ + public int MaximumFactionPoints { get; set; } = 60_000; + + public bool PveFactionPoints { get; set; } = true; + public int FactionPointsPerPveKill { get; set; } = 50; + + public bool PvpFactionPoints { get; set; } = true; + public int FactionPointsPerPvpKill { get; set; } = 150; + + public List BannedMapIdsToAngels { get; set; } = new() { 131 }; + public List BannedMapIdsToDemons { get; set; } = new() { 130 }; + + public List PointGeneration { get; set; } = new() { new PointGeneration() }; + + public TimeSpan ResetDate { get; set; } = TimeSpan.Zero; + + public TimeSpan MukrajuEndSpan { get; set; } = TimeSpan.FromMinutes(5); + public byte MukrajuRadius { get; set; } + public MukrajuSpawn AngelMukrajuSpawn { get; set; } = new(); + public MukrajuSpawn DemonMukrajuSpawn { get; set; } = new(); +} + +public class MukrajuSpawn +{ + public int MonsterVnum { get; set; } + public int MapId { get; set; } + public short MapX { get; set; } + public short MapY { get; set; } +} + +public class PointGeneration +{ + /// + /// Min and max included + /// + /// + public Range PlayerAmount { get; set; } = new(); + + public int PointsAmount { get; set; } = 100; +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Act4/Configuration/Act4DungeonsConfiguration.cs b/srcs/WingsAPI.Game/Act4/Configuration/Act4DungeonsConfiguration.cs new file mode 100644 index 0000000..8d1390a --- /dev/null +++ b/srcs/WingsAPI.Game/Act4/Configuration/Act4DungeonsConfiguration.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; + +namespace WingsEmu.Game.Act4.Configuration; + +public class Act4DungeonsConfiguration +{ + public int DungeonPortalMapId { get; set; } + public short DungeonPortalMapX { get; set; } + public short DungeonPortalMapY { get; set; } + + public int DungeonReturnPortalMapId { get; set; } + public short DungeonReturnPortalMapX { get; set; } + public short DungeonReturnPortalMapY { get; set; } + + public int DungeonEntryCostMultiplier { get; set; } + public TimeSpan DungeonDeathRevivalDelay { get; set; } = TimeSpan.FromSeconds(20); + public TimeSpan DungeonDuration { get; set; } = TimeSpan.FromMinutes(60); + public TimeSpan DungeonBossMapClosureAfterReward { get; set; } = TimeSpan.FromSeconds(30); + public TimeSpan DungeonSlowMoDelay { get; set; } = TimeSpan.FromSeconds(7); + + public List GuardiansForAngels { get; set; } = new() { new GuardianSpawn() }; + public List GuardiansForDemons { get; set; } = new() { new GuardianSpawn() }; +} + +public class GuardianSpawn +{ + public int MonsterVnum { get; set; } + public short MapX { get; set; } + public short MapY { get; set; } + public byte Direction { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Act4/DungeonInstance.cs b/srcs/WingsAPI.Game/Act4/DungeonInstance.cs new file mode 100644 index 0000000..4a653db --- /dev/null +++ b/srcs/WingsAPI.Game/Act4/DungeonInstance.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Raids; + +namespace WingsEmu.Game.Act4; + +public class DungeonInstance +{ + public DungeonInstance(long familyId, DungeonType dungeonType, IReadOnlyCollection dungeonSubInstances, + DungeonSubInstance spawnInstance, Position spawnPoint, RaidReward raidReward) + { + foreach (DungeonSubInstance dungeonSubInstance in dungeonSubInstances) + { + dungeonSubInstance.MapInstance.Initialize(DateTime.UtcNow.AddMilliseconds(-500)); + } + + FamilyId = familyId; + DungeonType = dungeonType; + DungeonSubInstances = GetDictionary(dungeonSubInstances); + SpawnInstance = spawnInstance; + SpawnPoint = spawnPoint; + DungeonReward = raidReward; + } + + public long FamilyId { get; } + public DungeonType DungeonType { get; } + public IReadOnlyDictionary DungeonSubInstances { get; } + public Position SpawnPoint { get; } + public DungeonSubInstance SpawnInstance { get; } + public RaidReward DungeonReward { get; } + public bool PlayerDeathInBossRoom { get; set; } + public DateTime StartInBoosRoom { get; set; } + + public DateTime? FinishSlowMoDate { get; set; } + public DateTime? CleanUpBossMapDate { get; set; } + + private static IReadOnlyDictionary GetDictionary(IEnumerable dungeonSubInstances) + { + var dictionary = new Dictionary(); + foreach (DungeonSubInstance dungeonSubInstance in dungeonSubInstances) + { + dictionary.TryAdd(dungeonSubInstance.MapInstance.Id, dungeonSubInstance); + } + + return dictionary; + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Act4/DungeonInstanceWrapper.cs b/srcs/WingsAPI.Game/Act4/DungeonInstanceWrapper.cs new file mode 100644 index 0000000..aa70f1f --- /dev/null +++ b/srcs/WingsAPI.Game/Act4/DungeonInstanceWrapper.cs @@ -0,0 +1,6 @@ +namespace WingsEmu.Game.Act4; + +public class DungeonInstanceWrapper +{ + public DungeonInstance DungeonInstance { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Act4/DungeonSubInstance.cs b/srcs/WingsAPI.Game/Act4/DungeonSubInstance.cs new file mode 100644 index 0000000..3d8f773 --- /dev/null +++ b/srcs/WingsAPI.Game/Act4/DungeonSubInstance.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Act4.Entities; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Raids; +using WingsEmu.Game.Triggers; + +namespace WingsEmu.Game.Act4; + +public class DungeonSubInstance : IEventTriggerContainer +{ + private readonly List _bosses = new(); + + private readonly IEventTriggerContainer _eventTriggerContainer; + + public DungeonSubInstance(IMapInstance mapInstance, IAsyncEventPipeline asyncEventPipeline, HatusHeads hatusHeads = null) + { + MapInstance = mapInstance; + _eventTriggerContainer = new EventTriggerContainer(asyncEventPipeline); + HatusHeads = hatusHeads; + } + + public IMapInstance MapInstance { get; } + + public HatusHeads HatusHeads { get; } + public IReadOnlyList Bosses => _bosses; + + public List LoopWaves { get; private set; } + public DateTime? LastDungeonWaveLoop { get; set; } + + public List LinearWaves { get; private set; } + public DateTime? LastDungeonWaveLinear { get; set; } + + public List PortalGenerators { get; private set; } + public DateTime? LastPortalGeneration { get; set; } + + public void AddEvent(string key, IAsyncEvent notification, bool removedOnTrigger = false) => _eventTriggerContainer.AddEvent(key, notification, removedOnTrigger); + public Task TriggerEvents(string key) => _eventTriggerContainer.TriggerEvents(key); + + public void SetLoopWaves(List dungeonWaves) + { + LoopWaves = dungeonWaves; + } + + public void SetLinearWaves(IEnumerable dungeonWaves) + { + LinearWaves = dungeonWaves.OrderBy(x => x.Delay).ToList(); + } + + public void SetPortalGenerators(IEnumerable portalGenerators) + { + PortalGenerators = portalGenerators.OrderBy(x => x.Delay).ToList(); + } + + public void AddDungeonMonster(IMonsterEntity monster) + { + if (monster.IsBoss) + { + _bosses.Add(monster); + } + } + + public void AddDungeonButton(ButtonMapItem buttonMapItem) + { + MapInstance.AddDrop(buttonMapItem); + } +} + +public class DungeonLoopWave +{ + public DateTime FirstSpawnWave { get; set; } + public TimeSpan Delay { get; init; } + public IReadOnlyList Monsters { get; init; } + public TimeSpan TickDelay { get; init; } + public DateTime LastMonsterSpawn { get; set; } + public bool IsScaledWithPlayerAmount { get; set; } +} + +public class DungeonLinearWave +{ + public IReadOnlyList Monsters { get; init; } + public TimeSpan Delay { get; init; } +} + +public class PortalGenerator +{ + public IPortalEntity Portal { get; init; } + public TimeSpan Delay { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Act4/DungeonType.cs b/srcs/WingsAPI.Game/Act4/DungeonType.cs new file mode 100644 index 0000000..bcf0459 --- /dev/null +++ b/srcs/WingsAPI.Game/Act4/DungeonType.cs @@ -0,0 +1,9 @@ +namespace WingsEmu.Game.Act4; + +public enum DungeonType : byte +{ + Morcos = 1, + Hatus = 2, + Calvinas = 3, + Berios = 4 +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Act4/Entities/CalvinasDragon.cs b/srcs/WingsAPI.Game/Act4/Entities/CalvinasDragon.cs new file mode 100644 index 0000000..974eb5f --- /dev/null +++ b/srcs/WingsAPI.Game/Act4/Entities/CalvinasDragon.cs @@ -0,0 +1,16 @@ +namespace WingsEmu.Game.Act4.Entities; + +public class CalvinasDragon +{ + public CoordType Axis { get; set; } + public short At { get; set; } + public byte Size { get; set; } + public short Start { get; set; } + public short End { get; set; } +} + +public enum CoordType +{ + X, + Y +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Act4/Entities/HatusHeads.cs b/srcs/WingsAPI.Game/Act4/Entities/HatusHeads.cs new file mode 100644 index 0000000..824b277 --- /dev/null +++ b/srcs/WingsAPI.Game/Act4/Entities/HatusHeads.cs @@ -0,0 +1,37 @@ +using System; + +namespace WingsEmu.Game.Act4.Entities; + +public class HatusHeads +{ + public HatusHeads(HatusDragonHead dragonHeads) => DragonHeads = dragonHeads; + + public HatusDragonHead DragonHeads { get; } + + public int DragonAttackWidth { get; } = 2; + + public DateTime CastTime { get; set; } + public HatusDragonHeadState HeadsState { get; set; } +} + +public class HatusDragonHead +{ + public short BluePositionX { get; set; } + public bool BlueIsActive { get; set; } + + public short RedPositionX { get; set; } + public bool RedIsActive { get; set; } + + public short GreenPositionX { get; set; } + public bool GreenIsActive { get; set; } +} + +public enum HatusDragonHeadState +{ + SHOW = 0, + IDLE = 1, + ATTACK_CAST = 2, + ATTACK_USE = 3, + TAKING_DAMAGE = 4, + HIDE_HEAD = 5 +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Act4/Event/Act4DungeonBossMapCleanUpEvent.cs b/srcs/WingsAPI.Game/Act4/Event/Act4DungeonBossMapCleanUpEvent.cs new file mode 100644 index 0000000..9204fa9 --- /dev/null +++ b/srcs/WingsAPI.Game/Act4/Event/Act4DungeonBossMapCleanUpEvent.cs @@ -0,0 +1,9 @@ +using PhoenixLib.Events; + +namespace WingsEmu.Game.Act4.Event; + +public class Act4DungeonBossMapCleanUpEvent : IAsyncEvent +{ + public DungeonInstance DungeonInstance { get; init; } + public DungeonSubInstance BossMap { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Act4/Event/Act4DungeonBroadcastBossClosedEvent.cs b/srcs/WingsAPI.Game/Act4/Event/Act4DungeonBroadcastBossClosedEvent.cs new file mode 100644 index 0000000..6647cc5 --- /dev/null +++ b/srcs/WingsAPI.Game/Act4/Event/Act4DungeonBroadcastBossClosedEvent.cs @@ -0,0 +1,8 @@ +using PhoenixLib.Events; + +namespace WingsEmu.Game.Act4.Event; + +public class Act4DungeonBroadcastBossClosedEvent : IAsyncEvent +{ + public DungeonInstanceWrapper DungeonInstanceWrapper { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Act4/Event/Act4DungeonBroadcastBossOpenEvent.cs b/srcs/WingsAPI.Game/Act4/Event/Act4DungeonBroadcastBossOpenEvent.cs new file mode 100644 index 0000000..8bc3aba --- /dev/null +++ b/srcs/WingsAPI.Game/Act4/Event/Act4DungeonBroadcastBossOpenEvent.cs @@ -0,0 +1,8 @@ +using PhoenixLib.Events; + +namespace WingsEmu.Game.Act4.Event; + +public class Act4DungeonBroadcastBossOpenEvent : IAsyncEvent +{ + public DungeonInstance DungeonInstance { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Act4/Event/Act4DungeonBroadcastPacketEvent.cs b/srcs/WingsAPI.Game/Act4/Event/Act4DungeonBroadcastPacketEvent.cs new file mode 100644 index 0000000..ee8d1a7 --- /dev/null +++ b/srcs/WingsAPI.Game/Act4/Event/Act4DungeonBroadcastPacketEvent.cs @@ -0,0 +1,8 @@ +using PhoenixLib.Events; + +namespace WingsEmu.Game.Act4.Event; + +public class Act4DungeonBroadcastPacketEvent : IAsyncEvent +{ + public DungeonInstance DungeonInstance { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Act4/Event/Act4DungeonEnterEvent.cs b/srcs/WingsAPI.Game/Act4/Event/Act4DungeonEnterEvent.cs new file mode 100644 index 0000000..0f3b24b --- /dev/null +++ b/srcs/WingsAPI.Game/Act4/Event/Act4DungeonEnterEvent.cs @@ -0,0 +1,8 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Act4.Event; + +public class Act4DungeonEnterEvent : PlayerEvent +{ + public bool Confirmed { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Act4/Event/Act4DungeonRewardEvent.cs b/srcs/WingsAPI.Game/Act4/Event/Act4DungeonRewardEvent.cs new file mode 100644 index 0000000..6fadf4c --- /dev/null +++ b/srcs/WingsAPI.Game/Act4/Event/Act4DungeonRewardEvent.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using PhoenixLib.Events; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Game.Act4.Event; + +public class Act4DungeonRewardEvent : IAsyncEvent +{ + public DungeonInstanceWrapper DungeonInstanceWrapper { get; init; } +} + +public class Act4DungeonWonEvent : IAsyncEvent +{ + public DungeonInstance DungeonInstance { get; init; } + public IClientSession DungeonLeader { get; init; } + public IEnumerable Members { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Act4/Event/Act4DungeonStartedEvent.cs b/srcs/WingsAPI.Game/Act4/Event/Act4DungeonStartedEvent.cs new file mode 100644 index 0000000..bc219de --- /dev/null +++ b/srcs/WingsAPI.Game/Act4/Event/Act4DungeonStartedEvent.cs @@ -0,0 +1,10 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Act4.Event; + +public class Act4DungeonStartedEvent : PlayerEvent +{ + public FactionType FactionType { get; init; } + public DungeonType DungeonType { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Act4/Event/Act4DungeonStopEvent.cs b/srcs/WingsAPI.Game/Act4/Event/Act4DungeonStopEvent.cs new file mode 100644 index 0000000..6529859 --- /dev/null +++ b/srcs/WingsAPI.Game/Act4/Event/Act4DungeonStopEvent.cs @@ -0,0 +1,8 @@ +using PhoenixLib.Events; + +namespace WingsEmu.Game.Act4.Event; + +public class Act4DungeonStopEvent : IAsyncEvent +{ + public DungeonInstance DungeonInstance { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Act4/Event/Act4DungeonSystemStartEvent.cs b/srcs/WingsAPI.Game/Act4/Event/Act4DungeonSystemStartEvent.cs new file mode 100644 index 0000000..f759055 --- /dev/null +++ b/srcs/WingsAPI.Game/Act4/Event/Act4DungeonSystemStartEvent.cs @@ -0,0 +1,6 @@ +using PhoenixLib.Events; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Act4.Event; + +public sealed record Act4DungeonSystemStartEvent(FactionType FactionType, DungeonType? DungeonType = null) : IAsyncEvent; \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Act4/Event/Act4DungeonSystemStopEvent.cs b/srcs/WingsAPI.Game/Act4/Event/Act4DungeonSystemStopEvent.cs new file mode 100644 index 0000000..796ceef --- /dev/null +++ b/srcs/WingsAPI.Game/Act4/Event/Act4DungeonSystemStopEvent.cs @@ -0,0 +1,5 @@ +using PhoenixLib.Events; + +namespace WingsEmu.Game.Act4.Event; + +public record Act4DungeonSystemStopEvent : IAsyncEvent; \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Act4/Event/Act4FactionPointsGenerationEvent.cs b/srcs/WingsAPI.Game/Act4/Event/Act4FactionPointsGenerationEvent.cs new file mode 100644 index 0000000..637f070 --- /dev/null +++ b/srcs/WingsAPI.Game/Act4/Event/Act4FactionPointsGenerationEvent.cs @@ -0,0 +1,7 @@ +using PhoenixLib.Events; + +namespace WingsEmu.Game.Act4.Event; + +public class Act4FactionPointsGenerationEvent : IAsyncEvent +{ +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Act4/Event/Act4FactionPointsIncreaseEvent.cs b/srcs/WingsAPI.Game/Act4/Event/Act4FactionPointsIncreaseEvent.cs new file mode 100644 index 0000000..402d4e4 --- /dev/null +++ b/srcs/WingsAPI.Game/Act4/Event/Act4FactionPointsIncreaseEvent.cs @@ -0,0 +1,20 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Act4.Event; + +public class Act4FactionPointsIncreaseEvent : PlayerEvent +{ + public Act4FactionPointsIncreaseEvent(int pointsToAdd) => PointsToAdd = pointsToAdd; + + public Act4FactionPointsIncreaseEvent(FactionType factionType, int pointsToAdd) + { + PreferedFactionType = factionType; + PointsToAdd = pointsToAdd; + } + + private FactionType? PreferedFactionType { get; } + public int PointsToAdd { get; } + + public FactionType FactionType => PreferedFactionType ?? Sender.PlayerEntity.Faction; +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Act4/Event/Act4FamilyDungeonWonEvent.cs b/srcs/WingsAPI.Game/Act4/Event/Act4FamilyDungeonWonEvent.cs new file mode 100644 index 0000000..95635d7 --- /dev/null +++ b/srcs/WingsAPI.Game/Act4/Event/Act4FamilyDungeonWonEvent.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using WingsEmu.Game._packetHandling; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Game.Act4.Event; + +public class Act4FamilyDungeonWonEvent : PlayerEvent +{ + public DungeonType DungeonType { get; init; } + public long FamilyId { get; init; } + public IEnumerable Members { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Act4/Event/Act4MukrajuDeathEvent.cs b/srcs/WingsAPI.Game/Act4/Event/Act4MukrajuDeathEvent.cs new file mode 100644 index 0000000..c0b9acd --- /dev/null +++ b/srcs/WingsAPI.Game/Act4/Event/Act4MukrajuDeathEvent.cs @@ -0,0 +1,11 @@ +using PhoenixLib.Events; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Act4.Event; + +public class Act4MukrajuDeathEvent : IAsyncEvent +{ + public Act4MukrajuDeathEvent(FactionType factionType) => FactionType = factionType; + + public FactionType FactionType { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Act4/Event/Act4MukrajuDespawnEvent.cs b/srcs/WingsAPI.Game/Act4/Event/Act4MukrajuDespawnEvent.cs new file mode 100644 index 0000000..f5b0977 --- /dev/null +++ b/srcs/WingsAPI.Game/Act4/Event/Act4MukrajuDespawnEvent.cs @@ -0,0 +1,7 @@ +using PhoenixLib.Events; + +namespace WingsEmu.Game.Act4.Event; + +public class Act4MukrajuDespawnEvent : IAsyncEvent +{ +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Act4/Event/Act4MukrajuSpawnEvent.cs b/srcs/WingsAPI.Game/Act4/Event/Act4MukrajuSpawnEvent.cs new file mode 100644 index 0000000..88584ae --- /dev/null +++ b/srcs/WingsAPI.Game/Act4/Event/Act4MukrajuSpawnEvent.cs @@ -0,0 +1,11 @@ +using PhoenixLib.Events; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Act4.Event; + +public class Act4MukrajuSpawnEvent : IAsyncEvent +{ + public Act4MukrajuSpawnEvent(FactionType factionType) => FactionType = factionType; + + public FactionType FactionType { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Act4/Event/Act4PutFlagEvent.cs b/srcs/WingsAPI.Game/Act4/Event/Act4PutFlagEvent.cs new file mode 100644 index 0000000..5c7deea --- /dev/null +++ b/srcs/WingsAPI.Game/Act4/Event/Act4PutFlagEvent.cs @@ -0,0 +1,9 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Game.Inventory; + +namespace WingsEmu.Game.Act4.Event; + +public class Act4PutFlagEvent : PlayerEvent +{ + public InventoryItem InventoryItem { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Act4/Event/Act4SystemFcBroadcastEvent.cs b/srcs/WingsAPI.Game/Act4/Event/Act4SystemFcBroadcastEvent.cs new file mode 100644 index 0000000..727cadd --- /dev/null +++ b/srcs/WingsAPI.Game/Act4/Event/Act4SystemFcBroadcastEvent.cs @@ -0,0 +1,5 @@ +using PhoenixLib.Events; + +namespace WingsEmu.Game.Act4.Event; + +public record Act4SystemFcBroadcastEvent : IAsyncEvent; \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Act4/IAct4DungeonManager.cs b/srcs/WingsAPI.Game/Act4/IAct4DungeonManager.cs new file mode 100644 index 0000000..efa47aa --- /dev/null +++ b/srcs/WingsAPI.Game/Act4/IAct4DungeonManager.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using WingsEmu.Game.Entities; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Act4; + +public interface IAct4DungeonManager +{ + public bool DungeonsActive { get; } + public DungeonType DungeonType { get; } + public FactionType AllowedFaction { get; } + public DateTime DungeonEnd { get; } + public TimeSpan DungeonEndSpan { get; } + public DateTime DungeonStart { get; } + public IReadOnlyList Dungeons { get; } + + public void EnableDungeons(DungeonType dungeonType, FactionType allowedFaction); + public void SetGuardiansAndPortal(IReadOnlyList guardians, IPortalEntity portal); + public (IReadOnlyList, IPortalEntity) GetAndCleanGuardians(); + public void DisableDungeons(); + + public void RegisterDungeon(DungeonInstance dungeonInstance); + public void UnregisterDungeon(DungeonInstance dungeonInstance); + public DungeonInstance GetDungeon(long familyId); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Act4/IAct4FlagManager.cs b/srcs/WingsAPI.Game/Act4/IAct4FlagManager.cs new file mode 100644 index 0000000..59e08d9 --- /dev/null +++ b/srcs/WingsAPI.Game/Act4/IAct4FlagManager.cs @@ -0,0 +1,108 @@ +using System.Threading; +using WingsEmu.Game.Helpers; + +namespace WingsEmu.Game.Act4; + +public interface IAct4FlagManager +{ + MapLocation AngelFlag { get; } + MapLocation DemonFlag { get; } + + void SetAngelFlag(MapLocation mapLocation); + void SetDemonFlag(MapLocation mapLocation); + + void RemoveAngelFlag(); + void RemoveDemonFlag(); +} + +public class Act4FlagManager : IAct4FlagManager +{ + private readonly ReaderWriterLockSlim _lock = new(); + + private MapLocation _angelFlag; + private MapLocation _demonFlag; + + public MapLocation AngelFlag + { + get + { + _lock.EnterReadLock(); + try + { + return _angelFlag; + } + finally + { + _lock.ExitReadLock(); + } + } + } + + public MapLocation DemonFlag + { + get + { + _lock.EnterReadLock(); + try + { + return _demonFlag; + } + finally + { + _lock.ExitReadLock(); + } + } + } + + public void SetAngelFlag(MapLocation mapLocation) + { + _lock.EnterWriteLock(); + try + { + _angelFlag = mapLocation; + } + finally + { + _lock.ExitWriteLock(); + } + } + + public void SetDemonFlag(MapLocation mapLocation) + { + _lock.EnterWriteLock(); + try + { + _demonFlag = mapLocation; + } + finally + { + _lock.ExitWriteLock(); + } + } + + public void RemoveAngelFlag() + { + _lock.EnterWriteLock(); + try + { + _angelFlag = null; + } + finally + { + _lock.ExitWriteLock(); + } + } + + public void RemoveDemonFlag() + { + _lock.EnterWriteLock(); + try + { + _demonFlag = null; + } + finally + { + _lock.ExitWriteLock(); + } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Act4/IDungeonFactory.cs b/srcs/WingsAPI.Game/Act4/IDungeonFactory.cs new file mode 100644 index 0000000..30bc49f --- /dev/null +++ b/srcs/WingsAPI.Game/Act4/IDungeonFactory.cs @@ -0,0 +1,6 @@ +namespace WingsEmu.Game.Act4; + +public interface IDungeonFactory +{ + DungeonInstance CreateDungeon(long familyId, DungeonType dungeonType); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Act4/IDungeonManager.cs b/srcs/WingsAPI.Game/Act4/IDungeonManager.cs new file mode 100644 index 0000000..4b784f7 --- /dev/null +++ b/srcs/WingsAPI.Game/Act4/IDungeonManager.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using WingsEmu.Game.Act4.Entities; + +namespace WingsEmu.Game.Act4; + +public interface IDungeonManager +{ + void AddNewHatusState(Guid mapInstanceId, HatusState hatusState); + HatusState GetHatusState(Guid mapInstanceId); + void RemoveHatusState(Guid mapInstanceId); + + void AddCalvinasDragons(Guid mapInstanceId, CalvinasState calvinasState); + CalvinasState GetCalvinasDragons(Guid mapInstanceId); + void RemoveCalvinasDragons(Guid mapInstanceId); +} + +public class DungeonManager : IDungeonManager +{ + private readonly ConcurrentDictionary _calvinasStates = new(); + private readonly ConcurrentDictionary _hatusStates = new(); + + public void AddNewHatusState(Guid mapInstanceId, HatusState hatusState) + { + _hatusStates.TryAdd(mapInstanceId, hatusState); + } + + public HatusState GetHatusState(Guid mapInstanceId) => !_hatusStates.TryGetValue(mapInstanceId, out HatusState hatusState) ? null : hatusState; + + public void RemoveHatusState(Guid mapInstanceId) + { + _hatusStates.TryRemove(mapInstanceId, out _); + } + + public void AddCalvinasDragons(Guid mapInstanceId, CalvinasState calvinasState) + { + _calvinasStates.TryAdd(mapInstanceId, calvinasState); + } + + public CalvinasState GetCalvinasDragons(Guid mapInstanceId) => !_calvinasStates.TryGetValue(mapInstanceId, out CalvinasState calvinasState) ? null : calvinasState; + + public void RemoveCalvinasDragons(Guid mapInstanceId) + { + _calvinasStates.TryRemove(mapInstanceId, out _); + } +} + +public class HatusState +{ + public TimeSpan CastTime { get; init; } + public double DealtDamage { get; init; } + + public bool BlueAttack { get; set; } + public short BlueX { get; set; } + + public bool RedAttack { get; set; } + public short RedX { get; set; } + + public bool GreenAttack { get; set; } + public short GreenX { get; set; } +} + +public class CalvinasState +{ + public DateTime CastTime { get; init; } + + public List CalvinasDragonsList { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Act5/Act5OpenNpcRunEvent.cs b/srcs/WingsAPI.Game/Act5/Act5OpenNpcRunEvent.cs new file mode 100644 index 0000000..80940f2 --- /dev/null +++ b/srcs/WingsAPI.Game/Act5/Act5OpenNpcRunEvent.cs @@ -0,0 +1,10 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Act5; + +public class Act5OpenNpcRunEvent : PlayerEvent +{ + public NpcRunType NpcRunType { get; init; } + public bool IsConfirm { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Algorithm/Events/GenerateExperienceEvent.cs b/srcs/WingsAPI.Game/Algorithm/Events/GenerateExperienceEvent.cs new file mode 100644 index 0000000..3bc7715 --- /dev/null +++ b/srcs/WingsAPI.Game/Algorithm/Events/GenerateExperienceEvent.cs @@ -0,0 +1,19 @@ +using PhoenixLib.Events; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Entities; + +namespace WingsEmu.Game.Algorithm.Events; + +public class GenerateExperienceEvent : IAsyncEvent +{ + public GenerateExperienceEvent(IPlayerEntity character, IMonsterEntity monsterEntity, long? monsterOwnerId) + { + Character = character; + MonsterEntity = monsterEntity; + MonsterOwnerId = monsterOwnerId; + } + + public IPlayerEntity Character { get; } + public IMonsterEntity MonsterEntity { get; } + public long? MonsterOwnerId { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Algorithm/IBattleEntityAlgorithmService.cs b/srcs/WingsAPI.Game/Algorithm/IBattleEntityAlgorithmService.cs new file mode 100644 index 0000000..ac64861 --- /dev/null +++ b/srcs/WingsAPI.Game/Algorithm/IBattleEntityAlgorithmService.cs @@ -0,0 +1,116 @@ +using WingsEmu.Game._enum; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Battle; +using WingsEmu.Packets.Enums.Character; + +namespace WingsEmu.Game.Algorithm; + +public interface IBattleEntityAlgorithmService +{ + /// + /// Returns the MaxHp based on basic algorithm + /// + /// + /// + /// + /// + /// + /// + int GetBasicHp(int race, int level, int modifier, int additionalHp = 0, bool isMonster = true); + + /// + /// Returns the MaxMp based on basic algorithm + /// + /// + /// + /// + /// + /// + /// + int GetBasicMp(int race, int level, int modifier, int additionalMp = 0, bool isMonster = true); + + /// + /// Returns the MaxHp based on ClassType + /// + /// + /// + /// + int GetBasicHpByClass(ClassType classType, int level); + + /// + /// Returns the MaxMp based on ClassType + /// + /// + /// + /// + int GetBasicMpByClass(ClassType classType, int level); + + /// + /// Get minimum/maximum attack damage + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + int GetAttack(bool isMin, int race, AttackType attackType, short weaponLevel, byte wInfo, short level, int modifier, int additional, bool isWild = true, short petLevel = 0, + MateType mateType = MateType.Pet); + + /// + /// Get hitrate + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + int GetHitrate(int race, AttackType attackType, short weaponLevel, short level, int modifier, int additional, bool isWild = true, short petLevel = 0, MateType mateType = MateType.Pet); + + /// + /// Get dodge + /// + /// + /// + /// + /// + /// + /// + /// + /// + int GetDodge(int race, short armorLevel, short level, int modifier, int additional, bool isWild = true, short petLevel = 0, MateType mateType = MateType.Pet); + + /// + /// Get defense + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + int GetDefense(int race, AttackType attackType, short armorLevel, short level, int modifier, int additional, bool isWild = true, short petLevel = 0, MateType mateType = MateType.Pet); + + /// + /// Get Speed + /// + /// + byte GetSpeed(ClassType classType); + + int GetBaseStatistic(int level, ClassType classType, StatisticType statisticType); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Algorithm/ICellonGenerationAlgorithm.cs b/srcs/WingsAPI.Game/Algorithm/ICellonGenerationAlgorithm.cs new file mode 100644 index 0000000..b576d54 --- /dev/null +++ b/srcs/WingsAPI.Game/Algorithm/ICellonGenerationAlgorithm.cs @@ -0,0 +1,12 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsEmu.DTOs.Items; + +namespace WingsEmu.Game.Algorithm; + +public interface ICellonGenerationAlgorithm +{ + public EquipmentOptionDTO GenerateOption(int cellonLevel); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Algorithm/ICharacterAlgorithm.cs b/srcs/WingsAPI.Game/Algorithm/ICharacterAlgorithm.cs new file mode 100644 index 0000000..5f444d5 --- /dev/null +++ b/srcs/WingsAPI.Game/Algorithm/ICharacterAlgorithm.cs @@ -0,0 +1,26 @@ +using WingsEmu.Game.Characters; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Character; + +namespace WingsEmu.Game.Algorithm; + +public class StaticCharacterAlgorithmService +{ + public static ICharacterAlgorithm Instance { get; private set; } + + public static void Initialize(ICharacterAlgorithm instance) + { + Instance = instance; + } +} + +public interface ICharacterAlgorithm +{ + long GetLevelXp(short level, bool isMate = false, MateType mateType = 0); + int GetSpecialistJobXp(short level, bool isFunSpecialist = false); + int GetHeroLevelXp(short level); + int GetJobXp(short level, bool isAdventurer = false); + int GetFairyXp(short level); + int GetRegenHp(IPlayerEntity character, ClassType type, bool isSiting); + int GetRegenMp(IPlayerEntity character, ClassType type, bool isSiting); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Algorithm/IDamageAlgorithm.cs b/srcs/WingsAPI.Game/Algorithm/IDamageAlgorithm.cs new file mode 100644 index 0000000..f9ad464 --- /dev/null +++ b/srcs/WingsAPI.Game/Algorithm/IDamageAlgorithm.cs @@ -0,0 +1,9 @@ +using WingsEmu.Game.Battle; +using WingsEmu.Game.Helpers.Damages; + +namespace WingsEmu.Game.Algorithm; + +public interface IDamageAlgorithm +{ + DamageAlgorithmResult GenerateDamage(IBattleEntityDump attacker, IBattleEntityDump target, SkillInfo skill); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Algorithm/IShellGenerationAlgorithm.cs b/srcs/WingsAPI.Game/Algorithm/IShellGenerationAlgorithm.cs new file mode 100644 index 0000000..45242c1 --- /dev/null +++ b/srcs/WingsAPI.Game/Algorithm/IShellGenerationAlgorithm.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using WingsEmu.DTOs.Items; + +namespace WingsEmu.Game.Algorithm; + +public interface IShellGenerationAlgorithm +{ + /// + /// + /// + /// + /// + /// + IEnumerable GenerateShell(byte shellType, int shellRarity, int shellLevel); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Arena/IArenaManager.cs b/srcs/WingsAPI.Game/Arena/IArenaManager.cs new file mode 100644 index 0000000..c56ce11 --- /dev/null +++ b/srcs/WingsAPI.Game/Arena/IArenaManager.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; +using WingsEmu.Game.Maps; + +namespace WingsEmu.Game.Arena; + +public interface IArenaManager +{ + public IMapInstance ArenaInstance { get; } + public IMapInstance FamilyArenaInstance { get; } + public Task Initialize(); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/BCard/BCardComponent.cs b/srcs/WingsAPI.Game/BCard/BCardComponent.cs new file mode 100644 index 0000000..a3db268 --- /dev/null +++ b/srcs/WingsAPI.Game/BCard/BCardComponent.cs @@ -0,0 +1,481 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using WingsEmu.Core.Extensions; +using WingsEmu.Core.Generics; +using WingsEmu.DTOs.BCards; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Buffs; + +public class BCardComponent : IBCardComponent +{ + private readonly List _allBCards = new(); + private readonly ConcurrentDictionary<(BCardType, byte), ThreadSafeHashSet> _bCards = new(); + private readonly ConcurrentDictionary> _bCardsByEquipmentType = new(); + + private readonly ConcurrentDictionary> _buffBCards = new(); + private readonly List<(int casterLevel, BCardDTO bCard)> _buffBCardsCasterLevel = new(); + private readonly ConcurrentDictionary<(BCardType, byte), Dictionary> _buffInformation = new(); + + private readonly ThreadSafeHashSet _chargeBCards = new(); + + private readonly ConcurrentDictionary<(BCardType, byte), byte> _hasBCardCache = new(); + + private readonly ReaderWriterLockSlim _lock = new(LockRecursionPolicy.SupportsRecursion); + + private readonly List _onAttack = new(); + private readonly List _onDefense = new(); + private readonly IRandomGenerator _randomGenerator; + + private readonly ConcurrentDictionary> _shellTriggerOnAttack = new(); + + public BCardComponent(IRandomGenerator randomGenerator) => _randomGenerator = randomGenerator; + + public void AddEquipmentBCards(EquipmentType equipmentType, IEnumerable bCards) + { + _lock.EnterWriteLock(); + try + { + List existingBCards = _bCardsByEquipmentType.GetOrDefault(equipmentType); + if (existingBCards == null) + { + existingBCards = new List(); + _bCardsByEquipmentType[equipmentType] = existingBCards; + } + + foreach (BCardDTO bCard in bCards) + { + existingBCards.Add(bCard); + AddBCard(bCard); + } + } + finally + { + _lock.ExitWriteLock(); + } + } + + public void ClearEquipmentBCards(EquipmentType equipmentType) + { + _lock.EnterWriteLock(); + try + { + _bCardsByEquipmentType.TryRemove(equipmentType, out List bCards); + if (bCards == null) + { + return; + } + + foreach (BCardDTO bCard in bCards) + { + RemoveBCard(bCard); + } + } + finally + { + _lock.ExitWriteLock(); + } + } + + public IReadOnlyList GetEquipmentBCards(EquipmentType equipmentType) + { + List eqBCards = _bCardsByEquipmentType.GetOrDefault(equipmentType); + if (eqBCards == null) + { + return Array.Empty(); + } + + return eqBCards; + } + + public IReadOnlyDictionary> GetEquipmentBCards() => _bCardsByEquipmentType; + + public (int firstData, int secondData) GetAllBCardsInformation(BCardType type, byte subType, int level) + { + int firstData = 0; + int secondData = 0; + double firstDataMultiply = 1; + double secondDataMultiply = 1; + + if (_bCards.TryGetValue((type, subType), out ThreadSafeHashSet list)) + { + foreach (BCardDTO bCard in list) + { + int firstDataValue = bCard.FirstDataValue(level); + int secondDataValue = bCard.SecondDataValue(level); + + if (type == BCardType.Morale) + { + switch (subType) + { + case (byte)AdditionalTypes.Morale.SkillCooldownIncreased: + firstDataMultiply *= 1 + 0.01 * firstDataValue; + secondDataMultiply *= 1 + 0.01 * secondDataValue; + break; + case (byte)AdditionalTypes.Morale.SkillCooldownDecreased: + firstDataMultiply *= 0.01 * (100 - firstDataValue); + secondDataMultiply *= 0.01 * (100 - secondDataValue); + break; + } + + continue; + } + + firstData += firstDataValue; + secondData += secondDataValue; + } + } + + if (_buffInformation.TryGetValue((type, subType), out Dictionary buff)) + { + foreach ((int casterLevel, BCardDTO bCard) in buff.Values) + { + int firstDataValue = bCard.FirstDataValue(casterLevel); + int secondDataValue = bCard.SecondDataValue(casterLevel); + + if (type == BCardType.Morale) + { + switch (subType) + { + case (byte)AdditionalTypes.Morale.SkillCooldownIncreased: + firstDataMultiply *= 1 + 0.01 * firstDataValue; + secondDataMultiply *= 1 + 0.01 * secondDataValue; + break; + case (byte)AdditionalTypes.Morale.SkillCooldownDecreased: + firstDataMultiply *= 0.01 * (100 - firstDataValue); + secondDataMultiply *= 0.01 * (100 - secondDataValue); + break; + } + + continue; + } + + firstData += firstDataValue; + secondData += secondDataValue; + } + } + + if (type == BCardType.Morale && (subType == (byte)AdditionalTypes.Morale.SkillCooldownIncreased || subType == (byte)AdditionalTypes.Morale.SkillCooldownDecreased)) + { + return ((int)(firstDataMultiply * 100), (int)(secondDataMultiply * 100)); + } + + return (firstData, secondData); + } + + public IReadOnlyList GetAllBCards() + { + _lock.EnterReadLock(); + try + { + return _allBCards.ToArray(); + } + finally + { + _lock.ExitReadLock(); + } + } + + public IReadOnlyList<(int casterLevel, BCardDTO bCard)> GetBuffBCards(Func<(int, BCardDTO), bool> predicate = null) + { + _lock.EnterReadLock(); + try + { + return _buffBCardsCasterLevel.FindAll(x => predicate == null || predicate(x)); + } + finally + { + _lock.ExitReadLock(); + } + } + + public bool HasBCard(BCardType bCardType, byte subType) => _hasBCardCache.TryGetValue((bCardType, subType), out byte amount) && amount > 0; + + public bool HasEquipmentsBCard(BCardType bCardType, byte subType) + { + return _bCardsByEquipmentType.Values.Any(x => x.Any(s => s.Type == (short)bCardType && s.SubType == subType)); + } + + public void AddBCard(BCardDTO bCard) + { + (BCardType, byte) key = ((BCardType)bCard.Type, bCard.SubType); + ThreadSafeHashSet existingBCards = _bCards.GetOrAdd(key, new ThreadSafeHashSet()); + existingBCards.Add(bCard); + + if (!_hasBCardCache.TryGetValue(key, out _)) + { + _hasBCardCache.TryAdd(key, 1); + } + else + { + _hasBCardCache[key]++; + } + + _allBCards.Add(bCard); + } + + public void RemoveBCard(BCardDTO bCard) + { + (BCardType, byte) key = ((BCardType)bCard.Type, bCard.SubType); + ThreadSafeHashSet existingBCards = _bCards.GetOrDefault(key); + + existingBCards?.Remove(bCard); + if (_hasBCardCache.TryGetValue(key, out byte amount)) + { + amount -= 1; + if (amount > 0) + { + _hasBCardCache[key] = amount; + } + else + { + _hasBCardCache.TryRemove(key, out _); + } + } + + _allBCards.Remove(bCard); + } + + public void AddBuffBCards(Buff buff, IEnumerable bCards = null) + { + _lock.EnterWriteLock(); + try + { + if (_buffBCards.ContainsKey(buff.BuffId) && bCards == null) + { + return; + } + + if (bCards != null) // Second BCards execution + { + foreach (BCardDTO bCard in bCards) + { + (BCardType, byte SubType) key = ((BCardType)bCard.Type, bCard.SubType); + if (bCard.ProcChance != 0) + { + int randomNumber = _randomGenerator.RandomNumber(); + if (randomNumber > bCard.ProcChance) + { + continue; + } + } + + if (!_buffBCards.TryGetValue(buff.BuffId, out HashSet hashSet)) + { + hashSet = new HashSet(); + _buffBCards[buff.BuffId] = hashSet; + } + + hashSet.Add(bCard); + _buffBCardsCasterLevel.Add((buff.CasterLevel, bCard)); + if (!_hasBCardCache.TryGetValue(key, out _)) + { + _hasBCardCache.TryAdd(key, 1); + } + else + { + _hasBCardCache[key]++; + } + + if (!_buffInformation.TryGetValue(key, out Dictionary newDictionary)) + { + newDictionary = new Dictionary(); + _buffInformation[((BCardType)bCard.Type, bCard.SubType)] = newDictionary; + } + + newDictionary[bCard.Id] = (buff.CasterLevel, bCard); + } + } + else + { + foreach (BCardDTO bCard in buff.BCards.Where(x => !x.IsSecondBCardExecution.HasValue || !x.IsSecondBCardExecution.Value)) + { + (BCardType, byte SubType) key = ((BCardType)bCard.Type, bCard.SubType); + if (bCard.ProcChance != 0) + { + int randomNumber = _randomGenerator.RandomNumber(); + if (randomNumber > bCard.ProcChance) + { + continue; + } + } + + if (!_buffBCards.TryGetValue(buff.BuffId, out HashSet hashSet)) + { + hashSet = new HashSet(); + _buffBCards[buff.BuffId] = hashSet; + } + + hashSet.Add(bCard); + _buffBCardsCasterLevel.Add((buff.CasterLevel, bCard)); + if (!_hasBCardCache.TryGetValue(key, out _)) + { + _hasBCardCache.TryAdd(key, 1); + } + else + { + _hasBCardCache[key]++; + } + + if (!_buffInformation.TryGetValue(key, out Dictionary newDictionary)) + { + newDictionary = new Dictionary(); + _buffInformation[((BCardType)bCard.Type, bCard.SubType)] = newDictionary; + } + + newDictionary[bCard.Id] = (buff.CasterLevel, bCard); + } + } + } + finally + { + _lock.ExitWriteLock(); + } + } + + public void RemoveBuffBCards(Buff buff) + { + _lock.EnterWriteLock(); + try + { + if (!_buffBCards.TryRemove(buff.BuffId, out HashSet bCards)) + { + return; + } + + foreach (BCardDTO bCard in bCards) + { + (BCardType, byte SubType) key = ((BCardType)bCard.Type, bCard.SubType); + if (!_buffInformation.TryGetValue(key, out Dictionary toRemove)) + { + _hasBCardCache.TryRemove(key, out _); + continue; + } + + toRemove.Remove(bCard.Id); + _buffBCardsCasterLevel.Remove((buff.CasterLevel, bCard)); + if (_hasBCardCache.TryGetValue(key, out byte amount)) + { + amount -= 1; + if (amount > 0) + { + _hasBCardCache[key] = amount; + continue; + } + + _hasBCardCache.TryRemove(key, out _); + _buffInformation.TryRemove(key, out _); + continue; + } + + _hasBCardCache.TryRemove(key, out _); + _buffInformation.TryRemove(key, out _); + } + } + finally + { + _lock.ExitWriteLock(); + } + } + + public void AddTriggerBCards(BCardTriggerType triggerType, List bCards) + { + _lock.EnterWriteLock(); + try + { + if (bCards == null) + { + return; + } + + if (!bCards.Any()) + { + return; + } + + switch (triggerType) + { + case BCardTriggerType.ATTACK: + _onAttack.AddRange(bCards); + break; + case BCardTriggerType.DEFENSE: + _onDefense.AddRange(bCards); + break; + } + } + finally + { + _lock.ExitWriteLock(); + } + } + + public void RemoveAllTriggerBCards(BCardTriggerType triggerType) + { + _lock.EnterWriteLock(); + try + { + if (triggerType == BCardTriggerType.ATTACK) + { + _onAttack.Clear(); + return; + } + + _onDefense.Clear(); + } + finally + { + _lock.ExitWriteLock(); + } + } + + public IReadOnlyList GetTriggerBCards(BCardTriggerType triggerType) + => triggerType == BCardTriggerType.ATTACK ? _onAttack : _onDefense; + + public void AddShellTrigger(bool isMainWeapon, List bCards) + { + if (!_shellTriggerOnAttack.TryGetValue(isMainWeapon, out List list)) + { + list = new List(); + _shellTriggerOnAttack[isMainWeapon] = list; + } + + list.AddRange(bCards); + } + + public void ClearShellTrigger(bool isMainWeapon) + { + if (!_shellTriggerOnAttack.TryGetValue(isMainWeapon, out List list)) + { + return; + } + + list.Clear(); + } + + public IReadOnlyList GetShellTriggers(bool isMainWeapon) => !_shellTriggerOnAttack.TryGetValue(isMainWeapon, out List list) ? Array.Empty() : list; + + public void AddChargeBCard(BCardDTO bCard) + { + if (_chargeBCards.Contains(bCard)) + { + return; + } + + _chargeBCards.Add(bCard); + } + + public void RemoveChargeBCard(BCardDTO bCard) + { + _chargeBCards.Remove(bCard); + } + + public void ClearChargeBCard() + { + _chargeBCards.Clear(); + } + + public IEnumerable GetChargeBCards() => _chargeBCards; +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/BCard/BCardExtension.cs b/srcs/WingsAPI.Game/BCard/BCardExtension.cs new file mode 100644 index 0000000..a2ea841 --- /dev/null +++ b/srcs/WingsAPI.Game/BCard/BCardExtension.cs @@ -0,0 +1,60 @@ +using WingsAPI.Packets.Enums.Shells; +using WingsEmu.DTOs.BCards; +using WingsEmu.Game._enum; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Buffs; + +public static class BCardExtension +{ + public static int FirstDataValue(this BCardDTO bCard, int senderLevel) + { + int firstDataValue = bCard.FirstDataScalingType switch + { + BCardScalingType.NORMAL_VALUE => bCard.FirstData, + BCardScalingType.LEVEL_MULTIPLIED => senderLevel * bCard.FirstData, + BCardScalingType.LEVEL_DIVIDED => bCard.FirstData == 0 ? 0 : senderLevel / bCard.FirstData + }; + + return firstDataValue; + } + + public static int SecondDataValue(this BCardDTO bCard, int senderLevel) + { + int secondDataValue = bCard.SecondDataScalingType switch + { + BCardScalingType.NORMAL_VALUE => bCard.SecondData, + BCardScalingType.LEVEL_MULTIPLIED => senderLevel * bCard.SecondData, + BCardScalingType.LEVEL_DIVIDED => bCard.SecondData == 0 ? 0 : senderLevel / bCard.SecondData + }; + + return secondDataValue; + } + + public static BCardDTO TryCreateBuffBCard(this ShellEffectType type, int value) + { + int? buffVnum = type switch + { + ShellEffectType.MinorBleeding => (short)BuffVnums.MINOR_BLEEDING, + ShellEffectType.Bleeding => (short)BuffVnums.BLEEDING, + ShellEffectType.HeavyBleeding => (short)BuffVnums.HEAVY_BLEEDING, + ShellEffectType.Blackout => (short)BuffVnums.BLACKOUT, + ShellEffectType.Freeze => (short)BuffVnums.FREEZE, + ShellEffectType.DeadlyBlackout => (short)BuffVnums.DEADLY_BLACKOUT, + _ => null + }; + + if (buffVnum == null) + { + return null; + } + + return new BCardDTO + { + Type = (short)BCardType.Buff, + SubType = (byte)AdditionalTypes.Buff.ChanceCausing, + FirstData = value, + SecondData = buffVnum.Value + }; + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/BCard/BCardTriggerType.cs b/srcs/WingsAPI.Game/BCard/BCardTriggerType.cs new file mode 100644 index 0000000..d5d5610 --- /dev/null +++ b/srcs/WingsAPI.Game/BCard/BCardTriggerType.cs @@ -0,0 +1,7 @@ +namespace WingsEmu.Game.Buffs; + +public enum BCardTriggerType : byte +{ + ATTACK = 0, + DEFENSE = 1 +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/BCard/BCardType.cs b/srcs/WingsAPI.Game/BCard/BCardType.cs new file mode 100644 index 0000000..776fd3c --- /dev/null +++ b/srcs/WingsAPI.Game/BCard/BCardType.cs @@ -0,0 +1,130 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Game.Buffs; + +public enum BCardType : short +{ + // 1-20 + SpecialAttack = 1, + SpecialDefence = 2, + AttackPower = 3, + Target = 4, + Critical = 5, + SpecialCritical = 6, + Element = 7, + IncreaseDamage = 8, + Defence = 9, + DodgeAndDefencePercent = 10, + Block = 11, + Absorption = 12, + ElementResistance = 13, + EnemyElementResistance = 14, + Damage = 15, + GuarantedDodgeRangedAttack = 16, + Morale = 17, + Casting = 18, + Move = 19, + Reflection = 20, + + // 21-40 + DrainAndSteal = 21, + HealingBurningAndCasting = 22, + HPMP = 23, + SpecialisationBuffResistance = 24, + Buff = 25, + Summons = 26, + SpecialEffects = 27, + Capture = 28, + SpecialDamageAndExplosions = 29, + SpecialEffects2 = 30, + CalculatingLevel = 31, + Recovery = 32, + MaxHPMP = 33, + MultAttack = 34, + MultDefence = 35, + TimeCircleSkills = 36, + RecoveryAndDamagePercent = 37, + Count = 38, + NoDefeatAndNoDamage = 39, + SpecialActions = 40, + + // 41-60 + Mode = 41, + NoCharacteristicValue = 42, + LightAndShadow = 43, + Item = 44, + DebuffResistance = 45, + SpecialBehaviour = 46, + Quest = 47, + SecondSPCard = 48, + SPCardUpgrade = 49, + HugeSnowman = 50, + Drain = 51, + LordMorcos = 52, + LordHatus = 53, + LordCalvinas = 54, + SESpecialist = 55, + LordBerios = 56, + FourthGlacernonFamilyRaid = 57, + BearSpirit = 58, + SummonSkill = 59, + InflictSkill = 60, + + // 61-80 + HideBarrelSkill = 62, + Unknown = 61, + FocusEnemyAttentionSkill = 63, + TauntSkill = 64, + FireCannoneerRangeBuff = 65, + VulcanoElementBuff = 66, + DamageConvertingSkill = 67, + MeditationSkill = 68, + FalconSkill = 69, + AbsorptionAndPowerSkill = 70, + LeonaPassiveSkill = 71, + FearSkill = 72, + SniperAttack = 73, + FrozenDebuff = 74, + JumpBackPush = 75, + FairyXPIncrease = 76, + SummonAndRecoverHP = 77, + TeamArenaBuff = 78, + ArenaCamera = 79, + DarkCloneSummon = 80, + + // 81-100 + AbsorbedSpirit = 81, + AngerSkill = 82, + MeteoriteTeleport = 83, + StealBuff = 84, + ChangingPlace = 85, + EffectSummon = 86, + ReputHeroLevel = 87, + BackToMiniland = 88, + DragonSkills = 89, + IncreaseDamageVersus = 90, + SP2MA_1 = 91, + SP2MA_2 = 92, + IncreaseElementProcent = 93, + IncreaseElementByResis = 94, + DropItemTwice = 95, + IncreaseElementFairy = 96, + IncreaseDamageDebuffs = 97, + IncreaseElementDamage = 98, + IncreaseDamageVersusMonsters = 99, + IncreaseDamageVersusMonsters_2 = 100, + + // 101-?? + IncreaseDamageInLoD = 101, + IncreaseSpPoints = 102, + IncreaseAllDamage = 103, + DealDamageAround = 104, + Runes_1 = 105, + Runes_2 = 106, + MagicShield = 107, + ReflectDamage = 108, + Tattoo = 109, + KingOfTheBeast = 110 +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/BCard/IBCardComponent.cs b/srcs/WingsAPI.Game/BCard/IBCardComponent.cs new file mode 100644 index 0000000..bb74733 --- /dev/null +++ b/srcs/WingsAPI.Game/BCard/IBCardComponent.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using WingsEmu.DTOs.BCards; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Buffs; + +public interface IBCardComponent +{ + public void AddEquipmentBCards(EquipmentType equipmentType, IEnumerable bCards); + public void ClearEquipmentBCards(EquipmentType equipmentType); + + public IReadOnlyList GetEquipmentBCards(EquipmentType equipmentType); + public IReadOnlyDictionary> GetEquipmentBCards(); + + public (int firstData, int secondData) GetAllBCardsInformation(BCardType type, byte subType, int level); + + public IReadOnlyList GetAllBCards(); + public IReadOnlyList<(int casterLevel, BCardDTO bCard)> GetBuffBCards(Func<(int, BCardDTO), bool> predicate = null); + + public bool HasBCard(BCardType bCardType, byte subType); + public bool HasEquipmentsBCard(BCardType bCardType, byte subType); + + public void AddBCard(BCardDTO bCard); + public void RemoveBCard(BCardDTO bCard); + + public void AddBuffBCards(Buff buff, IEnumerable bCards = null); + public void RemoveBuffBCards(Buff buff); + + public void AddTriggerBCards(BCardTriggerType triggerType, List bCards); + public void RemoveAllTriggerBCards(BCardTriggerType triggerType); + + public IReadOnlyList GetTriggerBCards(BCardTriggerType triggerType); + + public void AddShellTrigger(bool isMainWeapon, List bCards); + public void ClearShellTrigger(bool isMainWeapon); + public IReadOnlyList GetShellTriggers(bool isMainWeapon); + + public void AddChargeBCard(BCardDTO bCard); + public void RemoveChargeBCard(BCardDTO bCard); + public void ClearChargeBCard(); + public IEnumerable GetChargeBCards(); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/BCard/IBCardEffectAsyncHandler.cs b/srcs/WingsAPI.Game/BCard/IBCardEffectAsyncHandler.cs new file mode 100644 index 0000000..36c57cc --- /dev/null +++ b/srcs/WingsAPI.Game/BCard/IBCardEffectAsyncHandler.cs @@ -0,0 +1,11 @@ +namespace WingsEmu.Game.Buffs; + +/// +/// Handlers of an BCardEventContext +/// +public interface IBCardEffectAsyncHandler +{ + BCardType HandledType { get; } + + void Execute(IBCardEffectContext ctx); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/BCard/IBCardEffectContext.cs b/srcs/WingsAPI.Game/BCard/IBCardEffectContext.cs new file mode 100644 index 0000000..dd6877c --- /dev/null +++ b/srcs/WingsAPI.Game/BCard/IBCardEffectContext.cs @@ -0,0 +1,18 @@ +using WingsEmu.DTOs.BCards; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Helpers.Damages; + +namespace WingsEmu.Game.Buffs; + +/// +/// Bcard event context +/// +public interface IBCardEffectContext +{ + IBattleEntity Sender { get; } + IBattleEntity Target { get; } + SkillInfo Skill { get; } + BCardDTO BCard { get; } + Position Position { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/BCard/IBCardEffectHandlerContainer.cs b/srcs/WingsAPI.Game/BCard/IBCardEffectHandlerContainer.cs new file mode 100644 index 0000000..adfb603 --- /dev/null +++ b/srcs/WingsAPI.Game/BCard/IBCardEffectHandlerContainer.cs @@ -0,0 +1,16 @@ +using WingsEmu.DTOs.BCards; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Helpers.Damages; + +namespace WingsEmu.Game.Buffs; + +public interface IBCardEffectHandlerContainer +{ + void Register(IBCardEffectAsyncHandler handler); + + void Unregister(IBCardEffectAsyncHandler handler); + + void Execute(IBattleEntity target, IBattleEntity sender, BCardDTO bCard, SkillInfo skill = null, Position position = default, + BCardNpcMonsterTriggerType triggerType = BCardNpcMonsterTriggerType.NONE); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/BCard/IBCardEventContextFactory.cs b/srcs/WingsAPI.Game/BCard/IBCardEventContextFactory.cs new file mode 100644 index 0000000..8947ac3 --- /dev/null +++ b/srcs/WingsAPI.Game/BCard/IBCardEventContextFactory.cs @@ -0,0 +1,11 @@ +using WingsEmu.DTOs.BCards; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Helpers.Damages; + +namespace WingsEmu.Game.Buffs; + +public interface IBCardEventContextFactory +{ + IBCardEffectContext NewContext(IBattleEntity sender, IBattleEntity target, BCardDTO bCard, SkillInfo skill = null, Position position = default); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/BCard/StaticBCardEffectHandlerService.cs b/srcs/WingsAPI.Game/BCard/StaticBCardEffectHandlerService.cs new file mode 100644 index 0000000..f54354a --- /dev/null +++ b/srcs/WingsAPI.Game/BCard/StaticBCardEffectHandlerService.cs @@ -0,0 +1,11 @@ +namespace WingsEmu.Game.Buffs; + +public class StaticBCardEffectHandlerService +{ + public static IBCardEffectHandlerContainer Instance { get; private set; } + + public static void Initialize(IBCardEffectHandlerContainer instance) + { + Instance = instance; + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Battle/BattleEntityDumpExtensions.cs b/srcs/WingsAPI.Game/Battle/BattleEntityDumpExtensions.cs new file mode 100644 index 0000000..c032854 --- /dev/null +++ b/srcs/WingsAPI.Game/Battle/BattleEntityDumpExtensions.cs @@ -0,0 +1,393 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using WingsAPI.Data.Families; +using WingsAPI.Packets.Enums.Shells; +using WingsEmu.Core.Extensions; +using WingsEmu.DTOs.BCards; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Extensions.Mates; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Inventory.Event; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Monster; +using WingsEmu.Game.RainbowBattle.Event; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Game.Battle; + +public static class BattleEntityDumpExtensions +{ + private static readonly double[] _skillRankMultiplier = { 0, 0.3, 0.5, 0.8, 1, 1.2, 1.5, 2.5 }; + private static IRandomGenerator _randomGenerator => StaticRandomGenerator.Instance; + + public static bool HasBuff(this IBattleEntityDump entity, int cardId) + { + if (!entity.BuffsById.Any()) + { + return false; + } + + return entity.BuffsById.Contains(cardId); + } + + public static bool IsPlayer(this IEntity entity) => entity.Type == VisualType.Player; + + public static bool IsAlive(this IBattleEntity battleEntity) => battleEntity.Hp > 0; + + public static bool IsMonster(this IEntity entity) => entity.Type == VisualType.Monster; + + public static bool IsMate(this IEntity entity) => entity is IMateEntity; + + public static bool IsNpc(this IEntity entity) => entity is INpcEntity; + + public static bool IsPlayer(this IBattleEntityDump entity) => entity.Type == VisualType.Player; + + public static bool IsMonster(this IBattleEntityDump entity) => entity.Type == VisualType.Monster; + + public static bool IsMate(this IBattleEntityDump entity) => entity is MateBattleEntityDump; + + public static (int firstData, int secondData, int count) GetBCardInformation(this IBattleEntityDump attacker, BCardType type, byte subtype) + { + int count = 0; + + (int firstData, int secondData) = GetBuff(attacker, type, subtype, ref count); + return (firstData, secondData, count); + } + + public static (int firstData, int secondData, int count) TryFindPartnerSkillInformation(this IBattleEntityDump attacker, BCardType type, byte subtype, SkillInfo skillInfo) + { + if (!attacker.IsMate()) + { + return GetBCardInformation(attacker, type, subtype); + } + + if (!skillInfo.PartnerSkillRank.HasValue) + { + return GetBCardInformation(attacker, type, subtype); + } + + int count = 0; + + int firstData = 0; + int secondData = 0; + + if (attacker.BCards.TryGetValue((type, subtype), out List allBCardsExcept)) + { + HashSet bCardsToIgnore = new(); + foreach (BCardDTO bCard in allBCardsExcept) + { + foreach (BCardDTO skillBCard in skillInfo.BCards.Where(skillBCard => bCard.Id == skillBCard.Id)) + { + if (!bCardsToIgnore.Contains(skillBCard.Id)) + { + bCardsToIgnore.Add(skillBCard.Id); + } + + int firstDataSkillValue = (int)(bCard.FirstData * _skillRankMultiplier[skillInfo.PartnerSkillRank.Value]); + int secondDataSkillValue = (int)(bCard.SecondData * _skillRankMultiplier[skillInfo.PartnerSkillRank.Value]); + + firstData += firstDataSkillValue; + secondData += secondDataSkillValue; + count++; + } + + if (bCardsToIgnore.Contains(bCard.Id)) + { + continue; + } + + int firstDataValue = bCard.FirstDataValue(attacker.Level); + int secondDataValue = bCard.SecondDataValue(attacker.Level); + + firstData += firstDataValue; + secondData += secondDataValue; + count++; + } + } + + if (!attacker.BuffBCards.TryGetValue((type, subtype), out List<(int casterLevel, BCardDTO bCard)> getBuffs)) + { + return (firstData, secondData, count); + } + + foreach ((int casterLevel, BCardDTO bCard) in getBuffs) + { + int firstDataValue = bCard.FirstDataValue(casterLevel); + int secondDataValue = bCard.SecondDataValue(casterLevel); + + firstData += firstDataValue; + secondData += secondDataValue; + count++; + } + + return (firstData, secondData, count); + } + + public static double GetMultiplier(this IBattleEntityDump entity, int data) => data * 0.01; + + private static (int firstData, int secondData) GetBuff(IBattleEntityDump entity, BCardType type, byte subtype, ref int count) + { + int firstData = 0; + int secondData = 0; + + if (entity.BCards.TryGetValue((type, subtype), out List allBCardsExcept)) + { + foreach (BCardDTO bCard in allBCardsExcept) + { + int firstDataValue = bCard.FirstDataValue(entity.Level); + int secondDataValue = bCard.SecondDataValue(entity.Level); + + firstData += firstDataValue; + secondData += secondDataValue; + count++; + } + } + + if (!entity.BuffBCards.TryGetValue((type, subtype), out List<(int casterLevel, BCardDTO bCard)> getBuffs)) + { + return (firstData, secondData); + } + + foreach ((int casterLevel, BCardDTO bCard) in getBuffs) + { + int firstDataValue = bCard.FirstDataValue(casterLevel); + int secondDataValue = bCard.SecondDataValue(casterLevel); + + firstData += firstDataValue; + secondData += secondDataValue; + count++; + } + + return (firstData, secondData); + } + + public static bool HasBCard(this IBattleEntityDump entity, BCardType type, byte subType) + { + if (!entity.BCards.Any() && !entity.BuffBCards.Any()) + { + return false; + } + + return entity.BCards.ContainsKey((type, subType)) || entity.BuffBCards.ContainsKey((type, subType)); + } + + public static void BroadcastEffect(this IBattleEntityDump dump, EffectType effectType) + { + dump.MapInstance?.Broadcast($"eff {(byte)dump.Type} {dump.Id} {(short)effectType}"); + } + + public static bool IsInvisible(this IBattleEntityDump entity) => entity.HasBCard(BCardType.SpecialActions, (byte)AdditionalTypes.SpecialActions.Hide); + + public static bool IsSucceededChance(this IBattleEntityDump entity, int chance) => _randomGenerator.RandomNumber() <= chance; + + public static Enum GetRaceSubType(this IBattleEntityDump entity, MonsterRaceType type, byte value) + { + return type switch + { + MonsterRaceType.LowLevel => (MonsterSubRace.LowLevel)value, + MonsterRaceType.HighLevel => (MonsterSubRace.HighLevel)value, + MonsterRaceType.Furry => (MonsterSubRace.Furry)value, + MonsterRaceType.People => (MonsterSubRace.People)value, + MonsterRaceType.Angel => (MonsterSubRace.Angels)value, + MonsterRaceType.Undead => (MonsterSubRace.Undead)value, + MonsterRaceType.Spirit => (MonsterSubRace.Spirits)value, + MonsterRaceType.Other => (MonsterSubRace.Other)value, + MonsterRaceType.Fixed => (MonsterSubRace.Fixed)value, + _ => null + }; + } + + public static int GetFamilyUpgradeValue(this IBattleEntityDump entity, FamilyUpgradeType familyUpgradeType) => entity.FamilyUpgrades.GetValueOrDefault(familyUpgradeType); + + public static int GetShellWeaponEffectValue(this IBattleEntityDump entity, ShellEffectType effectType) + { + if (!entity.IsPlayer()) + { + return 0; + } + + var player = (PlayerBattleEntityDump)entity; + if (!player.ShellOptionsWeapon.Any()) + { + return 0; + } + + return player.ShellOptionsWeapon.GetOrDefault(effectType); + } + + public static int GetShellArmorEffectValue(this IBattleEntityDump entity, ShellEffectType effectType) + { + if (!entity.IsPlayer()) + { + return 0; + } + + var character = (PlayerBattleEntityDump)entity; + if (!character.ShellOptionArmor.Any()) + { + return 0; + } + + return character.ShellOptionArmor.GetOrDefault(effectType); + } + + public static int GetJewelsEffectValue(this IBattleEntityDump entity, CellonType effectType) + { + if (!entity.IsPlayer()) + { + return 0; + } + + var character = (PlayerBattleEntityDump)entity; + + int ring = character.RingCellonValues.GetValueOrDefault(effectType); + int necklace = character.NecklaceCellonValues.GetValueOrDefault(effectType); + int bracelet = character.BraceletCellonValues.GetValueOrDefault(effectType); + + return ring + necklace + bracelet; + } + + public static int GetMonsterDamageBonus(this IBattleEntityDump attacker, int level) + { + if (level < 45) + { + return -15; + } + + if (level < 55) + { + return 30 + (level - 45) * 1; + } + + if (level < 60) + { + return 95 + (level - 55) * 2; + } + + if (level < 65) + { + return 165 + (level - 60) * 3; + } + + if (level < 70) + { + return 245 + (level - 65) * 4; + } + + return 335 + (level - 70) * 5; + } + + public static bool IsOnMapType(this IBattleEntityDump entity, MapFlags type) => entity.MapInstance.HasMapFlag(type); + + public static async Task ShouldSaveDefender(this IBattleEntity attacker, IBattleEntity defender, int damage, + GameRevivalConfiguration gameRevivalConfiguration, IBuffFactory buffFactory) + { + switch (defender) + { + case IPlayerEntity playerEntity: + if (playerEntity.CheatComponent.HasGodMode) + { + return true; + } + + if (playerEntity.Hp - damage > 0) + { + return false; + } + + if (!playerEntity.RainbowBattleComponent.IsInRainbowBattle) + { + return false; + } + + await playerEntity.Session.EmitEventAsync(new RainbowBattleFreezeEvent + { + Killer = attacker + }); + + return true; + + case IMateEntity mateEntity: + { + if (attacker.IsPlayer()) + { + return false; + } + + if (mateEntity.MapInstance.HasMapFlag(MapFlags.IS_MINILAND_MAP)) + { + return false; + } + + IPlayerEntity owner = mateEntity.Owner; + + if (mateEntity.Hp - damage > 0) + { + return false; + } + + if (mateEntity.MateType == MateType.Pet ? !owner.IsPetAutoRelive : !owner.IsPartnerAutoRelive) + { + return false; + } + + bool shouldSave = false; + + List itemNeeded = mateEntity.MateType == MateType.Pet + ? gameRevivalConfiguration.MateRevivalConfiguration.MateInstantRevivalPenalizationSaver + : gameRevivalConfiguration.MateRevivalConfiguration.PartnerInstantRevivalPenalizationSaver; + + foreach (int item in itemNeeded) + { + InventoryItem getItem = owner.GetFirstItemByVnum(item); + if (getItem == null) + { + continue; + } + + await owner.Session.EmitEventAsync(new InventoryRemoveItemEvent(item, inventoryItem: getItem)); + shouldSave = true; + break; + } + + if (!shouldSave) + { + return false; + } + + (int mateHpToDecrease, int _) = + mateEntity.BCardComponent.GetAllBCardsInformation(BCardType.DamageConvertingSkill, (byte)AdditionalTypes.DamageConvertingSkill.HPRecoveryDecreased, mateEntity.Level); + + (int mateHpToIncrease, int _) = + mateEntity.BCardComponent.GetAllBCardsInformation(BCardType.DamageConvertingSkill, (byte)AdditionalTypes.DamageConvertingSkill.HPRecoveryIncreased, mateEntity.Level); + + int mateHpToChange = mateHpToIncrease - mateHpToDecrease; + int mateHpHeal = (int)(mateEntity.MaxHp * (1 + mateHpToChange / 100.0)); + + mateEntity.Hp = mateHpHeal; + mateEntity.Mp = mateEntity.MaxMp; + owner.Session.SendMateLife(mateEntity); + + GameDialogKey gameDialogKey = mateEntity.MateType == MateType.Pet ? GameDialogKey.PET_SHOUTMESSGE_SAVED_BY_SAVER : GameDialogKey.PARTNER_SHOUTMESSAGE_SAVED_BY_SAVER; + owner.Session.SendMsg(owner.Session.GetLanguage(gameDialogKey), MsgMessageType.Middle); + return true; + } + default: + return false; + } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Battle/BattleEntitySharedExtensions.cs b/srcs/WingsAPI.Game/Battle/BattleEntitySharedExtensions.cs new file mode 100644 index 0000000..fb7a38c --- /dev/null +++ b/srcs/WingsAPI.Game/Battle/BattleEntitySharedExtensions.cs @@ -0,0 +1,199 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game._enum; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Buffs.Events; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Mates; + +namespace WingsEmu.Game.Battle; + +public static class BattleEntitySharedExtensions +{ + public static async Task AddBuffAsync(this IBattleEntity battleEntity, params Buff[] buffs) + { + await battleEntity.EmitEventAsync(new BuffAddEvent(battleEntity, buffs)); + } + + public static async Task AddBuffsAsync(this IBattleEntity battleEntity, IEnumerable buffs) + { + await battleEntity.EmitEventAsync(new BuffAddEvent(battleEntity, buffs)); + } + + public static async Task CheckAct4Buff(this IPlayerEntity playerEntity, IBuffFactory buffFactory) + { + if (playerEntity.MapInstance == null) + { + return; + } + + if (playerEntity.BuffComponent == null) + { + return; + } + + if (!playerEntity.MapInstance.HasMapFlag(MapFlags.HAS_IMMUNITY_ON_MAP_CHANGE_ENABLED)) + { + return; + } + + Buff buff = buffFactory.CreateBuff((short)BuffVnums.INVICIBLE_IN_PVP, playerEntity, BuffFlag.NORMAL); + await playerEntity.AddBuffAsync(buff); + + if (!playerEntity.MateComponent.TeamMembers().Any()) + { + return; + } + + foreach (IMateEntity mate in playerEntity.MateComponent.TeamMembers()) + { + await mate.AddBuffAsync(buff); + } + } + + public static async Task CheckPvPBuff(this IBattleEntity battleEntity) + { + if (battleEntity.MapInstance == null) + { + return; + } + + if (battleEntity.BuffComponent == null) + { + return; + } + + bool pvp = battleEntity.BuffComponent.HasBuff((int)BuffVnums.PVP); + + if (!pvp) + { + return; + } + + if (!battleEntity.MapInstance.IsPvp) + { + return; + } + + Buff buffFire = battleEntity.BuffComponent.GetBuff((int)BuffVnums.PVP); + await battleEntity.RemoveBuffAsync(false, buffFire); + } + + public static async Task CheckAct52Buff(this IBattleEntity battleEntity, IBuffFactory buffFactory) + { + if (battleEntity.MapInstance == null) + { + return; + } + + if (battleEntity.BuffComponent == null) + { + return; + } + + if (battleEntity.MapInstance.HasMapFlag(MapFlags.HAS_BURNING_SWORD_ENABLED) == false) + { + if (!battleEntity.BuffComponent.HasBuff((int)BuffVnums.ACT_52_FIRE_DEBUFF)) + { + return; + } + + Buff buff = battleEntity.BuffComponent.GetBuff((int)BuffVnums.ACT_52_FIRE_DEBUFF); + await battleEntity.RemoveBuffAsync(true, buff); + + return; + } + + bool oil = battleEntity.BuffComponent.HasBuff((int)BuffVnums.ICE_FLOWER); + bool born = battleEntity.BuffComponent.HasBuff((int)BuffVnums.ACT_52_FIRE_DEBUFF); + + if (born) + { + if (!oil) + { + return; + } + + Buff buff = battleEntity.BuffComponent.GetBuff((int)BuffVnums.ACT_52_FIRE_DEBUFF); + await battleEntity.RemoveBuffAsync(true, buff); + return; + } + + if (!oil) + { + Buff burnBuff = buffFactory.CreateBuff((int)BuffVnums.ACT_52_FIRE_DEBUFF, battleEntity, BuffFlag.BIG | BuffFlag.NO_DURATION); + await battleEntity.AddBuffAsync(burnBuff); + return; + } + + Buff buffFire = battleEntity.BuffComponent.GetBuff((int)BuffVnums.ACT_52_FIRE_DEBUFF); + await battleEntity.RemoveBuffAsync(true, buffFire); + } + + public static async Task RemoveBuffAsync(this IBattleEntity battleEntity, int buffId, bool removePermanent = false) + { + Buff buff = battleEntity.BuffComponent.GetBuff(buffId); + if (buff is null) + { + return; + } + + await battleEntity.EmitEventAsync(new BuffRemoveEvent + { + Entity = battleEntity, + Buffs = new[] { buff }, + RemovePermanentBuff = removePermanent + }); + } + + public static async Task RemoveBuffAsync(this IBattleEntity battleEntity, bool removePermanentBuff, params Buff[] buffs) + { + if (buffs.Length == 0 || buffs.All(s => s == null)) + { + return; + } + + await battleEntity.EmitEventAsync(new BuffRemoveEvent + { + Entity = battleEntity, + Buffs = buffs, + RemovePermanentBuff = removePermanentBuff + }); + } + + public static async Task RemoveAllBuffsAsync(this IBattleEntity entity, bool removePermanentBuff) + { + if (!entity.BuffComponent.HasAnyBuff()) + { + return; + } + + await entity.RemoveBuffAsync(removePermanentBuff, entity.BuffComponent.GetAllBuffs().ToArray()); + } + + public static async Task RemoveBuffsOnDeathAsync(this IBattleEntity entity) + { + if (!entity.BuffComponent.HasAnyBuff()) + { + return; + } + + IReadOnlyList buffs = entity.BuffComponent.GetAllBuffs(x => !x.IsNotRemovedOnDeath()); + await entity.RemoveBuffAsync(false, buffs.ToArray()); + } + + public static async Task RemoveBuffsOnSpTransformAsync(this IBattleEntity entity) + { + if (!entity.BuffComponent.HasAnyBuff()) + { + return; + } + + IReadOnlyList buffs = entity.BuffComponent.GetAllBuffs(x => !x.IsNotDisappearOnSpChange()); + await entity.RemoveBuffAsync(false, buffs.ToArray()); + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Battle/BattleExecuteSkillEvent.cs b/srcs/WingsAPI.Game/Battle/BattleExecuteSkillEvent.cs new file mode 100644 index 0000000..540781f --- /dev/null +++ b/srcs/WingsAPI.Game/Battle/BattleExecuteSkillEvent.cs @@ -0,0 +1,24 @@ +using System; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Helpers.Damages; + +namespace WingsEmu.Game.Battle; + +public class BattleExecuteSkillEvent : IBattleEntityEvent +{ + public BattleExecuteSkillEvent(IBattleEntity entity, IBattleEntity target, SkillInfo skillInfo, DateTime endSkillCastTime, Position position = default) + { + Entity = entity; + Target = target; + SkillInfo = skillInfo; + Position = position; + EndSkillCastTime = endSkillCastTime; + } + + public IBattleEntity Target { get; } + public SkillInfo SkillInfo { get; } + public DateTime EndSkillCastTime { get; } + public Position Position { get; } + + public IBattleEntity Entity { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Battle/BuffProcessable.cs b/srcs/WingsAPI.Game/Battle/BuffProcessable.cs new file mode 100644 index 0000000..6c5e11a --- /dev/null +++ b/srcs/WingsAPI.Game/Battle/BuffProcessable.cs @@ -0,0 +1,20 @@ +using WingsEmu.Game.Entities; +using WingsEmu.Game.Helpers.Damages; + +namespace WingsEmu.Game.Battle; + +public class BuffProcessable +{ + public BuffProcessable(IBattleEntity caster, IBattleEntity target, SkillCast skillCast, Position position = default) + { + Caster = caster; + Target = target; + SkillCast = skillCast; + Position = position; + } + + public IBattleEntity Caster { get; } + public IBattleEntity Target { get; } + public SkillCast SkillCast { get; } + public Position Position { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Battle/BuffRequest.cs b/srcs/WingsAPI.Game/Battle/BuffRequest.cs new file mode 100644 index 0000000..ad7938b --- /dev/null +++ b/srcs/WingsAPI.Game/Battle/BuffRequest.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Helpers.Damages; + +namespace WingsEmu.Game.Battle; + +public class BuffRequest +{ + public BuffRequest(IBattleEntity caster, IEnumerable targets, SkillCast skillCast, Position position, IBattleEntity target = null) + { + Caster = caster; + Targets = targets; + SkillCast = skillCast; + Position = position; + Target = target; + } + + public IBattleEntity Caster { get; } + public IBattleEntity Target { get; } + public IEnumerable Targets { get; } + public SkillCast SkillCast { get; } + public Position Position { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Battle/CastingComponent.cs b/srcs/WingsAPI.Game/Battle/CastingComponent.cs new file mode 100644 index 0000000..2d7d61e --- /dev/null +++ b/srcs/WingsAPI.Game/Battle/CastingComponent.cs @@ -0,0 +1,52 @@ +using System; + +namespace WingsEmu.Game.Battle; + +public interface ICastingComponent +{ + public SkillCast SkillCast { get; } + public bool IsCastingSkill { get; } + public void SetCastingSkill(SkillInfo skill, DateTime time); + public void RemoveCastingSkill(); +} + +public class CastingComponent : ICastingComponent +{ + public SkillCast SkillCast { get; private set; } + + public bool IsCastingSkill => SkillCast != null; + + public void SetCastingSkill(SkillInfo skill, DateTime time) + { + if (SkillCast != null) + { + return; + } + + var skillCast = new SkillCast(skill, time); + SkillCast = skillCast; + } + + public void RemoveCastingSkill() + { + if (SkillCast == null) + { + return; + } + + SkillCast = null; + } +} + +public class SkillCast +{ + public SkillCast(SkillInfo skill, DateTime skillEndCastTime) + { + Skill = skill; + SkillEndCastTime = skillEndCastTime; + } + + public SkillInfo Skill { get; } + + public DateTime SkillEndCastTime { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Battle/ComboState.cs b/srcs/WingsAPI.Game/Battle/ComboState.cs new file mode 100644 index 0000000..5e17276 --- /dev/null +++ b/srcs/WingsAPI.Game/Battle/ComboState.cs @@ -0,0 +1,13 @@ +namespace WingsEmu.Game.Battle; + +public class ComboState +{ + public ComboState(long targetId) + { + Hit = 0; + TargetId = targetId; + } + + public int Hit { get; set; } + public long TargetId { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Battle/ElementType.cs b/srcs/WingsAPI.Game/Battle/ElementType.cs new file mode 100644 index 0000000..6d09fb2 --- /dev/null +++ b/srcs/WingsAPI.Game/Battle/ElementType.cs @@ -0,0 +1,11 @@ +namespace WingsEmu.Game.Battle; + +public enum ElementType +{ + Neutral = 0, + Fire = 1, + Water = 2, + Light = 3, + Shadow = 4, + All = 5 +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Battle/Event/ApplyHitEvent.cs b/srcs/WingsAPI.Game/Battle/Event/ApplyHitEvent.cs new file mode 100644 index 0000000..a21b4a0 --- /dev/null +++ b/srcs/WingsAPI.Game/Battle/Event/ApplyHitEvent.cs @@ -0,0 +1,19 @@ +using PhoenixLib.Events; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Helpers.Damages; + +namespace WingsEmu.Game.Battle; + +public class ApplyHitEvent : IAsyncEvent +{ + public ApplyHitEvent(IBattleEntity target, DamageAlgorithmResult processResults, HitInformation hitInformation) + { + Target = target; + ProcessResults = processResults; + HitInformation = hitInformation; + } + + public IBattleEntity Target { get; } + public DamageAlgorithmResult ProcessResults { get; } + public HitInformation HitInformation { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Battle/Event/EntityDamageEvent.cs b/srcs/WingsAPI.Game/Battle/Event/EntityDamageEvent.cs new file mode 100644 index 0000000..499858f --- /dev/null +++ b/srcs/WingsAPI.Game/Battle/Event/EntityDamageEvent.cs @@ -0,0 +1,13 @@ +using PhoenixLib.Events; +using WingsEmu.Game.Entities; + +namespace WingsEmu.Game.Battle; + +public class EntityDamageEvent : IAsyncEvent +{ + public IBattleEntity Damaged { get; set; } + public IBattleEntity Damager { get; set; } + public int Damage { get; set; } + public bool CanKill { get; set; } + public SkillInfo SkillInfo { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Battle/Event/ProcessBuffEvent.cs b/srcs/WingsAPI.Game/Battle/Event/ProcessBuffEvent.cs new file mode 100644 index 0000000..65ff208 --- /dev/null +++ b/srcs/WingsAPI.Game/Battle/Event/ProcessBuffEvent.cs @@ -0,0 +1,21 @@ +using PhoenixLib.Events; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Helpers.Damages; + +namespace WingsEmu.Game.Battle; + +public class ProcessBuffEvent : IAsyncEvent +{ + public ProcessBuffEvent(IBattleEntity caster, IBattleEntity target, SkillCast skillCast, Position position = default) + { + Caster = caster; + Target = target; + SkillCast = skillCast; + Position = position; + } + + public IBattleEntity Caster { get; } + public IBattleEntity Target { get; } + public SkillCast SkillCast { get; } + public Position Position { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Battle/Event/ProcessHitEvent.cs b/srcs/WingsAPI.Game/Battle/Event/ProcessHitEvent.cs new file mode 100644 index 0000000..a16dd2a --- /dev/null +++ b/srcs/WingsAPI.Game/Battle/Event/ProcessHitEvent.cs @@ -0,0 +1,23 @@ +// WingsEmu +// +// Developed by NosWings Team + +using PhoenixLib.Events; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Helpers.Damages; + +namespace WingsEmu.Game.Battle; + +public class ProcessHitEvent : IAsyncEvent +{ + public ProcessHitEvent(IBattleEntity caster, IBattleEntity target, SkillInfo skill, Position position) + { + Caster = caster; + Target = target; + HitInformation = new HitInformation(caster, skill, position); + } + + public IBattleEntity Caster { get; } + public IBattleEntity Target { get; } + public HitInformation HitInformation { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Battle/HitInformation.cs b/srcs/WingsAPI.Game/Battle/HitInformation.cs new file mode 100644 index 0000000..687393c --- /dev/null +++ b/srcs/WingsAPI.Game/Battle/HitInformation.cs @@ -0,0 +1,21 @@ +using WingsEmu.Game.Entities; +using WingsEmu.Game.Helpers.Damages; + +namespace WingsEmu.Game.Battle; + +public class HitInformation +{ + public HitInformation(IBattleEntity caster, SkillInfo skill, Position position) : this(caster, skill) => Position = position; + + public HitInformation(IBattleEntity caster, SkillInfo skill) + { + Caster = caster; + Skill = skill; + IsFirst = true; + } + + public IBattleEntity Caster { get; } + public SkillInfo Skill { get; } + public Position Position { get; } + public bool IsFirst { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Battle/HitProcessable.cs b/srcs/WingsAPI.Game/Battle/HitProcessable.cs new file mode 100644 index 0000000..77e04fc --- /dev/null +++ b/srcs/WingsAPI.Game/Battle/HitProcessable.cs @@ -0,0 +1,20 @@ +using WingsEmu.Game.Entities; +using WingsEmu.Game.Helpers.Damages; + +namespace WingsEmu.Game.Battle; + +public class HitProcessable +{ + public HitProcessable(IBattleEntity caster, IBattleEntity target, SkillCast skillCast, Position position) + { + Caster = caster; + Target = target; + SkillCast = skillCast; + Position = position; + } + + public IBattleEntity Caster { get; } + public IBattleEntity Target { get; } + public SkillCast SkillCast { get; } + public Position Position { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Battle/HitRequest.cs b/srcs/WingsAPI.Game/Battle/HitRequest.cs new file mode 100644 index 0000000..affea10 --- /dev/null +++ b/srcs/WingsAPI.Game/Battle/HitRequest.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Helpers.Damages; + +namespace WingsEmu.Game.Battle; + +public class HitRequest +{ + public HitRequest(IEnumerable<(IBattleEntity, DamageAlgorithmResult)> algorithmResult, HitInformation eHitInformation, IBattleEntity mainTarget) + { + Targets = algorithmResult; + EHitInformation = eHitInformation; + MainTarget = mainTarget; + } + + public IBattleEntity MainTarget { get; } + public IEnumerable<(IBattleEntity target, DamageAlgorithmResult result)> Targets { get; } + public HitInformation EHitInformation { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Battle/HostilityType.cs b/srcs/WingsAPI.Game/Battle/HostilityType.cs new file mode 100644 index 0000000..a080446 --- /dev/null +++ b/srcs/WingsAPI.Game/Battle/HostilityType.cs @@ -0,0 +1,17 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Game.Battle; + +public enum HostilityType +{ + NOT_HOSTILE, + ATTACK_IN_RANGE = 1, + ATTACK_MATES = 4, // pets + ATTACK_DEVILS_ONLY = 23, + ATTACK_ANGELS_ONLY = 24, + + ATTACK_NOT_WEARING_PHANTOM_AMULET = 100 + // if > 20 000, attacks the people who have HostilityType - 20000 quest +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Battle/IBattleEntityDump.cs b/srcs/WingsAPI.Game/Battle/IBattleEntityDump.cs new file mode 100644 index 0000000..b1f5b1b --- /dev/null +++ b/srcs/WingsAPI.Game/Battle/IBattleEntityDump.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using WingsAPI.Data.Families; +using WingsEmu.DTOs.BCards; +using WingsEmu.Game._enum; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Maps; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Battle; + +namespace WingsEmu.Game.Battle; + +public interface IBattleEntityDump +{ + VisualType Type { get; } + long Id { get; } + AttackType AttackType { get; } + ElementType Element { get; } + int ElementRate { get; } + + int Morale { get; } + + IReadOnlyDictionary<(BCardType type, byte subType), List> BCards { get; } + IReadOnlyDictionary<(BCardType type, byte subType), List<(int casterLevel, BCardDTO bCard)>> BuffBCards { get; } + ISet BuffsById { get; } + IReadOnlyDictionary FamilyUpgrades { get; } + + int Level { get; } + FactionType Faction { get; } + + int DamageMinimum { get; } + int DamageMaximum { get; } + + int AttackUpgrade { get; } + int DefenseUpgrade { get; } + + int HitRate { get; } + + int MaxHp { get; } + int MaxMp { get; } + + int WeaponDamageMinimum { get; } + int WeaponDamageMaximum { get; } + int CriticalChance { get; } + int CriticalDamage { get; } + int FireResistance { get; } + int WaterResistance { get; } + int LightResistance { get; } + int ShadowResistance { get; } + int MeleeDefense { get; } + int MeleeDodge { get; } + int RangeDefense { get; } + int RangeDodge { get; } + + int MagicalDefense { get; } + Position Position { get; } + IMapInstance MapInstance { get; } + + MonsterRaceType MonsterRaceType { get; } + Enum MonsterRaceSubType { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Battle/MTListHitTarget.cs b/srcs/WingsAPI.Game/Battle/MTListHitTarget.cs new file mode 100644 index 0000000..f4113ae --- /dev/null +++ b/srcs/WingsAPI.Game/Battle/MTListHitTarget.cs @@ -0,0 +1,29 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Battle; + +public class MTListHitTarget +{ + #region Instantiation + + public MTListHitTarget(VisualType entityType, long targetId) + { + EntityType = entityType; + TargetId = targetId; + } + + #endregion + + + #region Properties + + public VisualType EntityType { get; set; } + + public long TargetId { get; set; } + + #endregion +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Battle/Managers/BattleEntityDumpFactory.cs b/srcs/WingsAPI.Game/Battle/Managers/BattleEntityDumpFactory.cs new file mode 100644 index 0000000..662f2e4 --- /dev/null +++ b/srcs/WingsAPI.Game/Battle/Managers/BattleEntityDumpFactory.cs @@ -0,0 +1,20 @@ +using WingsEmu.Game.Characters; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Mates; + +namespace WingsEmu.Game.Battle; + +public class BattleEntityDumpFactory : IBattleEntityDumpFactory +{ + public IBattleEntityDump Dump(IPlayerEntity entity, SkillInfo skill, bool isDefender = false, bool isMainTarget = false) + => new PlayerBattleEntityDump(entity, skill, isDefender, isMainTarget); + + public IBattleEntityDump Dump(IMonsterEntity entity, SkillInfo skillCasted, bool isDefender = false, bool isMainTarget = false) + => new NpcMonsterEntityDump(entity, entity, skillCasted, isDefender, isMainTarget); + + public IBattleEntityDump Dump(INpcEntity npcEntity, SkillInfo skillCasted, bool isDefender = false, bool isMainTarget = false) + => new NpcMonsterEntityDump(npcEntity, npcEntity, skillCasted, isDefender, isMainTarget); + + public IBattleEntityDump Dump(IMateEntity entity, SkillInfo skillCasted, bool isDefender = false, bool isMainTarget = false) + => new MateBattleEntityDump(entity, skillCasted, isDefender, isMainTarget); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Battle/Managers/ChargeManager.cs b/srcs/WingsAPI.Game/Battle/Managers/ChargeManager.cs new file mode 100644 index 0000000..4653967 --- /dev/null +++ b/srcs/WingsAPI.Game/Battle/Managers/ChargeManager.cs @@ -0,0 +1,27 @@ +using WingsEmu.Game.Battle.Managers; + +namespace WingsEmu.Game.Battle; + +public class ChargeComponent : IChargeComponent +{ + private const int MAX_CHARGE = 7000; + + private int _charge; + + public void SetCharge(int chargeValue) + { + if (chargeValue > MAX_CHARGE) + { + chargeValue = MAX_CHARGE; + } + + _charge = chargeValue; + } + + public int GetCharge() => _charge; + + public void ResetCharge() + { + _charge = 0; + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Battle/Managers/IBattleEntityDumpFactory.cs b/srcs/WingsAPI.Game/Battle/Managers/IBattleEntityDumpFactory.cs new file mode 100644 index 0000000..767752a --- /dev/null +++ b/srcs/WingsAPI.Game/Battle/Managers/IBattleEntityDumpFactory.cs @@ -0,0 +1,13 @@ +using WingsEmu.Game.Characters; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Mates; + +namespace WingsEmu.Game.Battle; + +public interface IBattleEntityDumpFactory +{ + IBattleEntityDump Dump(IPlayerEntity entity, SkillInfo skillCasted, bool isDefender = false, bool isMainTarget = false); + IBattleEntityDump Dump(IMonsterEntity entity, SkillInfo skillCasted, bool isDefender = false, bool isMainTarget = false); + IBattleEntityDump Dump(INpcEntity npcEntity, SkillInfo skillCasted, bool isDefender = false, bool isMainTarget = false); + IBattleEntityDump Dump(IMateEntity entity, SkillInfo skillCasted, bool isDefender = false, bool isMainTarget = false); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Battle/Managers/IChargeManager.cs b/srcs/WingsAPI.Game/Battle/Managers/IChargeManager.cs new file mode 100644 index 0000000..6f8449c --- /dev/null +++ b/srcs/WingsAPI.Game/Battle/Managers/IChargeManager.cs @@ -0,0 +1,12 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Game.Battle.Managers; + +public interface IChargeComponent +{ + public void SetCharge(int chargeValue); + public int GetCharge(); + public void ResetCharge(); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Battle/Managers/IMeditationManager.cs b/srcs/WingsAPI.Game/Battle/Managers/IMeditationManager.cs new file mode 100644 index 0000000..934850d --- /dev/null +++ b/srcs/WingsAPI.Game/Battle/Managers/IMeditationManager.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using WingsEmu.Game.Entities; + +namespace WingsEmu.Game.Battle; + +public interface IMeditationManager +{ + void SaveMeditation(IBattleEntity caster, short meditationId, DateTime dateTime); + DateTime GetCastTime(IBattleEntity caster, short meditationId); + List<(short, DateTime)> GetAllMeditations(IBattleEntity caster); + void RemoveMeditation(IBattleEntity caster, short meditationId); + void RemoveAllMeditation(IBattleEntity caster); + bool HasMeditation(IBattleEntity caster); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Battle/Managers/IPhantomPositionManager.cs b/srcs/WingsAPI.Game/Battle/Managers/IPhantomPositionManager.cs new file mode 100644 index 0000000..cef662d --- /dev/null +++ b/srcs/WingsAPI.Game/Battle/Managers/IPhantomPositionManager.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Concurrent; +using WingsEmu.Game.Helpers.Damages; + +namespace WingsEmu.Game.Battle.Managers; + +public interface IPhantomPositionManager +{ + Position? GetPosition(Guid id); + void AddPosition(Guid id, Position position); +} + +public class PhantomPositionManager : IPhantomPositionManager +{ + private readonly ConcurrentDictionary _positions = new(); + + public Position? GetPosition(Guid id) + { + if (!_positions.TryRemove(id, out Position position)) + { + return null; + } + + return position; + } + + public void AddPosition(Guid id, Position position) + { + _positions.TryAdd(id, position); + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Battle/Managers/ISacrificeManager.cs b/srcs/WingsAPI.Game/Battle/Managers/ISacrificeManager.cs new file mode 100644 index 0000000..79e43d4 --- /dev/null +++ b/srcs/WingsAPI.Game/Battle/Managers/ISacrificeManager.cs @@ -0,0 +1,11 @@ +using WingsEmu.Game.Entities; + +namespace WingsEmu.Game.Battle; + +public interface ISacrificeManager +{ + void SaveSacrifice(IBattleEntity caster, IBattleEntity target); + IBattleEntity GetTarget(IBattleEntity caster); + IBattleEntity GetCaster(IBattleEntity target); + void RemoveSacrifice(IBattleEntity caster, IBattleEntity target); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Battle/Managers/ISkillExecutor.cs b/srcs/WingsAPI.Game/Battle/Managers/ISkillExecutor.cs new file mode 100644 index 0000000..791528e --- /dev/null +++ b/srcs/WingsAPI.Game/Battle/Managers/ISkillExecutor.cs @@ -0,0 +1,14 @@ +using WingsEmu.Game.Entities; +using WingsEmu.Game.Helpers.Damages; + +namespace WingsEmu.Game.Battle; + +public interface ISkillExecutor +{ + void ExecuteDamageZoneHitSkill(IBattleEntity caster, SkillCast skill, Position position); + void ExecuteBuffZoneHitSkill(IBattleEntity caster, SkillCast skill, Position position); + void ExecuteDebuffZoneHitSkill(IBattleEntity caster, SkillCast skill, Position position); + void ExecuteDamageSkill(IBattleEntity caster, IBattleEntity target, SkillCast skill, Position positionBeforeDash = default); + void ExecuteBuffSkill(IBattleEntity caster, IBattleEntity target, SkillCast skill); + void ExecuteDebuffSkill(IBattleEntity caster, IBattleEntity target, SkillCast skill); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Battle/Managers/ISkillUsageManager.cs b/srcs/WingsAPI.Game/Battle/Managers/ISkillUsageManager.cs new file mode 100644 index 0000000..e222e31 --- /dev/null +++ b/srcs/WingsAPI.Game/Battle/Managers/ISkillUsageManager.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Battle; + +public interface ISkillUsageManager +{ + List<(VisualType, long)> GetMultiTargets(long id); + void SetMultiTargets(long id, List<(VisualType, long)> targets); + void ResetMultiTargets(long id); + + ComboState GetComboState(long casterId, long targetId); + void ResetComboState(long id); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Battle/Managers/ITeleportManager.cs b/srcs/WingsAPI.Game/Battle/Managers/ITeleportManager.cs new file mode 100644 index 0000000..eb810cc --- /dev/null +++ b/srcs/WingsAPI.Game/Battle/Managers/ITeleportManager.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using WingsEmu.DTOs.ServerDatas; +using WingsEmu.Game.Helpers.Damages; + +namespace WingsEmu.Game.Battle; + +public interface ITeleportManager +{ + void SavePosition(long id, Position position); + Position GetPosition(long id); + void RemovePosition(long id); +} + +public interface ITeleporterManager +{ + Task InitializeAsync(); + IReadOnlyList GetTeleportByNpcId(long npcId); + IReadOnlyList GetTeleportByMapId(int mapId); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Battle/Managers/MeditationManager.cs b/srcs/WingsAPI.Game/Battle/Managers/MeditationManager.cs new file mode 100644 index 0000000..e753be1 --- /dev/null +++ b/srcs/WingsAPI.Game/Battle/Managers/MeditationManager.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using WingsEmu.Game.Entities; + +namespace WingsEmu.Game.Battle; + +public class MeditationManager : IMeditationManager +{ + private readonly Dictionary> _savedMeditationCaster = new(); + + + public void SaveMeditation(IBattleEntity caster, short meditationId, DateTime dateTime) + { + if (_savedMeditationCaster.ContainsKey(caster.Id)) + { + _savedMeditationCaster[caster.Id].Add((meditationId, dateTime)); + } + else + { + _savedMeditationCaster.Add(caster.Id, new List<(short, DateTime)> { (meditationId, dateTime) }); + } + } + + public DateTime GetCastTime(IBattleEntity caster, short meditationId) => _savedMeditationCaster[caster.Id].FirstOrDefault(s => s.Item1 == meditationId).Item2; + + public List<(short, DateTime)> GetAllMeditations(IBattleEntity caster) => _savedMeditationCaster[caster.Id]; + + public void RemoveAllMeditation(IBattleEntity caster) + { + _savedMeditationCaster.Remove(caster.Id); + } + + public void RemoveMeditation(IBattleEntity caster, short meditationId) + { + foreach ((short, DateTime) s in _savedMeditationCaster[caster.Id].ToList()) + { + if (s.Item1 == meditationId) + { + _savedMeditationCaster[caster.Id].Remove(s); + } + } + } + + public bool HasMeditation(IBattleEntity caster) => _savedMeditationCaster.ContainsKey(caster.Id); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Battle/Managers/SkillExecutor.cs b/srcs/WingsAPI.Game/Battle/Managers/SkillExecutor.cs new file mode 100644 index 0000000..c68b768 --- /dev/null +++ b/srcs/WingsAPI.Game/Battle/Managers/SkillExecutor.cs @@ -0,0 +1,126 @@ +using System; +using System.Linq; +using WingsEmu.DTOs.BCards; +using WingsEmu.Game._enum; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Skills; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.ServerPackets.Battle; + +namespace WingsEmu.Game.Battle; + +public class SkillExecutor : ISkillExecutor +{ + private readonly IBCardEffectHandlerContainer _bCardEffectHandlerContainer; + + public SkillExecutor(IBCardEffectHandlerContainer bCardEffectHandlerContainer) => _bCardEffectHandlerContainer = bCardEffectHandlerContainer; + + public void ExecuteDamageSkill(IBattleEntity caster, IBattleEntity target, SkillCast skill, Position positionBeforeDash = default) + { + if (IsDestroyerBomb(caster, skill)) + { + return; + } + + caster.BroadcastCastPacket(target, skill.Skill); + var hitProcessable = new HitProcessable(caster, target, skill, positionBeforeDash); + caster.MapInstance.AddCastHitRequest(hitProcessable); + } + + + public void ExecuteDamageZoneHitSkill(IBattleEntity caster, SkillCast skill, Position position) + { + caster.BroadcastCastNonTarget(skill.Skill); + var hitProcessable = new HitProcessable(caster, null, skill, position); + caster.MapInstance.AddCastHitRequest(hitProcessable); + } + + public void ExecuteBuffZoneHitSkill(IBattleEntity caster, SkillCast skill, Position position) + { + caster.BroadcastCastNonTarget(skill.Skill); + var buffProcessable = new BuffProcessable(caster, null, skill, position); + caster.MapInstance.AddCastBuffRequest(buffProcessable); + } + + public void ExecuteDebuffZoneHitSkill(IBattleEntity caster, SkillCast skill, Position position) + { + caster.BroadcastCastNonTarget(skill.Skill); + var buffProcessable = new BuffProcessable(caster, null, skill, position); + caster.MapInstance.AddCastBuffRequest(buffProcessable); + } + + public void ExecuteBuffSkill(IBattleEntity caster, IBattleEntity target, SkillCast skill) + { + SkillInfo skillInfo = skill.Skill; + bool isTeleport = skillInfo.Vnum == (short)SkillsVnums.ARCH_TELEPORT; + + caster.BroadcastCastPacket(target, skillInfo); + + if (isTeleport) + { + caster.BroadcastSuPacket(caster, skillInfo, 0, SuPacketHitMode.NoDamageSuccess); + foreach (BCardDTO x in skillInfo.BCards) + { + _bCardEffectHandlerContainer.Execute(caster, caster, x, skillInfo); + } + + return; + } + + caster.MapInstance.AddCastBuffRequest(new BuffProcessable(caster, target, skill)); + } + + public void ExecuteDebuffSkill(IBattleEntity caster, IBattleEntity target, SkillCast skill) + { + caster.BroadcastCastPacket(target, skill.Skill); + caster.MapInstance.AddCastBuffRequest(new BuffProcessable(caster, target, skill)); + } + + private bool IsDestroyerBomb(IBattleEntity caster, SkillCast skill) + { + if (skill.Skill.Vnum != (short)SkillsVnums.BOMB) + { + return false; + } + + if (caster is not IPlayerEntity character) + { + return false; + } + + if (!character.SkillComponent.BombEntityId.HasValue) + { + return false; + } + + long characterId = character.Id; + IMonsterEntity bomb = character.MapInstance.GetAliveMonsters(x => x.Id == character.SkillComponent.BombEntityId + && x.SummonerId == characterId && x.SummonerType == VisualType.Player && x.MonsterVNum == (short)MonsterVnum.BOMB).FirstOrDefault(); + if (bomb == null) + { + character.SkillComponent.BombEntityId = null; + return false; + } + + IBattleEntitySkill bombSkill = bomb.Skills.FirstOrDefault(); + if (bombSkill == null) + { + character.SkillComponent.BombEntityId = null; + return false; + } + + character.CancelCastingSkill(); + SkillInfo fakeBombSkill = character.GetFakeBombSkill(); + fakeBombSkill.Cooldown = (short)(skill.Skill.Cooldown == 0 ? 0 : fakeBombSkill.Cooldown); + character.Session.SendSkillCooldownResetAfter(fakeBombSkill.CastId, fakeBombSkill.Cooldown); + + bomb.EmitEvent(new BattleExecuteSkillEvent(bomb, bomb, bombSkill.Skill.GetInfo(), DateTime.UtcNow)); + character.SetSkillCooldown(fakeBombSkill); + character.SkillComponent.BombEntityId = null; + return true; + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Battle/Managers/SkillUsageManager.cs b/srcs/WingsAPI.Game/Battle/Managers/SkillUsageManager.cs new file mode 100644 index 0000000..8ec0ca7 --- /dev/null +++ b/srcs/WingsAPI.Game/Battle/Managers/SkillUsageManager.cs @@ -0,0 +1,43 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; +using WingsEmu.Core.Extensions; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Battle; + +public class SkillUsageManager : ISkillUsageManager +{ + private readonly ConcurrentDictionary _combos = new(); + private readonly ConcurrentDictionary> _multiTargets = new(); + + public List<(VisualType, long)> GetMultiTargets(long id) => _multiTargets.GetOrDefault(id, new List<(VisualType, long)>()); + + public void SetMultiTargets(long id, List<(VisualType, long)> targets) + { + _multiTargets[id] = targets; + } + + public void ResetMultiTargets(long id) + { + _multiTargets.TryRemove(id, out List<(VisualType, long)> targets); + } + + public ComboState GetComboState(long casterId, long targetId) + { + ComboState tmp = _combos.GetOrAdd(casterId, new ComboState(targetId)); + if (tmp.TargetId == targetId) + { + return tmp; + } + + tmp = new ComboState(targetId); + _combos[casterId] = tmp; + + return tmp; + } + + public void ResetComboState(long id) + { + _combos.TryRemove(id, out ComboState state); + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Battle/Managers/StaticSkillExecutor.cs b/srcs/WingsAPI.Game/Battle/Managers/StaticSkillExecutor.cs new file mode 100644 index 0000000..5f363d2 --- /dev/null +++ b/srcs/WingsAPI.Game/Battle/Managers/StaticSkillExecutor.cs @@ -0,0 +1,16 @@ +namespace WingsEmu.Game.Battle; + +public static class StaticSkillExecutor +{ + public static ISkillExecutor Instance { get; private set; } + + public static void Initialize(ISkillExecutor skillExecutor) + { + if (Instance != null) + { + return; + } + + Instance = skillExecutor; + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Battle/Managers/TeleportManager.cs b/srcs/WingsAPI.Game/Battle/Managers/TeleportManager.cs new file mode 100644 index 0000000..f6cdd7c --- /dev/null +++ b/srcs/WingsAPI.Game/Battle/Managers/TeleportManager.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using WingsEmu.Core.Extensions; +using WingsEmu.Game.Helpers.Damages; + +namespace WingsEmu.Game.Battle; + +public class TeleportManager : ITeleportManager +{ + private readonly Dictionary _savedPosition = new(); + + public void SavePosition(long id, Position position) => _savedPosition[id] = position; + public Position GetPosition(long id) => _savedPosition.GetOrDefault(id); + + public void RemovePosition(long id) + { + if (!_savedPosition.ContainsKey(id)) + { + return; + } + + _savedPosition.Remove(id); + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Battle/MateBattleEntityDump.cs b/srcs/WingsAPI.Game/Battle/MateBattleEntityDump.cs new file mode 100644 index 0000000..53848d0 --- /dev/null +++ b/srcs/WingsAPI.Game/Battle/MateBattleEntityDump.cs @@ -0,0 +1,209 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using WingsAPI.Data.Families; +using WingsEmu.DTOs.BCards; +using WingsEmu.Game._enum; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Items; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Mates; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Battle; + +namespace WingsEmu.Game.Battle; + +public class MateBattleEntityDump : IBattleEntityDump +{ + public MateBattleEntityDump(IMateEntity mateEntity, SkillInfo skill, bool isDefender, bool isMainTarget) + { + GameItemInstance mateWeapon = mateEntity.Weapon; + var bCards = new ConcurrentDictionary<(BCardType type, byte subType), List>(); + if (skill != null) + { + if (isDefender) + { + if (skill.BCardsType.TryGetValue(SkillCastType.BEFORE_ATTACK_ALL_TARGETS, out HashSet hashSetBCards)) + { + foreach (BCardDTO bCard in hashSetBCards) + { + if (!bCards.TryGetValue(((BCardType)bCard.Type, bCard.SubType), out List list)) + { + list = new List(); + bCards[((BCardType)bCard.Type, bCard.SubType)] = list; + } + + list.Add(bCard); + } + } + } + else + { + if (skill.BCardsType.TryGetValue(SkillCastType.BEFORE_ATTACK_SELF, out HashSet hashSetBCards)) + { + foreach (BCardDTO bCard in hashSetBCards) + { + if (!bCards.TryGetValue(((BCardType)bCard.Type, bCard.SubType), out List list)) + { + list = new List(); + bCards[((BCardType)bCard.Type, bCard.SubType)] = list; + } + + list.Add(bCard); + } + } + } + + if (isMainTarget) + { + if (skill.BCardsType.TryGetValue(SkillCastType.BEFORE_ATTACK_ON_MAIN_TARGET, out HashSet hashSetBCards)) + { + foreach (BCardDTO bCard in hashSetBCards) + { + if (!bCards.TryGetValue(((BCardType)bCard.Type, bCard.SubType), out List list)) + { + list = new List(); + bCards[((BCardType)bCard.Type, bCard.SubType)] = list; + } + + list.Add(bCard); + } + } + } + } + + foreach (BCardDTO bCard in mateEntity.BCardComponent.GetAllBCards()) + { + if (!bCards.TryGetValue(((BCardType)bCard.Type, bCard.SubType), out List list)) + { + list = new List(); + bCards[((BCardType)bCard.Type, bCard.SubType)] = list; + } + + list.Add(bCard); + } + + foreach (BCardDTO bCard in mateEntity.BCardComponent.GetTriggerBCards(BCardTriggerType.ATTACK)) + { + if (!bCards.TryGetValue(((BCardType)bCard.Type, bCard.SubType), out List list)) + { + list = new List(); + bCards[((BCardType)bCard.Type, bCard.SubType)] = list; + } + + list.Add(bCard); + } + + foreach (BCardDTO bCard in mateEntity.BCardComponent.GetTriggerBCards(BCardTriggerType.DEFENSE)) + { + if (!bCards.TryGetValue(((BCardType)bCard.Type, bCard.SubType), out List list)) + { + list = new List(); + bCards[((BCardType)bCard.Type, bCard.SubType)] = list; + } + + list.Add(bCard); + } + + ISet buffsById = mateEntity.BuffComponent.GetAllBuffsId().ToHashSet(); + IReadOnlyList<(int casterLevel, BCardDTO bCard)> buffBCards = mateEntity.BCardComponent.GetBuffBCards(); + var buffs = new ConcurrentDictionary<(BCardType type, byte subType), List<(int casterLevel, BCardDTO bCard)>>(); + foreach ((int casterLevel, BCardDTO bCard) in buffBCards) + { + (BCardType, byte SubType) key = ((BCardType)bCard.Type, bCard.SubType); + if (!buffs.TryGetValue(key, out List<(int casterLevel, BCardDTO bCard)> list)) + { + list = new List<(int casterLevel, BCardDTO bCard)>(); + buffs[key] = list; + } + + list.Add((casterLevel, bCard)); + } + + BuffsById = buffsById; + BCards = bCards; + BuffBCards = buffs; + Morale = mateEntity.Level; + Id = mateEntity.Id; + MapInstance = mateEntity.MapInstance; + Level = mateEntity.Level; + Type = VisualType.Npc; + Faction = mateEntity.Faction; + + Position = new Position(mateEntity.PositionX, mateEntity.PositionY); + MapInstance = mateEntity.MapInstance; + + MaxHp = mateEntity.MaxHp; + MaxMp = mateEntity.MaxMp; + + HitRate = mateEntity.HitRate; + CriticalChance = mateEntity.HitCriticalChance; + CriticalDamage = mateEntity.HitCriticalDamage; + AttackUpgrade = mateEntity.Attack; + FireResistance = mateEntity.FireResistance; + WaterResistance = mateEntity.WaterResistance; + LightResistance = mateEntity.LightResistance; + ShadowResistance = mateEntity.DarkResistance; + AttackType = mateEntity.AttackType; + + DefenseUpgrade = mateEntity.Defence; + MeleeDefense = mateEntity.CloseDefence; + RangeDefense = mateEntity.DistanceDefence; + MagicalDefense = mateEntity.MagicDefence; + MeleeDodge = mateEntity.DefenceDodge; + RangeDodge = mateEntity.DistanceDodge; + + WeaponDamageMinimum = mateWeapon?.DamageMinimum + mateWeapon?.GameItem.DamageMinimum ?? 0; + WeaponDamageMaximum = mateWeapon?.DamageMaximum + mateWeapon?.GameItem.DamageMaximum ?? 0; + DamageMinimum = mateEntity.DamagesMinimum - WeaponDamageMinimum; + DamageMaximum = mateEntity.DamagesMaximum - WeaponDamageMaximum; + + Element = (ElementType)mateEntity.Element; + ElementRate = mateEntity.ElementRate; + + MonsterRaceType = mateEntity.MonsterRaceType; + MonsterRaceSubType = mateEntity.GetMonsterRaceSubType(); + FamilyUpgrades = mateEntity.Owner.Family?.UpgradeValues ?? new Dictionary(); + } + + public VisualType Type { get; } + public long Id { get; } + public IMapInstance MapInstance { get; } + public MonsterRaceType MonsterRaceType { get; } + public Enum MonsterRaceSubType { get; } + public AttackType AttackType { get; } + public ElementType Element { get; } + public int ElementRate { get; } + public int Morale { get; } + + public IReadOnlyDictionary<(BCardType type, byte subType), List<(int casterLevel, BCardDTO bCard)>> BuffBCards { get; } + public IReadOnlyDictionary<(BCardType type, byte subType), List> BCards { get; } + public ISet BuffsById { get; } + public IReadOnlyDictionary FamilyUpgrades { get; } + public int Level { get; } + public FactionType Faction { get; } + public int DamageMinimum { get; } + public int DamageMaximum { get; } + public int AttackUpgrade { get; } + public int DefenseUpgrade { get; } + public int HitRate { get; } + public int MaxHp { get; } + public int MaxMp { get; } + public int WeaponDamageMinimum { get; } + public int WeaponDamageMaximum { get; } + public int CriticalChance { get; } + public int CriticalDamage { get; } + public int FireResistance { get; } + public int WaterResistance { get; } + public int LightResistance { get; } + public int ShadowResistance { get; } + public int MeleeDefense { get; } + public int MeleeDodge { get; } + public int RangeDefense { get; } + public int RangeDodge { get; } + public int MagicalDefense { get; } + public Position Position { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Battle/NpcMonsterEntityDump.cs b/srcs/WingsAPI.Game/Battle/NpcMonsterEntityDump.cs new file mode 100644 index 0000000..7d1bd00 --- /dev/null +++ b/srcs/WingsAPI.Game/Battle/NpcMonsterEntityDump.cs @@ -0,0 +1,256 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using WingsAPI.Data.Families; +using WingsEmu.DTOs.BCards; +using WingsEmu.Game._enum; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Items; +using WingsEmu.Game.Maps; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Battle; + +namespace WingsEmu.Game.Battle; + +public class NpcMonsterEntityDump : IBattleEntityDump +{ + public NpcMonsterEntityDump(IMonsterData npcMonster, IBattleEntity battleEntity, SkillInfo skill, bool isDefender, bool isMainTarget) + { + Id = battleEntity.Id; + Type = battleEntity.Type; + Morale = battleEntity.Level; + Level = npcMonster.BaseLevel; + MaxHp = battleEntity.MaxHp; + MaxMp = battleEntity.MaxMp; + var bCards = new Dictionary<(BCardType type, byte subType), List>(); + if (skill != null) + { + if (isDefender) + { + if (skill.BCardsType.TryGetValue(SkillCastType.BEFORE_ATTACK_ALL_TARGETS, out HashSet hashSetBCards)) + { + foreach (BCardDTO bCard in hashSetBCards) + { + if (!bCards.TryGetValue(((BCardType)bCard.Type, bCard.SubType), out List list)) + { + list = new List(); + bCards[((BCardType)bCard.Type, bCard.SubType)] = list; + } + + list.Add(bCard); + } + } + } + else + { + if (skill.BCardsType.TryGetValue(SkillCastType.BEFORE_ATTACK_SELF, out HashSet hashSetBCards)) + { + foreach (BCardDTO bCard in hashSetBCards) + { + if (!bCards.TryGetValue(((BCardType)bCard.Type, bCard.SubType), out List list)) + { + list = new List(); + bCards[((BCardType)bCard.Type, bCard.SubType)] = list; + } + + list.Add(bCard); + } + } + } + + if (isMainTarget) + { + if (skill.BCardsType.TryGetValue(SkillCastType.BEFORE_ATTACK_ON_MAIN_TARGET, out HashSet hashSetBCards)) + { + foreach (BCardDTO bCard in hashSetBCards) + { + if (!bCards.TryGetValue(((BCardType)bCard.Type, bCard.SubType), out List list)) + { + list = new List(); + bCards[((BCardType)bCard.Type, bCard.SubType)] = list; + } + + list.Add(bCard); + } + } + } + } + + foreach (BCardDTO bCard in battleEntity.BCardComponent.GetAllBCards()) + { + if (!bCards.TryGetValue(((BCardType)bCard.Type, bCard.SubType), out List list)) + { + list = new List(); + bCards[((BCardType)bCard.Type, bCard.SubType)] = list; + } + + list.Add(bCard); + } + + foreach (BCardDTO bCard in battleEntity.BCardComponent.GetTriggerBCards(BCardTriggerType.ATTACK)) + { + if (!bCards.TryGetValue(((BCardType)bCard.Type, bCard.SubType), out List list)) + { + list = new List(); + bCards[((BCardType)bCard.Type, bCard.SubType)] = list; + } + + list.Add(bCard); + } + + foreach (BCardDTO bCard in battleEntity.BCardComponent.GetTriggerBCards(BCardTriggerType.DEFENSE)) + { + if (!bCards.TryGetValue(((BCardType)bCard.Type, bCard.SubType), out List list)) + { + list = new List(); + bCards[((BCardType)bCard.Type, bCard.SubType)] = list; + } + + list.Add(bCard); + } + + ISet buffsById = battleEntity.BuffComponent.GetAllBuffsId().ToHashSet(); + IReadOnlyList<(int casterLevel, BCardDTO bCard)> buffBCards = battleEntity.BCardComponent.GetBuffBCards(); + var buffs = new Dictionary<(BCardType type, byte subType), List<(int casterLevel, BCardDTO bCard)>>(); + foreach ((int casterLevel, BCardDTO bCard) in buffBCards) + { + (BCardType, byte SubType) key = ((BCardType)bCard.Type, bCard.SubType); + if (!buffs.TryGetValue(key, out List<(int casterLevel, BCardDTO bCard)> list)) + { + list = new List<(int casterLevel, BCardDTO bCard)>(); + buffs[key] = list; + } + + list.Add((casterLevel, bCard)); + } + + BuffsById = buffsById; + BCards = bCards; + BuffBCards = buffs; + DamageMinimum = npcMonster.BaseDamageMinimum; + DamageMaximum = npcMonster.BaseDamageMaximum; + WeaponDamageMinimum = 0; + WeaponDamageMaximum = 0; + HitRate = npcMonster.BaseConcentrate; + CriticalChance = npcMonster.BaseCriticalChance; + CriticalDamage = npcMonster.BaseCriticalRate; + AttackType = npcMonster.AttackType; + AttackUpgrade = npcMonster.AttackUpgrade; + FireResistance = npcMonster.BaseFireResistance; + WaterResistance = npcMonster.BaseWaterResistance; + LightResistance = npcMonster.BaseLightResistance; + ShadowResistance = npcMonster.BaseDarkResistance; + AttackType = npcMonster.AttackType; + DefenseUpgrade = npcMonster.DefenceUpgrade; + MeleeDefense = npcMonster.BaseCloseDefence; + MeleeDodge = npcMonster.DefenceDodge; + RangeDefense = npcMonster.DistanceDefence; + RangeDodge = npcMonster.DistanceDefenceDodge; + MagicalDefense = npcMonster.MagicDefence; + Element = (ElementType)npcMonster.BaseElement; + ElementRate = npcMonster.BaseElementRate; + Position = new Position(battleEntity.PositionX, battleEntity.PositionY); + MapInstance = battleEntity.MapInstance; + Faction = battleEntity.Faction; + IsVesselMonster = battleEntity is IMonsterEntity { VesselMonster: true }; + MonsterRaceType = npcMonster.MonsterRaceType; + MonsterRaceSubType = npcMonster.GetMonsterRaceSubType(); + FamilyAttackAndDefense = 0; + FamilyUpgrades = new Dictionary(); + + if (!(battleEntity is IMonsterEntity monsterEntity)) + { + return; + } + + if (skill == null) + { + return; + } + + if (!monsterEntity.SummonerId.HasValue) + { + return; + } + + if (!monsterEntity.SummonerType.HasValue || monsterEntity.SummonerType.Value != VisualType.Player) + { + return; + } + + if (monsterEntity.MonsterVNum == (int)MonsterVnum.MINI_JAJAMARU) + { + return; + } + + IBattleEntity summoner = monsterEntity.MapInstance.GetBattleEntity(monsterEntity.SummonerType.Value, monsterEntity.SummonerId.Value); + if (summoner == null) + { + return; + } + + if (!(summoner is IPlayerEntity character)) + { + return; + } + + GameItemInstance weapon = character.MainWeapon; + if (weapon != null) + { + AttackUpgrade = weapon.Upgrade; + WeaponDamageMinimum = weapon.DamageMinimum + weapon.GameItem.DamageMinimum; + WeaponDamageMaximum = weapon.DamageMaximum + weapon.GameItem.DamageMaximum; + } + + DamageMinimum = character.DamagesMinimum + WeaponDamageMinimum; + DamageMaximum = character.DamagesMaximum + WeaponDamageMaximum; + HitRate = character.HitRate; + Morale = character.Level; + CriticalChance = character.HitCriticalChance; + CriticalDamage = character.HitCriticalDamage; + } + + public int FamilyAttackAndDefense { get; } + public bool IsVesselMonster { get; } + + public VisualType Type { get; } + public long Id { get; } + public IMapInstance MapInstance { get; } + public MonsterRaceType MonsterRaceType { get; } + public Enum MonsterRaceSubType { get; } + public AttackType AttackType { get; } + public ElementType Element { get; } + public int ElementRate { get; } + public int Morale { get; } + public IReadOnlyDictionary<(BCardType type, byte subType), List> BCards { get; } + public IReadOnlyDictionary<(BCardType type, byte subType), List<(int casterLevel, BCardDTO bCard)>> BuffBCards { get; } + public ISet BuffsById { get; } + public IReadOnlyDictionary FamilyUpgrades { get; } + public int Level { get; } + public FactionType Faction { get; } + public int DamageMinimum { get; } + public int DamageMaximum { get; } + public int AttackUpgrade { get; } + public int DefenseUpgrade { get; } + public int HitRate { get; } + public int MaxHp { get; } + public int MaxMp { get; } + public int WeaponDamageMinimum { get; } + public int WeaponDamageMaximum { get; } + public int CriticalChance { get; } + public int CriticalDamage { get; } + public int FireResistance { get; } + public int WaterResistance { get; } + public int LightResistance { get; } + public int ShadowResistance { get; } + public int MeleeDefense { get; } + public int MeleeDodge { get; } + public int RangeDefense { get; } + public int RangeDodge { get; } + public int MagicalDefense { get; } + public Position Position { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Battle/PlayerBattleEntityDump.cs b/srcs/WingsAPI.Game/Battle/PlayerBattleEntityDump.cs new file mode 100644 index 0000000..3439037 --- /dev/null +++ b/srcs/WingsAPI.Game/Battle/PlayerBattleEntityDump.cs @@ -0,0 +1,372 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using WingsAPI.Data.Families; +using WingsAPI.Packets.Enums.Shells; +using WingsEmu.DTOs.BCards; +using WingsEmu.DTOs.Items; +using WingsEmu.Game._enum; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Items; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Monster; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Battle; +using WingsEmu.Packets.Enums.Character; + +namespace WingsEmu.Game.Battle; + +public class PlayerBattleEntityDump : IBattleEntityDump +{ + public PlayerBattleEntityDump(IPlayerEntity playerEntity, SkillInfo skill, bool isDefender, bool isMainTarget) + { + bool useSecondWeapon = false; + GameItemInstance weapon = null; + var bCardDictionary = new Dictionary<(BCardType type, byte subType), List>(); + + if (skill != null) // get all info from attacker calculation + { + if (isDefender) + { + if (skill.BCardsType.TryGetValue(SkillCastType.BEFORE_ATTACK_ALL_TARGETS, out HashSet hashSetBCards)) + { + foreach (BCardDTO bCard in hashSetBCards) + { + if (!bCardDictionary.TryGetValue(((BCardType)bCard.Type, bCard.SubType), out List list)) + { + list = new List(); + bCardDictionary[((BCardType)bCard.Type, bCard.SubType)] = list; + } + + list.Add(bCard); + } + } + } + else + { + if (skill.BCardsType.TryGetValue(SkillCastType.BEFORE_ATTACK_SELF, out HashSet hashSetBCards)) + { + foreach (BCardDTO bCard in hashSetBCards) + { + if (!bCardDictionary.TryGetValue(((BCardType)bCard.Type, bCard.SubType), out List list)) + { + list = new List(); + bCardDictionary[((BCardType)bCard.Type, bCard.SubType)] = list; + } + + list.Add(bCard); + } + } + + HashSet chargeBCardToRemove = new(); + foreach (BCardDTO bCard in playerEntity.BCardComponent.GetChargeBCards()) + { + if (!bCardDictionary.TryGetValue(((BCardType)bCard.Type, bCard.SubType), out List list)) + { + list = new List(); + bCardDictionary[((BCardType)bCard.Type, bCard.SubType)] = list; + } + + list.Add(bCard); + chargeBCardToRemove.Add(bCard); + } + + foreach (BCardDTO bCard in chargeBCardToRemove) + { + playerEntity.BCardComponent.RemoveChargeBCard(bCard); + } + } + + if (isMainTarget) + { + if (skill.BCardsType.TryGetValue(SkillCastType.BEFORE_ATTACK_ON_MAIN_TARGET, out HashSet hashSetBCards)) + { + foreach (BCardDTO bCard in hashSetBCards) + { + if (!bCardDictionary.TryGetValue(((BCardType)bCard.Type, bCard.SubType), out List list)) + { + list = new List(); + bCardDictionary[((BCardType)bCard.Type, bCard.SubType)] = list; + } + + list.Add(bCard); + } + } + } + + switch (skill.AttackType) + { + case AttackType.Melee: + AttackType = AttackType.Melee; + switch (playerEntity.Class) + { + case ClassType.Archer: + DamageMinimum = playerEntity.SecondDamageMinimum; + DamageMaximum = playerEntity.SecondDamageMaximum; + HitRate = playerEntity.SecondHitRate; + CriticalChance = playerEntity.SecondHitCriticalChance; + CriticalDamage = playerEntity.SecondHitCriticalDamage; + useSecondWeapon = true; + { + GameItemInstance inventoryItem = playerEntity.SecondaryWeapon; + if (inventoryItem != null) + { + weapon = inventoryItem; + } + } + + break; + default: + { + GameItemInstance inventoryItem = playerEntity.MainWeapon; + if (inventoryItem != null) + { + weapon = inventoryItem; + } + } + break; + } + + break; + + case AttackType.Ranged: + AttackType = AttackType.Ranged; + switch (playerEntity.Class) + { + case ClassType.Adventurer: + case ClassType.Swordman: + case ClassType.Magician: + case ClassType.Wrestler: + DamageMinimum = playerEntity.SecondDamageMinimum; + DamageMaximum = playerEntity.SecondDamageMaximum; + HitRate = playerEntity.SecondHitRate; + CriticalChance = playerEntity.SecondHitCriticalChance; + CriticalDamage = playerEntity.SecondHitCriticalDamage; + weapon = playerEntity.SecondaryWeapon; + useSecondWeapon = true; + break; + case ClassType.Archer: + weapon = playerEntity.MainWeapon; + break; + } + + break; + + case AttackType.Magical: + AttackType = AttackType.Magical; + weapon = playerEntity.MainWeapon; + break; + + case AttackType.Other: + weapon = playerEntity.MainWeapon; + AttackType = playerEntity.Class switch + { + ClassType.Wrestler => AttackType.Melee, + ClassType.Adventurer => AttackType.Melee, + ClassType.Swordman => AttackType.Melee, + ClassType.Archer => AttackType.Ranged, + ClassType.Magician => AttackType.Magical + }; + + break; + + case AttackType.Dash: + AttackType = AttackType.Melee; + weapon = playerEntity.Class switch + { + ClassType.Adventurer => playerEntity.MainWeapon, + ClassType.Swordman => playerEntity.MainWeapon, + ClassType.Archer => playerEntity.SecondaryWeapon, + ClassType.Magician => playerEntity.MainWeapon, + ClassType.Wrestler => playerEntity.MainWeapon + }; + + if (playerEntity.Class == ClassType.Archer) + { + DamageMinimum = playerEntity.SecondDamageMinimum; + DamageMaximum = playerEntity.SecondDamageMaximum; + HitRate = playerEntity.SecondHitRate; + CriticalChance = playerEntity.SecondHitCriticalChance; + CriticalDamage = playerEntity.SecondHitCriticalDamage; + useSecondWeapon = true; + } + + break; + } + } + else + { + weapon = playerEntity.SecondaryWeapon; + useSecondWeapon = true; + AttackType = playerEntity.Class switch + { + ClassType.Adventurer => AttackType.Melee, + ClassType.Swordman => AttackType.Melee, + ClassType.Wrestler => AttackType.Melee, + ClassType.Archer => AttackType.Ranged, + ClassType.Magician => AttackType.Magical + }; + } + + if (weapon != null) + { + AttackUpgrade = weapon.Upgrade; + WeaponDamageMinimum = weapon.DamageMinimum + weapon.GameItem.DamageMinimum; + WeaponDamageMaximum = weapon.DamageMaximum + weapon.GameItem.DamageMaximum; + } + + GameItemInstance armor = playerEntity.Armor; + if (armor != null) + { + DefenseUpgrade = armor.Upgrade; + } + + IReadOnlyList bCards = playerEntity.BCardComponent.GetAllBCards(); + foreach (BCardDTO bCard in bCards) + { + if (!bCardDictionary.TryGetValue(((BCardType)bCard.Type, bCard.SubType), out List list)) + { + list = new List(); + bCardDictionary[((BCardType)bCard.Type, bCard.SubType)] = list; + } + + list.Add(bCard); + } + + ISet buffsById = playerEntity.BuffComponent.GetAllBuffsId().ToHashSet(); + IReadOnlyList<(int casterLevel, BCardDTO bCard)> buffBCards = playerEntity.BCardComponent.GetBuffBCards(); + var buffs = new Dictionary<(BCardType type, byte subType), List<(int casterLevel, BCardDTO bCard)>>(); + foreach ((int casterLevel, BCardDTO bCard) in buffBCards) + { + (BCardType, byte SubType) key = ((BCardType)bCard.Type, bCard.SubType); + if (!buffs.TryGetValue(key, out List<(int casterLevel, BCardDTO bCard)> list)) + { + list = new List<(int casterLevel, BCardDTO bCard)>(); + buffs[key] = list; + } + + list.Add((casterLevel, bCard)); + } + + short element = playerEntity.Fairy?.GameItem.Element == (short)ElementType.All ? skill.Element : playerEntity.Element; + + BuffBCards = buffs; + MaxHp = playerEntity.MaxHp; + MaxMp = playerEntity.MaxMp; + BuffsById = buffsById; + Type = VisualType.Player; + Id = playerEntity.Id; + Morale = playerEntity.Level; + Level = playerEntity.Level; + Faction = playerEntity.Faction; + DamageMinimum = !useSecondWeapon ? playerEntity.DamagesMinimum - WeaponDamageMinimum : DamageMinimum - WeaponDamageMinimum; + DamageMaximum = !useSecondWeapon ? playerEntity.DamagesMaximum - WeaponDamageMaximum : DamageMaximum - WeaponDamageMaximum; + CriticalChance = !useSecondWeapon ? playerEntity.HitCriticalChance : CriticalChance; + CriticalDamage = !useSecondWeapon ? playerEntity.HitCriticalDamage : CriticalDamage; + ShadowResistance = playerEntity.DarkResistance; + LightResistance = playerEntity.LightResistance; + WaterResistance = playerEntity.WaterResistance; + FireResistance = playerEntity.FireResistance; + HitRate = !useSecondWeapon ? playerEntity.HitRate : HitRate; + MeleeDodge = playerEntity.MeleeDodge; + RangeDodge = playerEntity.RangedDodge; + Element = (ElementType)element; + ElementRate = playerEntity.ElementRate + playerEntity.SpecialistElementRate; + BCards = bCardDictionary; + ShellOptionsWeapon = playerEntity.GetShellsValues(EquipmentOptionType.WEAPON_SHELL, !useSecondWeapon); + ShellOptionArmor = playerEntity.GetShellsValues(EquipmentOptionType.ARMOR_SHELL, false); + RingCellonValues = playerEntity.GetCellonValues(EquipmentType.Ring); + NecklaceCellonValues = playerEntity.GetCellonValues(EquipmentType.Necklace); + ; + BraceletCellonValues = playerEntity.GetCellonValues(EquipmentType.Bracelet); + Morale = playerEntity.Level; + Position = new Position(playerEntity.PositionX, playerEntity.PositionY); + MapInstance = playerEntity.MapInstance; + MonsterRaceType = MonsterRaceType.People; + MonsterRaceSubType = MonsterSubRace.People.Humanlike; + MeleeDefense = playerEntity.MeleeDefence; + RangeDefense = playerEntity.RangedDefence; + MagicalDefense = playerEntity.MagicDefence; + FamilyUpgrades = playerEntity.Family?.UpgradeValues ?? new Dictionary(); + + double decreaseMagicDamage = 1; + double damageReduction = 1; + if (playerEntity.Specialist != null && playerEntity.UseSp) + { + IncreaseJajamaruDamage = playerEntity.Specialist.ItemVNum == (short)ItemVnums.JAJAMARU_SP && + playerEntity.MateComponent.GetTeamMember(x => x.MateType == MateType.Partner && x.MonsterVNum == (short)MonsterVnum.SAKURA) != null; + + int elementSl = playerEntity.SpecialistComponent.GetSlElement(); + decreaseMagicDamage = elementSl switch + { + >= 20 and <= 49 => 0.95, + >= 50 and <= 79 => 0.9, + >= 80 and <= 99 => 0.85, + >= 100 => 0.8, + _ => decreaseMagicDamage + }; + + int slDefense = playerEntity.SpecialistComponent.GetSlDefense(); + damageReduction = slDefense switch + { + >= 10 and < 40 => 0.85, + >= 40 and < 80 => 0.5, + >= 80 and < 100 => 0.25, + 100 => 0, + _ => 1 + }; + } + + DecreaseMagicDamage = decreaseMagicDamage; + MinimalDamageReduction = damageReduction; + } + + public Dictionary ShellOptionArmor { get; } + public Dictionary ShellOptionsWeapon { get; } + public IReadOnlyDictionary RingCellonValues { get; } + public IReadOnlyDictionary NecklaceCellonValues { get; } + public IReadOnlyDictionary BraceletCellonValues { get; } + public double MinimalDamageReduction { get; } + public bool IncreaseJajamaruDamage { get; } + public double DecreaseMagicDamage { get; } + + public IReadOnlyDictionary<(BCardType type, byte subType), List> BCards { get; } + public IReadOnlyDictionary<(BCardType type, byte subType), List<(int casterLevel, BCardDTO bCard)>> BuffBCards { get; } + public ISet BuffsById { get; } + public IReadOnlyDictionary FamilyUpgrades { get; } + + public VisualType Type { get; } + public long Id { get; } + public IMapInstance MapInstance { get; } + public MonsterRaceType MonsterRaceType { get; } + public Enum MonsterRaceSubType { get; } + public AttackType AttackType { get; } + public ElementType Element { get; } + public int ElementRate { get; } + public int Morale { get; } + public int Level { get; } + public FactionType Faction { get; } + public int DamageMinimum { get; } + public int DamageMaximum { get; } + public int AttackUpgrade { get; } + public int DefenseUpgrade { get; } + public int HitRate { get; } + public int MaxHp { get; } + public int MaxMp { get; } + public int WeaponDamageMinimum { get; } + public int WeaponDamageMaximum { get; } + public int CriticalChance { get; } + public int CriticalDamage { get; } + public int FireResistance { get; } + public int WaterResistance { get; } + public int LightResistance { get; } + public int ShadowResistance { get; } + public int MeleeDefense { get; } + public int MeleeDodge { get; } + public int RangeDefense { get; } + public int RangeDodge { get; } + public int MagicalDefense { get; } + public Position Position { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Battle/SacrificeManager.cs b/srcs/WingsAPI.Game/Battle/SacrificeManager.cs new file mode 100644 index 0000000..8cf7a7c --- /dev/null +++ b/srcs/WingsAPI.Game/Battle/SacrificeManager.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using WingsEmu.Core.Extensions; +using WingsEmu.Game.Entities; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Battle; + +public class SacrificeManager : ISacrificeManager +{ + // Caster | Target + private readonly Dictionary<(VisualType, long), IBattleEntity> _savedSacrificeByCaster = new(); + + // Target | Caster + private readonly Dictionary<(VisualType, long), IBattleEntity> _savedSacrificeByTarget = new(); + + public void SaveSacrifice(IBattleEntity caster, IBattleEntity target) + { + _savedSacrificeByCaster[(caster.Type, caster.Id)] = target; + _savedSacrificeByTarget[(target.Type, target.Id)] = caster; + } + + public IBattleEntity GetTarget(IBattleEntity caster) => _savedSacrificeByCaster.GetOrDefault((caster.Type, caster.Id)); + public IBattleEntity GetCaster(IBattleEntity target) => _savedSacrificeByTarget.GetOrDefault((target.Type, target.Id)); + + public void RemoveSacrifice(IBattleEntity caster, IBattleEntity target) + { + _savedSacrificeByCaster.Remove((caster.Type, caster.Id)); + _savedSacrificeByTarget.Remove((target.Type, target.Id)); + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Battle/SkillInfo.cs b/srcs/WingsAPI.Game/Battle/SkillInfo.cs new file mode 100644 index 0000000..384ce64 --- /dev/null +++ b/srcs/WingsAPI.Game/Battle/SkillInfo.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using WingsEmu.DTOs.BCards; +using WingsEmu.DTOs.Skills; +using WingsEmu.Game._enum; +using WingsEmu.Packets.Enums.Battle; + +namespace WingsEmu.Game.Battle; + +public class SkillInfo +{ + public SkillType SkillType { get; set; } + public int Vnum { get; set; } + public AttackType AttackType { get; set; } + public List BCards { get; set; } = new(); + public short Element { get; set; } + + public short CastAnimation { get; set; } + public short CastEffect { get; set; } + public short CastId { get; set; } + + public short HitAnimation { get; set; } + public short HitEffect { get; set; } + + public byte Range { get; set; } + public short AoERange { get; set; } + + public short Cooldown { get; set; } + public short CastTime { get; set; } + public TargetType TargetType { get; set; } + public TargetHitType HitType { get; set; } + public List Combos { get; set; } = new(); + public IReadOnlyDictionary> BCardsType { get; set; } = new Dictionary>(); + public TargetAffectedEntities TargetAffectedEntities { get; set; } + public short HitChance { get; set; } + public bool IsUsingSecondWeapon { get; set; } + public bool IsComboSkill { get; set; } + public int ManaCost { get; set; } + public int? PartnerSkillRank { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Bazaar/BazaarItem.cs b/srcs/WingsAPI.Game/Bazaar/BazaarItem.cs new file mode 100644 index 0000000..6f63e5c --- /dev/null +++ b/srcs/WingsAPI.Game/Bazaar/BazaarItem.cs @@ -0,0 +1,31 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsAPI.Data.Bazaar; +using WingsEmu.Game.Items; + +namespace WingsEmu.Game.Bazaar; + +public class BazaarItem +{ + /// + /// Should not be used + /// + public BazaarItem() + { + } + + public BazaarItem(BazaarItemDTO bazaarItemDto, GameItemInstance item, string ownerName) + { + BazaarItemDto = bazaarItemDto; + Item = item; + OwnerName = ownerName; + } + + public BazaarItemDTO BazaarItemDto { get; set; } + + public GameItemInstance Item { get; set; } + + public string OwnerName { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Bazaar/Configuration/BazaarConfiguration.cs b/srcs/WingsAPI.Game/Bazaar/Configuration/BazaarConfiguration.cs new file mode 100644 index 0000000..7ecd797 --- /dev/null +++ b/srcs/WingsAPI.Game/Bazaar/Configuration/BazaarConfiguration.cs @@ -0,0 +1,14 @@ +namespace WingsEmu.Game.Bazaar.Configuration; + +public class BazaarConfiguration +{ + public int MaximumListedItems { get; set; } = 30; + + public int MaximumListedItemsMedal { get; set; } = 90; + + public int DelayClientBetweenRequestsInSecs { get; set; } = 3; + + public int DelayServerBetweenRequestsInSecs { get; set; } = 1; + + public int ItemsPerIndex { get; set; } = 30; +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Bazaar/Events/BazaarGetListedItemsEvent.cs b/srcs/WingsAPI.Game/Bazaar/Events/BazaarGetListedItemsEvent.cs new file mode 100644 index 0000000..df6311b --- /dev/null +++ b/srcs/WingsAPI.Game/Bazaar/Events/BazaarGetListedItemsEvent.cs @@ -0,0 +1,16 @@ +using WingsAPI.Packets.Enums.Bazaar; +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Bazaar.Events; + +public class BazaarGetListedItemsEvent : PlayerEvent +{ + public BazaarGetListedItemsEvent(ushort index, BazaarListedItemType filter) + { + Index = index; + Filter = filter; + } + + public ushort Index { get; } + public BazaarListedItemType Filter { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Bazaar/Events/BazaarItemAddEvent.cs b/srcs/WingsAPI.Game/Bazaar/Events/BazaarItemAddEvent.cs new file mode 100644 index 0000000..0e81208 --- /dev/null +++ b/srcs/WingsAPI.Game/Bazaar/Events/BazaarItemAddEvent.cs @@ -0,0 +1,24 @@ +using System; +using WingsEmu.Game._packetHandling; +using WingsEmu.Game.Inventory; + +namespace WingsEmu.Game.Bazaar.Events; + +public class BazaarItemAddEvent : PlayerEvent +{ + public InventoryItem InventoryItem { get; init; } + + public short Amount { get; init; } + + public DateTime ExpiryDate { get; init; } + + public short DayExpiryAmount { get; init; } + + public long PricePerItem { get; init; } + + public long Tax { get; init; } + + public bool UsedMedal { get; init; } + + public bool IsPackage { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Bazaar/Events/BazaarItemAddedEvent.cs b/srcs/WingsAPI.Game/Bazaar/Events/BazaarItemAddedEvent.cs new file mode 100644 index 0000000..71e09d3 --- /dev/null +++ b/srcs/WingsAPI.Game/Bazaar/Events/BazaarItemAddedEvent.cs @@ -0,0 +1,15 @@ +using System; +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Bazaar.Events; + +public class BazaarItemAddedEvent : PlayerEvent +{ + public int ItemVnum { get; init; } + public short Amount { get; init; } + public DateTime ExpiryDate { get; init; } + public long PricePerItem { get; init; } + public long Tax { get; init; } + public bool UsedMedal { get; init; } + public bool IsPackage { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Bazaar/Events/BazaarItemBoughtEvent.cs b/srcs/WingsAPI.Game/Bazaar/Events/BazaarItemBoughtEvent.cs new file mode 100644 index 0000000..948c824 --- /dev/null +++ b/srcs/WingsAPI.Game/Bazaar/Events/BazaarItemBoughtEvent.cs @@ -0,0 +1,14 @@ +using WingsEmu.DTOs.Items; +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Bazaar.Events; + +public class BazaarItemBoughtEvent : PlayerEvent +{ + public long BazaarItemId { get; init; } + public long SellerId { get; init; } + public string SellerName { get; init; } + public long PricePerItem { get; init; } + public int Amount { get; init; } + public ItemInstanceDTO BoughtItem { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Bazaar/Events/BazaarItemBuyEvent.cs b/srcs/WingsAPI.Game/Bazaar/Events/BazaarItemBuyEvent.cs new file mode 100644 index 0000000..12ca394 --- /dev/null +++ b/srcs/WingsAPI.Game/Bazaar/Events/BazaarItemBuyEvent.cs @@ -0,0 +1,19 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Bazaar.Events; + +public class BazaarItemBuyEvent : PlayerEvent +{ + public BazaarItemBuyEvent(long bazaarItemId, short amount, long pricePerItem) + { + BazaarItemId = bazaarItemId; + Amount = amount; + PricePerItem = pricePerItem; + } + + public long BazaarItemId { get; } + + public short Amount { get; } + + public long PricePerItem { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Bazaar/Events/BazaarItemChangePriceEvent.cs b/srcs/WingsAPI.Game/Bazaar/Events/BazaarItemChangePriceEvent.cs new file mode 100644 index 0000000..2dbd438 --- /dev/null +++ b/srcs/WingsAPI.Game/Bazaar/Events/BazaarItemChangePriceEvent.cs @@ -0,0 +1,19 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Bazaar.Events; + +public class BazaarItemChangePriceEvent : PlayerEvent +{ + public BazaarItemChangePriceEvent(long bazaarItemId, long newPricePerItem, bool confirmed) + { + BazaarItemId = bazaarItemId; + NewPricePerItem = newPricePerItem; + Confirmed = confirmed; + } + + public long BazaarItemId { get; } + + public long NewPricePerItem { get; } + + public bool Confirmed { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Bazaar/Events/BazaarItemExpiredEvent.cs b/srcs/WingsAPI.Game/Bazaar/Events/BazaarItemExpiredEvent.cs new file mode 100644 index 0000000..20d814e --- /dev/null +++ b/srcs/WingsAPI.Game/Bazaar/Events/BazaarItemExpiredEvent.cs @@ -0,0 +1,12 @@ +using WingsEmu.DTOs.Items; +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Bazaar.Events; + +public class BazaarItemExpiredEvent : PlayerEvent +{ + public long BazaarItemId { get; init; } + public long Price { get; init; } + public int Quantity { get; init; } + public ItemInstanceDTO Item { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Bazaar/Events/BazaarItemInsertedEvent.cs b/srcs/WingsAPI.Game/Bazaar/Events/BazaarItemInsertedEvent.cs new file mode 100644 index 0000000..2af1e8d --- /dev/null +++ b/srcs/WingsAPI.Game/Bazaar/Events/BazaarItemInsertedEvent.cs @@ -0,0 +1,13 @@ +using WingsEmu.DTOs.Items; +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Bazaar.Events; + +public class BazaarItemInsertedEvent : PlayerEvent +{ + public long BazaarItemId { get; init; } + public long Price { get; init; } + public int Quantity { get; init; } + public long Taxes { get; init; } + public ItemInstanceDTO ItemInstance { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Bazaar/Events/BazaarItemRemoveEvent.cs b/srcs/WingsAPI.Game/Bazaar/Events/BazaarItemRemoveEvent.cs new file mode 100644 index 0000000..d3a126b --- /dev/null +++ b/srcs/WingsAPI.Game/Bazaar/Events/BazaarItemRemoveEvent.cs @@ -0,0 +1,10 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Bazaar.Events; + +public class BazaarItemRemoveEvent : PlayerEvent +{ + public BazaarItemRemoveEvent(long bazaarItemId) => BazaarItemId = bazaarItemId; + + public long BazaarItemId { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Bazaar/Events/BazaarItemRemovedEvent.cs b/srcs/WingsAPI.Game/Bazaar/Events/BazaarItemRemovedEvent.cs new file mode 100644 index 0000000..ea8482e --- /dev/null +++ b/srcs/WingsAPI.Game/Bazaar/Events/BazaarItemRemovedEvent.cs @@ -0,0 +1,11 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Bazaar.Events; + +public class BazaarItemRemovedEvent : PlayerEvent +{ + public int ItemVnum { get; init; } + public short SoldAmount { get; init; } + public short Amount { get; init; } + public long TotalProfit { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Bazaar/Events/BazaarItemWithdrawnEvent.cs b/srcs/WingsAPI.Game/Bazaar/Events/BazaarItemWithdrawnEvent.cs new file mode 100644 index 0000000..ab0987b --- /dev/null +++ b/srcs/WingsAPI.Game/Bazaar/Events/BazaarItemWithdrawnEvent.cs @@ -0,0 +1,13 @@ +using WingsEmu.DTOs.Items; +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Bazaar.Events; + +public class BazaarItemWithdrawnEvent : PlayerEvent +{ + public long BazaarItemId { get; init; } + public long Price { get; init; } + public int Quantity { get; init; } + public ItemInstanceDTO ItemInstance { get; init; } + public long ClaimedMoney { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Bazaar/Events/BazaarOpenUiEvent.cs b/srcs/WingsAPI.Game/Bazaar/Events/BazaarOpenUiEvent.cs new file mode 100644 index 0000000..ca2a681 --- /dev/null +++ b/srcs/WingsAPI.Game/Bazaar/Events/BazaarOpenUiEvent.cs @@ -0,0 +1,10 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Bazaar.Events; + +public class BazaarOpenUiEvent : PlayerEvent +{ + public BazaarOpenUiEvent(bool throughMedal) => ThroughMedal = throughMedal; + + public bool ThroughMedal { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Bazaar/Events/BazaarSearchItemsEvent.cs b/srcs/WingsAPI.Game/Bazaar/Events/BazaarSearchItemsEvent.cs new file mode 100644 index 0000000..b79b034 --- /dev/null +++ b/srcs/WingsAPI.Game/Bazaar/Events/BazaarSearchItemsEvent.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using WingsAPI.Packets.Enums.Bazaar.Filter; +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Bazaar.Events; + +public class BazaarSearchItemsEvent : PlayerEvent +{ + public BazaarSearchItemsEvent(int index, BazaarCategoryFilterType categoryFilterType, byte subTypeFilter, BazaarLevelFilterType levelFilter, BazaarRarityFilterType rareFilter, + BazaarUpgradeFilterType upgradeFilter, BazaarSortFilterType orderFilter, IReadOnlyCollection itemVNumFilter) + { + Index = index; + CategoryFilterType = categoryFilterType; + SubTypeFilter = subTypeFilter; + LevelFilter = levelFilter; + RareFilter = rareFilter; + UpgradeFilter = upgradeFilter; + OrderFilter = orderFilter; + ItemVNumFilter = itemVNumFilter; + } + + public int Index { get; } + + public BazaarCategoryFilterType CategoryFilterType { get; } + + public byte SubTypeFilter { get; } + + public BazaarLevelFilterType LevelFilter { get; } + + public BazaarRarityFilterType RareFilter { get; } + + public BazaarUpgradeFilterType UpgradeFilter { get; } + + public BazaarSortFilterType OrderFilter { get; } + + public IReadOnlyCollection ItemVNumFilter { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Bazaar/IBazaarManager.cs b/srcs/WingsAPI.Game/Bazaar/IBazaarManager.cs new file mode 100644 index 0000000..8427910 --- /dev/null +++ b/srcs/WingsAPI.Game/Bazaar/IBazaarManager.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using WingsAPI.Communication; +using WingsAPI.Communication.Bazaar; + +namespace WingsEmu.Game.Bazaar; + +public interface IBazaarManager +{ + Task GetOwnerName(long characterId); + + Task GetBazaarItemById(long bazaarItemId); + + Task> GetListedItemsByCharacterId(long characterId); + + Task<(IReadOnlyCollection, RpcResponseType)> SearchBazaarItems(BazaarSearchContext bazaarSearchContext); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Buffs/Buff.cs b/srcs/WingsAPI.Game/Buffs/Buff.cs new file mode 100644 index 0000000..611a57d --- /dev/null +++ b/srcs/WingsAPI.Game/Buffs/Buff.cs @@ -0,0 +1,112 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Generic; +using WingsEmu.DTOs.BCards; +using WingsEmu.Game._enum; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Entities; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Buffs; + +public class Buff +{ + public Buff(Guid buffId, int cardId, int level, TimeSpan duration, int effectId, string name, + short timeoutBuff, BuffGroup buffGroup, byte timeoutBuffChance, int secondBCardsDelay, + int groupId, BuffCategory buffCategory, DateTime start, BuffFlag buffFlags, IReadOnlyCollection bCards, + bool isConstEffect, ElementType elementType, int casterLevel, IBattleEntity caster) + { + BuffId = buffId; + CardId = cardId; + Level = level; + Duration = duration; + EffectId = effectId; + GroupId = groupId; + Name = name; + TimeoutBuff = timeoutBuff; + BuffGroup = buffGroup; + TimeoutBuffChance = timeoutBuffChance; + SecondBCardsDelay = secondBCardsDelay; + BuffCategory = buffCategory; + Start = start; + BuffFlags = buffFlags; + BCards = bCards; + IsConstEffect = isConstEffect; + ElementType = elementType; + CasterLevel = casterLevel; + Caster = caster; + } + + public Guid BuffId { get; } + + public int CardId { get; } + + public int GroupId { get; } + + public int Level { get; } + + public TimeSpan Duration { get; private set; } + + public int EffectId { get; } + + public string Name { get; } + + public short TimeoutBuff { get; } + + public BuffGroup BuffGroup { get; } + + public byte TimeoutBuffChance { get; } + + public int SecondBCardsDelay { get; } + + public bool SecondBCardsExecuted { get; set; } + + public BuffFlag BuffFlags { get; } + + public BuffCategory BuffCategory { get; } + + public DateTime Start { get; private set; } + + public bool IsConstEffect { get; } + + public ElementType ElementType { get; } + + public int CasterLevel { get; } + + public IBattleEntity Caster { get; } + + public IReadOnlyCollection BCards { get; } + + public void SetBuffDuration(TimeSpan duration) + { + Start = DateTime.UtcNow; + Duration = duration; + } + + protected bool Equals(Buff other) => BuffId.Equals(other.BuffId); + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != GetType()) + { + return false; + } + + return Equals((Buff)obj); + } + + public override int GetHashCode() => BuffId.GetHashCode(); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Buffs/BuffComponent.cs b/srcs/WingsAPI.Game/Buffs/BuffComponent.cs new file mode 100644 index 0000000..cd20688 --- /dev/null +++ b/srcs/WingsAPI.Game/Buffs/BuffComponent.cs @@ -0,0 +1,158 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using WingsEmu.Core.Extensions; +using WingsEmu.Game._enum; +using WingsEmu.Game.Extensions; + +namespace WingsEmu.Game.Buffs; + +public class BuffComponent : IBuffComponent +{ + private readonly ConcurrentDictionary _buffByBuffId = new(); + private readonly ConcurrentDictionary _buffByCardId = new(); + private readonly ConcurrentDictionary _buffByGroupId = new(); + private readonly ConcurrentDictionary> _buffByTypes = new(); + private readonly List _buffs = new(); + private readonly ReaderWriterLockSlim _lock = new(LockRecursionPolicy.SupportsRecursion); + + public bool HasAnyBuff() => _buffs.Count != 0; + + public IReadOnlyList GetAllBuffs() + { + _lock.EnterReadLock(); + try + { + return _buffs.ToArray(); + } + finally + { + _lock.ExitReadLock(); + } + } + + public ICollection GetAllBuffsId() => _buffByCardId.Keys; + + public IReadOnlyList GetAllBuffs(Func predicate) + { + _lock.EnterReadLock(); + try + { + return _buffs.FindAll(x => x != null && (predicate == null || predicate(x))); + } + finally + { + _lock.ExitReadLock(); + } + } + + public void AddBuff(Buff buff) + { + if (buff == null) + { + return; + } + + _lock.EnterWriteLock(); + try + { + if (_buffByCardId.ContainsKey(buff.CardId)) + { + return; + } + + HashSet buffWithSameType = _buffByTypes.GetOrDefault(buff.BuffGroup); + if (buffWithSameType == null) + { + buffWithSameType = new HashSet(); + _buffByTypes[buff.BuffGroup] = buffWithSameType; + } + + buffWithSameType.Add(buff); + _buffs.Add(buff); + _buffByCardId.TryAdd(buff.CardId, buff); + _buffByBuffId.TryAdd(buff.BuffId, buff); + _buffByGroupId.TryAdd(buff.GroupId, buff); + } + finally + { + _lock.ExitWriteLock(); + } + } + + public Buff GetBuff(int cardId) + { + _lock.EnterReadLock(); + try + { + return !_buffByCardId.TryGetValue(cardId, out Buff buff) ? null : buff; + } + finally + { + _lock.ExitReadLock(); + } + } + + public Buff GetBuffByGroupId(int groupId) + { + _lock.EnterReadLock(); + try + { + return !_buffByGroupId.TryGetValue(groupId, out Buff buff) ? null : buff; + } + finally + { + _lock.ExitReadLock(); + } + } + + public bool HasBuff(BuffGroup buffGroup) => _buffByTypes.TryGetValue(buffGroup, out HashSet buff) && buff.Count > 0; + public bool HasBuff(int cardId) => _buffByCardId.ContainsKey(cardId); + public bool HasBuff(Guid buffId) => _buffByBuffId.ContainsKey(buffId); + + public void RemoveBuff(Guid buffId) + { + _lock.EnterWriteLock(); + try + { + _buffByBuffId.Remove(buffId, out Buff buff); + if (buff == null) + { + return; + } + + _buffs.Remove(buff); + _buffByCardId.TryRemove(buff.CardId, out _); + _buffByTypes.GetOrDefault(buff.BuffGroup)?.Remove(buff); + _buffByGroupId.TryRemove(buff.GroupId, out _); + } + finally + { + _lock.ExitWriteLock(); + } + } + + public void ClearNonPersistentBuffs() + { + _lock.EnterWriteLock(); + try + { + IEnumerable buffsToRemove = _buffByCardId.Values.ToArray(); + buffsToRemove = buffsToRemove.Where(s => !s.IsSavingOnDisconnect()); + foreach (Buff buff in buffsToRemove) + { + _buffByTypes[buff.BuffGroup].Remove(buff); + _buffByBuffId.TryRemove(buff.BuffId, out _); + _buffByCardId.TryRemove(buff.CardId, out _); + _buffByGroupId.TryRemove(buff.GroupId, out _); + _buffs.Remove(buff); + } + } + finally + { + _lock.ExitWriteLock(); + } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Buffs/BuffFactory.cs b/srcs/WingsAPI.Game/Buffs/BuffFactory.cs new file mode 100644 index 0000000..5ee4bcc --- /dev/null +++ b/srcs/WingsAPI.Game/Buffs/BuffFactory.cs @@ -0,0 +1,100 @@ +using System; +using WingsEmu.Game._enum; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Buffs; + +public class BuffFactory : IBuffFactory +{ + private readonly IBuffsDurationConfiguration _buffsDurationConfiguration; + private readonly ICardsManager _cardsManager; + private readonly IRandomGenerator _randomGenerator; + + public BuffFactory(ICardsManager cardsManager, IRandomGenerator randomGenerator, IBuffsDurationConfiguration buffsDurationConfiguration) + { + _cardsManager = cardsManager; + _randomGenerator = randomGenerator; + _buffsDurationConfiguration = buffsDurationConfiguration; + } + + public Buff CreateBuff(int cardId, IBattleEntity caster, bool forceCreationStats = false) => CreateBuff(cardId, caster, BuffFlag.NORMAL); + + public Buff CreateBuff(int cardId, IBattleEntity caster, TimeSpan duration, BuffFlag buffFlag, bool forceCreationStats = false) + { + Card card = _cardsManager.GetCardByCardId((short)cardId); + return card == null ? null : CreateBuff(cardId, caster, caster.Level, duration, buffFlag, BuffGroup.Neutral, BuffCategory.GeneralEffect, forceCreationStats); + } + + public Buff CreateBuff(int cardId, IBattleEntity caster, BuffGroup buffGroup, BuffFlag buffFlag, bool forceCreationStats = false) + { + Card card = _cardsManager.GetCardByCardId((short)cardId); + return card == null ? null : CreateBuff(cardId, caster, caster.Level, TimeSpan.FromMilliseconds(card.Duration * 100), buffFlag, buffGroup, card.BuffCategory, forceCreationStats); + } + + public Buff CreateBuff(int cardId, IBattleEntity caster, BuffFlag buffFlags, bool forceCreationStats = false) + { + Card card = _cardsManager.GetCardByCardId((short)cardId); + return card == null + ? null + : CreateBuff(cardId, caster, caster.Level, TimeSpan.FromMilliseconds(card.Duration * 100), buffFlags, (BuffGroup)card.BuffType, card.BuffCategory, forceCreationStats); + } + + public Buff CreateBuff(int cardId, IBattleEntity caster, BuffGroup buffGroup, BuffCategory buffCategory, bool forceCreationStats = false) + { + Card card = _cardsManager.GetCardByCardId((short)cardId); + return card == null ? null : CreateBuff(cardId, caster, caster.Level, TimeSpan.FromMilliseconds(card.Duration * 100), BuffFlag.NORMAL, buffGroup, buffCategory, forceCreationStats); + } + + public Buff CreateBuff(int cardId, IBattleEntity caster, int level, TimeSpan duration, BuffFlag buffCategory, bool forceCreationStats = false) => + CreateBuff(cardId, caster, level, duration, buffCategory, BuffGroup.Neutral, BuffCategory.GeneralEffect, forceCreationStats); + + public Buff CreateBuff(int cardId, IBattleEntity caster, TimeSpan duration, bool forceCreationStats = false) + { + Card card = _cardsManager.GetCardByCardId((short)cardId); + return card == null ? null : CreateBuff(cardId, caster, caster.Level, duration, BuffFlag.NORMAL, (BuffGroup)card.BuffType, card.BuffCategory, forceCreationStats); + } + + public Buff CreateBuff(int cardId, IBattleEntity caster, int level, TimeSpan duration, BuffFlag buffFlags, BuffGroup buffGroup, BuffCategory buffCategory, bool forceCreationStats) + { + Card card = _cardsManager.GetCardByCardId((short)cardId); + if (card == null) + { + return null; + } + + BuffDuration randomDuration = _buffsDurationConfiguration.GetBuffDurationById(cardId); + if (randomDuration != null && !forceCreationStats) + { + duration = TimeSpan.FromMilliseconds(_randomGenerator.RandomNumber(randomDuration.MinDuration, randomDuration.MaxDuration + 1)); + buffFlags = randomDuration.IsPermanent ? BuffFlag.BIG | BuffFlag.NO_DURATION : buffFlags; + } + + return new Buff( + Guid.NewGuid(), + card.Id, + card.Level, + duration, + card.EffectId, + card.Name, + card.TimeoutBuff, + buffGroup, + card.TimeoutBuffChance, + card.SecondBCardsDelay, + card.GroupId, + buffCategory, + DateTime.UtcNow, + buffFlags, + card.BCards, + card.IsConstEffect, + (ElementType)card.ElementType, + level, + caster + ); + } + + public Buff CreateBuff(int cardId, int level, TimeSpan duration, BuffFlag buffCategory, bool forceCreationStats = false) => + CreateBuff(cardId, null, level, duration, buffCategory, BuffGroup.Neutral, BuffCategory.GeneralEffect, forceCreationStats); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Buffs/BuffFactoryExtensions.cs b/srcs/WingsAPI.Game/Buffs/BuffFactoryExtensions.cs new file mode 100644 index 0000000..6f11ddf --- /dev/null +++ b/srcs/WingsAPI.Game/Buffs/BuffFactoryExtensions.cs @@ -0,0 +1,11 @@ +using System; +using WingsEmu.Game._enum; +using WingsEmu.Game.Entities; + +namespace WingsEmu.Game.Buffs; + +public static class BuffFactoryExtensions +{ + public static Buff CreateOneHourBuff(this IBuffFactory factory, IBattleEntity caster, int cardId, BuffFlag buffFlags) + => factory.CreateBuff(cardId, caster, TimeSpan.FromHours(1), buffFlags); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Buffs/BuffsDurationConfiguration.cs b/srcs/WingsAPI.Game/Buffs/BuffsDurationConfiguration.cs new file mode 100644 index 0000000..1d20f99 --- /dev/null +++ b/srcs/WingsAPI.Game/Buffs/BuffsDurationConfiguration.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using System.Collections.Immutable; + +namespace WingsEmu.Game.Buffs; + +public interface IBuffsDurationConfiguration +{ + BuffDuration GetBuffDurationById(int id); +} + +public class BuffsDurationConfiguration : IBuffsDurationConfiguration +{ + private readonly ImmutableDictionary _buffDurations; + + public BuffsDurationConfiguration(IEnumerable buffDurations) + { + _buffDurations = buffDurations.ToImmutableDictionary(s => s.BuffVnum); + } + + public BuffDuration GetBuffDurationById(int id) => _buffDurations.GetValueOrDefault(id); +} + +public class BuffDuration +{ + public int BuffVnum { get; set; } + public bool IsPermanent { get; set; } + public int MinDuration { get; set; } + public int MaxDuration { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Buffs/Card.cs b/srcs/WingsAPI.Game/Buffs/Card.cs new file mode 100644 index 0000000..fc3e2cf --- /dev/null +++ b/srcs/WingsAPI.Game/Buffs/Card.cs @@ -0,0 +1,14 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using WingsEmu.DTOs.BCards; +using WingsEmu.DTOs.Buffs; + +namespace WingsEmu.Game.Buffs; + +public class Card : CardDTO +{ + public List BCards { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Buffs/EquipmentOptionContainer.cs b/srcs/WingsAPI.Game/Buffs/EquipmentOptionContainer.cs new file mode 100644 index 0000000..b4a73ad --- /dev/null +++ b/srcs/WingsAPI.Game/Buffs/EquipmentOptionContainer.cs @@ -0,0 +1,143 @@ +using System.Collections.Generic; +using System.Linq; +using WingsAPI.Packets.Enums.Shells; +using WingsEmu.Core.Extensions; +using WingsEmu.DTOs.Items; +using WingsEmu.Game._enum; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Buffs; + +public class EquipmentOptionContainer : IEquipmentOptionContainer +{ + private readonly Dictionary> _cellons; + private readonly Dictionary<(bool isMainWeapon, EquipmentOptionType equipmentOptionType), Dictionary> _shells; + + public EquipmentOptionContainer() + { + _cellons = new Dictionary>(); + _shells = new Dictionary<(bool isMainWeapon, EquipmentOptionType equipmentOptionType), Dictionary>(); + } + + public void AddShells(EquipmentOptionType equipmentOptionType, List optionDto, bool isMainWeapon) + { + if (equipmentOptionType != EquipmentOptionType.ARMOR_SHELL && equipmentOptionType != EquipmentOptionType.WEAPON_SHELL) + { + return; + } + + if (optionDto == null) + { + return; + } + + if (!optionDto.Any()) + { + return; + } + + if (!_shells.TryGetValue((isMainWeapon, equipmentOptionType), out Dictionary dictionary)) + { + dictionary = new Dictionary(); + _shells[(isMainWeapon, equipmentOptionType)] = dictionary; + } + + foreach (EquipmentOptionDTO option in optionDto) + { + var shellOptionType = (ShellEffectType)option.Type; + dictionary[shellOptionType] = option.Value; + } + } + + public void ClearShells(EquipmentOptionType equipmentOptionType, bool isMainWeapon) + { + if (!_shells.TryGetValue((isMainWeapon, equipmentOptionType), out Dictionary dictionary)) + { + return; + } + + dictionary.Clear(); + } + + public Dictionary GetShellsValues(EquipmentOptionType equipmentOptionType, bool isMainWeapon) => + !_shells.TryGetValue((isMainWeapon, equipmentOptionType), out Dictionary dictionary) ? new Dictionary() : dictionary; + + public int GetMaxWeaponShellValue(ShellEffectType shellEffectType, bool isMainWeapon) + { + if (!_shells.TryGetValue((isMainWeapon, EquipmentOptionType.WEAPON_SHELL), out Dictionary dictionary)) + { + return default; + } + + return dictionary.GetOrDefault(shellEffectType); + } + + public int GetMaxArmorShellValue(ShellEffectType shellEffectType) + { + if (!_shells.TryGetValue((false, EquipmentOptionType.ARMOR_SHELL), out Dictionary dictionary)) + { + return default; + } + + return dictionary.GetOrDefault(shellEffectType); + } + + public void AddCellon(EquipmentType equipmentType, List optionDto) + { + if (equipmentType != EquipmentType.Necklace && equipmentType != EquipmentType.Bracelet && equipmentType != EquipmentType.Ring) + { + return; + } + + if (optionDto == null) + { + return; + } + + if (!optionDto.Any()) + { + return; + } + + if (!_cellons.TryGetValue(equipmentType, out Dictionary dictionary)) + { + dictionary = new Dictionary(); + _cellons[equipmentType] = dictionary; + } + + foreach (EquipmentOptionDTO option in optionDto) + { + dictionary[(CellonType)option.Type] = option.Value; + } + } + + public void ClearCellon(EquipmentType equipmentType) + { + if (!_cellons.TryGetValue(equipmentType, out Dictionary dictionary)) + { + return; + } + + dictionary.Clear(); + } + + public Dictionary GetCellonValues(EquipmentType equipmentType) + { + if (!_cellons.TryGetValue(equipmentType, out Dictionary dictionary)) + { + return new Dictionary(); + } + + return dictionary; + } + + public int GetCellonValue(EquipmentType equipmentType, CellonType type) + { + if (!_cellons.TryGetValue(equipmentType, out Dictionary dictionary)) + { + return default; + } + + return dictionary.GetOrDefault(type); + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Buffs/Events/AngelSpecialistElementalBuffEvent.cs b/srcs/WingsAPI.Game/Buffs/Events/AngelSpecialistElementalBuffEvent.cs new file mode 100644 index 0000000..9df0361 --- /dev/null +++ b/srcs/WingsAPI.Game/Buffs/Events/AngelSpecialistElementalBuffEvent.cs @@ -0,0 +1,9 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Game.Battle; + +namespace WingsEmu.Game.Buffs.Events; + +public class AngelSpecialistElementalBuffEvent : PlayerEvent +{ + public SkillInfo Skill { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Buffs/Events/BuffAddEvent.cs b/srcs/WingsAPI.Game/Buffs/Events/BuffAddEvent.cs new file mode 100644 index 0000000..ff513fb --- /dev/null +++ b/srcs/WingsAPI.Game/Buffs/Events/BuffAddEvent.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using WingsEmu.Game.Entities; + +namespace WingsEmu.Game.Buffs.Events; + +public class BuffAddEvent : IBattleEntityEvent +{ + public BuffAddEvent(IBattleEntity entity, IEnumerable buffs) + { + Entity = entity; + Buffs = buffs; + } + + public IEnumerable Buffs { get; } + + public IBattleEntity Entity { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Buffs/Events/BuffPartnerCheckEvent.cs b/srcs/WingsAPI.Game/Buffs/Events/BuffPartnerCheckEvent.cs new file mode 100644 index 0000000..7720d9f --- /dev/null +++ b/srcs/WingsAPI.Game/Buffs/Events/BuffPartnerCheckEvent.cs @@ -0,0 +1,7 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Buffs.Events; + +public class BuffPartnerCheckEvent : PlayerEvent +{ +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Buffs/Events/BuffRemoveEvent.cs b/srcs/WingsAPI.Game/Buffs/Events/BuffRemoveEvent.cs new file mode 100644 index 0000000..f01ddae --- /dev/null +++ b/srcs/WingsAPI.Game/Buffs/Events/BuffRemoveEvent.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using WingsEmu.Game.Entities; + +namespace WingsEmu.Game.Buffs.Events; + +public class BuffRemoveEvent : IBattleEntityEvent +{ + public IEnumerable Buffs { get; init; } + public bool RemovePermanentBuff { get; init; } + public bool ShowMessage { get; init; } = true; + public bool RemoveFromGroupId { get; init; } = true; + public IBattleEntity Entity { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Buffs/IBuffComponent.cs b/srcs/WingsAPI.Game/Buffs/IBuffComponent.cs new file mode 100644 index 0000000..3278e9e --- /dev/null +++ b/srcs/WingsAPI.Game/Buffs/IBuffComponent.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using WingsEmu.Game._enum; + +namespace WingsEmu.Game.Buffs; + +public interface IBuffComponent +{ + bool HasAnyBuff(); + IReadOnlyList GetAllBuffs(); + ICollection GetAllBuffsId(); + IReadOnlyList GetAllBuffs(Func predicate); + void AddBuff(Buff buff); + Buff GetBuff(int cardId); + Buff GetBuffByGroupId(int groupId); + bool HasBuff(BuffGroup buffType); + bool HasBuff(int cardId); + bool HasBuff(Guid buffId); + void RemoveBuff(Guid buffId); + void ClearNonPersistentBuffs(); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Buffs/IBuffFactory.cs b/srcs/WingsAPI.Game/Buffs/IBuffFactory.cs new file mode 100644 index 0000000..fa50791 --- /dev/null +++ b/srcs/WingsAPI.Game/Buffs/IBuffFactory.cs @@ -0,0 +1,18 @@ +using System; +using WingsEmu.Game._enum; +using WingsEmu.Game.Entities; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Buffs; + +public interface IBuffFactory +{ + Buff CreateBuff(int cardId, IBattleEntity caster, bool forceCreationStats = false); + Buff CreateBuff(int cardId, IBattleEntity caster, TimeSpan duration, bool forceCreationStats = false); + Buff CreateBuff(int cardId, IBattleEntity caster, BuffFlag buffFlags, bool forceCreationStats = false); + Buff CreateBuff(int cardId, IBattleEntity caster, TimeSpan duration, BuffFlag buffFlag, bool forceCreationStats = false); + Buff CreateBuff(int cardId, IBattleEntity caster, BuffGroup buffGroup, BuffFlag buffFlag, bool forceCreationStats = false); + Buff CreateBuff(int cardId, IBattleEntity caster, BuffGroup buffGroup, BuffCategory buffCategory, bool forceCreationStats = false); + Buff CreateBuff(int cardId, IBattleEntity caster, int level, TimeSpan duration, BuffFlag buffCategory, bool forceCreationStats = false); + Buff CreateBuff(int cardId, IBattleEntity caster, int level, TimeSpan duration, BuffFlag buffFlags, BuffGroup buffGroup, BuffCategory buffType, bool forceCreationStats); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Buffs/IEquipmentOptionContainer.cs b/srcs/WingsAPI.Game/Buffs/IEquipmentOptionContainer.cs new file mode 100644 index 0000000..43226b9 --- /dev/null +++ b/srcs/WingsAPI.Game/Buffs/IEquipmentOptionContainer.cs @@ -0,0 +1,25 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using WingsAPI.Packets.Enums.Shells; +using WingsEmu.DTOs.Items; +using WingsEmu.Game._enum; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Buffs; + +public interface IEquipmentOptionContainer +{ + public void AddShells(EquipmentOptionType equipmentOptionType, List optionDto, bool isMainWeapon); + public void ClearShells(EquipmentOptionType equipmentOptionType, bool isMainWeapon); + public Dictionary GetShellsValues(EquipmentOptionType equipmentOptionType, bool isMainWeapon); + public int GetMaxWeaponShellValue(ShellEffectType shellEffectType, bool isMainWeapon); + public int GetMaxArmorShellValue(ShellEffectType shellEffectType); + + public void AddCellon(EquipmentType equipmentType, List optionDto); + public void ClearCellon(EquipmentType equipmentType); + public Dictionary GetCellonValues(EquipmentType equipmentType); + public int GetCellonValue(EquipmentType equipmentType, CellonType type); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Buffs/StaticBuffFactory.cs b/srcs/WingsAPI.Game/Buffs/StaticBuffFactory.cs new file mode 100644 index 0000000..dc665b3 --- /dev/null +++ b/srcs/WingsAPI.Game/Buffs/StaticBuffFactory.cs @@ -0,0 +1,11 @@ +namespace WingsEmu.Game.Buffs; + +public static class StaticBuffFactory +{ + public static IBuffFactory Instance { get; private set; } + + public static void Initialize(IBuffFactory factory) + { + Instance = factory; + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Buffs/StaticMeditationManager.cs b/srcs/WingsAPI.Game/Buffs/StaticMeditationManager.cs new file mode 100644 index 0000000..6a6b6a7 --- /dev/null +++ b/srcs/WingsAPI.Game/Buffs/StaticMeditationManager.cs @@ -0,0 +1,13 @@ +using WingsEmu.Game.Battle; + +namespace WingsEmu.Game.Buffs; + +public static class StaticMeditationManager +{ + public static IMeditationManager Instance { get; private set; } + + public static void Initialize(IMeditationManager meditationManager) + { + Instance = meditationManager; + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Characters/BasicPlayerDump.cs b/srcs/WingsAPI.Game/Characters/BasicPlayerDump.cs new file mode 100644 index 0000000..3a2c6f9 --- /dev/null +++ b/srcs/WingsAPI.Game/Characters/BasicPlayerDump.cs @@ -0,0 +1,17 @@ +using WingsEmu.DTOs.Items; +using WingsEmu.Packets.Enums.Character; + +namespace WingsEmu.Game.Characters; + +public class BasicPlayerDump +{ + public long CharacterId { get; init; } + public int Level { get; init; } + public ClassType Class { get; init; } + public ItemInstanceDTO Specialist { get; init; } + public int TotalFireResistance { get; init; } + public int TotalWaterResistance { get; init; } + public int TotalLightResistance { get; init; } + public int TotalDarkResistance { get; init; } + public int? FairyLevel { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Characters/Events/AddExpEvent.cs b/srcs/WingsAPI.Game/Characters/Events/AddExpEvent.cs new file mode 100644 index 0000000..6e0ff5a --- /dev/null +++ b/srcs/WingsAPI.Game/Characters/Events/AddExpEvent.cs @@ -0,0 +1,16 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Characters.Events; + +public class AddExpEvent : PlayerEvent +{ + public AddExpEvent(long exp, LevelType levelType) + { + Exp = exp; + LevelType = levelType; + } + + public long Exp { get; } + public LevelType LevelType { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Characters/Events/AddStaticBonusEvent.cs b/srcs/WingsAPI.Game/Characters/Events/AddStaticBonusEvent.cs new file mode 100644 index 0000000..20d0427 --- /dev/null +++ b/srcs/WingsAPI.Game/Characters/Events/AddStaticBonusEvent.cs @@ -0,0 +1,11 @@ +using WingsEmu.DTOs.Bonus; +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Characters.Events; + +public class AddStaticBonusEvent : PlayerEvent +{ + public AddStaticBonusEvent(CharacterStaticBonusDto staticBonusDto) => StaticBonusDto = staticBonusDto; + + public CharacterStaticBonusDto StaticBonusDto { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Characters/Events/BankOpenEvent.cs b/srcs/WingsAPI.Game/Characters/Events/BankOpenEvent.cs new file mode 100644 index 0000000..c06a08e --- /dev/null +++ b/srcs/WingsAPI.Game/Characters/Events/BankOpenEvent.cs @@ -0,0 +1,10 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Game.Inventory; + +namespace WingsEmu.Game.Characters.Events; + +public class BankOpenEvent : PlayerEvent +{ + public long? NpcId { get; init; } + public InventoryItem BankCard { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Characters/Events/BattleEntityHealEvent.cs b/srcs/WingsAPI.Game/Characters/Events/BattleEntityHealEvent.cs new file mode 100644 index 0000000..c701fff --- /dev/null +++ b/srcs/WingsAPI.Game/Characters/Events/BattleEntityHealEvent.cs @@ -0,0 +1,11 @@ +using WingsEmu.Game.Entities; + +namespace WingsEmu.Game.Characters.Events; + +public class BattleEntityHealEvent : IBattleEntityEvent +{ + public int HpHeal { get; init; } + public int MpHeal { get; init; } + public bool HealMates { get; init; } + public IBattleEntity Entity { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Characters/Events/BoxOpenedEvent.cs b/srcs/WingsAPI.Game/Characters/Events/BoxOpenedEvent.cs new file mode 100644 index 0000000..346f79f --- /dev/null +++ b/srcs/WingsAPI.Game/Characters/Events/BoxOpenedEvent.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using WingsEmu.DTOs.Items; +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Characters.Events; + +public class BoxOpenedEvent : PlayerEvent +{ + public ItemInstanceDTO Box { get; init; } + public List Rewards { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Characters/Events/CellonUpgradeEvent.cs b/srcs/WingsAPI.Game/Characters/Events/CellonUpgradeEvent.cs new file mode 100644 index 0000000..162a911 --- /dev/null +++ b/srcs/WingsAPI.Game/Characters/Events/CellonUpgradeEvent.cs @@ -0,0 +1,17 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Items; + +namespace WingsEmu.Game.Characters.Events; + +public class CellonUpgradeEvent : PlayerEvent +{ + public CellonUpgradeEvent(InventoryItem cellon, GameItemInstance upgradableItem) + { + Cellon = cellon; + UpgradableItem = upgradableItem; + } + + public InventoryItem Cellon { get; } + public GameItemInstance UpgradableItem { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Characters/Events/CellonUpgradedEvent.cs b/srcs/WingsAPI.Game/Characters/Events/CellonUpgradedEvent.cs new file mode 100644 index 0000000..3023098 --- /dev/null +++ b/srcs/WingsAPI.Game/Characters/Events/CellonUpgradedEvent.cs @@ -0,0 +1,11 @@ +using WingsEmu.DTOs.Items; +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Characters.Events; + +public class CellonUpgradedEvent : PlayerEvent +{ + public ItemInstanceDTO Item { get; init; } + public int CellonVnum { get; init; } + public bool Succeed { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Characters/Events/ChangeClassEvent.cs b/srcs/WingsAPI.Game/Characters/Events/ChangeClassEvent.cs new file mode 100644 index 0000000..99846e8 --- /dev/null +++ b/srcs/WingsAPI.Game/Characters/Events/ChangeClassEvent.cs @@ -0,0 +1,15 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Packets.Enums.Character; + +namespace WingsEmu.Game.Characters.Events; + +public class ChangeClassEvent : PlayerEvent +{ + public ClassType NewClass { get; set; } + + public bool ShouldObtainNewFaction { get; set; } + + public bool ShouldObtainBasicItems { get; set; } + + public bool ShouldResetJobLevel { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Characters/Events/ChangeFactionEvent.cs b/srcs/WingsAPI.Game/Characters/Events/ChangeFactionEvent.cs new file mode 100644 index 0000000..57c4963 --- /dev/null +++ b/srcs/WingsAPI.Game/Characters/Events/ChangeFactionEvent.cs @@ -0,0 +1,9 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Characters.Events; + +public class ChangeFactionEvent : PlayerEvent +{ + public FactionType NewFaction { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Characters/Events/CharacterBonusExpiredEvent.cs b/srcs/WingsAPI.Game/Characters/Events/CharacterBonusExpiredEvent.cs new file mode 100644 index 0000000..2d792f4 --- /dev/null +++ b/srcs/WingsAPI.Game/Characters/Events/CharacterBonusExpiredEvent.cs @@ -0,0 +1,7 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Characters.Events; + +public class CharacterBonusExpiredEvent : PlayerEvent +{ +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Characters/Events/CharacterLoadEvent.cs b/srcs/WingsAPI.Game/Characters/Events/CharacterLoadEvent.cs new file mode 100644 index 0000000..112bbc6 --- /dev/null +++ b/srcs/WingsAPI.Game/Characters/Events/CharacterLoadEvent.cs @@ -0,0 +1,54 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Threading.Tasks; +using WingsEmu.Game._packetHandling; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Game.Characters.Events; + +public static class CharacterEventsExtensions +{ + public static async Task CharacterDisconnect(this IClientSession session) + { + await session.EmitEventAsync(new CharacterDisconnectedEvent + { + DisconnectionTime = DateTime.UtcNow + }); + } + + public static async Task RestAsync(this IClientSession session, bool restTeamMemberMates = false, bool force = false) + { + await session.EmitEventAsync(new PlayerRestEvent + { + RestTeamMemberMates = restTeamMemberMates, + Force = force + }); + } +} + +public class CharacterPreLoadEvent : PlayerEvent +{ + public CharacterPreLoadEvent(byte slot) => Slot = slot; + + public byte Slot { get; } +} + +public class CharacterLoadEvent : PlayerEvent +{ +} + +public class CharacterDisconnectedEvent : PlayerEvent +{ + public DateTime DisconnectionTime { get; set; } +} + +/// +/// Forces the save of A Character +/// Will be removed when we fully manage saveable data through microservices +/// +public class SessionSaveEvent : PlayerEvent +{ +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Characters/Events/CharacterRemoveManagersEvent.cs b/srcs/WingsAPI.Game/Characters/Events/CharacterRemoveManagersEvent.cs new file mode 100644 index 0000000..05e4150 --- /dev/null +++ b/srcs/WingsAPI.Game/Characters/Events/CharacterRemoveManagersEvent.cs @@ -0,0 +1,7 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Characters.Events; + +public class CharacterRemoveManagersEvent : PlayerEvent +{ +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Characters/Events/GamblingEvent.cs b/srcs/WingsAPI.Game/Characters/Events/GamblingEvent.cs new file mode 100644 index 0000000..642bf9e --- /dev/null +++ b/srcs/WingsAPI.Game/Characters/Events/GamblingEvent.cs @@ -0,0 +1,21 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Game.Inventory; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Characters.Events; + +public class GamblingEvent : PlayerEvent +{ + public GamblingEvent(InventoryItem item, InventoryItem amulet, RarifyMode mode, RarifyProtection protection) + { + Item = item; + Amulet = amulet; + Mode = mode; + Protection = protection; + } + + public RarifyProtection Protection { get; } + public RarifyMode Mode { get; } + public InventoryItem Item { get; } + public InventoryItem Amulet { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Characters/Events/GenerateGoldEvent.cs b/srcs/WingsAPI.Game/Characters/Events/GenerateGoldEvent.cs new file mode 100644 index 0000000..56e8845 --- /dev/null +++ b/srcs/WingsAPI.Game/Characters/Events/GenerateGoldEvent.cs @@ -0,0 +1,19 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Characters.Events; + +public class GenerateGoldEvent : PlayerEvent +{ + public GenerateGoldEvent(long amount, bool isQuest = false, bool sendMessage = true, bool fallBackToBank = false) + { + Amount = amount; + IsQuest = isQuest; + SendMessage = sendMessage; + FallBackToBank = fallBackToBank; + } + + public long Amount { get; } + public bool IsQuest { get; } + public bool SendMessage { get; } + public bool FallBackToBank { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Characters/Events/GenerateReputationEvent.cs b/srcs/WingsAPI.Game/Characters/Events/GenerateReputationEvent.cs new file mode 100644 index 0000000..31da88a --- /dev/null +++ b/srcs/WingsAPI.Game/Characters/Events/GenerateReputationEvent.cs @@ -0,0 +1,9 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Characters.Events; + +public class GenerateReputationEvent : PlayerEvent +{ + public long Amount { get; init; } + public bool SendMessage { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Characters/Events/GetDefaultMorphEvent.cs b/srcs/WingsAPI.Game/Characters/Events/GetDefaultMorphEvent.cs new file mode 100644 index 0000000..403b693 --- /dev/null +++ b/srcs/WingsAPI.Game/Characters/Events/GetDefaultMorphEvent.cs @@ -0,0 +1,7 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Characters.Events; + +public class GetDefaultMorphEvent : PlayerEvent +{ +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Characters/Events/InvitationEvent.cs b/srcs/WingsAPI.Game/Characters/Events/InvitationEvent.cs new file mode 100644 index 0000000..409f68c --- /dev/null +++ b/srcs/WingsAPI.Game/Characters/Events/InvitationEvent.cs @@ -0,0 +1,17 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Characters.Events; + +public class InvitationEvent : PlayerEvent +{ + public InvitationEvent(long targetCharacterId, InvitationType type) + { + TargetCharacterId = targetCharacterId; + Type = type; + } + + public long TargetCharacterId { get; } + + public InvitationType Type { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Characters/Events/InviteJoinMinilandEvent.cs b/srcs/WingsAPI.Game/Characters/Events/InviteJoinMinilandEvent.cs new file mode 100644 index 0000000..f918766 --- /dev/null +++ b/srcs/WingsAPI.Game/Characters/Events/InviteJoinMinilandEvent.cs @@ -0,0 +1,17 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Characters.Events; + +public class InviteJoinMinilandEvent : PlayerEvent +{ + public InviteJoinMinilandEvent(string target, bool isFirstStep, bool isByFriend = false) + { + Target = target; + IsFirstStep = isFirstStep; + IsByFriend = isByFriend; + } + + public string Target { get; } + public bool IsFirstStep { get; } + public bool IsByFriend { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Characters/Events/ItemGambledEvent.cs b/srcs/WingsAPI.Game/Characters/Events/ItemGambledEvent.cs new file mode 100644 index 0000000..664a0e8 --- /dev/null +++ b/srcs/WingsAPI.Game/Characters/Events/ItemGambledEvent.cs @@ -0,0 +1,15 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Characters.Events; + +public class ItemGambledEvent : PlayerEvent +{ + public int ItemVnum { get; init; } + public RarifyMode Mode { get; init; } + public RarifyProtection Protection { get; init; } + public int? Amulet { get; init; } + public bool Succeed { get; init; } + public short OriginalRarity { get; init; } + public short? FinalRarity { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Characters/Events/ItemSummedEvent.cs b/srcs/WingsAPI.Game/Characters/Events/ItemSummedEvent.cs new file mode 100644 index 0000000..0c1319b --- /dev/null +++ b/srcs/WingsAPI.Game/Characters/Events/ItemSummedEvent.cs @@ -0,0 +1,12 @@ +using WingsEmu.DTOs.Items; +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Characters.Events; + +public class ItemSummedEvent : PlayerEvent +{ + public ItemInstanceDTO LeftItem { get; init; } + public ItemInstanceDTO RightItem { get; init; } + public bool Succeed { get; init; } + public int SumLevel { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Characters/Events/ItemUpgradedEvent.cs b/srcs/WingsAPI.Game/Characters/Events/ItemUpgradedEvent.cs new file mode 100644 index 0000000..542766a --- /dev/null +++ b/srcs/WingsAPI.Game/Characters/Events/ItemUpgradedEvent.cs @@ -0,0 +1,17 @@ +using WingsAPI.Packets.Enums; +using WingsEmu.DTOs.Items; +using WingsEmu.Game._packetHandling; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Characters.Events; + +public class ItemUpgradedEvent : PlayerEvent +{ + public ItemInstanceDTO Item { get; init; } + public UpgradeMode Mode { get; init; } + public UpgradeProtection Protection { get; init; } + public bool HasAmulet { get; init; } + public short OriginalUpgrade { get; init; } + public UpgradeResult Result { get; init; } + public long TotalPrice { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Characters/Events/KillBonusEvent.cs b/srcs/WingsAPI.Game/Characters/Events/KillBonusEvent.cs new file mode 100644 index 0000000..278185d --- /dev/null +++ b/srcs/WingsAPI.Game/Characters/Events/KillBonusEvent.cs @@ -0,0 +1,9 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Game.Entities; + +namespace WingsEmu.Game.Characters.Events; + +public class KillBonusEvent : PlayerEvent +{ + public IMonsterEntity MonsterEntity { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Characters/Events/LevelUpEvent.cs b/srcs/WingsAPI.Game/Characters/Events/LevelUpEvent.cs new file mode 100644 index 0000000..3c60289 --- /dev/null +++ b/srcs/WingsAPI.Game/Characters/Events/LevelUpEvent.cs @@ -0,0 +1,10 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Characters.Events; + +public class LevelUpEvent : PlayerEvent +{ + public LevelType LevelType { get; init; } + public int? ItemVnum { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Characters/Events/LevelUpMateEvent.cs b/srcs/WingsAPI.Game/Characters/Events/LevelUpMateEvent.cs new file mode 100644 index 0000000..8e92530 --- /dev/null +++ b/srcs/WingsAPI.Game/Characters/Events/LevelUpMateEvent.cs @@ -0,0 +1,14 @@ +using WingsAPI.Packets.Enums; +using WingsEmu.Game._packetHandling; +using WingsEmu.Game.Helpers; + +namespace WingsEmu.Game.Characters.Events; + +public class LevelUpMateEvent : PlayerEvent +{ + public byte Level { get; init; } + public int NosMateMonsterVnum { get; init; } + public MateLevelUpType LevelUpType { get; init; } + public Location Location { get; init; } + public int? ItemVnum { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Characters/Events/MinilandSignPostJoinEvent.cs b/srcs/WingsAPI.Game/Characters/Events/MinilandSignPostJoinEvent.cs new file mode 100644 index 0000000..584036d --- /dev/null +++ b/srcs/WingsAPI.Game/Characters/Events/MinilandSignPostJoinEvent.cs @@ -0,0 +1,8 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Characters.Events; + +public class MinilandSignPostJoinEvent : PlayerEvent +{ + public long PlayerId { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Characters/Events/MonsterCaptureEvent.cs b/srcs/WingsAPI.Game/Characters/Events/MonsterCaptureEvent.cs new file mode 100644 index 0000000..6f80ff7 --- /dev/null +++ b/srcs/WingsAPI.Game/Characters/Events/MonsterCaptureEvent.cs @@ -0,0 +1,19 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Entities; + +namespace WingsEmu.Game.Characters.Events; + +public class MonsterCaptureEvent : PlayerEvent +{ + public MonsterCaptureEvent(IMonsterEntity target, bool isSkill, SkillInfo skill = null) + { + Target = target; + IsSkill = isSkill; + Skill = skill; + } + + public IMonsterEntity Target { get; } + public bool IsSkill { get; } + public SkillInfo Skill { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Characters/Events/NormalChatEvent.cs b/srcs/WingsAPI.Game/Characters/Events/NormalChatEvent.cs new file mode 100644 index 0000000..cefee43 --- /dev/null +++ b/srcs/WingsAPI.Game/Characters/Events/NormalChatEvent.cs @@ -0,0 +1,8 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Characters.Events; + +public class NormalChatEvent : PlayerEvent +{ + public string Message { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Characters/Events/PlayerChangeChannelAct4Event.cs b/srcs/WingsAPI.Game/Characters/Events/PlayerChangeChannelAct4Event.cs new file mode 100644 index 0000000..1f96292 --- /dev/null +++ b/srcs/WingsAPI.Game/Characters/Events/PlayerChangeChannelAct4Event.cs @@ -0,0 +1,7 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Characters.Events; + +public class PlayerChangeChannelAct4Event : PlayerEvent +{ +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Characters/Events/PlayerChangeChannelEvent.cs b/srcs/WingsAPI.Game/Characters/Events/PlayerChangeChannelEvent.cs new file mode 100644 index 0000000..548ca6b --- /dev/null +++ b/srcs/WingsAPI.Game/Characters/Events/PlayerChangeChannelEvent.cs @@ -0,0 +1,24 @@ +using WingsAPI.Communication.ServerApi.Protocol; +using WingsAPI.Packets.Enums; +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Characters.Events; + +public class PlayerChangeChannelEvent : PlayerEvent +{ + public PlayerChangeChannelEvent(SerializableGameServer gameServer, ItModeType modeType, int mapId, short mapX, short mapY) + { + GameServer = gameServer; + ModeType = modeType; + MapId = mapId; + MapX = mapX; + MapY = mapY; + } + + public SerializableGameServer GameServer { get; } + public ItModeType ModeType { get; } + + public int MapId { get; } + public short MapX { get; } + public short MapY { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Characters/Events/PlayerDeathEvent.cs b/srcs/WingsAPI.Game/Characters/Events/PlayerDeathEvent.cs new file mode 100644 index 0000000..4d104de --- /dev/null +++ b/srcs/WingsAPI.Game/Characters/Events/PlayerDeathEvent.cs @@ -0,0 +1,9 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Game.Entities; + +namespace WingsEmu.Game.Characters.Events; + +public class PlayerDeathEvent : PlayerEvent +{ + public IBattleEntity Killer { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Characters/Events/PlayerRestEvent.cs b/srcs/WingsAPI.Game/Characters/Events/PlayerRestEvent.cs new file mode 100644 index 0000000..c817c47 --- /dev/null +++ b/srcs/WingsAPI.Game/Characters/Events/PlayerRestEvent.cs @@ -0,0 +1,9 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Characters.Events; + +public class PlayerRestEvent : PlayerEvent +{ + public bool RestTeamMemberMates { get; set; } + public bool Force { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Characters/Events/PlayerReturnFromAct4Event.cs b/srcs/WingsAPI.Game/Characters/Events/PlayerReturnFromAct4Event.cs new file mode 100644 index 0000000..c76f613 --- /dev/null +++ b/srcs/WingsAPI.Game/Characters/Events/PlayerReturnFromAct4Event.cs @@ -0,0 +1,7 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Characters.Events; + +public class PlayerReturnFromAct4Event : PlayerEvent +{ +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Characters/Events/RemoveAdditionalHpMpEvent.cs b/srcs/WingsAPI.Game/Characters/Events/RemoveAdditionalHpMpEvent.cs new file mode 100644 index 0000000..eee5a6e --- /dev/null +++ b/srcs/WingsAPI.Game/Characters/Events/RemoveAdditionalHpMpEvent.cs @@ -0,0 +1,9 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Characters.Events; + +public class RemoveAdditionalHpMpEvent : PlayerEvent +{ + public int Hp { get; set; } + public int Mp { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Characters/Events/RemoveItemTimeEvent.cs b/srcs/WingsAPI.Game/Characters/Events/RemoveItemTimeEvent.cs new file mode 100644 index 0000000..fc51327 --- /dev/null +++ b/srcs/WingsAPI.Game/Characters/Events/RemoveItemTimeEvent.cs @@ -0,0 +1,7 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Characters.Events; + +public class InventoryExpiredItemsEvent : PlayerEvent +{ +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Characters/Events/RollItemBoxEvent.cs b/srcs/WingsAPI.Game/Characters/Events/RollItemBoxEvent.cs new file mode 100644 index 0000000..6f35370 --- /dev/null +++ b/srcs/WingsAPI.Game/Characters/Events/RollItemBoxEvent.cs @@ -0,0 +1,9 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Game.Inventory; + +namespace WingsEmu.Game.Characters.Events; + +public class RollItemBoxEvent : PlayerEvent +{ + public InventoryItem Item { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Characters/Events/ShellIdentifiedEvent.cs b/srcs/WingsAPI.Game/Characters/Events/ShellIdentifiedEvent.cs new file mode 100644 index 0000000..4f251ba --- /dev/null +++ b/srcs/WingsAPI.Game/Characters/Events/ShellIdentifiedEvent.cs @@ -0,0 +1,9 @@ +using WingsEmu.DTOs.Items; +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Characters.Events; + +public class ShellIdentifiedEvent : PlayerEvent +{ + public ItemInstanceDTO Shell { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Characters/Events/SpPerfectedEvent.cs b/srcs/WingsAPI.Game/Characters/Events/SpPerfectedEvent.cs new file mode 100644 index 0000000..ed60e64 --- /dev/null +++ b/srcs/WingsAPI.Game/Characters/Events/SpPerfectedEvent.cs @@ -0,0 +1,11 @@ +using WingsEmu.DTOs.Items; +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Characters.Events; + +public class SpPerfectedEvent : PlayerEvent +{ + public ItemInstanceDTO Sp { get; init; } + public bool Success { get; init; } + public byte SpPerfectionLevel { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Characters/Events/SpTransformEvent.cs b/srcs/WingsAPI.Game/Characters/Events/SpTransformEvent.cs new file mode 100644 index 0000000..5b25355 --- /dev/null +++ b/srcs/WingsAPI.Game/Characters/Events/SpTransformEvent.cs @@ -0,0 +1,9 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Game.Items; + +namespace WingsEmu.Game.Characters.Events; + +public class SpTransformEvent : PlayerEvent +{ + public GameItemInstance Specialist { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Characters/Events/SpUntransformEvent.cs b/srcs/WingsAPI.Game/Characters/Events/SpUntransformEvent.cs new file mode 100644 index 0000000..ae0ad9d --- /dev/null +++ b/srcs/WingsAPI.Game/Characters/Events/SpUntransformEvent.cs @@ -0,0 +1,7 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Characters.Events; + +public class SpUntransformEvent : PlayerEvent +{ +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Characters/Events/SpUpgradedEvent.cs b/srcs/WingsAPI.Game/Characters/Events/SpUpgradedEvent.cs new file mode 100644 index 0000000..4a0e3ad --- /dev/null +++ b/srcs/WingsAPI.Game/Characters/Events/SpUpgradedEvent.cs @@ -0,0 +1,15 @@ +using WingsAPI.Packets.Enums; +using WingsEmu.DTOs.Items; +using WingsEmu.Game._packetHandling; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Characters.Events; + +public class SpUpgradedEvent : PlayerEvent +{ + public ItemInstanceDTO Sp { get; init; } + public UpgradeMode UpgradeMode { get; init; } + public SpUpgradeResult UpgradeResult { get; init; } + public short OriginalUpgrade { get; init; } + public bool IsProtected { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Characters/Events/SpecialistRefreshEvent.cs b/srcs/WingsAPI.Game/Characters/Events/SpecialistRefreshEvent.cs new file mode 100644 index 0000000..493ca0f --- /dev/null +++ b/srcs/WingsAPI.Game/Characters/Events/SpecialistRefreshEvent.cs @@ -0,0 +1,8 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Characters.Events; + +public class SpecialistRefreshEvent : PlayerEvent +{ + public bool Force { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Characters/Events/SpeedBoosterEvent.cs b/srcs/WingsAPI.Game/Characters/Events/SpeedBoosterEvent.cs new file mode 100644 index 0000000..169eacd --- /dev/null +++ b/srcs/WingsAPI.Game/Characters/Events/SpeedBoosterEvent.cs @@ -0,0 +1,7 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Characters.Events; + +public class SpeedBoosterEvent : PlayerEvent +{ +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Characters/Events/StrangeBehaviorEvent.cs b/srcs/WingsAPI.Game/Characters/Events/StrangeBehaviorEvent.cs new file mode 100644 index 0000000..ff28be9 --- /dev/null +++ b/srcs/WingsAPI.Game/Characters/Events/StrangeBehaviorEvent.cs @@ -0,0 +1,29 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Threading.Tasks; +using WingsEmu.Game._packetHandling; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Game.Characters.Events; + +public static class PlayerStrangeBehaviorExtensions +{ + public static async Task NotifyStrangeBehavior(this IClientSession session, StrangeBehaviorSeverity severity, string reason) + { + await session.EmitEventAsync(new StrangeBehaviorEvent(severity, reason)); + } +} + +public class StrangeBehaviorEvent : PlayerEvent +{ + public StrangeBehaviorEvent(StrangeBehaviorSeverity severity, string reason) + { + Severity = severity; + Reason = reason; + } + + public StrangeBehaviorSeverity Severity { get; } + public string Reason { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Characters/Events/StrangeBehaviorSeverity.cs b/srcs/WingsAPI.Game/Characters/Events/StrangeBehaviorSeverity.cs new file mode 100644 index 0000000..8c4781e --- /dev/null +++ b/srcs/WingsAPI.Game/Characters/Events/StrangeBehaviorSeverity.cs @@ -0,0 +1,9 @@ +namespace WingsEmu.Game.Characters.Events; + +public enum StrangeBehaviorSeverity +{ + NORMAL, // harmless packet logger usage + ABUSING, // harmful packet logger usage + SEVERE_ABUSE, // trying to exploit without high game severity (teleporting...) + DANGER // trying to crash, dupe... +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Characters/Events/UpgradeItemEvent.cs b/srcs/WingsAPI.Game/Characters/Events/UpgradeItemEvent.cs new file mode 100644 index 0000000..8597870 --- /dev/null +++ b/srcs/WingsAPI.Game/Characters/Events/UpgradeItemEvent.cs @@ -0,0 +1,13 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Game.Inventory; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Characters.Events; + +public class UpgradeItemEvent : PlayerEvent +{ + public InventoryItem Inv { get; set; } + public UpgradeMode Mode { get; set; } + public UpgradeProtection Protection { get; set; } + public FixedUpMode HasAmulet { get; set; } = FixedUpMode.None; +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Characters/Events/VehicleCheckMapSpeedEvent.cs b/srcs/WingsAPI.Game/Characters/Events/VehicleCheckMapSpeedEvent.cs new file mode 100644 index 0000000..6d20870 --- /dev/null +++ b/srcs/WingsAPI.Game/Characters/Events/VehicleCheckMapSpeedEvent.cs @@ -0,0 +1,7 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Characters.Events; + +public class VehicleCheckMapSpeedEvent : PlayerEvent +{ +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Characters/Events/VehicleRemoveEvent.cs b/srcs/WingsAPI.Game/Characters/Events/VehicleRemoveEvent.cs new file mode 100644 index 0000000..ced5751 --- /dev/null +++ b/srcs/WingsAPI.Game/Characters/Events/VehicleRemoveEvent.cs @@ -0,0 +1,10 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Characters.Events; + +public class RemoveVehicleEvent : PlayerEvent +{ + public RemoveVehicleEvent(bool showMates = false) => ShowMates = showMates; + + public bool ShowMates { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Characters/IPlayerEntity.cs b/srcs/WingsAPI.Game/Characters/IPlayerEntity.cs new file mode 100644 index 0000000..5379fc2 --- /dev/null +++ b/srcs/WingsAPI.Game/Characters/IPlayerEntity.cs @@ -0,0 +1,250 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using WingsAPI.Data.Character; +using WingsAPI.Data.Miniland; +using WingsEmu.DTOs.Account; +using WingsEmu.DTOs.Bonus; +using WingsEmu.DTOs.Buffs; +using WingsEmu.DTOs.Inventory; +using WingsEmu.DTOs.Mates; +using WingsEmu.DTOs.Quests; +using WingsEmu.DTOs.Quicklist; +using WingsEmu.DTOs.Skills; +using WingsEmu.DTOs.Titles; +using WingsEmu.Game._enum; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Cheats; +using WingsEmu.Game.Entities; +using WingsEmu.Game.EntityStatistics; +using WingsEmu.Game.Exchange; +using WingsEmu.Game.Families; +using WingsEmu.Game.Groups; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Mails; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Quests; +using WingsEmu.Game.Quicklist; +using WingsEmu.Game.Raids; +using WingsEmu.Game.RainbowBattle; +using WingsEmu.Game.Relations; +using WingsEmu.Game.RespawnReturn; +using WingsEmu.Game.Revival; +using WingsEmu.Game.Shops; +using WingsEmu.Game.Skills; +using WingsEmu.Game.SnackFood; +using WingsEmu.Game.Specialists; +using WingsEmu.Game.TimeSpaces; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Character; + +namespace WingsEmu.Game.Characters; + +public interface IPlayerEntity : IBattleEntity, IEquipmentOptionContainer, IQuestContainer, ICharacterRevivalComponent, IFamilyComponent, IComboSkillComponent, ISkillCooldownComponent, + IAngelElementBuffComponent, IScoutComponent, IRelationComponent, IGroupComponent, IInventoryComponent, IExchangeComponent, IBubbleComponent, IPartnerInventoryComponent, + IRaidComponent, IFoodSnackComponent +{ + int AdditionalHp { get; set; } + int AdditionalMp { get; set; } + bool TriggerAmbush { get; set; } + DateTime LastMapChange { get; set; } + DateTime? LastSkillCombo { get; set; } + AuthorityType Authority { get; set; } + int CurrentMinigame { get; set; } + int SecondDamageMinimum { get; } + int SecondDamageMaximum { get; } + int HitRate { get; } + int HitCriticalChance { get; } + int HitCriticalDamage { get; } + int SecondHitRate { get; } + int SecondHitCriticalChance { get; } + int SecondHitCriticalDamage { get; } + int MeleeDefence { get; } + int RangedDefence { get; } + int MagicDefence { get; } + int MeleeDodge { get; } + int RangedDodge { get; } + int SpecialistElementRate { get; } + DateTime GameStartDate { get; set; } + bool HasShopOpened { get; set; } + bool Invisible { get; } + bool IsCustomSpeed { get; set; } + bool IsShopping { get; set; } + bool IsSitting { get; set; } + bool IsOnVehicle { get; set; } + bool IsMorphed { get; set; } + (VisualType, long) LastEntity { get; set; } + LastWalk LastWalk { get; set; } + int LastNRunId { get; set; } + int LastPulse { get; set; } + DateTime LastPulseTick { get; set; } + DateTime LastDefence { get; set; } + DateTime LastItemUpgrade { get; set; } + DateTime LastDeath { get; set; } + DateTime LastEffect { get; set; } + DateTime LastEffectMinigame { get; set; } + DateTime LastHealth { get; set; } + DateTime LastPortal { get; set; } + DateTime LastPotion { get; set; } + DateTime LastSnack { get; set; } + DateTime LastFood { get; set; } + DateTime LastSkillUse { get; set; } + DateTime LastSpeedChange { get; set; } + DateTime LastTransform { get; set; } + DateTime LastDayNight { get; set; } + DateTime? SpCooldownEnd { get; set; } + DateTime Bubble { get; set; } + DateTime SpyOutStart { get; set; } + DateTime ItemsToRemove { get; set; } + DateTime BonusesToRemove { get; set; } + DateTime? RandomMapTeleport { get; set; } + DateTime LastMove { get; set; } + DateTime LastPutItem { get; set; } + DateTime LastSentNote { get; set; } + DateTime? CheckWeedingBuff { get; set; } + DateTime LastPvPAttack { get; set; } + DateTime LastRainbowArrowEffect { get; set; } + DateTime LastRainbowEffects { get; set; } + Guid MapInstanceId { get; set; } + IMapInstance Miniland { get; set; } + int Morph { get; set; } + int MorphUpgrade { get; set; } + int MorphUpgrade2 { get; set; } + IClientSession Session { get; } + ConcurrentDictionary CharacterSkills { get; } + ConcurrentDictionary SkillsSp { get; set; } + ConcurrentDictionary HitsByMonsters { get; } + bool UseSp { get; set; } + byte VehicleSpeed { get; set; } + byte VehicleMapSpeed { get; set; } + int WareHouseSize { get; set; } + DateTime LastBuySearchBazaarRefresh { get; set; } + DateTime LastBuyBazaarRefresh { get; set; } + DateTime LastListItemBazaar { get; set; } + DateTime LastAdministrationBazaarRefresh { get; set; } + DateTime LastMonsterCaught { get; set; } + bool IsSeal { get; set; } + bool IsRemovingSpecialistPoints { get; set; } + bool IsWarehouseOpen { get; set; } + bool IsPartnerWarehouseOpen { get; set; } + bool IsCraftingItem { get; set; } + bool IsBankOpen { get; set; } + DateTime LastUnfreezedPlayer { get; set; } + DateTime LastSpPacketSent { get; set; } + DateTime LastSpRemovingProcess { get; set; } + DateTime LastAttack { get; set; } + bool InitialScpPacketSent { get; set; } + long AccountId { get; set; } + int Act4Dead { get; set; } + int Act4Kill { get; set; } + int Act4Points { get; set; } + int ArenaWinner { get; set; } + string Biography { get; set; } + bool BuffBlocked { get; set; } + bool ShowRaidDeathInfo { get; set; } + ClassType Class { get; set; } + short Compliment { get; set; } + float Dignity { get; set; } + bool EmoticonsBlocked { get; set; } + bool ExchangeBlocked { get; set; } + bool FamilyRequestBlocked { get; set; } + bool FriendRequestBlocked { get; set; } + GenderType Gender { get; set; } + long Gold { get; set; } + bool GroupRequestBlocked { get; set; } + HairColorType HairColor { get; set; } + HairStyleType HairStyle { get; set; } + bool HeroChatBlocked { get; set; } + byte HeroLevel { get; set; } + long HeroXp { get; set; } + bool HpBlocked { get; set; } + bool IsPetAutoRelive { get; set; } + bool IsPartnerAutoRelive { get; set; } + byte JobLevel { get; set; } + long JobLevelXp { get; set; } + long LevelXp { get; set; } + int MapId { get; set; } + short MapX { get; set; } + short MapY { get; set; } + int MasterPoints { get; set; } + int MasterTicket { get; set; } + byte MaxPetCount { get; set; } + byte MaxPartnerCount { get; set; } + bool MinilandInviteBlocked { get; set; } + string MinilandMessage { get; set; } + short MinilandPoint { get; set; } + MinilandState MinilandState { get; set; } + bool MouseAimLock { get; set; } + string Name { get; set; } + bool QuickGetUp { get; set; } + bool HideHat { get; set; } + bool UiBlocked { get; set; } + long Reput { get; set; } + byte Slot { get; set; } + int SpPointsBonus { get; set; } + int SpPointsBasic { get; set; } + int TalentLose { get; set; } + int TalentSurrender { get; set; } + int TalentWin { get; set; } + bool WhisperBlocked { get; set; } + int? LastMinilandProducedItem { get; set; } + bool IsGettingLosingReputation { get; set; } + byte DeathsOnAct4 { get; set; } + long ArenaKills { get; set; } + long ArenaDeaths { get; set; } + TimeSpan? MuteRemainingTime { get; set; } + DateTime LastMuteTick { get; set; } + DateTime LastSitting { get; set; } + DateTime? LastChatMuteMessage { get; set; } + DateTime LastInventorySort { get; set; } + DateTime? ArenaImmunity { get; set; } + List PartnerInventory { get; set; } + List NosMates { get; set; } + HashSet CompletedTimeSpaces { get; set; } + List PartnerWarehouse { get; set; } + List Bonus { get; set; } + List StaticBuffs { get; set; } + List Quicklist { get; set; } + List LearnedSkills { get; set; } + List Titles { get; set; } + List CompletedScripts { get; set; } + List CompletedQuests { get; set; } + List CompletedPeriodicQuests { get; set; } + List ActiveQuests { get; set; } + List MinilandObjects { get; set; } + List Inventory { get; set; } + List EquippedStuffs { get; set; } + CharacterLifetimeStatsDto LifetimeStats { get; set; } + CharacterRaidRestrictionDto RaidRestrictionDto { get; set; } + RainbowBattleLeaverBusterDto RainbowBattleLeaverBusterDto { get; set; } + + IQuicklistComponent QuicklistComponent { get; } + IMateComponent MateComponent { get; } + IHomeComponent HomeComponent { get; } + ISkillComponent SkillComponent { get; } + ICheatComponent CheatComponent { get; } + ISpecialistStatsComponent SpecialistComponent { get; } + IPlayerStatisticsComponent StatisticsComponent { get; } + ITimeSpaceComponent TimeSpaceComponent { get; } + IShopComponent ShopComponent { get; } + IMailNoteComponent MailNoteComponent { get; } + IRainbowBattleComponent RainbowBattleComponent { get; } + + + void RefreshCharacterStats(bool refreshHpMp = true); + void AddStaticBonus(CharacterStaticBonusDto bonus); + void AddStaticBonuses(IEnumerable bonuses); + IReadOnlyList GetStaticBonuses(); + CharacterStaticBonusDto GetStaticBonus(Predicate predicate); + int GetCp(); + int GetDignityIco(); + List GetExtraPortal(); + void SetSession(IClientSession clientSession); + int HealthHpLoad(); + int HealthMpLoad(); + bool HasBuff(BuffVnums buffVnum); + void SetFaction(FactionType faction); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Characters/IPlayerEntityFactory.cs b/srcs/WingsAPI.Game/Characters/IPlayerEntityFactory.cs new file mode 100644 index 0000000..5608ade --- /dev/null +++ b/srcs/WingsAPI.Game/Characters/IPlayerEntityFactory.cs @@ -0,0 +1,9 @@ +using WingsAPI.Data.Character; + +namespace WingsEmu.Game.Characters; + +public interface IPlayerEntityFactory +{ + public IPlayerEntity CreatePlayerEntity(CharacterDTO characterDto); + public CharacterDTO CreateCharacterDto(IPlayerEntity playerEntity); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Characters/LastWalk.cs b/srcs/WingsAPI.Game/Characters/LastWalk.cs new file mode 100644 index 0000000..2f6a95e --- /dev/null +++ b/srcs/WingsAPI.Game/Characters/LastWalk.cs @@ -0,0 +1,13 @@ +using System; +using WingsEmu.Game.Helpers.Damages; + +namespace WingsEmu.Game.Characters; + +public class LastWalk +{ + public DateTime WalkTimeStart { get; set; } + public Position StartPosition { get; set; } + public DateTime WalkTimeEnd { get; set; } + public Position EndPosition { get; set; } + public int? MapId { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Characters/PlayerEntity.Family.cs b/srcs/WingsAPI.Game/Characters/PlayerEntity.Family.cs new file mode 100644 index 0000000..309073f --- /dev/null +++ b/srcs/WingsAPI.Game/Characters/PlayerEntity.Family.cs @@ -0,0 +1,39 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using WingsEmu.Game.Families; +using WingsEmu.Packets.Enums.Families; + +namespace WingsEmu.Game.Characters; + +public partial class PlayerEntity +{ + private readonly IFamilyComponent _familyComponent; + + public bool IsFamilyWarehouseOpen + { + get => _familyComponent.IsFamilyWarehouseOpen; + set => _familyComponent.IsFamilyWarehouseOpen = value; + } + + public bool IsFamilyWarehouseLogsOpen + { + get => _familyComponent.IsFamilyWarehouseLogsOpen; + set => _familyComponent.IsFamilyWarehouseLogsOpen = value; + } + + public IFamily Family => _familyComponent.Family; + public FamilyMembership FamilyMembership => _familyComponent.FamilyMembership; + + public void SetFamilyMembership(FamilyMembership membership) => _familyComponent.SetFamilyMembership(membership); + + public bool IsHeadOfFamily() => _familyComponent.IsHeadOfFamily(); + public bool IsInFamily() => _familyComponent.IsInFamily(); + public List GetFamilyMembers() => _familyComponent.GetFamilyMembers(); + public FamilyAuthority GetFamilyAuthority() => _familyComponent.GetFamilyAuthority(); + public FamilyMembership GetMembershipByAuthority(FamilyAuthority familyAuthority) => _familyComponent.GetMembershipByAuthority(familyAuthority); + public FamilyMembership GetMembershipById(long id) => _familyComponent.GetMembershipById(id); + public byte GetAmountOfMembersByType(FamilyAuthority type) => _familyComponent.GetAmountOfMembersByType(type); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Characters/PlayerEntity.Revival.cs b/srcs/WingsAPI.Game/Characters/PlayerEntity.Revival.cs new file mode 100644 index 0000000..9803418 --- /dev/null +++ b/srcs/WingsAPI.Game/Characters/PlayerEntity.Revival.cs @@ -0,0 +1,46 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using WingsEmu.Game.Revival; + +namespace WingsEmu.Game.Characters; + +/// +/// Revival +/// +public partial class PlayerEntity +{ + private readonly CharacterRevivalComponent _characterRevivalComponent; + + public DateTime RevivalDateTimeForExecution => _characterRevivalComponent.RevivalDateTimeForExecution; + + public RevivalType RevivalType => _characterRevivalComponent.RevivalType; + + public ForcedType ForcedType => _characterRevivalComponent.ForcedType; + + public DateTime AskRevivalDateTimeForExecution => _characterRevivalComponent.AskRevivalDateTimeForExecution; + + public AskRevivalType AskRevivalType => _characterRevivalComponent.AskRevivalType; + + public void UpdateRevival(DateTime revivalDateTimeForExecution, RevivalType revivalType, ForcedType forcedType) + { + _characterRevivalComponent.UpdateRevival(revivalDateTimeForExecution, revivalType, forcedType); + } + + public void DisableRevival() + { + _characterRevivalComponent.DisableRevival(); + } + + public void UpdateAskRevival(DateTime askRevivalDateTimeForExecution, AskRevivalType askRevivalType) + { + _characterRevivalComponent.UpdateAskRevival(askRevivalDateTimeForExecution, askRevivalType); + } + + public void DisableAskRevival() + { + _characterRevivalComponent.DisableAskRevival(); + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Characters/PlayerEntity.Skills.cs b/srcs/WingsAPI.Game/Characters/PlayerEntity.Skills.cs new file mode 100644 index 0000000..616d77d --- /dev/null +++ b/srcs/WingsAPI.Game/Characters/PlayerEntity.Skills.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Skills; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Characters; + +public partial class PlayerEntity +{ + private readonly IAngelElementBuffComponent _angelElementBuffComponent; + private readonly ICastingComponent _castingComponent; + private readonly IComboSkillComponent _comboSkillComponent; + private readonly IEndBuffDamageComponent _endBuffDamageComponent; + private readonly IScoutComponent _scoutComponent; + private readonly ISkillCooldownComponent _skillCooldownComponent; + public IWildKeeperComponent WildKeeperComponent { get; } + public ISkillComponent SkillComponent { get; } + + public void SaveComboSkill(ComboSkillState comboSkillState) => _comboSkillComponent.SaveComboSkill(comboSkillState); + + public ComboSkillState GetComboState() => _comboSkillComponent.GetComboState(); + + public void IncreaseComboState(byte castId) => _comboSkillComponent.IncreaseComboState(castId); + + public void CleanComboState() => _comboSkillComponent.CleanComboState(); + + public SkillCast SkillCast => _castingComponent.SkillCast; + + public bool IsCastingSkill => _castingComponent.IsCastingSkill; + + public void SetCastingSkill(SkillInfo skill, DateTime time) + { + _castingComponent.SetCastingSkill(skill, time); + } + + public void RemoveCastingSkill() + { + _castingComponent.RemoveCastingSkill(); + } + + public ConcurrentQueue<(DateTime time, short castId)> SkillCooldowns => _skillCooldownComponent.SkillCooldowns; + + public ConcurrentQueue<(DateTime time, short castId, MateType mateType)> MatesSkillCooldowns => _skillCooldownComponent.MatesSkillCooldowns; + + public void AddSkillCooldown(DateTime time, short castId) + { + _skillCooldownComponent.AddSkillCooldown(time, castId); + } + + public void ClearSkillCooldowns() + { + _skillCooldownComponent.ClearSkillCooldowns(); + } + + public void AddMateSkillCooldown(DateTime time, short castId, MateType mateType) + { + _skillCooldownComponent.AddMateSkillCooldown(time, castId, mateType); + } + + public void ClearMateSkillCooldowns() + { + _skillCooldownComponent.ClearMateSkillCooldowns(); + } + + public ElementType? AngelElement => _angelElementBuffComponent.AngelElement; + + public void AddAngelElement(ElementType elementType) + { + _angelElementBuffComponent.AddAngelElement(elementType); + } + + public void RemoveAngelElement() + { + _angelElementBuffComponent.RemoveAngelElement(); + } + + public IReadOnlyDictionary EndBuffDamages => _endBuffDamageComponent.EndBuffDamages; + + public void AddEndBuff(short buffVnum, int damage) + { + _endBuffDamageComponent.AddEndBuff(buffVnum, damage); + } + + public int DecreaseDamageEndBuff(short buffVnum, int damage) => _endBuffDamageComponent.DecreaseDamageEndBuff(buffVnum, damage); + + public void RemoveEndBuffDamage(short buffVnum) + { + _endBuffDamageComponent.RemoveEndBuffDamage(buffVnum); + } + + public ScoutStateType ScoutStateType => _scoutComponent.ScoutStateType; + + public void ChangeScoutState(ScoutStateType stateType) + { + _scoutComponent.ChangeScoutState(stateType); + } + + public void SetCharge(int chargeValue) + { + _chargeComponent.SetCharge(chargeValue); + } + + public int GetCharge() => _chargeComponent.GetCharge(); + + public void ResetCharge() => _chargeComponent.ResetCharge(); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Characters/PlayerEntity.Stats.cs b/srcs/WingsAPI.Game/Characters/PlayerEntity.Stats.cs new file mode 100644 index 0000000..fcd98c4 --- /dev/null +++ b/srcs/WingsAPI.Game/Characters/PlayerEntity.Stats.cs @@ -0,0 +1,377 @@ +using System; +using System.Collections.Generic; +using WingsAPI.Packets.Enums.Shells; +using WingsEmu.DTOs.Items; +using WingsEmu.DTOs.Quests; +using WingsEmu.DTOs.Relations; +using WingsEmu.Game._enum; +using WingsEmu.Game.Battle.Managers; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Exchange; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Groups; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Items; +using WingsEmu.Game.Mails; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Quests; +using WingsEmu.Game.Raids; +using WingsEmu.Game.Relations; +using WingsEmu.Game.Shops; +using WingsEmu.Game.SnackFood; +using WingsEmu.Game.TimeSpaces; +using WingsEmu.Game.Warehouse; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Relations; + +namespace WingsEmu.Game.Characters; + +public partial class PlayerEntity +{ + private readonly IChargeComponent _chargeComponent; + private readonly IInventoryComponent _inventory; + + private readonly IRaidComponent _raidComponent; + private readonly IBubbleComponent _bubbleComponent; + private int _criticalChance; + private int _criticalDamage; + private readonly IEquipmentOptionContainer _eqOptions; + private readonly IEventTriggerContainer _eventTriggerContainer; + private readonly IExchangeComponent _exchange; + private readonly IFoodSnackComponent _foodSnackComponent; + private readonly IGroupComponent _groupComponent; + private int _magicDamageMax; + private int _magicDamageMin; + private int _magicDefense; + private int _meleeDamageMax; + private int _meleeDamageMin; + private int _meleeDefense; + private int _meleeDodge; + private int _meleeHitRate; + private readonly IPartnerInventoryComponent _partnerInventory; + private readonly IQuestContainer _questContainer; + private int _rangedDamageMax; + private int _rangedDamageMin; + private int _rangedDefense; + private int _rangedDodge; + private int _rangedHitRate; + private readonly IRelationComponent _relationComponent; + private byte _speed; + + public void RefreshCharacterStats(bool refreshHpMp = true) + { + if (refreshHpMp) + { + MaxHp = this.GetMaxHp(_algorithm.GetBasicHpByClass(Class, Level)); + MaxMp = this.GetMaxMp(_algorithm.GetBasicMpByClass(Class, Level)); + } + + _speed = !IsCustomSpeed ? this.GetSpeed(_algorithm.GetSpeed(Class)) : _speed; + _meleeDamageMin = _algorithm.GetBaseStatistic(Level, Class, StatisticType.ATTACK_MELEE); + _meleeDamageMax = _algorithm.GetBaseStatistic(Level, Class, StatisticType.ATTACK_MELEE); + _rangedDamageMin = _algorithm.GetBaseStatistic(Level, Class, StatisticType.ATTACK_RANGED); + _rangedDamageMax = _algorithm.GetBaseStatistic(Level, Class, StatisticType.ATTACK_RANGED); + _magicDamageMin = _algorithm.GetBaseStatistic(Level, Class, StatisticType.ATTACK_MAGIC); + _magicDamageMax = _algorithm.GetBaseStatistic(Level, Class, StatisticType.ATTACK_MAGIC); + _meleeHitRate = _algorithm.GetBaseStatistic(Level, Class, StatisticType.HITRATE_MELEE); + _rangedHitRate = _algorithm.GetBaseStatistic(Level, Class, StatisticType.HITRATE_RANGED); + _criticalChance = 0; + _criticalDamage = 0; + _meleeDefense = _algorithm.GetBaseStatistic(Level, Class, StatisticType.DEFENSE_MELEE); + _rangedDefense = _algorithm.GetBaseStatistic(Level, Class, StatisticType.DEFENSE_RANGED); + _magicDefense = _algorithm.GetBaseStatistic(Level, Class, StatisticType.DEFENSE_MAGIC); + _meleeDodge = _algorithm.GetBaseStatistic(Level, Class, StatisticType.DODGE_MELEE); + ; + _rangedDodge = _algorithm.GetBaseStatistic(Level, Class, StatisticType.DODGE_RANGED); + StatisticsComponent.RefreshPlayerStatistics(); + } + + public ITimeSpaceComponent TimeSpaceComponent { get; } + public IBCardComponent BCardComponent { get; } + public IMateComponent MateComponent { get; } + public IBuffComponent BuffComponent { get; } + public IShopComponent ShopComponent { get; } + public IMailNoteComponent MailNoteComponent { get; } + + public bool HasQuestWithQuestType(QuestType questType) => _questContainer.HasQuestWithQuestType(questType); + public bool HasQuestWithId(int questId) => _questContainer.HasQuestWithId(questId); + public bool HasCompletedScriptByIndex(int scriptId, int scriptIndex) => _questContainer.HasCompletedScriptByIndex(scriptId, scriptIndex); + + public IEnumerable GetCurrentQuests() => _questContainer.GetCurrentQuests(); + public IEnumerable GetCompletedQuests() => _questContainer.GetCompletedQuests(); + public IEnumerable GetCompletedPeriodicQuests() => _questContainer.GetCompletedPeriodicQuests(); + public IEnumerable GetCurrentQuestsByType(QuestType questType) => _questContainer.GetCurrentQuestsByType(questType); + public IEnumerable GetCurrentQuestsByTypes(IReadOnlyCollection questTypes) => _questContainer.GetCurrentQuestsByTypes(questTypes); + public IEnumerable GetQuestsProgress() => _questContainer.GetQuestsProgress(); + + public CharacterQuest GetCurrentQuest(int questId) => _questContainer.GetCurrentQuest(questId); + + public void AddActiveQuest(CharacterQuest quest) + { + _questContainer.AddActiveQuest(quest); + } + + public void RemoveActiveQuest(int questId) + { + _questContainer.RemoveActiveQuest(questId); + } + + public void AddCompletedQuest(CharacterQuest quest) + { + _questContainer.AddCompletedQuest(quest); + } + + public void AddCompletedPeriodicQuest(CharacterQuest quest) + { + _questContainer.AddCompletedPeriodicQuest(quest); + } + + public IEnumerable GetCompletedScripts() => _questContainer.GetCompletedScripts(); + public IEnumerable GetCompletedScriptsByType(TutorialActionType scriptType) => _questContainer.GetCompletedScriptsByType(scriptType); + + public void SaveScript(int scriptId, int scriptIndex, TutorialActionType scriptType, DateTime savingDate) => _questContainer.SaveScript(scriptId, scriptIndex, scriptType, savingDate); + + public CompletedScriptsDto GetLastCompletedScript() => _questContainer.GetLastCompletedScript(); + public CompletedScriptsDto GetLastCompletedScriptByType(TutorialActionType scriptType) => _questContainer.GetLastCompletedScriptByType(scriptType); + + public void IncreasePendingSoundFlowerQuests() => _questContainer.IncreasePendingSoundFlowerQuests(); + public void DecreasePendingSoundFlowerQuests() => _questContainer.DecreasePendingSoundFlowerQuests(); + public int GetPendingSoundFlowerQuests() => _questContainer.GetPendingSoundFlowerQuests(); + + public IReadOnlyList GetRelations() => _relationComponent.GetRelations(); + + public IEnumerable GetFriendRelations() => _relationComponent.GetFriendRelations(); + + public IEnumerable GetBlockedRelations() => _relationComponent.GetBlockedRelations(); + + public bool IsBlocking(long targetId) => _relationComponent.IsBlocking(targetId); + public bool IsFriend(long targetId) => _relationComponent.IsFriend(targetId); + public bool IsMarried(long targetId) => _relationComponent.IsMarried(targetId); + public bool IsFriendsListFull() => _relationComponent.IsFriendsListFull(); + + public void AddRelation(CharacterRelationDTO relation) + { + _relationComponent.AddRelation(relation); + } + + public void RemoveRelation(long targetCharacterId, CharacterRelationType relationType) + { + _relationComponent.RemoveRelation(targetCharacterId, relationType); + } + + public long GetGroupId() => _groupComponent.GetGroupId(); + public PlayerGroup GetGroup() => _groupComponent.GetGroup(); + + public void AddMember(IPlayerEntity member) => _groupComponent.AddMember(member); + + public void RemoveMember(IPlayerEntity member) => _groupComponent.RemoveMember(member); + + public void SetGroup(PlayerGroup playerGroup) => _groupComponent.SetGroup(playerGroup); + + public void RemoveGroup() => _groupComponent.RemoveGroup(); + + public bool IsInGroup() => _groupComponent.IsInGroup(); + + public bool IsLeaderOfGroup(long characterId) => _groupComponent.IsLeaderOfGroup(characterId); + + public bool IsGroupFull() => _groupComponent.IsGroupFull(); + + + public IEnumerable GetAllPlayerInventoryItems() => _inventory.GetAllPlayerInventoryItems(); + + public InventoryItem GetFirstItemByVnum(int vnum) => _inventory.GetFirstItemByVnum(vnum); + + public IEnumerable GetItemsByInventoryType(InventoryType type) => _inventory.GetItemsByInventoryType(type); + + public IEnumerable EquippedItems => _inventory.EquippedItems; + public GameItemInstance GetItemInstanceFromEquipmentSlot(EquipmentType type) => _inventory.GetItemInstanceFromEquipmentSlot(type); + + public GameItemInstance MainWeapon => _inventory.MainWeapon; + + public GameItemInstance SecondaryWeapon => _inventory.SecondaryWeapon; + + public GameItemInstance Armor => _inventory.Armor; + + public GameItemInstance Amulet => _inventory.Amulet; + + public GameItemInstance Hat => _inventory.Hat; + + public GameItemInstance Gloves => _inventory.Gloves; + + public GameItemInstance Ring => _inventory.Ring; + + public GameItemInstance Necklace => _inventory.Necklace; + + public GameItemInstance Bracelet => _inventory.Bracelet; + + public GameItemInstance Boots => _inventory.Boots; + + public GameItemInstance Fairy => _inventory.Fairy; + + public GameItemInstance Specialist => _inventory.Specialist; + + public GameItemInstance Mask => _inventory.Mask; + + public GameItemInstance CostumeSuit => _inventory.CostumeSuit; + + public GameItemInstance CostumeHat => _inventory.CostumeHat; + + public GameItemInstance WeaponSkin => _inventory.WeaponSkin; + + public GameItemInstance Wings => _inventory.Wings; + + public bool InventoryIsInitialized => _inventory.InventoryIsInitialized; + + public int CountItemWithVnum(int vnum) => _inventory.CountItemWithVnum(vnum); + + public bool HasSpaceFor(int vnum, short amount = 1) => _inventory.HasSpaceFor(vnum, amount); + + public void AddItemToInventory(InventoryItem inventoryItem) + { + _inventory.AddItemToInventory(inventoryItem); + } + + public bool HasItem(int vnum, short amount = 1) => _inventory.HasItem(vnum, amount); + + public bool RemoveItemFromSlotAndType(short slot, InventoryType type, out InventoryItem removedItem) => _inventory.RemoveItemFromSlotAndType(slot, type, out removedItem); + public bool RemoveItemAmountByVnum(int vnum, short amount, out InventoryItem removedItem) => _inventory.RemoveItemAmountByVnum(vnum, amount, out removedItem); + + public void EquipItem(InventoryItem item, EquipmentType type, bool force = false) => _inventory.EquipItem(item, type, force); + + public void TakeOffItem(EquipmentType type, short? slot = null, InventoryType? inventoryType = null) => _inventory.TakeOffItem(type, slot, inventoryType); + + public InventoryItem GetInventoryItemFromEquipmentSlot(EquipmentType type) => _inventory.GetInventoryItemFromEquipmentSlot(type); + + public InventoryItem GetItemBySlotAndType(short slot, InventoryType type) => _inventory.GetItemBySlotAndType(slot, type); + + public InventoryItem FindItemWithoutFullStack(int vnum, short amount) => _inventory.FindItemWithoutFullStack(vnum, amount); + + public void AddShells(EquipmentOptionType equipmentOptionType, List optionDto, bool isMainWeapon) + { + _eqOptions.AddShells(equipmentOptionType, optionDto, isMainWeapon); + } + + public void ClearShells(EquipmentOptionType equipmentOptionType, bool isMainWeapon) + { + _eqOptions.ClearShells(equipmentOptionType, isMainWeapon); + } + + public Dictionary GetShellsValues(EquipmentOptionType equipmentOptionType, bool isMainWeapon) => _eqOptions.GetShellsValues(equipmentOptionType, isMainWeapon); + + public int GetMaxWeaponShellValue(ShellEffectType shellEffectType, bool isMainWeapon) => _eqOptions.GetMaxWeaponShellValue(shellEffectType, isMainWeapon); + + public int GetMaxArmorShellValue(ShellEffectType shellEffectType) => _eqOptions.GetMaxArmorShellValue(shellEffectType); + + public void AddCellon(EquipmentType equipmentType, List optionDto) => _eqOptions.AddCellon(equipmentType, optionDto); + + public void ClearCellon(EquipmentType equipmentType) => _eqOptions.ClearCellon(equipmentType); + + public Dictionary GetCellonValues(EquipmentType equipmentType) => _eqOptions.GetCellonValues(equipmentType); + + public int GetCellonValue(EquipmentType equipmentType, CellonType type) => _eqOptions.GetCellonValue(equipmentType, type); + + public void SetExchange(PlayerExchange exchange) => _exchange.SetExchange(exchange); + + public void RemoveExchange() => _exchange.RemoveExchange(); + + public PlayerExchange GetExchange() => _exchange.GetExchange(); + + public bool IsInExchange() => _exchange.IsInExchange(); + + public long GetTargetId() => _exchange.GetTargetId(); + + public void SaveBubble(string message) => _bubbleComponent.SaveBubble(message); + + public bool IsUsingBubble() => _bubbleComponent.IsUsingBubble(); + + public string GetMessage() => _bubbleComponent.GetMessage(); + + public void RemoveBubble() => _bubbleComponent.RemoveBubble(); + + public IReadOnlyList PartnerGetEquippedItems(short partnerSlot) => _partnerInventory.PartnerGetEquippedItems(partnerSlot); + + public IReadOnlyList GetPartnersEquippedItems() => _partnerInventory.GetPartnersEquippedItems(); + + public void PartnerEquipItem(InventoryItem item, short partnerSlot) => _partnerInventory.PartnerEquipItem(item, partnerSlot); + + public void PartnerEquipItem(GameItemInstance item, short partnerSlot) => _partnerInventory.PartnerEquipItem(item, partnerSlot); + + public void PartnerTakeOffItem(EquipmentType type, short partnerSlot) => _partnerInventory.PartnerTakeOffItem(type, partnerSlot); + + public PartnerInventoryItem PartnerGetEquippedItem(EquipmentType type, short partnerSlot) => _partnerInventory.PartnerGetEquippedItem(type, partnerSlot); + + public void AddPartnerWarehouseItem(GameItemInstance item, short slot) => _partnerInventory.AddPartnerWarehouseItem(item, slot); + + public void RemovePartnerWarehouseItem(short slot) => _partnerInventory.RemovePartnerWarehouseItem(slot); + + public PartnerWarehouseItem GetPartnerWarehouseItem(short slot) => _partnerInventory.GetPartnerWarehouseItem(slot); + + public IReadOnlyList PartnerWarehouseItems() => _partnerInventory.PartnerWarehouseItems(); + + public byte GetPartnerWarehouseSlots() => _partnerInventory.GetPartnerWarehouseSlots(); + + public byte GetPartnerWarehouseSlotsWithoutBackpack() => _partnerInventory.GetPartnerWarehouseSlotsWithoutBackpack(); + + public bool HasSpaceForPartnerWarehouseItem() => _partnerInventory.HasSpaceForPartnerWarehouseItem(); + public bool HasSpaceForPartnerItemWarehouse(int itemVnum, short amount = 1) => _partnerInventory.HasSpaceForPartnerItemWarehouse(itemVnum, amount); + + public byte RaidDeaths => _raidComponent.RaidDeaths; + + public bool IsInRaidParty => _raidComponent.IsInRaidParty; + + public bool HasRaidStarted => _raidComponent.HasRaidStarted; + + public RaidParty Raid => _raidComponent.Raid; + + public bool IsRaidLeader(long characterId) => _raidComponent.IsRaidLeader(characterId); + + public bool RaidTeamIsFull => _raidComponent.RaidTeamIsFull; + + public void SetRaidParty(RaidParty raidParty) + { + _raidComponent.SetRaidParty(raidParty); + } + + public void AddRaidDeath() + { + _raidComponent.AddRaidDeath(); + } + + public void RemoveRaidDeath() + { + _raidComponent.RemoveRaidDeath(); + } + + public FoodProgress GetFoodProgress => _foodSnackComponent.GetFoodProgress; + public SnackProgress GetSnackProgress => _foodSnackComponent.GetSnackProgress; + public AdditionalFoodProgress GetAdditionalFoodProgress => _foodSnackComponent.GetAdditionalFoodProgress; + public AdditionalSnackProgress GetAdditionalSnackProgress => _foodSnackComponent.GetAdditionalSnackProgress; + + public bool AddSnack(IGameItem gameItem) => _foodSnackComponent.AddSnack(gameItem); + + public void AddAdditionalSnack(int max, int amount, bool isHp, int cap = 100) + { + _foodSnackComponent.AddAdditionalSnack(max, amount, isHp, cap); + } + + public bool AddFood(IGameItem gameItem) => _foodSnackComponent.AddFood(gameItem); + + public void AddAdditionalFood(int max, int amount, bool isHp, int cap = 100) + { + _foodSnackComponent.AddAdditionalFood(max, amount, isHp, cap); + } + + public void ClearFoodBuffer() + { + _foodSnackComponent.ClearFoodBuffer(); + } + + public void ClearSnackBuffer() + { + _foodSnackComponent.ClearSnackBuffer(); + } + + public bool IsWarehouseOpen { get; set; } + public bool IsPartnerWarehouseOpen { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Characters/PlayerEntity.cs b/srcs/WingsAPI.Game/Characters/PlayerEntity.cs new file mode 100644 index 0000000..c6d4f3b --- /dev/null +++ b/srcs/WingsAPI.Game/Characters/PlayerEntity.cs @@ -0,0 +1,856 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Data.Character; +using WingsAPI.Data.Miniland; +using WingsEmu.Core.Generics; +using WingsEmu.DTOs.Account; +using WingsEmu.DTOs.Bonus; +using WingsEmu.DTOs.Buffs; +using WingsEmu.DTOs.Inventory; +using WingsEmu.DTOs.Mates; +using WingsEmu.DTOs.Quests; +using WingsEmu.DTOs.Quicklist; +using WingsEmu.DTOs.Skills; +using WingsEmu.DTOs.Titles; +using WingsEmu.Game._enum; +using WingsEmu.Game.Algorithm; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Battle.Managers; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Cheats; +using WingsEmu.Game.Entities; +using WingsEmu.Game.EntityStatistics; +using WingsEmu.Game.Exchange; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Families; +using WingsEmu.Game.Groups; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Mails; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Portals; +using WingsEmu.Game.Quests; +using WingsEmu.Game.Quicklist; +using WingsEmu.Game.Raids; +using WingsEmu.Game.RainbowBattle; +using WingsEmu.Game.Relations; +using WingsEmu.Game.RespawnReturn; +using WingsEmu.Game.Revival; +using WingsEmu.Game.Shops; +using WingsEmu.Game.Skills; +using WingsEmu.Game.SnackFood; +using WingsEmu.Game.Specialists; +using WingsEmu.Game.TimeSpaces; +using WingsEmu.Game.Triggers; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Character; + +namespace WingsEmu.Game.Characters; + +public partial class PlayerEntity : IPlayerEntity +{ + private readonly IBattleEntityAlgorithmService _algorithm; + private readonly ICharacterAlgorithm _characterAlgorithm; + private readonly IAsyncEventPipeline _eventPipeline; + private readonly IFamilyManager _familyManager; + private readonly IFoodSnackComponentFactory _foodSnackComponentFactory; + private readonly IMapManager _mapManager; + private readonly IPortalFactory _portalFactory; + private readonly IRandomGenerator _randomGenerator; + + public PlayerEntity(CharacterDTO characterDto, IFamilyManager familyManager, IRandomGenerator randomGenerator, IMapManager mapManager, ICharacterAlgorithm characterAlgorithm, + IFoodSnackComponentFactory foodSnackComponentFactory, IAsyncEventPipeline eventPipeline, IBattleEntityAlgorithmService algorithm, IPortalFactory portalFactory, IItemsManager itemsManager) + { + Id = (int)characterDto.Id; + Hp = characterDto.Hp; + Mp = characterDto.Mp; + Level = characterDto.Level; + Faction = characterDto.Faction; + AccountId = characterDto.AccountId; + Act4Dead = characterDto.Act4Dead; + Act4Kill = characterDto.Act4Kill; + Act4Points = characterDto.Act4Points; + ArenaWinner = characterDto.ArenaWinner; + Biography = characterDto.Biography; + BuffBlocked = characterDto.BuffBlocked; + Class = characterDto.Class; + Compliment = characterDto.Compliment; + Dignity = characterDto.Dignity; + EmoticonsBlocked = characterDto.EmoticonsBlocked; + ExchangeBlocked = characterDto.ExchangeBlocked; + FamilyRequestBlocked = characterDto.FamilyRequestBlocked; + FriendRequestBlocked = characterDto.FriendRequestBlocked; + Gender = characterDto.Gender; + Gold = characterDto.Gold; + GroupRequestBlocked = characterDto.GroupRequestBlocked; + HairColor = characterDto.HairColor; + HairStyle = characterDto.HairStyle; + HeroChatBlocked = characterDto.HeroChatBlocked; + HeroLevel = characterDto.HeroLevel; + HeroXp = characterDto.HeroXp; + HpBlocked = characterDto.HpBlocked; + IsPetAutoRelive = characterDto.IsPetAutoRelive; + IsPartnerAutoRelive = characterDto.IsPartnerAutoRelive; + JobLevel = characterDto.JobLevel; + JobLevelXp = characterDto.JobLevelXp; + LevelXp = characterDto.LevelXp; + MapId = characterDto.MapId; + MapX = characterDto.MapX; + MapY = characterDto.MapY; + MasterPoints = characterDto.MasterPoints; + MasterTicket = characterDto.MasterTicket; + MaxPetCount = characterDto.MaxPetCount; + MaxPartnerCount = characterDto.MaxPartnerCount; + MinilandInviteBlocked = characterDto.MinilandInviteBlocked; + MinilandMessage = characterDto.MinilandMessage; + MinilandPoint = characterDto.MinilandPoint; + MinilandState = characterDto.MinilandState; + MouseAimLock = characterDto.MouseAimLock; + Name = characterDto.Name; + QuickGetUp = characterDto.QuickGetUp; + HideHat = characterDto.HideHat; + UiBlocked = characterDto.UiBlocked; + Reput = characterDto.Reput; + Slot = characterDto.Slot; + SpPointsBonus = characterDto.SpPointsBonus; + SpPointsBasic = characterDto.SpPointsBasic; + TalentLose = characterDto.TalentLose; + TalentSurrender = characterDto.TalentSurrender; + TalentWin = characterDto.TalentWin; + WhisperBlocked = characterDto.WhisperBlocked; + PartnerInventory = characterDto.PartnerInventory; + NosMates = characterDto.NosMates; + PartnerWarehouse = characterDto.PartnerWarehouse; + Bonus = characterDto.Bonus; + StaticBuffs = characterDto.StaticBuffs; + Quicklist = characterDto.Quicklist; + LearnedSkills = characterDto.LearnedSkills; + Titles = characterDto.Titles; + CompletedScripts = characterDto.CompletedScripts; + CompletedQuests = characterDto.CompletedQuests; + CompletedPeriodicQuests = characterDto.CompletedPeriodicQuests; + ActiveQuests = characterDto.ActiveQuests; + CompletedTimeSpaces = characterDto.CompletedTimeSpaces; + MinilandObjects = characterDto.MinilandObjects; + Inventory = characterDto.Inventory; + EquippedStuffs = characterDto.EquippedStuffs; + LifetimeStats = characterDto.LifetimeStats; + RaidRestrictionDto = characterDto.RaidRestrictionDto; + RainbowBattleLeaverBusterDto = characterDto.RainbowBattleLeaverBusterDto; + _familyManager = familyManager; + _randomGenerator = randomGenerator; + _mapManager = mapManager; + _characterAlgorithm = characterAlgorithm; + _foodSnackComponentFactory = foodSnackComponentFactory; + _eventPipeline = eventPipeline; + _algorithm = algorithm; + _portalFactory = portalFactory; + + _raidComponent = new RaidComponent(); + TimeSpaceComponent = new TimeSpaceComponent(); + _familyComponent = new FamilyComponent(_familyManager); + _inventory = new InventoryComponent(this, itemsManager); + _exchange = new ExchangeComponent(); + _characterRevivalComponent = new CharacterRevivalComponent(); + _groupComponent = new GroupComponent(); + _relationComponent = new RelationComponent(); + _eventTriggerContainer = new EventTriggerContainer(_eventPipeline); + BCardComponent = new BCardComponent(_randomGenerator); + _questContainer = new BasicQuestContainer(); + BuffComponent = new BuffComponent(); + _eqOptions = new EquipmentOptionContainer(); + _comboSkillComponent = new ComboSkillComponent(); + _bubbleComponent = new BubbleComponent(); + _partnerInventory = new PartnerInventoryComponent(this); + _castingComponent = new CastingComponent(); + _skillCooldownComponent = new SkillCooldownComponent(); + _chargeComponent = new ChargeComponent(); + _angelElementBuffComponent = new AngelElementBuffComponent(); + _endBuffDamageComponent = new EndBuffDamageComponent(); + _scoutComponent = new ScoutComponent(); + QuicklistComponent = new QuicklistComponent(); + _foodSnackComponent = _foodSnackComponentFactory.CreateFoodSnackComponent(); + HomeComponent = new HomeComponent(characterDto.ReturnPoint); + MateComponent = new MateComponent(); + WildKeeperComponent = new WildKeeperComponent(); + SkillComponent = new SkillComponent(); + CheatComponent = new CheatComponent(); + SpecialistComponent = new SpecialistStatsComponent(this); + StatisticsComponent = new PlayerStatisticsComponent(this); + ChargeComponent = new ChargeComponent(); + ShopComponent = new ShopComponent(); + MailNoteComponent = new MailNoteComponent(); + RainbowBattleComponent = new RainbowBattleComponent(); + + RefreshCharacterStats(); + + Killer = null; + SpCooldownEnd = null; + LastWalk = new LastWalk + { + MapId = null + }; + + LastDefence = DateTime.MinValue; + LastItemUpgrade = DateTime.MinValue; + LastHealth = DateTime.MinValue; + LastSkillUse = DateTime.MinValue; + LastEffect = DateTime.MinValue; + LastDayNight = DateTime.MinValue; + Bubble = DateTime.MinValue; + ItemsToRemove = DateTime.UtcNow; + LastPulseTick = DateTime.UtcNow; + LastAdministrationBazaarRefresh = DateTime.UtcNow; + LastBuySearchBazaarRefresh = DateTime.UtcNow; + LastListItemBazaar = DateTime.UtcNow; + Session = null; + + Skills = new List(); + } + + public DateTime LastDivinePotion { get; set; } + + public DateTime? GrowthShield { get; set; } + + public int AdditionalHp { get; set; } + public int AdditionalMp { get; set; } + + public int Hp { get; set; } + public int MaxHp { get; set; } + + public int Mp { get; set; } + public int MaxMp { get; set; } + + public bool TriggerAmbush { get; set; } + + public DateTime LastMapChange { get; set; } + + public DateTime? LastSkillCombo { get; set; } + + public AuthorityType Authority { get; set; } + + public int CurrentMinigame { get; set; } + + public byte Level { get; set; } + public byte Direction { get; set; } = 2; + + public int DamagesMinimum + { + get => GetDamagesMinimum(true); + set { } + } + + public int DamagesMaximum + { + get => GetDamagesMaximum(true); + set { } + } + + public int SecondDamageMinimum => GetDamagesMinimum(false); + + public int SecondDamageMaximum => GetDamagesMaximum(false); + + public int HitRate => GetHitRate(true); + + public int HitCriticalChance => GetCriticalChance(true); + + public int HitCriticalDamage => GetCriticalDamage(true); + + public int SecondHitRate => GetHitRate(false); + + public int SecondHitCriticalChance => GetCriticalChance(false); + + public int SecondHitCriticalDamage => GetCriticalDamage(false); + + public int MeleeDefence => this.GetDefence(_meleeDefense, StatisticType.DEFENSE_MELEE); + + public int RangedDefence => this.GetDefence(_rangedDefense, StatisticType.DEFENSE_RANGED); + + public int MagicDefence => this.GetDefence(_magicDefense, StatisticType.DEFENSE_MAGIC); + + public int MeleeDodge => this.GetDodge(_meleeDodge, StatisticType.DODGE_MELEE); + + public int RangedDodge => this.GetDodge(_rangedDodge, StatisticType.DODGE_RANGED); + + public byte Element + { + get + { + if (Fairy == null || Fairy.GameItem.Element == (byte)ElementType.All) + { + return 0; + } + + return (byte)Fairy?.GameItem.Element; + } + set { } + } + + public int ElementRate + { + get => this.GetElement(false); + set { } + } + + public int SpecialistElementRate => this.GetElement(true); + + public int FireResistance + { + get => this.GetResistance(StatisticType.FIRE); + set { } + } + + public int WaterResistance + { + get => this.GetResistance(StatisticType.WATER); + set { } + } + + public int LightResistance + { + get => this.GetResistance(StatisticType.LIGHT); + set { } + } + + public int DarkResistance + { + get => this.GetResistance(StatisticType.DARK); + set { } + } + + public DateTime GameStartDate { get; set; } + + public bool HasShopOpened { get; set; } + + public bool Invisible => this.IsInvisible(); + + public bool IsCustomSpeed { get; set; } + + public bool IsShopping { get; set; } + + public bool IsSitting { get; set; } + + public bool IsOnVehicle { get; set; } + + public bool IsMorphed { get; set; } + + public (VisualType, long) LastEntity { get; set; } + + public DateTime? RandomMapTeleport { get; set; } + + public DateTime LastMove { get; set; } + + public DateTime LastPutItem { get; set; } + + public DateTime LastSentNote { get; set; } + + public DateTime? CheckWeedingBuff { get; set; } + public DateTime LastPvPAttack { get; set; } + + public DateTime LastRainbowArrowEffect { get; set; } + public DateTime LastRainbowEffects { get; set; } + + public LastWalk LastWalk { get; set; } + + public int LastNRunId { get; set; } + + public int LastPulse { get; set; } + + public DateTime LastPulseTick { get; set; } + + public DateTime LastDefence { get; set; } + + public DateTime LastItemUpgrade { get; set; } + + public DateTime LastDeath { get; set; } + + public DateTime LastEffect { get; set; } + + public DateTime LastEffectMinigame { get; set; } + + public DateTime LastHealth { get; set; } + + public DateTime LastPortal { get; set; } + + public DateTime LastPotion { get; set; } + + public DateTime LastSnack { get; set; } + + public DateTime LastFood { get; set; } + + public DateTime LastSkillUse { get; set; } + + public DateTime LastSpeedChange { get; set; } + + public DateTime LastTransform { get; set; } + + public DateTime LastDayNight { get; set; } + + public DateTime? SpCooldownEnd { get; set; } + + public DateTime Bubble { get; set; } + + public DateTime SpyOutStart { get; set; } + + public DateTime ItemsToRemove { get; set; } + + public DateTime BonusesToRemove { get; set; } + + public IMapInstance MapInstance => _mapManager.GetMapInstance(MapInstanceId); + + public Guid MapInstanceId { get; set; } + + public IMapInstance Miniland { get; set; } + + public int Morph { get; set; } + + public int MorphUpgrade { get; set; } + + public int MorphUpgrade2 { get; set; } + + public int? LastMinilandProducedItem { get; set; } + + public bool IsGettingLosingReputation { get; set; } + + public byte DeathsOnAct4 { get; set; } + public long ArenaKills { get; set; } + public long ArenaDeaths { get; set; } + + public TimeSpan? MuteRemainingTime { get; set; } + + public DateTime LastMuteTick { get; set; } + public DateTime LastSitting { get; set; } + + public DateTime? LastChatMuteMessage { get; set; } + public DateTime LastInventorySort { get; set; } + + public DateTime? ArenaImmunity { get; set; } + + public Position Position + { + get => new(PositionX, PositionY); + set + { + Position pos = value; + PositionX = pos.X; + PositionY = pos.Y; + } + } + + public short PositionX { get; set; } + + public short PositionY { get; set; } + + public FactionType Faction { get; private set; } + + public IClientSession Session { get; private set; } + + public byte Size { get; set; } = 10; + + public List Skills { get; set; } + public IChargeComponent ChargeComponent { get; } + public ThreadSafeHashSet AggroedEntities { get; } = new(); + + public ConcurrentDictionary CharacterSkills { get; } = new(); + + public ConcurrentDictionary SkillsSp { get; set; } + + public byte Speed + { + get => _speed; + + set + { + LastSpeedChange = DateTime.UtcNow; + _speed = value > 59 ? (byte)59 : value; + } + } + + public IBattleEntity Killer { get; set; } + + public void AddStaticBonus(CharacterStaticBonusDto bonus) => Bonus.Add(bonus); + + public void AddStaticBonuses(IEnumerable bonuses) => Bonus.AddRange(bonuses); + + public IReadOnlyList GetStaticBonuses() => Bonus; + + public CharacterStaticBonusDto GetStaticBonus(Predicate predicate) + { + return Bonus.FirstOrDefault(x => predicate(x)); + } + + public ConcurrentDictionary HitsByMonsters { get; } = new(); + + public bool UseSp { get; set; } + + public byte VehicleSpeed { get; set; } + + public byte VehicleMapSpeed { get; set; } + + public int WareHouseSize { get; set; } + + public DateTime LastBuySearchBazaarRefresh { get; set; } + + public DateTime LastBuyBazaarRefresh { get; set; } + + public DateTime LastListItemBazaar { get; set; } + + public DateTime LastAdministrationBazaarRefresh { get; set; } + + public DateTime LastMonsterCaught { get; set; } + + public bool IsSeal { get; set; } + + public bool IsRemovingSpecialistPoints { get; set; } + + public VisualType Type => VisualType.Player; + public int Id { get; } + public long AccountId { get; set; } + public int Act4Dead { get; set; } + public int Act4Kill { get; set; } + public int Act4Points { get; set; } + public int ArenaWinner { get; set; } + public string Biography { get; set; } + public bool BuffBlocked { get; set; } + public bool ShowRaidDeathInfo { get; set; } + public ClassType Class { get; set; } + public short Compliment { get; set; } + public float Dignity { get; set; } + public bool EmoticonsBlocked { get; set; } + public bool ExchangeBlocked { get; set; } + public bool FamilyRequestBlocked { get; set; } + public bool FriendRequestBlocked { get; set; } + public GenderType Gender { get; set; } + public long Gold { get; set; } + public bool GroupRequestBlocked { get; set; } + public HairColorType HairColor { get; set; } + public HairStyleType HairStyle { get; set; } + public bool HeroChatBlocked { get; set; } + public byte HeroLevel { get; set; } + public long HeroXp { get; set; } + public bool HpBlocked { get; set; } + public bool IsPetAutoRelive { get; set; } + public bool IsPartnerAutoRelive { get; set; } + public byte JobLevel { get; set; } + public long JobLevelXp { get; set; } + public long LevelXp { get; set; } + public int MapId { get; set; } + public short MapX { get; set; } + public short MapY { get; set; } + public int MasterPoints { get; set; } + public int MasterTicket { get; set; } + public byte MaxPetCount { get; set; } + public byte MaxPartnerCount { get; set; } + public bool MinilandInviteBlocked { get; set; } + public string MinilandMessage { get; set; } + public short MinilandPoint { get; set; } + public MinilandState MinilandState { get; set; } + public bool MouseAimLock { get; set; } + public string Name { get; set; } + public bool QuickGetUp { get; set; } + public bool HideHat { get; set; } + public bool UiBlocked { get; set; } + public long Reput { get; set; } + public byte Slot { get; set; } + public int SpPointsBonus { get; set; } + public int SpPointsBasic { get; set; } + public int TalentLose { get; set; } + public int TalentSurrender { get; set; } + public int TalentWin { get; set; } + public bool WhisperBlocked { get; set; } + public List PartnerInventory { get; set; } + public List NosMates { get; set; } + public HashSet CompletedTimeSpaces { get; set; } + public List PartnerWarehouse { get; set; } + public List Bonus { get; set; } + public List StaticBuffs { get; set; } + public List Quicklist { get; set; } + public List LearnedSkills { get; set; } + public List Titles { get; set; } + public List CompletedScripts { get; set; } + public List CompletedQuests { get; set; } + public List CompletedPeriodicQuests { get; set; } + public List ActiveQuests { get; set; } + public List MinilandObjects { get; set; } + public List Inventory { get; set; } + public List EquippedStuffs { get; set; } + public CharacterLifetimeStatsDto LifetimeStats { get; set; } + public CharacterRaidRestrictionDto RaidRestrictionDto { get; set; } + public RainbowBattleLeaverBusterDto RainbowBattleLeaverBusterDto { get; set; } + + public bool HasBuff(BuffVnums buffVnum) => BuffComponent.HasBuff((short)buffVnum); + + public void SetFaction(FactionType faction) + { + Faction = faction; + } + + public void AddEvent(string trigger, IAsyncEvent notification, bool removeOnTrigger = false) + { + _eventTriggerContainer.AddEvent(trigger, notification, removeOnTrigger); + } + + public async Task TriggerEvents(string key) => await _eventTriggerContainer.TriggerEvents(key); + + public async Task EmitEventAsync(T eventArgs) where T : IBattleEntityEvent + { + if (eventArgs.Entity != this) + { + throw new ArgumentException("An event should be emitted only from the event sender"); + } + + await _eventPipeline.ProcessEventAsync(eventArgs); + } + + public void EmitEvent(T eventArgs) where T : IBattleEntityEvent + { + EmitEventAsync(eventArgs).ConfigureAwait(false).GetAwaiter().GetResult(); + } + + public bool HasCompletedQuest(int questId) => _questContainer.HasCompletedQuest(questId); + public void RemoveCompletedQuest(int questId) => _questContainer.RemoveCompletedQuest(questId); + + public void RemoveCompletedScript(int scriptId, int scriptIndex) => _questContainer.RemoveCompletedScript(scriptId, scriptIndex); + + public void RemoveAllCompletedScripts() => _questContainer.RemoveAllCompletedScripts(); + public void ClearCompletedPeriodicQuests() => _questContainer.ClearCompletedPeriodicQuests(); + + public IHomeComponent HomeComponent { get; } + public ICheatComponent CheatComponent { get; } + + protected bool Equals(IPlayerEntity other) => Id == other.Id && Type == other.Type; + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + return obj.GetType() == GetType() && Equals((IPlayerEntity)obj); + } + + public override int GetHashCode() + { + unchecked + { + return Id.GetHashCode() * 397 ^ Type.GetHashCode(); + } + } + + #region Methods + + private int GetDamagesMinimum(bool isMainWeapon) + { + return Class switch + { + ClassType.Swordman => this.GetDamage(isMainWeapon ? _meleeDamageMin : _rangedDamageMin, true, isMainWeapon ? StatisticType.ATTACK_MELEE : StatisticType.ATTACK_RANGED, isMainWeapon), + ClassType.Wrestler => this.GetDamage(isMainWeapon ? _meleeDamageMax : _rangedDamageMax, true, isMainWeapon ? StatisticType.ATTACK_MELEE : StatisticType.ATTACK_RANGED, isMainWeapon), + ClassType.Adventurer => this.GetDamage(isMainWeapon ? _meleeDamageMin : _rangedDamageMin, true, isMainWeapon ? StatisticType.ATTACK_MELEE : StatisticType.ATTACK_RANGED, isMainWeapon), + ClassType.Archer => this.GetDamage(isMainWeapon ? _rangedDamageMin : _meleeDamageMin, true, isMainWeapon ? StatisticType.ATTACK_RANGED : StatisticType.ATTACK_MELEE, isMainWeapon), + ClassType.Magician => this.GetDamage(isMainWeapon ? _magicDamageMin : _rangedDamageMin, true, isMainWeapon ? StatisticType.ATTACK_MAGIC : StatisticType.ATTACK_RANGED, isMainWeapon) + }; + } + + private int GetHitRate(bool isMainWeapon) + { + int hitRate = 0; + if (Class == ClassType.Magician && isMainWeapon) + { + return this.GetHitRate(0, true, StatisticType.HITRATE_MAGIC); + } + + if (isMainWeapon) + { + switch (Class) + { + case ClassType.Swordman: + case ClassType.Wrestler: + case ClassType.Adventurer: + hitRate = this.GetHitRate(_meleeHitRate, true, StatisticType.HITRATE_MELEE); + break; + case ClassType.Archer: + hitRate = this.GetHitRate(_rangedHitRate, true, StatisticType.HITRATE_RANGED); + break; + } + } + else + { + switch (Class) + { + case ClassType.Wrestler: + case ClassType.Archer: + case ClassType.Adventurer: + hitRate = this.GetHitRate(_meleeHitRate, false, StatisticType.HITRATE_MELEE); + break; + case ClassType.Magician: + case ClassType.Swordman: + hitRate = this.GetHitRate(_rangedHitRate, false, StatisticType.HITRATE_RANGED); + break; + } + } + + return hitRate; + } + + private int GetCriticalChance(bool isMainWeapon) + { + if (Class == ClassType.Magician && isMainWeapon) + { + return 0; + } + + return this.GetCriticalChance(_criticalChance, isMainWeapon); + } + + private int GetCriticalDamage(bool isMainWeapon) + { + if (Class == ClassType.Magician && isMainWeapon) + { + return 0; + } + + return this.GetCriticalDamage(_criticalDamage, isMainWeapon); + } + + private int GetDamagesMaximum(bool isMainWeapon) + { + return Class switch + { + ClassType.Swordman => + this.GetDamage(isMainWeapon ? _meleeDamageMax : _rangedDamageMax, false, isMainWeapon ? StatisticType.ATTACK_MELEE : StatisticType.ATTACK_RANGED, isMainWeapon), + ClassType.Wrestler => + this.GetDamage(isMainWeapon ? _meleeDamageMax : _rangedDamageMax, false, isMainWeapon ? StatisticType.ATTACK_MELEE : StatisticType.ATTACK_RANGED, isMainWeapon), + ClassType.Adventurer => + this.GetDamage(isMainWeapon ? _meleeDamageMax : _rangedDamageMax, false, isMainWeapon ? StatisticType.ATTACK_MELEE : StatisticType.ATTACK_RANGED, isMainWeapon), + ClassType.Archer => + this.GetDamage(isMainWeapon ? _rangedDamageMax : _meleeDamageMax, false, isMainWeapon ? StatisticType.ATTACK_RANGED : StatisticType.ATTACK_MELEE, isMainWeapon), + ClassType.Magician => + this.GetDamage(isMainWeapon ? _magicDamageMax : _rangedDamageMax, false, isMainWeapon ? StatisticType.ATTACK_MAGIC : StatisticType.ATTACK_RANGED, isMainWeapon) + }; + } + + public bool IsCraftingItem { get; set; } + public bool IsBankOpen { get; set; } + public DateTime LastUnfreezedPlayer { get; set; } + public DateTime LastSpPacketSent { get; set; } + public DateTime LastSpRemovingProcess { get; set; } + public DateTime LastSpPointProcess { get; set; } + public DateTime LastAttack { get; set; } + public bool InitialScpPacketSent { get; set; } + + public IQuicklistComponent QuicklistComponent { get; } + public ISpecialistStatsComponent SpecialistComponent { get; } + public IPlayerStatisticsComponent StatisticsComponent { get; } + public IRainbowBattleComponent RainbowBattleComponent { get; } + + public int GetCp() + { + int cpMax = (Class > 0 ? 40 : 0) + JobLevel * 2; + int cpUsed = 0; + + foreach (CharacterSkill skill in CharacterSkills.Values) + { + if (skill == null) + { + continue; + } + + if (skill.Skill.IsPassiveSkill()) + { + continue; + } + + cpUsed += skill.Skill.CPCost; + } + + return cpMax - cpUsed; + } + + public int GetDignityIco() + { + int icoDignity = 1; + + if (Dignity <= -100) + { + icoDignity = 2; + } + + if (Dignity <= -200) + { + icoDignity = 3; + } + + if (Dignity <= -400) + { + icoDignity = 4; + } + + if (Dignity <= -600) + { + icoDignity = 5; + } + + if (Dignity <= -800) + { + icoDignity = 6; + } + + return icoDignity; + } + + public List GetExtraPortal() + { + if (MapInstance == null || Miniland == null) + { + return new List(); + } + + return MapInstance.GenerateMinilandEntryPortals(Miniland, _portalFactory); + } + + [Obsolete("Move to constructor")] + public void SetSession(IClientSession clientSession) + { + Session = clientSession; + } + + public int HealthHpLoad() + { + if (IsSitting) + { + return _characterAlgorithm.GetRegenHp(this, Class, true); + } + + return LastDefence.AddSeconds(4) <= DateTime.UtcNow ? _characterAlgorithm.GetRegenHp(this, Class, false) : 0; + } + + public int HealthMpLoad() + { + if (IsSitting) + { + return _characterAlgorithm.GetRegenMp(this, Class, true); + } + + return LastDefence.AddSeconds(4) <= DateTime.UtcNow ? _characterAlgorithm.GetRegenMp(this, Class, false) : 0; + } + + #endregion +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Chat/ChatGenericEvent.cs b/srcs/WingsAPI.Game/Chat/ChatGenericEvent.cs new file mode 100644 index 0000000..8f4169e --- /dev/null +++ b/srcs/WingsAPI.Game/Chat/ChatGenericEvent.cs @@ -0,0 +1,15 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsEmu.Game._packetHandling; +using WingsEmu.Game._playerActionLogs; + +namespace WingsEmu.Game.Chat; + +public class ChatGenericEvent : PlayerEvent +{ + public string Message { get; init; } + public ChatType ChatType { get; init; } + public long? TargetCharacterId { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Chat/ChatMessageReceivedEvent.cs b/srcs/WingsAPI.Game/Chat/ChatMessageReceivedEvent.cs new file mode 100644 index 0000000..26233d7 --- /dev/null +++ b/srcs/WingsAPI.Game/Chat/ChatMessageReceivedEvent.cs @@ -0,0 +1,21 @@ +// WingsEmu +// +// Developed by NosWings Team + +using PhoenixLib.Events; + +namespace WingsEmu.Game.Chat; + +public abstract class ChatMessageReceivedEvent : IAsyncEvent +{ + protected ChatMessageReceivedEvent(string senderName, string senderMessage, long senderChannelId) + { + SenderName = senderName; + SenderMessage = senderMessage; + SenderChannelId = senderChannelId; + } + + public string SenderName { get; } + public string SenderMessage { get; } + public long SenderChannelId { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Chat/ChatSendFriendMessageEvent.cs b/srcs/WingsAPI.Game/Chat/ChatSendFriendMessageEvent.cs new file mode 100644 index 0000000..debf243 --- /dev/null +++ b/srcs/WingsAPI.Game/Chat/ChatSendFriendMessageEvent.cs @@ -0,0 +1,9 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Chat; + +public class ChatSendFriendMessageEvent : PlayerEvent +{ + public long TargetId { get; set; } + public string Message { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Chat/ChatSpeakerEvent.cs b/srcs/WingsAPI.Game/Chat/ChatSpeakerEvent.cs new file mode 100644 index 0000000..5220d48 --- /dev/null +++ b/srcs/WingsAPI.Game/Chat/ChatSpeakerEvent.cs @@ -0,0 +1,19 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Game.Items; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Game.Chat; + +public class ChatSpeakerEvent : PlayerEvent +{ + public ChatSpeakerEvent(SpeakerType chatSpeakerType, string message, GameItemInstance item = null) + { + ChatSpeakerType = chatSpeakerType; + Message = message; + Item = item; + } + + public SpeakerType ChatSpeakerType { get; set; } + public string Message { get; set; } + public GameItemInstance Item { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Chat/ChatType.cs b/srcs/WingsAPI.Game/Chat/ChatType.cs new file mode 100644 index 0000000..9776b63 --- /dev/null +++ b/srcs/WingsAPI.Game/Chat/ChatType.cs @@ -0,0 +1,13 @@ +namespace WingsEmu.Game._playerActionLogs; + +public enum ChatType +{ + General, + Whisper, + Shout, + HeroChat, + FamilyChat, + GroupChat, + FriendChat, + SpeechBubble +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Chat/FamilyChatReceivedEvent.cs b/srcs/WingsAPI.Game/Chat/FamilyChatReceivedEvent.cs new file mode 100644 index 0000000..582135c --- /dev/null +++ b/srcs/WingsAPI.Game/Chat/FamilyChatReceivedEvent.cs @@ -0,0 +1,10 @@ +namespace WingsEmu.Game.Chat; + +public class FamilyChatReceivedEvent : ChatMessageReceivedEvent +{ + public FamilyChatReceivedEvent(string senderName, string senderMessage, long senderChannelId, long targetFamilyId) + : base(senderName, senderMessage, senderChannelId) => + TargetFamilyId = targetFamilyId; + + public long TargetFamilyId { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Chat/FriendChatReceivedEvent.cs b/srcs/WingsAPI.Game/Chat/FriendChatReceivedEvent.cs new file mode 100644 index 0000000..7ce3503 --- /dev/null +++ b/srcs/WingsAPI.Game/Chat/FriendChatReceivedEvent.cs @@ -0,0 +1,10 @@ +namespace WingsEmu.Game.Chat; + +public class FriendChatReceivedEvent : ChatMessageReceivedEvent +{ + public FriendChatReceivedEvent(string senderName, string senderMessage, long senderChannelId, long targetCharacterId) + : base(senderName, senderMessage, senderChannelId) => + TargetCharacterId = targetCharacterId; + + public long TargetCharacterId { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Chat/GlobalPlayerChatReceivedEvent.cs b/srcs/WingsAPI.Game/Chat/GlobalPlayerChatReceivedEvent.cs new file mode 100644 index 0000000..a068abd --- /dev/null +++ b/srcs/WingsAPI.Game/Chat/GlobalPlayerChatReceivedEvent.cs @@ -0,0 +1,9 @@ +namespace WingsEmu.Game.Chat; + +public class GlobalPlayerChatReceivedEvent : ChatMessageReceivedEvent +{ + public GlobalPlayerChatReceivedEvent(string senderName, string senderMessage, long senderChannelId) + : base(senderName, senderMessage, senderChannelId) + { + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Cheats/CheatComponent.cs b/srcs/WingsAPI.Game/Cheats/CheatComponent.cs new file mode 100644 index 0000000..2ca8ae3 --- /dev/null +++ b/srcs/WingsAPI.Game/Cheats/CheatComponent.cs @@ -0,0 +1,10 @@ +namespace WingsEmu.Game.Cheats; + +public class CheatComponent : ICheatComponent +{ + public bool HasOneHitKill { get; set; } + public bool HasGodMode { get; set; } + public bool IsInvisible { get; set; } + public bool HasNoCooldown { get; set; } + public bool HasNoTargetLimit { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Cheats/ICheatComponent.cs b/srcs/WingsAPI.Game/Cheats/ICheatComponent.cs new file mode 100644 index 0000000..b76ad1b --- /dev/null +++ b/srcs/WingsAPI.Game/Cheats/ICheatComponent.cs @@ -0,0 +1,13 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Game.Cheats; + +public interface ICheatComponent +{ + public bool HasGodMode { get; set; } + public bool IsInvisible { get; set; } + public bool HasNoCooldown { get; set; } + public bool HasNoTargetLimit { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Commands/IGlobalCommandExecutor.cs b/srcs/WingsAPI.Game/Commands/IGlobalCommandExecutor.cs new file mode 100644 index 0000000..f4039b6 --- /dev/null +++ b/srcs/WingsAPI.Game/Commands/IGlobalCommandExecutor.cs @@ -0,0 +1,25 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Threading.Tasks; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Game.Commands; + +public interface IGlobalCommandExecutor +{ + /// + /// Method which will parse the message and try to execute the command. + /// + /// + /// + void HandleCommand(string command, IClientSession sender, string prefix); + + /// + /// Method which will parse the message and try to execute the command. + /// + /// Raw message to parse. + /// + Task HandleCommandAsync(string command, IClientSession sender, string prefix); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Compliments/ComplimentsMonthlyRefreshEvent.cs b/srcs/WingsAPI.Game/Compliments/ComplimentsMonthlyRefreshEvent.cs new file mode 100644 index 0000000..f5c3537 --- /dev/null +++ b/srcs/WingsAPI.Game/Compliments/ComplimentsMonthlyRefreshEvent.cs @@ -0,0 +1,8 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Compliments; + +public class ComplimentsMonthlyRefreshEvent : PlayerEvent +{ + public bool Force { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Compliments/IComplimentsManager.cs b/srcs/WingsAPI.Game/Compliments/IComplimentsManager.cs new file mode 100644 index 0000000..9c5431a --- /dev/null +++ b/srcs/WingsAPI.Game/Compliments/IComplimentsManager.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace WingsEmu.Game.Compliments; + +public interface IComplimentsManager +{ + Task CanRefresh(long characterId); + Task CanCompliment(long accountId); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Configurations/Act5NpcRunCraftItemConfig.cs b/srcs/WingsAPI.Game/Configurations/Act5NpcRunCraftItemConfig.cs new file mode 100644 index 0000000..c182c55 --- /dev/null +++ b/srcs/WingsAPI.Game/Configurations/Act5NpcRunCraftItemConfig.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using System.Linq; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Configurations; + +public interface IAct5NpcRunCraftItemConfiguration +{ + Act5NpcRunCraftItemConfig GetConfigByNpcRun(NpcRunType npcRunType); +} + +public class Act5NpcRunCraftItemConfiguration : IAct5NpcRunCraftItemConfiguration +{ + private readonly Dictionary _act5NpcRunItemConfigs; + + public Act5NpcRunCraftItemConfiguration(IEnumerable configs) + { + _act5NpcRunItemConfigs = configs.Where(x => x?.NeededItems != null).ToDictionary(x => x.NpcRun); + } + + public Act5NpcRunCraftItemConfig GetConfigByNpcRun(NpcRunType npcRunType) => _act5NpcRunItemConfigs.GetValueOrDefault(npcRunType); +} + +public class Act5NpcRunCraftItemConfig +{ + public NpcRunType NpcRun { get; set; } + public int CraftedItem { get; set; } + public int Amount { get; set; } + public bool? ItemByClass { get; set; } + public List NeededItems { get; set; } +} + +public class Act5NpcRunCraftItemConfigItem +{ + public int Item { get; set; } + public int Amount { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Configurations/BankReputationConfiguration.cs b/srcs/WingsAPI.Game/Configurations/BankReputationConfiguration.cs new file mode 100644 index 0000000..2e4f6c7 --- /dev/null +++ b/srcs/WingsAPI.Game/Configurations/BankReputationConfiguration.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using WingsEmu.Core.Extensions; +using WingsEmu.Game._enum; + +namespace WingsEmu.Game.Configurations; + +public interface IBankReputationConfiguration +{ + public BankRankInfo GetBankRankInfo(ReputationType reputationType); + public BankPenaltyInfo GetBankPenaltyInfo(ReputationType reputationType); +} + +public class BankReputationConfiguration : IBankReputationConfiguration +{ + private readonly ImmutableDictionary _penaltiesByReputation; + private readonly ImmutableDictionary _ranksByReputation; + + public BankReputationConfiguration(BankReputationInfo bankReputationInfo) + { + var bankRankTypes = new Dictionary(); + foreach (BankRankInfo bankRankInfo in bankReputationInfo.BankRanks) + { + foreach (ReputationType reputation in bankRankInfo.Reputations) + { + bankRankTypes.TryAdd(reputation, bankRankInfo); + } + } + + _ranksByReputation = bankRankTypes.ToImmutableDictionary(); + _penaltiesByReputation = bankReputationInfo.BankPenalties.ToImmutableDictionary(s => s.Reputation); + } + + public BankRankInfo GetBankRankInfo(ReputationType reputationType) => _ranksByReputation.GetOrDefault(reputationType); + public BankPenaltyInfo GetBankPenaltyInfo(ReputationType reputationType) => _penaltiesByReputation.GetOrDefault(reputationType); +} + +public class BankReputationInfo +{ + public List BankRanks { get; set; } + public List BankPenalties { get; set; } +} + +public class BankRankInfo +{ + public BankRankType BankRank { get; set; } + public List Reputations { get; set; } +} + +public class BankPenaltyInfo +{ + public ReputationType Reputation { get; set; } + public int GoldCost { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Configurations/BuffsToRemoveConfiguration.cs b/srcs/WingsAPI.Game/Configurations/BuffsToRemoveConfiguration.cs new file mode 100644 index 0000000..fdbcb06 --- /dev/null +++ b/srcs/WingsAPI.Game/Configurations/BuffsToRemoveConfiguration.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using WingsEmu.Core.Extensions; + +namespace WingsEmu.Game.Configurations; + +public interface IBuffsToRemoveConfig +{ + short[] GetBuffsToRemove(BuffsToRemoveType type); +} + +public class BuffsToRemoveConfig : IBuffsToRemoveConfig +{ + private readonly Dictionary _buffs = new(); + + public BuffsToRemoveConfig(BuffsToRemoveConfiguration configuration) + { + _buffs[BuffsToRemoveType.ATTACKER] = configuration.AttackerBuffs; + _buffs[BuffsToRemoveType.DEFENDER] = configuration.DefenderBuffs; + _buffs[BuffsToRemoveType.PVP] = configuration.PvpBuffs; + } + + public short[] GetBuffsToRemove(BuffsToRemoveType type) => _buffs.GetOrDefault(type); +} + +public class BuffsToRemoveConfiguration +{ + public short[] AttackerBuffs { get; set; } + public short[] DefenderBuffs { get; set; } + public short[] PvpBuffs { get; set; } +} + +public enum BuffsToRemoveType +{ + ATTACKER, + DEFENDER, + PVP +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Configurations/ChestDropItemConfiguration.cs b/srcs/WingsAPI.Game/Configurations/ChestDropItemConfiguration.cs new file mode 100644 index 0000000..920faed --- /dev/null +++ b/srcs/WingsAPI.Game/Configurations/ChestDropItemConfiguration.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using WingsEmu.Core.Extensions; + +namespace WingsEmu.Game.Configurations; + +public interface IChestDropItemConfig +{ + ChestDropItemConfiguration GetChestByDataId(int id); +} + +public class ChestDropItemConfig : IChestDropItemConfig +{ + private readonly Dictionary _configs = new(); + + public ChestDropItemConfig(IEnumerable configurations) + { + foreach (ChestDropItemConfiguration config in configurations) + { + _configs[config.Id] = config; + } + } + + public ChestDropItemConfiguration GetChestByDataId(int id) => _configs.GetOrDefault(id); +} + +public class ChestDropItemConfiguration +{ + public short Id { get; set; } + public int ItemChance { get; set; } + public List PossibleItems { get; set; } +} + +public class ChestDropItemDrop +{ + public int ItemVnum { get; set; } + public int Amount { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Configurations/GameMinMaxConfiguration.cs b/srcs/WingsAPI.Game/Configurations/GameMinMaxConfiguration.cs new file mode 100644 index 0000000..a41581c --- /dev/null +++ b/srcs/WingsAPI.Game/Configurations/GameMinMaxConfiguration.cs @@ -0,0 +1,64 @@ +using System.Runtime.Serialization; + +namespace WingsEmu.Game.Configurations; + +[DataContract] +public class GameMinMaxConfiguration +{ + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public short MaxLevel { get; set; } = 99; + + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public short MaxMateLevel { get; set; } = 99; + + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public short MaxJobLevel { get; set; } = 80; + + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public short MaxSpLevel { get; set; } = 99; + + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public short MaxHeroLevel { get; set; } = 60; + + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public short HeroMinLevel { get; set; } = 88; + + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public short MinLodLevel { get; set; } = 55; + + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public int MaxGold { get; set; } = 1_000_000_000; + + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public long MaxBankGold { get; set; } = 100_000_000_000; + + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public byte MaxBotCodeAttempts { get; set; } = 3; + + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public float MaxDignity { get; set; } = 200; + + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public float MinDignity { get; set; } = -1000; + + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public long MaxReputation { get; set; } = long.MaxValue; + + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public long MinReputation { get; set; } + + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public short MaxMateLoyalty { get; set; } = 1000; + + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public short MinMateLoyalty { get; set; } + + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public short MaxNpcTalkRange { get; set; } = 4; + + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public int MaxSpAdditionalPoints { get; set; } = 1_000_000; + + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public short MaxSpBasePoints { get; set; } = 1000; +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Configurations/GameRateConfiguration.cs b/srcs/WingsAPI.Game/Configurations/GameRateConfiguration.cs new file mode 100644 index 0000000..87ce194 --- /dev/null +++ b/srcs/WingsAPI.Game/Configurations/GameRateConfiguration.cs @@ -0,0 +1,56 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Runtime.Serialization; + +namespace WingsEmu.Game.Configurations; + +[DataContract] +public class GameRateConfiguration +{ + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public int MobXpRate { get; set; } = 1; + + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public int JobXpRate { get; set; } = 1; + + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public int HeroXpRate { get; set; } = 1; + + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public int FairyXpRate { get; set; } = 1; + + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public int MateXpRate { get; set; } = 1; + + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public int PartnerXpRate { get; set; } = 1; + + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public int FamilyXpRate { get; set; } = 1; + + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public int ReputRate { get; set; } = 1; + + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public int MobDropRate { get; set; } = 1; + + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public int MobDropChance { get; set; } = 1; + + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public int GoldDropRate { get; set; } = 1; + + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public int GoldRate { get; set; } = 1; + + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public int GoldDropChance { get; set; } = 1; + + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public int GenericDropRate { get; set; } = 1; + + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public int GenericDropChance { get; set; } = 1; +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Configurations/GameRevivalConfiguration.cs b/srcs/WingsAPI.Game/Configurations/GameRevivalConfiguration.cs new file mode 100644 index 0000000..e525897 --- /dev/null +++ b/srcs/WingsAPI.Game/Configurations/GameRevivalConfiguration.cs @@ -0,0 +1,13 @@ +using System.Runtime.Serialization; + +namespace WingsEmu.Game.Configurations; + +[DataContract] +public class GameRevivalConfiguration +{ + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public PlayerRevivalConfiguration PlayerRevivalConfiguration { get; set; } = new(); + + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public MateRevivalConfiguration MateRevivalConfiguration { get; set; } = new(); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Configurations/GeneralQuestsConfiguration.cs b/srcs/WingsAPI.Game/Configurations/GeneralQuestsConfiguration.cs new file mode 100644 index 0000000..82f6baf --- /dev/null +++ b/srcs/WingsAPI.Game/Configurations/GeneralQuestsConfiguration.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; + +namespace WingsEmu.Game.Configurations; + +public class GeneralQuestsConfiguration +{ + public HashSet GeneralQuests { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Configurations/GibberishConfiguration.cs b/srcs/WingsAPI.Game/Configurations/GibberishConfiguration.cs new file mode 100644 index 0000000..84df2ee --- /dev/null +++ b/srcs/WingsAPI.Game/Configurations/GibberishConfiguration.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; + +namespace WingsEmu.Game.Configurations; + +public interface IGibberishConfig +{ + IReadOnlyList GetKeysById(int id); +} + +public class GibberishConfig : IGibberishConfig +{ + private readonly ConcurrentDictionary> _keys = new(); + + public GibberishConfig(IEnumerable configuration) + { + foreach (GibberishConfiguration config in configuration) + { + _keys.TryAdd(config.Id, config.Keys ?? new List()); + } + } + + public IReadOnlyList GetKeysById(int id) => _keys.TryGetValue(id, out IReadOnlyList list) ? list : Array.Empty(); +} + +public class GibberishConfiguration +{ + public int Id { get; set; } + public List Keys { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Configurations/MateRevivalConfiguration.cs b/srcs/WingsAPI.Game/Configurations/MateRevivalConfiguration.cs new file mode 100644 index 0000000..efd29d2 --- /dev/null +++ b/srcs/WingsAPI.Game/Configurations/MateRevivalConfiguration.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; +using WingsEmu.DTOs.Account; +using WingsEmu.Game._enum; + +namespace WingsEmu.Game.Configurations; + +[DataContract] +public class MateRevivalConfiguration +{ + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public List MateInstantRevivalPenalizationSaver { get; set; } = new() + { + (int)ItemVnums.NOSMATE_GUARDIAN_ANGEL_LIMITED, + (int)ItemVnums.NOSMATE_GUARDIAN_ANGEL + }; + + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public int MateInstantRevivalPenalizationSaverAmount { get; set; } = 1; + + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public List PartnerInstantRevivalPenalizationSaver { get; set; } = new() + { + (int)ItemVnums.PARTNER_GUARDIAN_ANGEL_LIMITED, + (int)ItemVnums.PARTNER_GUARDIAN_ANGEL + }; + + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public int PartnerInstantRevivalPenalizationSaverAmount { get; set; } = 1; + + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public TimeSpan DelayedRevivalDelay { get; set; } = TimeSpan.FromMinutes(3); + + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public int DelayedRevivalPenalizationSaver { get; set; } = (int)ItemVnums.SEED_OF_POWER; + + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public int DelayedRevivalPenalizationSaverAmount { get; set; } = 5; + + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public short LoyaltyDeathPenalizationAmount { get; set; } = 50; + + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public AuthorityType NoLoyaltyDeathPenalizationMinAuthority { get; set; } = AuthorityType.VipPlus; +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Configurations/Miniland/AntiExploitConfiguration.cs b/srcs/WingsAPI.Game/Configurations/Miniland/AntiExploitConfiguration.cs new file mode 100644 index 0000000..a097e35 --- /dev/null +++ b/srcs/WingsAPI.Game/Configurations/Miniland/AntiExploitConfiguration.cs @@ -0,0 +1,16 @@ +using System; + +namespace WingsEmu.Game.Configurations.Miniland; + +public class AntiExploitConfiguration +{ + public double MinigameAbuseDetectionThreshold { get; set; } = 0.5; + + public TimeSpan CommonTimeExpendedInMinigamesPerDay { get; set; } = TimeSpan.FromHours(2); + + public double PercentageForSameScoreCheck { get; set; } = 0.5; + + public int UseSameScoreCheckAtXMinigames { get; set; } = 10; + + public bool GiveRewardsToPossibleFalsePositives { get; set; } = false; +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Configurations/Miniland/ForcedPlacing.cs b/srcs/WingsAPI.Game/Configurations/Miniland/ForcedPlacing.cs new file mode 100644 index 0000000..42d1462 --- /dev/null +++ b/srcs/WingsAPI.Game/Configurations/Miniland/ForcedPlacing.cs @@ -0,0 +1,10 @@ +using WingsEmu.Game._enum; + +namespace WingsEmu.Game.Configurations.Miniland; + +public class ForcedPlacing +{ + public MinilandItemSubType SubType { get; set; } = MinilandItemSubType.HOUSE; + + public SerializablePosition ForcedLocation { get; set; } = new(); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Configurations/Miniland/GlobalMinigameConfiguration.cs b/srcs/WingsAPI.Game/Configurations/Miniland/GlobalMinigameConfiguration.cs new file mode 100644 index 0000000..17f8908 --- /dev/null +++ b/srcs/WingsAPI.Game/Configurations/Miniland/GlobalMinigameConfiguration.cs @@ -0,0 +1,32 @@ +using WingsEmu.Game._enum; + +namespace WingsEmu.Game.Configurations.Miniland; + +public class GlobalMinigameConfiguration +{ + public int MaxmimumMinigamePoints { get; set; } = 2_000; + + public short MinigamePointsCostPerMinigame { get; set; } = 100; + + public int ProductionCouponVnum { get; set; } = (int)ItemVnums.PRODUCTION_COUPON; + + public short ProductionCouponPointsAmount { get; set; } = 500; + + public long RepairDurabilityGoldCost { get; set; } = 100; + + public int RepairDurabilityCouponVnum { get; set; } = (int)ItemVnums.DURABILITY_COUPON; + + public int DurabilityCouponRepairingAmount { get; set; } = 300; + + public int DurabilityWarning { get; set; } = 1_000; + + public int MinigameMaximumRewards { get; set; } = 999; + + public int DoubleRewardCouponVnum { get; set; } = (int)ItemVnums.REWARD_COUPON; + + public int MinigameRewardsInventoryWarning { get; set; } = 880; + + public int MinigameRewardsInventoryUltimatum { get; set; } = 970; + + public AntiExploitConfiguration AntiExploitConfiguration { get; set; } = new(); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Configurations/Miniland/Minigame.cs b/srcs/WingsAPI.Game/Configurations/Miniland/Minigame.cs new file mode 100644 index 0000000..d00324e --- /dev/null +++ b/srcs/WingsAPI.Game/Configurations/Miniland/Minigame.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; + +namespace WingsEmu.Game.Configurations.Miniland; + +public class Minigame +{ + public int Vnum { get; set; } + + public MinigameType Type { get; set; } + + public int MinimumLevel { get; set; } + + public int MinimumReputation { get; set; } + + public List Rewards { get; set; } = new() + { + new() + { + RewardLevel = RewardLevel.FirstReward, + Rewards = new List + { + new() + { + Amount = 1, + Vnum = 1 + }, + new() + { + Amount = 2, + Vnum = 1014 + } + } + } + }; +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Configurations/Miniland/MinigameConfiguration.cs b/srcs/WingsAPI.Game/Configurations/Miniland/MinigameConfiguration.cs new file mode 100644 index 0000000..0433907 --- /dev/null +++ b/srcs/WingsAPI.Game/Configurations/Miniland/MinigameConfiguration.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; + +namespace WingsEmu.Game.Configurations.Miniland; + +public class MinigameConfiguration +{ + public List Minigames { get; set; } = new() + { + new() + }; + + public List ScoresHolders { get; set; } = new() + { + new() + }; + + public GlobalMinigameConfiguration Configuration { get; set; } = new(); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Configurations/Miniland/MinigameReward.cs b/srcs/WingsAPI.Game/Configurations/Miniland/MinigameReward.cs new file mode 100644 index 0000000..c936e0e --- /dev/null +++ b/srcs/WingsAPI.Game/Configurations/Miniland/MinigameReward.cs @@ -0,0 +1,8 @@ +namespace WingsEmu.Game.Configurations.Miniland; + +public class MinigameReward +{ + public int Vnum { get; set; } + + public int Amount { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Configurations/Miniland/MinigameRewards.cs b/srcs/WingsAPI.Game/Configurations/Miniland/MinigameRewards.cs new file mode 100644 index 0000000..a17bd58 --- /dev/null +++ b/srcs/WingsAPI.Game/Configurations/Miniland/MinigameRewards.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; + +namespace WingsEmu.Game.Configurations.Miniland; + +public class MinigameRewards +{ + public RewardLevel RewardLevel { get; set; } + + public ushort DurabilityCost { get; set; } + + public List Rewards { get; set; } = new(); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Configurations/Miniland/MinigameScoresHolder.cs b/srcs/WingsAPI.Game/Configurations/Miniland/MinigameScoresHolder.cs new file mode 100644 index 0000000..92d1c56 --- /dev/null +++ b/srcs/WingsAPI.Game/Configurations/Miniland/MinigameScoresHolder.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace WingsEmu.Game.Configurations.Miniland; + +public class MinigameScoresHolder +{ + public MinigameType Type { get; set; } + + public List Scores { get; set; } = new() + { + new() + }; +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Configurations/Miniland/MinigameType.cs b/srcs/WingsAPI.Game/Configurations/Miniland/MinigameType.cs new file mode 100644 index 0000000..99bf931 --- /dev/null +++ b/srcs/WingsAPI.Game/Configurations/Miniland/MinigameType.cs @@ -0,0 +1,11 @@ +namespace WingsEmu.Game.Configurations.Miniland; + +public enum MinigameType +{ + Quarry = 0, + Sawmill = 1, + Shooting = 2, + Fishing = 3, + Typewriter = 4, + Memory = 5 +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Configurations/Miniland/Miniland.cs b/srcs/WingsAPI.Game/Configurations/Miniland/Miniland.cs new file mode 100644 index 0000000..3416cf5 --- /dev/null +++ b/srcs/WingsAPI.Game/Configurations/Miniland/Miniland.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using WingsEmu.Game._enum; + +namespace WingsEmu.Game.Configurations.Miniland; + +public class Miniland +{ + public SerializablePosition ArrivalSerializablePosition = new() { X = 5, Y = 8 }; + + public int DefaultMaximumCapacity = 10; + public int MapVnum { get; set; } = (int)MapIds.MINILAND; + + public int MapItemVnum { get; set; } = (int)ItemVnums.MINILAND_THEME_SPRING; + + public List ForcedPlacings { get; set; } = new(); + + public List RestrictedZones { get; set; } = new(); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Configurations/Miniland/MinilandConfiguration.cs b/srcs/WingsAPI.Game/Configurations/Miniland/MinilandConfiguration.cs new file mode 100644 index 0000000..767a404 --- /dev/null +++ b/srcs/WingsAPI.Game/Configurations/Miniland/MinilandConfiguration.cs @@ -0,0 +1,7 @@ +using System.Collections.Generic; + +namespace WingsEmu.Game.Configurations.Miniland; + +public class MinilandConfiguration : List +{ +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Configurations/Miniland/RestrictedZone.cs b/srcs/WingsAPI.Game/Configurations/Miniland/RestrictedZone.cs new file mode 100644 index 0000000..7b5fd39 --- /dev/null +++ b/srcs/WingsAPI.Game/Configurations/Miniland/RestrictedZone.cs @@ -0,0 +1,10 @@ +namespace WingsEmu.Game.Configurations.Miniland; + +public class RestrictedZone +{ + public RestrictionType RestrictionTag { get; set; } + + public SerializablePosition Corner1 { get; set; } = new(); + + public SerializablePosition Corner2 { get; set; } = new(); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Configurations/Miniland/RestrictionType.cs b/srcs/WingsAPI.Game/Configurations/Miniland/RestrictionType.cs new file mode 100644 index 0000000..5a1316d --- /dev/null +++ b/srcs/WingsAPI.Game/Configurations/Miniland/RestrictionType.cs @@ -0,0 +1,10 @@ +namespace WingsEmu.Game.Configurations.Miniland; + +public enum RestrictionType +{ + Unconstructable, + OnlyTerraceObjects, + OnlyGardenObjects, + OnlySoilObjects, + OnlyMates +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Configurations/Miniland/RewardLevel.cs b/srcs/WingsAPI.Game/Configurations/Miniland/RewardLevel.cs new file mode 100644 index 0000000..ebf0359 --- /dev/null +++ b/srcs/WingsAPI.Game/Configurations/Miniland/RewardLevel.cs @@ -0,0 +1,11 @@ +namespace WingsEmu.Game.Configurations.Miniland; + +public enum RewardLevel : long +{ + NoReward = -1, + FirstReward = 0, + SecondReward = 1, + ThirdReward = 2, + FourthReward = 3, + FifthReward = 4 +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Configurations/Miniland/ScoreHolder.cs b/srcs/WingsAPI.Game/Configurations/Miniland/ScoreHolder.cs new file mode 100644 index 0000000..e6f64ef --- /dev/null +++ b/srcs/WingsAPI.Game/Configurations/Miniland/ScoreHolder.cs @@ -0,0 +1,13 @@ +using System; +using WingsEmu.Core; + +namespace WingsEmu.Game.Configurations.Miniland; + +public class ScoreHolder +{ + public RewardLevel RewardLevel { get; set; } = RewardLevel.NoReward; + + public Range ScoreRange { get; set; } + + public TimeSpan MinimumTimeOfCompletion { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Configurations/Miniland/SerializablePosition.cs b/srcs/WingsAPI.Game/Configurations/Miniland/SerializablePosition.cs new file mode 100644 index 0000000..a9e4cb3 --- /dev/null +++ b/srcs/WingsAPI.Game/Configurations/Miniland/SerializablePosition.cs @@ -0,0 +1,8 @@ +namespace WingsEmu.Game.Configurations.Miniland; + +public class SerializablePosition +{ + public short X { get; set; } + + public short Y { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Configurations/MonsterTalkingConfiguration.cs b/srcs/WingsAPI.Game/Configurations/MonsterTalkingConfiguration.cs new file mode 100644 index 0000000..e426eb4 --- /dev/null +++ b/srcs/WingsAPI.Game/Configurations/MonsterTalkingConfiguration.cs @@ -0,0 +1,42 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; + +namespace WingsEmu.Game.Configurations; + +public interface IMonsterTalkingConfig +{ + bool HasPossibleMessages(int monsterVnum); + IReadOnlyList PossibleMessage(int monsterVnum); +} + +public class MonsterTalkingConfig : IMonsterTalkingConfig +{ + private readonly ConcurrentDictionary> _monsterMessage = new(); + private readonly HashSet _monstersWithMessage = new(); + + public MonsterTalkingConfig(IEnumerable configurations) + { + foreach (MonsterTalkingConfiguration config in configurations) + { + if (config?.PossibleMessages == null || config.MobVnums == null) + { + continue; + } + + foreach (int mobVnum in config.MobVnums) + { + _monstersWithMessage.Add(mobVnum); + _monsterMessage.TryAdd(mobVnum, config.PossibleMessages); + } + } + } + + public bool HasPossibleMessages(int monsterVnum) => _monstersWithMessage.Contains(monsterVnum); + public IReadOnlyList PossibleMessage(int monsterVnum) => _monsterMessage.TryGetValue(monsterVnum, out List list) ? list : null; +} + +public class MonsterTalkingConfiguration +{ + public List PossibleMessages { get; set; } + public List MobVnums { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Configurations/NpcRunTypeQuestsConfiguration.cs b/srcs/WingsAPI.Game/Configurations/NpcRunTypeQuestsConfiguration.cs new file mode 100644 index 0000000..5d69002 --- /dev/null +++ b/srcs/WingsAPI.Game/Configurations/NpcRunTypeQuestsConfiguration.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using WingsEmu.Core.Extensions; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Configurations; + +public interface INpcRunTypeQuestsConfiguration +{ + List GetPossibleQuestsByNpcRunType(NpcRunType npcRunType); + bool HaveTheSameNpcRunType(int firstQuestVnum, int secondQuestVnum); +} + +public class NpcRunTypeQuestsConfiguration : INpcRunTypeQuestsConfiguration +{ + private readonly ImmutableDictionary _questsByNpcRunType; + + public NpcRunTypeQuestsConfiguration(IEnumerable questsByNpcRunType) + { + _questsByNpcRunType = questsByNpcRunType.ToImmutableDictionary(s => s.NpcRunType); + } + + public List GetPossibleQuestsByNpcRunType(NpcRunType npcRunType) => _questsByNpcRunType.ContainsKey(npcRunType) + ? _questsByNpcRunType.GetOrDefault(npcRunType).PossibleQuests + : new List(); + + public bool HaveTheSameNpcRunType(int firstQuestVnum, int secondQuestVnum) => + _questsByNpcRunType.Any(s => s.Value.PossibleQuests.Contains(firstQuestVnum) && s.Value.PossibleQuests.Contains(secondQuestVnum)); +} + +public class NpcRunTypeQuestsInfo +{ + public NpcRunType NpcRunType { get; set; } + public List PossibleQuests { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Configurations/PartnerSpecialistBasicConfiguration.cs b/srcs/WingsAPI.Game/Configurations/PartnerSpecialistBasicConfiguration.cs new file mode 100644 index 0000000..91a4edc --- /dev/null +++ b/srcs/WingsAPI.Game/Configurations/PartnerSpecialistBasicConfiguration.cs @@ -0,0 +1,30 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; + +namespace WingsEmu.Game.Configurations; + +public interface IPartnerSpecialistBasicConfig +{ + short GetAttackEffect(short morph); +} + +public class PartnerSpecialistBasicConfig : IPartnerSpecialistBasicConfig +{ + private readonly ConcurrentDictionary _effects = new(); + + public PartnerSpecialistBasicConfig(IEnumerable configurations) + { + foreach (PartnerSpecialistBasicConfiguration partner in configurations) + { + _effects.TryAdd(partner.MorphId, partner.Attack); + } + } + + public short GetAttackEffect(short morph) => (short)(_effects.TryGetValue(morph, out short attack) ? attack : 0); +} + +public class PartnerSpecialistBasicConfiguration +{ + public short MorphId { get; set; } + public short Attack { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Configurations/PerfUpgradeConfiguration.cs b/srcs/WingsAPI.Game/Configurations/PerfUpgradeConfiguration.cs new file mode 100644 index 0000000..e328312 --- /dev/null +++ b/srcs/WingsAPI.Game/Configurations/PerfUpgradeConfiguration.cs @@ -0,0 +1,23 @@ +using System.Runtime.Serialization; +using WingsEmu.Core; + +namespace WingsEmu.Game.Configurations; + +[DataContract] +public class PerfUpgradeConfiguration +{ + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public Range SpPerfUpgradeRange { get; set; } + + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public int GoldNeeded { get; set; } + + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public int StonesNeeded { get; set; } + + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public byte SuccessChance { get; set; } + + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public Range StatAmountRange { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Configurations/PeriodicQuestsConfiguration.cs b/srcs/WingsAPI.Game/Configurations/PeriodicQuestsConfiguration.cs new file mode 100644 index 0000000..d7d66bd --- /dev/null +++ b/srcs/WingsAPI.Game/Configurations/PeriodicQuestsConfiguration.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using System.Linq; +using WingsEmu.DTOs.Quests; + +namespace WingsEmu.Game.Configurations; + +public interface IPeriodicQuestsConfiguration +{ + bool IsDailyQuest(QuestDto quest); + IReadOnlyCollection GetDailyQuests(); + PeriodicQuestSet GetPeriodicQuestSetByQuestId(int questId); +} + +public class PeriodicQuestsConfiguration : IPeriodicQuestsConfiguration +{ + // References the starting quest of a questline + public HashSet DailyQuests { get; set; } = new(); + public bool IsDailyQuest(QuestDto quest) => DailyQuests?.Any(s => s?.QuestVnums?.Contains(quest.Id) ?? false) ?? false; + public IReadOnlyCollection GetDailyQuests() => DailyQuests; + public PeriodicQuestSet GetPeriodicQuestSetByQuestId(int questId) => DailyQuests.FirstOrDefault(s => s.QuestVnums.Contains(questId)); +} + +public class PeriodicQuestSet +{ + public HashSet QuestVnums { get; set; } = new(); + public int Id { get; set; } + public bool? PerNoswingsAccount { get; set; } = false; +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Configurations/PlayerRevivalConfiguration.cs b/srcs/WingsAPI.Game/Configurations/PlayerRevivalConfiguration.cs new file mode 100644 index 0000000..d42a6f2 --- /dev/null +++ b/srcs/WingsAPI.Game/Configurations/PlayerRevivalConfiguration.cs @@ -0,0 +1,23 @@ +using System; +using System.Runtime.Serialization; + +namespace WingsEmu.Game.Configurations; + +[DataContract] +public class PlayerRevivalConfiguration +{ + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public PlayerRevivalPenalization PlayerRevivalPenalization { get; set; } = new(); + + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public TimeSpan RevivalDialogDelay { get; set; } = TimeSpan.FromSeconds(2); + + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public TimeSpan ForcedRevivalDelay { get; set; } = TimeSpan.FromSeconds(30); + + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public TimeSpan Act4SealRevivalDelay { get; set; } = TimeSpan.FromSeconds(2); + + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public TimeSpan Act4RevivalDelay { get; set; } = TimeSpan.FromSeconds(30); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Configurations/PlayerRevivalPenalization.cs b/srcs/WingsAPI.Game/Configurations/PlayerRevivalPenalization.cs new file mode 100644 index 0000000..b981d4d --- /dev/null +++ b/srcs/WingsAPI.Game/Configurations/PlayerRevivalPenalization.cs @@ -0,0 +1,29 @@ +using System.Runtime.Serialization; +using WingsEmu.Game._enum; + +namespace WingsEmu.Game.Configurations; + +[DataContract] +public class PlayerRevivalPenalization +{ + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public byte MaxLevelWithoutRevivalPenalization { get; set; } = 20; + + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public int BaseMapRevivalPenalizationSaver { get; set; } = (int)ItemVnums.SEED_OF_POWER; + + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public int BaseMapRevivalPenalizationSaverAmount { get; set; } = 10; + + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public int BaseMapRevivalPenalizationDebuff { get; set; } = (int)BuffVnums.RESURRECTION_SIDE_EFFECTS; + + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public byte MaxLevelWithDignityPenalizationIncrement { get; set; } = 50; + + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public byte DignityPenalizationIncrementMultiplier { get; set; } = 1; + + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public long ArenaGoldPenalization { get; set; } = 100; +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Configurations/QuestTeleportDialogConfiguration.cs b/srcs/WingsAPI.Game/Configurations/QuestTeleportDialogConfiguration.cs new file mode 100644 index 0000000..cb30665 --- /dev/null +++ b/srcs/WingsAPI.Game/Configurations/QuestTeleportDialogConfiguration.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; + +namespace WingsEmu.Game.Configurations; + +public class QuestTeleportDialogConfiguration : List +{ +} + +public class QuestTeleportDialogInfo +{ + public int RunId { get; set; } + public int MapId { get; set; } + public bool AskForTeleport { get; set; } + public short PositionX { get; set; } + public short PositionY { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Configurations/QuestsRatesConfiguration.cs b/srcs/WingsAPI.Game/Configurations/QuestsRatesConfiguration.cs new file mode 100644 index 0000000..89e6cc7 --- /dev/null +++ b/srcs/WingsAPI.Game/Configurations/QuestsRatesConfiguration.cs @@ -0,0 +1,13 @@ +namespace WingsEmu.Game.Configurations; + +public class QuestsRatesConfiguration +{ + public int XpRate { get; set; } = 1; + public int JobXpRate { get; set; } = 1; + public int HeroXpRate { get; set; } = 1; + public int ReputRate { get; set; } = 1; + public int GoldRate { get; set; } = 1; + + public int BaseXp { get; set; } = 1000; + public int BaseGold { get; set; } = 1000; +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Configurations/RainbowBattleConfiguration.cs b/srcs/WingsAPI.Game/Configurations/RainbowBattleConfiguration.cs new file mode 100644 index 0000000..97c5043 --- /dev/null +++ b/srcs/WingsAPI.Game/Configurations/RainbowBattleConfiguration.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using WingsEmu.Core; + +namespace WingsEmu.Game.Configurations; + +public class RainbowBattleConfiguration +{ + public int MapId { get; init; } + public List Warnings { get; init; } + public int MinimumPlayers { get; init; } + public int MaximumPlayers { get; init; } + public short SecondsBeingFrozen { get; init; } + public short DelayBetweenCapture { get; init; } + + public short RedStartX { get; init; } + public short RedEndX { get; init; } + public short BlueStartX { get; init; } + public short BlueEndX { get; init; } + + public short RedStartY { get; init; } + public short RedEndY { get; init; } + public short BlueStartY { get; init; } + public short BlueEndY { get; init; } + + public short UnfreezeActivityPoints { get; init; } + public short CaptureActivityPoints { get; init; } + public short UsingSkillActivityPoints { get; init; } + public short NeededActivityPoints { get; init; } + public short KillActivityPoints { get; init; } + public short DeathActivityPoints { get; init; } + public short WalkingActivityPoints { get; init; } + + public List> LevelRange { get; init; } + + public List MainFlags { get; init; } + public List MediumFlags { get; init; } + public List SmallFlags { get; init; } + + public int ReputationMultiplier { get; init; } +} + +public class FlagPosition +{ + public short X { get; init; } + public short Y { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Configurations/RelictConfiguration.cs b/srcs/WingsAPI.Game/Configurations/RelictConfiguration.cs new file mode 100644 index 0000000..0168d4c --- /dev/null +++ b/srcs/WingsAPI.Game/Configurations/RelictConfiguration.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; + +namespace WingsEmu.Game.Configurations; + +public class RelictConfiguration : List +{ +} + +public class RelictConfigurationInfo +{ + public int RelictVnum { get; set; } + public int ExaminationCost { get; set; } + public List Rolls { get; set; } +} + +public class RelictRollCategory +{ + public int Chance { get; set; } + public List Items { get; set; } +} + +public class RelictRollItem +{ + public int ItemVnum { get; set; } + public int Amount { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Configurations/ReputationConfiguration.cs b/srcs/WingsAPI.Game/Configurations/ReputationConfiguration.cs new file mode 100644 index 0000000..184f1f4 --- /dev/null +++ b/srcs/WingsAPI.Game/Configurations/ReputationConfiguration.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using System.Linq; +using WingsEmu.Game._enum; + +namespace WingsEmu.Game.Configurations; + +public interface IReputationConfiguration +{ + public ReputationInfo GetReputationInfo(long reputation, int place); +} + +public class ReputationConfiguration : IReputationConfiguration +{ + private readonly List _reputationInfos; + + public ReputationConfiguration(IEnumerable reputationInfos) + { + _reputationInfos = reputationInfos.OrderByDescending(s => s.Threshold).ToList(); + } + + public ReputationInfo GetReputationInfo(long reputation, int place) + { + var reputationInfo = _reputationInfos.Where(s => reputation >= s.Threshold).ToList(); + return reputationInfo.FirstOrDefault(s => place >= s.MaxPlayers && place <= s.MinPlayers) ?? reputationInfo.FirstOrDefault(); + } +} + +public class ReputationInfo +{ + public ReputationType Rank { get; set; } + public long Threshold { get; set; } + public int? MinPlayers { get; set; } + public int? MaxPlayers { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Configurations/ReturnDefaultConfiguration.cs b/srcs/WingsAPI.Game/Configurations/ReturnDefaultConfiguration.cs new file mode 100644 index 0000000..b85192d --- /dev/null +++ b/srcs/WingsAPI.Game/Configurations/ReturnDefaultConfiguration.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using WingsAPI.Packets.Enums; +using WingsEmu.Game._enum; + +namespace WingsEmu.Game.Configurations; + +public interface IRespawnDefaultConfiguration +{ + public RespawnDefault GetReturn(RespawnType type); + public RespawnDefault GetReturnAct5(Act5RespawnType type); +} + +public class RespawnDefaultConfiguration : IRespawnDefaultConfiguration +{ + private readonly ImmutableDictionary _respawns; + private readonly Dictionary _respawnsAct5 = new(); + + public RespawnDefaultConfiguration(IEnumerable returns) + { + _respawns = returns.ToImmutableDictionary(s => s.Name); + foreach (RespawnDefault getReturn in returns) + { + Act5RespawnType? act5RespawnType = getReturn.Name switch + { + RespawnType.MORTAZ_DESERT_PORT => Act5RespawnType.MORTAZ_DESERT_PORT, + RespawnType.AKAMUR_CAMP => Act5RespawnType.AKAMUR_CAMP, + RespawnType.DESERT_EAGLE_CITY => Act5RespawnType.DESERT_EAGLE_CITY, + _ => null + }; + + if (act5RespawnType is null) + { + continue; + } + + _respawnsAct5[act5RespawnType.Value] = getReturn; + } + } + + public RespawnDefault GetReturn(RespawnType type) => _respawns.GetValueOrDefault(type); + public RespawnDefault GetReturnAct5(Act5RespawnType type) => _respawnsAct5.GetValueOrDefault(type); +} + +public class RespawnDefault +{ + public RespawnType Name { get; set; } + public short MapId { get; set; } + public short MapX { get; set; } + public short MapY { get; set; } + public byte Radius { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Configurations/SpPerfStats.cs b/srcs/WingsAPI.Game/Configurations/SpPerfStats.cs new file mode 100644 index 0000000..1b259af --- /dev/null +++ b/srcs/WingsAPI.Game/Configurations/SpPerfStats.cs @@ -0,0 +1,13 @@ +namespace WingsEmu.Game.Configurations; + +public enum SpPerfStats +{ + Attack, + Defense, + Element, + HpMp, + ResistanceFire, + ResistanceWater, + ResistanceLight, + ResistanceDark +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Configurations/SpPerfectionConfiguration.cs b/srcs/WingsAPI.Game/Configurations/SpPerfectionConfiguration.cs new file mode 100644 index 0000000..0d8a95d --- /dev/null +++ b/srcs/WingsAPI.Game/Configurations/SpPerfectionConfiguration.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using System.Runtime.Serialization; +using WingsEmu.Game._enum; + +namespace WingsEmu.Game.Configurations; + +[DataContract] +public class SpPerfectionConfiguration +{ + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public List PerfUpgradeConfigurations { get; set; } = new() + { + new() + }; + + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public Dictionary StatProbabilityConfiguration { get; set; } = new() + { + { SpPerfStats.Attack, 10 }, + { SpPerfStats.Defense, 10 }, + { SpPerfStats.Element, 10 }, + { SpPerfStats.HpMp, 10 }, + { SpPerfStats.ResistanceFire, 10 }, + { SpPerfStats.ResistanceWater, 10 }, + { SpPerfStats.ResistanceLight, 10 }, + { SpPerfStats.ResistanceDark, 10 } + }; + + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public List SpStonesLinks { get; set; } = new() + { + new() + { + StoneVnum = (int)ItemVnums.RUBY_COMP, + SpVnums = new List + { + (int)ItemVnums.GLADIATOR + } + } + }; +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Configurations/SpStoneLink.cs b/srcs/WingsAPI.Game/Configurations/SpStoneLink.cs new file mode 100644 index 0000000..b0cb6ed --- /dev/null +++ b/srcs/WingsAPI.Game/Configurations/SpStoneLink.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace WingsEmu.Game.Configurations; + +[DataContract] +public class SpStoneLink +{ + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public int StoneVnum { get; set; } + + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public List SpVnums { get; set; } = new(); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Configurations/SpUpgradeConfiguration.cs b/srcs/WingsAPI.Game/Configurations/SpUpgradeConfiguration.cs new file mode 100644 index 0000000..8a15ffc --- /dev/null +++ b/srcs/WingsAPI.Game/Configurations/SpUpgradeConfiguration.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace WingsEmu.Game.Configurations; + +[DataContract] +public class SpUpgradeConfiguration : List +{ +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Configurations/SpecialItem.cs b/srcs/WingsAPI.Game/Configurations/SpecialItem.cs new file mode 100644 index 0000000..386be11 --- /dev/null +++ b/srcs/WingsAPI.Game/Configurations/SpecialItem.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace WingsEmu.Game.Configurations; + +[DataContract] +public class SpecialItem +{ + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public int ItemVnum { get; set; } + + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public int Amount { get; set; } + + [DataMember(EmitDefaultValue = true, IsRequired = false)] + public List SpVnums { get; set; } = new(); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Configurations/SubActsConfiguration.cs b/srcs/WingsAPI.Game/Configurations/SubActsConfiguration.cs new file mode 100644 index 0000000..662e57b --- /dev/null +++ b/srcs/WingsAPI.Game/Configurations/SubActsConfiguration.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using System.Linq; +using WingsEmu.Core.Extensions; + +namespace WingsEmu.Game.Configurations; + +public interface ISubActConfiguration +{ + IEnumerable GetConfigurations(); + SubActsConfiguration GetConfigurationById(long id); + SubActsConfiguration GetConfigurationByTimeSpaceId(long id); +} + +public class SubActConfiguration : ISubActConfiguration +{ + private readonly List _configurations; + private readonly Dictionary _configurationsById = new(); + private readonly Dictionary _configurationsByTimeSpaceId = new(); + + public SubActConfiguration(IEnumerable configurations) + { + _configurations = configurations.ToList(); + + foreach (SubActsConfiguration configuration in _configurations) + { + _configurationsById[configuration.Id] = configuration; + } + + foreach (SubActsConfiguration configuration in configurations.Where(x => x.TimeSpaces != null)) + { + foreach (long timeSpaceId in configuration.TimeSpaces) + { + _configurationsByTimeSpaceId[timeSpaceId] = configuration; + } + } + } + + public IEnumerable GetConfigurations() => _configurations; + + public SubActsConfiguration GetConfigurationById(long id) => _configurationsById.GetOrDefault(id); + + public SubActsConfiguration GetConfigurationByTimeSpaceId(long id) => _configurationsByTimeSpaceId.GetOrDefault(id); +} + +public class SubActsConfiguration +{ + public short Id { get; set; } + public byte Act { get; set; } + public byte SubAct { get; set; } + public long[] TimeSpaces { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Configurations/TimeSpaceConfiguration.cs b/srcs/WingsAPI.Game/Configurations/TimeSpaceConfiguration.cs new file mode 100644 index 0000000..4321ac0 --- /dev/null +++ b/srcs/WingsAPI.Game/Configurations/TimeSpaceConfiguration.cs @@ -0,0 +1,78 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using WingsEmu.Core.Extensions; + +namespace WingsEmu.Game.Configurations; + +public interface ITimeSpaceConfiguration +{ + TimeSpaceFileConfiguration GetTimeSpaceConfiguration(long timeSpaceId); + IEnumerable GetTimeSpaceConfigurationsByMapId(int mapId); +} + +public class TimeSpaceConfiguration : ITimeSpaceConfiguration +{ + private readonly ImmutableDictionary _configurations; + private readonly Dictionary> _configurationsByMapId = new(); + + public TimeSpaceConfiguration(IEnumerable configurations) + { + _configurations = configurations.ToImmutableDictionary(s => s.TsId); + + foreach (TimeSpaceFileConfiguration configuration in configurations.Where(x => x.Placement != null)) + { + foreach (TimeSpacePlacement placement in configuration.Placement) + { + if (!_configurationsByMapId.TryGetValue(placement.MapId, out List list)) + { + list = new List(); + _configurationsByMapId[placement.MapId] = list; + } + + list.Add(configuration); + } + } + } + + public TimeSpaceFileConfiguration GetTimeSpaceConfiguration(long timeSpaceId) => _configurations.GetOrDefault(timeSpaceId); + public IEnumerable GetTimeSpaceConfigurationsByMapId(int mapId) => _configurationsByMapId.GetOrDefault(mapId); +} + +public class TimeSpaceFileConfiguration +{ + public long TsId { get; set; } + public byte MinLevel { get; set; } + public byte MaxLevel { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public bool IsHero { get; set; } + public bool IsSpecial { get; set; } + public bool IsHidden { get; set; } + public byte MinPlayers { get; set; } + public byte MaxPlayers { get; set; } + public byte SeedsOfPowerRequired { get; set; } + public TimeSpaceRewards Rewards { get; set; } + public List Placement { get; set; } + public int? ReputationMultiplier { get; set; } +} + +public class TimeSpaceRewards +{ + public List Draw { get; set; } + public List Special { get; set; } + public List Bonus { get; set; } +} + +public class TimeSpaceItemConfiguration +{ + public short ItemVnum { get; set; } + public short Amount { get; set; } +} + +public class TimeSpacePlacement +{ + public int MapId { get; set; } + public short MapX { get; set; } + public short MapY { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Configurations/TimeSpaceNpcRunConfiguration.cs b/srcs/WingsAPI.Game/Configurations/TimeSpaceNpcRunConfiguration.cs new file mode 100644 index 0000000..0940c92 --- /dev/null +++ b/srcs/WingsAPI.Game/Configurations/TimeSpaceNpcRunConfiguration.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; + +namespace WingsEmu.Game.Configurations; + +public interface ITimeSpaceNpcRunConfig +{ + int? GetQuestByTimeSpaceId(long timeSpaceId); + bool DoesTimeSpaceExist(long timeSpaceId); +} + +public class TimeSpaceNpcRunConfig : ITimeSpaceNpcRunConfig +{ + private readonly HashSet _timeSpaces = new(); + private readonly Dictionary _timeSpacesWithQuests = new(); + + public TimeSpaceNpcRunConfig(IEnumerable configurations) + { + foreach (TimeSpaceNpcRunConfiguration configuration in configurations) + { + _timeSpaces.Add(configuration.TimeSpaceId); + if (!configuration.QuestVnum.HasValue) + { + continue; + } + + _timeSpacesWithQuests[configuration.TimeSpaceId] = configuration.QuestVnum.Value; + } + } + + public int? GetQuestByTimeSpaceId(long timeSpaceId) => _timeSpacesWithQuests.TryGetValue(timeSpaceId, out int questVnum) ? questVnum : null; + public bool DoesTimeSpaceExist(long timeSpaceId) => _timeSpaces.Contains(timeSpaceId); +} + +public class TimeSpaceNpcRunConfiguration +{ + public int? QuestVnum { get; set; } + public long TimeSpaceId { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Configurations/UpgradeConfiguration.cs b/srcs/WingsAPI.Game/Configurations/UpgradeConfiguration.cs new file mode 100644 index 0000000..69b6955 --- /dev/null +++ b/srcs/WingsAPI.Game/Configurations/UpgradeConfiguration.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using System.Runtime.Serialization; +using WingsEmu.Core; + +namespace WingsEmu.Game.Configurations; + +[DataContract] +public class UpgradeConfiguration +{ + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public Range SpUpgradeRange { get; set; } + + [DataMember(EmitDefaultValue = true, IsRequired = false)] + public byte SpLevelNeeded { get; set; } + + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public short DestroyChance { get; set; } + + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public short SuccessChance { get; set; } + + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public long GoldNeeded { get; set; } + + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public int FeatherNeeded { get; set; } + + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public int FullMoonsNeeded { get; set; } + + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public int ScrollVnum { get; set; } + + [DataMember(EmitDefaultValue = true, IsRequired = true)] + public List SpecialItemsNeeded { get; set; } = new() + { + new() + }; +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Configurations/UpgradeItemConfiguration.cs b/srcs/WingsAPI.Game/Configurations/UpgradeItemConfiguration.cs new file mode 100644 index 0000000..bfd8b3d --- /dev/null +++ b/srcs/WingsAPI.Game/Configurations/UpgradeItemConfiguration.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace WingsEmu.Game.Configurations; + +[DataContract] +public class UpgradeItemConfiguration : List +{ +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Configurations/UpgradeItemStats.cs b/srcs/WingsAPI.Game/Configurations/UpgradeItemStats.cs new file mode 100644 index 0000000..cee5b0d --- /dev/null +++ b/srcs/WingsAPI.Game/Configurations/UpgradeItemStats.cs @@ -0,0 +1,12 @@ +namespace WingsEmu.Game.Configurations; + +public class UpgradeItemStats +{ + public short Upgrade { get; set; } + public short UpFix { get; set; } + public short UpFail { get; set; } + + public int Gold { get; set; } + public int Cella { get; set; } + public short Gem { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Configurations/UpgradeNormalItemConfiguration.cs b/srcs/WingsAPI.Game/Configurations/UpgradeNormalItemConfiguration.cs new file mode 100644 index 0000000..9b4a60b --- /dev/null +++ b/srcs/WingsAPI.Game/Configurations/UpgradeNormalItemConfiguration.cs @@ -0,0 +1,8 @@ +using System.Runtime.Serialization; + +namespace WingsEmu.Game.Configurations; + +[DataContract] +public class UpgradeNormalItemConfiguration : UpgradeItemConfiguration +{ +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Configurations/UpgradePhenomenalItemConfiguration.cs b/srcs/WingsAPI.Game/Configurations/UpgradePhenomenalItemConfiguration.cs new file mode 100644 index 0000000..ff50484 --- /dev/null +++ b/srcs/WingsAPI.Game/Configurations/UpgradePhenomenalItemConfiguration.cs @@ -0,0 +1,8 @@ +using System.Runtime.Serialization; + +namespace WingsEmu.Game.Configurations; + +[DataContract] +public class UpgradePhenomenalItemConfiguration : UpgradeItemConfiguration +{ +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Core/ECS/IMapSystem.cs b/srcs/WingsAPI.Game/Core/ECS/IMapSystem.cs new file mode 100644 index 0000000..2144a05 --- /dev/null +++ b/srcs/WingsAPI.Game/Core/ECS/IMapSystem.cs @@ -0,0 +1,18 @@ +using System; + +namespace WingsEmu.Game._ECS; + +public interface IMapSystem +{ + string Name { get; } + + + /// + /// + /// + /// + void ProcessTick(DateTime date, bool isTickRefresh = false); + + void PutIdleState(); + void Clear(); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Core/ECS/ITickManager.cs b/srcs/WingsAPI.Game/Core/ECS/ITickManager.cs new file mode 100644 index 0000000..8bf9498 --- /dev/null +++ b/srcs/WingsAPI.Game/Core/ECS/ITickManager.cs @@ -0,0 +1,15 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Game._ECS; + +public interface ITickManager +{ + void AddProcessable(ITickProcessable processable); + + void RemoveProcessable(ITickProcessable processable); + + void Start(); + void Stop(); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Core/ECS/ITickProcessable.cs b/srcs/WingsAPI.Game/Core/ECS/ITickProcessable.cs new file mode 100644 index 0000000..c361ee1 --- /dev/null +++ b/srcs/WingsAPI.Game/Core/ECS/ITickProcessable.cs @@ -0,0 +1,14 @@ +using System; + +namespace WingsEmu.Game._ECS; + +public interface ITickProcessable +{ + Guid Id { get; } + string Name { get; } + + /// + /// + /// + void ProcessTick(DateTime date); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Core/ECS/Systems/IBattleSystem.cs b/srcs/WingsAPI.Game/Core/ECS/Systems/IBattleSystem.cs new file mode 100644 index 0000000..38febd4 --- /dev/null +++ b/srcs/WingsAPI.Game/Core/ECS/Systems/IBattleSystem.cs @@ -0,0 +1,15 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsEmu.Game.Battle; + +namespace WingsEmu.Game._ECS.Systems; + +public interface IBattleSystem +{ + void AddCastHitRequest(HitProcessable hitProcessable); + void AddCastBuffRequest(BuffProcessable buffProcessable); + void AddHitRequest(HitRequest hitRequest); + void AddBuffRequest(BuffRequest buffRequest); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Core/ECS/Systems/ICharacterSystem.cs b/srcs/WingsAPI.Game/Core/ECS/Systems/ICharacterSystem.cs new file mode 100644 index 0000000..30e43b8 --- /dev/null +++ b/srcs/WingsAPI.Game/Core/ECS/Systems/ICharacterSystem.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Helpers.Damages; + +namespace WingsEmu.Game._ECS.Systems; + +public interface ICharacterSystem +{ + IPlayerEntity GetCharacterById(long id); + IReadOnlyList GetCharacters(); + IReadOnlyList GetCharacters(Func predicate); + IReadOnlyList GetCharactersInRange(Position pos, short distance, Func predicate); + IReadOnlyList GetCharactersInRange(Position pos, short distance); + IReadOnlyList GetClosestCharactersInRange(Position pos, short distance); + IReadOnlyList GetAliveCharacters(); + IReadOnlyList GetAliveCharacters(Func predicate); + IReadOnlyList GetAliveCharactersInRange(Position pos, short distance, Func predicate); + IReadOnlyList GetAliveCharactersInRange(Position pos, short distance); + void AddCharacter(IPlayerEntity character); + void RemoveCharacter(IPlayerEntity entity); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Core/ECS/Systems/IDropSystem.cs b/srcs/WingsAPI.Game/Core/ECS/Systems/IDropSystem.cs new file mode 100644 index 0000000..4aa5081 --- /dev/null +++ b/srcs/WingsAPI.Game/Core/ECS/Systems/IDropSystem.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using WingsEmu.Game.Maps; + +namespace WingsEmu.Game._ECS.Systems; + +public interface IDropSystem +{ + IReadOnlyList Drops { get; } + void AddDrop(MapItem item); + bool RemoveDrop(long dropId); + bool HasDrop(long dropId); + MapItem GetDrop(long dropId); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Core/ECS/Systems/IMateSystem.cs b/srcs/WingsAPI.Game/Core/ECS/Systems/IMateSystem.cs new file mode 100644 index 0000000..923a545 --- /dev/null +++ b/srcs/WingsAPI.Game/Core/ECS/Systems/IMateSystem.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Mates; + +namespace WingsEmu.Game._ECS.Systems; + +public interface IMateSystem +{ + IReadOnlyList GetAliveMates(); + IReadOnlyList GetAliveMates(Func predicate); + IReadOnlyList GetAliveMatesInRange(Position position, short range); + IReadOnlyList GetClosestMatesInRange(Position position, short range); + IReadOnlyList GetAliveMatesInRange(Position position, short range, Func predicate); + IMateEntity GetMateById(long mateId); + void AddMate(IMateEntity entity); + void RemoveMate(IMateEntity entity); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Core/ECS/Systems/IMonsterSystem.cs b/srcs/WingsAPI.Game/Core/ECS/Systems/IMonsterSystem.cs new file mode 100644 index 0000000..0ee95d8 --- /dev/null +++ b/srcs/WingsAPI.Game/Core/ECS/Systems/IMonsterSystem.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Helpers.Damages; + +namespace WingsEmu.Game._ECS.Systems; + +public interface IMonsterSystem +{ + IMonsterEntity GetMonsterById(long id); + IMonsterEntity GetMonsterByUniqueId(Guid id); + IReadOnlyList GetAliveMonsters(); + IReadOnlyList GetDeadMonsters(); + IReadOnlyList GetAliveMonsters(Func predicate); + IReadOnlyList GetAliveMonstersInRange(Position pos, short distance); + IReadOnlyList GetClosestMonstersInRange(Position pos, short distance); + void AddMonster(IMonsterEntity entity); + void RemoveMonster(IMonsterEntity entity); + + /// + /// This method will be used only if player/mate used some kind of debuff or voke to provoke monster, but it won't be + /// added to Monster's Damagers list. + /// + /// + /// + /// + void AddEntityToTargets(IMonsterEntity monsterEntity, IBattleEntity target); + + void MonsterRefreshTarget(IMonsterEntity target, IBattleEntity caster, in DateTime time, bool isByAttacking = false); + void ForgetAll(IMonsterEntity monsterEntity, in DateTime time, bool clearDamagers = true); + void RemoveTarget(IMonsterEntity monsterEntity, IBattleEntity entity, bool checkIfPlayer = false); + + void ActivateMode(IMonsterEntity monsterEntity); + void DeactivateMode(IMonsterEntity monsterEntity); + + void IncreaseMonsterDeathsOnMap(); + long MonsterDeathsOnMap(); + byte CurrentVessels(); + bool IsSummonLimitReached(int? summonerId, SummonType? summonSummonType); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Core/ECS/Systems/INpcSystem.cs b/srcs/WingsAPI.Game/Core/ECS/Systems/INpcSystem.cs new file mode 100644 index 0000000..00c120e --- /dev/null +++ b/srcs/WingsAPI.Game/Core/ECS/Systems/INpcSystem.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Helpers.Damages; + +namespace WingsEmu.Game._ECS.Systems; + +public interface INpcSystem +{ + IReadOnlyList GetAliveNpcs(); + IReadOnlyList GetAliveNpcs(Func predicate); + IReadOnlyList GetPassiveNpcs(); + IReadOnlyList GetAliveNpcsInRange(Position pos, short distance, Func predicate); + IReadOnlyList GetAliveNpcsInRange(Position pos, short distance); + IReadOnlyList GetClosestNpcsInRange(Position pos, short distance); + void NpcRefreshTarget(INpcEntity npcEntity, IBattleEntity target); + INpcEntity GetNpcById(long id); + void AddNpc(INpcEntity entity); + void RemoveNpc(INpcEntity entity); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Core/GuriHandling/Event/GuriEvent.cs b/srcs/WingsAPI.Game/Core/GuriHandling/Event/GuriEvent.cs new file mode 100644 index 0000000..0de31f5 --- /dev/null +++ b/srcs/WingsAPI.Game/Core/GuriHandling/Event/GuriEvent.cs @@ -0,0 +1,15 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game._Guri.Event; + +public class GuriEvent : PlayerEvent +{ + public long EffectId { get; set; } + + public int Data { get; set; } + + public long? User { get; set; } + + public string Value { get; set; } + public string[] Packet { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Core/GuriHandling/IGuriHandler.cs b/srcs/WingsAPI.Game/Core/GuriHandling/IGuriHandler.cs new file mode 100644 index 0000000..14eff45 --- /dev/null +++ b/srcs/WingsAPI.Game/Core/GuriHandling/IGuriHandler.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; +using WingsEmu.Game._Guri.Event; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Game._Guri; + +public interface IGuriHandler +{ + long GuriEffectId { get; } + + Task ExecuteAsync(IClientSession session, GuriEvent e); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Core/GuriHandling/IGuriHandlerContainer.cs b/srcs/WingsAPI.Game/Core/GuriHandling/IGuriHandlerContainer.cs new file mode 100644 index 0000000..e5214dd --- /dev/null +++ b/srcs/WingsAPI.Game/Core/GuriHandling/IGuriHandlerContainer.cs @@ -0,0 +1,16 @@ +using System.Threading.Tasks; +using WingsEmu.Game._Guri.Event; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Game._Guri; + +public interface IGuriHandlerContainer +{ + void Register(IGuriHandler handler); + + void Unregister(long guriEffectId); + + void Handle(IClientSession player, GuriEvent args); + + Task HandleAsync(IClientSession player, GuriEvent args); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Core/ItemHandling/Configuration/ICostumeScrollConfiguration.cs b/srcs/WingsAPI.Game/Core/ItemHandling/Configuration/ICostumeScrollConfiguration.cs new file mode 100644 index 0000000..9a2e01b --- /dev/null +++ b/srcs/WingsAPI.Game/Core/ItemHandling/Configuration/ICostumeScrollConfiguration.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using System.Linq; +using WingsEmu.Core.Extensions; + +namespace WingsEmu.Game._ItemUsage.Configuration; + +public interface ICostumeScrollConfiguration +{ + IReadOnlyList GetScrollMorphs(short scrollId); +} + +public class CostumeScrollConfiguration : ICostumeScrollConfiguration +{ + private readonly Dictionary _morphs; + + public CostumeScrollConfiguration(CostumeMorphFileConfiguration morphs) + { + _morphs = morphs.ToDictionary(s => s.ScrollItemVnum); + } + + public IReadOnlyList GetScrollMorphs(short scrollId) + { + CostumeMorph tmp = _morphs.GetOrDefault(scrollId); + return tmp?.PossibleMorphs; + } +} + +public class CostumeMorphFileConfiguration : List +{ +} + +public class CostumeMorph +{ + public int ScrollItemVnum { get; set; } + public List PossibleMorphs { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Core/ItemHandling/Configuration/ISpPartnerConfiguration.cs b/srcs/WingsAPI.Game/Core/ItemHandling/Configuration/ISpPartnerConfiguration.cs new file mode 100644 index 0000000..9cd0392 --- /dev/null +++ b/srcs/WingsAPI.Game/Core/ItemHandling/Configuration/ISpPartnerConfiguration.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using System.Collections.Immutable; + +namespace WingsEmu.Game._ItemUsage.Configuration; + +public interface ISpPartnerConfiguration +{ + SpPartnerInfo GetByMorph(short morphId); +} + +public class SpPartnerConfiguration : ISpPartnerConfiguration +{ + private readonly ImmutableDictionary _partnerInfo; + + public SpPartnerConfiguration(IEnumerable partnerInfo) + { + _partnerInfo = partnerInfo.ToImmutableDictionary(s => s.MorphId); + } + + public SpPartnerInfo GetByMorph(short morphId) => _partnerInfo.GetValueOrDefault(morphId); +} + +public class SpPartnerInfo +{ + public int MorphId { get; set; } + public string Name { get; set; } + public int BuffId { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Core/ItemHandling/Configuration/ISpWingConfiguration.cs b/srcs/WingsAPI.Game/Core/ItemHandling/Configuration/ISpWingConfiguration.cs new file mode 100644 index 0000000..13c9131 --- /dev/null +++ b/srcs/WingsAPI.Game/Core/ItemHandling/Configuration/ISpWingConfiguration.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using System.Linq; +using WingsEmu.Core.Extensions; + +namespace WingsEmu.Game._ItemUsage.Configuration; + +public interface ISpWingConfiguration +{ + SpWingInfo GetSpWingInfo(int wingType); +} + +public class SpWingConfiguration : ISpWingConfiguration +{ + private readonly SpWingInfoConfiguration _conf; + + public SpWingConfiguration(SpWingInfoConfiguration conf) => _conf = conf; + + public SpWingInfo GetSpWingInfo(int wingType) => _conf.GetOrDefault(wingType); +} + +public class MateBuffConfigsContainer : IMateBuffConfigsContainer +{ + private readonly Dictionary _conf; + + public MateBuffConfigsContainer(MateBuffConfiguration conf) + { + _conf = conf.ToDictionary(s => s.PetVnum, s => s); + } + + public MateBuffIndividualConfig GetMateBuffInfo(int mateVnum) => _conf.GetOrDefault(mateVnum); +} + +public interface IMateBuffConfigsContainer +{ + MateBuffIndividualConfig GetMateBuffInfo(int mateVnum); +} + +public class MateBuffConfiguration : List +{ +} + +public class MateBuffIndividualConfig +{ + public int PetVnum { get; set; } + public List BuffIds { get; set; } +} + +public class SpWingInfoConfiguration : Dictionary +{ +} + +public class SpWingInfo +{ + public List Buffs { get; set; } +} + +public class WingBuff +{ + public int BuffId { get; set; } + public bool IsPermanent { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Core/ItemHandling/Event/InventoryUseItemEvent.cs b/srcs/WingsAPI.Game/Core/ItemHandling/Event/InventoryUseItemEvent.cs new file mode 100644 index 0000000..120d275 --- /dev/null +++ b/srcs/WingsAPI.Game/Core/ItemHandling/Event/InventoryUseItemEvent.cs @@ -0,0 +1,13 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Game.Inventory; + +namespace WingsEmu.Game._ItemUsage.Event; + +public class InventoryUseItemEvent : PlayerEvent +{ + public InventoryItem Item { get; set; } + + public byte Option { get; set; } + + public string[] Packet { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Core/ItemHandling/Event/InventoryUsedItemEvent.cs b/srcs/WingsAPI.Game/Core/ItemHandling/Event/InventoryUsedItemEvent.cs new file mode 100644 index 0000000..14f66d1 --- /dev/null +++ b/srcs/WingsAPI.Game/Core/ItemHandling/Event/InventoryUsedItemEvent.cs @@ -0,0 +1,9 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Game.Inventory; + +namespace WingsEmu.Game.Core.ItemHandling.Event; + +public class InventoryUsedItemEvent : PlayerEvent +{ + public InventoryItem Item { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Core/ItemHandling/IItemHandler.cs b/srcs/WingsAPI.Game/Core/ItemHandling/IItemHandler.cs new file mode 100644 index 0000000..ce33b79 --- /dev/null +++ b/srcs/WingsAPI.Game/Core/ItemHandling/IItemHandler.cs @@ -0,0 +1,15 @@ +using System.Threading.Tasks; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game._ItemUsage; + +public interface IItemHandler +{ + ItemType ItemType { get; } + + long[] Effects { get; } + + Task HandleAsync(IClientSession session, InventoryUseItemEvent e); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Core/ItemHandling/IItemHandlerContainer.cs b/srcs/WingsAPI.Game/Core/ItemHandling/IItemHandlerContainer.cs new file mode 100644 index 0000000..cacf331 --- /dev/null +++ b/srcs/WingsAPI.Game/Core/ItemHandling/IItemHandlerContainer.cs @@ -0,0 +1,18 @@ +using System.Threading.Tasks; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Game._ItemUsage; + +public interface IItemHandlerContainer +{ + Task RegisterItemHandler(IItemHandler handler); + Task RegisterItemHandler(IItemUsageByVnumHandler handler); + + Task UnregisterAsync(IItemHandler handler); + Task UnregisterAsync(IItemUsageByVnumHandler handler); + + void Handle(IClientSession player, InventoryUseItemEvent e); + + Task HandleAsync(IClientSession player, InventoryUseItemEvent e); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Core/ItemHandling/IItemUsageByVnumHandler.cs b/srcs/WingsAPI.Game/Core/ItemHandling/IItemUsageByVnumHandler.cs new file mode 100644 index 0000000..db2c04a --- /dev/null +++ b/srcs/WingsAPI.Game/Core/ItemHandling/IItemUsageByVnumHandler.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Game._ItemUsage; + +public interface IItemUsageByVnumHandler +{ + long[] Vnums { get; } + + Task HandleAsync(IClientSession session, InventoryUseItemEvent e); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Core/Multilanguage/GameDialogKey.cs b/srcs/WingsAPI.Game/Core/Multilanguage/GameDialogKey.cs new file mode 100644 index 0000000..c3ad05c --- /dev/null +++ b/srcs/WingsAPI.Game/Core/Multilanguage/GameDialogKey.cs @@ -0,0 +1,928 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Game._i18n; + +public enum GameDialogKey +{ + /* + * RELATIONSHIPS + */ + FRIEND_INFO_DELETED, + FRIEND_INFO_FRIENDLIST_FULL, + FRIEND_INFO_ALREADY_FRIEND, + FRIEND_INFO_REQUEST_BLOCKED, + FRIEND_DIALOG_DO_YOU_WANT_TO_ADD, + FRIEND_INFO_ADDED, + FRIEND_INFO_REFUSED, + FRIEND_TALKMESSAGE_DIFFRENT_FACTION, + FRIEND_CHATMESSAGE_CHARACTER_LOGGED_IN, + FRIEND_CHATMESSAGE_CHARACTER_LOGGED_OUT, + FRIEND_SHOUTMESSAGE_CHARACTER_NOT_FRIEND, + FRIEND_MESSAGE_NOT_FRIEND, + + BLACKLIST_INFO_DELETED, + BLACKLIST_INFO_ADDED, + BLACKLIST_INFO_BLOCKED, + BLACKLIST_INFO_BLOCKING, + + /* + * QUESTS + * prefix: QUEST_ + */ + QUEST_CHATMESSAGE_KENKO_IS_TOO_STRONG, + QUEST_CHATMESSAGE_X_HUNTING_Y_Z, + QUEST_CHATMESSAGE_ITEM_PICK_UP, + QUEST_DIALOG_START_MAIN_QUEST, + QUEST_DIALOG_TELEPORT_TO_OBJECTIVE, + QUEST_SHOUTMESSAGE_ALREADY_COMPLETED, + QUEST_SHOUTMESSAGE_ALREADY_COMPLETED_MAIN, + QUEST_SHOUTMESSAGE_ALREADY_COMPLETED_PERIODIC, + QUEST_SHOUTMESSAGE_ALREADY_HAVE_QUEST, + QUEST_SHOUTMESSAGE_ALREADY_MAIN_QUEST, + QUEST_SHOUTMESSAGE_INCOMPLETE_QUESTS, + QUEST_SHOUTMESSAGE_LOW_LEVEL, + QUEST_SHOUTMESSAGE_SLOT_FULL, + QUEST_CHATMESSAGE_NEW_MISSION_FROM_NPC, + QUEST_INFO_CANT_GIVE_UP, + + /* + * OPTIONS + * prefix: OPTIONS_ + */ + OPTIONS_SHOUTMESSAGE_BUFF_DISABLED, + OPTIONS_SHOUTMESSAGE_BUFF_ENABLED, + OPTIONS_SHOUTMESSAGE_EMOTE_DISABLED, + OPTIONS_SHOUTMESSAGE_EMOTE_ENABLED, + OPTIONS_SHOUTMESSAGE_TRADE_DISABLED, + OPTIONS_SHOUTMESSAGE_TRADE_ENABLED, + OPTIONS_SHOUTMESSAGE_FRIEND_REQUESTS_DISABLED, + OPTIONS_SHOUTMESSAGE_FRIEND_REQUESTS_ENABLED, + OPTIONS_SHOUTMESSAGE_GROUP_REQUESTS_DISABLED, + OPTIONS_SHOUTMESSAGE_GROUP_REQUESTS_ENABLED, + OPTIONS_SHOUTMESSAGE_PET_AUTO_RELIVE_ENABLED, + OPTIONS_SHOUTMESSAGE_PET_AUTO_RELIVE_DISABLED, + OPTIONS_SHOUTMESSAGE_SPEAKERS_DISABLED, + OPTIONS_SHOUTMESSAGE_SPEAKERS_ENABLED, + OPTIONS_SHOUTMESSAGE_HP_DISABLED, + OPTIONS_SHOUTMESSAGE_HP_ENABLED, + OPTIONS_SHOUTMESSAGE_MINILAND_INVITES_DISABLED, + OPTIONS_SHOUTMESSAGE_MINILAND_INVITES_ENABLED, + OPTIONS_SHOUTMESSAGE_MOUSE_DISABLED, + OPTIONS_SHOUTMESSAGE_MOUSE_ENABLED, + OPTIONS_SHOUTMESSAGE_QUICK_GET_UP_ENABLED, + OPTIONS_SHOUTMESSAGE_QUICK_GET_UP_DISABLED, + OPTIONS_SHOUTMESSAGE_WHISPER_DISABLED, + OPTIONS_SHOUTMESSAGE_WHISPER_ENABLED, + OPTIONS_SHOUTMESSAGE_FAMILY_REQUESTS_DISABLED, + OPTIONS_SHOUTMESSAGE_FAMILY_REQUESTS_ENABLED, + OPTIONS_SHOUTMESSAGE_UI_DISABLED, + OPTIONS_SHOUTMESSAGE_UI_ENABLED, + OPTIONS_SHOUTMESSAGE_HIDE_HAT_ENABLED, + OPTIONS_SHOUTMESSAGE_HIDE_HAT_DISABLED, + + + /* + * PORTAL + */ + + PORTAL_CHATMESSAGE_TOO_EARLY, + PORTAL_CHATMESSAGE_BLOCKED, + + /* + * COMPLIMENT + */ + COMMEND_CHATMESSAGE_NOT_MINLVL, + COMMEND_CHATMESSAGE_GIVEN, + COMMEND_CHATMESSAGE_RECEIVED, + COMMEND_CHATMESSAGE_COOLDOWN, + COMMEND_CHATMESSAGE_LOGIN_COOLDOWN, + + + /* + * RAIDS + */ + RAID_DIALOG_INVITED_YOU, + RAID_SHOUTMESSAGE_X_JOINED, + RAID_CHATMESSAGE_GROUP_REQUEST, + RAID_CHATMESSAGE_LEVEL_TOO_HIGH, + RAID_MESSAGE_DISOLVED, + RAID_SHOUTMESSAGE_REGISTERED, + RAID_SHOUTMESSAGE_UNREGISTERED, + + RAID_CHATMESSAGE_KICKED, + RAID_CHATMESSAGE_KICKED_OTHER, + RAID_CHATMESSAGE_NEW_LEADER, + RAID_CHATMESSAGE_X_JOINED, + RAID_CHATMESSAGE_AMULET_NEEDED, + RAID_CHATMESSAGE_LIMIT_REACHED, + RAID_CHATMESSAGE_LIMIT_LEFT, + + RAID_SHOUTMESSAGE_RAID_TEAM_FULL, + RAID_SHOUTMESSAGE_LEFT, + RAID_SHOUTMESSAGE_KICKED, + RAID_SHOUTMESSAGE_KICKED_OTHER, + RAID_SHOUTMESSAGE_NEW_LEADER, + RAID_SHOUTMESSAGE_COMPLETED, + RAID_SHOUTMESSAGE_TARGETS_COMPLETED, + RAID_SHOUTMESSAGE_TARGETS_UPDATED, + RAID_SHOUTMESSAGE_BUTTON_ALREADY_USED, + RAID_DIALOG_ASK_START_RAID, + RAID_CHATMESSAGE_START_NOT_IN_RAID_PARTY, + RAID_CHATMESSAGE_START_IS_NOT_LEADER, + RAID_CHATMESSAGE_START_RAID_TYPE_IS_WRONG, + RAID_INFO_NO_EXIST, + RAID_CHATMESSAGE_NO_EXIST_OR_ALREADY_STARTED, + RAID_SHOUTMESSAGE_CANT_CREATE_RAID_GROUP, + RAID_CHATMESSAGE_LOW_LEVEL, + RAID_CHATMESSAGE_LEFT, + RAID_BROADCAST_LOOKING_FOR_TEAM_MEMBERS, + RAID_SHOUTMESSAGE_CANT_JOIN_FROM_ARENA, + RAID_SHOUTMESSAGE_ALREADY_IN_RAID, + RAID_SHOUTMESSAGE_IN_GROUP, + RAID_SHOUTMESSAGE_CANT_INVITE_GROUP, + RAID_SHOUTMESSAGE_PLAYER_DEATH, + RAID_INFO_PLAYER_DEATH, + RAID_SHOUTMESSAGE_MONSTERS_WAVE, + RAID_INFO_NO_LIVES_LEFT, + + RAID_NAME_CUBY, + RAID_NAME_GINSENG, + RAID_NAME_CASTRA, + RAID_NAME_GIANTBLACKSPIDER, + RAID_NAME_SLADE, + RAID_NAME_CHICKENKING, + RAID_NAME_NAMAJU, + RAID_NAME_ROBBERGANG, + /* + * SP PREFECTION + */ + + PERFECTSP_MESSAGE_SUCCESS, + PERFECTSP_ATTACK, + PERFECTSP_DEFENSE, + PERFECTSP_ELEMENT, + PERFECTSP_HPMP, + PERFECTSP_FIRE, + PERFECTSP_WATER, + PERFECTSP_LIGHT, + PERFECTSP_SHADOW, + PERFECTSP_MESSAGE_FAILURE, + + /* + * INSTANT BATTLE + */ + INSTANT_COMBAT_SHOUTMESSAGE_WAVE_SECONDS_REMAINING, + INSTANT_COMBAT_SHOUTMESSAGE_WAVE_FAILED, + INSTANT_COMBAT_SHOUTMESSAGE_MONSTERS_INCOMING, + INSTANT_COMBAT_SHOUTMESSAGE_SECONDS_REMAINING, + INSTANT_COMBAT_SHOUTMESSAGE_MINUTES_REMAINING, + INSTANT_COMBAT_SHOUTMESSAGE_SUCCEEDED, + + /* + * UPGRADES + */ + + UPGRADE_MESSAGE_SP_FAILED_SAVED, + UPGRADE_MESSAGE_SP_DESTROYED, + UPGRADE_MESSAGE_SP_FAILED, + UPGRADE_MESSAGE_SP_SUCCESS, + + GAMBLING_INFO_AMULET_DESTROYED, + GAMBLING_MESSAGE_ALREADY_MAX_RARE, + GAMBLING_MESSAGE_ITEM_IS_HEROIC, + GAMBLING_MESSAGE_AMULET_FAIL_SAVED, + GAMBLING_MESSAGE_FAILED, + GAMBLING_MESSAGE_FAILED_ITEM_SAVED, + GAMBLING_CHATMESSAGE_ITEM_IS_FIXED, + GAMBLING_CHATMESSAGE_ITEM_IS_NOT_HEROIC, + GAMBLING_MESSAGE_SUCCESS, + + SUM_MESSAGE_SUCCESS, + SUM_MESSAGE_FAILED, + UPGRADE_MESSAGE_FAILED, + INFORMATION_CHATMESSAGE_SCROLL_PROTECT_USED, + UPGRADE_MESSAGE_FAILED_ITEM_SAVED, + ITEM_SHOUTMESSAGE_CANT_UPGRADE_DESTROYED_SP, + UPGRADE_MESSAGE_FIXED, + UPGRADE_MESSAGE_SUCCESS, + ITEM_CHATMESSAGE_NO_GILLION, + + /* + * SHELL + */ + SHELLS_SHOUTMESSAGE_MUST_BE_IDENTIFIED, + SHELLS_SHOUTMESSAGE_FOR_ARMOR_ONLY, + SHELLS_SHOUTMESSAGE_FOR_WEAPON_ONLY, + SHELLS_SHOUTMESSAGE_RARITY_TOO_HIGH, + SHELLS_SHOUTMESSAGE_LEVEL_TOO_HIGH, + SHELLS_SHOUTMESSAGE_BROKEN, + SHELLS_SHOUTMESSAGE_OPTION_SET, + SHELLS_SHOUTMESSAGE_ALREADY_IDENTIFIED, + SHELLS_SHOUTMESSAGE_IDENTIFIED, + SHELLS_INFO_SHELL_CANT_BE_IDENTIFIED, + SHELLS_DIALOG_REPLACE_OPTIONS, + SHELLS_DIALOG_ADD_OPTIONS, + SHELLS_SHOUTMESSAGE_ERASED, + SHELL_SHOUTMESSAGE_NEED_PERFUM_TO_CHANGE_SHELL, + + /* + * GROUPS + */ + GROUP_SHOUTMESSAGE_YOU_ARE_NOT_LEADER, + GROUP_INFO_NEW_LEADER, + GROUP_INFO_LEFT, + GROUP_SHOUTMESSAGE_CLOSED, + GROUP_SHOUTMESSAGE_SHARING, + GROUP_SHOUTMESSAGE_SHARING_BY_ORDER, + GROUP_INFO_FULL, + GROUP_INFO_ALREADY_IN_GROUP, + GROUP_INFO_BLOCKED, + GROUP_INFO_REQUEST, + GROUP_INFO_JOIN, + GROUP_INFO_ADMIN, + GROUP_CHATMESSAGE_REQUEST_REFUSED, + GROUP_DIALOG_INVITED_YOU, + GROUP_INFO_SHARE_POINT_TO_MEMBERS, + GROUP_DIALOG_ASK_SHARE_POINT, + GROUP_SHOUTMESSAGE_SHARE_POINT_DECLINED, + GROUP_SHOUTMESSAGE_SHARE_POINT_ACCEPTED, + GROUP_SHOUTMESSAGE_SHARE_POINT_YOU_ACCEPTED, + GROUP_SHOUTMESSAGE_SHARE_POINT_YOU_DECLINED, + + /* + * INVENTORY + */ + INVENTORY_DIALOG_ASK_TO_DELETE, + INVENTORY_DIALOG_SURE_TO_DELETE, + INVENTORY_DIALOG_ASK_SORT, + ITEM_DIALOG_ASK_OPEN_BOX, + ITEM_DIALOG_ASK_PET_STORE, + ITEM_DIALOG_ASK_OPEN_PET_BEAD, + ITEM_SHOUTMESSAGE_NOT_DROPPABLE_HERE, + ITEM_DIALOG_ASK_NOT_TRADABLE, + INVENTORY_CHATMESSAGE_X_ITEM_ACQUIRED, + INVENTORY_CHATMESSAGE_GOLD_BONUS_ACQUIRED, + ITEM_CHATMESSAGE_TIMEOUT, + GROUP_CHATMESSAGE_DROP_ORDERED, + INFORMATION_SHOUTMESSAGE_NO_AMMO_ADVENTURER, + AMMO_LOADED_ADVENTURER, + INFORMATION_SHOUTMESSAGE_NO_WEAPON, + INFORMATION_SHOUTMESSAGE_NO_AMMO_SWORDSMAN, + AMMO_LOADED_SWORDSMAN, + INFORMATION_SHOUTMESSAGE_NO_AMMO_ARCHER, + AMMO_LOADED_ARCHER, + INVENTORY_SHOUTMESSAGE_NOT_ENOUGH_ITEMS, + ITEM_MESSAGE_CANT_USE, + ITEM_SHOUTMESSAGE_NOT_DROPPABLE, + ITEM_DIALOG_ASK_CHANGE_SP_WINGS, + ITEM_DIALOG_ASK_USE, + ITEM_MESSAGE_ERASER_NO_SHELL, + BANK_CHATMESSAGE_HAS_DEBIT_CARD_ALREADY, + ITEM_CHATMESSAGE_SIGNPOST_ALREADY_IN, + ITEM_CHATMESSAGE_UNFIXED, + ITEM_DIALOG_ASK_BIND, + ITEM_SHOUTMESSAGE_CANT_WEAR_SP_DESTROYED, + ITEM_MESSAGE_CANT_WEAR, + ITEM_MESSAGE_CANT_WEAR_PARTNER, + ITEM_SHOUTMESSAGE_FAIRY_WRONG_ELEMENT, + ITEM_CHATMESSAGE_CANT_USE_THAT, + INFORMATION_CHATMESSAGE_NOT_YOUR_ITEM, + ITEM_SHOUTMESSAGE_VEHICLE_DEBUFF, + ITEM_SHOUTMESSAGE_CHEST_EMPTY, + DROP_SHOUTMESSAGE_BAD_DROP_AMOUNT, + ITEM_SHOUTMESSAGE_MIMIC_FAKE_POTION, + ITEM_CHATMESSAGE_ON_MOUNT, + ITEM_DIALOG_ASK_EXCHANGE, + ITEM_SHOUTMESSAGE_RELICT_LOW_LEVEL, + ITEM_SHOUTMESSAGE_IS_LIMITED, + + /* + * MINILAND + */ + MINILAND_INFO_CHANGED, + MINILAND_SHOUTMESSAGE_PRIVATE, + MINILAND_SHOUTMESSAGE_LOCK, + MINILAND_SHOUTMESSAGE_PUBLIC, + MINILAND_SHOUTMESSAGE_CLOSED, + MINIGAME_INFO_NO_REWARD, + MINIGAME_INFO_POINTS_FULL, + MINIGAME_CHATMESSAGE_PRODUCTION_REFRESHED, + MINILAND_CHATMESSAGE_NOT_FULLFILLING_MINIGAME_REQUIREMENTS, + MINILAND_INFO_DURABILITY_MAXIMUM, + MINILAND_SHOUTMESSAGE_FULL, + MINILAND_INFO_INVITE_LOCK, + MINILAND_DIALOG_ASK_INVITE, + MINILAND_INFO_NO_ENOUGH_REWARD_COUPON, + MINILAND_INFO_NOT_ENOUGH_DURABILITY_POINT, + MINILAND_INFO_NOT_ENOUGH_PRODUCTION_POINTS, + MINILAND_DIALOG_ASK_MINIGAME_FOR_FUN, + MINIGAME_INFO_REFILL, + MINILAND_WELCOME_MESSAGE, + MINILAND_MESSAGE_VISITOR, + MINILAND_SHOUTMESSAGE_NEED_BE_LOCKED, + + /* + * EXCHANGE + */ + TRADE_SHOUTMESSAGE_ITEM_NOT_TRADABLE, + TRADE_SHOUTMESSAGE_NOT_ALLOWED_IN_RAID, + TRADE_SHOUTMESSAGE_NOT_ALLOWED_WITH_RAID_MEMBER, + TRADE_INFO_WAITING_FOR_TARGET, + TRADE_MODAL_ALREADY_IN_EXCHANGE, + TRADE_CHATMESSAGE_YOU_REFUSED, + TRADE_CHATMESSAGE_REFUSED, + TRADE_CHATMESSAGE_BLOCKED, + TRADE_DIALOG_ASK_FOR_TRADE, + TRADE_DIALOG_INCOMING_EXCHANGE, + INFORMATION_INFO_PLAYER_IN_BATTLE, + TRADE_INFO_PLAYER_IN_BATTLE, + + /* + * TITLES + */ + TITLE_INFO_VISIBLE_ENABLED, + TITLE_INFO_VISIBLE_DISABLED, + TITLE_INFO_EFFECT_ENABLED, + TITLE_INFO_EFFECT_DISABLED, + + /* + * DIGNITY + */ + DIGNITY_CHATMESSAGE_LOSS, + DIGNITY_CHATMESSAGE_RESTORE, + + /* + * CELLONS + */ + CELLON_SHOUTMESSAGE_LEVEL_TOO_HIGH, + CELLON_SHOUTMESSAGE_OPTIONS_FULL, + CELLON_SHOUTMESSAGE_FAIL, + CELLON_SHOUTMESSAGE_SUCCESS, + + /* + * INTERACTION + */ + INTERACTION_MESSAGE_NOT_ENOUGH_GOLD, + INTERACTION_SHOUTMESSAGE_GET_PROTECTION_POWER_NEUTRAL, + INTERACTION_SHOUTMESSAGE_GET_PROTECTION_POWER_ANGEL, + INTERACTION_SHOUTMESSAGE_GET_PROTECTION_POWER_DEMON, + INTERACTION_LOG_ITEM_NOT_SELLABLE, + INTERACTION_MESSAGE_NOT_ENOUGH_PLACE, + INTERACTION_CHATMESSAGE_XP_NOT_FIRST_HIT, + ITEM_DIALOG_ASK_CHANGE_FACTION, + INFORMATION_CHATMESSAGE_NO_WINGS, + INTERACTION_SHOUTMESSAGE_NEED_SP_UPGRADE_FOR_WINGS, + ITEM_SHOUTMESSAGE_FACTION_CANT_IN_FAMILY, + INFORMATION_CHATMESSAGE_REPUT_INCREASE, + INFORMATION_CHATMESSAGE_REPUT_DECREASE, + INFORMATION_SHOUTMESSAGE_LEVELUP, + INFORMATION_SHOUTMESSAGE_FAIRYMAX, + INFORMATION_SHOUTMESSAGE_FAIRY_LEVELUP, + INFORMATION_SHOUTMESSAGE_JOB_LEVELUP, + INFORMATION_HERO_LEVELUP, + INFORMATION_SHOUTMESSAGE_CLASS_CHANGED, + INFORMATION_SHOUTMESSAGE_SEX_CHANGED, + INFORMATION_MESSAGE_USER_NOT_CONNECTED, + INFORMATION_SHOUTMESSAGE_REMOVE_VEHICLE, + INFORMATION_SHOUTMESSAGE_NO_ADNVENTURER, + MUTE_MESSAGE_FEMALE, + MUTE_MESSAGE_MALE, + MUTE_SHOUTMESSAGE_PLAYER_IS_MUTED, + MUTE_SHOUTMESSAGE_YOU_ARE_MUTED, + INFORMATION_CHATMESSAGE_NOT_ENOUGH_REPUT, + INFORMATION_CHATMESSAGE_NOT_REQUIERED_LEVEL, + INFORMATION_MESSAGE_MAX_GOLD, + BANK_INFO_TOO_MUCH_GOLD, + BANK_INFO_NOT_ENOUGH_FUNDS, + INFORMATION_MESSAGE_NOT_HUNGRY_FEMALE, + INFORMATION_MESSAGE_NOT_HUNGRY_MALE, + INFORMATION_SHOUTMESSAGE_SP_LVL_LOW, + INFORMATION_CHATMESSAGE_NOT_ENOUGH_MP, + INFORMATION_SHOUTMESSAGE_NEED_LEAVE_GROUP, + INFORMATION_MESSAGE_USER_NOT_FOUND, + INFORMATION_CHATMESSAGE_IMPOSSIBLE_TO_USE, + CHARACTER_CREATION_INFO_ALREADY_TAKEN, + CHARACTER_CREATION_INFO_BANNED_CHARNAME, + CHARACTER_CREATION_INFO_INVALID_CHARNAME, + CHARACTER_DELETION_INFO_UNABLE_TO_RETRIEVE_CHARACTER, + CHARACTER_DELETION_INFO_FAILED, + HARVEST_SHOUTMESSAGE_TRY_FAIL, + INFORMATION_CHATMESSAGE_USER_NOT_HERO, + INFORMATION_SHOUTMESSAGE_EQ_NOT_EMPTY, + SPECIALIST_SHOUTMESSAGE_CANT_ATTACK_YET, + HARVEST_SHOUTMESSAGE_FAIL_TRY_AGAIN, + INFORMATION_SHOUTMESSAGE_NO_WIG, + INFORMATION_CHATMESSAGE_USER_WHISPER_BLOCKED, + SPECIALIST_CHATMESSAGE_TRANSFORMATION_NEEDED, + INFORMATION_SHOUTMESSAGE_SP_POINTS_SET, + ITEM_SHOUTMESSAGE_CRAFTED_OBJECT, + INTERACTION_SPEAKER_UNDER_COOLDOWN, + INTERACTION_VESSEL_LIMIT_REACHED, + PARTNER_WAREHOUSE_MESSAGE_NO_ENOUGH_PLACE, + + /* + * SHOP + */ + SHOP_LOG_SELL_ITEM_VALID, + SHOP_INFO_OPEN, + SHOP_INFO_TARGET_MAX_GOLD, + TRADE_SHOUTMESSAGE_HAS_SHOP_OPEN, + SHOP_INFO_NEAR_PORTAL, + SHOP_INFO_NOT_ALLOWED_IN_RAID, + SHOP_INFO_NOT_ALLOWED, + SHOP_CHATMESSAGE_ONLY_TRADABLE_ITEMS, + SHOP_CHATMESSAGE_EMPTY, + SHOP_INFO_ALREADY_OPEN, + SHOP_DEFAULT_NAME, + PERSONAL_SHOP_LOG_PURCHASE_OWNER, + PERSONAL_SHOP_LOG_PURCHASE_BUYER, + NPC_SHOP_LOG_PURCHASE, + + /* + * REVIVAL + */ + REVIVE_DIALOG_SEEDS_OF_POWER, + REVIVE_DIALOG_FREE, + REVIVE_DIALOG_ARENA, + INFORMATION_CHATMESSAGE_REQUIRED_ITEM_EXPENDED, + + + /* + * SKILLS + */ + INFORMATION_SHOUTMESSAGE_NO_SPECIALIST_CARD, + INFORMATION_SHOUTMESSAGE_MUST_BE_IN_CLASSIC_MAP, + INFORMATION_SHOUTMESSAGE_NOT_ENOUGH_CP, + INFORMATION_SHOUTMESSAGE_TOO_LOW_LVL, + INFORMATION_MESSAGE_LOW_JOB, + + /* + * RESPAWN + */ + RESPAWN_DIALOG_ASK_CHANGE_SPAWN_LOCATION, + RESPAWNLOCATION_SHOUTMESSAGE_CHANGED, + ITEM_DIALOG_WANT_TO_SAVE_POSITION, + + + /* + * CHARACTER SELECTION SCREEN + */ + ACCOUNT_INFO_BAD_ID, + + /* + * BUFF + */ + BUFF_CHATMESSAGE_UNDER_EFFECT, + BUFF_CHATMESSAGE_EFFECT_TERMINATED, + + /* + * TITLE + */ + TITLE_DIALOG_ASK_ADD, + TITLE_INFO_UNLOCKED, + + /* + * BANK + */ + BANK_MESSAGE_DEPOSIT, + BANK_MESSAGE_MAX_GOLD_REACHED, + BANK_MESSAGE_BALANCE, + BANK_DIALOG_ASK_WITHDRAW, + BANK_LOG_WITHDRAW_BANK, + BANK_DIALOG_ASK_DEPOSIT, + BANK_LOG_OPEN_BANK, + + /* + * INFORMATION + */ + COMMAND_CHATMESSAGE_LANGUAGE_CURRENT, + COMMAND_CHATMESSAGE_LANGUAGE_CHANGED, + ADMIN_BROADCAST_CHATMESSAGE_SENDER, + INFORMATION_SHOUTMESSAGE_SHUTDOWN_SEC, + INFORMATION_CHANNEL, + DROP_SHOUTMESSAGE_FULL, + INFORMATION_INFO_PLAYER_OFFLINE, + GROUP_CHATMESSAGE_DROP_SHARED, + ITEM_CHATMESSAGE_CANT_USE_TWICE, + ITEM_SHOUTMESSAGE_SAME_FACTION, + ITEM_CHATMESSAGE_EFFECT_ACTIVATED, + SPEAKER_NAME, + SPECIALIST_CHATMESSAGE_TRANSFORM_DISAPPEAR, + BUFF_CHATMESSAGE_EFFECT_RESISTANCE, + INSTANT_COMBAT_NAME, + MUTE_CHATMESSAGE_TIME_LEFT, + INTERACTION_CHATMESSAGE_INTRODUCTION_SET, + INFORMATION_SHOUTMESSAGE_USER_NOT_BASEMAP, + BAZAAR_SHOUTMESSAGE_NOTICE_BAZAAR, + INFORMATION_CHATMESSAGE_ON_LOGIN_BAZAAR_MEDAL, + INFORMATION_CHATMESSAGE_USER_WHISPER_SENT, + + /* + * CLASS TYPE + */ + CLASS_NAME_ADVENTURER, + CLASS_NAME_ARCHER, + CLASS_NAME_SWORDSMAN, + CLASS_NAME_MAGE, + CLASS_NAME_MARTIAL_ARTIST, + + /* + * GAME EVENT + */ + GAMEEVENT_SHOUTMESSAGE_PREPARATION_MINUTES, + GAMEEVENT_SHOUTMESSAGE_PREPARATION_SECONDS, + GAMEEVENT_MESSAGE_START, + GAMEEVENT_DIALOG_ASK_PARTICIPATE, + GAMEEVENT_DIALOG_ASK_PARTICIPATE_WITH_COST, + GAMEEVENT_MESSAGE_NOT_IN_GENERAL_MAP, + GAMEEVENT_SHOUTMESSAGE_NOT_ENOUGH_PLAYERS, + + /* + * COMMANDS + */ + COMMANDS_CHATMESSAGE_AVAILABLE, + COMMANDS_CHATMESSAGE_HELP, + COMMANDS_CHATMESSAGE_USAGES, + COMMANDS_CHATMESSAGE_UNDOCUMENTED, + + /* + * WEDDING + */ + WEDDING_DIALOG_REQUEST_VERIFICATION, + WEDDING_DIALOG_REQUEST_RECEIVED, + WEDDING_SHOUTMESSAGE_BROADCAST, + WEDDING_SHOUTMESSAGE_REFUSED_BROADCAST, + WEDDING_INFO_DIVORCED, + WEDDING_DIALOG_ASK_DIVORCE_CONFIRM, + + /* + * SPECIALISTS + */ + + SPECIALIST_SHOUTMESSAGE_LEVELUP, + SPECIALIST_SHOUTMESSAGE_POINTS_ADDED, + SPECIALIST_SHOUTMESSAGE_IN_COOLDOWN, + SPECIALIST_SHOUTMESSAGE_USE_NEEDED, + SPECIALIST_SHOUTMESSAGE_NO_POINTS, + INFORMATION_CHATMESSAGE_WIN_SP_POINT, + SPECIALIST_CHATMESSAGE_SP_BLOCKED, + SPECIALIST_SHOUTMESSAGE_RESURRECTED, + SPECIALIST_SHOUTMESSAGE_NO_REMOVE_DEBUFFS, + SPECIALIST_SHOUTMESSAGE_POINTS_RESET, + ITEM_SHOUTMESSAGE_SAME_SP_WINGS_ALREADY_SET, + INFORMATION_SHOUTMESSAGE_REMOVE_SP, + INFORMATION_CHATMESSAGE_SP_STAY_TIME, + INFORMATION_CHATMESSAGE_PSP_STAY_TIME, + + /* + * PARTNER SPECIALIST + */ + PSP_AEGIR, + PSP_BARNI, + PSP_FREYA, + PSP_SHINOBI, + PSP_LOTUS, + PSP_ORKANI, + PSP_FOXY, + PSP_MARU, + PSP_MOTHER_MARU, + PSP_HONGBI, + PSP_CHEONGBI, + PSP_LUCIFER, + PSP_LAURENA, + PSP_AMON, + PSP_LUCY, + PSP_CHLOE, + PSP_FIONA, + PSP_JINN, + PSP_ELIZA, + PSP_DANIEL, + PSP_PALINA, + PSP_HARLEQUIN, + PSP_NELIA, + PSP_VENUS, + PSP_RAGNAR, + PSP_MACAVITY, + PSP_GUARDIAN_LUCIFER, + PSP_SHERIFF_CHLOE, + PSP_ARCHDAEMON_AMON, + PSP_YUNA, + PSP_AMORA, + PSP_MAD_MARCH_HARE, + PSP_PERTI, + PSP_PHARAOH, + PSP_DIALOG_ASK_RESET_ALL_SKILLS, + PSP_MESSAGE_ALL_SKILLS_RESET, + PSP_MESSAGE_ONE_SKILL_RESET, + + /* + * PET + */ + PET_MESSAGE_ALREADY_IN_TEAM, + PET_MESSAGE_KICKED, + PARTNER_MESSAGE_KICKED, + PET_SHOUTMESSAGE_HIGHER_LEVEL, + ITEM_INFO_PET_NAME_TOO_LONG, + PET_CHATMESSAGE_ALREADY_CAN_PICK_UP, + PET_CHATMESSAGE_EAT_EVERYTHING, + PARTNER_CHATMESSAGE_EAT_EVERYTHING, + PET_DIALOG_ASK_SLOT_INCREASE, + PARTNER_DIALOG_ASK_SLOT_INCREASE, + PET_CHATMESSAGE_CAN_PICK_UP, + PET_MESSAGE_SUMMONABLE, + PET_MESSAGE_IS_ALREADY_SUMMONABLE, + PET_INFO_RELEASED, + PARTNER_INFO_RELEASED, + INFORMATION_SHOUTMESSAGE_PET_IN_TEAM_UNRELEASABLE, + INFORMATION_SHOUTMESSAGE_PARTNER_IN_TEAM_UNRELEASABLE, + INFORMATION_SHOUTMESSAGE_PARTNER_EQ_UNRELEASABLE, + PET_SHOUTMESSAGE_LEVEL_UP, + PARTNER_SHOUTMESSAGE_LEVEL_UP, + PET_SHOUTMESSGE_SAVED_BY_SAVER, + PET_SHOUTMESSAGE_DIGNITY_LOW, + PARTNER_SHOUTMESSAGE_SAVED_BY_SAVER, + PET_INFO_STORED, + PARTNER_INFO_STORED, + PET_INFO_LEAVE_BEAD, + PARTNER_INFO_LEAVE_BEAD, + PARTNER_INFO_IS_WEARING_SP, + PARTNER_INFO_NEED_AGILITY_POINTS, + PARTNER_INFO_SP_NEW_SKILL, + PARTNER_INFO_LEVEL_IS_TOO_LOW, + PARTNER_SHOUTMESSAGE_SP_IN_COOLDOWN, + PARTNER_CHATMESSAGE_TRANSFORM_DISAPPEAR, + PARTNER_MESSAGE_100_AGILITY, + OPTIONS_SHOUTMESSAGE_PARTNER_AUTO_RELIVE_ENABLED, + OPTIONS_SHOUTMESSAGE_PARTNER_AUTO_RELIVE_DISABLED, + INFORMATION_SHOUTMESSAGE_PARTNER_ITEM_DONE, + PARTNER_MESSAGE_NO_SP_EQUIPPED, + PARTNER_INFO_NO_PARTNER_IN_TEAM, + PARTNER_SHOUTMESSAGE_ALREADY_HAVE_SAME_PARTNER, + PET_SHOUTMESSAGE_WILL_BE_BACK, + PARTNER_SHOUTMESSAGE_WILL_BE_BACK, + PET_SHOUTMESSAGE_WENT_BACK_TO_MINILAND, + PARTNER_SHOUTMESSAGE_WENT_BACK_TO_MINILAND, + PET_CHATMESSAGE_BEAD_EXTRACT, + PARTNER_CHATMESSAGE_BEAD_EXTRACT, + PET_DIALOG_ASK_SEND_BACK, + INFORMATION_SHOUTMESSAGE_MAX_PET_COUNT, + INFORMATION_SHOUTMESSAGE_MAX_PARTNER_COUNT, + INFORMATION_SHOUTMESSAGE_MAX_PET_SLOTS, + INFORMATION_SHOUTMESSAGE_MAX_PARTNER_SLOTS, + ITEM_CHATMESSAGE_CANNOT_BE_WITH_SHELL, + PSP_DIALOG_ASK_RESET_SINGLE_SKILL, + ITEM_SHOUTMESSAGE_ONLY_IN_MINILAND, + + /* + * SKILLS + */ + SKILL_SHOUTMESSAGE_SACRIFICE, + SKILL_SHOUTMESSAGE_SACRIFICE_WARNING, + SKILL_SHOUTMESSAGE_SACRIFICE_REMOVED, + SKILL_CHATMESSAGE_CANT_LEARN_MORPHED, + SKILL_SHOUTMESSAGE_CANT_LEARN_COOLDOWN, + SKILL_CHATMESSAGE_ALREADY_LEARNT, + SKILL_CHATMESSAGE_CANT_LEARN_NEED_BASE, + SKILL_DIALOG_CONFIRM_REPLACE_UPGRADE, + SKILL_CHATMESSAGE_WAS_INTERRUPTED, + SKILL_SHOUTMESSAGE_MONSTER_MUST_BE_LOW_HP, + SKILL_SHOUTMESSAGE_CAPTURE_IMPOSSIBLE, + SKILL_SHOUTMESSAGE_CAPTURE_DIGNITY_LOW, + SKILL_SHOUTMESSAGE_CAPTURE_IN_RAID, + SKILL_SHOUTMESSAGE_MONSTER_LEVEL_MUST_BE_LOWER_THAN_YOURS, + SKILL_SHOUTMESSAGE_CAPTURE_FAILED, + SKILL_SHOUTMESSAGE_CAUGHT_PET, + SKILL_SHOUTMESSAGE_CANT_LEARN, + SKILL_SHOUTMESSAGE_SKILLS_IN_LOADING, + SKILL_CHATMESSAGE_NOT_TO_REFUND, + SKILL_SHOUTMESSAGE_LEARNED, + + /* + * FAMILIES + */ + FAMILY_SHOUTMESSAGE_CHANGED_HEAD, + FAMILY_INFO_ASK_FAMILY_LEAVE, + FAMILY_INFO_CREATION_BANNED_NAME, + FAMILY_INFO_USED_DAILY_MESSAGE, + FAMILY_SHOUTMESSAGE_LEVEL_UP, + FAMILY_SHOUTMESSAGE_ALREADY_THAT_FACTION, + FAMILY_SHOUTMESSAGE_SHOUT, + FAMILY_SHOUTMESSAGE_HEAD_CHANGE_SEX, + FAMILY_SHOUTMESSAGE_CHANGE_FACTION_UNDER_COOLDOWN, + FAMILY_SHOUTMESSAGE_CHANGE_FAMILY_ON_COOLDOWN, + + FAMILY_RANK_HEAD, + FAMILY_RANK_DEPUTY, + FAMILY_RANK_KEEPER, + FAMILY_RANK_MEMBER, + FAMILY_INFO_GROUP_MEMBER_ALREADY_IN_FAMILY, + FAMILY_INFO_GROUP_NOT_FULL, + FAMILY_DIALOG_ALREADY_HAS_THAT_AUTHORITY, + FAMILY_DIALOG_CANNOT_CHANGE_YOUR_OWN_AUTHORITY, + FAMILY_DIALOG_CANNOT_KICK_YOURSELF, + FAMILY_DIALOG_HEAD_CANNOT_LEAVE_FAMILY, + FAMILY_INFO_LEAVE, + FAMILY_INFO_INVITATION_NOT_ALLOWED, + FAMILY_INFO_FULL, + FAMILY_INFO_INVITATION_REFUSED, + FAMILY_INFO_ALREADY_IN_FAMILY, + FAMILY_DIALOG_ASK_JOIN_CONFIRMATION, + FAMILY_INFO_CREATION_GROUP_REQUIRED, + FAMILY_DIALOG_CHANGE_HEAD, + FAMILY_INFO_DEPUTIES_LIMIT_REACHED, + FAMILY_INFO_KEEPER_LIMIT_REACHED, + FAMILY_INFO_CANT_GIVE_SAME_AUTHORITY, + FAMILY_INFO_TARGET_NEEDS_TO_BE_DEPUTY, + ITEM_DIALOG_ASK_CHANGE_FAMILY_FACTION, + FAMILY_INFO_CREATION_SOMEONE_ALREADY_IN_FAMILY, + FAMILY_INFO_NAME_ALREADY_USED, + FAMILY_SHOUTMESSAGE_FAMILY_CREATED, + FAMILY_INFO_NO_FAMILY_WAREHOUSE, + FAMILY_INFO_NO_FAMILY_RIGHT, + FAMILY_INFO_NOT_FAMILY_HEAD, + FAMILY_SHOUTMESSAGE_MEMBER_JOINED, + FAMILY_CHATMESSAGE_CHARACTER_LOGGED_IN, + FAMILY_DIALOG_ASK_JOIN_FAMILY, + FAMILY_DIALOG_ASK_DISMISS_FAMILY, + FAMILY_INFO_NO_FAMILY, + FAMILY_INFO_NOT_IN_FAMILY, + FAMILY_INFO_WAREHOUSE_INVALID_ITEM, + FAMILY_INFO_WAREHOUSE_NOT_ENOUGH_PERMISSION, + FAMILY_INFO_WAREHOUSE_UNEXPECTED_ERROR, + FAMILY_INFO_SERVICE_MAINTENANCE_MODE, + FAMILY_CHATMESSAGE_UPGRADE_BOUGHT, + FAMILY_MESSAGE_UPGRADE_NO_PREVIOUS, + FAMILY_INFO_TODAY_LOW_LEVEL, + + FAMILY_ACHIEVEMENT_UNLOCKED, + FAMILY_CHATMESSAGE_XP_GAINED, + + /* + * Messages under chat + */ + MESSAGE_UNDER_CHAT_0, + MESSAGE_UNDER_CHAT_1, + MESSAGE_UNDER_CHAT_2, + MESSAGE_UNDER_CHAT_3, + MESSAGE_UNDER_CHAT_4, + MESSAGE_UNDER_CHAT_5, + + /* + * MAILS/NOTES + */ + MAIL_CHATMESSAGE_YOU_HAVE_X_MAILS, + NOTE_CHATMESSAGE_YOU_HAVE_X_NOTES, + MAIL_CHATMESSAGE_NEW_MAIL_RECEIVE, + NOTE_CHATMESSAGE_NEW_NOTE, + + /* + * BAZAAR + */ + BAZAAR_CHATMESSAGE_ITEM_ADDED, + BAZAAR_SHOUTMESSAGE_ITEM_ADDED, + BAZAAR_SHOUTMESSAGE_ITEM_PRICE_CHANGED, + BAZAAR_INFO_MAXIMUM_ITEMS_REACHED, + BAZAAR_INFO_MAINTENANCE_MODE, + BAZAAR_INFO_RESYNC, + BAZAAR_INFO_ITEM_CHANGED, + BAZAAR_INFO_PRICE_EXCEEDS_LIMITS, + BAZAAR_INFO_NEEDS_MEDAL, + BAZAAR_QNA_CHANGE_PRICE_FEE, + BAZAAR_CHATMESSAGE_ITEM_SOLD, + + BANK_SHOUTMESSAGE_EXCEED_GOLD_SENT_TO_BANK, + + + ACT4_SHOUTMESSAGE_MUKRAJU_DEATH, + ACT4_SHOUTMESSAGE_MUKRAJU_DESPAWNED, + ACT4_SHOUTMESSAGE_MUKRAJU_SPAWNED, + ACT4_SHOUTMESSAGE_CAMP_ANGELS, + ACT4_SHOUTMESSAGE_CAMP_DEMONS, + ACT4_NEED_FACTION, + ACT4_CHANNEL_OFFLINE, + ACT4_CHATMESSAGE_PVP_KILL, + ACT4_CHATMESSAGE_PVP_DEATH, + ACT4_CHATMESSAGE_PVP_ANGELS, + ACT4_CHATMESSAGE_PVP_DEMONS, + ACT4_DUNGEON_SHOUTMESSAGE_STARTED, + ACT4_DUNGEON_QNAMESSAGE_ENTRY_COST, + ACT4_DUNGEON_SHOUTMESSAGE_BOSS_CLOSED, + ACT4_DUNGEON_SHOUTMESSAGE_BOSS_COMPLETED, + ACT4_DUNGEON_SHOUTMESSAGE_BOSS_OPEN, + ACT4_DUNGEON_REVIVAL_DIALOG, + ACT4_DUNGEON_CHATMESSAGE_FAMILY_NEEDED, + ACT4_CHATMESSAGE_KILL_INFO, + ACT4_SHOUTMESSAGE_FLAG_ALREADY_IN, + ACT4_SHOUTMESSAGE_FLAG_PLACED_ANGEL, + ACT4_SHOUTMESSAGE_FLAG_PLACED_DEMON, + ACT4_ASK_DIALOG_DUNGEON_EXIT, + ACT4_CHATMESSAGE_LESS_REPUTATION, + + ACT5_MESSAGE_RETURN_DISABLED, + ACT5_CHATMESSAGE_NO_RECIPE, + + DOLL_SHOUTMESSAGE_NOT_IN_ZONE, + DOLL_SHOUTMESSAGE_DOLLS_LIMIT, + DOLL_MESSAGE_MATE_DEFENSE_FAIL, + DOLL_SHOUTMESSAGE_WRONG_DOLL, + DOLL_MESSAGE_MATE_ATTACK_FAIL, + DOLL_MESSAGE_MATE_ATTACK_UPGRADE, + DOLL_MESSAGE_MATE_DEFENSE_UPGRADE, + + TIMESPACE_DIALOG_ASK_START, + TIMESPACE_CHATMESSAGE_ONLY_TEAM_LEADER_START, + TIMESPACE_CHATMESSAGE_GATE_LOCKED, + TIMESPACE_SHOUTMESSAGE_DOOR_OPENED, + TIMESPACE_SHOUTMESSAGE_TIMESPACE_ENTER, + TIMESPACE_SHOUTMESSAGE_WAIT_REWARD, + TIMESPACE_DIALOG_ASK_REVIVAL, + TIMESPACE_SHOUTMESSAGE_MISSION_UPDATED, + TIMESPACE_SHOUTMESSAGE_LEVER_OPERATED, + TIMESPACE_SHOUTMESSAGE_ENEMIES_REINFORCEMENTS, + TIMESPACE_CHATMESSAGE_NOT_FINISHED_TASKS, + TIMESPACE_SHOUTMESSAGE_QUICK_MISSION_DONE, + TIMESPACE_SHOUTMESSAGE_DOOR_OPENED_SOMEWHERE, + TIMESPACE_DIALOG_ASK_JOIN_TO_PLAYER, + TIMESPACE_CHATMESSAGE_LOW_LEVEL, + TIMESPACE_CHATMESSAGE_HIGH_LEVEL, + TIMESPACE_SHOUTMESSAGE_TS_FULL, + TIMESPACE_SHOUTMESSAGE_ALREADY_STARTED, + TIMESPACE_DIALOG_ASK_START_RECORD, + TIMESPACE_CHATMESSAGE_TIME_SPACE_PENALTY, + TIMESPACE_CHATMESSAGE_EASY_MODE, + TIMESPACE_CHATMESSAGE_ATTACK_COOLDOWN, + TIMESPACE_SHOUTMESSAGE_WRONG_ACT, + TIMESPACE_SHOUTMESSAGE_WRONG_MAP, + TIMESPACE_SHOUTMESSAGE_CLOSE_PORTAL, + TIMESPACE_SHOUTMESSAGE_NOT_OWNER, + TIMESPACE_SHOUTMESSAGE_NOT_ENOUGH_PLAYERS, + TIMESPACE_SHOUTMESSAGE_NOT_THE_SAME_MAP, + TIMESPACE_SHOUTMESSAGE_MEMBER_NOT_IN_CLASSIC_MAP, + TIMESPACE_SHOUTMESSAGE_CANT_BE_IN_GROUP, + TIMESPACE_SHOUTMESSAGE_MUST_BE_IN_GROUP, + + SHIP_SHOUTMESSAGE_SECONDS_REMAINING, + SHIP_SHOUTMESSAGE_MINUTES_REMAINING, + SHIP_CHATMESSAGE_LEVEL_REQ, + SHIP_SHOUTMESSAGE_COOLDOWN, + + BUFF_CHATMESSAGE_SIDE_EFFECTS, + + /* + * ARENA + */ + ARENA_INFO_ASK_ENTER, + ARENA_SHOUTMESSAGE_PVP_KILL, + FAMILYARENA_INFO_ASK_ENTER, + ARENA_CHATMESSAGE_PVP_ACTIVE, + + /* + * MAINTENANCE + */ + MAINTENANCE_SHOUTMESSAGE_NOTIFY_STOPPED, + MAINTENANCE_SHOUTMESSAGE_NOTIFY_EXECUTED, + MAINTENANCE_SHOUTMESSAGE_NOTIFY_LIFTED, + MAINTENANCE_SHOUTMESSAGE_NOTIFY_WARNING_HOURS, + MAINTENANCE_SHOUTMESSAGE_NOTIFY_WARNING_MINUTES, + MAINTENANCE_SHOUTMESSAGE_NOTIFY_WARNING_SECONDS, + MAINTENANCE_SHOUTMESSAGE_NOTIFY_RESCHEDULED_HOURS, + MAINTENANCE_SHOUTMESSAGE_NOTIFY_RESCHEDULED_MINUTES, + MAINTENANCE_SHOUTMESSAGE_NOTIFY_RESCHEDULED_SECONDS, + + /* + * ACCOUNT + */ + ACCOUNT_INFO_WAREHOUSE_UNEXPECTED_ERROR, + ACCOUNT_INFO_SERVICE_MAINTENANCE_MODE, + + /* + * RAINBOW + */ + RAINBOW_BATTLE_EVENT_NAME, + RAINBOW_BATTLE_MESSAGE_NOT_ENOUGH_PLAYERS, + RAINBOW_BATTLE_MESSAGE_LOW_LEVEL, + RAINBOW_BATTLE_SHOUTMESSAGE_START, + RAINBOW_BATTLE_MESSAGE_PLAYER_LEFT, + RAINBOW_BATTLE_MESSAGE_YOU_WON, + RAINBOW_BATTLE_MESSAGE_YOU_LOSE, + RAINBOW_BATTLE_SHOUTMESSAGE_RED_TEAM, + RAINBOW_BATTLE_SHOUTMESSAGE_BLUE_TEAM, + RAINBOW_BATTLE_CHATMESSAGE_ALREADY_CAPTURED, + RAINBOW_BATTLE_CHATMESSAGE_CAPTURE_COOLDOWN, + RAINBOW_BATTLE_MESSAGE_ALLY_CAPTURED, + RAINBOW_BATTLE_MESSAGE_RED_TEAM_FLAG_CAPTURED, + RAINBOW_BATTLE_MESSAGE_BLUE_TEAM_FLAG_CAPTURED, + RAINBOW_BATTLE_SHOUTMESSAGE_FROZEN, + RAINBOW_BATTLE_SHOUTMESSAGE_UNFROZEN, + RAINBOW_BATTLE_CHATMESSAGE_NOT_ENOUGH_ACTIVITY_POINTS, + RAINBOW_BATTLE_CHATMESSAGE_PENALTY_NEXT_REWARD, + RAINBOW_BATTLE_CHATMESSAGE_PENALTY_LEFT, + + /* + * HOT RELOAD MANAGEMENT + */ + ITEM_USAGE_MSG_ITEM_DISABLED_TEMPORARILY, + GAME_FEATURE_DISABLED +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Core/Multilanguage/GameLanguageServiceExtensions.cs b/srcs/WingsAPI.Game/Core/Multilanguage/GameLanguageServiceExtensions.cs new file mode 100644 index 0000000..16d715d --- /dev/null +++ b/srcs/WingsAPI.Game/Core/Multilanguage/GameLanguageServiceExtensions.cs @@ -0,0 +1,19 @@ +using WingsEmu.DTOs.Buffs; +using WingsEmu.DTOs.Skills; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Items; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Game._i18n; + +public static class GameLanguageServiceExtensions +{ + public static string GetItemName(this IGameLanguageService gameLanguage, IGameItem item, IClientSession session) => gameLanguage.GetLanguage(GameDataType.Item, item.Name, session.UserLanguage); + + public static string GetNpcMonsterName(this IGameLanguageService gameLanguage, IMonsterData item, IClientSession session) => + gameLanguage.GetLanguage(GameDataType.NpcMonster, item.Name, session.UserLanguage); + + public static string GetSkillName(this IGameLanguageService gameLanguage, SkillDTO item, IClientSession session) => gameLanguage.GetLanguage(GameDataType.Skill, item.Name, session.UserLanguage); + + public static string GetCardName(this IGameLanguageService gameLanguage, CardDTO item, IClientSession session) => gameLanguage.GetLanguage(GameDataType.Card, item.Name, session.UserLanguage); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Core/Multilanguage/IGameDataLanguageService.cs b/srcs/WingsAPI.Game/Core/Multilanguage/IGameDataLanguageService.cs new file mode 100644 index 0000000..9b6a83e --- /dev/null +++ b/srcs/WingsAPI.Game/Core/Multilanguage/IGameDataLanguageService.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using PhoenixLib.MultiLanguage; + +namespace WingsEmu.Game._i18n; + +public interface IGameDataLanguageService +{ + /// + /// + /// + /// + /// + /// + string GetLanguage(GameDataType dataType, string dataName, RegionLanguageType lang); + + Dictionary GetDataTranslations(GameDataType dataType, RegionLanguageType lang); + + /// + /// Reload + /// + /// + Task Reload(bool isFullReload = false); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Core/Multilanguage/IGameLanguageService.cs b/srcs/WingsAPI.Game/Core/Multilanguage/IGameLanguageService.cs new file mode 100644 index 0000000..8b06e45 --- /dev/null +++ b/srcs/WingsAPI.Game/Core/Multilanguage/IGameLanguageService.cs @@ -0,0 +1,39 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using PhoenixLib.MultiLanguage; + +namespace WingsEmu.Game._i18n; + +public interface IGameLanguageService : IGameDataLanguageService +{ + /// + /// Will return the string by its Key & + /// Used for plugins mainly + /// + /// + /// + /// + string GetLanguage(string key, RegionLanguageType lang); + + string GetLanguageFormat(string key, RegionLanguageType lang, params object[] formatParams); + + /// + /// Will return the string by its key & + /// Used for ChickenAPI mainly + /// + /// + /// + /// + string GetLanguage(T key, RegionLanguageType lang) where T : Enum; + + /// + /// + /// + /// + /// + /// + string GetLanguageFormat(T key, RegionLanguageType lang, params object[] formatParams) where T : Enum; +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Core/Multilanguage/IUserLanguageService.cs b/srcs/WingsAPI.Game/Core/Multilanguage/IUserLanguageService.cs new file mode 100644 index 0000000..755c7c8 --- /dev/null +++ b/srcs/WingsAPI.Game/Core/Multilanguage/IUserLanguageService.cs @@ -0,0 +1,35 @@ +using PhoenixLib.MultiLanguage; + +namespace WingsEmu.Game._i18n; + +/// +/// This will use IGameLanguageService +/// +public interface IUserLanguageService +{ + /// + /// Will return the string by its Key & + /// Used for plugins mainly + /// + /// + /// + string GetLanguage(string key); + + string GetLanguageFormat(string key, params object[] formatParams); + + /// + /// Will return the string by its key & + /// Used for ChickenAPI mainly + /// + /// + /// + /// + string GetLanguage(GameDialogKey key); + + /// + /// + /// + /// + /// + string GetLanguageFormat(GameDialogKey dataType, params object[] formatParams); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Core/Multilanguage/StaticGameLanguageService.cs b/srcs/WingsAPI.Game/Core/Multilanguage/StaticGameLanguageService.cs new file mode 100644 index 0000000..bb45902 --- /dev/null +++ b/srcs/WingsAPI.Game/Core/Multilanguage/StaticGameLanguageService.cs @@ -0,0 +1,11 @@ +namespace WingsEmu.Game._i18n; + +public class StaticGameLanguageService +{ + public static IGameLanguageService Instance { get; private set; } + + public static void Initialize(IGameLanguageService service) + { + Instance = service; + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Core/NpcDialogHandling/Event/NpcDialogEvent.cs b/srcs/WingsAPI.Game/Core/NpcDialogHandling/Event/NpcDialogEvent.cs new file mode 100644 index 0000000..7e07a99 --- /dev/null +++ b/srcs/WingsAPI.Game/Core/NpcDialogHandling/Event/NpcDialogEvent.cs @@ -0,0 +1,17 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game._NpcDialog.Event; + +public class NpcDialogEvent : PlayerEvent +{ + public NpcRunType NpcRunType { get; set; } + + public short Argument { get; set; } + + public VisualType VisualType { get; set; } + + public long NpcId { get; set; } + + public byte? Confirmation { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Core/NpcDialogHandling/INpcDialogAsyncHandler.cs b/srcs/WingsAPI.Game/Core/NpcDialogHandling/INpcDialogAsyncHandler.cs new file mode 100644 index 0000000..498902b --- /dev/null +++ b/srcs/WingsAPI.Game/Core/NpcDialogHandling/INpcDialogAsyncHandler.cs @@ -0,0 +1,13 @@ +using System.Threading.Tasks; +using WingsEmu.Game._NpcDialog.Event; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game._NpcDialog; + +public interface INpcDialogAsyncHandler +{ + NpcRunType[] NpcRunTypes { get; } + + Task Execute(IClientSession session, NpcDialogEvent e); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Core/NpcDialogHandling/INpcDialogHandlerContainer.cs b/srcs/WingsAPI.Game/Core/NpcDialogHandling/INpcDialogHandlerContainer.cs new file mode 100644 index 0000000..7413748 --- /dev/null +++ b/srcs/WingsAPI.Game/Core/NpcDialogHandling/INpcDialogHandlerContainer.cs @@ -0,0 +1,15 @@ +using System.Threading.Tasks; +using WingsEmu.Game._NpcDialog.Event; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Game._NpcDialog; + +public interface INpcDialogHandlerContainer +{ + void Register(INpcDialogAsyncHandler handler); + void Unregister(INpcDialogAsyncHandler handler); + + void Execute(IClientSession player, NpcDialogEvent e); + + Task ExecuteAsync(IClientSession player, NpcDialogEvent e); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Core/PacketHandling/ICharacterScreenPacketHandler.cs b/srcs/WingsAPI.Game/Core/PacketHandling/ICharacterScreenPacketHandler.cs new file mode 100644 index 0000000..791d5ea --- /dev/null +++ b/srcs/WingsAPI.Game/Core/PacketHandling/ICharacterScreenPacketHandler.cs @@ -0,0 +1,5 @@ +namespace WingsEmu.Game._packetHandling; + +public interface ICharacterScreenPacketHandler : IUnauthedPacketHandler +{ +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Core/PacketHandling/IGamePacketHandler.cs b/srcs/WingsAPI.Game/Core/PacketHandling/IGamePacketHandler.cs new file mode 100644 index 0000000..b9f946c --- /dev/null +++ b/srcs/WingsAPI.Game/Core/PacketHandling/IGamePacketHandler.cs @@ -0,0 +1,5 @@ +namespace WingsEmu.Game._packetHandling; + +public interface IGamePacketHandler : IPacketHandler +{ +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Core/PacketHandling/IPacketHandler.cs b/srcs/WingsAPI.Game/Core/PacketHandling/IPacketHandler.cs new file mode 100644 index 0000000..d0afc90 --- /dev/null +++ b/srcs/WingsAPI.Game/Core/PacketHandling/IPacketHandler.cs @@ -0,0 +1,14 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Threading.Tasks; +using WingsEmu.Game.Networking; +using WingsEmu.Packets; + +namespace WingsEmu.Game._packetHandling; + +public interface IPacketHandler +{ + Task HandleAsync(IClientSession session, IPacket packet); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Core/PacketHandling/IPacketHandlerContainer.cs b/srcs/WingsAPI.Game/Core/PacketHandling/IPacketHandlerContainer.cs new file mode 100644 index 0000000..592a113 --- /dev/null +++ b/srcs/WingsAPI.Game/Core/PacketHandling/IPacketHandlerContainer.cs @@ -0,0 +1,45 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Threading.Tasks; +using WingsEmu.Game.Networking; +using WingsEmu.Packets; + +namespace WingsEmu.Game._packetHandling; + +public interface IPacketHandlerContainer where T : IPacketHandler +{ + /// + /// Registers the given packet handler for the given packetType + /// + /// + /// + void Register(Type packetType, T handler); + + /// + /// Unregisters the given packetType from the container + /// + /// + void Unregister(Type packetType); + + /// + /// Executes the given packet with the given packetType + /// assuming that the sender is the given session + /// + /// + /// + /// + void Execute(IClientSession session, IClientPacket packet, Type packetType); + + /// + /// Asynchronously executes the given packet with the given packetType + /// assuming that the sender is the given session + /// + /// + /// + /// + /// + Task ExecuteAsync(IClientSession session, IClientPacket packet, Type packetType); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Core/PacketHandling/IUnauthedPacketHandler.cs b/srcs/WingsAPI.Game/Core/PacketHandling/IUnauthedPacketHandler.cs new file mode 100644 index 0000000..585f9d7 --- /dev/null +++ b/srcs/WingsAPI.Game/Core/PacketHandling/IUnauthedPacketHandler.cs @@ -0,0 +1,5 @@ +namespace WingsEmu.Game._packetHandling; + +public interface IUnauthedPacketHandler : IPacketHandler +{ +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Core/PacketHandling/PlayerEvent.cs b/srcs/WingsAPI.Game/Core/PacketHandling/PlayerEvent.cs new file mode 100644 index 0000000..4f1aa4c --- /dev/null +++ b/srcs/WingsAPI.Game/Core/PacketHandling/PlayerEvent.cs @@ -0,0 +1,13 @@ +// WingsEmu +// +// Developed by NosWings Team + +using PhoenixLib.Events; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Game._packetHandling; + +public class PlayerEvent : IAsyncEvent +{ + public IClientSession Sender { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Entities/Event/GenerateEntityDeathEvent.cs b/srcs/WingsAPI.Game/Entities/Event/GenerateEntityDeathEvent.cs new file mode 100644 index 0000000..696273a --- /dev/null +++ b/srcs/WingsAPI.Game/Entities/Event/GenerateEntityDeathEvent.cs @@ -0,0 +1,9 @@ +namespace WingsEmu.Game.Entities.Event; + +public class GenerateEntityDeathEvent : IBattleEntityEvent +{ + public IBattleEntity Attacker { get; init; } + + public bool? IsByMainWeapon { get; init; } + public IBattleEntity Entity { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Entities/Event/MapJoinMonsterEntityEvent.cs b/srcs/WingsAPI.Game/Entities/Event/MapJoinMonsterEntityEvent.cs new file mode 100644 index 0000000..c8b668e --- /dev/null +++ b/srcs/WingsAPI.Game/Entities/Event/MapJoinMonsterEntityEvent.cs @@ -0,0 +1,15 @@ +namespace WingsEmu.Game.Entities.Event; + +public class MapJoinMonsterEntityEvent : MonsterEntityEvent +{ + public MapJoinMonsterEntityEvent(IMonsterEntity monsterEntity, short? mapX = null, short? mapY = null, bool showEffect = false) : base(monsterEntity) + { + MapX = mapX; + MapY = mapY; + ShowEffect = showEffect; + } + + public short? MapX { get; } + public short? MapY { get; } + public bool ShowEffect { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Entities/Event/MapJoinNpcEntityEvent.cs b/srcs/WingsAPI.Game/Entities/Event/MapJoinNpcEntityEvent.cs new file mode 100644 index 0000000..7a1ff6b --- /dev/null +++ b/srcs/WingsAPI.Game/Entities/Event/MapJoinNpcEntityEvent.cs @@ -0,0 +1,13 @@ +namespace WingsEmu.Game.Entities.Event; + +public class MapJoinNpcEntityEvent : NpcEntityEvent +{ + public MapJoinNpcEntityEvent(INpcEntity npcEntity, short? mapX = null, short? mapY = null) : base(npcEntity) + { + MapX = mapX; + MapY = mapY; + } + + public short? MapX { get; } + public short? MapY { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Entities/Event/MapLeaveMonsterEntityEvent.cs b/srcs/WingsAPI.Game/Entities/Event/MapLeaveMonsterEntityEvent.cs new file mode 100644 index 0000000..708a30e --- /dev/null +++ b/srcs/WingsAPI.Game/Entities/Event/MapLeaveMonsterEntityEvent.cs @@ -0,0 +1,8 @@ +namespace WingsEmu.Game.Entities.Event; + +public class MapLeaveMonsterEntityEvent : MonsterEntityEvent +{ + public MapLeaveMonsterEntityEvent(IMonsterEntity monsterEntity) : base(monsterEntity) + { + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Entities/Event/MapLeaveNpcEntityEvent.cs b/srcs/WingsAPI.Game/Entities/Event/MapLeaveNpcEntityEvent.cs new file mode 100644 index 0000000..1be1269 --- /dev/null +++ b/srcs/WingsAPI.Game/Entities/Event/MapLeaveNpcEntityEvent.cs @@ -0,0 +1,8 @@ +namespace WingsEmu.Game.Entities.Event; + +public class MapLeaveNpcEntityEvent : NpcEntityEvent +{ + public MapLeaveNpcEntityEvent(INpcEntity npcEntity) : base(npcEntity) + { + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Entities/Extensions/VisualEntitiesExtensions.cs b/srcs/WingsAPI.Game/Entities/Extensions/VisualEntitiesExtensions.cs new file mode 100644 index 0000000..fbc6bb3 --- /dev/null +++ b/srcs/WingsAPI.Game/Entities/Extensions/VisualEntitiesExtensions.cs @@ -0,0 +1,67 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsEmu.Game.Extensions; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Game.Entities.Extensions; + +public static class VisualEntitiesExtensions +{ + public static string GenerateIn(this IMonsterEntity monsterEntity, bool inEffect = false) + { + if (monsterEntity.IsStillAlive) + { + return + $"in 3 {monsterEntity.MonsterVNum} {monsterEntity.Id} {monsterEntity.PositionX} {monsterEntity.PositionY} {monsterEntity.Direction} {monsterEntity.GetHpPercentage()} {monsterEntity.GetMpPercentage()} 0 0 0 -1 {(inEffect ? 0 : 1)} 0 {monsterEntity.Morph} - 0 -1 0 0 0 0 0 0 0 0"; + } + + return string.Empty; + } + + public static string GenerateIn(this INpcEntity entity, bool inEffect = false) + { + if (entity.IsStillAlive) + { + string timeSpaceOwnerName = entity.TimeSpaceOwnerId.HasValue ? entity.MapInstance.GetCharacterById(entity.TimeSpaceOwnerId.Value)?.Name : null; + + return + "in 2 " + + $"{entity.NpcVNum} " + + $"{entity.Id} " + + $"{entity.PositionX} " + + $"{entity.PositionY} " + + $"{entity.Direction} " + + $"{entity.GetHpPercentage()} " + + $"{entity.GetMpPercentage()} " + + $"{(entity.TimeSpaceInfo != null ? 10002 : entity.Dialog)} " + + "0 " + + $"{(entity.CharacterPartnerId.HasValue ? 3 : 0)} " + + $"{entity.CharacterPartnerId ?? -1} " + + $"{(inEffect ? 0 : 1)} " + + $"{(entity.IsSitting ? 1 : 0)} " + + "-1 " + + $"{(string.IsNullOrEmpty(entity.CustomName) ? "@" : entity.CustomName.Replace(' ', '^'))} " + + "0 " + + "-1 " + + "0 " + + "0 " + + "0 " + + "0 " + + "0 " + + "0 " + + "0 " + + "0 " + + $"{(entity.TimeSpaceInfo != null ? 647 : 0)} " + + $"{(string.IsNullOrEmpty(timeSpaceOwnerName) ? "0" : timeSpaceOwnerName)} " + + "0"; + } + + return string.Empty; + } + + public static string GenerateOut(this IEntity entity) => $"out {(byte)entity.Type} {entity.Id}"; + public static void BroadcastChatBubble(this IEntity entity, string message, ChatMessageColorType type) => entity.MapInstance.Broadcast(entity.GenerateSayPacket(message, type)); + public static string GenerateSayPacket(this IEntity entity, string message, ChatMessageColorType type) => $"say {(byte)entity.Type} {entity.Id} {(byte)type} {message}"; +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Entities/IAiEntity.cs b/srcs/WingsAPI.Game/Entities/IAiEntity.cs new file mode 100644 index 0000000..2213455 --- /dev/null +++ b/srcs/WingsAPI.Game/Entities/IAiEntity.cs @@ -0,0 +1,6 @@ +namespace WingsEmu.Game.Entities; + +public interface IAiEntity : IBattleEntity +{ + public bool IsStillAlive { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Entities/IBattleEntity.cs b/srcs/WingsAPI.Game/Entities/IBattleEntity.cs new file mode 100644 index 0000000..9b6528e --- /dev/null +++ b/srcs/WingsAPI.Game/Entities/IBattleEntity.cs @@ -0,0 +1,51 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Generic; +using WingsEmu.Core.Generics; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Battle.Managers; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Skills; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Entities; + +public interface IBattleEntity : IMoveableEntity, IEventTriggerContainer, IBattleEntityEventEmitter, ICastingComponent, IEndBuffDamageComponent +{ + public byte Level { get; set; } + public byte Direction { get; set; } + + byte HpPercentage => Math.Max((byte)1, (byte)(MaxHp <= 1 ? 1 : (byte)(Hp / (float)MaxHp * 100))); + public int Hp { get; set; } + public int MaxHp { get; set; } + + byte MpPercentage => Math.Max((byte)1, (byte)(MaxMp <= 1 ? 1 : (byte)(Mp / (float)MaxMp * 100))); + public int Mp { get; set; } + public int MaxMp { get; set; } + + public byte Element { get; set; } + + public int ElementRate { get; set; } + + public int FireResistance { get; set; } + + public int WaterResistance { get; set; } + + public int LightResistance { get; set; } + + public int DarkResistance { get; set; } + + public int DamagesMinimum { get; set; } + public int DamagesMaximum { get; set; } + public FactionType Faction { get; } + public IBattleEntity Killer { get; set; } + public byte Size { get; set; } + public List Skills { get; } + public IBuffComponent BuffComponent { get; } + public IBCardComponent BCardComponent { get; } + public IChargeComponent ChargeComponent { get; } + public ThreadSafeHashSet AggroedEntities { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Entities/IBattleEntityEvent.cs b/srcs/WingsAPI.Game/Entities/IBattleEntityEvent.cs new file mode 100644 index 0000000..5e02d28 --- /dev/null +++ b/srcs/WingsAPI.Game/Entities/IBattleEntityEvent.cs @@ -0,0 +1,8 @@ +using PhoenixLib.Events; + +namespace WingsEmu.Game.Entities; + +public interface IBattleEntityEvent : IAsyncEvent +{ + IBattleEntity Entity { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Entities/IBattleEntityEventEmitter.cs b/srcs/WingsAPI.Game/Entities/IBattleEntityEventEmitter.cs new file mode 100644 index 0000000..140602d --- /dev/null +++ b/srcs/WingsAPI.Game/Entities/IBattleEntityEventEmitter.cs @@ -0,0 +1,5 @@ +namespace WingsEmu.Game.Entities; + +public interface IBattleEntityEventEmitter : IGenericEventEmitter +{ +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Entities/IEntity.cs b/srcs/WingsAPI.Game/Entities/IEntity.cs new file mode 100644 index 0000000..803aba6 --- /dev/null +++ b/srcs/WingsAPI.Game/Entities/IEntity.cs @@ -0,0 +1,15 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsEmu.Game.Maps; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Entities; + +public interface IEntity +{ + public VisualType Type { get; } + public int Id { get; } + public IMapInstance MapInstance { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Entities/IGenericEventEmitter.cs b/srcs/WingsAPI.Game/Entities/IGenericEventEmitter.cs new file mode 100644 index 0000000..541dcbd --- /dev/null +++ b/srcs/WingsAPI.Game/Entities/IGenericEventEmitter.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace WingsEmu.Game.Entities; + +public interface IGenericEventEmitter +{ + public Task EmitEventAsync(T eventArgs) where T : TEventType; + public void EmitEvent(T eventArgs) where T : TEventType; +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Entities/IMonsterAdditionalData.cs b/srcs/WingsAPI.Game/Entities/IMonsterAdditionalData.cs new file mode 100644 index 0000000..bfa0bfa --- /dev/null +++ b/srcs/WingsAPI.Game/Entities/IMonsterAdditionalData.cs @@ -0,0 +1,17 @@ +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Entities; + +public interface IMonsterAdditionalData +{ + bool IsMateTrainer { get; } + bool IsBonus { get; set; } + bool IsBoss { get; } + bool IsTarget { get; } + bool IsMoving { get; } + bool VesselMonster { get; } + long? SummonerId { get; } + VisualType? SummonerType { get; } + SummonType? SummonType { get; } + bool IsHostile { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Entities/IMonsterData.cs b/srcs/WingsAPI.Game/Entities/IMonsterData.cs new file mode 100644 index 0000000..e1221f4 --- /dev/null +++ b/srcs/WingsAPI.Game/Entities/IMonsterData.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; +using WingsAPI.Data.Drops; +using WingsEmu.DTOs.BCards; +using WingsEmu.Game._enum; +using WingsEmu.Game.Skills; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Battle; + +namespace WingsEmu.Game.Entities; + +public interface IMonsterData +{ + short AmountRequired { get; } + byte ArmorLevel { get; } + AttackType AttackType { get; } + byte AttackUpgrade { get; } + byte BasicCastTime { get; } + short BasicCooldown { get; } + byte BasicRange { get; } + short AttackEffect { get; } + short BasicHitChance { get; } + bool CanBeCollected { get; } + bool CanBeDebuffed { get; } + bool CanBeCaught { get; } + bool CanBePushed { get; } + bool CanRegenMp { get; } + bool CanWalk { get; } + int CellSize { get; } + int CleanDamageMin { get; } + int CleanDamageMax { get; } + int CleanHitRate { get; } + int CleanMeleeDefence { get; } + int CleanRangeDefence { get; } + int CleanMagicDefence { get; } + int CleanDodge { get; } + int CleanHp { get; } + int CleanMp { get; } + short BaseCloseDefence { get; } + short BaseConcentrate { get; } + short BaseCriticalChance { get; } + short BaseCriticalRate { get; } + bool DamagedOnlyLastJajamaruSkill { get; } + int BaseDamageMaximum { get; } + int BaseDamageMinimum { get; } + short BaseDarkResistance { get; } + short DeathEffect { get; } + int BaseMaxHp { get; } + int BaseMaxMp { get; } + short DefenceDodge { get; } + byte DefenceUpgrade { get; } + bool DisappearAfterHitting { get; } + bool DisappearAfterSeconds { get; } + bool DisappearAfterSecondsMana { get; } + short DistanceDefence { get; } + short DistanceDefenceDodge { get; } + byte BaseElement { get; } + short BaseElementRate { get; } + short BaseFireResistance { get; } + FactionType? SuggestedFaction { get; } + int GiveDamagePercentage { get; } + int GroupAttack { get; } + bool HasMode { get; } + int RawHostility { get; } + int IconId { get; } + bool IsPercent { get; } + int JobXp { get; } + byte BaseLevel { get; } + short BaseLightResistance { get; } + short MagicDefence { get; } + short MagicMpFactor { get; } + short MeleeHpFactor { get; } + sbyte MinimumAttackRange { get; } + int MonsterVNum { get; } + string Name { get; } + byte NoticeRange { get; } + bool OnDefenseOnlyOnce { get; } + short PermanentEffect { get; } + MonsterRaceType MonsterRaceType { get; } + byte MonsterRaceSubType { get; } + short RangeDodgeFactor { get; } + TimeSpan BaseRespawnTime { get; } + int SpawnMobOrColor { get; } + byte BaseSpeed { get; } + int SpriteSize { get; } + int TakeDamages { get; } + short VNumRequired { get; } + short BaseWaterResistance { get; } + byte WeaponLevel { get; } + byte WinfoValue { get; } + int Xp { get; } + byte MaxTries { get; } + short CollectionCooldown { get; } + byte CollectionDanceTime { get; } + bool TeleportRemoveFromInventory { get; } + short BasicDashSpeed { get; } + bool ModeIsHpTriggered { get; } + byte ModeLimiterType { get; } + short ModeRangeTreshold { get; } + short ModeCModeVnum { get; } + short ModeHpTresholdOrItemVnum { get; } + short MidgardDamage { get; } + bool HasDash { get; } + bool DropToInventory { get; } + int BaseXp { get; } + int BaseJobXp { get; } + + public IReadOnlyList Drops { get; } + public bool CanSeeInvisible { get; } + public IReadOnlyList BCards { get; } + public IReadOnlyList ModeBCards { get; } + public IReadOnlyList MonsterSkills { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Entities/IMonsterEntity.cs b/srcs/WingsAPI.Game/Entities/IMonsterEntity.cs new file mode 100644 index 0000000..0597599 --- /dev/null +++ b/srcs/WingsAPI.Game/Entities/IMonsterEntity.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Monster; +using WingsEmu.Game.Raids; + +namespace WingsEmu.Game.Entities; + +public interface IMonsterEntity : INpcMonsterEntity, IMonsterAdditionalData +{ + Guid UniqueId { get; } + ConcurrentDictionary PlayersDamage { get; } + DateTime LastMpRegen { get; set; } + Position? GoToBossPosition { get; set; } + bool IsInstantBattle { get; set; } + IEnumerable RaidDrop { get; } + DateTime LastBonusEffectTime { get; set; } + DateTime AttentionTime { get; set; } + ConcurrentDictionary Waypoints { get; set; } + DateTime LastWayPoint { get; set; } + byte CurrentWayPoint { get; set; } + void GenerateDeath(IBattleEntity killer = null); + void RefreshStats(); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Entities/IMonsterEntityFactory.cs b/srcs/WingsAPI.Game/Entities/IMonsterEntityFactory.cs new file mode 100644 index 0000000..f0ec058 --- /dev/null +++ b/srcs/WingsAPI.Game/Entities/IMonsterEntityFactory.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Raids; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Entities; + +public enum SummonType +{ + NORMAL, + FROM_SKILL, + MONSTER_WAVE +} + +public class MonsterEntityBuilder +{ + public bool IsBonus { get; init; } + public bool IsBoss { get; init; } + public bool IsHostile { get; init; } + public bool IsMateTrainer { get; init; } + public bool IsRespawningOnDeath { get; init; } + public bool IsTarget { get; init; } + public bool IsVesselMonster { get; init; } + public bool IsWalkingAround { get; init; } + public short PositionX { get; init; } + public short PositionY { get; init; } + public byte Direction { get; init; } = 2; + public short SetHitChance { get; init; } + public Position? GoToBossPosition { get; init; } + public bool IsInstantBattle { get; init; } + public IEnumerable RaidDrop { get; init; } + public byte? Level { get; init; } + + public SummonType? SummonType { get; init; } + public long? SummonerId { get; init; } + public VisualType? SummonerType { get; init; } + public FactionType? FactionType { get; init; } + + public float? HpMultiplier { get; init; } + public float? MpMultiplier { get; init; } + + public Guid? GeneratedGuid { get; init; } +} + +public interface IMonsterEntityFactory +{ + /// + /// Creates a default respawnable, walking around monster + /// + /// + /// + /// + /// + IMonsterEntity CreateMapMonster(MapMonsterDTO monsterDto, IMapInstance mapInstance); + + IMonsterEntity CreateMonster(int monsterVNum, IMapInstance mapInstance, MonsterEntityBuilder monsterAdditionalData = null); + + IMonsterEntity CreateMonster(int? id, int monsterVNum, IMapInstance mapInstance, MonsterEntityBuilder monsterAdditionalData = null); + IMonsterEntity CreateMonster(IMonsterData monsterData, IMapInstance mapInstance, MonsterEntityBuilder monsterAdditionalData = null); + IMonsterEntity CreateMonster(int? entityId, IMonsterData monsterData, IMapInstance mapInstance, MonsterEntityBuilder monsterAdditionalData = null); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Entities/IMoveableEntity.cs b/srcs/WingsAPI.Game/Entities/IMoveableEntity.cs new file mode 100644 index 0000000..80308b7 --- /dev/null +++ b/srcs/WingsAPI.Game/Entities/IMoveableEntity.cs @@ -0,0 +1,16 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsEmu.Game.Helpers.Damages; + +namespace WingsEmu.Game.Entities; + +public interface IMoveableEntity : IEntity +{ + public Position Position { get; set; } + public short PositionX { get; } + public short PositionY { get; } + + public byte Speed { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Entities/INpcAdditionalData.cs b/srcs/WingsAPI.Game/Entities/INpcAdditionalData.cs new file mode 100644 index 0000000..6fab286 --- /dev/null +++ b/srcs/WingsAPI.Game/Entities/INpcAdditionalData.cs @@ -0,0 +1,25 @@ +using WingsEmu.Game.Characters; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.RainbowBattle; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Entities; + +public interface INpcAdditionalData +{ + bool IsTimeSpaceMate { get; } + bool IsProtected { get; } + IPlayerEntity MinilandOwner { get; } + bool NpcShouldRespawn { get; } + bool CanMove { get; } + public bool CanAttack { get; } + public byte NpcDirection { get; } + public bool IsHostile { get; } + public float? HpMultiplier { get; } + public float? MpMultiplier { get; } + public byte? CustomLevel { get; } + public FactionType FactionType { get; } + public long? TimeSpaceOwnerId { get; } + public TimeSpaceFileConfiguration TimeSpaceInfo { get; } + public RainBowFlag RainbowFlag { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Entities/INpcEntity.cs b/srcs/WingsAPI.Game/Entities/INpcEntity.cs new file mode 100644 index 0000000..4cc7935 --- /dev/null +++ b/srcs/WingsAPI.Game/Entities/INpcEntity.cs @@ -0,0 +1,34 @@ +using System; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Maps; +using WingsEmu.Game.RainbowBattle; +using WingsEmu.Game.Shops; + +namespace WingsEmu.Game.Entities; + +public interface INpcEntity : INpcMonsterEntity, INpcAdditionalData +{ + Guid UniqueId { get; } + bool IsHostile { get; } + bool CanAttack { get; } + public int? QuestDialog { get; } + short Dialog { get; } + short Effect { get; } + TimeSpan EffectDelay { get; } + bool IsMoving { get; } + bool IsSitting { get; } + int MapId { get; } + int NpcVNum { get; } + ShopNpc ShopNpc { get; set; } + bool HasGodMode { get; } + byte CurrentCollection { get; set; } + DateTime LastCollection { get; set; } + string CustomName { get; } + long? CharacterPartnerId { get; set; } + DateTime LastBasicAttack { get; set; } + DateTime LastTimeSpaceHeal { get; set; } + RainBowFlag RainbowFlag { get; set; } + long? TimeSpaceOwnerId { get; } + TimeSpaceFileConfiguration TimeSpaceInfo { get; } + void ChangeMapInstance(IMapInstance mapInstance); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Entities/INpcEntityFactory.cs b/srcs/WingsAPI.Game/Entities/INpcEntityFactory.cs new file mode 100644 index 0000000..7ed40b0 --- /dev/null +++ b/srcs/WingsAPI.Game/Entities/INpcEntityFactory.cs @@ -0,0 +1,15 @@ +using WingsEmu.DTOs.Maps; +using WingsEmu.Game.Maps; + +namespace WingsEmu.Game.Entities; + +public interface INpcEntityFactory +{ + public INpcEntity CreateMapNpc(int monsterVNum, IMapInstance mapInstance, int? id = null, INpcAdditionalData npcAdditionalData = null); + public INpcEntity CreateMapNpc(IMonsterData monsterDto, IMapInstance mapInstance, int? id = null, INpcAdditionalData npcAdditionalData = null); + public INpcEntity CreateMapNpc(MapNpcDTO npcDto, IMapInstance mapInstance, int? id = null, INpcAdditionalData npcAdditionalData = null); + public INpcEntity CreateMapNpc(IMonsterData monsterDto, MapNpcDTO npcDto, IMapInstance mapInstance, int? id = null, INpcAdditionalData npcAdditionalData = null); + + INpcEntity CreateNpc(int monsterVNum, IMapInstance mapInstance, int? id = null, INpcAdditionalData npcAdditionalData = null); + INpcEntity CreateNpc(IMonsterData monsterDto, IMapInstance mapInstance, int? id = null, INpcAdditionalData npcAdditionalData = null); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Entities/INpcMonsterEntity.cs b/srcs/WingsAPI.Game/Entities/INpcMonsterEntity.cs new file mode 100644 index 0000000..0bffb6f --- /dev/null +++ b/srcs/WingsAPI.Game/Entities/INpcMonsterEntity.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using WingsEmu.Core.Generics; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Skills; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Entities; + +public interface INpcMonsterEntity : IAiEntity, IMonsterData +{ + SkillInfo BasicSkill { get; } + ThreadSafeHashSet Damagers { get; } + ThreadSafeHashSet Targets { get; } + ThreadSafeHashSet<(VisualType, long)> TargetsByVisualTypeAndId { get; } + DateTime LastTargetsRefresh { get; set; } + DateTime Death { get; set; } + short FirstX { get; set; } + short FirstY { get; set; } + DateTime LastEffect { get; set; } + DateTime LastSkill { get; set; } + DateTime LastSpecialHpDecrease { get; set; } + bool ShouldRespawn { get; } + bool ReturningToFirstPosition { get; set; } + bool ShouldFindNewTarget { get; set; } + bool FindNewPositionAroundTarget { get; set; } + bool IsApproachingTarget { get; set; } + bool OnFirstDamageReceive { get; set; } + DateTime SpawnDate { get; set; } + IBattleEntity Target { get; set; } + DateTime NextTick { get; set; } + DateTime NextAttackReady { get; set; } + bool ModeIsActive { get; set; } + short Morph { get; set; } + long ModeDeathsSinceRespawn { get; set; } + (VisualType, long) LastAttackedEntity { get; set; } + bool IsRunningAway { get; set; } + byte ReturnTimeOut { get; set; } + + IReadOnlyList NotBasicSkills { get; } + IReadOnlyList SkillsWithoutDashSkill { get; } + INpcMonsterSkill ReplacedBasicSkill { get; } + INpcMonsterSkill DashSkill { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Entities/IShopFactory.cs b/srcs/WingsAPI.Game/Entities/IShopFactory.cs new file mode 100644 index 0000000..cee8ba7 --- /dev/null +++ b/srcs/WingsAPI.Game/Entities/IShopFactory.cs @@ -0,0 +1,9 @@ +using WingsAPI.Data.Shops; +using WingsEmu.Game.Shops; + +namespace WingsEmu.Game.Entities; + +public interface IShopFactory +{ + ShopNpc CreateShop(ShopDTO shopDto); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Entities/MonsterEntityEvent.cs b/srcs/WingsAPI.Game/Entities/MonsterEntityEvent.cs new file mode 100644 index 0000000..3e3010f --- /dev/null +++ b/srcs/WingsAPI.Game/Entities/MonsterEntityEvent.cs @@ -0,0 +1,9 @@ +namespace WingsEmu.Game.Entities; + +public class MonsterEntityEvent : IBattleEntityEvent +{ + public MonsterEntityEvent(IMonsterEntity monsterEntity) => MonsterEntity = monsterEntity; + + public IMonsterEntity MonsterEntity { get; } + public IBattleEntity Entity => MonsterEntity; +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Entities/NpcAdditionalData.cs b/srcs/WingsAPI.Game/Entities/NpcAdditionalData.cs new file mode 100644 index 0000000..0083225 --- /dev/null +++ b/srcs/WingsAPI.Game/Entities/NpcAdditionalData.cs @@ -0,0 +1,25 @@ +using WingsEmu.Game.Characters; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.RainbowBattle; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Entities; + +public class NpcAdditionalData : INpcAdditionalData +{ + public bool IsTimeSpaceMate { get; init; } + public bool IsProtected { get; init; } + public IPlayerEntity MinilandOwner { get; init; } + public bool NpcShouldRespawn { get; init; } + public bool CanMove { get; init; } + public bool CanAttack { get; init; } + public byte NpcDirection { get; init; } + public bool IsHostile { get; init; } + public float? HpMultiplier { get; init; } + public float? MpMultiplier { get; init; } + public byte? CustomLevel { get; init; } + public FactionType FactionType { get; init; } + public long? TimeSpaceOwnerId { get; init; } + public TimeSpaceFileConfiguration TimeSpaceInfo { get; init; } + public RainBowFlag RainbowFlag { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Entities/NpcEntityEvent.cs b/srcs/WingsAPI.Game/Entities/NpcEntityEvent.cs new file mode 100644 index 0000000..94f1f87 --- /dev/null +++ b/srcs/WingsAPI.Game/Entities/NpcEntityEvent.cs @@ -0,0 +1,9 @@ +namespace WingsEmu.Game.Entities; + +public class NpcEntityEvent : IBattleEntityEvent +{ + public NpcEntityEvent(INpcEntity npcEntity) => NpcEntity = npcEntity; + + public INpcEntity NpcEntity { get; } + public IBattleEntity Entity => NpcEntity; +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/EntityStatistics/IMateStatisticsComponent.cs b/srcs/WingsAPI.Game/EntityStatistics/IMateStatisticsComponent.cs new file mode 100644 index 0000000..7c8be97 --- /dev/null +++ b/srcs/WingsAPI.Game/EntityStatistics/IMateStatisticsComponent.cs @@ -0,0 +1,23 @@ +using WingsEmu.Game.Mates; + +namespace WingsEmu.Game.EntityStatistics; + +public interface IMateStatisticsComponent +{ + public int MinDamage { get; } + public int MaxDamage { get; } + public int HitRate { get; } + public int CriticalChance { get; } + public int CriticalDamage { get; } + public int MeleeDefense { get; } + public int RangeDefense { get; } + public int MagicDefense { get; } + public int MeleeDodge { get; } + public int RangeDodge { get; } + public int FireResistance { get; } + public int WaterResistance { get; } + public int LightResistance { get; } + public int ShadowResistance { get; } + + public void RefreshMateStatistics(IMateEntity mateEntity); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/EntityStatistics/IPlayerStatisticsComponent.cs b/srcs/WingsAPI.Game/EntityStatistics/IPlayerStatisticsComponent.cs new file mode 100644 index 0000000..d5f3c49 --- /dev/null +++ b/srcs/WingsAPI.Game/EntityStatistics/IPlayerStatisticsComponent.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; + +namespace WingsEmu.Game.EntityStatistics; + +public interface IPlayerStatisticsComponent +{ + IReadOnlyDictionary Passives { get; } + public int MinDamage { get; } + public int MaxDamage { get; } + public int HitRate { get; } + public int CriticalChance { get; } + public int CriticalDamage { get; } + public int SecondMinDamage { get; } + public int SecondMaxDamage { get; } + public int SecondHitRate { get; } + public int SecondCriticalChance { get; } + public int SecondCriticalDamage { get; } + public int MeleeDefense { get; } + public int RangeDefense { get; } + public int MagicDefense { get; } + public int MeleeDodge { get; } + public int RangeDodge { get; } + public int FireResistance { get; } + public int WaterResistance { get; } + public int LightResistance { get; } + public int ShadowResistance { get; } + void RefreshPassives(); + public void RefreshPlayerStatistics(); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/EntityStatistics/MateStatisticsComponent.cs b/srcs/WingsAPI.Game/EntityStatistics/MateStatisticsComponent.cs new file mode 100644 index 0000000..7cbcf55 --- /dev/null +++ b/srcs/WingsAPI.Game/EntityStatistics/MateStatisticsComponent.cs @@ -0,0 +1,376 @@ +using System.Collections.Generic; +using System.Linq; +using WingsAPI.Data.Families; +using WingsEmu.Core.Extensions; +using WingsEmu.DTOs.BCards; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Mates; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Battle; + +namespace WingsEmu.Game.EntityStatistics; + +public class MateStatisticsComponent : IMateStatisticsComponent +{ + private readonly Dictionary _stats = new(); + private IMateEntity _mateEntity; + + public MateStatisticsComponent(IMateEntity mateEntity) => _mateEntity = mateEntity; + + public int MinDamage => _mateEntity?.DamagesMinimum + _stats.GetValueOrDefault(Statistics.MIN_DAMAGE) ?? 0; + public int MaxDamage => _mateEntity?.DamagesMaximum + _stats.GetValueOrDefault(Statistics.MAX_DAMAGE) ?? 0; + public int HitRate => _mateEntity?.HitRate + _stats.GetValueOrDefault(Statistics.MIN_DAMAGE) ?? 0; + public int CriticalChance => _mateEntity?.HitCriticalChance + _stats.GetValueOrDefault(Statistics.CRITICAL_CHANCE) ?? 0; + public int CriticalDamage => _mateEntity?.HitCriticalDamage + _stats.GetValueOrDefault(Statistics.CRITICAL_DAMAGE) ?? 0; + public int MeleeDefense => _mateEntity?.CloseDefence + _stats.GetValueOrDefault(Statistics.MELEE_DEFENSE) ?? 0; + public int RangeDefense => _mateEntity?.DistanceDefence + _stats.GetValueOrDefault(Statistics.RANGE_DEFENSE) ?? 0; + public int MagicDefense => _mateEntity?.MagicDefence + _stats.GetValueOrDefault(Statistics.MAGIC_DEFENSE) ?? 0; + public int MeleeDodge => _mateEntity?.DefenceDodge + _stats.GetValueOrDefault(Statistics.MELEE_DODGE) ?? 0; + public int RangeDodge => _mateEntity?.DistanceDodge + _stats.GetValueOrDefault(Statistics.RANGE_DODGE) ?? 0; + public int FireResistance => _mateEntity?.FireResistance + _stats.GetValueOrDefault(Statistics.FIRE_RESISTANCE) ?? 0; + public int WaterResistance => _mateEntity?.WaterResistance + _stats.GetValueOrDefault(Statistics.WATER_RESISTANCE) ?? 0; + public int LightResistance => _mateEntity?.LightResistance + _stats.GetValueOrDefault(Statistics.LIGHT_RESISTANCE) ?? 0; + public int ShadowResistance => _mateEntity?.DarkResistance + _stats.GetValueOrDefault(Statistics.SHADOW_RESISTANCE) ?? 0; + + public void RefreshMateStatistics(IMateEntity mateEntity) + { + _mateEntity = mateEntity; + if (_mateEntity.MateType == MateType.Pet) + { + return; + } + + _stats.Clear(); + int minDamage = 0; + int maxDamage = 0; + int hitRate = 0; + int criticalChance = 0; + int criticalDamage = 0; + int meleeDefense = 0; + int rangeDefense = 0; + int magicDefense = 0; + int meleeDodge = 0; + int rangeDodge = 0; + int fireResistance = 0; + int waterResistance = 0; + int lightResistance = 0; + int shadowResistance = 0; + + AttackType attackType = _mateEntity.AttackType; + + byte playerLevel = _mateEntity.Level; + + IReadOnlyList bCards = _mateEntity.BCardComponent.GetAllBCards(); + IEnumerable minMaxDamageBCards = bCards.Where(x => x.Type == (short)BCardType.AttackPower); + IEnumerable hitRateBCards = bCards.Where(x => x.Type == (short)BCardType.Target); + IEnumerable criticalBCards = bCards.Where(x => x.Type == (short)BCardType.Critical); + IEnumerable defenseBCards = bCards.Where(x => x.Type == (short)BCardType.Defence); + IEnumerable dodgeBCards = bCards.Where(x => x.Type == (short)BCardType.DodgeAndDefencePercent); + IEnumerable resistanceBCards = bCards.Where(x => x.Type == (short)BCardType.ElementResistance); + + foreach (BCardDTO bCard in minMaxDamageBCards) + { + int firstData = bCard.FirstDataValue(playerLevel); + + switch ((AdditionalTypes.AttackPower)bCard.SubType) + { + case AdditionalTypes.AttackPower.AllAttacksIncreased: + minDamage += firstData; + maxDamage += firstData; + break; + case AdditionalTypes.AttackPower.AllAttacksDecreased: + minDamage -= firstData; + maxDamage -= firstData; + break; + case AdditionalTypes.AttackPower.MeleeAttacksIncreased: + if (attackType != AttackType.Melee) + { + break; + } + + minDamage += firstData; + maxDamage += firstData; + break; + case AdditionalTypes.AttackPower.MeleeAttacksDecreased: + if (attackType != AttackType.Melee) + { + break; + } + + minDamage -= firstData; + maxDamage -= firstData; + break; + case AdditionalTypes.AttackPower.RangedAttacksIncreased: + if (attackType != AttackType.Ranged) + { + break; + } + + minDamage += firstData; + maxDamage += firstData; + break; + case AdditionalTypes.AttackPower.RangedAttacksDecreased: + if (attackType != AttackType.Ranged) + { + break; + } + + minDamage -= firstData; + maxDamage -= firstData; + break; + case AdditionalTypes.AttackPower.MagicalAttacksIncreased: + if (attackType != AttackType.Magical) + { + break; + } + + minDamage += firstData; + maxDamage += firstData; + break; + case AdditionalTypes.AttackPower.MagicalAttacksDecreased: + if (attackType != AttackType.Magical) + { + break; + } + + minDamage -= firstData; + maxDamage -= firstData; + break; + } + } + + foreach (BCardDTO bCard in hitRateBCards) + { + int firstData = bCard.FirstDataValue(playerLevel); + switch ((AdditionalTypes.Target)bCard.SubType) + { + case AdditionalTypes.Target.AllHitRateIncreased: + hitRate += firstData; + break; + case AdditionalTypes.Target.AllHitRateDecreased: + hitRate -= firstData; + break; + case AdditionalTypes.Target.MeleeHitRateIncreased: + if (attackType != AttackType.Melee) + { + break; + } + + hitRate += firstData; + break; + case AdditionalTypes.Target.MeleeHitRateDecreased: + if (attackType != AttackType.Melee) + { + break; + } + + hitRate -= firstData; + break; + case AdditionalTypes.Target.RangedHitRateIncreased: + if (attackType != AttackType.Ranged) + { + break; + } + + hitRate += firstData; + break; + case AdditionalTypes.Target.RangedHitRateDecreased: + if (attackType != AttackType.Ranged) + { + break; + } + + hitRate -= firstData; + break; + case AdditionalTypes.Target.MagicalConcentrationIncreased: + if (attackType != AttackType.Magical) + { + break; + } + + hitRate += firstData; + break; + case AdditionalTypes.Target.MagicalConcentrationDecreased: + if (attackType != AttackType.Magical) + { + break; + } + + hitRate -= firstData; + break; + } + } + + foreach (BCardDTO bCard in criticalBCards) + { + int firstData = bCard.FirstDataValue(playerLevel); + + switch ((AdditionalTypes.Critical)bCard.SubType) + { + case AdditionalTypes.Critical.InflictingIncreased: + if (attackType == AttackType.Magical) + { + break; + } + + criticalChance += firstData; + break; + case AdditionalTypes.Critical.InflictingReduced: + if (attackType == AttackType.Magical) + { + break; + } + + criticalChance -= firstData; + break; + case AdditionalTypes.Critical.DamageIncreased: + if (attackType == AttackType.Magical) + { + break; + } + + criticalDamage += firstData; + break; + case AdditionalTypes.Critical.DamageIncreasedInflictingReduced: + if (attackType == AttackType.Magical) + { + break; + } + + criticalDamage -= firstData; + break; + } + } + + foreach (BCardDTO bCard in defenseBCards) + { + int firstData = bCard.FirstDataValue(playerLevel); + + switch ((AdditionalTypes.Defence)bCard.SubType) + { + case AdditionalTypes.Defence.AllIncreased: + meleeDefense += firstData; + rangeDefense += firstData; + magicDefense += firstData; + break; + case AdditionalTypes.Defence.AllDecreased: + meleeDefense -= firstData; + rangeDefense -= firstData; + magicDefense -= firstData; + break; + case AdditionalTypes.Defence.MeleeIncreased: + meleeDefense += firstData; + break; + case AdditionalTypes.Defence.MeleeDecreased: + meleeDefense -= firstData; + break; + case AdditionalTypes.Defence.RangedIncreased: + rangeDefense += firstData; + break; + case AdditionalTypes.Defence.RangedDecreased: + rangeDefense -= firstData; + break; + case AdditionalTypes.Defence.MagicalIncreased: + magicDefense += firstData; + break; + case AdditionalTypes.Defence.MagicalDecreased: + magicDefense -= firstData; + break; + } + } + + foreach (BCardDTO bCard in dodgeBCards) + { + int firstData = bCard.FirstDataValue(playerLevel); + + switch ((AdditionalTypes.DodgeAndDefencePercent)bCard.SubType) + { + case AdditionalTypes.DodgeAndDefencePercent.DodgeIncreased: + meleeDodge += firstData; + rangeDodge += firstData; + break; + case AdditionalTypes.DodgeAndDefencePercent.DodgeDecreased: + meleeDodge -= firstData; + rangeDodge -= firstData; + break; + case AdditionalTypes.DodgeAndDefencePercent.DodgingMeleeIncreased: + meleeDodge += firstData; + break; + case AdditionalTypes.DodgeAndDefencePercent.DodgingMeleeDecreased: + meleeDodge -= firstData; + break; + case AdditionalTypes.DodgeAndDefencePercent.DodgingRangedIncreased: + rangeDodge += firstData; + break; + case AdditionalTypes.DodgeAndDefencePercent.DodgingRangedDecreased: + rangeDodge -= firstData; + break; + } + } + + foreach (BCardDTO bCard in resistanceBCards) + { + int firstData = bCard.FirstDataValue(playerLevel); + + switch ((AdditionalTypes.ElementResistance)bCard.SubType) + { + case AdditionalTypes.ElementResistance.AllIncreased: + fireResistance += firstData; + waterResistance += firstData; + lightResistance += firstData; + shadowResistance += firstData; + break; + case AdditionalTypes.ElementResistance.AllDecreased: + fireResistance -= firstData; + waterResistance -= firstData; + lightResistance -= firstData; + shadowResistance -= firstData; + break; + case AdditionalTypes.ElementResistance.FireIncreased: + fireResistance += firstData; + break; + case AdditionalTypes.ElementResistance.FireDecreased: + fireResistance -= firstData; + break; + case AdditionalTypes.ElementResistance.WaterIncreased: + waterResistance += firstData; + break; + case AdditionalTypes.ElementResistance.WaterDecreased: + waterResistance -= firstData; + break; + case AdditionalTypes.ElementResistance.LightIncreased: + lightResistance += firstData; + break; + case AdditionalTypes.ElementResistance.LightDecreased: + lightResistance -= firstData; + break; + case AdditionalTypes.ElementResistance.DarkIncreased: + shadowResistance += firstData; + break; + case AdditionalTypes.ElementResistance.DarkDecreased: + shadowResistance -= firstData; + break; + } + } + + fireResistance += _mateEntity.Owner?.Family?.UpgradeValues?.GetOrDefault(FamilyUpgradeType.FIRE_RESISTANCE) ?? 0; + waterResistance += _mateEntity.Owner?.Family?.UpgradeValues?.GetOrDefault(FamilyUpgradeType.WATER_RESISTANCE) ?? 0; + lightResistance += _mateEntity.Owner?.Family?.UpgradeValues?.GetOrDefault(FamilyUpgradeType.LIGHT_RESISTANCE) ?? 0; + shadowResistance += _mateEntity.Owner?.Family?.UpgradeValues?.GetOrDefault(FamilyUpgradeType.DARK_RESISTANCE) ?? 0; + + _stats[Statistics.MIN_DAMAGE] = minDamage; + _stats[Statistics.MAX_DAMAGE] = maxDamage; + _stats[Statistics.HITRATE] = hitRate; + _stats[Statistics.CRITICAL_CHANCE] = criticalChance; + _stats[Statistics.CRITICAL_DAMAGE] = criticalDamage; + _stats[Statistics.MELEE_DEFENSE] = meleeDefense; + _stats[Statistics.RANGE_DEFENSE] = rangeDefense; + _stats[Statistics.MAGIC_DEFENSE] = magicDefense; + _stats[Statistics.MELEE_DODGE] = meleeDodge; + _stats[Statistics.RANGE_DODGE] = rangeDodge; + _stats[Statistics.FIRE_RESISTANCE] = fireResistance; + _stats[Statistics.WATER_RESISTANCE] = waterResistance; + _stats[Statistics.LIGHT_RESISTANCE] = lightResistance; + _stats[Statistics.SHADOW_RESISTANCE] = shadowResistance; + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/EntityStatistics/PlayerStatisticsComponent.cs b/srcs/WingsAPI.Game/EntityStatistics/PlayerStatisticsComponent.cs new file mode 100644 index 0000000..d72a412 --- /dev/null +++ b/srcs/WingsAPI.Game/EntityStatistics/PlayerStatisticsComponent.cs @@ -0,0 +1,613 @@ +using System.Collections.Generic; +using System.Linq; +using WingsAPI.Data.Families; +using WingsAPI.Packets.Enums.Shells; +using WingsEmu.Core.Extensions; +using WingsEmu.DTOs.BCards; +using WingsEmu.DTOs.Skills; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Extensions; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Character; + +namespace WingsEmu.Game.EntityStatistics; + +public class PlayerStatisticsComponent : IPlayerStatisticsComponent +{ + private readonly Dictionary _passive = new(); + private readonly IPlayerEntity _playerEntity; + private readonly Dictionary _stats = new(); + + public PlayerStatisticsComponent(IPlayerEntity playerEntity) => _playerEntity = playerEntity; + + public IReadOnlyDictionary Passives => _passive; + + public void RefreshPassives() + { + _passive.Clear(); + IEnumerable passiveSkills = _playerEntity.CharacterSkills.Values.Where(x => x?.Skill != null && x.Skill.IsPassiveSkill()).Select(x => x.Skill); + + int hp = 0; + int mp = 0; + int meleeAttack = 0; + int rangedAttack = 0; + int magicAttack = 0; + int regenHp = 0; + int regenMp = 0; + int passiveRegen = 1; + int meleeDefence = 0; + int rangedDefence = 0; + int magicDefence = 0; + int meleeHitrate = 0; + int rangedHitrate = 0; + int meleeDodge = 0; + int rangedDodge = 0; + + foreach (SkillDTO skill in passiveSkills) + { + switch (skill.CastId) + { + case 0: + meleeAttack += skill.UpgradeSkill; + meleeDefence += skill.UpgradeSkill / 2; + break; + case 1: + rangedAttack += skill.UpgradeSkill; + rangedDefence += skill.UpgradeSkill / 2; + meleeHitrate += skill.UpgradeSkill * 2; + rangedHitrate += skill.UpgradeSkill * 2; + meleeDodge += skill.UpgradeSkill; + rangedDodge += skill.UpgradeSkill; + break; + case 2: + magicAttack += skill.UpgradeSkill; + magicDefence += skill.UpgradeSkill / 2; + break; + case 4: + hp += skill.UpgradeSkill; + break; + case 5: + mp += skill.UpgradeSkill; + break; + case 6: + meleeAttack += skill.UpgradeSkill; + rangedAttack += skill.UpgradeSkill; + magicAttack += skill.UpgradeSkill; + break; + case 7: + meleeDefence += skill.UpgradeSkill; + rangedDefence += skill.UpgradeSkill; + magicDefence += skill.UpgradeSkill; + break; + case 8: + regenHp += skill.UpgradeSkill; + break; + case 9: + regenMp += skill.UpgradeSkill; + break; + case 10: + passiveRegen += skill.UpgradeSkill; + break; + } + } + + _passive[PassiveType.HP] = hp; + _passive[PassiveType.MP] = mp; + _passive[PassiveType.MELEE_ATTACK] = meleeAttack; + _passive[PassiveType.RANGED_ATTACK] = rangedAttack; + _passive[PassiveType.MAGIC_ATTACK] = magicAttack; + _passive[PassiveType.REGEN_HP] = regenHp; + _passive[PassiveType.REGEN_MP] = regenMp; + _passive[PassiveType.PASSIVE_REGEN] = passiveRegen; + _passive[PassiveType.MELEE_DEFENCE] = meleeDefence; + _passive[PassiveType.RANGED_DEFENCE] = rangedDefence; + _passive[PassiveType.MAGIC_DEFENCE] = magicDefence; + _passive[PassiveType.MELEE_HIRATE] = meleeHitrate; + _passive[PassiveType.RANGED_HIRATE] = rangedHitrate; + _passive[PassiveType.MELEE_DODGE] = meleeDodge; + _passive[PassiveType.RANGED_DODGE] = rangedDodge; + } + + public int MinDamage => _playerEntity.DamagesMinimum + _stats.GetValueOrDefault(Statistics.MIN_DAMAGE); + public int MaxDamage => _playerEntity.DamagesMaximum + _stats.GetValueOrDefault(Statistics.MAX_DAMAGE); + public int HitRate => _playerEntity.HitRate + _stats.GetValueOrDefault(Statistics.HITRATE); + public int CriticalChance => _playerEntity.HitCriticalChance + _stats.GetValueOrDefault(Statistics.CRITICAL_CHANCE); + public int CriticalDamage => _playerEntity.HitCriticalDamage + _stats.GetValueOrDefault(Statistics.CRITICAL_DAMAGE); + public int SecondMinDamage => _playerEntity.SecondDamageMinimum + _stats.GetValueOrDefault(Statistics.SECOND_MIN_DAMAGE); + public int SecondMaxDamage => _playerEntity.SecondDamageMaximum + _stats.GetValueOrDefault(Statistics.SECOND_MAX_DAMAGE); + public int SecondHitRate => _playerEntity.SecondHitRate + _stats.GetValueOrDefault(Statistics.SECOND_HITRATE); + public int SecondCriticalChance => _playerEntity.SecondHitCriticalChance + _stats.GetValueOrDefault(Statistics.SECOND_CRITICAL_CHANCE); + public int SecondCriticalDamage => _playerEntity.SecondHitCriticalDamage + _stats.GetValueOrDefault(Statistics.SECOND_CRITICAL_DAMAGE); + public int MeleeDefense => _playerEntity.MeleeDefence + _stats.GetValueOrDefault(Statistics.MELEE_DEFENSE); + public int RangeDefense => _playerEntity.RangedDefence + _stats.GetValueOrDefault(Statistics.RANGE_DEFENSE); + public int MagicDefense => _playerEntity.MagicDefence + _stats.GetValueOrDefault(Statistics.MAGIC_DEFENSE); + public int MeleeDodge => _playerEntity.MeleeDodge + _stats.GetValueOrDefault(Statistics.MELEE_DODGE); + public int RangeDodge => _playerEntity.RangedDodge + _stats.GetValueOrDefault(Statistics.RANGE_DODGE); + public int FireResistance => _playerEntity.FireResistance + _stats.GetValueOrDefault(Statistics.FIRE_RESISTANCE); + public int WaterResistance => _playerEntity.WaterResistance + _stats.GetValueOrDefault(Statistics.WATER_RESISTANCE); + public int LightResistance => _playerEntity.LightResistance + _stats.GetValueOrDefault(Statistics.LIGHT_RESISTANCE); + public int ShadowResistance => _playerEntity.DarkResistance + _stats.GetValueOrDefault(Statistics.SHADOW_RESISTANCE); + + public void RefreshPlayerStatistics() + { + _stats.Clear(); + int minDamage = 0; + int maxDamage = 0; + int hitRate = 0; + int criticalChance = 0; + int criticalDamage = 0; + int secondMinDamage = 0; + int secondMaxDamage = 0; + int secondHitRate = 0; + int secondCriticalChance = 0; + int secondCriticalDamage = 0; + int meleeDefense = 0; + int rangeDefense = 0; + int magicDefense = 0; + int meleeDodge = 0; + int rangeDodge = 0; + int fireResistance = 0; + int waterResistance = 0; + int lightResistance = 0; + int shadowResistance = 0; + + ClassType classType = _playerEntity.Class; + + byte playerLevel = _playerEntity.Level; + + IReadOnlyList bCards = _playerEntity.BCardComponent.GetAllBCards(); + IEnumerable minMaxDamageBCards = bCards.Where(x => x.Type == (short)BCardType.AttackPower); + IEnumerable hitRateBCards = bCards.Where(x => x.Type == (short)BCardType.Target); + IEnumerable criticalBCards = bCards.Where(x => x.Type == (short)BCardType.Critical); + IEnumerable defenseBCards = bCards.Where(x => x.Type == (short)BCardType.Defence); + IEnumerable dodgeBCards = bCards.Where(x => x.Type == (short)BCardType.DodgeAndDefencePercent); + IEnumerable resistanceBCards = bCards.Where(x => x.Type == (short)BCardType.ElementResistance); + + foreach (BCardDTO bCard in minMaxDamageBCards) + { + int firstData = bCard.FirstDataValue(playerLevel); + + switch ((AdditionalTypes.AttackPower)bCard.SubType) + { + case AdditionalTypes.AttackPower.AllAttacksIncreased: + minDamage += firstData; + maxDamage += firstData; + secondMinDamage += firstData; + secondMaxDamage += firstData; + break; + case AdditionalTypes.AttackPower.AllAttacksDecreased: + minDamage -= firstData; + maxDamage -= firstData; + secondMinDamage -= firstData; + secondMaxDamage -= firstData; + break; + case AdditionalTypes.AttackPower.MeleeAttacksIncreased: + minDamage += classType switch + { + ClassType.Adventurer => firstData, + ClassType.Swordman => firstData, + ClassType.Wrestler => firstData, + _ => 0 + }; + + maxDamage += classType switch + { + ClassType.Adventurer => firstData, + ClassType.Swordman => firstData, + ClassType.Wrestler => firstData, + _ => 0 + }; + + secondMinDamage += classType switch + { + ClassType.Archer => firstData, + _ => 0 + }; + + secondMaxDamage += classType switch + { + ClassType.Archer => firstData, + _ => 0 + }; + break; + case AdditionalTypes.AttackPower.MeleeAttacksDecreased: + minDamage -= classType switch + { + ClassType.Adventurer => firstData, + ClassType.Swordman => firstData, + ClassType.Wrestler => firstData, + _ => 0 + }; + + maxDamage -= classType switch + { + ClassType.Adventurer => firstData, + ClassType.Swordman => firstData, + ClassType.Wrestler => firstData, + _ => 0 + }; + + secondMinDamage -= classType switch + { + ClassType.Archer => firstData, + _ => 0 + }; + + secondMaxDamage -= classType switch + { + ClassType.Archer => firstData, + _ => 0 + }; + break; + case AdditionalTypes.AttackPower.RangedAttacksIncreased: + minDamage += classType switch + { + ClassType.Archer => firstData, + _ => 0 + }; + + maxDamage += classType switch + { + ClassType.Archer => firstData, + _ => 0 + }; + + if (classType == ClassType.Archer) + { + break; + } + + secondMinDamage += firstData; + secondMaxDamage += firstData; + break; + case AdditionalTypes.AttackPower.RangedAttacksDecreased: + minDamage -= classType switch + { + ClassType.Archer => firstData, + _ => 0 + }; + + maxDamage -= classType switch + { + ClassType.Archer => firstData, + _ => 0 + }; + + if (classType == ClassType.Archer) + { + break; + } + + secondMinDamage -= firstData; + secondMaxDamage -= firstData; + break; + case AdditionalTypes.AttackPower.MagicalAttacksIncreased: + if (classType != ClassType.Magician) + { + break; + } + + minDamage += firstData; + maxDamage += firstData; + break; + case AdditionalTypes.AttackPower.MagicalAttacksDecreased: + if (classType != ClassType.Magician) + { + break; + } + + minDamage -= firstData; + maxDamage -= firstData; + break; + } + } + + foreach (BCardDTO bCard in hitRateBCards) + { + int firstData = bCard.FirstDataValue(playerLevel); + switch ((AdditionalTypes.Target)bCard.SubType) + { + case AdditionalTypes.Target.AllHitRateIncreased: + hitRate += firstData; + secondHitRate += firstData; + break; + case AdditionalTypes.Target.AllHitRateDecreased: + hitRate -= firstData; + secondHitRate -= firstData; + break; + case AdditionalTypes.Target.MeleeHitRateIncreased: + hitRate += classType switch + { + ClassType.Adventurer => firstData, + ClassType.Swordman => firstData, + ClassType.Wrestler => firstData, + _ => 0 + }; + + secondHitRate += classType switch + { + ClassType.Archer => firstData, + _ => 0 + }; + break; + case AdditionalTypes.Target.MeleeHitRateDecreased: + hitRate -= classType switch + { + ClassType.Adventurer => firstData, + ClassType.Swordman => firstData, + ClassType.Wrestler => firstData, + _ => 0 + }; + + secondHitRate -= classType switch + { + ClassType.Archer => firstData, + _ => 0 + }; + break; + case AdditionalTypes.Target.RangedHitRateIncreased: + hitRate += classType switch + { + ClassType.Archer => firstData, + _ => 0 + }; + + if (classType == ClassType.Archer) + { + break; + } + + secondHitRate += firstData; + break; + case AdditionalTypes.Target.RangedHitRateDecreased: + hitRate -= classType switch + { + ClassType.Archer => firstData, + _ => 0 + }; + + if (classType == ClassType.Archer) + { + break; + } + + secondHitRate -= firstData; + break; + case AdditionalTypes.Target.MagicalConcentrationIncreased: + if (classType != ClassType.Magician) + { + break; + } + + hitRate += firstData; + break; + case AdditionalTypes.Target.MagicalConcentrationDecreased: + if (classType != ClassType.Magician) + { + break; + } + + hitRate -= firstData; + break; + } + } + + foreach (BCardDTO bCard in criticalBCards) + { + int firstData = bCard.FirstDataValue(playerLevel); + + switch ((AdditionalTypes.Critical)bCard.SubType) + { + case AdditionalTypes.Critical.InflictingIncreased: + criticalChance += classType switch + { + ClassType.Adventurer => firstData, + ClassType.Swordman => firstData, + ClassType.Archer => firstData, + ClassType.Wrestler => firstData, + _ => 0 + }; + + secondCriticalChance += firstData; + break; + case AdditionalTypes.Critical.InflictingReduced: + criticalChance -= classType switch + { + ClassType.Adventurer => firstData, + ClassType.Swordman => firstData, + ClassType.Archer => firstData, + ClassType.Wrestler => firstData, + _ => 0 + }; + + secondCriticalChance -= firstData; + break; + case AdditionalTypes.Critical.DamageIncreased: + criticalDamage += classType switch + { + ClassType.Adventurer => firstData, + ClassType.Swordman => firstData, + ClassType.Archer => firstData, + ClassType.Wrestler => firstData, + _ => 0 + }; + + secondCriticalDamage += firstData; + break; + case AdditionalTypes.Critical.DamageIncreasedInflictingReduced: + criticalDamage -= classType switch + { + ClassType.Adventurer => firstData, + ClassType.Swordman => firstData, + ClassType.Archer => firstData, + ClassType.Wrestler => firstData, + _ => 0 + }; + + secondCriticalDamage -= firstData; + break; + } + } + + foreach (BCardDTO bCard in defenseBCards) + { + int firstData = bCard.FirstDataValue(playerLevel); + + switch ((AdditionalTypes.Defence)bCard.SubType) + { + case AdditionalTypes.Defence.AllIncreased: + meleeDefense += firstData; + rangeDefense += firstData; + magicDefense += firstData; + break; + case AdditionalTypes.Defence.AllDecreased: + meleeDefense -= firstData; + rangeDefense -= firstData; + magicDefense -= firstData; + break; + case AdditionalTypes.Defence.MeleeIncreased: + meleeDefense += firstData; + break; + case AdditionalTypes.Defence.MeleeDecreased: + meleeDefense -= firstData; + break; + case AdditionalTypes.Defence.RangedIncreased: + rangeDefense += firstData; + break; + case AdditionalTypes.Defence.RangedDecreased: + rangeDefense -= firstData; + break; + case AdditionalTypes.Defence.MagicalIncreased: + magicDefense += firstData; + break; + case AdditionalTypes.Defence.MagicalDecreased: + magicDefense -= firstData; + break; + } + } + + foreach (BCardDTO bCard in dodgeBCards) + { + int firstData = bCard.FirstDataValue(playerLevel); + + switch ((AdditionalTypes.DodgeAndDefencePercent)bCard.SubType) + { + case AdditionalTypes.DodgeAndDefencePercent.DodgeIncreased: + meleeDodge += firstData; + rangeDodge += firstData; + break; + case AdditionalTypes.DodgeAndDefencePercent.DodgeDecreased: + meleeDodge -= firstData; + rangeDodge -= firstData; + break; + case AdditionalTypes.DodgeAndDefencePercent.DodgingMeleeIncreased: + meleeDodge += firstData; + break; + case AdditionalTypes.DodgeAndDefencePercent.DodgingMeleeDecreased: + meleeDodge -= firstData; + break; + case AdditionalTypes.DodgeAndDefencePercent.DodgingRangedIncreased: + rangeDodge += firstData; + break; + case AdditionalTypes.DodgeAndDefencePercent.DodgingRangedDecreased: + rangeDodge -= firstData; + break; + } + } + + foreach (BCardDTO bCard in resistanceBCards) + { + int firstData = bCard.FirstDataValue(playerLevel); + + switch ((AdditionalTypes.ElementResistance)bCard.SubType) + { + case AdditionalTypes.ElementResistance.AllIncreased: + fireResistance += firstData; + waterResistance += firstData; + lightResistance += firstData; + shadowResistance += firstData; + break; + case AdditionalTypes.ElementResistance.AllDecreased: + fireResistance -= firstData; + waterResistance -= firstData; + lightResistance -= firstData; + shadowResistance -= firstData; + break; + case AdditionalTypes.ElementResistance.FireIncreased: + fireResistance += firstData; + break; + case AdditionalTypes.ElementResistance.FireDecreased: + fireResistance -= firstData; + break; + case AdditionalTypes.ElementResistance.WaterIncreased: + waterResistance += firstData; + break; + case AdditionalTypes.ElementResistance.WaterDecreased: + waterResistance -= firstData; + break; + case AdditionalTypes.ElementResistance.LightIncreased: + lightResistance += firstData; + break; + case AdditionalTypes.ElementResistance.LightDecreased: + lightResistance -= firstData; + break; + case AdditionalTypes.ElementResistance.DarkIncreased: + shadowResistance += firstData; + break; + case AdditionalTypes.ElementResistance.DarkDecreased: + shadowResistance -= firstData; + break; + } + } + + minDamage += _playerEntity.GetMaxWeaponShellValue(ShellEffectType.DamageImproved, true); + maxDamage += _playerEntity.GetMaxWeaponShellValue(ShellEffectType.DamageImproved, true); + secondMinDamage += _playerEntity.GetMaxWeaponShellValue(ShellEffectType.DamageImproved, false); + secondMaxDamage += _playerEntity.GetMaxWeaponShellValue(ShellEffectType.DamageImproved, false); + + criticalChance += _playerEntity.GetMaxWeaponShellValue(ShellEffectType.CriticalChance, true); + criticalDamage += _playerEntity.GetMaxWeaponShellValue(ShellEffectType.CriticalDamage, true); + secondCriticalChance += _playerEntity.GetMaxWeaponShellValue(ShellEffectType.CriticalChance, false); + secondCriticalDamage += _playerEntity.GetMaxWeaponShellValue(ShellEffectType.CriticalDamage, false); + + meleeDefense += _playerEntity.GetMaxArmorShellValue(ShellEffectType.CloseDefence); + rangeDefense += _playerEntity.GetMaxArmorShellValue(ShellEffectType.DistanceDefence); + magicDefense += _playerEntity.GetMaxArmorShellValue(ShellEffectType.MagicDefence); + + fireResistance += _playerEntity.GetMaxArmorShellValue(ShellEffectType.IncreasedFireResistance); + waterResistance += _playerEntity.GetMaxArmorShellValue(ShellEffectType.IncreasedWaterResistance); + lightResistance += _playerEntity.GetMaxArmorShellValue(ShellEffectType.IncreasedLightResistance); + shadowResistance += _playerEntity.GetMaxArmorShellValue(ShellEffectType.IncreasedDarkResistance); + + fireResistance += _playerEntity.GetMaxArmorShellValue(ShellEffectType.IncreasedAllResistance); + waterResistance += _playerEntity.GetMaxArmorShellValue(ShellEffectType.IncreasedAllResistance); + lightResistance += _playerEntity.GetMaxArmorShellValue(ShellEffectType.IncreasedAllResistance); + shadowResistance += _playerEntity.GetMaxArmorShellValue(ShellEffectType.IncreasedAllResistance); + + fireResistance += _playerEntity.Family?.UpgradeValues?.GetOrDefault(FamilyUpgradeType.FIRE_RESISTANCE) ?? 0; + waterResistance += _playerEntity.Family?.UpgradeValues?.GetOrDefault(FamilyUpgradeType.WATER_RESISTANCE) ?? 0; + lightResistance += _playerEntity.Family?.UpgradeValues?.GetOrDefault(FamilyUpgradeType.LIGHT_RESISTANCE) ?? 0; + shadowResistance += _playerEntity.Family?.UpgradeValues?.GetOrDefault(FamilyUpgradeType.DARK_RESISTANCE) ?? 0; + + _stats[Statistics.MIN_DAMAGE] = minDamage; + _stats[Statistics.MAX_DAMAGE] = maxDamage; + _stats[Statistics.HITRATE] = hitRate; + _stats[Statistics.CRITICAL_CHANCE] = criticalChance; + _stats[Statistics.CRITICAL_DAMAGE] = criticalDamage; + _stats[Statistics.SECOND_MIN_DAMAGE] = secondMinDamage; + _stats[Statistics.SECOND_MAX_DAMAGE] = secondMaxDamage; + _stats[Statistics.SECOND_HITRATE] = secondHitRate; + _stats[Statistics.SECOND_CRITICAL_CHANCE] = secondCriticalChance; + _stats[Statistics.SECOND_CRITICAL_DAMAGE] = secondCriticalDamage; + _stats[Statistics.MELEE_DEFENSE] = meleeDefense; + _stats[Statistics.RANGE_DEFENSE] = rangeDefense; + _stats[Statistics.MAGIC_DEFENSE] = magicDefense; + _stats[Statistics.MELEE_DODGE] = meleeDodge; + _stats[Statistics.RANGE_DODGE] = rangeDodge; + _stats[Statistics.FIRE_RESISTANCE] = fireResistance; + _stats[Statistics.WATER_RESISTANCE] = waterResistance; + _stats[Statistics.LIGHT_RESISTANCE] = lightResistance; + _stats[Statistics.SHADOW_RESISTANCE] = shadowResistance; + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/EntityStatistics/Statistics.cs b/srcs/WingsAPI.Game/EntityStatistics/Statistics.cs new file mode 100644 index 0000000..6db811b --- /dev/null +++ b/srcs/WingsAPI.Game/EntityStatistics/Statistics.cs @@ -0,0 +1,43 @@ +namespace WingsEmu.Game.EntityStatistics; + +public enum Statistics +{ + MIN_DAMAGE, + MAX_DAMAGE, + HITRATE, + CRITICAL_CHANCE, + CRITICAL_DAMAGE, + SECOND_MIN_DAMAGE, + SECOND_MAX_DAMAGE, + SECOND_HITRATE, + SECOND_CRITICAL_CHANCE, + SECOND_CRITICAL_DAMAGE, + MELEE_DEFENSE, + RANGE_DEFENSE, + MAGIC_DEFENSE, + MELEE_DODGE, + RANGE_DODGE, + FIRE_RESISTANCE, + WATER_RESISTANCE, + LIGHT_RESISTANCE, + SHADOW_RESISTANCE +} + +public enum PassiveType +{ + HP, + MP, + MELEE_ATTACK, + RANGED_ATTACK, + MAGIC_ATTACK, + REGEN_HP, + REGEN_MP, + PASSIVE_REGEN, + MELEE_DEFENCE, + RANGED_DEFENCE, + MAGIC_DEFENCE, + MELEE_HIRATE, + RANGED_HIRATE, + MELEE_DODGE, + RANGED_DODGE +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Exchange/Event/ExchangeCloseEvent.cs b/srcs/WingsAPI.Game/Exchange/Event/ExchangeCloseEvent.cs new file mode 100644 index 0000000..866a76b --- /dev/null +++ b/srcs/WingsAPI.Game/Exchange/Event/ExchangeCloseEvent.cs @@ -0,0 +1,9 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Exchange.Event; + +public class ExchangeCloseEvent : PlayerEvent +{ + public ExcCloseType Type { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Exchange/Event/ExchangeJoinEvent.cs b/srcs/WingsAPI.Game/Exchange/Event/ExchangeJoinEvent.cs new file mode 100644 index 0000000..60304ce --- /dev/null +++ b/srcs/WingsAPI.Game/Exchange/Event/ExchangeJoinEvent.cs @@ -0,0 +1,9 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Game.Exchange.Event; + +public class ExchangeJoinEvent : PlayerEvent +{ + public IClientSession Target { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Exchange/Event/ExchangeRegisterEvent.cs b/srcs/WingsAPI.Game/Exchange/Event/ExchangeRegisterEvent.cs new file mode 100644 index 0000000..82980b0 --- /dev/null +++ b/srcs/WingsAPI.Game/Exchange/Event/ExchangeRegisterEvent.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using WingsEmu.Game._packetHandling; +using WingsEmu.Game.Inventory; + +namespace WingsEmu.Game.Exchange.Event; + +public class ExchangeRegisterEvent : PlayerEvent +{ + public List<(InventoryItem, short)> InventoryItems { get; set; } + + public int Gold { get; set; } + + public long BankGold { get; set; } + + public string Packets { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Exchange/Event/ExchangeTransferItemsEvent.cs b/srcs/WingsAPI.Game/Exchange/Event/ExchangeTransferItemsEvent.cs new file mode 100644 index 0000000..22377cd --- /dev/null +++ b/srcs/WingsAPI.Game/Exchange/Event/ExchangeTransferItemsEvent.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using WingsEmu.DTOs.Items; +using WingsEmu.Game._packetHandling; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Game.Exchange.Event; + +public class ExchangeTransferItemsEvent : PlayerEvent +{ + public IClientSession Target { get; set; } + + public List<(InventoryItem, short)> SenderItems { get; set; } + + public List<(InventoryItem, short)> TargetItems { get; set; } + + public int SenderGold { get; set; } + + public long SenderBankGold { get; set; } + + public int TargetGold { get; set; } + + public long TargetBankGold { get; set; } +} + +public class ExchangeCompletedEvent : PlayerEvent +{ + public IClientSession Target { get; init; } + + public List<(ItemInstanceDTO, short)> SenderItems { get; init; } + + public List<(ItemInstanceDTO, short)> TargetItems { get; init; } + + public int SenderGold { get; init; } + + public long SenderBankGold { get; init; } + + public int TargetGold { get; init; } + + public long TargetBankGold { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Exchange/Event/TradeRequestedEvent.cs b/srcs/WingsAPI.Game/Exchange/Event/TradeRequestedEvent.cs new file mode 100644 index 0000000..eaa25a8 --- /dev/null +++ b/srcs/WingsAPI.Game/Exchange/Event/TradeRequestedEvent.cs @@ -0,0 +1,8 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Exchange.Event; + +public class TradeRequestedEvent : PlayerEvent +{ + public long TargetId { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Exchange/ExchangeComponent.cs b/srcs/WingsAPI.Game/Exchange/ExchangeComponent.cs new file mode 100644 index 0000000..abfb06e --- /dev/null +++ b/srcs/WingsAPI.Game/Exchange/ExchangeComponent.cs @@ -0,0 +1,29 @@ +namespace WingsEmu.Game.Exchange; + +public class ExchangeComponent : IExchangeComponent +{ + private PlayerExchange _exchange; + + public ExchangeComponent() => _exchange = null; + + public void SetExchange(PlayerExchange exchange) + { + if (_exchange != null) + { + return; + } + + _exchange = exchange; + } + + public void RemoveExchange() + { + _exchange = null; + } + + public PlayerExchange GetExchange() => _exchange; + + public bool IsInExchange() => _exchange != null; + + public long GetTargetId() => _exchange?.TargetId ?? 0; +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Exchange/IExchangeComponent.cs b/srcs/WingsAPI.Game/Exchange/IExchangeComponent.cs new file mode 100644 index 0000000..43e6182 --- /dev/null +++ b/srcs/WingsAPI.Game/Exchange/IExchangeComponent.cs @@ -0,0 +1,10 @@ +namespace WingsEmu.Game.Exchange; + +public interface IExchangeComponent +{ + public void SetExchange(PlayerExchange exchange); + public void RemoveExchange(); + public PlayerExchange GetExchange(); + public bool IsInExchange(); + public long GetTargetId(); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Exchange/PlayerExchange.cs b/srcs/WingsAPI.Game/Exchange/PlayerExchange.cs new file mode 100644 index 0000000..eaf94d7 --- /dev/null +++ b/srcs/WingsAPI.Game/Exchange/PlayerExchange.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using WingsEmu.Game.Inventory; + +namespace WingsEmu.Game.Exchange; + +public class PlayerExchange +{ + public PlayerExchange(long senderId, long targetId) + { + SenderId = senderId; + TargetId = targetId; + } + + public long SenderId { get; } + + public long TargetId { get; } + + public List<(InventoryItem, short)> Items { get; set; } + + public int Gold { get; set; } + + public long BankGold { get; set; } + + public bool RegisteredItems { get; set; } + + public bool AcceptedTrade { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Extensions/AlgorithmExtension.cs b/srcs/WingsAPI.Game/Extensions/AlgorithmExtension.cs new file mode 100644 index 0000000..3150025 --- /dev/null +++ b/srcs/WingsAPI.Game/Extensions/AlgorithmExtension.cs @@ -0,0 +1,852 @@ +using System.Collections.Generic; +using WingsEmu.DTOs.Items; +using WingsEmu.Game._enum; +using WingsEmu.Game.Algorithm; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Entities; +using WingsEmu.Game.EntityStatistics; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Items; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Mates; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Extensions; + +public static class AlgorithmExtension +{ + private static readonly HashSet EquipmentTypes = new() + { + EquipmentType.Sp, + EquipmentType.MainWeapon, + EquipmentType.SecondaryWeapon, + EquipmentType.Armor + }; + + private static readonly HashSet StatsTypes = new() + { + StatisticType.HITRATE_MAGIC, + StatisticType.FIRE, + StatisticType.WATER, + StatisticType.LIGHT, + StatisticType.DARK + }; + + public static byte GetSpeed(this IBattleEntity entity, int speed) + { + (int firstData, int secondData) increaseSpeedBCard = + entity.BCardComponent.GetAllBCardsInformation(BCardType.Move, (byte)AdditionalTypes.Move.MovementSpeedIncreased, entity.Level); + (int firstData, int secondData) decreaseSpeedBCard = + entity.BCardComponent.GetAllBCardsInformation(BCardType.Move, (byte)AdditionalTypes.Move.MovementSpeedDecreased, entity.Level); + + int increaseSpeed = increaseSpeedBCard.firstData; + increaseSpeed -= decreaseSpeedBCard.firstData; + + speed += increaseSpeed; + + switch (entity) + { + case IPlayerEntity character: + switch (character.IsOnVehicle) + { + case true: + return (byte)(character.VehicleSpeed + character.VehicleMapSpeed); + case false when character.Specialist != null && character.UseSp: + speed += character.Specialist.GameItem.Speed; + break; + } + + if (character.IsInvisible()) + { + speed += character.BCardComponent.GetAllBCardsInformation(BCardType.Move, (byte)AdditionalTypes.Move.InvisibleMovement, entity.Level).firstData; + speed -= character.BCardComponent.GetAllBCardsInformation(BCardType.Move, (byte)AdditionalTypes.Move.InvisibleMovementNegated, entity.Level).firstData; + } + + speed += IncreaseByMap(character); + break; + case IMateEntity mateEntity: + if (mateEntity.CanPickUp) + { + speed += 2; + } + + if (mateEntity.Specialist != null && mateEntity.IsUsingSp) + { + speed += mateEntity.Specialist.GameItem.Speed; + } + + if (mateEntity.IsTeamMember) + { + speed += 2; + } + + if (mateEntity.MateType == MateType.Partner && mateEntity.Skin != 0) + { + speed += 2; + } + + if (mateEntity.Owner.HasBuff(BuffVnums.GUARDIAN_BLESS)) + { + speed += 2; + } + + break; + } + + double multiplier = 1 + (entity.BCardComponent.GetAllBCardsInformation(BCardType.Move, (byte)AdditionalTypes.Move.MoveSpeedIncreasedPercentage, entity.Level).firstData * 0.01 + - entity.BCardComponent.GetAllBCardsInformation(BCardType.Move, (byte)AdditionalTypes.Move.MoveSpeedDecreasedPercentage, entity.Level).firstData * 0.01); + + speed = (int)(speed * multiplier); + + if (speed < 0) + { + speed = 0; + } + + return speed > 59 ? (byte)59 : (byte)speed; + } + + private static int IncreaseByMap(IPlayerEntity character) + { + int increaseSpeed = 0; + IMapInstance mapInstance = character.MapInstance; + if (mapInstance == null) + { + return increaseSpeed; + } + + if (mapInstance.MapInstanceType == MapInstanceType.RainbowBattle) + { + increaseSpeed += character.BCardComponent + .GetAllBCardsInformation(BCardType.IncreaseDamageVersus, (byte)AdditionalTypes.IncreaseDamageVersus.PvpDamageAndSpeedRainbowBattleIncrease, character.Level).secondData; + } + + return increaseSpeed; + } + + public static int GetMaxHp(this IBattleEntity battleEntity, int baseHp) + { + int hp = baseHp; + if (battleEntity is IPlayerEntity character) + { + hp += character.StatisticsComponent.Passives.GetValueOrDefault(PassiveType.HP); + if (character.UseSp) + { + double multiplier = 1; + int point = character.SpecialistComponent.GetSlHp(); + + if (point <= 50) + { + multiplier += point * 0.01; + } + else + { + multiplier += 0.5 + (point * 0.01 - 0.5) * 2; + } + + hp = (int)(hp * multiplier); + + hp += character.SpecialistComponent.Hp + character.SpecialistComponent.SpHP * 100; + } + } + + int increaseHp = 0; + double hpMultiplier = 1; + + bool bearSpirit = battleEntity.BCardComponent.HasBCard(BCardType.BearSpirit, (byte)AdditionalTypes.BearSpirit.IncreaseMaximumHP); + if (bearSpirit) + { + double bearMultiplier = + battleEntity.BCardComponent.GetAllBCardsInformation(BCardType.BearSpirit, (byte)AdditionalTypes.BearSpirit.IncreaseMaximumHP, battleEntity.Level).firstData * 0.01; + int bearIncrease = (int)(hp * bearMultiplier); + if (bearIncrease > 5000) + { + bearIncrease = 5000; + } + + hp += bearIncrease; + } + + + hpMultiplier += battleEntity.BCardComponent.GetAllBCardsInformation(BCardType.MaxHPMP, (byte)AdditionalTypes.MaxHPMP.IncreasesMaximumHP, battleEntity.Level).firstData * 0.01; + hpMultiplier -= battleEntity.BCardComponent.GetAllBCardsInformation(BCardType.MaxHPMP, (byte)AdditionalTypes.MaxHPMP.DecreasesMaximumHP, battleEntity.Level).firstData * 0.01; + + increaseHp += battleEntity.BCardComponent.GetAllBCardsInformation(BCardType.MaxHPMP, (byte)AdditionalTypes.MaxHPMP.MaximumHPIncreased, battleEntity.Level).firstData; + increaseHp += battleEntity.BCardComponent.GetAllBCardsInformation(BCardType.MaxHPMP, (byte)AdditionalTypes.MaxHPMP.MaximumHPMPIncreased, battleEntity.Level).firstData; + + increaseHp -= battleEntity.BCardComponent.GetAllBCardsInformation(BCardType.MaxHPMP, (byte)AdditionalTypes.MaxHPMP.MaximumHPDecreased, battleEntity.Level).firstData; + increaseHp -= battleEntity.BCardComponent.GetAllBCardsInformation(BCardType.MaxHPMP, (byte)AdditionalTypes.MaxHPMP.MaximumHPMPDecreased, battleEntity.Level).firstData; + + int increaseFinalHp = (int)(hp * hpMultiplier); + hp = increaseFinalHp + increaseHp; + + if (battleEntity is IPlayerEntity playerEntity) + { + hp += playerEntity.GetJewelsCellonsValue(CellonType.Hp); + } + + return hp; + } + + public static int GetMaxMp(this IBattleEntity battleEntity, int baseMp) + { + int mp = baseMp; + + if (battleEntity is IPlayerEntity character) + { + mp += character.StatisticsComponent.Passives.GetValueOrDefault(PassiveType.MP); + + if (character.UseSp) + { + double multiplier = 1; + int point = character.SpecialistComponent.GetSlHp(); + + if (point <= 50) + { + multiplier += point * 0.01; + } + else + { + multiplier += 0.5 + (point * 0.01 - 0.5) * 2; + } + + mp = (int)(mp * multiplier); + + mp += character.SpecialistComponent.Mp + character.SpecialistComponent.SpHP * 100; + } + } + + int increaseMp = 0; + double mpMultiplier = 1; + + bool bearSpirit = battleEntity.BCardComponent.HasBCard(BCardType.BearSpirit, (byte)AdditionalTypes.BearSpirit.IncreaseMaximumMP); + double bearMultiplier = battleEntity.BCardComponent.GetAllBCardsInformation(BCardType.BearSpirit, (byte)AdditionalTypes.BearSpirit.IncreaseMaximumMP, battleEntity.Level).Item1 * 0.01; + if (bearSpirit) + { + int bearIncrease = (int)(mp * bearMultiplier); + if (bearIncrease > 5000) + { + bearIncrease = 5000; + } + + mp += bearIncrease; + } + + mpMultiplier += battleEntity.BCardComponent.GetAllBCardsInformation(BCardType.MaxHPMP, (byte)AdditionalTypes.MaxHPMP.IncreasesMaximumMP, battleEntity.Level).firstData * 0.01; + mpMultiplier -= battleEntity.BCardComponent.GetAllBCardsInformation(BCardType.MaxHPMP, (byte)AdditionalTypes.MaxHPMP.DecreasesMaximumMP, battleEntity.Level).firstData * 0.01; + + increaseMp += battleEntity.BCardComponent.GetAllBCardsInformation(BCardType.MaxHPMP, (byte)AdditionalTypes.MaxHPMP.MaximumMPIncreased, battleEntity.Level).firstData; + increaseMp += battleEntity.BCardComponent.GetAllBCardsInformation(BCardType.MaxHPMP, (byte)AdditionalTypes.MaxHPMP.MaximumHPMPIncreased, battleEntity.Level).firstData; + + increaseMp -= battleEntity.BCardComponent.GetAllBCardsInformation(BCardType.MaxHPMP, (byte)AdditionalTypes.MaxHPMP.MaximumMPDecreased, battleEntity.Level).firstData; + increaseMp -= battleEntity.BCardComponent.GetAllBCardsInformation(BCardType.MaxHPMP, (byte)AdditionalTypes.MaxHPMP.MaximumHPMPDecreased, battleEntity.Level).firstData; + + int increaseFinalMp = (int)(mp * mpMultiplier); + mp = increaseFinalMp + increaseMp; + + if (battleEntity is IPlayerEntity playerEntity) + { + mp += playerEntity.GetJewelsCellonsValue(CellonType.Mp); + } + + return mp; + } + + + public static void RefreshMaxHpMp(this IBattleEntity entity, IBattleEntityAlgorithmService algorithm) + { + switch (entity) + { + case IPlayerEntity character: + character.MaxHp = algorithm.GetBasicHpByClass(character.Class, character.Level); + character.MaxMp = algorithm.GetBasicMpByClass(character.Class, character.Level); + break; + case IMateEntity mateEntity: + mateEntity.MaxHp = algorithm.GetBasicHp((int)mateEntity.MonsterRaceType, mateEntity.Level, mateEntity.MeleeHpFactor, mateEntity.CleanHp, false); + mateEntity.MaxMp = algorithm.GetBasicMp((int)mateEntity.MonsterRaceType, mateEntity.Level, mateEntity.MagicMpFactor, mateEntity.CleanMp, false); + break; + } + } + + public static long GetLevelXp(this IPlayerEntity character, ICharacterAlgorithm characterAlgorithm) => characterAlgorithm.GetLevelXp(character.Level); + public static int GetJobXp(this IPlayerEntity character, ICharacterAlgorithm characterAlgorithm, bool isAdventurer = false) => characterAlgorithm.GetJobXp(character.JobLevel, isAdventurer); + public static int GetHeroXp(this IPlayerEntity character, ICharacterAlgorithm characterAlgorithm) => characterAlgorithm.GetHeroLevelXp(character.HeroLevel); + + public static int GetSpJobXp(this IPlayerEntity character, ICharacterAlgorithm characterAlgorithm, bool isFunSpecialist) => + characterAlgorithm.GetSpecialistJobXp(character.Specialist.SpLevel, isFunSpecialist); + + public static int GetResistance(this IPlayerEntity character, StatisticType type) + { + int resistance = 0; + + GameItemInstance specialistInstance = character.Specialist; + if (character.UseSp && specialistInstance != null) + { + resistance += type switch + { + StatisticType.FIRE => character.SpecialistComponent.FireResistance + specialistInstance.GameItem.FireResistance + character.SpecialistComponent.SpFire, + StatisticType.WATER => character.SpecialistComponent.WaterResistance + specialistInstance.GameItem.WaterResistance + character.SpecialistComponent.SpWater, + StatisticType.LIGHT => character.SpecialistComponent.LightResistance + specialistInstance.GameItem.LightResistance + character.SpecialistComponent.SpLight, + StatisticType.DARK => character.SpecialistComponent.DarkResistance + specialistInstance.GameItem.DarkResistance + character.SpecialistComponent.SpDark + }; + } + + resistance += character.FindMoreStats(type); + + return resistance; + } + + private static int FindMoreStats(this IPlayerEntity character, StatisticType type) + { + int stats = 0; + + IReadOnlyDictionary passives = character.StatisticsComponent.Passives; + + stats += type switch + { + StatisticType.ATTACK_MELEE => passives.GetValueOrDefault(PassiveType.MELEE_ATTACK), + StatisticType.ATTACK_RANGED => passives.GetValueOrDefault(PassiveType.RANGED_ATTACK), + StatisticType.ATTACK_MAGIC => passives.GetValueOrDefault(PassiveType.MAGIC_ATTACK), + StatisticType.HITRATE_MELEE => passives.GetValueOrDefault(PassiveType.MELEE_HIRATE), + StatisticType.HITRATE_RANGED => passives.GetValueOrDefault(PassiveType.RANGED_HIRATE), + StatisticType.DEFENSE_MELEE => passives.GetValueOrDefault(PassiveType.MELEE_DEFENCE), + StatisticType.DEFENSE_RANGED => passives.GetValueOrDefault(PassiveType.RANGED_DEFENCE), + StatisticType.DEFENSE_MAGIC => passives.GetValueOrDefault(PassiveType.MAGIC_DEFENCE), + StatisticType.DODGE_MELEE => passives.GetValueOrDefault(PassiveType.MELEE_DODGE), + StatisticType.DODGE_RANGED => passives.GetValueOrDefault(PassiveType.RANGED_DODGE), + _ => 0 + }; + + foreach (InventoryItem inventoryItem in character.EquippedItems) + { + GameItemInstance item = inventoryItem?.ItemInstance; + + if (item == null) + { + continue; + } + + if (EquipmentTypes.Contains(item.GameItem.EquipmentSlot) && !StatsTypes.Contains(type)) + { + continue; + } + + if (item.Type == ItemInstanceType.SpecialistInstance) + { + continue; + } + + stats += type switch + { + StatisticType.HITRATE_MELEE => item.HitRate + item.GameItem.HitRate, + StatisticType.HITRATE_RANGED => item.HitRate + item.GameItem.HitRate, + StatisticType.DEFENSE_MELEE => item.CloseDefence + item.GameItem.CloseDefence, + StatisticType.DEFENSE_RANGED => item.DistanceDefence + item.GameItem.DistanceDefence, + StatisticType.DEFENSE_MAGIC => item.MagicDefence + item.GameItem.MagicDefence, + StatisticType.DODGE_MELEE => item.DefenceDodge + item.GameItem.DefenceDodge, + StatisticType.DODGE_RANGED => item.DistanceDefenceDodge + item.GameItem.DistanceDefenceDodge, + StatisticType.FIRE => item.FireResistance + item.GameItem.FireResistance, + StatisticType.WATER => item.WaterResistance + item.GameItem.WaterResistance, + StatisticType.LIGHT => item.LightResistance + item.GameItem.LightResistance, + StatisticType.DARK => item.DarkResistance + item.GameItem.DarkResistance, + _ => 0 + }; + } + + return stats; + } + + public static int FindMoreStats(this IMateEntity mateEntity, StatisticType type) + { + int stats = 0; + + if (mateEntity.MateType == MateType.Pet) + { + return stats; + } + + foreach (PartnerInventoryItem inventoryItem in mateEntity.Owner.PartnerGetEquippedItems(mateEntity.PetSlot)) + { + GameItemInstance item = inventoryItem?.ItemInstance; + + if (item == null) + { + continue; + } + + if (item.GameItem.EquipmentSlot == EquipmentType.Sp && !mateEntity.IsUsingSp) + { + continue; + } + + stats += type switch + { + StatisticType.HITRATE_MELEE => item.HitRate + item.GameItem.HitRate, + StatisticType.HITRATE_RANGED => item.HitRate + item.GameItem.HitRate, + StatisticType.DEFENSE_MELEE => item.CloseDefence + item.GameItem.CloseDefence, + StatisticType.DEFENSE_RANGED => item.DistanceDefence + item.GameItem.DistanceDefence, + StatisticType.DEFENSE_MAGIC => item.MagicDefence + item.GameItem.MagicDefence, + StatisticType.DODGE_MELEE => item.DefenceDodge + item.GameItem.DefenceDodge, + StatisticType.DODGE_RANGED => item.DistanceDefenceDodge + item.GameItem.DistanceDefenceDodge, + StatisticType.FIRE => item.FireResistance + item.GameItem.FireResistance, + StatisticType.WATER => item.WaterResistance + item.GameItem.WaterResistance, + StatisticType.LIGHT => item.LightResistance + item.GameItem.LightResistance, + StatisticType.DARK => item.DarkResistance + item.GameItem.DarkResistance, + _ => 0 + }; + } + + return stats; + } + + public static int GetHitRate(this IPlayerEntity character, int basic, bool isMainWeapon, StatisticType type) + { + int hitRate = basic; + GameItemInstance mainWeapon = character.MainWeapon; + GameItemInstance secondaryWeapon = character.SecondaryWeapon; + + if (character.UseSp && type != StatisticType.HITRATE_MAGIC) + { + hitRate += character.SpecialistComponent.HitRate; + } + + hitRate += character.FindMoreStats(type); + + if (isMainWeapon) + { + if (mainWeapon == null) + { + return hitRate; + } + + return hitRate + mainWeapon.HitRate + mainWeapon.GameItem.HitRate; + } + + if (secondaryWeapon == null) + { + return hitRate; + } + + return hitRate + secondaryWeapon.HitRate + secondaryWeapon.GameItem.HitRate; + } + + public static int GetDodge(this IPlayerEntity character, int basic, StatisticType type) + { + int dodge = basic; + GameItemInstance armorInstance = character.Armor; + + if (character.UseSp) + { + dodge += type switch + { + StatisticType.DODGE_MELEE => character.SpecialistComponent.DefenceDodge, + StatisticType.DODGE_RANGED => character.SpecialistComponent.DistanceDefenceDodge + }; + } + + dodge += character.FindMoreStats(type); + + if (armorInstance == null) + { + return dodge; + } + + dodge += type switch + { + StatisticType.DODGE_MELEE => armorInstance.DefenceDodge + armorInstance.GameItem.DefenceDodge, + StatisticType.DODGE_RANGED => armorInstance.DistanceDefenceDodge + armorInstance.GameItem.DistanceDefenceDodge + }; + + return dodge; + } + + public static int GetCriticalChance(this IPlayerEntity character, int basic, bool isMainWeapon) + { + int criticalChance = basic; + GameItemInstance mainWeapon = character.MainWeapon; + GameItemInstance secondaryWeapon = character.SecondaryWeapon; + + if (character.UseSp) + { + criticalChance += character.SpecialistComponent.CriticalLuckRate; + } + + if (isMainWeapon) + { + if (mainWeapon == null) + { + return criticalChance; + } + + return criticalChance + mainWeapon.GameItem.CriticalLuckRate; + } + + if (secondaryWeapon == null) + { + return criticalChance; + } + + return criticalChance + secondaryWeapon.GameItem.CriticalLuckRate; + } + + public static int GetCriticalDamage(this IPlayerEntity character, int basic, bool isMainWeapon) + { + int criticalDamage = basic; + GameItemInstance mainWeapon = character.MainWeapon; + GameItemInstance secondaryWeapon = character.SecondaryWeapon; + + if (character.UseSp) + { + criticalDamage += character.SpecialistComponent.CriticalRate; + } + + if (isMainWeapon) + { + if (mainWeapon == null) + { + return criticalDamage; + } + + return criticalDamage + mainWeapon.GameItem.CriticalRate; + } + + if (secondaryWeapon == null) + { + return criticalDamage; + } + + return criticalDamage + secondaryWeapon.GameItem.CriticalRate; + } + + public static int GetElement(this IPlayerEntity character, bool isSpecialistRate) + { + int element = 0; + GameItemInstance specialist = character.Specialist; + if (!isSpecialistRate) + { + bool isBuffed = character.HasBuff(BuffVnums.FAIRY_BOOSTER); + + GameItemInstance fairy = character.Fairy; + if (fairy == null) + { + return element; + } + + element += character.BCardComponent.GetAllBCardsInformation(BCardType.IncreaseElementFairy, (byte)AdditionalTypes.IncreaseElementFairy.FairyElementIncrease, character.Level).firstData; + element -= character.BCardComponent.GetAllBCardsInformation(BCardType.IncreaseElementFairy, (byte)AdditionalTypes.IncreaseElementFairy.FairyElementDecrease, character.Level).firstData; + + int finalElement = element + fairy.ElementRate + fairy.GameItem.ElementRate + (isBuffed ? 30 : 0); + + return finalElement < 0 ? 0 : finalElement; + } + + if (!character.UseSp || specialist == null) + { + return element; + } + + int point = character.SpecialistComponent.GetSlElement(); + int p; + + if (point <= 50) + { + p = point; + } + else + { + p = 50 + (point - 50) * 2; + } + + return p + character.SpecialistComponent.SpElement; + } + + public static int GetDefence(this IPlayerEntity character, int basic, StatisticType statisticType) + { + int armor = basic; + GameItemInstance armorInstance = character.Armor; + GameItemInstance specialist = character.Specialist; + + if (character.UseSp) + { + int point = character.SpecialistComponent.GetSlDefense(); + + int p = point switch + { + <= 10 => point, + <= 20 => 10 + (point - 10) * 2, + <= 30 => 30 + (point - 20) * 3, + <= 40 => 60 + (point - 30) * 4, + <= 50 => 100 + (point - 40) * 5, + <= 60 => 150 + (point - 50) * 6, + <= 70 => 210 + (point - 60) * 7, + <= 80 => 280 + (point - 70) * 8, + <= 90 => 360 + (point - 80) * 9, + <= 100 => 450 + (point - 90) * 10, + _ => 0 + }; + + armor += character.SpecialistComponent.SpDefence * 10; + switch (statisticType) + { + case StatisticType.DEFENSE_MELEE: + armor += character.SpecialistComponent.CloseDefence; + break; + case StatisticType.DEFENSE_RANGED: + armor += character.SpecialistComponent.DistanceDefence; + break; + case StatisticType.DEFENSE_MAGIC: + armor += character.SpecialistComponent.MagicDefence; + break; + } + + armor += p; + } + + armor += character.FindMoreStats(statisticType); + + if (armorInstance == null) + { + return armor; + } + + switch (statisticType) + { + case StatisticType.DEFENSE_MELEE: + return armor + armorInstance.CloseDefence + armorInstance.GameItem.CloseDefence; + case StatisticType.DEFENSE_RANGED: + return armor + armorInstance.DistanceDefence + armorInstance.GameItem.DistanceDefence; + case StatisticType.DEFENSE_MAGIC: + return armor + armorInstance.MagicDefence + armorInstance.GameItem.MagicDefence; + } + + return armor; + } + + public static int GetDamage(this IPlayerEntity character, int basic, bool isMin, StatisticType type, bool isMainWeapon = true) + { + int damage = basic; + GameItemInstance mainWeapon = character.MainWeapon; + GameItemInstance secondaryWeapon = character.SecondaryWeapon; + + if (character.UseSp) + { + int point = character.SpecialistComponent.GetSlHit(); + + int p = point switch + { + <= 10 => point * 5, + <= 20 => 50 + (point - 10) * 6, + <= 30 => 110 + (point - 20) * 7, + <= 40 => 180 + (point - 30) * 8, + <= 50 => 260 + (point - 40) * 9, + <= 60 => 350 + (point - 50) * 10, + <= 70 => 450 + (point - 60) * 11, + <= 80 => 560 + (point - 70) * 13, + <= 90 => 690 + (point - 80) * 14, + <= 94 => 830 + (point - 90) * 15, + <= 95 => 890 + 16, + <= 97 => 906 + (point - 95) * 17, + <= 100 => 940 + (point - 97) * 20, + _ => 0 + }; + + damage += p; + damage += character.SpecialistComponent.SpDamage * 10; + if (isMin) + { + damage += character.SpecialistComponent.DamageMinimum; + } + else + { + damage += character.SpecialistComponent.DamageMaximum; + } + } + + damage += character.FindMoreStats(type); + + if (isMainWeapon) + { + if (mainWeapon == null) + { + return damage; + } + + if (isMin) + { + return damage + mainWeapon.DamageMinimum + mainWeapon.GameItem.DamageMinimum; + } + + return damage + mainWeapon.DamageMaximum + mainWeapon.GameItem.DamageMaximum; + } + + if (secondaryWeapon == null) + { + return damage; + } + + if (isMin) + { + return damage + secondaryWeapon.DamageMinimum + secondaryWeapon.GameItem.DamageMinimum; + } + + return damage + secondaryWeapon.DamageMaximum + secondaryWeapon.GameItem.DamageMaximum; + } + + public static int SlPoint(this GameItemInstance specialistInstance, short spPoint, SpecialistPointsType type) + { + try + { + int point = 0; + switch (type) + { + case SpecialistPointsType.ATTACK: + point = spPoint switch + { + <= 10 => spPoint, + <= 28 => 10 + (spPoint - 10) / 2, + <= 88 => 19 + (spPoint - 28) / 3, + <= 168 => 39 + (spPoint - 88) / 4, + <= 268 => 59 + (spPoint - 168) / 5, + <= 334 => 79 + (spPoint - 268) / 6, + <= 383 => 90 + (spPoint - 334) / 7, + <= 391 => 97 + (spPoint - 383) / 8, + <= 400 => 98 + (spPoint - 391) / 9, + <= 410 => 99 + (spPoint - 400) / 10, + _ => point + }; + + break; + + case SpecialistPointsType.DEFENCE: + point = spPoint switch + { + <= 10 => spPoint, + <= 48 => 10 + (spPoint - 10) / 2, + <= 81 => 29 + (spPoint - 48) / 3, + <= 161 => 40 + (spPoint - 81) / 4, + <= 236 => 60 + (spPoint - 161) / 5, + <= 290 => 75 + (spPoint - 236) / 6, + <= 360 => 84 + (spPoint - 290) / 7, + <= 400 => 97 + (spPoint - 360) / 8, + <= 410 => 99 + (spPoint - 400) / 10, + _ => point + }; + + break; + + case SpecialistPointsType.ELEMENT: + point = spPoint switch + { + <= 20 => spPoint, + <= 40 => 20 + (spPoint - 20) / 2, + <= 70 => 30 + (spPoint - 40) / 3, + <= 110 => 40 + (spPoint - 70) / 4, + <= 210 => 50 + (spPoint - 110) / 5, + <= 270 => 70 + (spPoint - 210) / 6, + <= 410 => 80 + (spPoint - 270) / 7, + _ => point + }; + + break; + + case SpecialistPointsType.HPMP: + point = spPoint switch + { + <= 10 => spPoint, + <= 50 => 10 + (spPoint - 10) / 2, + <= 110 => 30 + (spPoint - 50) / 3, + <= 150 => 50 + (spPoint - 110) / 4, + <= 200 => 60 + (spPoint - 150) / 5, + <= 260 => 70 + (spPoint - 200) / 6, + <= 330 => 80 + (spPoint - 260) / 7, + <= 410 => 90 + (spPoint - 330) / 8, + _ => point + }; + + break; + } + + return point; + } + catch + { + return 0; + } + } + + public static int SpPointsBasic(this GameItemInstance specialistInstance) + { + short spLevel = specialistInstance.SpLevel; + int point = (spLevel - 20) * 3; + if (spLevel <= 20) + { + point = 0; + } + + switch (specialistInstance.Upgrade) + { + case 1: + point += 5; + break; + + case 2: + point += 10; + break; + + case 3: + point += 15; + break; + + case 4: + point += 20; + break; + + case 5: + point += 28; + break; + + case 6: + point += 36; + break; + + case 7: + point += 46; + break; + + case 8: + point += 56; + break; + + case 9: + point += 68; + break; + + case 10: + point += 80; + break; + + case 11: + point += 95; + break; + + case 12: + point += 110; + break; + + case 13: + point += 128; + break; + + case 14: + point += 148; + break; + + case 15: + point += 173; + break; + } + + return point; + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Extensions/BattleEntityExtension.cs b/srcs/WingsAPI.Game/Extensions/BattleEntityExtension.cs new file mode 100644 index 0000000..2d39298 --- /dev/null +++ b/srcs/WingsAPI.Game/Extensions/BattleEntityExtension.cs @@ -0,0 +1,1513 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using PhoenixLib.Logging; +using WingsAPI.Packets.Enums.Shells; +using WingsEmu.DTOs.BCards; +using WingsEmu.DTOs.Maps; +using WingsEmu.DTOs.Skills; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Networking.Broadcasting; +using WingsEmu.Game.RainbowBattle; +using WingsEmu.Game.Skills; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Battle; +using WingsEmu.Packets.Enums.Character; +using WingsEmu.Packets.Enums.Chat; +using WingsEmu.Packets.ServerPackets.Battle; + +namespace WingsEmu.Game.Extensions; + +public static class BattleEntityExtension +{ + /// + /// Check if the BattleEntity is in the given Range. + /// + /// The BattleEntity from which position check + /// The X coordinate on the Map of the object to check. + /// The Y coordinate on the Map of the object to check. + /// The maximum distance of the object to check. + /// True if the BattleEntity is in range, False if not. + public static bool IsInRange(this IBattleEntity battleEntity, short mapX, short mapY, byte distance) => battleEntity.Position.GetDistance(mapX, mapY) <= distance; + + public static bool IsInvisible(this IBattleEntity seen) + { + if (seen is IPlayerEntity player && player.CheatComponent.IsInvisible) + { + return true; + } + + return seen.BCardComponent.HasBCard(BCardType.SpecialActions, (byte)AdditionalTypes.SpecialActions.Hide) + || seen.BCardComponent.HasBCard(BCardType.FalconSkill, (byte)AdditionalTypes.FalconSkill.Hide) + || seen.BCardComponent.HasBCard(BCardType.FalconSkill, (byte)AdditionalTypes.FalconSkill.Ambush); + } + + public static bool HasGodMode(this IBattleEntity entity) + { + if (entity is IPlayerEntity player && player.CheatComponent.HasGodMode) + { + return true; + } + + if (entity.BCardComponent.HasBCard(BCardType.TimeCircleSkills, (byte)AdditionalTypes.TimeCircleSkills.DisableHPConsumption)) + { + return true; + } + + if (entity.BCardComponent.HasBCard(BCardType.NoDefeatAndNoDamage, (byte)AdditionalTypes.NoDefeatAndNoDamage.NeverReceiveDamage)) + { + return true; + } + + return entity.BCardComponent.HasBCard(BCardType.HideBarrelSkill, (byte)AdditionalTypes.HideBarrelSkill.NoHPConsumption); + } + + public static int ApplyCooldownReduction(this IBattleEntity battleEntity, SkillInfo skill) + { + short cooldown = skill.Cooldown; + if (battleEntity.NoCooldown()) + { + return 0; + } + + if (ResetByFairyWings(battleEntity, skill)) + { + return 0; + } + + (int firstData, int secondData) cooldownDecrease = + battleEntity.BCardComponent.GetAllBCardsInformation(BCardType.Morale, (byte)AdditionalTypes.Morale.SkillCooldownDecreased, battleEntity.Level); + (int firstData, int secondData) cooldownIncrease = + battleEntity.BCardComponent.GetAllBCardsInformation(BCardType.Morale, (byte)AdditionalTypes.Morale.SkillCooldownIncreased, battleEntity.Level); + double increaseMultiplier = cooldownIncrease.firstData * 0.01; + double decreaseMultiplier = cooldownDecrease.firstData * 0.01; + double newCooldown = cooldown * increaseMultiplier * decreaseMultiplier; + cooldown = (short)newCooldown; + + return cooldown < 0 ? 0 : cooldown; + } + + private static bool ResetByFairyWings(IBattleEntity battleEntity, SkillInfo skill) + { + if (!battleEntity.BCardComponent.HasBCard(BCardType.EffectSummon, (byte)AdditionalTypes.EffectSummon.CooldownResetChance)) + { + return false; + } + + if (skill.TargetAffectedEntities != TargetAffectedEntities.Enemies) + { + return false; + } + + if (skill.CastId == 0) + { + return false; + } + + if (battleEntity is not IPlayerEntity playerEntity) + { + return false; + } + + int firstData = playerEntity.BCardComponent.GetAllBCardsInformation(BCardType.EffectSummon, (byte)AdditionalTypes.EffectSummon.CooldownResetChance, playerEntity.Level).firstData; + if (StaticRandomGenerator.Instance.RandomNumber() > firstData) + { + return false; + } + + if (!playerEntity.SkillComponent.SkillsResets.TryGetValue(skill.Vnum, out byte usages)) + { + usages = 0; + } + + if (playerEntity.SkillComponent.LastSkillVnum.HasValue && playerEntity.SkillComponent.LastSkillVnum.Value == skill.Vnum) + { + usages += 1; + } + else + { + playerEntity.SkillComponent.SkillsResets.Clear(); + } + + if (usages > 1) + { + return false; + } + + playerEntity.SkillComponent.LastSkillVnum = skill.Vnum; + playerEntity.SkillComponent.SkillsResets[skill.Vnum] = usages; + playerEntity.BroadcastEffectInRange(EffectType.FairyResetBuff); + + return true; + } + + private static bool NoCooldown(this IBattleEntity entity) => entity is IPlayerEntity character && character.CheatComponent.HasNoCooldown; + + public static IEnumerable GetEnemiesInRange(this IBattleEntity sender, IBattleEntity caster, short range) => sender.Position.GetEnemiesInRange(caster, range); + + public static IEnumerable GetAlliesInRange(this IBattleEntity sender, IBattleEntity caster, short range) => sender.Position.GetAlliesInRange(caster, range); + + public static List GetCellsInLine(this Position pos1, Position pos2) + { + var cells = new List(); + + short x0 = pos1.X; + short y0 = pos1.Y; + + short x1 = pos2.X; + short y1 = pos2.Y; + + int dx = Math.Abs(x1 - x0); + int sx = x0 < x1 ? 1 : -1; + + int dy = -Math.Abs(y1 - y0); + int sy = y0 < y1 ? 1 : -1; + + int err = dx + dy; + + while (true) + { + cells.Add(new Position(x0, y0)); + if (x0 == x1 && y0 == y1) + { + break; + } + + int e2 = 2 * err; + if (e2 >= dy) + { + err += dy; + x0 += (short)sx; + } + + if (e2 > dx) + { + continue; + } + + err += dx; + y0 += (short)sy; + } + + return cells; + } + + public static void ChangeSize(this IBattleEntity battleEntity, byte size) + { + battleEntity.Size = size; + battleEntity.MapInstance.Broadcast(battleEntity.GenerateScal()); + } + + public static int GetHpPercentage(this IBattleEntity target) + { + if (target.Hp == 0 || target.MaxHp == 0) + { + return 0; + } + + int hpPercentage = (int)(target.Hp / (float)target.MaxHp * 100); + return hpPercentage == 0 ? 1 : hpPercentage; + } + + public static bool ShouldSendScal(this IBattleEntity battleEntity) => battleEntity.Size != 10; + + public static string GenerateScal(this IBattleEntity battleEntity) => $"char_sc {((byte)battleEntity.Type).ToString()} {battleEntity.Id.ToString()} {battleEntity.Size.ToString()}"; + + public static string GenerateEffectPacket(this IBattleEntity battleEntity, int effectVnum) => $"eff {(byte)battleEntity.Type} {battleEntity.Id} {effectVnum}"; + + public static string GenerateEffectPacket(this IBattleEntity battleEntity, EffectType effectType) => battleEntity.GenerateEffectPacket((int)effectType); + + public static void BroadcastEffectInRange(this IBattleEntity entity, EffectType effectType) + { + if (entity.IsInvisibleGm()) + { + return; + } + + entity.MapInstance.Broadcast(entity.GenerateEffectPacket((int)effectType), new RangeBroadcast(entity.PositionX, entity.PositionY)); + } + + public static void BroadcastEffectInRange(this IBattleEntity entity, int effectId) + { + if (entity.IsInvisibleGm()) + { + return; + } + + entity.MapInstance.Broadcast(entity.GenerateEffectPacket(effectId), new RangeBroadcast(entity.PositionX, entity.PositionY)); + } + + public static string GenerateMvPacket(this IBattleEntity entity, int speed = default) + => $"mv {(byte)entity.Type} {entity.Id} {entity.PositionX} {entity.PositionY} {(speed == default ? entity.Speed : speed).ToString()}"; + + public static string GenerateStPacket(this IBattleEntity target) + { + return + $"st {(byte)target.Type} {target.Id} {target.Level} {(target is IPlayerEntity character ? character.HeroLevel : 0)} {target.HpPercentage} {target.MpPercentage} {target.Hp} {target.Mp}{target.BuffComponent.GetAllBuffs().Aggregate(string.Empty, (current, buff) => current + $" {buff.CardId}.{buff.CasterLevel}")}"; + } + + public static string GenerateTeleportPacket(this IBattleEntity entity, short x, short y) => $"tp {(byte)entity.Type} {entity.Id} {x} {y} 0"; + + public static void TeleportOnMap(this IBattleEntity entity, short x, short y, bool teleportMates = false) + { + var newPosition = new Position(x, y); + switch (entity) + { + case IPlayerEntity playerEntity: + + if (!teleportMates) + { + entity.ChangePosition(newPosition); + entity.MapInstance.Broadcast(entity.GenerateTeleportPacket(x, y)); + return; + } + + IReadOnlyList nosMate = playerEntity.MateComponent.TeamMembers(); + foreach (IMateEntity mate in nosMate) + { + mate.ChangePosition(newPosition); + playerEntity.MapInstance.Broadcast(mate.GenerateTeleportPacket(x, y)); + } + + break; + } + + entity.ChangePosition(newPosition); + entity.MapInstance.Broadcast(entity.GenerateTeleportPacket(x, y)); + } + + public static int GetMpPercentage(this IBattleEntity target) + { + if (target.Mp == 0 || target.MaxMp == 0) + { + return 0; + } + + int mpPercentage = (int)(target.Mp / (float)target.MaxMp * 100); + return mpPercentage == 0 ? 1 : mpPercentage; + } + + public static bool IsPvpActivated(this IBattleEntity entity, IBattleEntity target) + { + if (!target.IsPlayer() && !target.IsMate()) + { + return false; + } + + if (entity.Id == target.Id && entity.Type == target.Type) + { + return false; + } + + if (entity.BuffComponent.HasBuff((short)BuffVnums.PVP) && !entity.MapInstance.IsPvp) + { + // BIGU SWITCHU 2021 KEKW + + // 1 -> when target is Player (A) and A have PvP buff and entity isn't in the same group with A + // 2 -> when target is Mate (B) and owner (C) have PvP buff and entity isn't in the same group with C + // 3 -> when caster is Mate and target is Player (D) and Mate Owner (E) have PvP buff and D have PvP buff and E isn't in the same group with D + // 4 => when caster is Mate (F - Owner) and target is Mate (G - Owner) and F and G have PvP buff and F isn't in the same group with G + + return entity switch + { + IPlayerEntity character when target is IPlayerEntity characterTarget => + characterTarget.BuffComponent.HasBuff((short)BuffVnums.PVP) && character.IsInGroupOf(characterTarget) == false, + + IPlayerEntity character when target is IMateEntity mateTarget => + mateTarget.Owner.Id != character.Id && mateTarget.Owner.BuffComponent.HasBuff((short)BuffVnums.PVP) && character.IsInGroupOf(mateTarget.Owner) == false, + + IMateEntity mate when target is IPlayerEntity character => + mate.Owner.Id != character.Id && character.BuffComponent.HasBuff((short)BuffVnums.PVP) && mate.Owner.BuffComponent.HasBuff((short)BuffVnums.PVP) + && mate.Owner.IsInGroupOf(character) == false, + + IMateEntity mate when target is IMateEntity mateTarget => + mate.Owner.Id != mateTarget.Owner.Id && mate.Owner.BuffComponent.HasBuff((short)BuffVnums.PVP) && mateTarget.Owner.BuffComponent.HasBuff((short)BuffVnums.PVP) + && mate.Owner.IsInGroupOf(mateTarget.Owner) == false, + + _ => false + }; + } + + if (entity.MapInstance.MapInstanceType == MapInstanceType.RainbowBattle) + { + switch (entity) + { + case IPlayerEntity character when target is IPlayerEntity characterTarget: + + RainbowBattleParty rainbowParty = character.RainbowBattleComponent.RainbowBattleParty; + + return rainbowParty is { Started: true, FinishTime: null } + && !characterTarget.RainbowBattleComponent.IsFrozen && !character.RainbowBattleComponent.IsFrozen + && characterTarget.RainbowBattleComponent.Team != character.RainbowBattleComponent.Team; + case IPlayerEntity character when target is IMateEntity mateTarget: + + RainbowBattleParty rainbowPartyTwo = character.RainbowBattleComponent.RainbowBattleParty; + + return rainbowPartyTwo is { Started: true, FinishTime: null } + && mateTarget.Owner.Id != character.Id && !mateTarget.Owner.RainbowBattleComponent.IsFrozen + && !character.RainbowBattleComponent.IsFrozen && mateTarget.Owner.RainbowBattleComponent.Team != character.RainbowBattleComponent.Team; + case IMateEntity mate when target is IPlayerEntity character: + + RainbowBattleParty rainbowPartyThree = mate.Owner.RainbowBattleComponent.RainbowBattleParty; + + return rainbowPartyThree is { Started: true, FinishTime: null } + && !mate.Owner.RainbowBattleComponent.IsFrozen && !character.RainbowBattleComponent.IsFrozen + && mate.Owner.Id != character.Id && mate.Owner.RainbowBattleComponent.Team != character.RainbowBattleComponent.Team; + case IMateEntity mate when target is IMateEntity mateTarget: + + RainbowBattleParty rainbowPartyFour = mate.Owner.RainbowBattleComponent.RainbowBattleParty; + + return rainbowPartyFour is { Started: true, FinishTime: null } + && !mate.Owner.RainbowBattleComponent.IsFrozen && !mateTarget.Owner.RainbowBattleComponent.IsFrozen + && mate.Owner.Id != mateTarget.Owner.Id && mate.Owner.RainbowBattleComponent.Team != mateTarget.Owner.RainbowBattleComponent.Team; + default: + return false; + } + } + + switch (entity.MapInstance.IsPvp) + { + case false: + case true when !target.IsInPvpZone(): + return false; + } + + if (entity.MapInstance.HasMapFlag(MapFlags.ACT_4)) + { + return entity.Faction != target.Faction; + } + + return entity switch + { + IPlayerEntity character when target is IPlayerEntity characterTarget + => !character.ArenaImmunity.HasValue && !characterTarget.ArenaImmunity.HasValue && character.IsInGroupOf(characterTarget) == false, + + IPlayerEntity character when target is IMateEntity mateTarget + => mateTarget.Owner.IsInPvpZone() && !character.ArenaImmunity.HasValue && !mateTarget.Owner.ArenaImmunity.HasValue && mateTarget.Owner.Id != character.Id && + character.IsInGroupOf(mateTarget.Owner) == false, + + IMateEntity mate when target is IPlayerEntity character + => mate.Owner.IsInPvpZone() && !character.ArenaImmunity.HasValue && !mate.Owner.ArenaImmunity.HasValue && mate.Owner.Id != character.Id && mate.Owner.IsInGroupOf(character) == false, + + IMateEntity mate when target is IMateEntity mateTarget + => mateTarget.Owner.IsInPvpZone() && mate.Owner.IsInPvpZone() && !mateTarget.Owner.ArenaImmunity.HasValue && !mate.Owner.ArenaImmunity.HasValue && + mate.Owner.Id != mateTarget.Owner.Id && mate.Owner.IsInGroupOf(mateTarget.Owner) == false, + _ => false + }; + } + + public static bool IsEnemyWith(this IBattleEntity entity, IBattleEntity target) + { + if (entity.IsSameEntity(target)) + { + return false; + } + + switch (target) + { + case IPlayerEntity playerEntity: + if (playerEntity.IsSeal) + { + return false; + } + + break; + case IMateEntity mateEntity: + if (mateEntity.Owner.IsOnVehicle) + { + return false; + } + + break; + } + + switch (entity) + { + case IPlayerEntity: + if (target is not IMonsterEntity monsterEntitySummoner) + { + return target.IsMonster() || entity.IsPvpActivated(target); + } + + if (entity.Id == monsterEntitySummoner.SummonerId && entity.Type == monsterEntitySummoner.SummonerType && !monsterEntitySummoner.IsMateTrainer) + { + return false; + } + + if (monsterEntitySummoner.SummonerType is VisualType.Player && !monsterEntitySummoner.IsMateTrainer) + { + if (!monsterEntitySummoner.MapInstance.IsPvp || !monsterEntitySummoner.SummonerId.HasValue) + { + return false; + } + + IPlayerEntity player = monsterEntitySummoner.MapInstance.GetCharacterById(monsterEntitySummoner.SummonerId.Value); + return player is null || player.IsPvpActivated(entity); + } + + if (monsterEntitySummoner.Faction == entity.Faction && monsterEntitySummoner.MapInstance.HasMapFlag(MapFlags.ACT_4)) + { + return false; + } + + return target.IsMonster() || entity.IsPvpActivated(target); + case IMateEntity mate: + if (target is not IMonsterEntity monsterSummoner) + { + return entity.IsPvpActivated(target); + } + + if (monsterSummoner.IsMateTrainer) + { + return true; + } + + int mateOwnerId = mate.Owner.Id; + if (monsterSummoner.SummonerType is not VisualType.Player) + { + return true; + } + + if (!monsterSummoner.SummonerId.HasValue) + { + return true; + } + + if (!monsterSummoner.MapInstance.IsPvp) + { + return monsterSummoner.SummonerId.Value != mateOwnerId; + } + + IPlayerEntity playerMate = monsterSummoner.MapInstance.GetCharacterById(monsterSummoner.SummonerId.Value); + return playerMate is null || playerMate.IsPvpActivated(mate.Owner); + } + + if (entity.IsNpc()) + { + return !(target.IsNpc() || target.IsPlayer() || target.IsMate()); + } + + if (entity is not IMonsterEntity mob) + { + return false; + } + + if (mob.MapInstance.HasMapFlag(MapFlags.ACT_4)) + { + if (target.Faction == mob.Faction) + { + return false; + } + } + + if (mob.IsMateTrainer || !mob.SummonerId.HasValue || !mob.SummonerType.HasValue || mob.MonsterVNum == (short)MonsterVnum.ONYX_MONSTER) + { + return target.IsPlayer() || target.IsNpc() || target.IsMate() || target.CheckIfIsSummonedMonster(mob); + } + + if (mob.SummonerId == target.Id && mob.SummonerType == target.Type) + { + return false; + } + + if (target is IMonsterEntity monsterEntity) + { + if (mob.SummonerId == monsterEntity.SummonerId) + { + return false; + } + + if (monsterEntity.IsMateTrainer) + { + return false; + } + + if (monsterEntity.SummonerType is null or VisualType.Monster && mob.SummonerType is null or VisualType.Monster) + { + return false; + } + } + + IBattleEntity summoner = mob.MapInstance?.GetBattleEntity(mob.SummonerType.Value, mob.SummonerId.Value); + if (summoner == null && mob.SummonerType.Value == VisualType.Player) + { + return false; + } + + return summoner switch + { + IPlayerEntity character => character.IsEnemyWith(target), + _ => target.IsPlayer() || target.IsMate() || target.IsMonster() || target.IsNpc() + }; + } + + public static bool CanMonsterBeAttacked(this IBattleEntity entity, IMonsterEntity monsterEntity) + { + if (monsterEntity.SummonerType is not VisualType.Player) + { + return true; + } + + if (monsterEntity.SummonerId is null) + { + return true; + } + + IPlayerEntity summoner = monsterEntity.MapInstance.GetCharacterById(monsterEntity.SummonerId.Value); + return summoner == null || summoner.IsEnemyWith(entity); + } + + private static bool CheckIfIsSummonedMonster(this IBattleEntity entity, IMonsterEntity monster) + { + if (entity is not IMonsterEntity monsterEntity) + { + return false; + } + + if (monsterEntity.SummonerType is not VisualType.Player) + { + return false; + } + + if (monster.SummonerType == monsterEntity.SummonerType && monsterEntity.SummonerId == monster.SummonerId) + { + return false; + } + + IPlayerEntity player = monster.SummonerId.HasValue ? entity.MapInstance.GetCharacterById(monster.SummonerId.Value) : null; + return player == null || player.IsEnemyWith(monster); + } + + public static bool IsAllyWith(this IBattleEntity entity, IBattleEntity target) + { + if (target.IsMate()) + { + if (target is IMateEntity mateEntity && mateEntity.Owner.IsOnVehicle) + { + return false; + } + } + + if (entity.IsPlayer()) + { + if (target is IMonsterEntity monster) + { + if (monster.Faction == entity.Faction && monster.MapInstance != null && monster.MapInstance.HasMapFlag(MapFlags.ACT_4)) + { + return true; + } + } + + return !target.IsMonster() && (target.IsNpc() || entity.IsPvpActivated(target) == false); + } + + if (entity.IsMate()) + { + if (target is IMonsterEntity monster) + { + if (monster.Faction == entity.Faction && monster.MapInstance != null && monster.MapInstance.HasMapFlag(MapFlags.ACT_4)) + { + return true; + } + } + + return !target.IsMonster() && (target.IsNpc() || entity.IsPvpActivated(target) == false); + } + + if (entity.IsMonster()) + { + return target.IsMonster(); + } + + if (!entity.IsNpc()) + { + return false; + } + + if (target is not IMonsterEntity mons) + { + return true; + } + + if (mons.IsMateTrainer) + { + return true; + } + + return mons.SummonerType is VisualType.Player; + } + + public static void BroadcastCastPacket(this IBattleEntity caster, IBattleEntity target, SkillInfo skillInfo) + { + caster.MapInstance.Broadcast(caster.GenerateCtPacket(target, skillInfo)); + } + + public static string GenerateCtPacket(this IBattleEntity caster, IBattleEntity target, SkillInfo skill) => + $"ct {(byte)caster.Type} {caster.Id} {(target == null ? 0 : (byte)target.Type)} {target?.Id ?? -1} {(skill.CastAnimation == 0 ? -1 : skill.CastAnimation)} {(skill.CastEffect == 0 ? -1 : skill.CastEffect)} {skill.Vnum}"; + + public static string GenerateCleanSuPacket(this IBattleEntity entity, IBattleEntity target, int damage) => + $"su {(byte)entity.Type} {entity.Id} {(byte)target.Type} {target.Id} -1 0 -1 0 0 0 {(target.IsAlive() ? 1 : 0)} {target.HpPercentage} {damage} 0 0"; + + public static string GenerateSuCapturePacket(this IBattleEntity caster, IBattleEntity target, SkillInfo skill, bool failed) => + $"su {(byte)caster.Type} {caster.Id} {(byte)target.Type} {target.Id} {skill.Vnum} {skill.Cooldown} {(failed ? 16 : 15)} -1 -1 -1 1 0 0 -1 0"; + + public static string GenerateSuPacket(this IBattleEntity caster, IBattleEntity target, SkillInfo skill, int damages, SuPacketHitMode hitMode, Position position, bool isReflection, bool isFirst) + { + bool isArchMageSkill = skill.Vnum == (short)SkillsVnums.HOLY_EXPLOSION; + return "su " + + $"{(byte)caster.Type} " + + $"{caster.Id} " + + $"{(byte)target.Type} " + + $"{target.Id} " + + $"{(isReflection ? -1 : skill.Vnum)} " + + $"{(isReflection ? 0 : skill.Cooldown)} " + + $"{(isReflection || skill.Vnum == (short)SkillsVnums.SPY_OUT_SKILL ? -1 : skill.HitAnimation)} " + + $"{(isReflection && isArchMageSkill ? 1047 : skill.HitEffect)} " + + "0 " + + "0 " + + $"{(target.IsAlive() ? 1 : 0)} " + + $"{target.HpPercentage} " + + $"{damages} " + + $"{(sbyte)hitMode} " + + $"{(isReflection ? 1 : 0)}"; + } + + public static string GenerateSuDashPacket(this IBattleEntity caster, IBattleEntity target, SkillInfo skill, int damages, SuPacketHitMode hitMode, bool isReflection = false, bool isFirst = false) + { + if (isReflection) + { + return "su " + + $"{(byte)caster.Type} " + + $"{caster.Id} " + + $"{(byte)target.Type} " + + $"{target.Id} " + + $"{(isFirst ? -1 : skill.Vnum)} " + + "0 " + + "-1 " + + $"{(isFirst ? skill.HitEffect : -1)} " + + "-1 " + + "-1 " + + $"{(target.IsAlive() ? 1 : 0)} " + + $"{target.GetHpPercentage()} " + + $"{damages} " + + $"{(sbyte)hitMode} " + + "1"; + } + + return "su " + + $"{(byte)caster.Type} " + + $"{caster.Id} " + + $"{(byte)target.Type} " + + $"{target.Id} " + + $"{(!isFirst ? -1 : skill.Vnum)} " + + $"{skill.Cooldown} " + + $"{(isFirst ? skill.HitAnimation : -1)} " + + $"{(isFirst ? skill.HitEffect : -1)} " + + $"{(isFirst ? caster.PositionX : -1)} " + + $"{(isFirst ? caster.PositionY : -1)} " + + $"{(target.IsAlive() ? 1 : 0)} " + + $"{target.GetHpPercentage()} " + + $"{damages} " + + $"{(sbyte)hitMode} " + + $"{0}"; + } + + public static string GenerateIcon(this IBattleEntity entity, bool isItem, int vnum) => $"icon {(byte)entity.Type} {entity.Id} {(isItem ? 1 : 2)} {vnum}"; + + public static void SendIconPacket(this IBattleEntity entity, bool isItem, int vnum) + { + switch (entity) + { + case IPlayerEntity character: + character.Session.SendPacket(character.GenerateIcon(isItem, vnum)); + break; + case IMateEntity mateEntity: + mateEntity.Owner?.Session.SendPacket(mateEntity.GenerateIcon(isItem, vnum)); + break; + } + } + + public static void BroadcastCastNonTarget(this IBattleEntity caster, SkillInfo skillInfo) + { + caster.MapInstance.Broadcast(caster.GenerateCastNonTargetPacket(skillInfo)); + } + + public static void BroadcastNonTargetSkill(this IBattleEntity caster, Position position, SkillInfo skill) + { + caster.MapInstance.Broadcast(caster.GenerateNonTargetSkill(position, skill)); + } + + public static string GenerateNonTargetSkill(this IBattleEntity caster, Position position, SkillInfo skill) => + $"bs 1 {caster.Id} {position.X} {position.Y} {skill.Vnum} {skill.Cooldown} {skill.HitAnimation} {skill.HitEffect} 0 0 1 1 0 0 0"; + + public static string GenerateCastNonTargetPacket(this IBattleEntity caster, SkillInfo skill) => $"ct_n 1 {caster.Id} 3 -1 {skill.CastAnimation} {skill.CastEffect} {skill.Vnum}"; + + public static void BroadcastSuPacket(this IBattleEntity caster, IBattleEntity target, SkillInfo skill, int damages, SuPacketHitMode hitMode, bool isReflection = false, bool isFirst = false) + { + if (skill.AttackType == AttackType.Dash) + { + caster.MapInstance.Broadcast(caster.GenerateSuDashPacket(target, skill, damages, hitMode, isReflection, isFirst)); + return; + } + + caster.MapInstance.Broadcast(caster.GenerateSuPacket(target, skill, damages, hitMode, target.Position, isReflection, isFirst)); + } + + public static void BroadcastCleanSuPacket(this IBattleEntity caster, IBattleEntity target, int damage) => caster.MapInstance.Broadcast(caster.GenerateCleanSuPacket(target, damage)); + + public static bool CharacterCanCastOrCancel(this IBattleEntity entity, CharacterSkill skill, IGameLanguageService gameLanguage, SkillInfo skillInfo, bool removeAmmo) + { + var character = (IPlayerEntity)entity; + + if (character.Mp < skillInfo.ManaCost) + { + character.Session.SendCancelPacket(CancelType.NotInCombatMode); + character.Session.SendSound(SoundType.NO_MANA); + character.Session.SendChatMessage(gameLanguage.GetLanguage(GameDialogKey.INFORMATION_CHATMESSAGE_NOT_ENOUGH_MP, character.Session.UserLanguage), ChatMessageColorType.Yellow); + return false; + } + + if (!character.WeaponLoaded(skill, gameLanguage, removeAmmo)) + { + character.Session.SendDebugMessage("[BATTLE_DEBUG] No Weapon loaded"); + character.Session.SendCancelPacket(CancelType.NotInCombatMode); + return false; + } + + if (character.SkillCanBeUsed(skill)) + { + return true; + } + + character.Session.SendDebugMessage("[BATTLE_DEBUG] Skill under cooldown"); + character.Session.SendCancelPacket(CancelType.NotInCombatMode); + return false; + } + + public static bool CharacterCanCastPartnerSkill(this IBattleEntity entity, IBattleEntitySkill skill, SkillInfo skillInfo) + { + var mateEntity = (IMateEntity)entity; + return mateEntity.Mp >= skill.Skill.MpCost && mateEntity.SkillCanBeUsed(skill, DateTime.UtcNow); + } + + public static void SendTargetConstBuffEffect(this IClientSession session, IBattleEntity battleEntity, Buff buff, int time) + { + session.SendPacket(battleEntity.GenerateConstBuffEffect(buff, time)); + } + + public static void SendTargetConstBuffEffects(this IClientSession session, IBattleEntity battleEntity) + { + session.SendPackets(battleEntity.GenerateConstBuffEffects()); + } + + public static void BroadcastConstBuffEffect(this IBattleEntity battleEntity, Buff buff, int time) + { + battleEntity.MapInstance?.Broadcast(battleEntity.GenerateConstBuffEffect(buff, time)); + } + + public static void BroadcastConstBuffEffects(this IBattleEntity battleEntity) + { + battleEntity.MapInstance?.Broadcast(battleEntity.GenerateConstBuffEffects()); + } + + public static IEnumerable GenerateConstBuffEffects(this IBattleEntity battleEntity) + { + return battleEntity.BuffComponent.GetAllBuffs(b => b.IsConstEffect).Select(buff => battleEntity.GenerateConstBuffEffect(buff, (int)buff.Duration.TotalMilliseconds)); + } + + public static string GenerateConstBuffEffect(this IBattleEntity battleEntity, Buff buff, int time) => + $"bf_e {((byte)battleEntity.Type).ToString()} {battleEntity.Id.ToString()} {buff.CardId.ToString()} {time.ToString()}"; + + public static async Task RemoveNegativeBuffs(this IBattleEntity entity, int level) + { + IReadOnlyList buffs = entity.BuffComponent.GetAllBuffs(x => x.Level <= level && x.BuffGroup == BuffGroup.Bad && x.IsNormal()); + await entity.RemoveBuffAsync(false, buffs.ToArray()); + } + + public static async Task RemovePositiveBuffs(this IBattleEntity entity, int level) + { + IReadOnlyList buffs = entity.BuffComponent.GetAllBuffs(x => x.Level <= level && x.BuffGroup == BuffGroup.Good && x.IsNormal()); + await entity.RemoveBuffAsync(false, buffs.ToArray()); + } + + public static async Task RemoveNeutralBuffs(this IBattleEntity entity, int level) + { + IReadOnlyList buffs = entity.BuffComponent.GetAllBuffs(x => x.Level <= level && x.BuffGroup == BuffGroup.Neutral && x.IsNormal()); + await entity.RemoveBuffAsync(false, buffs.ToArray()); + } + + public static async Task RemoveInvisibility(this IBattleEntity damager) + { + if (!damager.BuffComponent.HasAnyBuff()) + { + return; + } + + if (damager is IPlayerEntity { Invisible: false }) + { + return; + } + + if (!damager.BCardComponent.HasBCard(BCardType.SpecialActions, (byte)AdditionalTypes.SpecialActions.Hide)) + { + return; + } + + if (damager is not IPlayerEntity charDamager) + { + return; + } + + IReadOnlyList buffs = charDamager.BuffComponent.GetAllBuffs(b => b.BCards.Any(t => t.Type == (short)BCardType.SpecialActions && t.SubType == (byte)AdditionalTypes.SpecialActions.Hide)); + foreach (Buff buff in buffs) + { + await damager.RemoveBuffAsync(false, buff); + charDamager.Session.UpdateVisibility(); + } + + foreach (IMateEntity mateEntity in charDamager.MateComponent.TeamMembers()) + { + if (!mateEntity.BuffComponent.HasAnyBuff()) + { + continue; + } + + IReadOnlyList mateBuffs = + mateEntity.BuffComponent.GetAllBuffs(b => b.BCards.Any(t => t.Type == (short)BCardType.SpecialActions && t.SubType == (byte)AdditionalTypes.SpecialActions.Hide)); + foreach (Buff mateBuff in mateBuffs) + { + await mateEntity.RemoveBuffAsync(false, mateBuff); + } + } + } + + public static bool CanPerformMove(this IBattleEntity entity) + { + switch (entity) + { + case IPlayerEntity playerEntity: + RainbowBattleParty rainbowBattleParty = playerEntity.RainbowBattleComponent.RainbowBattleParty; + if (rainbowBattleParty != null) + { + if (!playerEntity.RainbowBattleComponent.RainbowBattleParty.Started) + { + return false; + } + + if (playerEntity.RainbowBattleComponent.IsFrozen) + { + return false; + } + } + + break; + case IMateEntity mateEntity: + if (mateEntity.MapInstance is { MapInstanceType: MapInstanceType.RainbowBattle }) + { + return false; + } + + break; + } + + return !entity.BCardComponent.HasBCard(BCardType.Move, (byte)AdditionalTypes.Move.MovementImpossible) + && !entity.BuffComponent.HasBuff((int)BuffVnums.ETERNAL_ICE) && entity.IsAlive(); + } + + public static bool CanPerformAttack(this IBattleEntity entity) + { + if (entity.BCardComponent.HasBCard(BCardType.SpecialAttack, (byte)AdditionalTypes.SpecialAttack.NoAttack)) + { + return false; + } + + if (entity.BuffComponent.HasBuff((int)BuffVnums.ETERNAL_ICE)) + { + return false; + } + + if (!entity.IsAlive()) + { + return false; + } + + switch (entity) + { + case IPlayerEntity playerEntity: + if (playerEntity.IsSeal) + { + return false; + } + + if (playerEntity.RainbowBattleComponent.IsFrozen) + { + return false; + } + + break; + case IMonsterEntity mapMonster: + switch (mapMonster.AttackType) + { + case AttackType.Melee: + if (mapMonster.BCardComponent.HasBCard(BCardType.SpecialAttack, (byte)AdditionalTypes.SpecialAttack.MeleeDisabled)) + { + return false; + } + + break; + case AttackType.Ranged: + if (mapMonster.BCardComponent.HasBCard(BCardType.SpecialAttack, (byte)AdditionalTypes.SpecialAttack.RangedDisabled)) + { + return false; + } + + break; + case AttackType.Magical: + if (mapMonster.BCardComponent.HasBCard(BCardType.SpecialAttack, (byte)AdditionalTypes.SpecialAttack.MagicDisabled)) + { + return false; + } + + break; + } + + break; + case IMateEntity mateEntity: + if (mateEntity.MapInstance is { MapInstanceType: MapInstanceType.RainbowBattle }) + { + return false; + } + + switch (mateEntity.AttackType) + { + case AttackType.Melee: + if (mateEntity.BCardComponent.HasBCard(BCardType.SpecialAttack, (byte)AdditionalTypes.SpecialAttack.MeleeDisabled)) + { + return false; + } + + break; + case AttackType.Ranged: + if (mateEntity.BCardComponent.HasBCard(BCardType.SpecialAttack, (byte)AdditionalTypes.SpecialAttack.RangedDisabled)) + { + return false; + } + + break; + case AttackType.Magical: + if (mateEntity.BCardComponent.HasBCard(BCardType.SpecialAttack, (byte)AdditionalTypes.SpecialAttack.MagicDisabled)) + { + return false; + } + + break; + } + + break; + case INpcEntity mapNpc: + switch (mapNpc.AttackType) + { + case AttackType.Melee: + if (mapNpc.BCardComponent.HasBCard(BCardType.SpecialAttack, (byte)AdditionalTypes.SpecialAttack.MeleeDisabled)) + { + return false; + } + + break; + case AttackType.Ranged: + if (mapNpc.BCardComponent.HasBCard(BCardType.SpecialAttack, (byte)AdditionalTypes.SpecialAttack.RangedDisabled)) + { + return false; + } + + break; + case AttackType.Magical: + if (mapNpc.BCardComponent.HasBCard(BCardType.SpecialAttack, (byte)AdditionalTypes.SpecialAttack.MagicDisabled)) + { + return false; + } + + break; + } + + break; + } + + return true; + } + + public static bool CanPerformAttack(this IPlayerEntity playerEntity, SkillInfo skillInfo) + { + return skillInfo.AttackType switch + { + AttackType.Melee => !playerEntity.BCardComponent.HasBCard(BCardType.SpecialAttack, (byte)AdditionalTypes.SpecialAttack.MeleeDisabled), + AttackType.Ranged => !playerEntity.BCardComponent.HasBCard(BCardType.SpecialAttack, (byte)AdditionalTypes.SpecialAttack.RangedDisabled), + AttackType.Magical => !playerEntity.BCardComponent.HasBCard(BCardType.SpecialAttack, (byte)AdditionalTypes.SpecialAttack.MagicDisabled), + _ => true + }; + } + + public static void ApplyAttackBCard(this IBattleEntity attacker, IBattleEntity defender, SkillInfo skillInfo, IBCardEffectHandlerContainer bCardHandler) + { + EquipmentType eqType = skillInfo.IsUsingSecondWeapon ? EquipmentType.MainWeapon : EquipmentType.SecondaryWeapon; + bool isPlayer = attacker.IsPlayer(); + + if (isPlayer) + { + IReadOnlyList shellBCards = attacker.BCardComponent.GetShellTriggers(!skillInfo.IsUsingSecondWeapon); + foreach (BCardDTO shellBCard in shellBCards) + { + bCardHandler.Execute(defender, attacker, shellBCard); + } + + byte executeMainOrSecond = skillInfo.IsUsingSecondWeapon + ? (byte)AdditionalTypes.SpecialEffects2.SecondaryWeaponCausingChance + : (byte)AdditionalTypes.SpecialEffects2.MainWeaponCausingChance; + + foreach ((int _, BCardDTO bCard) in attacker.BCardComponent.GetBuffBCards(x => x.Item2.Type == (short)BCardType.SpecialEffects2 && x.Item2.SubType == executeMainOrSecond)) + { + bCardHandler.Execute(defender, attacker, bCard); + } + + IReadOnlyDictionary> dictionary = attacker.BCardComponent.GetEquipmentBCards(); + + foreach (KeyValuePair> bCardDictionary in dictionary) + { + EquipmentType equipmentType = bCardDictionary.Key; + if (equipmentType == eqType) + { + continue; + } + + if (equipmentType != EquipmentType.MainWeapon && equipmentType != EquipmentType.SecondaryWeapon) + { + continue; + } + + if (bCardDictionary.Value == null) + { + continue; + } + + foreach (BCardDTO bCard in bCardDictionary.Value) + { + var castType = (CastType)bCard.CastType; + switch (castType) + { + case CastType.ATTACK: + bCardHandler.Execute(defender, attacker, bCard); + break; + case CastType.SELF: + case CastType.DEFENSE: + bCardHandler.Execute(attacker, attacker, bCard); + break; + } + } + } + + return; + } + + if (attacker.IsMate()) + { + IReadOnlyDictionary> mateEq = attacker.BCardComponent.GetEquipmentBCards(); + + foreach (KeyValuePair> bCardDictionary in mateEq) + { + EquipmentType equipmentType = bCardDictionary.Key; + if (equipmentType == eqType) + { + continue; + } + + if (equipmentType != EquipmentType.MainWeapon && equipmentType != EquipmentType.SecondaryWeapon) + { + continue; + } + + if (bCardDictionary.Value == null) + { + continue; + } + + foreach (BCardDTO bCard in bCardDictionary.Value) + { + var castType = (CastType)bCard.CastType; + switch (castType) + { + case CastType.ATTACK: + bCardHandler.Execute(defender, attacker, bCard); + break; + case CastType.SELF: + case CastType.DEFENSE: + bCardHandler.Execute(attacker, attacker, bCard); + break; + } + } + } + } + + IReadOnlyList attackerBCards = attacker.BCardComponent.GetTriggerBCards(BCardTriggerType.ATTACK); + + if (!attackerBCards.Any()) + { + return; + } + + if (attacker.BCardComponent.HasBCard(BCardType.Mode, (byte)AdditionalTypes.Mode.DirectDamage)) + { + return; + } + + foreach (BCardDTO bCard in attackerBCards.ToList()) + { + switch ((CastType)bCard.CastType) + { + case CastType.ATTACK: // during attacking on enemy (the target) + bCardHandler.Execute(defender, attacker, bCard); + break; + case CastType.DEFENSE: // during attacking on self + bCardHandler.Execute(attacker, attacker, bCard); + break; + default: + continue; + } + } + } + + public static void ApplyDefenderBCard(this IBattleEntity defender, IBattleEntity attacker, SkillInfo skillInfo, IBCardEffectHandlerContainer bCardHandler) + { + bool isPlayer = defender.IsPlayer(); + + if (isPlayer) + { + IReadOnlyDictionary> dictionary = defender.BCardComponent.GetEquipmentBCards(); + + foreach ((EquipmentType equipmentType, List value) in dictionary) + { + if (equipmentType is EquipmentType.MainWeapon or EquipmentType.SecondaryWeapon) + { + continue; + } + + if (value == null) + { + continue; + } + + foreach (BCardDTO bCard in value) + { + var castType = (CastType)bCard.CastType; + switch (castType) + { + case CastType.ATTACK: + bCardHandler.Execute(attacker, defender, bCard, skillInfo); + break; + case CastType.SELF: + case CastType.DEFENSE: + bCardHandler.Execute(defender, defender, bCard, skillInfo); + break; + } + } + } + + return; + } + + IReadOnlyList defenderBCards = defender.BCardComponent.GetTriggerBCards(BCardTriggerType.DEFENSE); + + if (!defenderBCards.Any()) + { + return; + } + + foreach (BCardDTO bCard in defenderBCards.ToList()) + { + switch ((CastType)bCard.CastType) + { + case CastType.ATTACK: // on getting hit on enemy (the attacker) + bCardHandler.Execute(attacker, defender, bCard); + break; + case CastType.DEFENSE: // on getting hit on self + bCardHandler.Execute(defender, defender, bCard); + break; + } + } + } + + + public static void InitializeBCards(this IBattleEntity entity) + { + var attackBCards = new List(); + var defenseBCards = new List(); + + IReadOnlyList entityBCards = entity switch + { + IMonsterEntity mapMonster => mapMonster.BCards, + IMateEntity mateEntity => mateEntity.BCards, + INpcEntity mapNpc => mapNpc.BCards, + _ => null + }; + + if (entityBCards == null) + { + Log.Warn($"Couldn't initialize BCards of this BattleEntity -> VisualType: '{entity.Type.ToString()}' Id: '{entity.Id.ToString()}'"); + return; + } + + foreach (BCardDTO bCard in entityBCards) + { + switch (bCard.NpcTriggerType) + { + case BCardNpcTriggerType.ON_ATTACK: + if ((CastType)bCard.CastType == CastType.SELF) + { + entity.BCardComponent.AddBCard(bCard); + } + else + { + attackBCards.Add(bCard); + } + + break; + case BCardNpcTriggerType.ON_DEFENSE: + if ((CastType)bCard.CastType == CastType.SELF) + { + entity.BCardComponent.AddBCard(bCard); + } + else + { + defenseBCards.Add(bCard); + } + + break; + default: + entity.BCardComponent.AddBCard(bCard); + break; + } + } + + entity.BCardComponent.AddTriggerBCards(BCardTriggerType.ATTACK, attackBCards); + entity.BCardComponent.AddTriggerBCards(BCardTriggerType.DEFENSE, defenseBCards); + + if (entity is not IMonsterEntity monsterEntity) + { + return; + } + + monsterEntity.RefreshStats(); + } + + public static bool IsSameEntity(this IBattleEntity entity, IBattleEntity target) => entity != null && target != null && entity.Id == target.Id && entity.Type == target.Type; + + public static void SetSkillCooldown(this IBattleEntity caster, SkillInfo skill) + { + short cooldownReduction = skill.Cooldown; + List skills = caster.Skills; + IBattleEntitySkill battleEntitySkill; + + if (caster.IsMonster() || caster.IsNpc()) + { + battleEntitySkill = skills.FirstOrDefault(x => x.Skill.Id == skill.Vnum); + } + else + { + if (caster.IsPlayer()) + { + battleEntitySkill = skills.FirstOrDefault(x => x.Skill.CastId == skill.CastId && x.Skill.SkillType == SkillType.NormalPlayerSkill); + } + else + { + battleEntitySkill = skills.FirstOrDefault(x => x.Skill.CastId == skill.CastId); + } + } + + if (battleEntitySkill == null) + { + return; + } + + DateTime time = DateTime.UtcNow.AddMilliseconds(cooldownReduction * 100); + battleEntitySkill.LastUse = time; + + switch (caster) + { + case IPlayerEntity character: + character.AddSkillCooldown(time, skill.CastId); + break; + case IMateEntity mateEntity: + if (skill.Vnum == 0) + { + return; + } + + mateEntity.Owner?.AddMateSkillCooldown(time, skill.CastId, mateEntity.MateType); + break; + } + } + + public static string GenerateDiePacket(this IBattleEntity entity) => $"die {(byte)entity.Type} {entity.Id} {(byte)entity.Type} {entity.Id}"; + public static void BroadcastDie(this IBattleEntity entity) => entity.MapInstance.Broadcast(entity.GenerateDiePacket()); + public static string GeneratePushPacket(this IBattleEntity entity, short x, short y, int value) => $"guri 3 {(byte)entity.Type} {entity.Id} {x} {y} 3 {value} 2 -1"; + + public static string GenerateDashGuriPacket(this IBattleEntity entity, short x, short y, int value) + => $"guri 3 {(byte)entity.Type} {entity.Id} {x} {y} 3 2 2 {value}"; + + public static async Task RemoveSacrifice(this IBattleEntity caster, IBattleEntity target, ISacrificeManager sacrificeManager, IGameLanguageService gameLanguage) + { + Buff sacrifice = caster.BuffComponent.GetBuff((short)BuffVnums.SPIRIT_OF_SACRIFICE); + await caster.RemoveBuffAsync(false, sacrifice); + Buff nobleGesture = target.BuffComponent.GetBuff((short)BuffVnums.NOBLE_GESTURE); + await target.RemoveBuffAsync(false, nobleGesture); + string message; + if (caster is IPlayerEntity character) + { + message = gameLanguage.GetLanguage(GameDialogKey.SKILL_SHOUTMESSAGE_SACRIFICE_REMOVED, character.Session.UserLanguage); + character.Session.SendMsg(message, MsgMessageType.SmallMiddle); + } + + if (target is IPlayerEntity targetCharacter) + { + message = gameLanguage.GetLanguage(GameDialogKey.SKILL_SHOUTMESSAGE_SACRIFICE_REMOVED, targetCharacter.Session.UserLanguage); + targetCharacter.Session.SendMsg(message, MsgMessageType.SmallMiddle); + } + } + + public static bool CanBeInterrupted(this IPlayerEntity character, SkillInfo skillInfo) + { + if (character.Class != ClassType.Magician && character.Class != ClassType.Adventurer) + { + return false; + } + + if (character.GetMaxWeaponShellValue(ShellEffectType.AntiMagicDisorder) != 0) + { + return false; + } + + if (skillInfo.AttackType != AttackType.Magical || skillInfo.IsUsingSecondWeapon) + { + return false; + } + + return !character.SkillComponent.IsSkillInterrupted; + } + + public static string GenerateShadowFigurePacket(this IBattleEntity entity, int firstValue, int secondValue) => $"guri 0 {(byte)entity.Type} {entity.Id} {firstValue} {secondValue}"; + + public static void BroadcastShadowFigurePacket(this IBattleEntity entity, int firstValue, int secondValue) => + entity.MapInstance?.Broadcast(entity.GenerateShadowFigurePacket(firstValue, secondValue)); + + public static void ShadowAppears(this IBattleEntity battleEntity, bool broadcastZeroValue, Buff buff = null) + { + if (buff != null && broadcastZeroValue && buff.BCards.Any(x => x.Type == (short)BCardType.SpecialEffects && x.SubType == (byte)AdditionalTypes.SpecialEffects.ShadowAppears)) + { + battleEntity.BroadcastShadowFigurePacket(0, 0); + } + + if (!battleEntity.BuffComponent.HasAnyBuff()) + { + return; + } + + if (!battleEntity.BCardComponent.HasBCard(BCardType.SpecialEffects, (byte)AdditionalTypes.SpecialEffects.ShadowAppears)) + { + battleEntity.BroadcastShadowFigurePacket(0, 0); + return; + } + + (int firstData, int secondData) = battleEntity.BCardComponent.GetAllBCardsInformation(BCardType.SpecialEffects, + (byte)AdditionalTypes.SpecialEffects.ShadowAppears, battleEntity.Level); + if (firstData == 0 && secondData == 0) + { + battleEntity.BroadcastShadowFigurePacket(0, 0); + return; + } + + battleEntity.BroadcastShadowFigurePacket(!broadcastZeroValue ? firstData : 0, secondData); + } + + public static DateTime GenerateSkillCastTime(this IBattleEntity entity, SkillInfo skillInfo, bool isPartnerSkill = false) + { + DateTime time = DateTime.UtcNow.AddMilliseconds(GenerateSkillCastTimeNumber(entity, skillInfo, isPartnerSkill)); + return time; + } + + public static short GenerateSkillCastTimeNumber(this IBattleEntity entity, SkillInfo skillInfo, bool isPartnerSkill = false) + { + short milliSeconds = skillInfo.CastTime; + int toAddMilliSeconds = skillInfo.AttackType switch + { + AttackType.Melee => (short)entity.BCardComponent.GetAllBCardsInformation(BCardType.JumpBackPush, (byte)AdditionalTypes.JumpBackPush.MeleeDurationIncreased, entity.Level).firstData, + AttackType.Ranged => (short)entity.BCardComponent.GetAllBCardsInformation(BCardType.JumpBackPush, (byte)AdditionalTypes.JumpBackPush.RangedDurationIncreased, entity.Level).firstData, + AttackType.Magical => (short)entity.BCardComponent.GetAllBCardsInformation(BCardType.JumpBackPush, (byte)AdditionalTypes.JumpBackPush.MagicalDurationIncreased, entity.Level).firstData, + _ => 0 + }; + + toAddMilliSeconds -= skillInfo.AttackType switch + { + AttackType.Melee => (short)entity.BCardComponent.GetAllBCardsInformation(BCardType.JumpBackPush, (byte)AdditionalTypes.JumpBackPush.MeleeDurationDecreased, entity.Level).firstData, + AttackType.Ranged => (short)entity.BCardComponent.GetAllBCardsInformation(BCardType.JumpBackPush, (byte)AdditionalTypes.JumpBackPush.RangedDurationDecreased, entity.Level).firstData, + AttackType.Magical => (short)entity.BCardComponent.GetAllBCardsInformation(BCardType.JumpBackPush, (byte)AdditionalTypes.JumpBackPush.MagicalDurationDecreased, entity.Level).firstData, + _ => 0 + }; + + int toIncrease = entity.BCardComponent.GetAllBCardsInformation(BCardType.Casting, (byte)AdditionalTypes.Casting.EffectDurationIncreased, entity.Level).firstData; + int toDecrease = entity.BCardComponent.GetAllBCardsInformation(BCardType.Casting, (byte)AdditionalTypes.Casting.EffectDurationDecreased, entity.Level).firstData; + + toAddMilliSeconds += (short)((toAddMilliSeconds == 0 ? milliSeconds * 10 : toAddMilliSeconds) * toIncrease * 0.01); + toAddMilliSeconds -= (short)((toAddMilliSeconds == 0 ? milliSeconds * 10 : toAddMilliSeconds) * toDecrease * 0.01); + + milliSeconds += (short)(toAddMilliSeconds / 10); + + if (isPartnerSkill) + { + milliSeconds = (short)((milliSeconds - 1) * 2); + } + + return (short)(milliSeconds * 100); + } + + public static int CalculateManaUsage(this IBattleEntity entity, int mana, SkillDTO skill = null) + { + double manaMultiplier = 1; + + if (skill is { AttackType: AttackType.Magical }) + { + manaMultiplier -= entity.BCardComponent.GetAllBCardsInformation(BCardType.Casting, + (byte)AdditionalTypes.Casting.ManaForSkillsDecreased, entity.Level).firstData * 0.01; + + manaMultiplier += entity.BCardComponent.GetAllBCardsInformation(BCardType.Casting, + (byte)AdditionalTypes.Casting.ManaForSkillsIncreased, entity.Level).firstData * 0.01; + } + + if (entity is IPlayerEntity playerEntity) + { + manaMultiplier -= playerEntity.GetJewelsCellonsValue(CellonType.MpConsumption) * 0.01; + manaMultiplier -= playerEntity.GetMaxWeaponShellValue(ShellEffectType.ReducedMPConsume) * 0.01; + } + + return (int)(mana * manaMultiplier); + } + + public static void RemoveEntityMp(this IBattleEntity battleEntity, short mana, SkillDTO skill = null) + { + int mp = battleEntity.CalculateManaUsage(mana, skill); + + battleEntity.Mp = battleEntity.Mp - mp < 0 ? 0 : battleEntity.Mp - mp; + } + + public static bool IsMateTrainer(this IBattleEntity entity) => entity is IMonsterEntity { IsMateTrainer: true }; + + public static bool IsInvisibleGm(this IBattleEntity entity) => entity is IPlayerEntity player && player.CheatComponent.IsInvisible; +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Extensions/BroadcasterExtensions.cs b/srcs/WingsAPI.Game/Extensions/BroadcasterExtensions.cs new file mode 100644 index 0000000..c08ac29 --- /dev/null +++ b/srcs/WingsAPI.Game/Extensions/BroadcasterExtensions.cs @@ -0,0 +1,56 @@ +using System; +using System.Threading.Tasks; +using WingsEmu.Game._i18n; +using WingsEmu.Game.GameEvent; +using WingsEmu.Game.GameEvent.Configuration; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Networking.Broadcasting; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Extensions; + +public static class BroadcasterExtensions +{ + private static IGameLanguageService _languageService => StaticGameLanguageService.Instance; + + public static void BroadcastToFamily(this IBroadcaster broadcaster, string packet, long familyId) => broadcaster.Broadcast(packet, new FamilyBroadcast(familyId)); + + public static void BroadcastToFamily(this IBroadcaster broadcaster, long familyId, Func> func) + { + broadcaster.BroadcastAsync(func, new FamilyBroadcast(familyId)); + } + + public static void BroadcastToFamilyExceptFamilyHead(this IBroadcaster broadcaster, long familyId, Func> func) + { + broadcaster.BroadcastAsync(func, new FamilyBroadcast(familyId)); + } + + public static void BroadcastToGameMaster(this IBroadcaster broadcaster, string message) => broadcaster.Broadcast($"say 0 0 11 {message}", new OnlyGameMasters()); + + public static void BroadcastGameEventAsk(this IBroadcaster broadcaster, GameEventType gameType, IGlobalGameEventConfiguration gameEventConfiguration) + { + switch (gameType) + { + case GameEventType.InstantBattle: + // Do you want to take part in the event: Instant Combat? + broadcaster.Broadcast(x => x.GenerateEventAsk(QnamlType.InstantCombat, "guri 506", x.GenerateGameEventMessage(GameDialogKey.INSTANT_COMBAT_NAME)), + new InBaseMapBroadcast(), new NotMutedBroadcast()); + break; + } + } + + private static string GenerateGameEventMessage(this IClientSession x, GameDialogKey gameEventName, long cost = default) + { + if (cost == default) + { + return _languageService.GetLanguageFormat(GameDialogKey.GAMEEVENT_DIALOG_ASK_PARTICIPATE, x.UserLanguage, + _languageService.GetLanguage(gameEventName, x.UserLanguage)); + } + + return _languageService.GetLanguageFormat(GameDialogKey.GAMEEVENT_DIALOG_ASK_PARTICIPATE_WITH_COST, x.UserLanguage, + _languageService.GetLanguage(gameEventName, x.UserLanguage), cost); + } + + public static void BroadcastToGameMaster(this IBroadcaster broadcaster, IClientSession player, string message) + => broadcaster.Broadcast($"say 0 0 11 [GM_ONLY] Player: {player?.PlayerEntity.Name} - {message}", new OnlyGameMasters()); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Extensions/BuffExtension.cs b/srcs/WingsAPI.Game/Extensions/BuffExtension.cs new file mode 100644 index 0000000..e2b0ddb --- /dev/null +++ b/srcs/WingsAPI.Game/Extensions/BuffExtension.cs @@ -0,0 +1,267 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using WingsAPI.Packets.Enums.Shells; +using WingsEmu.DTOs.BCards; +using WingsEmu.Game._enum; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Buffs.Events; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Extensions; + +public static class BuffExtension +{ + private static readonly HashSet _buffResistance = new() + { + (short)BCardType.DebuffResistance, + (short)BCardType.AngerSkill, + (short)BCardType.SpecialisationBuffResistance, + (short)BCardType.DragonSkills, + (short)BCardType.Buff + }; + + public static bool IsNormal(this Buff buff) => buff != null && buff.BuffFlags.HasFlag(BuffFlag.NORMAL); + public static bool IsNotRemovedOnDeath(this Buff buff) => buff != null && buff.BuffFlags.HasFlag(BuffFlag.NOT_REMOVED_ON_DEATH); + public static bool IsNotDisappearOnSpChange(this Buff buff) => buff != null && buff.BuffFlags.HasFlag(BuffFlag.NOT_REMOVED_ON_SP_CHANGE); + public static bool IsRefreshAtExpiration(this Buff buff) => buff != null && buff.BuffFlags.HasFlag(BuffFlag.REFRESH_AT_EXPIRATION); + public static bool IsDisappearOnPvp(this Buff buff) => buff != null && buff.BuffFlags.HasFlag(BuffFlag.DISAPPEAR_ON_PVP); + public static bool IsBigBuff(this Buff buff) => buff != null && buff.BuffFlags.HasFlag(BuffFlag.BIG); + public static bool IsNoDuration(this Buff buff) => buff != null && buff.BuffFlags.HasFlag(BuffFlag.NO_DURATION); + public static bool IsSavingOnDisconnect(this Buff buff) => buff != null && buff.BuffFlags.HasFlag(BuffFlag.SAVING_ON_DISCONNECT); + public static bool IsPartnerBuff(this Buff buff) => buff != null && buff.BuffFlags.HasFlag(BuffFlag.PARTNER); + public static bool IsPartnerRankBuff(this int buffVnum) => buffVnum >= 2000; + + public static int RemainingTimeInMilliseconds(this Buff buff) => (int)(buff.Start - DateTime.UtcNow + buff.Duration).TotalMilliseconds; + + public static async Task CheckPartnerBuff(this IClientSession session) => await session.EmitEventAsync(new BuffPartnerCheckEvent()); + + public static double CheckForResistance(this IBattleEntity target, Buff b, ICardsManager cardsManager, out double buffCounter, out double specializedResistance) + { + var bCards = new List<(int casterLevel, BCardDTO bCard)>(); + + IReadOnlyList buffDebuffResistance = target.BCardComponent.GetAllBCards(); + IReadOnlyList<(int casterLevel, BCardDTO bCard)> buffsBCards = target.BCardComponent.GetBuffBCards(); + + foreach (BCardDTO bCard in buffDebuffResistance) + { + bCards.Add((target.Level, bCard)); + } + + bCards.AddRange(buffsBCards); + + double debuffCounter = 1; + buffCounter = 1; + specializedResistance = 1; + + int secondDataSum = 0; + foreach ((int casterLevel, BCardDTO resistanceBCard) in bCards) + { + if (!_buffResistance.Contains(resistanceBCard.Type)) + { + continue; + } + + int firstDataValue = resistanceBCard.FirstDataValue(casterLevel); + int secondDataValue = resistanceBCard.SecondDataValue(casterLevel); + + switch (resistanceBCard.Type) + { + case (short)BCardType.SpecialisationBuffResistance when resistanceBCard.SubType == (byte)AdditionalTypes.SpecialisationBuffResistance.ResistanceToEffect || + resistanceBCard.SubType == (byte)AdditionalTypes.SpecialisationBuffResistance.ResistanceToEffectNegated: + { + if (secondDataValue == 0) + { + continue; + } + + Card anotherBuff = cardsManager.GetCardByCardId(secondDataValue); + if (anotherBuff == null) + { + continue; + } + + if (b.GroupId == anotherBuff.GroupId && b.Level <= anotherBuff.Level) + { + specializedResistance *= 1.0 - firstDataValue / 100.0; + } + + break; + } + case (short)BCardType.DragonSkills when resistanceBCard.SubType == (byte)AdditionalTypes.DragonSkills.CannotUseBuffChance: + { + if (b.Level <= resistanceBCard.SecondData) + { + buffCounter *= 1.0 - firstDataValue / 100.0; + } + + break; + } + case (short)BCardType.DebuffResistance when b.BuffGroup == BuffGroup.Bad: + { + switch (resistanceBCard.SubType) + { + case (byte)AdditionalTypes.DebuffResistance.NeverBadDiseaseEffectChance when b.BuffCategory == BuffCategory.DiseaseSeries && b.Level <= firstDataValue: + secondDataSum -= secondDataValue; + break; + case (byte)AdditionalTypes.DebuffResistance.NeverBadGeneralEffectChance when b.BuffCategory == BuffCategory.GeneralEffect && b.Level <= firstDataValue: + secondDataSum -= secondDataValue; + break; + case (byte)AdditionalTypes.DebuffResistance.NeverBadMagicEffectChance when b.BuffCategory == BuffCategory.MagicEffect && b.Level <= firstDataValue: + secondDataSum -= secondDataValue; + break; + case (byte)AdditionalTypes.DebuffResistance.NeverBadToxicEffectChance when b.BuffCategory == BuffCategory.PoisonType && b.Level <= firstDataValue: + secondDataSum -= secondDataValue; + break; + case (byte)AdditionalTypes.DebuffResistance.IncreaseBadDiseaseEffectChance when b.BuffCategory == BuffCategory.DiseaseSeries && b.Level <= firstDataValue: + secondDataSum += secondDataValue; + break; + case (byte)AdditionalTypes.DebuffResistance.IncreaseBadGeneralEffectChance when b.BuffCategory == BuffCategory.GeneralEffect && b.Level <= firstDataValue: + secondDataSum += secondDataValue; + break; + case (byte)AdditionalTypes.DebuffResistance.IncreaseBadMagicEffectChance when b.BuffCategory == BuffCategory.MagicEffect && b.Level <= firstDataValue: + secondDataSum += secondDataValue; + break; + case (byte)AdditionalTypes.DebuffResistance.IncreaseBadToxicEffectChance when b.BuffCategory == BuffCategory.PoisonType && b.Level <= firstDataValue: + secondDataSum += secondDataValue; + break; + } + + switch (resistanceBCard.SubType) + { + case (byte)AdditionalTypes.DebuffResistance.NeverBadEffectChance when b.Level <= firstDataValue: + secondDataSum -= secondDataValue; + break; + case (byte)AdditionalTypes.DebuffResistance.IncreaseBadEffectChance when b.Level <= firstDataValue: + secondDataSum += secondDataValue; + break; + } + + break; + } + case (short)BCardType.AngerSkill when b.BuffGroup == BuffGroup.Good: + switch (resistanceBCard.SubType) + { + case (byte)AdditionalTypes.AngerSkill.BlockGoodEffectNegated when b.Level <= secondDataValue: + case (byte)AdditionalTypes.AngerSkill.BlockGoodEffect when b.Level <= secondDataValue: + buffCounter *= 1.0 - firstDataValue / 100.0; + break; + } + + break; + case (short)BCardType.Buff when resistanceBCard.SubType == (byte)AdditionalTypes.Buff.PreventingBadEffect: + if (firstDataValue != b.GroupId) + { + break; + } + + if (b.Level > secondDataValue) + { + break; + } + + secondDataSum -= 80; + + break; + } + } + + debuffCounter *= 1.0 + secondDataSum / 100.0; + + if (target is not IPlayerEntity playerEntity) + { + return debuffCounter; + } + + int shell = playerEntity.GetMaxArmorShellValue(ShellEffectType.ReducedAllNegativeEffect); + debuffCounter *= 1.0 - shell / 100.0; + + int reducedByBuffVnum = 0; + + switch ((BuffVnums)b.CardId) + { + case BuffVnums.MINOR_BLEEDING: + reducedByBuffVnum = playerEntity.GetMaxArmorShellValue(ShellEffectType.ReducedMinorBleeding); + reducedByBuffVnum += playerEntity.GetMaxArmorShellValue(ShellEffectType.ReducedBleedingAndMinorBleeding); + break; + case BuffVnums.BLEEDING: + reducedByBuffVnum = playerEntity.GetMaxArmorShellValue(ShellEffectType.ReducedBleedingAndMinorBleeding); + break; + case BuffVnums.BLACKOUT: + reducedByBuffVnum = playerEntity.GetMaxArmorShellValue(ShellEffectType.ReducedStun); + break; + case BuffVnums.HAND_OF_DEATH: + reducedByBuffVnum = playerEntity.GetMaxArmorShellValue(ShellEffectType.ReducedParalysis); + break; + case BuffVnums.FREEZE: + reducedByBuffVnum = playerEntity.GetMaxArmorShellValue(ShellEffectType.ReducedFreeze); + break; + case BuffVnums.BLIND: + reducedByBuffVnum = playerEntity.GetMaxArmorShellValue(ShellEffectType.ReducedBlind); + break; + case BuffVnums.BIND: + reducedByBuffVnum = playerEntity.GetMaxArmorShellValue(ShellEffectType.ReducedSlow); + break; + case BuffVnums.WEAKEN_DEFENCE_POWER: + reducedByBuffVnum = playerEntity.GetMaxArmorShellValue(ShellEffectType.ReducedArmorDeBuff); + break; + case BuffVnums.SHOCK: + reducedByBuffVnum = playerEntity.GetMaxArmorShellValue(ShellEffectType.ReducedShock); + break; + case BuffVnums.PARALYSIS: + reducedByBuffVnum = playerEntity.GetMaxArmorShellValue(ShellEffectType.ReducedPoisonParalysis); + break; + } + + debuffCounter *= 1.0 - reducedByBuffVnum / 100.0; + + int reducedByGroupId = (BuffGroupIds)b.GroupId switch + { + BuffGroupIds.STUNS => playerEntity.GetMaxArmorShellValue(ShellEffectType.ReducedAllStun), + BuffGroupIds.BLEEDING => playerEntity.GetMaxArmorShellValue(ShellEffectType.ReducedAllBleedingType), + _ => 0 + }; + + debuffCounter *= 1.0 - reducedByGroupId / 100.0; + + return debuffCounter; + } + + public static void SendBuffsPacket(this IClientSession session) + { + if (!session.PlayerEntity.BuffComponent.HasAnyBuff()) + { + return; + } + + IReadOnlyList buffs = session.PlayerEntity.BuffComponent.GetAllBuffs(); + foreach (Buff buff in buffs) + { + if (!buff.IsBigBuff()) + { + switch (buff.CardId) + { + case (short)BuffVnums.CHARGE when session.PlayerEntity.BCardComponent.GetChargeBCards().Any(): + int sum = session.PlayerEntity.BCardComponent.GetChargeBCards().Sum(x => x.FirstDataValue(session.PlayerEntity.Level)); + session.SendBfPacket(buff, sum, sum); + break; + case (short)BuffVnums.CHARGE: + session.SendBfPacket(buff, session.PlayerEntity.ChargeComponent.GetCharge(), session.PlayerEntity.ChargeComponent.GetCharge()); + break; + default: + session.SendBfLeftPacket(buff); + break; + } + } + else + { + session.SendStaticBuffUiPacket(buff, buff.RemainingTimeInMilliseconds()); + } + } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Extensions/CharacterExtension.cs b/srcs/WingsAPI.Game/Extensions/CharacterExtension.cs new file mode 100644 index 0000000..c80f417 --- /dev/null +++ b/srcs/WingsAPI.Game/Extensions/CharacterExtension.cs @@ -0,0 +1,1039 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using WingsAPI.Data.Character; +using WingsEmu.DTOs.Account; +using WingsEmu.DTOs.BCards; +using WingsEmu.DTOs.Bonus; +using WingsEmu.DTOs.Items; +using WingsEmu.DTOs.Maps; +using WingsEmu.DTOs.Titles; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage.Configuration; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions.Mates; +using WingsEmu.Game.Groups; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Inventory.Event; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Maps.Event; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Mates.Events; +using WingsEmu.Game.Miniland; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Networking.Broadcasting; +using WingsEmu.Game.RespawnReturn.Event; +using WingsEmu.Game.Skills; +using WingsEmu.Game.Warehouse; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Battle; +using WingsEmu.Packets.Enums.Character; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Game.Extensions; + +public enum VisualRankType +{ + User = 0, + GameMaster = 2, + Admin = 3, + Developer = 6, + CommunityManager = 7 +} + +public static class CharacterExtension +{ + public static bool IsActionForbidden(this IClientSession session) + { + if (session.Account.Authority == AuthorityType.GameMaster) + { + session.SendMsg("As a Game Master, you can't do that.", MsgMessageType.Middle); + return true; + } + + return false; + } + + public static bool IsGameMaster(this IClientSession session) => session.Account.Authority >= AuthorityType.GameMaster; + + public static byte IngameVisualRank(this IClientSession session) + { + switch (session.Account.Authority) + { + case AuthorityType.GameMaster: + case AuthorityType.SuperGameMaster: + return (byte)VisualRankType.GameMaster; + case AuthorityType.CommunityManager: + return (byte)VisualRankType.CommunityManager; + case AuthorityType.GameAdmin: + return (byte)VisualRankType.Developer; + case AuthorityType.Owner: + case AuthorityType.Root: + return (byte)VisualRankType.Admin; + default: + return 0; + } + } + + public static bool IsInGroupOf(this IPlayerEntity character, IPlayerEntity target) + { + if (!character.IsInGroup()) + { + return false; + } + + if (!target.IsInGroup()) + { + return false; + } + + PlayerGroup characterGroup = character.GetGroup(); + PlayerGroup targetGroup = target.GetGroup(); + return characterGroup.GroupId == targetGroup.GroupId; + } + + public static string CharacterName(this IClientSession session) => session.PlayerEntity.Name; + + public static bool IsUsingFairyBooster(this IPlayerEntity character) => character.HasBuff(BuffVnums.FAIRY_BOOSTER); + + public static bool CanReceiveMate(this IPlayerEntity character, MateType type) => + type == MateType.Pet + ? character.MaxPetCount > character.MateComponent.GetMates(s => s.MateType == MateType.Pet).Count + : character.MaxPartnerCount > character.MateComponent.GetMates(s => s.MateType == MateType.Partner).Count; + + public static int GetSpCooldown(this IPlayerEntity character) => character.SpCooldownEnd == null ? 0 : (int)(character.SpCooldownEnd.Value - DateTime.UtcNow).TotalSeconds; + + public static bool IsSpCooldownElapsed(this IPlayerEntity character) + { + if (!character.SpCooldownEnd.HasValue) + { + return true; + } + + return character.SpCooldownEnd.Value < DateTime.UtcNow; + } + + public static void RemovePetBuffs(this IClientSession session, IMateEntity mateEntity, IMateBuffConfigsContainer buffConfigs) + { + MateBuffIndividualConfig buffs = buffConfigs.GetMateBuffInfo(mateEntity.NpcMonsterVNum); + if (buffs != null) + { + foreach (int buffId in buffs.BuffIds) + { + Buff buff = session.PlayerEntity.BuffComponent.GetBuff(buffId); + if (buff == null) + { + continue; + } + + session.PlayerEntity.RemoveBuffAsync(true, buff).ConfigureAwait(false).GetAwaiter().GetResult(); + } + } + + if (mateEntity.MateType != MateType.Pet) + { + return; + } + + session.SendMateSkillPacket(mateEntity); + } + + public static async Task AddPetBuff(this IClientSession session, IMateEntity mateEntity, IMateBuffConfigsContainer buffConfigs, IBuffFactory buffFactory) + { + MateBuffIndividualConfig buffs = buffConfigs.GetMateBuffInfo(mateEntity.NpcMonsterVNum); + if (buffs != null) + { + foreach (int cardId in buffs.BuffIds) + { + Buff removeBuff = session.PlayerEntity.BuffComponent.GetBuff(cardId); + await session.PlayerEntity.RemoveBuffAsync(true, removeBuff); + await session.PlayerEntity.AddBuffAsync(buffFactory.CreateBuff(cardId, mateEntity, BuffFlag.PARTNER)); + } + } + + session.SendMateSkillPacket(mateEntity); + } + + public static void ChangeMap(this IClientSession session, IMapInstance mapInstance, short? x = null, short? y = null) + { + session.EmitEvent(new JoinMapEvent(mapInstance, x, y)); + } + + public static void ChangeMap(this IClientSession session, Guid mapInstanceGuid, short? x = null, short? y = null) + { + session.EmitEvent(new JoinMapEvent(mapInstanceGuid, x, y)); + } + + public static void ChangeMap(this IClientSession session, int mapInstanceId, short? x = null, short? y = null) + { + session.EmitEvent(new JoinMapEvent(mapInstanceId, x, y)); + } + + public static void ChangeToLastBaseMap(this IClientSession session) + { + session.EmitEvent(new JoinMapEvent(session.PlayerEntity.MapId, session.PlayerEntity.MapX, session.PlayerEntity.MapY)); + } + + public static void CharacterInvisible(this IPlayerEntity character, bool triggerAmbush = false) + { + character.TriggerAmbush = triggerAmbush; + foreach (IMateEntity mate in character.MateComponent.TeamMembers()) + { + if (!character.Session.HasCurrentMapInstance) + { + continue; + } + + character.Session.CurrentMapInstance.Broadcast(mate.GenerateOut()); + } + } + + public static void RefreshPassiveBCards(this IClientSession session) + { + session.PlayerEntity.StatisticsComponent.RefreshPassives(); + session.RefreshStatChar(); + session.RefreshStat(); + session.SendCondPacket(); + } + + public static void RefreshTitleBCards(this IPlayerEntity character, IItemsManager itemsManager, CharacterTitleDto title, IBCardEffectHandlerContainer bCardHandler, bool removeBCards = false) + { + IGameItem item = itemsManager.GetItem(title.ItemVnum); + if (item == null) + { + return; + } + + foreach (BCardDTO bCard in item.BCards) + { + if (removeBCards) + { + character.BCardComponent.RemoveBCard(bCard); + continue; + } + + character.BCardComponent.AddBCard(bCard); + bCardHandler.Execute(character, character, bCard); + } + } + + public static void RefreshEquipmentValues(this IPlayerEntity session, GameItemInstance itemInstance, bool cleanValues = false) + { + EquipmentType equipmentType = itemInstance.GameItem.EquipmentSlot; + + if (cleanValues) + { + switch (equipmentType) + { + case EquipmentType.Armor: + case EquipmentType.MainWeapon: + case EquipmentType.SecondaryWeapon: + session.ClearShells(equipmentType == EquipmentType.Armor ? EquipmentOptionType.ARMOR_SHELL : EquipmentOptionType.WEAPON_SHELL, + equipmentType == EquipmentType.MainWeapon); + session.BCardComponent.ClearEquipmentBCards(equipmentType); + + if (equipmentType != EquipmentType.Armor) + { + session.BCardComponent.ClearShellTrigger(equipmentType == EquipmentType.MainWeapon); + } + + session.SpecialistComponent.RefreshSlStats(); + break; + case EquipmentType.Ring: + case EquipmentType.Necklace: + case EquipmentType.Bracelet: + session.BCardComponent.ClearEquipmentBCards(itemInstance.GameItem.EquipmentSlot); + session.ClearCellon(equipmentType); + break; + default: + session.BCardComponent.ClearEquipmentBCards(itemInstance.GameItem.EquipmentSlot); + break; + } + + return; + } + + switch (equipmentType) + { + case EquipmentType.Armor: + case EquipmentType.MainWeapon: + case EquipmentType.SecondaryWeapon: + session.AddShells(equipmentType == EquipmentType.Armor ? EquipmentOptionType.ARMOR_SHELL : EquipmentOptionType.WEAPON_SHELL, + itemInstance.EquipmentOptions, equipmentType == EquipmentType.MainWeapon); + + session.BCardComponent.AddEquipmentBCards(equipmentType, itemInstance.GameItem.BCards); + + if (equipmentType != EquipmentType.Armor) + { + session.TryAddShellBuffs(itemInstance); + } + + session.SpecialistComponent.RefreshSlStats(); + break; + case EquipmentType.Ring: + case EquipmentType.Necklace: + case EquipmentType.Bracelet: + session.BCardComponent.AddEquipmentBCards(equipmentType, itemInstance.GameItem.BCards); + session.AddCellon(equipmentType, itemInstance.EquipmentOptions); + break; + case EquipmentType.Sp: + if (!session.UseSp) + { + return; + } + + session.BCardComponent.AddEquipmentBCards(equipmentType, itemInstance.GameItem.BCards); + break; + default: + session.BCardComponent.AddEquipmentBCards(equipmentType, itemInstance.GameItem.BCards); + break; + } + } + + public static bool CanFight(this IPlayerEntity character) => !character.IsSitting && !character.IsInExchange(); + + public static int GetTopReputationPlace(this IPlayerEntity character, IReadOnlyList topReputations) + { + for (int i = 0; i < topReputations.Count; i++) + { + if (topReputations[i].Id != character.Id) + { + continue; + } + + return i + 1; + } + + return 0; + } + + public static BankRankType GetBankRank(this IPlayerEntity character, IReputationConfiguration reputationConfiguration, IBankReputationConfiguration bankReputationConfiguration, + IReadOnlyList topReputation) + { + if (character.HaveStaticBonus(StaticBonusType.CuarryBankMedal)) + { + return BankRankType.VIP; + } + + ReputationType reputationType = character.GetReputationIcon(reputationConfiguration, topReputation); + BankRankInfo bankRankInfo = bankReputationConfiguration.GetBankRankInfo(reputationType); + return bankRankInfo?.BankRank ?? 0; + } + + public static int GetBankPenalty(this IPlayerEntity character, IReputationConfiguration reputationConfiguration, IBankReputationConfiguration bankReputationConfiguration, + IReadOnlyList topReputation) + { + if (character.HaveStaticBonus(StaticBonusType.CuarryBankMedal)) + { + return 0; + } + + ReputationType reputationType = character.GetReputationIcon(reputationConfiguration, topReputation); + BankPenaltyInfo bankPenaltyInfo = bankReputationConfiguration.GetBankPenaltyInfo(reputationType); + return bankPenaltyInfo?.GoldCost ?? 0; + } + + public static ReputationType GetReputationIcon(this IPlayerEntity character, IReputationConfiguration reputationConfiguration, IReadOnlyList topReputation) + { + ReputationInfo reputationInfo = reputationConfiguration.GetReputationInfo(character.Reput, character.GetTopReputationPlace(topReputation)); + return reputationInfo?.Rank ?? 0; + } + + public static void SendTargetEq(this IClientSession session, IPlayerEntity target) + { + string inv0 = "inv 0", + inv1 = "inv 1", + inv2 = "inv 2", + inv3 = "inv 3", + inv6 = "inv 6", + inv7 = "inv 7"; // inv 3 used for miniland objects + + foreach (InventoryItem invItem in target.GetAllPlayerInventoryItems().OrderBy(x => x.Slot)) + { + GameItemInstance inv = invItem.ItemInstance; + switch (invItem.InventoryType) + { + case InventoryType.Equipment: + if (inv.GameItem.EquipmentSlot == EquipmentType.Sp) + { + if (inv.Type != ItemInstanceType.SpecialistInstance) + { + continue; + } + + inv0 += $" {invItem.Slot}.{inv.ItemVNum}.{inv.Rarity}.{inv.Upgrade}.{inv.SpStoneUpgrade}"; + } + else + { + inv0 += + $" {invItem.Slot}.{inv.ItemVNum}.{inv.Rarity}.{(inv.GameItem.IsColorable ? inv.Design : inv.Upgrade)}.0.{inv.GetRunesCount()}"; + } + + break; + + case InventoryType.Main: + inv1 += $" {invItem.Slot}.{inv.ItemVNum}.{inv.Amount}.0"; + break; + + case InventoryType.Etc: + inv2 += $" {invItem.Slot}.{inv.ItemVNum}.{inv.Amount}.0"; + break; + + case InventoryType.Miniland: + inv3 += $" {invItem.Slot}.{inv.ItemVNum}.{inv.Amount}"; + break; + + case InventoryType.Specialist: + if (inv.Type != ItemInstanceType.SpecialistInstance) + { + continue; + } + + inv6 += $" {invItem.Slot}.{inv.ItemVNum}.{inv.Rarity}.{inv.Upgrade}.{inv.SpStoneUpgrade}"; + + break; + + case InventoryType.Costume: + if (inv.Type != ItemInstanceType.WearableInstance) + { + continue; + } + + inv7 += $" {invItem.Slot}.{inv.ItemVNum}.{inv.Rarity}.{inv.Upgrade}.0"; + + break; + } + } + + session.SendPacket(inv0); + session.SendPacket(inv1); + session.SendPacket(inv2); + session.SendPacket(inv3); + session.SendPacket(inv6); + session.SendPacket(inv7); + session.SendPacket(session.GenerateMlObjListPacket()); + } + + public static bool IsMuted(this IClientSession session) => session.PlayerEntity.MuteRemainingTime.HasValue; + + public static void SendMuteMessage(this IClientSession session) + { + if (!session.PlayerEntity.MuteRemainingTime.HasValue) + { + return; + } + + TimeSpan? remainingTime = session.PlayerEntity.MuteRemainingTime; + GameDialogKey messageType = session.PlayerEntity.Gender == GenderType.Female ? GameDialogKey.MUTE_MESSAGE_FEMALE : GameDialogKey.MUTE_MESSAGE_MALE; + + session.CurrentMapInstance?.Broadcast(s => + session.GenerateSayPacket(s.GetLanguage(messageType), ChatMessageColorType.PlayerSay)); + + string timeLeft = remainingTime.Value.ToString(@"hh\:mm\:ss"); + session.SendChatMessage(session.GetLanguageFormat(GameDialogKey.MUTE_CHATMESSAGE_TIME_LEFT, timeLeft), ChatMessageColorType.Red); + } + + public static void SendCharConstBuffEffect(this IClientSession session) + { + IReadOnlyList buffs = session.PlayerEntity.BuffComponent.GetAllBuffs(b => b.IsConstEffect); + foreach (Buff buff in buffs) + { + session.PlayerEntity.BroadcastConstBuffEffect(buff, (int)buff.Duration.TotalMilliseconds); + } + } + + public static void NotifyRarifyResult(this IClientSession session, IGameLanguageService gameLanguage, short rare) + { + session.SendChatMessage(gameLanguage.GetLanguageFormat(GameDialogKey.GAMBLING_MESSAGE_SUCCESS, session.UserLanguage, rare), ChatMessageColorType.Green); + session.SendMsg(gameLanguage.GetLanguageFormat(GameDialogKey.GAMBLING_MESSAGE_SUCCESS, session.UserLanguage, rare), MsgMessageType.Middle); + session.BroadcastEffect(3005, new RangeBroadcast(session.PlayerEntity.PositionX, session.PlayerEntity.PositionY)); + session.SendShopEndPacket(ShopEndType.Npc); + } + + public static async Task Respawn(this IClientSession session) + { + await session.EmitEventAsync(new RespawnPlayerEvent()); + } + + /// + /// Returns True if the gold was removed or False if it wasn't. + /// + /// + /// + /// + public static bool RemoveGold(this IPlayerEntity character, long gold) + { + long goldToRemove = Math.Abs(gold); + if (goldToRemove > character.Gold) + { + return false; + } + + character.Gold -= goldToRemove; + character.Session.RefreshGold(); + return true; + } + + /// + /// Returns True if the dignity was added or False if it wasn't. + /// + /// + /// + /// + /// + public static bool AddDignity(this IPlayerEntity character, float dignity, GameMinMaxConfiguration minMaxConfiguration, IGameLanguageService languageService, + IReputationConfiguration reputationConfiguration, IReadOnlyList topReputation) + { + if (character.Dignity >= minMaxConfiguration.MaxDignity) + { + return false; + } + + float oldDignity = character.Dignity; + character.Dignity += Math.Abs(dignity); + + if (character.Dignity > minMaxConfiguration.MaxDignity) + { + character.Dignity = minMaxConfiguration.MaxDignity; + } + + character.Session.RefreshReputation(reputationConfiguration, topReputation); + character.Session.SendChatMessage(languageService.GetLanguageFormat(GameDialogKey.DIGNITY_CHATMESSAGE_RESTORE, character.Session.UserLanguage, character.Dignity - oldDignity), + ChatMessageColorType.Green); + return true; + } + + public static async Task RemoveDignity(this IPlayerEntity character, float dignity, GameMinMaxConfiguration minMaxConfiguration, IGameLanguageService languageService, + IReputationConfiguration reputationConfiguration, IReadOnlyList topReputation) + { + if (character.Dignity < minMaxConfiguration.MinDignity) + { + character.Dignity = minMaxConfiguration.MinDignity; + return; + } + + float oldDignity = character.Dignity; + character.Dignity -= Math.Abs(dignity); + + if (character.Dignity < minMaxConfiguration.MinDignity) + { + character.Dignity = minMaxConfiguration.MinDignity; + } + + character.Session.RefreshReputation(reputationConfiguration, topReputation); + character.Session.SendChatMessage(languageService.GetLanguageFormat(GameDialogKey.DIGNITY_CHATMESSAGE_LOSS, character.Session.UserLanguage, oldDignity - character.Dignity), + ChatMessageColorType.Red); + if (character.GetDignityIco() == 6) + { + foreach (IMateEntity teamMember in character.MateComponent.TeamMembers()) + { + await character.Session.EmitEventAsync(new MateLeaveTeamEvent + { + MateEntity = teamMember + }); + + if (teamMember.IsAlive()) + { + continue; + } + + teamMember.Hp = 1; + teamMember.Mp = 1; + } + + character.Session.SendMsg(character.Session.GetLanguage(GameDialogKey.PET_SHOUTMESSAGE_DIGNITY_LOW), MsgMessageType.Middle); + } + } + + /// + /// Returns true if the reputation was removed. + /// + /// + /// + /// + public static bool RemoveReputation(this IPlayerEntity character, long reputation, bool sendMessage = true) + { + long value = Math.Abs(reputation); + if (character.Reput < value) + { + return false; + } + + character.Session.EmitEventAsync(new GenerateReputationEvent + { + Amount = -value, + SendMessage = sendMessage + }); + return true; + } + + public static async Task Restore(this IPlayerEntity character, bool restorePlayer = true, bool restoreMates = true, + bool restoreHealth = true, bool restoreMana = true, bool removeBuffs = true) + { + var entitiesToHeal = new List(); + + if (restorePlayer) + { + entitiesToHeal.Add(character); + } + + if (restoreMates) + { + entitiesToHeal.AddRange(character.MateComponent.TeamMembers()); + } + + foreach (IBattleEntity entity in entitiesToHeal) + { + if (restoreHealth) + { + entity.Hp = entity.MaxHp; + } + + if (restoreMana) + { + entity.Mp = entity.MaxMp; + } + + if (removeBuffs) + { + await entity.RemoveBuffsOnDeathAsync(); + } + } + + if (restorePlayer) + { + character.Session.RefreshStat(); + character.Session.RefreshStatInfo(); + character.Session.SendCondPacket(); + } + + if (restoreMates) + { + character.Session.RefreshMateStats(); + } + } + + public static bool IsNotStackableInventoryType(this IGameItem gameItem) + { + InventoryType inventoryType = gameItem.Type; + return inventoryType is InventoryType.Costume or InventoryType.Equipment or InventoryType.Specialist or InventoryType.Miniland; + } + + public static bool HaveStaticBonus(this IPlayerEntity playerEntity, StaticBonusType bonusType) => playerEntity.Bonus.Any(x => x.StaticBonusType == bonusType); + + public static short GetInventorySlots(this IPlayerEntity character, bool removeInventory = false, InventoryType? inventoryType = null) + { + byte size = 48; + bool hasBackPack = character.HaveStaticBonus(StaticBonusType.Backpack); + bool hasInventoryExtension = character.HaveStaticBonus(StaticBonusType.InventoryExpansion); + + if (hasBackPack) + { + size += 12; + } + + if (hasInventoryExtension) + { + size += 60; + } + + switch (inventoryType) + { + case null: + break; + case InventoryType.Etc: + size += 36; + break; + case InventoryType.Specialist: + size = 45; + break; + case InventoryType.Costume: + size = 60; + break; + case InventoryType.Miniland: + size = 50; + break; + } + + return (short)(removeInventory ? size - 1 : size); + } + + public static bool HaveSlotInSpecialInventoryType(this IPlayerEntity character, InventoryType type) + { + short slots = character.GetInventorySlots(false, type); + for (short i = 0; i < slots; i++) + { + InventoryItem item = character.GetItemBySlotAndType(i, type); + if (item?.ItemInstance == null) + { + return true; + } + } + + return false; + } + + public static short GetNextInventorySlot(this IPlayerEntity character, InventoryType type) + { + short slot = 0; + switch (type) + { + case InventoryType.Costume: + case InventoryType.Specialist: + IOrderedEnumerable items = character.GetItemsByInventoryType(type).OrderBy(x => x?.ItemInstance.ItemVNum); + for (short i = 0; i < items.Count(); i++) + { + InventoryItem item = items.ElementAt(i); + if (item == null) + { + continue; + } + + if (item.Slot != i) + { + character.Session.SendInventoryRemovePacket(item); + } + + item.Slot = i; + } + + break; + default: + for (slot = 0; slot < character.GetInventorySlots(inventoryType: type); slot++) + { + InventoryItem anotherItem = character.GetItemBySlotAndType(slot, type); + if (anotherItem != null) + { + continue; + } + + break; + } + + break; + } + + return slot; + } + + public static byte? GetNextPartnerWarehouseSlot(this IPlayerEntity playerEntity) + { + byte? slot = null; + + for (byte warehouseSlot = 0; warehouseSlot < playerEntity.GetPartnerWarehouseSlotsWithoutBackpack(); warehouseSlot++) + { + PartnerWarehouseItem anotherItem = playerEntity.GetPartnerWarehouseItem(warehouseSlot); + if (anotherItem != null) + { + continue; + } + + slot = warehouseSlot; + break; + } + + return slot; + } + + public static bool WeaponLoaded(this IPlayerEntity character, IBattleEntitySkill ski, IGameLanguageService gameLanguage, bool removeAmmo) + { + if (ski == null) + { + return false; + } + + GameItemInstance inv; + switch (character.Class) + { + default: + return false; + + case ClassType.Adventurer: + if (!ski.Skill.IsUsingSecondWeapon) + { + return true; + } + + inv = character.SecondaryWeapon; + if (inv == null) + { + character.Session.SendMsg(gameLanguage.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_NO_WEAPON, character.Session.UserLanguage), MsgMessageType.Middle); + return false; + } + + if (character.CountItemWithVnum((short)ItemVnums.AMMO_ADVENTURER) < 1 && inv.Ammo == 0) + { + character.Session.SendMsg(gameLanguage.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_NO_AMMO_ADVENTURER, character.Session.UserLanguage), MsgMessageType.Middle); + return false; + } + + if (!removeAmmo) + { + return true; + } + + if (inv.Ammo > 0) + { + inv.Ammo--; + return true; + } + + character.Session.EmitEvent(new InventoryRemoveItemEvent((short)ItemVnums.AMMO_ADVENTURER)); + inv.Ammo = 100; + character.Session.SendChatMessage(gameLanguage.GetLanguage(GameDialogKey.AMMO_LOADED_ADVENTURER, character.Session.UserLanguage), ChatMessageColorType.Yellow); + + return true; + + + case ClassType.Swordman: + if (!ski.Skill.IsUsingSecondWeapon) + { + return true; + } + + inv = character.SecondaryWeapon; + if (inv == null) + { + character.Session.SendMsg(gameLanguage.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_NO_WEAPON, character.Session.UserLanguage), MsgMessageType.Middle); + return false; + } + + if (character.CountItemWithVnum((short)ItemVnums.AMMO_SWORDSMAN) < 1 && inv.Ammo == 0) + { + character.Session.SendMsg(gameLanguage.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_NO_AMMO_SWORDSMAN, character.Session.UserLanguage), MsgMessageType.Middle); + return false; + } + + if (!removeAmmo) + { + return true; + } + + if (inv.Ammo > 0) + { + inv.Ammo--; + return true; + } + + character.Session.EmitEvent(new InventoryRemoveItemEvent((short)ItemVnums.AMMO_SWORDSMAN)); + inv.Ammo = 100; + character.Session.SendChatMessage(gameLanguage.GetLanguage(GameDialogKey.AMMO_LOADED_SWORDSMAN, character.Session.UserLanguage), ChatMessageColorType.Yellow); + + return true; + + case ClassType.Archer: + if (ski.Skill.AttackType != AttackType.Ranged) + { + return true; + } + + inv = character.MainWeapon; + + if (inv == null) + { + character.Session.SendMsg(gameLanguage.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_NO_WEAPON, character.Session.UserLanguage), MsgMessageType.Middle); + return false; + } + + if (character.CountItemWithVnum((short)ItemVnums.AMMO_ARCHER) < 1 && inv.Ammo == 0) + { + character.Session.SendMsg(gameLanguage.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_NO_AMMO_ARCHER, character.Session.UserLanguage), MsgMessageType.Middle); + return false; + } + + if (!removeAmmo) + { + return true; + } + + if (inv.Ammo > 0) + { + inv.Ammo--; + return true; + } + + character.Session.EmitEvent(new InventoryRemoveItemEvent((short)ItemVnums.AMMO_ARCHER)); + inv.Ammo = 100; + character.Session.SendChatMessage(gameLanguage.GetLanguage(GameDialogKey.AMMO_LOADED_ARCHER, character.Session.UserLanguage), ChatMessageColorType.Yellow); + + return true; + + case ClassType.Magician: + if (!ski.Skill.IsUsingSecondWeapon) + { + return true; + } + + inv = character.SecondaryWeapon; + if (inv != null) + { + return true; + } + + character.Session.SendMsg(gameLanguage.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_NO_WEAPON, character.Session.UserLanguage), MsgMessageType.Middle); + return false; + + case ClassType.Wrestler: + return true; + } + } + + public static void SendStartStartupInventory(this IClientSession session) + { + string inv0 = "inv 0", + inv1 = "inv 1", + inv2 = "inv 2", + inv3 = "inv 3", + inv6 = "inv 6", + inv7 = "inv 7"; // inv 3 used for miniland objects + + foreach (InventoryItem invItem in session.PlayerEntity.GetAllPlayerInventoryItems().OrderBy(x => x.Slot)) + { + GameItemInstance inv = invItem.ItemInstance; + switch (invItem.InventoryType) + { + case InventoryType.Equipment: + if (inv.GameItem.EquipmentSlot == EquipmentType.Sp) + { + if (inv.Type != ItemInstanceType.SpecialistInstance) + { + continue; + } + + inv0 += $" {invItem.Slot}.{inv.ItemVNum}.{inv.Rarity}.{inv.Upgrade}.{inv.SpStoneUpgrade}"; + } + else + { + if (inv.GameItem.ItemSubType == 7) + { + inv0 += $" {invItem.Slot}.{inv.ItemVNum}.{inv.Rarity}.{(inv.GameItem.IsColorable ? inv.Design : inv.Upgrade)}.{(inv.IsBound ? 1 : 0)}.{inv.GetRunesCount()}"; + break; + } + + inv0 += $" {invItem.Slot}.{inv.ItemVNum}.{inv.Rarity}.{(inv.GameItem.IsColorable ? inv.Design : inv.Upgrade)}.0.{inv.GetRunesCount()}"; + } + + break; + + case InventoryType.Main: + inv1 += $" {invItem.Slot}.{inv.ItemVNum}.{inv.Amount}.0"; + break; + + case InventoryType.Etc: + inv2 += $" {invItem.Slot}.{inv.ItemVNum}.{inv.Amount}.0"; + break; + + case InventoryType.Miniland: + inv3 += $" {invItem.Slot}.{inv.ItemVNum}.{inv.Amount}"; + break; + + case InventoryType.Specialist: + if (inv.Type != ItemInstanceType.SpecialistInstance) + { + continue; + } + + inv6 += $" {invItem.Slot}.{inv.ItemVNum}.{inv.Rarity}.{inv.Upgrade}.{inv.SpStoneUpgrade}"; + + break; + + case InventoryType.Costume: + if (inv.Type != ItemInstanceType.WearableInstance) + { + continue; + } + + inv7 += $" {invItem.Slot}.{inv.ItemVNum}.{inv.Rarity}.{inv.Upgrade}.0"; + + break; + } + } + + session.SendPacket(inv0); + session.SendPacket(inv1); + session.SendPacket(inv2); + session.SendPacket(inv3); + session.SendPacket(inv6); + session.SendPacket(inv7); + session.SendPacket(session.GenerateMlObjListPacket()); + } + + public static string GenerateMlObjListPacket(this IClientSession session) + { + string mlobjstring = "mlobjlst"; + foreach (InventoryItem invItem in session.PlayerEntity.GetAllPlayerInventoryItems() + .Where(s => s.InventoryType == InventoryType.Miniland) + .OrderBy(s => s.Slot)) + { + GameItemInstance item = invItem.ItemInstance; + MapDesignObject mp = session.PlayerEntity.MapInstance?.MapDesignObjects.FirstOrDefault(s => s.InventorySlot == invItem.Slot); + bool used = mp != null; + + if (item.GameItem.IsWarehouse && used) + { + session.PlayerEntity.WareHouseSize = item.GameItem.MinilandObjectPoint; + } + + mlobjstring += + $" {invItem.Slot}.{(used ? 1 : 0)}.{(used ? mp.MapX : 0)}.{(used ? mp.MapY : 0)}.{(item.GameItem.Width != 0 ? item.GameItem.Width : 1)}.{(item.GameItem.Height != 0 ? item.GameItem.Height : 1)}.{(used ? mp.InventoryItem.ItemInstance.DurabilityPoint : 0)}.100.0.1"; + } + + return mlobjstring; + } + + public static bool IsInAct5(this IClientSession session) + { + if (session?.CurrentMapInstance == null) + { + return false; + } + + return session.CurrentMapInstance.HasMapFlag(MapFlags.ACT_5_1) || session.CurrentMapInstance.HasMapFlag(MapFlags.ACT_5_2); + } + + public static bool CantPerformActionOnAct4(this IClientSession session) + { + if (!session.CurrentMapInstance.HasMapFlag(MapFlags.ACT_4)) + { + return false; + } + + if (session.PlayerEntity.IsSeal) + { + return true; + } + + session.SendMsg(session.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_MUST_BE_IN_CLASSIC_MAP), MsgMessageType.Middle); + return true; + } + + public static void BroadcastInTeamMembers(this IClientSession session, IGameLanguageService gameLanguageService, ISpPartnerConfiguration spPartner) + { + foreach (IMateEntity mate in session.PlayerEntity.MateComponent.TeamMembers()) + { + if (!mate.IsAlive()) + { + mate.Position = session.PlayerEntity.Position; + continue; + } + + mate.TeleportNearCharacter(); + session.SendCondMate(mate); + session.PlayerEntity.MapInstance.Broadcast(x => + { + bool isAnonymous = session.CurrentMapInstance.HasMapFlag(MapFlags.ACT_4) && x.PlayerEntity.Faction != session.PlayerEntity.Faction; + string inPacket = mate.GenerateIn(gameLanguageService, x.UserLanguage, spPartner, isAnonymous); + return inPacket; + }); + } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Extensions/CharacterPacketExtension.cs b/srcs/WingsAPI.Game/Extensions/CharacterPacketExtension.cs new file mode 100644 index 0000000..f871b32 --- /dev/null +++ b/srcs/WingsAPI.Game/Extensions/CharacterPacketExtension.cs @@ -0,0 +1,982 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using WingsAPI.Data.Character; +using WingsAPI.Packets.Enums; +using WingsEmu.DTOs.Bonus; +using WingsEmu.DTOs.Maps; +using WingsEmu.DTOs.Titles; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Algorithm; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Configurations.Miniland; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions.Mates; +using WingsEmu.Game.Families; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Networking.Broadcasting; +using WingsEmu.Game.Revival; +using WingsEmu.Game.Skills; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Character; +using WingsEmu.Packets.Enums.Families; +using WingsEmu.Packets.Enums.Mails; +using WingsEmu.Packets.Enums.Titles; +using WingsEmu.Packets.ServerPackets; +using WingsEmu.Packets.ServerPackets.Titles; + +namespace WingsEmu.Game.Extensions; + +public static class CharacterPacketExtension +{ + private static readonly HashSet FairyMorphsWithoutBoostChange = new() + { + 0, //Normal + 4, // Solaris + 9, // Azuris + 14, // Magmaris + 15 // Vacuris + }; + + private static IGameLanguageService GameLanguage => StaticGameLanguageService.Instance; + + # region Send Packets + + public static void RefreshStat(this IClientSession session, bool isTransform = false) => session.SendPacket(session.GenerateStatPacket(isTransform)); + public static void RefreshStatInfo(this IClientSession session) => session.SendPacket(session.GenerateStatInfoPacket()); + public static void RefreshStatChar(this IClientSession session, bool refreshHpMp = true) => session.SendPacket(session.GenerateStatCharPacket(refreshHpMp)); + public static void RefreshGold(this IClientSession session) => session.SendPacket(session.GenerateGoldPacket()); + + public static void RefreshReputation(this IClientSession session, IReputationConfiguration reputationConfiguration, IReadOnlyList topReputation) + => session.SendPacket(session.GenerateFdPacket(reputationConfiguration, topReputation)); + + public static void RefreshLevel(this IClientSession session, ICharacterAlgorithm characterAlgorithm) => session.SendPacket(session.GenerateLevPacket(characterAlgorithm)); + public static void SendTitlePacket(this IClientSession session) => session.SendPacket(session.GenerateTitlePacket()); + public static void SendCondPacket(this IClientSession session) => session.SendPacket(session.GenerateCondPacket()); + + public static void SendTeleportPacket(this IClientSession session) => + session.SendPacket(session.PlayerEntity.GenerateTeleportPacket(session.PlayerEntity.PositionX, session.PlayerEntity.PositionY)); + + public static void SendMatesScPacket(this IClientSession session, IGameLanguageService languageService) + { + session.SendPackets(session.PlayerEntity.MateComponent.GetMates().Select(s => s.GenerateScPacket(languageService, session.UserLanguage))); + } + + public static void SendFcPacket(this IClientSession session, Act4Status act4Status) => session.SendPacket(UiPacketExtension.GenerateFcPacket(session.PlayerEntity.Faction, act4Status)); + public static void SendTargetTitInfoPacket(this IClientSession session, IClientSession target) => session.SendPacket(target.GenerateTitInfoPacket()); + public static void SendTitInfoPacket(this IClientSession session) => session.SendPacket(session.GenerateTitInfoPacket()); + public static void SendEffect(this IClientSession session, EffectType effectType) => session.SendPacket(session.GenerateEffectPacket(effectType)); + public static void SendMateEffect(this IClientSession session, IMateEntity entity, EffectType effectType) => session.SendPacket(entity.GenerateEffectPacket(effectType)); + public static void SendNpcEffect(this IClientSession session, INpcEntity entity, EffectType effectType) => session.SendPacket(entity.GenerateEffectPacket(effectType)); + public static void RefreshFaction(this IClientSession session) => session.SendPacket(session.GenerateFactionPacket()); + public static void ShowInventoryExtensions(this IClientSession session) => session.SendPacket(session.GenerateExtsPacket()); + public static void SendLevelUp(this IClientSession session) => session.SendPacket(session.GenerateLevelUpPacket()); + public static void SendCModePacket(this IClientSession session) => session.SendPacket(session.GenerateCModePacket()); + public static void SendEqPacket(this IClientSession session) => session.SendPacket(session.GenerateEqPacket()); + public static void RefreshEquipment(this IClientSession session) => session.SendPacket(session.GenerateEquipPacket()); + public static void SendInventoryRemovePacket(this IClientSession session, InventoryType type, int slot) => session.SendPacket(session.GenerateInventoryRemovePacket(type, slot)); + + public static void RefreshInventorySlot(this IClientSession session, short slot, InventoryType type) + { + InventoryItem inventory = session.PlayerEntity.GetItemBySlotAndType(slot, type); + if (inventory == null) + { + return; + } + + session.SendInventoryAddPacket(inventory); + } + + public static void SendInventoryAddPacket(this IClientSession session, InventoryItem itemInstance) + { + if (itemInstance == null) + { + return; + } + + if (itemInstance.InventoryType == InventoryType.EquippedItems) + { + return; + } + + session.SendPacket(itemInstance.GenerateInventoryAdd()); + } + + public static void SendInventoryRemovePacket(this IClientSession session, InventoryItem itemInstance) + { + if (itemInstance?.InventoryType == null) + { + return; + } + + if (itemInstance.InventoryType == InventoryType.EquippedItems) + { + return; + } + + session.SendPacket(itemInstance.GenerateInventoryRemove()); + } + + public static void SendAtPacket(this IClientSession session) => session.SendPacket(session.GenerateAtPacket()); + public static void SendCMapPacket(this IClientSession session, bool isEntering) => session.SendPacket(session.GenerateCMapPacket(isEntering)); + + public static void SendCInfoPacket(this IClientSession session, IFamily family, IReputationConfiguration reputationConfiguration, IReadOnlyList topReputation) => + session.SendPacket(session.GenerateCInfoPacket(family, reputationConfiguration, topReputation)); + + public static void SendDelay(this IClientSession session, int delay, GuriType type, string argument) => session.SendPacket(session.GenerateDelayPacket(delay, type, argument)); + public static void RefreshMateStats(this IClientSession session) => session.SendPackets(session.GeneratePstPackets()); + public static void RefreshFairy(this IClientSession session) => session.SendPacket(session.GeneratePairyPacket()); + public static void SendMapOutPacket(this IClientSession session) => session.SendPacket(session.GenerateMapOutPacket()); + public static void SendScnPackets(this IClientSession session) => session.SendPackets(session.GenerateScnPackets()); + public static void SendScpPackets(this IClientSession session, byte? page = null) => session.SendPackets(session.GenerateScpPackets(page)); + public static void SendPClearPacket(this IClientSession session) => session.SendPacket(session.GeneratePClearPacket()); + public static void SendScpStcPacket(this IClientSession session) => session.SendPacket(session.GenerateScpStcPacket()); + public static void SendRdiPacket(this IClientSession session, int vnum, short amount) => session.SendPacket(session.GenerateRdiPacket(vnum, amount)); + + public static void RefreshSkillList(this IClientSession session) + { + session.SendPacket(session.GenerateSkillListPacket()); + } + + public static void SendBfPacket(this IClientSession session, Buff buff, int time, int charge = 0) => session.SendPacket(session.GenerateBfPacket(buff, time, charge)); + public static void SendBfLeftPacket(this IClientSession session, Buff buff) => session.SendPacket(session.GenerateBfLeftPacket(buff)); + public static void SendStaticBuffUiPacket(this IClientSession session, Buff buff, int time) => session.SendPacket(session.GenerateStaticBuffPacket(buff, time, true)); + public static void SendEmptyStaticBuffUiPacket(this IClientSession session, Buff buff) => session.SendPacket(session.GenerateStaticBuffPacket(buff, 0, false)); + public static void UpdateVisibility(this IClientSession session) => session.Broadcast(session.GenerateClPacket()); + public static void SendBfPacket(this IClientSession session, Buff buff) => session.SendPacket(session.GenerateBfPacket(buff)); + public static void SendWopenPacket(this IClientSession session, WindowType type) => session.SendPacket(session.GenerateWopenPacket(type)); + public static void SendWopenPacket(this IClientSession session, byte type, short data = 0, byte? value = null) => session.SendPacket(session.GenerateWopenPacket(type, data, value)); + public static void SendWopenPacket(this IClientSession session, WindowType type, short data = 0, byte? value = null) => session.SendPacket(session.GenerateWopenPacket((byte)type, data, value)); + public static void OpenNosBazaarUi(this IClientSession session, MedalType medal, int time) => session.SendPacket(session.GenerateWopenBazaar(medal, time)); + public static void CloseNosBazaarUi(this IClientSession session) => session.SendWopenPacket(WindowType.CLOUSE_UI); + public static void SendShopEndPacket(this IClientSession session, ShopEndType type) => session.SendPacket(session.GenerateShopEnd(type)); + public static void SendExcClosePacket(this IClientSession session, ExcCloseType type) => session.SendPacket(session.GenerateExcClosePacket(type)); + public static void SendMzPacket(this IClientSession session, string ip, short port) => session.SendPacket(session.GenerateMzPacket(ip, port)); + public static void SendItPacket(this IClientSession session, ItModeType mode) => session.SendPacket(session.GenerateItPacket(mode)); + public static void SendTaClosePacket(this IClientSession session) => session.SendPacket(session.GenerateTaClosePacket()); + public static void SendTalentCameraPacket(this IClientSession session) => session.SendPacket(session.GenerateTalentCameraPacket()); + public static void SendTalentArenaTimerPacket(this IClientSession session) => session.SendPacket(session.GenerateTalentArenaTimerPacket()); + + public static void SendClinitPacket(this IClientSession session, IEnumerable characters) => session.SendPacket(session.GenerateTopComplimentPacket("clinit", characters)); + public static void SendFlinitPacket(this IClientSession session, IEnumerable characters) => session.SendPacket(session.GenerateTopReputationPacket("flinit", characters)); + public static void SendKdlinitPacket(this IClientSession session, IEnumerable characters) => session.SendPacket(session.GenerateTopPvPPointsPacket("kdlinit", characters)); + + public static void SendBsInfoPacket(this IClientSession session, BsInfoType bsInfoType, GameType gameType, ushort time, QueueWindow window) => + session.SendPacket(session.GenerateBsInfoPacket(bsInfoType, gameType, time, window)); + + public static void SendPdtiPacket(this IClientSession session, PdtiType type, int vnum, short amount, short wearSlot, ushort upgrade, short rare) => + session.SendPacket(session.GeneratePdtiPacket(type, vnum, amount, wearSlot, upgrade, rare)); + + public static void SendParcelPacket(this IClientSession session, ParcelActionType actionType, MailType mailType, int giftId) => + session.SendPacket(session.GenerateParcelPacket(actionType, mailType, giftId)); + + public static void SendTwkPacket(this IClientSession session) => session.SendPacket(session.GenerateTwkPacket()); + public static void SendZzimPacket(this IClientSession session) => session.SendPacket(session.GenerateZzimPacket()); + public static void SendRecipeNpcList(this IClientSession session, IEnumerable recipes) => session.SendPacket(session.GenerateRecipeNpcList(recipes)); + public static void SendRecipeItemList(this IClientSession session, IEnumerable recipes, IGameItem gameItem) => session.SendPacket(session.GenerateRecipeItemList(recipes, gameItem)); + public static void SendRecipeCraftItemList(this IClientSession session, Recipe recipe) => session.SendPacket(session.GenerateRecipeCraftItemList(recipe)); + + public static void SendMessageUnderChat(this IClientSession session) + { + string key = "MESSAGE_UNDER_CHAT_"; + for (short i = 0; i < 10; i++) + { + if (!Enum.TryParse($"{key}{i}", out GameDialogKey gameDialogKey)) + { + continue; + } + + string message = session.GetLanguage(gameDialogKey); + session.SendPacket(session.GenerateMessageUnderChat(i, message)); + } + } + + public static void SendSound(this IClientSession session, SoundType type) => session.SendPacket(session.GenerateSound(type)); + + public static void BroadcastSoundInRange(this IClientSession session, short type) => + session.Broadcast(session.GenerateSound(type), new RangeBroadcast(session.PlayerEntity.PositionX, session.PlayerEntity.PositionY)); + + public static void SendStaticBonuses(this IClientSession session) => session.SendPacket(session.GenerateStaticBonusPacket()); + public static void SendPstPackets(this IClientSession session) => session.SendPackets(session.GeneratePstPackets()); + + public static void SendDancePacket(this IClientSession session, bool isOn) + { + if (!isOn) + { + return; + } + + session.SendPacket(session.GenerateDance(true)); + } + + public static void SendMapMusicPacket(this IClientSession session, short music) => session.SendPacket(session.GenerateMapMusic(music)); + + public static void TrySendScalPacket(this IClientSession session, IBattleEntity battleEntity = default) + { + IBattleEntity entityToScal = battleEntity ?? session.PlayerEntity; + if (!entityToScal.ShouldSendScal()) + { + return; + } + + session.SendPacket(entityToScal.GenerateScal()); + } + + public static void RefreshZoom(this IClientSession session) + { + session.SendGuriPacket(15, session.PlayerEntity.SkillComponent.Zoom, session.PlayerEntity.Id); + } + + public static void SendMinigameStart(this IClientSession session, MinigameType minigameType) => session.SendPacket(GenerateMinigameStart(minigameType)); + + public static void SendStPacket(this IClientSession session, IBattleEntity entity) => session.SendPacket(entity.GenerateStPacket()); + + public static void SendEmptySuPacket(this IClientSession session, SkillInfo skill) => session.SendPacket(session.GenerateEmptySuPacket(skill)); + + public static void BroadcastGetPacket(this IClientSession session, long dropId) => session.Broadcast(session.PlayerEntity.GenerateGet(dropId)); + + public static void BroadcastMateGetPacket(this IMateEntity mate, long dropId) => mate.MapInstance.Broadcast(mate.GenerateGet(dropId)); + + public static void SendGbPacket(this IClientSession session, BankType type, IReputationConfiguration reputationConfiguration, IBankReputationConfiguration bankReputationConfiguration, + IReadOnlyList topReputation) => + session.SendPacket(session.GenerateGb(type, reputationConfiguration, bankReputationConfiguration, topReputation)); + + public static void SendAmuletBuffPacket(this IClientSession session, GameItemInstance item) => session.SendPacket(session.PlayerEntity.GenerateAmuletBuff(item)); + + public static void SendEmptyAmuletBuffPacket(this IClientSession session) => session.SendPacket(session.PlayerEntity.GenerateEmptyAmuletBuff()); + + public static void SendEmptyRecipeCraftItem(this IClientSession session) => session.SendPacket(session.GenerateEmptyRecipeCraftItemPacket()); + + #endregion + + #region Generate Packets + + public static string GenerateMinigameStart(MinigameType minigameType) => $"mlo_st {(byte)minigameType}"; + + public static string GenerateSound(this IClientSession session, SoundType type) => $"guri 19 1 {session.PlayerEntity.Id} {(short)type}"; + public static string GenerateSound(this IClientSession session, short type) => $"guri 19 1 {session.PlayerEntity.Id} {type}"; + + public static string GenerateTaFcPacket(this IClientSession session, byte type) => $"ta_fc {type} {session.PlayerEntity.Id}"; + + public static string GenerateMessageUnderChat(this IClientSession session, short count, string message) => $"bn {count} {message.Replace(" ", "^")}"; + + public static string GenerateRecipeNpcList(this IClientSession session, IEnumerable recipes) + { + string recipeList = "m_list 2"; + + recipeList = recipes.Aggregate(recipeList, (current, s) => s.Amount > 0 ? current + $" {s.ProducedItemVnum}" : string.Empty); + recipeList += " -100"; + return recipeList; + } + + public static string GenerateRecipeItemList(this IClientSession session, IEnumerable recipes, IGameItem gameItem) + { + string recipeList = "m_list 2"; + + recipeList = recipes.Where(s => s.Amount > 0).Aggregate(recipeList, (current, s) => current + $" {s.ProducedItemVnum}"); + return recipeList + (gameItem.EffectValue is <= 111 and >= 109 ? " 999" : string.Empty); + } + + public static string GenerateRecipeCraftItemList(this IClientSession session, Recipe recipe) + { + string recList = $"m_list 3 {recipe.Amount.ToString()}"; + recList = recipe.Items.Aggregate(recList, (current, ite) => ite.Amount > 0 ? current + $" {ite.ItemVNum.ToString()} {ite.Amount.ToString()}" : string.Empty); + recList += " -1"; + return recList; + } + + public static string GenerateEmptyRecipeCraftItemPacket(this IClientSession session) => "m_list 3 0 -1"; + + public static string GenerateClPacket(this IClientSession session) => + $"cl {session.PlayerEntity.Id} {(session.PlayerEntity.Invisible ? 1 : 0)} {(session.PlayerEntity.CheatComponent.IsInvisible ? 1 : 0)}"; + + public static string GenerateBfPacket(this IClientSession session, Buff buff, int time, int charge) => + $"bf 1 {session.PlayerEntity.Id} {charge}.{buff.CardId}.{(charge == 0 ? (int)buff.Duration.TotalMilliseconds == 0 ? time : (int)(buff.Duration.TotalMilliseconds / 100) : charge)} {buff.CasterLevel}"; + + public static string GenerateBfLeftPacket(this IClientSession session, Buff buff) => + $"bf 1 {session.PlayerEntity.Id} 0.{buff.CardId}.{buff.RemainingTimeInMilliseconds() / 100} {buff.CasterLevel}"; + + public static string GenerateStaticBuffPacket(this IClientSession session, Buff buff, int time, bool isActive) => + $"vb {buff.CardId} {(isActive ? 1 : 0)} {(!buff.IsNoDuration() ? time / 100 : -1)}"; + + public static string GenerateBfPacket(this IClientSession session, Buff buff) => $"bf 1 {session.PlayerEntity.Id} 0.{buff.CardId}.0 {buff.CasterLevel}"; + + public static string GenerateRdiPacket(this IClientSession session, int vnum, short amount) => $"rdi {vnum} {amount}"; + + public static string GenerateScpStcPacket(this IClientSession session) => $"sc_p_stc {session.PlayerEntity.MaxPetCount / 10 - 1} {session.PlayerEntity.MaxPartnerCount - 3}"; + + public static string GeneratePClearPacket(this IClientSession session) => "p_clear"; + + public static string GenerateWopenPacket(this IClientSession session, WindowType type) => $"wopen {(byte)type} 0"; + + public static string GenerateWopenPacket(this IClientSession session, byte type, short data, byte? value) => $"wopen {type} {data} {(value.HasValue ? value.Value.ToString() : string.Empty)}"; + + public static string GenerateWopenBazaar(this IClientSession session, MedalType medal, int time) => $"wopen 32 {(byte)medal} {time}"; + + public static string GenerateShopEnd(this IClientSession session, ShopEndType type) => $"shop_end {(byte)type}"; + + public static string GenerateExcClosePacket(this IClientSession session, ExcCloseType type) => $"exc_close {(byte)type}"; + + public static string GenerateMzPacket(this IClientSession session, string ip, short port) => $"mz {ip} {port} {session.PlayerEntity.Slot}"; + + public static string GenerateItPacket(this IClientSession session, ItModeType mode) => $"it {(byte)mode}"; + + public static string GenerateTaClosePacket(this IClientSession session) => "ta_close"; + + public static string GenerateTalentCameraPacket(this IClientSession session) => "ta_sv 0"; + + public static string GenerateTalentArenaTimerPacket(this IClientSession session) => "ta_s"; + + public static string GenerateBsInfoPacket(this IClientSession session, BsInfoType bsInfoType, GameType gameType, ushort time, QueueWindow window) => + $"bsinfo {(byte)bsInfoType} {(byte)gameType} {time} {(byte)window}"; + + public static string GeneratePdtiPacket(this IClientSession session, PdtiType type, int vnum, short amount, short wearSlot, ushort upgrade, short rare) => + $"pdti {(byte)type} {vnum} {amount} {wearSlot} {upgrade} {rare}"; + + public static string GenerateParcelPacket(this IClientSession session, ParcelActionType actionType, MailType mailType, int giftId) => $"parcel {(byte)actionType} {(byte)mailType} {giftId}"; + + public static string GenerateTwkPacket(this IClientSession session) => $"twk 1 {session.PlayerEntity.Id} {session.Account.Name} {session.PlayerEntity.Name} shtmxpdlfeoqkr " + + $"{session.Account.Language.ToString().ToLower()} {session.Account.Language.ToString().ToLower()}"; //the 1 is server id + + public static string GenerateZzimPacket(this IClientSession session) => "zzim"; + + public static string GenerateEventAsk(this IClientSession session, QnamlType type, string packet, string message) => $"qnaml {(byte)type} #{packet.Replace(' ', '^')} {message}"; + + public static string GenerateTopComplimentPacket(this IClientSession session, string basicSeed, IEnumerable characters) + => characters.Aggregate(basicSeed, (current, dto) => current + $" {dto.Id.ToString()}|{dto.Level.ToString()}|{dto.HeroLevel.ToString()}|{dto.Compliment.ToString()}|{dto.Name}"); + + public static string GenerateTopReputationPacket(this IClientSession session, string basicSeed, IEnumerable characters) + => characters.Aggregate(basicSeed, (current, dto) => current + $" {dto.Id.ToString()}|{dto.Level.ToString()}|{dto.HeroLevel.ToString()}|{dto.Reput.ToString()}|{dto.Name}"); + + public static string GenerateTopPvPPointsPacket(this IClientSession session, string basicSeed, IEnumerable characters) + => characters.Aggregate(basicSeed, (current, dto) => current + $" {dto.Id.ToString()}|{dto.Level.ToString()}|{dto.HeroLevel.ToString()}|{dto.Act4Points.ToString()}|{dto.Name}"); + + public static IEnumerable GenerateScnPackets(this IClientSession session) + { + var list = new List(); + foreach (IMateEntity mate in session.PlayerEntity.MateComponent.GetMates(x => x.MateType == MateType.Partner)) + { + list.Add(mate.GenerateScPacket(GameLanguage, session.UserLanguage)); + } + + return list; + } + + public static IEnumerable GenerateScpPackets(this IClientSession session, byte? page) + { + var list = new List(); + + if (!page.HasValue) + { + foreach (IMateEntity s in session.PlayerEntity.MateComponent.GetMates(x => x.MateType == MateType.Pet)) + { + list.Add(s.GenerateScPacket(GameLanguage, session.UserLanguage)); + } + } + else + { + foreach (IMateEntity s in session.PlayerEntity.MateComponent.GetMates(x => x.MateType == MateType.Pet).Skip(page.Value * 10).Take(10)) + { + list.Add(s.GenerateScPacket(GameLanguage, session.UserLanguage)); + } + } + + return list; + } + + public static string GenerateMapOutPacket(this IClientSession session) => "mapout"; + + public static string GenerateOutPacket(this IClientSession session) => $"out 1 {session.PlayerEntity.Id}"; + + public static string GeneratePairyPacket(this IClientSession session) + { + bool isBuffed = session.PlayerEntity.HasBuff(BuffVnums.FAIRY_BOOSTER); + + GameItemInstance fairy = session.PlayerEntity.Fairy; + return fairy == null + ? $"pairy 1 {session.PlayerEntity.Id} 0 0 0 0" + : $"pairy 1 {session.PlayerEntity.Id} 4 {session.PlayerEntity.Element} {fairy.ElementRate + fairy.GameItem.ElementRate} {fairy.GameItem.Morph + (isBuffed && !FairyMorphsWithoutBoostChange.Contains(fairy.GameItem.Morph) ? 5 : 0)}"; + } + + public static IEnumerable GeneratePstPackets(this IClientSession session) => session.PlayerEntity.MateComponent.TeamMembers().OrderBy(s => s.MateType).Select(s => s.GeneratePst()); + + public static string GenerateDelayPacket(this IClientSession session, int delay, GuriType type, string argument) => $"delay {delay} {(byte)type} #{argument.Replace(' ', '^')}"; + + public static string GenerateAtPacket(this IClientSession session) => + "at " + + $"{session.PlayerEntity.Id} " + + $"{session.PlayerEntity.MapInstance.MapVnum} " + + $"{session.PlayerEntity.PositionX} " + + $"{session.PlayerEntity.PositionY} " + + "2 " + + $"{(byte)(session.IsGameMaster() ? 2 : 0)} " + + $"{session.PlayerEntity.MapInstance.MapMusic ?? session.PlayerEntity.MapInstance.Music} " + + "-1"; + + public static string GenerateInventoryRemovePacket(this IClientSession session, InventoryType type, int slot) => $"ivn {(byte)type} {slot}.-1.0.0.0.0"; + + public static string GenerateEqPacket(this IClientSession session) + { + int color = (byte)session.PlayerEntity.HairColor; + GameItemInstance head = session.PlayerEntity.Hat; + + if (head != null && head.GameItem.IsColorable) + { + color = head.Design; + } + + byte gmMode = 0; + + if (session.IsGameMaster()) + { + gmMode = 2; + + if (session.PlayerEntity.CheatComponent.IsInvisible) + { + gmMode = 6; + } + } + + return + $"eq {session.PlayerEntity.Id} {gmMode} {(byte)session.PlayerEntity.Gender} {(byte)session.PlayerEntity.HairStyle} {color} {(byte)session.PlayerEntity.Class} {session.GenerateEqListForPacket()} {session.GenerateEqRareUpgradeForPacket()}"; + } + + public static string GenerateLevelUpPacket(this IClientSession session) => $"levelup {session.PlayerEntity.Id}"; + + public static string GenerateExtsPacket(this IClientSession session) => + $"exts 0 {session.PlayerEntity.GetInventorySlots(false, InventoryType.Equipment)} {session.PlayerEntity.GetInventorySlots(false, InventoryType.Main)} {session.PlayerEntity.GetInventorySlots(false, InventoryType.Etc)}"; + + public static string GenerateSkillListPacket(this IClientSession session) + { + List characterSkills; + characterSkills = session.PlayerEntity.UseSp + ? session.PlayerEntity.SkillsSp.Values.OrderBy(s => s.Skill.CastId).ToList() + : session.PlayerEntity.CharacterSkills.Values.OrderBy(s => s.Skill.CastId).ToList(); + + string skibase = string.Empty; + if (session.PlayerEntity.UseSp && session.PlayerEntity.Specialist != null) + { + skibase = $"{characterSkills.ElementAt(0).SkillVNum} {characterSkills.ElementAt(0).SkillVNum}"; + characterSkills.AddRange(session.PlayerEntity.CharacterSkills.Where(s => s.Value.Skill.IsPassiveSkill()).Select(s => s.Value)); + } + else + { + skibase = $"{200 + 20 * (byte)session.PlayerEntity.Class} {201 + 20 * (byte)session.PlayerEntity.Class}"; + } + + string tatooSkills = string.Empty; + string generatedSkills = characterSkills?.Aggregate(string.Empty, (current, ski) => current + $" {ski.SkillVNum}"); + return $"ski {skibase}{tatooSkills}{generatedSkills}"; + } + + public static string GenerateStaticBonusPacket(this IClientSession session) + { + string bonusList = string.Empty; + foreach (CharacterStaticBonusDto bonus in session.PlayerEntity.GetStaticBonuses()) + { + int time = bonus.DateEnd == null ? -1 : Math.Abs((int)(DateTime.UtcNow - bonus.DateEnd.Value).TotalHours); + bonusList += $"{bonus.ItemVnum}.{time} "; + } + + return $"umi {bonusList}"; + } + + public static string GenerateStatPacket(this IClientSession session, bool isTransform) + { + IPlayerEntity character = session.PlayerEntity; + double option = + (character.WhisperBlocked ? Math.Pow(2, (int)CharacterOption.WhisperBlocked - 1) : 0) + + (character.FamilyRequestBlocked ? Math.Pow(2, (int)CharacterOption.FamilyRequestBlocked - 1) : 0) + + (!character.MouseAimLock ? Math.Pow(2, (int)CharacterOption.MouseAimLock - 1) : 0) + + (character.MinilandInviteBlocked ? Math.Pow(2, (int)CharacterOption.MinilandInviteBlocked - 1) : 0) + + (character.ExchangeBlocked ? Math.Pow(2, (int)CharacterOption.ExchangeBlocked - 1) : 0) + + (character.FriendRequestBlocked ? Math.Pow(2, (int)CharacterOption.FriendRequestBlocked - 1) : 0) + + (character.EmoticonsBlocked ? Math.Pow(2, (int)CharacterOption.EmoticonsBlocked - 1) : 0) + + (character.HpBlocked ? Math.Pow(2, (int)CharacterOption.HpBlocked - 1) : 0) + + (character.BuffBlocked ? Math.Pow(2, (int)CharacterOption.BuffBlocked - 1) : 0) + + (character.GroupRequestBlocked ? Math.Pow(2, (int)CharacterOption.GroupRequestBlocked - 1) : 0) + + (character.HeroChatBlocked ? Math.Pow(2, (int)CharacterOption.HeroChatBlocked - 1) : 0) + + (character.QuickGetUp ? Math.Pow(2, (int)CharacterOption.QuickGetUp - 1) : 0) + + (character.HideHat ? Math.Pow(2, (int)CharacterOption.HideHat - 1) : 0) + + (character.UiBlocked ? Math.Pow(2, (int)CharacterOption.UiBlocked - 1) : 0) + + (!character.IsPetAutoRelive ? 64 : 0) + + (!character.IsPartnerAutoRelive ? 128 : 0) + + (!character.CanPerformAttack() ? 131072 : 0) + + (!character.CanPerformMove() ? 262144 : 0); + + if (character.GameStartDate.AddSeconds(5) > DateTime.UtcNow) + { + return $"stat {character.Hp} {character.MaxHp} {character.Mp} {character.MaxMp} {(isTransform ? 1 : 0)} {option}"; + } + + if (character.Hp < 0) + { + character.Hp = 0; + } + + if (character.Mp < 0) + { + character.Mp = 0; + } + + if (character.Hp > character.MaxHp) + { + character.Hp = character.MaxHp; + } + + if (character.Mp > character.MaxMp) + { + character.Mp = character.MaxMp; + } + + return $"stat {character.Hp} {character.MaxHp} {character.Mp} {character.MaxMp} {(isTransform ? 1 : 0)} {option}"; + } + + public static string GenerateStatInfoPacket(this IClientSession session) => + $"st 1 {session.PlayerEntity.Id} {session.PlayerEntity.Level} {session.PlayerEntity.HeroLevel} {session.PlayerEntity.GetHpPercentage()} {session.PlayerEntity.GetMpPercentage()} {session.PlayerEntity.Hp} {session.PlayerEntity.Mp}{session.PlayerEntity.BuffComponent.GetAllBuffs().Aggregate(string.Empty, (current, buff) => current + $" {buff.CardId}")}"; + + public static string GenerateStatCharPacket(this IClientSession session, bool refreshHpMp = true) + { + IPlayerEntity playerEntity = session.PlayerEntity; + + int mainAttackType = 0; + int secondAttackType = 0; + + switch (playerEntity.Class) + { + case (byte)ClassType.Adventurer: + mainAttackType = 0; + secondAttackType = 1; + break; + + case ClassType.Magician: + mainAttackType = 2; + secondAttackType = 1; + break; + + case ClassType.Swordman: + mainAttackType = 0; + secondAttackType = 1; + break; + + case ClassType.Archer: + mainAttackType = 1; + secondAttackType = 0; + break; + } + + GameItemInstance mainWeapon = session.PlayerEntity.MainWeapon; + GameItemInstance secondWeapon = session.PlayerEntity.SecondaryWeapon; + GameItemInstance armor = session.PlayerEntity.Armor; + + int weaponUpgrade = mainWeapon?.Upgrade ?? 0; + int secondaryUpgrade = secondWeapon?.Upgrade ?? 0; + int armorUpgrade = armor?.Upgrade ?? 0; + + session.PlayerEntity.RefreshCharacterStats(refreshHpMp); + + return + "sc " + + $"{mainAttackType} " + + $"{weaponUpgrade.ToString()} " + + $"{playerEntity.StatisticsComponent.MinDamage.ToString()} " + + $"{playerEntity.StatisticsComponent.MaxDamage.ToString()} " + + $"{playerEntity.StatisticsComponent.HitRate.ToString()} " + + $"{playerEntity.StatisticsComponent.CriticalChance.ToString()} " + + $"{playerEntity.StatisticsComponent.CriticalDamage.ToString()} " + + $"{secondAttackType} " + + $"{secondaryUpgrade.ToString()} " + + $"{playerEntity.StatisticsComponent.SecondMinDamage.ToString()} " + + $"{playerEntity.StatisticsComponent.SecondMaxDamage.ToString()} " + + $"{playerEntity.StatisticsComponent.SecondHitRate.ToString()} " + + $"{playerEntity.StatisticsComponent.SecondCriticalChance.ToString()} " + + $"{playerEntity.StatisticsComponent.SecondCriticalDamage.ToString()} " + + $"{armorUpgrade.ToString()} " + + $"{playerEntity.StatisticsComponent.MeleeDefense.ToString()} " + + $"{playerEntity.StatisticsComponent.MeleeDodge.ToString()} " + + $"{playerEntity.StatisticsComponent.RangeDefense.ToString()} " + + $"{playerEntity.StatisticsComponent.RangeDodge.ToString()} " + + $"{playerEntity.StatisticsComponent.MagicDefense.ToString()} " + + $"{playerEntity.StatisticsComponent.FireResistance.ToString()} " + + $"{playerEntity.StatisticsComponent.WaterResistance.ToString()} " + + $"{playerEntity.StatisticsComponent.LightResistance.ToString()} " + + $"{playerEntity.StatisticsComponent.ShadowResistance.ToString()}"; + } + + public static string GenerateRevive(this IPlayerEntity character) => + $"revive 1 {character.Id} {(character.TimeSpaceComponent.IsInTimeSpaceParty ? character.TimeSpaceComponent.TimeSpace.Instance.Lives < 0 ? 1 : character.TimeSpaceComponent.TimeSpace.Instance.Lives : 0)}"; + + public static string GenerateRevivalPacket(RevivalType revivalType) => $"revival {((int)revivalType).ToString()}"; + public static string GenerateGoldPacket(this IClientSession session) => $"gold {session.PlayerEntity.Gold} {session.Account.BankMoney / 100}"; + + public static string GenerateLevPacket(this IClientSession session, ICharacterAlgorithm characterAlgorithm) + { + bool usingSp = session.PlayerEntity.Specialist != null && session.PlayerEntity.UseSp; + bool isAdventurer = session.PlayerEntity.Class == ClassType.Adventurer && session.PlayerEntity.JobLevel < 20; + return + $"lev {session.PlayerEntity.Level}" + + $" {(session.PlayerEntity.Level > 92 ? session.PlayerEntity.LevelXp / 1000 : session.PlayerEntity.LevelXp)}" + + $" {(!usingSp ? session.PlayerEntity.JobLevel : session.PlayerEntity.Specialist.SpLevel)}" + + $" {(!usingSp ? session.PlayerEntity.JobLevelXp : session.PlayerEntity.Specialist.Xp)}" + + $" {(session.PlayerEntity.Level > 92 ? session.PlayerEntity.GetLevelXp(characterAlgorithm) / 1000 : session.PlayerEntity.GetLevelXp(characterAlgorithm))}" + + $" {(!usingSp ? session.PlayerEntity.GetJobXp(characterAlgorithm, isAdventurer) : session.PlayerEntity.GetSpJobXp(characterAlgorithm, session.PlayerEntity.Specialist.IsFunSpecialist()))}" + + $" {session.PlayerEntity.Reput} {session.PlayerEntity.GetCp()}" + + $" {session.PlayerEntity.HeroXp}" + + $" {session.PlayerEntity.HeroLevel}" + + $" {session.PlayerEntity.GetHeroXp(characterAlgorithm)}" + + " 0"; + } + + public static bool IsFunSpecialist(this GameItemInstance itemInstance) => itemInstance.ItemVNum == (short)ItemVnums.PIRATE_SP || itemInstance.ItemVNum == (short)ItemVnums.PYJAMA_SP || + itemInstance.ItemVNum == (short)ItemVnums.CHICKEN_SP; + + public static string GenerateFdPacket(this IClientSession session, IReputationConfiguration reputationConfiguration, IReadOnlyList topReputation) => + $"fd {session.PlayerEntity.Reput} {(byte)session.PlayerEntity.GetReputationIcon(reputationConfiguration, topReputation)} {(int)session.PlayerEntity.Dignity} {Math.Abs(session.PlayerEntity.GetDignityIco())}"; + + public static string GenerateCInfoPacket(this IClientSession session, IFamily family, IReputationConfiguration reputationConfiguration, IReadOnlyList topReputation) => + "c_info" + + $" {session.PlayerEntity.Name}" + + " -" + + $" {(session.PlayerEntity.IsInGroup() ? session.PlayerEntity.GetGroup().GroupId : -1)}" + + $" {(family != null ? $"{family.Id}.-1 " + $"{family.Name}({GameLanguage.GetLanguage(session.PlayerEntity.GetFamilyAuthority().GetMemberLanguageKey(), session.UserLanguage) ?? ""})" : "-1 -")}" + + $" {session.PlayerEntity.Id}" + + $" {(session.IsGameMaster() ? 3 : 0)}" + + $" {(byte)session.PlayerEntity.Gender}" + + $" {(byte)session.PlayerEntity.HairStyle}" + + $" {(byte)session.PlayerEntity.HairColor}" + + $" {(byte)session.PlayerEntity.Class}" + + $" {(session.PlayerEntity.GetDignityIco() == 1 ? (byte)session.PlayerEntity.GetReputationIcon(reputationConfiguration, topReputation) : -session.PlayerEntity.GetDignityIco())}" + + $" {session.PlayerEntity.Compliment}" + + $" {(session.PlayerEntity.UseSp || session.PlayerEntity.IsOnVehicle ? session.PlayerEntity.Morph : 0)}" + + $" {(session.PlayerEntity.Invisible || session.PlayerEntity.CheatComponent.IsInvisible ? 1 : 0)}" + + $" {family?.Level ?? 0}" + + $" {(session.PlayerEntity.UseSp && session.PlayerEntity.Specialist != null ? session.PlayerEntity.Specialist.Upgrade : 0)}" + + $" {(session.PlayerEntity.UseSp ? session.PlayerEntity.MorphUpgrade : 0)}" + + $" {session.PlayerEntity.ArenaWinner}"; + + public static GameDialogKey GetMemberLanguageKey(this FamilyAuthority authority) + { + return authority switch + { + FamilyAuthority.Head => GameDialogKey.FAMILY_RANK_HEAD, + FamilyAuthority.Deputy => GameDialogKey.FAMILY_RANK_DEPUTY, + FamilyAuthority.Keeper => GameDialogKey.FAMILY_RANK_KEEPER, + _ => GameDialogKey.FAMILY_RANK_MEMBER + }; + } + + public static string GenerateCMapPacket(this IClientSession session, bool isEntering) => + $"c_map 0 {session.PlayerEntity.MapInstance?.MapNameId.ToString() ?? ""} {(isEntering ? 1 : 0)}"; + + public static string GenerateCModePacket(this IClientSession session) => + $"c_mode 1 {session.PlayerEntity.Id} {(session.PlayerEntity.UseSp || session.PlayerEntity.IsOnVehicle || session.PlayerEntity.IsMorphed || session.PlayerEntity.IsSeal ? session.PlayerEntity.Morph : 0)} {(session.PlayerEntity.UseSp ? session.PlayerEntity.MorphUpgrade : 0)} {(session.PlayerEntity.UseSp || !session.PlayerEntity.IsSeal ? session.PlayerEntity.MorphUpgrade2 : 0)} {session.PlayerEntity.ArenaWinner} {session.PlayerEntity.Size} 0"; + + public static string GenerateCondPacket(this IClientSession session) => + $"cond 1 {session.PlayerEntity.Id} {(session.PlayerEntity.CanPerformAttack() == false ? 1 : 0)} {(session.PlayerEntity.CanPerformMove() == false ? 1 : 0)} {session.PlayerEntity.Speed}"; + + public static string GenerateInPacket(this IClientSession session, IReputationConfiguration reputationConfiguration, IReadOnlyList topReputation, bool foe = false, + bool showInEffect = false) + { + IPlayerEntity character = session.PlayerEntity; + + bool isOnAct4 = session.CurrentMapInstance.HasMapFlag(MapFlags.ACT_4); + byte faction = (byte)(isOnAct4 && !session.IsGameMaster() ? character.Faction == FactionType.Angel ? 3 : 4 : 0); + + // string chars = "!§$%&/()=?*+~#"; + string name = character.Name; + if (foe && !session.IsGameMaster()) + { + name = "NosWings"; + } + + int color = (int)character.HairColor; + GameItemInstance headWearable = character.Hat; + if (headWearable?.GameItem.IsColorable == true) + { + color = headWearable.Design; + } + + CharacterTitleDto titleVnum = session.PlayerEntity.Titles.FirstOrDefault(x => x.IsVisible); + bool shouldChangeMorph = character.IsUsingFairyBooster() && character.Fairy?.GameItem.Morph > 4 && character.Fairy.GameItem.Morph != 9 && character.Fairy.GameItem.Morph != 14; + IFamily family = character.Family; + + string familyId = $"{(foe ? "-1" : family != null ? $"{family.Id}.-1" : -1)}"; + + return "in 1 " + + $"{name} " + + "- " + + $"{character.Id} " + + $"{character.PositionX} " + + $"{character.PositionY} " + + $"{character.Direction} " + + $"{(session.IsGameMaster() ? 2 : 0)} " + + $"{(byte)character.Gender} " + + $"{(byte)character.HairStyle} " + + $"{color} " + + $"{(byte)character.Class} " + + $"{session.GenerateEqListForPacket()} " + + $"{character.GetHpPercentage()} " + + $"{character.GetMpPercentage()} " + + $"{(character.IsSitting ? 1 : 0)} " + + $"{(character.IsInGroup() ? character.GetGroup().GroupId : -1)} " + + $"{(character.Fairy != null ? 4 : 0)} " + + $"{character.Element} " + + $"{(character.IsUsingFairyBooster() ? 1 : 0)} " + + $"{character.Fairy?.GameItem.Morph + (shouldChangeMorph ? 5 : 0) ?? 0} " + + $"{(showInEffect ? 0 : 1)} " + + $"{(character.UseSp || character.IsOnVehicle || character.IsMorphed || character.IsSeal ? character.Morph : 0)} " + + $"{session.GenerateEqRareUpgradeForPacket(true)} " + + $"{familyId} " + + $"{(foe ? name : family?.Name ?? "-")} " + + $"{(character.GetDignityIco() == 1 ? (byte)character.GetReputationIcon(reputationConfiguration, topReputation) : -character.GetDignityIco())} " + + $"{(character.Invisible ? 1 : 0)} " + + $"{(character.UseSp ? character.MorphUpgrade : 0)} " + + $"{faction} " + + $"{(character.UseSp ? character.MorphUpgrade2 : 0)} " + + $"{character.Level} " + + $"{family?.Level ?? 0} " + + "0|0|0 " + + $"{character.ArenaWinner} " + + $"{character.Compliment} " + + $"{character.Size} " + + $"{character.HeroLevel} " + + $"{(!isOnAct4 ? titleVnum?.ItemVnum ?? 0 : 0)}"; + } + + public static TitInfoPacket GenerateTitInfoPacket(this IClientSession session) => new() + { + VisualType = session.PlayerEntity.Type, + VisualId = session.PlayerEntity.Id, + VisibleTitleVnum = session.PlayerEntity.Titles.FirstOrDefault(s => s.IsVisible)?.ItemVnum ?? 0, + EffectTitleVnum = session.PlayerEntity.Titles.FirstOrDefault(s => s.IsEquipped)?.ItemVnum ?? 0 + }; + + public static TitlePacket GenerateTitlePacket(this IClientSession session) => new() + { + Titles = session.PlayerEntity.Titles.Select(x => new TitleSubPacket + { + ItemVnum = x.TitleId, + State = x.IsEquipped ? x.IsVisible ? TitleStatus.EquippedEffectAndVisible : + TitleStatus.EquippedEffect : + x.IsVisible ? TitleStatus.EquippedVisible : TitleStatus.Available + }).ToList() + }; + + public static EffectServerPacket GenerateEffectPacket(this IClientSession session, EffectType effectType) => session.GenerateEffectPacket((short)effectType); + + public static EffectServerPacket GenerateEffectPacket(this IClientSession session, int effectId) => new() + { + EffectType = 1, + CharacterId = session.PlayerEntity.Id, + Id = effectId + }; + + public static string GenerateFactionPacket(this IClientSession session) => $"fs {(byte)session.PlayerEntity.Faction}"; + + public static string GenerateEqListForPacket(this IClientSession session) + { + string[] inventoryArray = new string[17]; + for (short i = 0; i < 17; i++) + { + InventoryItem invItem = session.PlayerEntity.GetInventoryItemFromEquipmentSlot((EquipmentType)i); + GameItemInstance item = invItem?.ItemInstance; + if (item != null) + { + inventoryArray[i] = item.ItemVNum.ToString(); + } + else + { + inventoryArray[i] = "-1"; + } + } + + return + $"{(session.PlayerEntity.HideHat ? "0" : inventoryArray[(byte)EquipmentType.Hat])}.{inventoryArray[(byte)EquipmentType.Armor]}.{inventoryArray[(byte)EquipmentType.MainWeapon]}.{inventoryArray[(byte)EquipmentType.SecondaryWeapon]}.{inventoryArray[(byte)EquipmentType.Mask]}.{inventoryArray[(byte)EquipmentType.Fairy]}.{inventoryArray[(byte)EquipmentType.CostumeSuit]}.{(session.PlayerEntity.HideHat ? "0" : inventoryArray[(byte)EquipmentType.CostumeHat])}.{inventoryArray[(byte)EquipmentType.WeaponSkin]}.{inventoryArray[(byte)EquipmentType.Wings]}"; + } + + public static string GenerateEqRareUpgradeForPacket(this IClientSession session, bool isInPacket = false) + { + GameItemInstance mainWeapon = session.PlayerEntity.MainWeapon; + GameItemInstance armor = session.PlayerEntity.Armor; + + short weaponRare = mainWeapon?.Rarity ?? 0; + byte weaponUpgrade = mainWeapon?.Upgrade ?? 0; + short armorRare = armor?.Rarity ?? 0; + byte armorUpgrade = armor?.Upgrade ?? 0; + + if (!isInPacket) + { + return $"{weaponUpgrade}{weaponRare} {armorUpgrade}{armorRare}"; + } + + var mainWeaponString = new StringBuilder(); + var armorString = new StringBuilder(); + + if (mainWeapon == null) + { + mainWeaponString.Append('0'); + } + else + { + int upgrade = weaponUpgrade * 10; + mainWeaponString.Append(upgrade == 0 ? $"{weaponRare}" : $"{weaponUpgrade}{weaponRare}"); + } + + if (armor == null) + { + armorString.Append('0'); + } + else + { + int upgrade = armorUpgrade * 10; + armorString.Append(upgrade == 0 ? $"{armorRare}" : $"{armorUpgrade}{armorRare}"); + } + + return $"{mainWeaponString} {armorString}"; + } + + public static string GenerateEquipPacket(this IClientSession session) + { + string equipments = string.Empty; + short weaponRare = 0; + byte weaponUpgrade = 0; + short armorRare = 0; + byte armorUpgrade = 0; + + foreach (InventoryItem invItem in session.PlayerEntity.EquippedItems) + { + GameItemInstance item = invItem?.ItemInstance; + if (item == null) + { + continue; + } + + switch (item.GameItem.EquipmentSlot) + { + case EquipmentType.Armor: + { + armorRare = item.Rarity; + armorUpgrade = item.Upgrade; + break; + } + case EquipmentType.MainWeapon: + { + weaponRare = item.Rarity; + weaponUpgrade = item.Upgrade; + break; + } + } + + equipments += + $" {(byte)item.GameItem.EquipmentSlot}.{item.GameItem.Id}.{item.Rarity}.{(item.GameItem.IsColorable ? item.Design : item.Upgrade)}.0.{item.GetRunesCount()}"; + } + + return $"equip {(weaponUpgrade == 0 ? string.Empty : weaponUpgrade.ToString())}{weaponRare} {(armorUpgrade == 0 ? string.Empty : armorUpgrade.ToString())}{armorRare}{equipments}"; + } + + + public static string GenerateReqInfo(this IClientSession session, IReputationConfiguration reputationConfiguration, IReadOnlyList topReputation) + { + GameItemInstance mainWeapon = session.PlayerEntity.MainWeapon; + GameItemInstance secondWeapon = session.PlayerEntity.SecondaryWeapon; + GameItemInstance armor = session.PlayerEntity.Armor; + GameItemInstance specialist = session.PlayerEntity.Specialist; + + bool hasMainWeapon = mainWeapon != null; + bool hasSecondWeapon = secondWeapon != null; + bool hasArmor = armor != null; + + bool isPvpPrimary = hasMainWeapon && mainWeapon.GameItem.Name.Contains(": "); + bool isPvpSecondary = hasSecondWeapon && secondWeapon.GameItem.Name.Contains(": "); + bool isPvpArmor = hasArmor && armor.GameItem.Name.Contains(": "); + + IPlayerEntity character = session.PlayerEntity; + IFamily family = character.Family; + + return "tc_info " + + $"{character.Level} " + + $"{character.Name} " + + $"{character.Element} " + + $"{character.ElementRate} " + + $"{(byte)character.Class} " + + $"{(byte)character.Gender} " + + $"{family?.Id ?? -1} " + + $"{(family == null ? "-" : family.Name)} " + + $"{(byte)session.PlayerEntity.GetReputationIcon(reputationConfiguration, topReputation)} " + + $"{session.PlayerEntity.GetDignityIco()} " + + $"{(hasMainWeapon ? 1 : 0)} " + + $"{(hasMainWeapon ? mainWeapon.Rarity : 0)} " + + $"{(hasMainWeapon ? mainWeapon.Upgrade : 0)} " + + $"{(hasSecondWeapon ? 1 : 0)} " + + $"{(hasSecondWeapon ? secondWeapon.Rarity : 0)} " + + $"{(hasSecondWeapon ? secondWeapon.Upgrade : 0)} " + + $"{(hasArmor ? 1 : 0)} " + + $"{(hasArmor ? armor.Rarity : 0)} " + + $"{(hasArmor ? armor.Upgrade : 0)} " + + $"{character.Act4Kill} " + + $"{character.Act4Dead} " + + $"{character.Reput} " + + "0 " + + "0 " + + "0 " + + $"{(character.UseSp && specialist != null ? specialist.GameItem.Morph : 0)} " + + $"{character.TalentWin} " + + $"{character.TalentLose} " + + $"{character.TalentSurrender} " + + $"{character.MasterPoints} " + + $"{character.MasterTicket} " + + $"{character.Compliment} " + + $"{character.Act4Points} " + + $"{(isPvpPrimary ? 1 : 0)} " + + $"{(isPvpSecondary ? 1 : 0)} " + + $"{(isPvpArmor ? 1 : 0)} " + + $"{character.HeroLevel} " + + $"{character.Biography}"; + } + + public static string GenerateEmptySuPacket(this IClientSession session, SkillInfo skillInfo) + => "su " + + $"{(byte)session.PlayerEntity.Type} " + + $"{session.PlayerEntity.Id} " + + $"{(byte)session.PlayerEntity.Type} " + + $"{session.PlayerEntity.Id} " + + $"{skillInfo.Vnum} " + + $"{skillInfo.Cooldown} " + + $"{skillInfo.HitAnimation} " + + "-1 " + + "0 " + + "1 " + + $"{session.PlayerEntity.GetHpPercentage()} " + + "0 " + + "-2 " + + "0"; + + public static string GenerateGet(this IBattleEntity entity, long id) => $"get {(byte)entity.Type} {entity.Id} {id} 0"; + + public static string GenerateAmuletBuff(this IPlayerEntity character, GameItemInstance item) + { + if (item.DurabilityPoint != 0) + { + return $"bf 1 {character.Id} {item.DurabilityPoint}.62.{item.GameItem.LeftUsages} {character.Level}"; + } + + return $"bf 1 {character.Id} 0.62.{(item.ItemDeleteTime.HasValue ? (long)(item.ItemDeleteTime.Value - DateTime.UtcNow).TotalSeconds * 10 : 0)} {character.Level}"; + } + + public static string GenerateEmptyAmuletBuff(this IPlayerEntity character) => $"bf 1 {character.Id} 0.62.0 {character.Level}"; + + public static string GenerateDance(this IClientSession session, bool isOn) => $"dance {(isOn ? 2.ToString() : string.Empty)}"; + + public static string GenerateMapMusic(this IClientSession session, short music) => $"bgm2 {music}"; + + #endregion +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Extensions/CollectionExtension.cs b/srcs/WingsAPI.Game/Extensions/CollectionExtension.cs new file mode 100644 index 0000000..5853637 --- /dev/null +++ b/srcs/WingsAPI.Game/Extensions/CollectionExtension.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.Linq; + +namespace WingsEmu.Core.Extensions; + +public static class CollectionExtension +{ + public static IEnumerable> Split(this IEnumerable source, int size) + { + int pos = 0; + source = source.ToArray(); + while (source.Skip(pos).Any()) + { + yield return source.Skip(pos).Take(size); + pos += size; + } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Extensions/ConcurrentBagExtension.cs b/srcs/WingsAPI.Game/Extensions/ConcurrentBagExtension.cs new file mode 100644 index 0000000..9697131 --- /dev/null +++ b/srcs/WingsAPI.Game/Extensions/ConcurrentBagExtension.cs @@ -0,0 +1,36 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Concurrent; +using System.Linq; + +namespace WingsEmu.Core.Extensions; + +public static class ConcurrentBagExtensions +{ + #region Methods + + public static ConcurrentBag Replace(this ConcurrentBag queue, Func predicate) => new(queue.ToList().Where(predicate)); + + public static void Clear(this ConcurrentBag queue) + { + while (!queue.IsEmpty) + { + queue.TryTake(out T _); + } + } + + public static void RemoveWhere(this ConcurrentBag queue, Func predicate, out ConcurrentBag queueReturned) + { + queueReturned = new ConcurrentBag(queue.Where(predicate)); + } + + private static Func Not(this Func predicate) + { + return value => !predicate(value); + } + + #endregion +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Extensions/DamageExtension.cs b/srcs/WingsAPI.Game/Extensions/DamageExtension.cs new file mode 100644 index 0000000..0d60ca6 --- /dev/null +++ b/srcs/WingsAPI.Game/Extensions/DamageExtension.cs @@ -0,0 +1,1879 @@ +using System; +using WingsAPI.Data.Families; +using WingsAPI.Packets.Enums.Shells; +using WingsEmu.Game._enum; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Helpers.Damages.Calculation; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Monster; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Battle; + +namespace WingsEmu.Game.Extensions; + +public static class DamageExtension +{ + private static readonly double[] _plus = { 0, 0.1, 0.15, 0.22, 0.32, 0.43, 0.54, 0.65, 0.9, 1.2, 2 }; + private static IRandomGenerator _randomGenerator => StaticRandomGenerator.Instance; + + public static bool IsMiss(this IBattleEntityDump attacker, IBattleEntityDump defender, CalculationBasicStatistics basicStatistics) + { + bool isPvP = attacker.IsPlayer() && defender.IsPlayer(); + + int morale = basicStatistics.AttackerMorale; + int attackerHitRate = basicStatistics.AttackerHitRate; + int targetMorale = basicStatistics.DefenderMorale; + int targetDodge = basicStatistics.DefenderDodge; + + if (defender.HasBCard(BCardType.SpecialDefence, (byte)AdditionalTypes.SpecialDefence.NoDefence)) + { + return false; + } + + if (isPvP && attacker.GetShellWeaponEffectValue(ShellEffectType.NeverMissInPVP) > 0) + { + return false; + } + + if (attacker.IsMonster()) + { + IBattleEntity monster = attacker.MapInstance.GetBattleEntity(attacker.Type, attacker.Id); + if (monster != null && monster is IMonsterEntity mapMonster && mapMonster.MonsterRaceType == MonsterRaceType.Fixed && mapMonster.MonsterRaceSubType == 1) + { + return false; + } + } + + if (isPvP) + { + int shellChance = attacker.AttackType switch + { + AttackType.Melee => defender.GetShellArmorEffectValue(ShellEffectType.CloseDefenceDodgeInPVP), + AttackType.Ranged => defender.GetShellArmorEffectValue(ShellEffectType.DistanceDefenceDodgeInPVP), + AttackType.Magical => defender.GetShellArmorEffectValue(ShellEffectType.IgnoreMagicDamage), + _ => 0 + }; + + if (shellChance != 0 && _randomGenerator.RandomNumber() <= shellChance) + { + return true; + } + + if (attacker.AttackType == AttackType.Magical) + { + return false; + } + + shellChance = defender.GetShellArmorEffectValue(ShellEffectType.DodgeAllDamage); + if (shellChance != 0 && _randomGenerator.RandomNumber() <= shellChance) + { + return true; + } + } + + if (attacker.AttackType == AttackType.Magical) + { + return false; + } + + int attackerHitRateFinal = attackerHitRate + morale * 4; + double targetDodgeFinal = targetDodge + targetMorale * 4; + double difference = attackerHitRateFinal - targetDodgeFinal; + + // formula by friends111 + double chance = 100 / Math.PI * Math.Atan(0.015 * difference + 2) + 50; + if (chance <= 40) + { + chance = 40; + } + + double randomChance = _randomGenerator.RandomNumber() * 1.0; + if (attacker.GetBCardInformation(BCardType.GuarantedDodgeRangedAttack, (byte)AdditionalTypes.GuarantedDodgeRangedAttack.AttackHitChance).count == 0) + { + if (chance <= randomChance) + { + return true; + } + } + else + { + chance = attacker.GetBCardInformation(BCardType.GuarantedDodgeRangedAttack, (byte)AdditionalTypes.GuarantedDodgeRangedAttack.AttackHitChance).firstData; + if (chance <= randomChance) + { + return true; + } + } + + return false; + } + + public static CalculationBasicStatistics CalculateBasicStatistics(this IBattleEntityDump attacker, IBattleEntityDump defender, SkillInfo skill) + { + bool isPvP = attacker.IsPlayer() && defender.IsPlayer(); + + #region Attacker + + int attackerMorale = attacker.Morale; + int attackerAttackUpgrade = attacker.AttackUpgrade; + int attackerHitRate = attacker.HitRate; + int attackerCriticalChance = attacker.CriticalChance; + int attackerCriticalDamage = attacker.CriticalDamage; + + int attackerElementRate = attacker.ElementRate; + + #endregion + + #region Defender + + int defenderMorale = defender.Morale; + int defenderDefenseUpgrade = defender.DefenseUpgrade; + + int defenderDefense = attacker.AttackType switch + { + AttackType.Melee => defender.MeleeDefense, + AttackType.Ranged => defender.RangeDefense, + AttackType.Magical => defender.MagicalDefense + }; + + int defenderDodge = attacker.AttackType switch + { + AttackType.Melee => defender.MeleeDodge, + AttackType.Ranged => defender.RangeDodge, + AttackType.Magical => 0 + }; + + int defenderResistance = attacker.Element switch + { + ElementType.Neutral => 0, + ElementType.Fire => defender.FireResistance, + ElementType.Water => defender.WaterResistance, + ElementType.Light => defender.LightResistance, + ElementType.Shadow => defender.ShadowResistance + }; + + #endregion + + /* MORALE */ + + if (!defender.HasBCard(BCardType.Morale, (byte)AdditionalTypes.Morale.LockMorale)) // 17 | 2 + { + attackerMorale += attacker.GetBCardInformation(BCardType.Morale, (byte)AdditionalTypes.Morale.MoraleIncreased).firstData; + attackerMorale -= attacker.GetBCardInformation(BCardType.Morale, (byte)AdditionalTypes.Morale.MoraleDecreased).firstData; + } + + if (!attacker.HasBCard(BCardType.Morale, (byte)AdditionalTypes.Morale.IgnoreEnemyMorale)) // 17 | 4 + { + defenderMorale += defender.GetBCardInformation(BCardType.Morale, (byte)AdditionalTypes.Morale.MoraleIncreased).firstData; + defenderMorale -= defender.GetBCardInformation(BCardType.Morale, (byte)AdditionalTypes.Morale.MoraleDecreased).firstData; + } + + if (attacker.HasBCard(BCardType.Morale, (byte)AdditionalTypes.Morale.MoraleHalved)) + { + attackerMorale /= 2; + } + + if (attacker.HasBCard(BCardType.Morale, (byte)AdditionalTypes.Morale.MoraleDoubled)) + { + attackerMorale *= 2; + } + + if (defender.HasBCard(BCardType.Morale, (byte)AdditionalTypes.Morale.MoraleHalved)) + { + defenderMorale /= 2; + } + + if (defender.HasBCard(BCardType.Morale, (byte)AdditionalTypes.Morale.MoraleDoubled)) + { + defenderMorale *= 2; + } + + /* ATTACK UPGRADE */ + + attackerAttackUpgrade += attacker.GetBCardInformation(BCardType.AttackPower, (byte)AdditionalTypes.AttackPower.AttackLevelIncreased).firstData; + attackerAttackUpgrade -= attacker.GetBCardInformation(BCardType.AttackPower, (byte)AdditionalTypes.AttackPower.AttackLevelDecreased).firstData; + + if (attacker.HasBCard(BCardType.CalculatingLevel, (byte)AdditionalTypes.CalculatingLevel.CalculatedAttackLevel) && attackerAttackUpgrade > 0) + { + attackerAttackUpgrade = 0; + } + + /* DEFENSE UPGRADE */ + + defenderDefenseUpgrade += defender.GetBCardInformation(BCardType.Defence, (byte)AdditionalTypes.Defence.DefenceLevelIncreased).firstData; + defenderDefenseUpgrade -= defender.GetBCardInformation(BCardType.Defence, (byte)AdditionalTypes.Defence.DefenceLevelDecreased).firstData; + + if (defender.HasBCard(BCardType.CalculatingLevel, (byte)AdditionalTypes.CalculatingLevel.CalculatedDefenceLevel) && defenderDefenseUpgrade > 0) + { + defenderDefenseUpgrade = 0; + } + + /* HITRATE */ + + attackerHitRate += attacker.GetBCardInformation(BCardType.Target, (byte)AdditionalTypes.Target.AllHitRateIncreased).firstData; + attackerHitRate -= attacker.GetBCardInformation(BCardType.Target, (byte)AdditionalTypes.Target.AllHitRateDecreased).firstData; + attackerHitRate += attacker.AttackType switch + { + AttackType.Melee => attacker.GetBCardInformation(BCardType.Target, (byte)AdditionalTypes.Target.MeleeHitRateIncreased).firstData, + AttackType.Ranged => attacker.GetBCardInformation(BCardType.Target, (byte)AdditionalTypes.Target.RangedHitRateIncreased).firstData, + _ => 0 + }; + + attackerHitRate -= attacker.AttackType switch + { + AttackType.Melee => attacker.GetBCardInformation(BCardType.Target, (byte)AdditionalTypes.Target.MeleeHitRateDecreased).firstData, + AttackType.Ranged => attacker.GetBCardInformation(BCardType.Target, (byte)AdditionalTypes.Target.RangedHitRateDecreased).firstData, + _ => 0 + }; + + attackerHitRate += (int)(attackerHitRate * attacker.GetMultiplier(attacker.GetBCardInformation(BCardType.IncreaseSpPoints, (byte)AdditionalTypes.IncreaseSpPoints.AccuracyIncrease).firstData)); + attackerHitRate -= (int)(attackerHitRate * attacker.GetMultiplier(attacker.GetBCardInformation(BCardType.IncreaseSpPoints, (byte)AdditionalTypes.IncreaseSpPoints.AccuracyDecrease).firstData)); + attackerHitRate += (int)(attackerHitRate * attacker.GetMultiplier(attacker.GetBCardInformation(BCardType.MagicShield, (byte)AdditionalTypes.MagicShield.AccuracyIncrease).firstData)); + + /* CRITICAL CHANCE */ + + attackerCriticalChance += attacker.GetBCardInformation(BCardType.Critical, (byte)AdditionalTypes.Critical.InflictingIncreased).firstData; + attackerCriticalChance -= attacker.GetBCardInformation(BCardType.Critical, (byte)AdditionalTypes.Critical.InflictingReduced).firstData; + + attackerCriticalChance -= defender.GetBCardInformation(BCardType.Critical, (byte)AdditionalTypes.Critical.DamageIncreasedInflictingReduced).firstData; + + attackerCriticalChance += attacker.GetShellWeaponEffectValue(ShellEffectType.CriticalChance); + attackerCriticalChance -= defender.GetShellArmorEffectValue(ShellEffectType.ReducedCritChanceRecive); + + attackerCriticalChance += defender.GetBCardInformation(BCardType.Critical, (byte)AdditionalTypes.Critical.ReceivingIncreased).firstData; + attackerCriticalChance -= defender.GetBCardInformation(BCardType.Critical, (byte)AdditionalTypes.Critical.ReceivingDecreased).firstData; + + if (defender.IsMonster()) + { + MonsterRaceType monsterRaceType = defender.MonsterRaceType; + Enum monsterRaceSubType = defender.MonsterRaceSubType; + + if (attacker.HasBCard(BCardType.SpecialisationBuffResistance, (byte)AdditionalTypes.SpecialisationBuffResistance.IncreaseCriticalAgainst)) + { + (int firstData, int secondData, int count) raceBCard = + attacker.GetBCardInformation(BCardType.SpecialisationBuffResistance, (byte)AdditionalTypes.SpecialisationBuffResistance.IncreaseCriticalAgainst); + + int monsterRace = raceBCard.firstData; + var bCardRaceType = (MonsterRaceType)Math.Floor(monsterRace / 10.0); + Enum bCardRaceSubType = attacker.GetRaceSubType(bCardRaceType, (byte)(monsterRace % 10)); + + if (monsterRaceType == bCardRaceType && bCardRaceSubType != null && Equals(monsterRaceSubType, bCardRaceSubType)) + { + attackerCriticalChance += raceBCard.secondData; + } + } + } + + if (defender.HasBCard(BCardType.SniperAttack, (byte)AdditionalTypes.SniperAttack.ReceiveCriticalFromSniper) && skill != null && skill.Vnum == (short)SkillsVnums.SNIPER) + { + attackerCriticalChance = defender.GetBCardInformation(BCardType.SniperAttack, (byte)AdditionalTypes.SniperAttack.ReceiveCriticalFromSniper).firstData; + } + + if (defender.HasBCard(BCardType.SpecialCritical, (byte)AdditionalTypes.SpecialCritical.InflictingChancePercent)) + { + attackerCriticalChance = defender.GetBCardInformation(BCardType.SpecialCritical, (byte)AdditionalTypes.SpecialCritical.InflictingChancePercent).firstData; + } + + if (defender.HasBCard(BCardType.SpecialCritical, (byte)AdditionalTypes.SpecialCritical.ReceivingChancePercent)) + { + attackerCriticalChance = defender.GetBCardInformation(BCardType.SpecialCritical, (byte)AdditionalTypes.SpecialCritical.ReceivingChancePercent).firstData; + } + + if (attacker.HasBCard(BCardType.SpecialCritical, (byte)AdditionalTypes.SpecialCritical.AlwaysInflict)) + { + attackerCriticalChance = 100; + } + + if (defender.HasBCard(BCardType.SpecialCritical, (byte)AdditionalTypes.SpecialCritical.AlwaysReceives)) + { + attackerCriticalChance = 100; + } + + if (defender.HasBCard(BCardType.SpecialCritical, (byte)AdditionalTypes.SpecialCritical.NeverReceives)) + { + attackerCriticalChance = 0; + } + + if (attacker.HasBCard(BCardType.SpecialCritical, (byte)AdditionalTypes.SpecialCritical.NeverInflict)) + { + attackerCriticalChance = 0; + } + + /* CRITICAL DAMAGE */ + + int damageCriticalIncrease = attacker.GetBCardInformation(BCardType.Critical, (byte)AdditionalTypes.Critical.DamageIncreased).firstData; + + if (damageCriticalIncrease != 0) + { + damageCriticalIncrease -= defender.GetBCardInformation(BCardType.Critical, (byte)AdditionalTypes.Critical.DamageIncreasedInflictingReduced).firstData; + } + + attackerCriticalDamage += attacker.GetShellWeaponEffectValue(ShellEffectType.CriticalDamage); + attackerCriticalDamage += damageCriticalIncrease; + attackerCriticalDamage -= defender.GetJewelsEffectValue(CellonType.CriticalDamageDecrease); + attackerCriticalDamage += defender.GetBCardInformation(BCardType.Critical, (byte)AdditionalTypes.Critical.DamageFromCriticalIncreased).firstData; + attackerCriticalDamage -= defender.GetBCardInformation(BCardType.Critical, (byte)AdditionalTypes.Critical.DamageFromCriticalDecreased).firstData; + + if (defender.HasBCard(BCardType.StealBuff, (byte)AdditionalTypes.StealBuff.ReduceCriticalReceivedChance)) + { + (int firstData, int secondData, _) = defender.GetBCardInformation(BCardType.StealBuff, (byte)AdditionalTypes.StealBuff.ReduceCriticalReceivedChance); + if (defender.IsSucceededChance(firstData)) + { + attackerCriticalDamage -= secondData; + } + } + + /* ELEMENT RATE */ + + if (attacker.HasBCard(BCardType.IncreaseElementFairy, (byte)AdditionalTypes.IncreaseElementFairy.FairyElementIncreaseWhileAttackingChance) && attacker.Element != ElementType.Neutral) + { + (int firstData, int secondData, _) = attacker.GetBCardInformation(BCardType.IncreaseElementFairy, (byte)AdditionalTypes.IncreaseElementFairy.FairyElementIncreaseWhileAttackingChance); + if (attacker.IsSucceededChance(firstData)) + { + attackerElementRate += secondData; + } + } + + /* DEFENSE */ + + defenderDefense += defender.GetBCardInformation(BCardType.Defence, (byte)AdditionalTypes.Defence.AllIncreased).firstData; + defenderDefense -= defender.GetBCardInformation(BCardType.Defence, (byte)AdditionalTypes.Defence.AllDecreased).firstData; + + defenderDefense += attacker.AttackType switch + { + AttackType.Melee => defender.GetBCardInformation(BCardType.Defence, (byte)AdditionalTypes.Defence.MeleeIncreased).firstData, + AttackType.Ranged => defender.GetBCardInformation(BCardType.Defence, (byte)AdditionalTypes.Defence.RangedIncreased).firstData, + AttackType.Magical => defender.GetBCardInformation(BCardType.Defence, (byte)AdditionalTypes.Defence.MagicalIncreased).firstData + }; + + defenderDefense += attacker.AttackType switch + { + AttackType.Melee => defender.GetShellArmorEffectValue(ShellEffectType.CloseDefence), + AttackType.Ranged => defender.GetShellArmorEffectValue(ShellEffectType.DistanceDefence), + AttackType.Magical => defender.GetShellArmorEffectValue(ShellEffectType.MagicDefence) + }; + + defenderDefense -= attacker.AttackType switch + { + AttackType.Melee => defender.GetBCardInformation(BCardType.Defence, (byte)AdditionalTypes.Defence.MeleeDecreased).firstData, + AttackType.Ranged => defender.GetBCardInformation(BCardType.Defence, (byte)AdditionalTypes.Defence.RangedDecreased).firstData, + AttackType.Magical => defender.GetBCardInformation(BCardType.Defence, (byte)AdditionalTypes.Defence.MagicalDecreased).firstData + }; + + defenderDefense += (int)(defenderDefense * defender.GetMultiplier(defender.GetShellArmorEffectValue(ShellEffectType.PercentageTotalDefence))); + defenderDefense += (int)(defenderDefense * + defender.GetMultiplier(defender.GetBCardInformation(BCardType.DodgeAndDefencePercent, (byte)AdditionalTypes.DodgeAndDefencePercent.DefenceIncreased).firstData)); + defenderDefense -= (int)(defenderDefense * + defender.GetMultiplier(defender.GetBCardInformation(BCardType.DodgeAndDefencePercent, (byte)AdditionalTypes.DodgeAndDefencePercent.DefenceReduced).firstData)); + + bool nullifiedDefense = attacker.AttackType switch + { + AttackType.Melee => defender.HasBCard(BCardType.SpecialDefence, (byte)AdditionalTypes.SpecialDefence.MeleeDefenceNullified), + AttackType.Ranged => defender.HasBCard(BCardType.SpecialDefence, (byte)AdditionalTypes.SpecialDefence.RangedDefenceNullified), + AttackType.Magical => defender.HasBCard(BCardType.SpecialDefence, (byte)AdditionalTypes.SpecialDefence.MagicDefenceNullified) + }; + + if (nullifiedDefense) + { + defenderDefense = 0; + } + + if (defender.HasBCard(BCardType.SpecialDefence, (byte)AdditionalTypes.SpecialDefence.AllDefenceNullified)) + { + defenderDefense = 0; + } + + /* DODGE */ + + defenderDodge += defender.GetBCardInformation(BCardType.DodgeAndDefencePercent, (byte)AdditionalTypes.DodgeAndDefencePercent.DodgeIncreased).firstData; + defenderDodge -= defender.GetBCardInformation(BCardType.DodgeAndDefencePercent, (byte)AdditionalTypes.DodgeAndDefencePercent.DodgeDecreased).firstData; + + defenderDodge += (int)(defenderDodge * defender.GetMultiplier(defender.GetBCardInformation(BCardType.MagicShield, (byte)AdditionalTypes.MagicShield.DodgeIncrease).firstData)); + + defenderDodge += attacker.AttackType switch + { + AttackType.Melee => defender.GetBCardInformation(BCardType.DodgeAndDefencePercent, (byte)AdditionalTypes.DodgeAndDefencePercent.DodgingMeleeIncreased).firstData, + AttackType.Ranged => defender.GetBCardInformation(BCardType.DodgeAndDefencePercent, (byte)AdditionalTypes.DodgeAndDefencePercent.DodgingRangedIncreased).firstData, + AttackType.Magical => 0 + }; + + defenderDodge -= attacker.AttackType switch + { + AttackType.Melee => defender.GetBCardInformation(BCardType.DodgeAndDefencePercent, (byte)AdditionalTypes.DodgeAndDefencePercent.DodgingMeleeDecreased).firstData, + AttackType.Ranged => defender.GetBCardInformation(BCardType.DodgeAndDefencePercent, (byte)AdditionalTypes.DodgeAndDefencePercent.DodgingRangedDecreased).firstData, + AttackType.Magical => 0 + }; + + /* RESISTANCE */ + + defenderResistance -= attacker.GetBCardInformation(BCardType.EnemyElementResistance, (byte)AdditionalTypes.EnemyElementResistance.AllDecreased).firstData; + defenderResistance -= attacker.Element switch + { + ElementType.Neutral => 0, + ElementType.Fire => attacker.GetBCardInformation(BCardType.EnemyElementResistance, (byte)AdditionalTypes.EnemyElementResistance.FireDecreased).firstData, + ElementType.Water => attacker.GetBCardInformation(BCardType.EnemyElementResistance, (byte)AdditionalTypes.EnemyElementResistance.WaterDecreased).firstData, + ElementType.Light => attacker.GetBCardInformation(BCardType.EnemyElementResistance, (byte)AdditionalTypes.EnemyElementResistance.LightDecreased).firstData, + ElementType.Shadow => attacker.GetBCardInformation(BCardType.EnemyElementResistance, (byte)AdditionalTypes.EnemyElementResistance.DarkDecreased).firstData + }; + + defenderResistance += attacker.GetBCardInformation(BCardType.EnemyElementResistance, (byte)AdditionalTypes.EnemyElementResistance.AllIncreased).firstData; + defenderResistance += attacker.Element switch + { + ElementType.Neutral => 0, + ElementType.Fire => attacker.GetBCardInformation(BCardType.EnemyElementResistance, (byte)AdditionalTypes.EnemyElementResistance.FireIncreased).firstData, + ElementType.Water => attacker.GetBCardInformation(BCardType.EnemyElementResistance, (byte)AdditionalTypes.EnemyElementResistance.WaterIncreased).firstData, + ElementType.Light => attacker.GetBCardInformation(BCardType.EnemyElementResistance, (byte)AdditionalTypes.EnemyElementResistance.LightIncreased).firstData, + ElementType.Shadow => attacker.GetBCardInformation(BCardType.EnemyElementResistance, (byte)AdditionalTypes.EnemyElementResistance.DarkIncreased).firstData + }; + + defenderResistance -= defender.GetBCardInformation(BCardType.ElementResistance, (byte)AdditionalTypes.ElementResistance.AllDecreased).firstData; + defenderResistance -= attacker.Element switch + { + ElementType.Neutral => 0, + ElementType.Fire => defender.GetBCardInformation(BCardType.ElementResistance, (byte)AdditionalTypes.ElementResistance.FireDecreased).firstData, + ElementType.Water => defender.GetBCardInformation(BCardType.ElementResistance, (byte)AdditionalTypes.ElementResistance.WaterDecreased).firstData, + ElementType.Light => defender.GetBCardInformation(BCardType.ElementResistance, (byte)AdditionalTypes.ElementResistance.LightDecreased).firstData, + ElementType.Shadow => defender.GetBCardInformation(BCardType.ElementResistance, (byte)AdditionalTypes.ElementResistance.DarkDecreased).firstData + }; + + defenderResistance += defender.GetBCardInformation(BCardType.ElementResistance, (byte)AdditionalTypes.ElementResistance.AllIncreased).firstData; + defenderResistance += attacker.Element switch + { + ElementType.Neutral => 0, + ElementType.Fire => defender.GetBCardInformation(BCardType.ElementResistance, (byte)AdditionalTypes.ElementResistance.FireIncreased).firstData, + ElementType.Water => defender.GetBCardInformation(BCardType.ElementResistance, (byte)AdditionalTypes.ElementResistance.WaterIncreased).firstData, + ElementType.Light => defender.GetBCardInformation(BCardType.ElementResistance, (byte)AdditionalTypes.ElementResistance.LightIncreased).firstData, + ElementType.Shadow => defender.GetBCardInformation(BCardType.ElementResistance, (byte)AdditionalTypes.ElementResistance.DarkIncreased).firstData + }; + + defenderResistance += defender.GetShellArmorEffectValue(ShellEffectType.IncreasedAllResistance); + defenderResistance += attacker.Element switch + { + ElementType.Neutral => 0, + ElementType.Fire => defender.GetShellArmorEffectValue(ShellEffectType.IncreasedFireResistance), + ElementType.Water => defender.GetShellArmorEffectValue(ShellEffectType.IncreasedWaterResistance), + ElementType.Light => defender.GetShellArmorEffectValue(ShellEffectType.IncreasedLightResistance), + ElementType.Shadow => defender.GetShellArmorEffectValue(ShellEffectType.IncreasedDarkResistance) + }; + + defenderResistance += attacker.Element switch + { + ElementType.Neutral => 0, + ElementType.Fire => defender.GetFamilyUpgradeValue(FamilyUpgradeType.FIRE_RESISTANCE), + ElementType.Water => defender.GetFamilyUpgradeValue(FamilyUpgradeType.WATER_RESISTANCE), + ElementType.Light => defender.GetFamilyUpgradeValue(FamilyUpgradeType.LIGHT_RESISTANCE), + ElementType.Shadow => defender.GetFamilyUpgradeValue(FamilyUpgradeType.DARK_RESISTANCE) + }; + + defenderResistance += (int)(defenderResistance * + defender.GetMultiplier(defender.GetBCardInformation(BCardType.IncreaseElementByResis, (byte)AdditionalTypes.IncreaseElementByResis.AllElementResisIncrease).firstData)); + defenderResistance -= (int)(defenderResistance * + defender.GetMultiplier(defender.GetBCardInformation(BCardType.IncreaseElementByResis, (byte)AdditionalTypes.IncreaseElementByResis.AllElementResisDecrease).firstData)); + + defenderResistance += attacker.Element switch + { + ElementType.Neutral => 0, + ElementType.Fire => (int)(defenderResistance * defender.GetMultiplier(defender.GetBCardInformation( + BCardType.IncreaseElementByResis, (byte)AdditionalTypes.IncreaseElementByResis.FireElementResisIncrease).firstData)), + ElementType.Water => (int)(defenderResistance * defender.GetMultiplier(defender.GetBCardInformation( + BCardType.IncreaseElementByResis, (byte)AdditionalTypes.IncreaseElementByResis.WaterElementResisIncrease).firstData)), + ElementType.Light => (int)(defenderResistance * defender.GetMultiplier(defender.GetBCardInformation( + BCardType.IncreaseElementByResis, (byte)AdditionalTypes.IncreaseElementByResis.LightElementResisIncrease).firstData)), + ElementType.Shadow => (int)(defenderResistance * defender.GetMultiplier(defender.GetBCardInformation( + BCardType.IncreaseElementByResis, (byte)AdditionalTypes.IncreaseElementByResis.ShadowElementResisIncrease).firstData)) + }; + + defenderResistance -= attacker.Element switch + { + ElementType.Neutral => 0, + ElementType.Fire => (int)(defenderResistance * defender.GetMultiplier(defender.GetBCardInformation( + BCardType.IncreaseElementByResis, (byte)AdditionalTypes.IncreaseElementByResis.FireElementResisDecrease).firstData)), + ElementType.Water => (int)(defenderResistance * defender.GetMultiplier(defender.GetBCardInformation( + BCardType.IncreaseElementByResis, (byte)AdditionalTypes.IncreaseElementByResis.WaterElementResisDecrease).firstData)), + ElementType.Light => (int)(defenderResistance * defender.GetMultiplier(defender.GetBCardInformation( + BCardType.IncreaseElementByResis, (byte)AdditionalTypes.IncreaseElementByResis.LightElementResisDecrease).firstData)), + ElementType.Shadow => (int)(defenderResistance * defender.GetMultiplier(defender.GetBCardInformation( + BCardType.IncreaseElementByResis, (byte)AdditionalTypes.IncreaseElementByResis.ShadowElementResisDecrease).firstData)) + }; + + if (isPvP) + { + defenderResistance -= (int)(defenderResistance * attacker.GetShellWeaponEffectValue(ShellEffectType.ReducesEnemyAllResistancesInPVP) * 0.01); + defenderResistance -= attacker.Element switch + { + ElementType.Neutral => 0, + ElementType.Fire => (int)(defenderResistance * attacker.GetShellWeaponEffectValue(ShellEffectType.ReducesEnemyFireResistanceInPVP) * 0.01), + ElementType.Water => (int)(defenderResistance * attacker.GetShellWeaponEffectValue(ShellEffectType.ReducesEnemyWaterResistanceInPVP) * 0.01), + ElementType.Light => (int)(defenderResistance * attacker.GetShellWeaponEffectValue(ShellEffectType.ReducesEnemyLightResistanceInPVP) * 0.01), + ElementType.Shadow => (int)(defenderResistance * attacker.GetShellWeaponEffectValue(ShellEffectType.ReducesEnemyDarkResistanceInPVP) * 0.01) + }; + } + + if (defender.HasBCard(BCardType.NoCharacteristicValue, (byte)AdditionalTypes.NoCharacteristicValue.AllResistancesNullified)) + { + defenderResistance = 0; + } + + bool removeResistance = attacker.Element switch + { + ElementType.Neutral => false, + ElementType.Fire => defender.HasBCard(BCardType.NoCharacteristicValue, (byte)AdditionalTypes.NoCharacteristicValue.FireResistanceNullified), + ElementType.Water => defender.HasBCard(BCardType.NoCharacteristicValue, (byte)AdditionalTypes.NoCharacteristicValue.WaterResistanceNullified), + ElementType.Light => defender.HasBCard(BCardType.NoCharacteristicValue, (byte)AdditionalTypes.NoCharacteristicValue.LightResistanceNullified), + ElementType.Shadow => defender.HasBCard(BCardType.NoCharacteristicValue, (byte)AdditionalTypes.NoCharacteristicValue.DarkResistanceNullified) + }; + + if (removeResistance) + { + defenderResistance = 0; + } + + return new CalculationBasicStatistics + { + AttackerMorale = attackerMorale, + AttackerAttackUpgrade = attackerAttackUpgrade, + AttackerHitRate = attackerHitRate, + AttackerCriticalChance = attackerCriticalChance, + AttackerCriticalDamage = attackerCriticalDamage, + AttackerElementRate = attackerElementRate, + DefenderMorale = defenderMorale, + DefenderDefenseUpgrade = defenderDefenseUpgrade, + DefenderDefense = defenderDefense, + DefenderDodge = defenderDodge, + DefenderResistance = defenderResistance + }; + } + + public static CalculationPhysicalDamage CalculatePhysicalDamage(this IBattleEntityDump attacker, IBattleEntityDump defender, SkillInfo skill) + { + bool isPvP = attacker.IsPlayer() && defender.IsPlayer(); + + double damagePercentage = 1; + double damagePercentageSecond = 0; + double ignoreEnemyDefense = 1; + double vesselLodDamage = 1; + double vesselGlacernonDamage = 1; + double increaseAllDamage = 1; + double increaseAllDamageAttackType = 1; + double increaseDamageMagicDefense = 0; + int increaseDamageRace = 0; + double increaseDamageRacePercentage = 1; + double increaseLoDDamage = 1; + double increaseVesselDamage = 1; + double increaseDamageFaction = 1; + double increaseDamageInPvP = 1; + double increaseDamageVersusMonstersMapType = 1; + double increaseAllDamageVersusMonsters = 1; + + /* DAMAGE */ + int damage = attacker.TryFindPartnerSkillInformation(BCardType.AttackPower, (byte)AdditionalTypes.AttackPower.AllAttacksIncreased, skill).firstData; + damage -= attacker.GetBCardInformation(BCardType.AttackPower, (byte)AdditionalTypes.AttackPower.AllAttacksDecreased).firstData; + + damage += attacker.AttackType switch + { + AttackType.Melee => attacker.TryFindPartnerSkillInformation(BCardType.AttackPower, (byte)AdditionalTypes.AttackPower.MeleeAttacksIncreased, skill).firstData, + AttackType.Ranged => attacker.TryFindPartnerSkillInformation(BCardType.AttackPower, (byte)AdditionalTypes.AttackPower.RangedAttacksIncreased, skill).firstData, + AttackType.Magical => attacker.TryFindPartnerSkillInformation(BCardType.AttackPower, (byte)AdditionalTypes.AttackPower.MagicalAttacksIncreased, skill).firstData + }; + + damage -= attacker.AttackType switch + { + AttackType.Melee => attacker.GetBCardInformation(BCardType.AttackPower, (byte)AdditionalTypes.AttackPower.MeleeAttacksDecreased).firstData, + AttackType.Ranged => attacker.GetBCardInformation(BCardType.AttackPower, (byte)AdditionalTypes.AttackPower.RangedAttacksDecreased).firstData, + AttackType.Magical => attacker.GetBCardInformation(BCardType.AttackPower, (byte)AdditionalTypes.AttackPower.MagicalAttacksDecreased).firstData + }; + + if (isPvP) + { + damage += attacker.GetBCardInformation(BCardType.SpecialisationBuffResistance, (byte)AdditionalTypes.SpecialisationBuffResistance.IncreaseDamageInPVP).firstData; + damage -= attacker.GetBCardInformation(BCardType.SpecialisationBuffResistance, (byte)AdditionalTypes.SpecialisationBuffResistance.DecreaseDamageInPVP).firstData; + } + + /* DAMAGE PERCENTAGE */ + if (attacker.HasBCard(BCardType.Critical, (byte)AdditionalTypes.Critical.DamageIncreasingChance)) + { + (int firstData, int secondData, _) = attacker.GetBCardInformation(BCardType.Critical, (byte)AdditionalTypes.Critical.DamageIncreasingChance); + if (attacker.IsSucceededChance(firstData)) + { + damagePercentage *= 1 + secondData * 0.01; + } + } + + /* DAMAGE PERCENTAGE - SOFT DAMAGE */ + if (attacker.HasBCard(BCardType.IncreaseDamage, (byte)AdditionalTypes.IncreaseDamage.IncreasingProbability)) + { + (int firstData, int secondData, _) = attacker.GetBCardInformation(BCardType.IncreaseDamage, (byte)AdditionalTypes.IncreaseDamage.IncreasingProbability); + if (attacker.IsSucceededChance(firstData)) + { + damagePercentageSecond = secondData * 0.01; + } + } + + if (damagePercentageSecond != 0) + { + if (attacker.HasBCard(BCardType.IncreaseDamage, (byte)AdditionalTypes.IncreaseDamage.DecreasingProbability)) + { + (int firstData, int secondData, _) = attacker.GetBCardInformation(BCardType.IncreaseDamage, (byte)AdditionalTypes.IncreaseDamage.DecreasingProbability); + if (attacker.IsSucceededChance(firstData)) + { + damagePercentageSecond = secondData * 0.01 <= 0 ? 0 : damagePercentageSecond - secondData * 0.01; + } + } + } + + /* MULTIPLY DAMAGE */ + double multiplyDamage = 1; + int multiplyDamageInt = attacker.GetBCardInformation(BCardType.MultAttack, (byte)AdditionalTypes.MultAttack.AllAttackIncreased).firstData; + multiplyDamageInt += attacker.AttackType switch + { + AttackType.Melee => attacker.GetBCardInformation(BCardType.MultAttack, (byte)AdditionalTypes.MultAttack.MeleeAttackIncreased).firstData, + AttackType.Ranged => attacker.GetBCardInformation(BCardType.MultAttack, (byte)AdditionalTypes.MultAttack.RangedAttackIncreased).firstData, + AttackType.Magical => attacker.GetBCardInformation(BCardType.MultAttack, (byte)AdditionalTypes.MultAttack.MagicalAttackIncreased).firstData + }; + + multiplyDamageInt -= attacker.GetBCardInformation(BCardType.MultAttack, (byte)AdditionalTypes.MultAttack.AllAttackDecreased).firstData; + multiplyDamageInt -= attacker.AttackType switch + { + AttackType.Melee => attacker.GetBCardInformation(BCardType.MultAttack, (byte)AdditionalTypes.MultAttack.MeleeAttackDecreased).firstData, + AttackType.Ranged => attacker.GetBCardInformation(BCardType.MultAttack, (byte)AdditionalTypes.MultAttack.RangedAttackDecreased).firstData, + AttackType.Magical => attacker.GetBCardInformation(BCardType.MultAttack, (byte)AdditionalTypes.MultAttack.MagicalAttackDecreased).firstData + }; + + multiplyDamage = multiplyDamageInt == 0 ? 1 : multiplyDamageInt; + + /* INVISIBLE DAMAGE */ + int invisibleDamage = attacker.IsInvisible() ? attacker.GetBCardInformation(BCardType.LightAndShadow, (byte)AdditionalTypes.LightAndShadow.AdditionalDamageWhenHidden).firstData : 0; + + /* END CRITICAL DAMAGE */ + double endCriticalDamage = 1; + endCriticalDamage += attacker.GetBCardInformation(BCardType.Count, (byte)AdditionalTypes.Count.IncreaseCritAttackOnEnd).firstData; + + /* INCREASE DAMAGE BY RACE */ + if (defender.IsMonster()) + { + MonsterRaceType monsterRaceType = defender.MonsterRaceType; + Enum monsterRaceSubType = defender.MonsterRaceSubType; + + int monsterRace; + + MonsterRaceType bCardRaceType; + Enum bCardRaceSubType; + if (attacker.HasBCard(BCardType.SpecialisationBuffResistance, (byte)AdditionalTypes.SpecialisationBuffResistance.IncreaseDamageAgainst)) + { + (int firstData, int secondData, int count) raceBCard = + attacker.GetBCardInformation(BCardType.SpecialisationBuffResistance, (byte)AdditionalTypes.SpecialisationBuffResistance.IncreaseDamageAgainst); + + monsterRace = raceBCard.firstData; + bCardRaceType = (MonsterRaceType)Math.Floor(monsterRace / 10.0); + bCardRaceSubType = attacker.GetRaceSubType(bCardRaceType, (byte)(monsterRace % 10)); + + if (monsterRaceType == bCardRaceType && bCardRaceSubType != null && Equals(monsterRaceSubType, bCardRaceSubType)) + { + increaseDamageRace += raceBCard.secondData; + } + } + + if (attacker.HasBCard(BCardType.LeonaPassiveSkill, (byte)AdditionalTypes.LeonaPassiveSkill.IncreaseDamageAgainst)) + { + (int firstData, int secondData, int count) raceBCard = + attacker.GetBCardInformation(BCardType.LeonaPassiveSkill, (byte)AdditionalTypes.LeonaPassiveSkill.IncreaseDamageAgainst); + + monsterRace = raceBCard.firstData; + bCardRaceType = (MonsterRaceType)Math.Floor(monsterRace / 10.0); + bCardRaceSubType = attacker.GetRaceSubType(bCardRaceType, (byte)(monsterRace % 10)); + + if (monsterRaceType == bCardRaceType && bCardRaceSubType != null && Equals(monsterRaceSubType, bCardRaceSubType)) + { + increaseDamageRacePercentage += raceBCard.secondData * 0.01; + } + } + + switch (monsterRaceSubType) + { + case MonsterSubRace.LowLevel.Animal: + increaseDamageRacePercentage += attacker.GetShellWeaponEffectValue(ShellEffectType.DamageIncreasedSmallMonster) * 0.01; + increaseDamageRacePercentage += attacker.GetShellWeaponEffectValue(ShellEffectType.DamageIncreasedAnimal) * 0.01; + break; + case MonsterSubRace.HighLevel.Animal: + increaseDamageRacePercentage += attacker.GetShellWeaponEffectValue(ShellEffectType.DamageIncreasedBigMonster) * 0.01; + increaseDamageRacePercentage += attacker.GetShellWeaponEffectValue(ShellEffectType.DamageIncreasedAnimal) * 0.01; + break; + case MonsterSubRace.LowLevel.Plant: + increaseDamageRacePercentage += attacker.GetShellWeaponEffectValue(ShellEffectType.DamageIncreasedSmallMonster) * 0.01; + increaseDamageRacePercentage += attacker.GetShellWeaponEffectValue(ShellEffectType.DamageIncreasedPlant) * 0.01; + break; + case MonsterSubRace.HighLevel.Plant: + increaseDamageRacePercentage += attacker.GetShellWeaponEffectValue(ShellEffectType.DamageIncreasedBigMonster) * 0.01; + increaseDamageRacePercentage += attacker.GetShellWeaponEffectValue(ShellEffectType.DamageIncreasedPlant) * 0.01; + break; + case MonsterSubRace.LowLevel.Monster: + increaseDamageRacePercentage += attacker.GetShellWeaponEffectValue(ShellEffectType.DamageIncreasedSmallMonster) * 0.01; + increaseDamageRacePercentage += attacker.GetShellWeaponEffectValue(ShellEffectType.DamageIncreasedEnemy) * 0.01; + break; + case MonsterSubRace.HighLevel.Monster: + increaseDamageRacePercentage += attacker.GetShellWeaponEffectValue(ShellEffectType.DamageIncreasedBigMonster) * 0.01; + increaseDamageRacePercentage += attacker.GetShellWeaponEffectValue(ShellEffectType.DamageIncreasedEnemy) * 0.01; + break; + case MonsterSubRace.Undead.LowLevelUndead: + increaseDamageRacePercentage += attacker.GetShellWeaponEffectValue(ShellEffectType.DamageIncreasedSmallMonster) * 0.01; + increaseDamageRacePercentage += attacker.GetShellWeaponEffectValue(ShellEffectType.DamageIncreasedUnDead) * 0.01; + break; + case MonsterSubRace.Undead.HighLevelUndead: + increaseDamageRacePercentage += attacker.GetShellWeaponEffectValue(ShellEffectType.DamageIncreasedBigMonster) * 0.01; + increaseDamageRacePercentage += attacker.GetShellWeaponEffectValue(ShellEffectType.DamageIncreasedUnDead) * 0.01; + break; + case MonsterSubRace.Undead.Vampire: + increaseDamageRacePercentage += attacker.GetShellWeaponEffectValue(ShellEffectType.DamageIncreasedUnDead) * 0.01; + break; + case MonsterSubRace.Spirits.LowLevelGhost: + increaseDamageRacePercentage += attacker.GetShellWeaponEffectValue(ShellEffectType.DamageIncreasedSmallMonster) * 0.01; + break; + case MonsterSubRace.Spirits.HighLevelGhost: + increaseDamageRacePercentage += attacker.GetShellWeaponEffectValue(ShellEffectType.DamageIncreasedBigMonster) * 0.01; + break; + case MonsterSubRace.Spirits.LowLevelSpirit: + increaseDamageRacePercentage += attacker.GetShellWeaponEffectValue(ShellEffectType.DamageIncreasedSmallMonster) * 0.01; + break; + case MonsterSubRace.Spirits.HighLevelSpirit: + increaseDamageRacePercentage += attacker.GetShellWeaponEffectValue(ShellEffectType.DamageIncreasedBigMonster) * 0.01; + break; + } + } + + /* DECREASE DAMAGE BY RACE */ + if (defender.IsPlayer() && defender.HasBCard(BCardType.SpecialisationBuffResistance, (byte)AdditionalTypes.SpecialisationBuffResistance.ReduceDamageAgainst)) + { + (int firstData, int secondData, int count) raceBCard = + defender.GetBCardInformation(BCardType.SpecialisationBuffResistance, (byte)AdditionalTypes.SpecialisationBuffResistance.ReduceDamageAgainst); + + int monsterRace = raceBCard.firstData; + var bCardRaceType = (MonsterRaceType)Math.Floor(monsterRace / 10.0); + Enum bCardRaceSubType = attacker.GetRaceSubType(bCardRaceType, (byte)(monsterRace % 10)); + + if (attacker.MonsterRaceType == bCardRaceType && bCardRaceSubType != null && Equals(attacker.MonsterRaceSubType, bCardRaceSubType)) + { + increaseDamageRace -= raceBCard.secondData; + } + } + + /* IGNORE DEFENDER DEFENSE */ + if (attacker.HasBCard(BCardType.StealBuff, (byte)AdditionalTypes.StealBuff.IgnoreDefenceChance)) + { + (int firstData, int secondData, _) = attacker.GetBCardInformation(BCardType.StealBuff, (byte)AdditionalTypes.StealBuff.IgnoreDefenceChance); + if (attacker.IsSucceededChance(firstData)) + { + ignoreEnemyDefense -= secondData * 0.01; + attacker.BroadcastEffect(EffectType.IgnoreDefence); + } + } + + /* INCREASE DAMAGE BY FACTION */ + if (isPvP) + { + (int firstData, int secondData, int count) factionBCard; + + switch (attacker.Faction) + { + case FactionType.Angel: + factionBCard = attacker.GetBCardInformation(BCardType.ChangingPlace, (byte)AdditionalTypes.ChangingPlace.IncreaseDamageVersusDemons); + increaseDamageFaction += factionBCard.firstData * 0.01; + break; + case FactionType.Demon: + factionBCard = attacker.GetBCardInformation(BCardType.ChangingPlace, (byte)AdditionalTypes.ChangingPlace.IncreaseDamageVersusAngels); + increaseDamageFaction += factionBCard.firstData * 0.01; + break; + } + } + + /* INCREASE DAMAGE VS VESSEL MONSTERS */ + if (defender.IsMonster()) + { + bool isVesselMonster = defender is NpcMonsterEntityDump { IsVesselMonster: true }; + if (isVesselMonster) + { + (int firstData, int secondData, int count) vesselBCard = + attacker.GetBCardInformation(BCardType.IncreaseDamageVersus, (byte)AdditionalTypes.IncreaseDamageVersus.VesselAndLodMobDamageIncrease); + vesselLodDamage += vesselBCard.firstData * 0.01; + + vesselBCard = attacker.GetBCardInformation(BCardType.IncreaseDamageVersus, (byte)AdditionalTypes.IncreaseDamageVersus.VesselAndFrozenCrownMobDamageIncrease); + vesselGlacernonDamage += vesselBCard.firstData * 0.01; + + vesselBCard = attacker.GetBCardInformation(BCardType.IncreaseDamageInLoD, (byte)AdditionalTypes.IncreaseDamageInLoD.VesselMonstersAttackIncrease); + increaseVesselDamage += vesselBCard.firstData * 0.01; + } + } + + /* INCREASE DAMAGE VS LOD MONSTERS */ + if (attacker.MapInstance.MapId == (short)MapIds.LAND_OF_DEATH) + { + (int firstData, int secondData, int count) + lodBCard = attacker.GetBCardInformation(BCardType.IncreaseDamageVersus, (byte)AdditionalTypes.IncreaseDamageVersus.VesselAndLodMobDamageIncrease); + + vesselLodDamage += lodBCard.secondData * 0.01; + + lodBCard = attacker.GetBCardInformation(BCardType.IncreaseDamageInLoD, (byte)AdditionalTypes.IncreaseDamageInLoD.LodMonstersAttackIncrease); + increaseLoDDamage += lodBCard.firstData * 0.01; + } + + /* INCREASE ALL DAMAGE */ + increaseAllDamage += attacker.GetMultiplier(attacker.GetBCardInformation(BCardType.IncreaseAllDamage, (byte)AdditionalTypes.IncreaseAllDamage.AllAttackIncrease).firstData); + increaseAllDamage -= attacker.GetMultiplier(attacker.GetBCardInformation(BCardType.IncreaseAllDamage, (byte)AdditionalTypes.IncreaseAllDamage.AllAttackDecrease).firstData); + + /* INCREASE ALL DAMAGE BY ATTACK TYPE */ + increaseAllDamageAttackType += attacker.AttackType switch + { + AttackType.Melee => attacker.GetMultiplier(attacker.GetBCardInformation(BCardType.IncreaseAllDamage, (byte)AdditionalTypes.IncreaseAllDamage.MeleeAttackIncrease).firstData), + AttackType.Ranged => attacker.GetMultiplier(attacker.GetBCardInformation(BCardType.IncreaseAllDamage, (byte)AdditionalTypes.IncreaseAllDamage.RangeAttackIncrease).firstData), + AttackType.Magical => attacker.GetMultiplier(attacker.GetBCardInformation(BCardType.IncreaseAllDamage, (byte)AdditionalTypes.IncreaseAllDamage.MagicAttackIncrease).firstData) + }; + + increaseAllDamageAttackType -= attacker.AttackType switch + { + AttackType.Melee => attacker.GetMultiplier(attacker.GetBCardInformation(BCardType.IncreaseAllDamage, (byte)AdditionalTypes.IncreaseAllDamage.MeleeAttackDecrease).firstData), + AttackType.Ranged => attacker.GetMultiplier(attacker.GetBCardInformation(BCardType.IncreaseAllDamage, (byte)AdditionalTypes.IncreaseAllDamage.RangeAttackDecrease).firstData), + AttackType.Magical => attacker.GetMultiplier(attacker.GetBCardInformation(BCardType.IncreaseAllDamage, (byte)AdditionalTypes.IncreaseAllDamage.MagicAttackDecrease).firstData) + }; + + /* INCREASE ALL DAMAGE BY MAGIC DEFENSE */ + increaseDamageMagicDefense += attacker.GetMultiplier(attacker.GetBCardInformation(BCardType.ReflectDamage, (byte)AdditionalTypes.ReflectDamage.AllAttackIncreasePerMagicDefense).firstData); + + /* INCREASE/DECREASE PVP DAMAGE */ + (int firstData, int secondData, int count) pvpBCard = attacker.GetBCardInformation(BCardType.LeonaPassiveSkill, (byte)AdditionalTypes.LeonaPassiveSkill.AttackIncreasedInPVP); + if (isPvP) + { + increaseDamageInPvP += pvpBCard.firstData * 0.01; + increaseDamageInPvP += attacker.GetShellWeaponEffectValue(ShellEffectType.PercentageDamageInPVP) * 0.01; + if (attacker.MapInstance.MapInstanceType == MapInstanceType.RainbowBattle) + { + increaseDamageInPvP += attacker.GetBCardInformation(BCardType.IncreaseDamageVersus, (byte)AdditionalTypes.IncreaseDamageVersus.PvpDamageAndSpeedRainbowBattleIncrease).firstData * 0.01; + } + + pvpBCard = attacker.GetBCardInformation(BCardType.LeonaPassiveSkill, (byte)AdditionalTypes.LeonaPassiveSkill.AttackDecreasedInPVP); + increaseDamageInPvP -= pvpBCard.firstData * 0.01; + increaseDamageInPvP -= defender.GetShellWeaponEffectValue(ShellEffectType.PercentageAllPVPDefence) * 0.01; + } + else + { + if (pvpBCard.secondData == 1) + { + increaseDamageInPvP -= pvpBCard.firstData * 0.01; + } + } + + /* INCREASE/DECRASE ATTACK */ + double increaseAttack = 1; + (int firstData, int secondData, int count) attackBCard = defender.GetBCardInformation(BCardType.Damage, (byte)AdditionalTypes.Damage.DamageIncreased); + increaseAttack += defender.GetMultiplier(attackBCard.firstData); + + attackBCard = defender.GetBCardInformation(BCardType.Damage, (byte)AdditionalTypes.Damage.DamageDecreased); + increaseAttack -= defender.GetMultiplier(attackBCard.firstData); + + /* INCREASE/DECREASE ATTACK BY ATTACK TYPE*/ + double increaseAttackAttackType = 1; + increaseAttackAttackType += attacker.AttackType switch + { + AttackType.Melee => defender.GetMultiplier(defender.GetBCardInformation(BCardType.Damage, (byte)AdditionalTypes.Damage.MeleeIncreased).firstData), + AttackType.Ranged => defender.GetMultiplier(defender.GetBCardInformation(BCardType.Damage, (byte)AdditionalTypes.Damage.RangedIncreased).firstData), + AttackType.Magical => defender.GetMultiplier(defender.GetBCardInformation(BCardType.Damage, (byte)AdditionalTypes.Damage.MagicalIncreased).firstData) + }; + + increaseAttackAttackType -= attacker.AttackType switch + { + AttackType.Melee => defender.GetMultiplier(defender.GetBCardInformation(BCardType.Damage, (byte)AdditionalTypes.Damage.MeleeDecreased).firstData), + AttackType.Ranged => defender.GetMultiplier(defender.GetBCardInformation(BCardType.Damage, (byte)AdditionalTypes.Damage.RangedDecreased).firstData), + AttackType.Magical => defender.GetMultiplier(defender.GetBCardInformation(BCardType.Damage, (byte)AdditionalTypes.Damage.MagicalDecreased).firstData) + }; + + /* INCREASE SHADOW ATTACK */ + double increaseDamageShadowFairy = 1; + if (attacker.Element == ElementType.Shadow) + { + (int firstData, int secondData, int count) shadowBCard = + attacker.GetBCardInformation(BCardType.DarkCloneSummon, (byte)AdditionalTypes.DarkCloneSummon.DarkElementDamageIncreaseChance); + + if (attacker.IsSucceededChance(shadowBCard.firstData)) + { + increaseDamageShadowFairy += shadowBCard.secondData * 0.01; + } + } + + /* INCREASE ALL ATTACKS */ + double increaseAllAttacks = 1 + attacker.GetMultiplier(attacker.GetBCardInformation(BCardType.Item, (byte)AdditionalTypes.Item.AttackIncreased).firstData); + + double markOfDeath = 1; + double knockdown = 1; + double loserSigh = 1; + double comboSkill = 1; + + if (skill != null) + { + if (defender.HasBuff((int)BuffVnums.MARK_OF_DEATH) && skill.Vnum == (int)SkillsVnums.SPIRIT_SPLITTER) + { + markOfDeath = 3; + } + + if (defender.HasBuff((int)BuffVnums.KNOCKDOWN) && skill.Vnum == (int)SkillsVnums.EXECUTION) + { + knockdown = 1.2; + } + + if (defender.HasBuff((int)BuffVnums.LOSER_SIGH) && skill.Vnum == (int)SkillsVnums.EXECUTION) + { + loserSigh = 1.4; + } + + if (skill.IsComboSkill && defender.HasBCard(BCardType.DamageConvertingSkill, (byte)AdditionalTypes.DamageConvertingSkill.AdditionalDamageCombo)) + { + comboSkill += defender.GetMultiplier(defender.GetBCardInformation(BCardType.DamageConvertingSkill, (byte)AdditionalTypes.DamageConvertingSkill.AdditionalDamageCombo).firstData); + } + } + + double damageFromDebuff = markOfDeath * knockdown * loserSigh * comboSkill; + + /* INCREASE DAMAGE VS HIGH MONSTERS*/ + double increaseDamageHighMonsters = 0; + if (defender.IsMonster() && defender.Level > attacker.Level && attacker.HasBCard(BCardType.EffectSummon, (byte)AdditionalTypes.EffectSummon.IfMobHigherLevelDamageIncrease)) + { + (int firstData, int secondData, int count) increaseDamageMonsterBCard = + attacker.GetBCardInformation(BCardType.EffectSummon, (byte)AdditionalTypes.EffectSummon.IfMobHigherLevelDamageIncrease); + if (attacker.IsSucceededChance(increaseDamageMonsterBCard.firstData)) + { + increaseDamageHighMonsters += attacker.GetMultiplier(increaseDamageMonsterBCard.secondData); + } + } + + /* INCREASE ALL ATTACKS VERSUS MONSTERS */ + if (!isPvP) + { + increaseAllDamageVersusMonsters += attacker.GetBCardInformation(BCardType.IncreaseElementFairy, (byte)AdditionalTypes.IncreaseElementFairy.DamageToMonstersIncrease).firstData * 0.01; + increaseAllDamageVersusMonsters -= attacker.GetBCardInformation(BCardType.IncreaseElementFairy, (byte)AdditionalTypes.IncreaseElementFairy.DamageToMonstersDecrease).firstData * 0.01; + } + + return new CalculationPhysicalDamage + { + CleanDamage = damage, + DamagePercentage = damagePercentage, + DamagePercentageSecond = damagePercentageSecond, + MultiplyDamage = multiplyDamage, + EndCriticalDamage = endCriticalDamage, + IgnoreEnemyDefense = ignoreEnemyDefense, + VesselLoDDamage = vesselLodDamage, + VesselGlacernonDamage = vesselGlacernonDamage, + IncreaseAllDamage = increaseAllDamage, + IncreaseAllDamageAttackType = increaseAllDamageAttackType, + IncreaseDamageMagicDefense = increaseDamageMagicDefense, + IncreaseDamageRace = increaseDamageRace, + IncreaseDamageRacePercentage = increaseDamageRacePercentage, + IncreaseLoDDamage = increaseLoDDamage, + IncreaseVesselDamage = increaseVesselDamage, + IncreaseDamageFaction = increaseDamageFaction, + InvisibleDamage = invisibleDamage, + IncreaseDamageInPvP = increaseDamageInPvP, + IncreaseAttack = increaseAttack, + IncreaseAttackAttackType = increaseAttackAttackType, + IncreaseDamageShadowFairy = increaseDamageShadowFairy, + IncreaseAllAttacks = increaseAllAttacks, + IncreaseDamageByDebuffs = damageFromDebuff, + IncreaseDamageHighMonsters = increaseDamageHighMonsters, + IncreaseDamageVersusMonsters = increaseDamageVersusMonstersMapType, + IncreaseAllAttacksVersusMonsters = increaseAllDamageVersusMonsters + }; + } + + public static CalculationElementDamage CalculateElementDamage(this IBattleEntityDump attacker, IBattleEntityDump defender, SkillInfo skill) + { + int element = 0; + double elementMultiply = 0; + + if (attacker.HasBCard(BCardType.NoCharacteristicValue, (byte)AdditionalTypes.NoCharacteristicValue.AllPowersNullified)) + { + return new CalculationElementDamage(); + } + + if (skill != null && skill.Element != (byte)attacker.Element) + { + return new CalculationElementDamage(); + } + + bool turnOffElement = attacker.Element switch + { + ElementType.Neutral => true, + ElementType.Fire => attacker.HasBCard(BCardType.NoCharacteristicValue, (byte)AdditionalTypes.NoCharacteristicValue.FireElementNullified), + ElementType.Water => attacker.HasBCard(BCardType.NoCharacteristicValue, (byte)AdditionalTypes.NoCharacteristicValue.WaterElementNullified), + ElementType.Light => attacker.HasBCard(BCardType.NoCharacteristicValue, (byte)AdditionalTypes.NoCharacteristicValue.LightElementNullified), + ElementType.Shadow => attacker.HasBCard(BCardType.NoCharacteristicValue, (byte)AdditionalTypes.NoCharacteristicValue.DarkElementNullified) + }; + + if (turnOffElement) + { + return new CalculationElementDamage(); + } + + switch (attacker.Element) + { + case ElementType.Fire: + element += attacker.TryFindPartnerSkillInformation(BCardType.Element, (byte)AdditionalTypes.Element.FireIncreased, skill).firstData; + element -= attacker.GetBCardInformation(BCardType.Element, (byte)AdditionalTypes.Element.FireDecreased).firstData; + element += attacker.GetBCardInformation(BCardType.IncreaseDamage, (byte)AdditionalTypes.IncreaseDamage.FireIncreased).firstData; + element -= attacker.GetBCardInformation(BCardType.IncreaseDamage, (byte)AdditionalTypes.IncreaseDamage.FireDecreased).firstData; + element += attacker.GetShellWeaponEffectValue(ShellEffectType.IncreasedFireProperties); + element += (int)(element * + attacker.GetMultiplier(attacker.GetBCardInformation(BCardType.IncreaseElementProcent, (byte)AdditionalTypes.IncreaseElementProcent.FireElementIncrease).firstData)); + element -= (int)(element * + attacker.GetMultiplier(attacker.GetBCardInformation(BCardType.IncreaseElementProcent, (byte)AdditionalTypes.IncreaseElementProcent.FireElementDecrease).firstData)); + break; + case ElementType.Water: + element += attacker.TryFindPartnerSkillInformation(BCardType.Element, (byte)AdditionalTypes.Element.WaterIncreased, skill).firstData; + element -= attacker.GetBCardInformation(BCardType.Element, (byte)AdditionalTypes.Element.WaterDecreased).firstData; + element += attacker.GetBCardInformation(BCardType.IncreaseDamage, (byte)AdditionalTypes.IncreaseDamage.WaterIncreased).firstData; + element -= attacker.GetBCardInformation(BCardType.IncreaseDamage, (byte)AdditionalTypes.IncreaseDamage.WaterDecreased).firstData; + element += attacker.GetShellWeaponEffectValue(ShellEffectType.IncreasedWaterProperties); + element += (int)(element * + attacker.GetMultiplier(attacker.GetBCardInformation(BCardType.IncreaseElementProcent, (byte)AdditionalTypes.IncreaseElementProcent.WaterElementIncrease).firstData)); + element -= (int)(element * + attacker.GetMultiplier(attacker.GetBCardInformation(BCardType.IncreaseElementProcent, (byte)AdditionalTypes.IncreaseElementProcent.WaterElementDecrease).firstData)); + break; + case ElementType.Light: + element += attacker.TryFindPartnerSkillInformation(BCardType.Element, (byte)AdditionalTypes.Element.LightIncreased, skill).firstData; + element -= attacker.GetBCardInformation(BCardType.Element, (byte)AdditionalTypes.Element.LightDecreased).firstData; + element += attacker.GetBCardInformation(BCardType.IncreaseDamage, (byte)AdditionalTypes.IncreaseDamage.LightIncreased).firstData; + element -= attacker.GetBCardInformation(BCardType.IncreaseDamage, (byte)AdditionalTypes.IncreaseDamage.LightDecreased).firstData; + element += attacker.GetShellWeaponEffectValue(ShellEffectType.IncreasedLightProperties); + element += (int)(element * + attacker.GetMultiplier(attacker.GetBCardInformation(BCardType.IncreaseElementProcent, (byte)AdditionalTypes.IncreaseElementProcent.LightElementIncrease).firstData)); + element -= (int)(element * + attacker.GetMultiplier(attacker.GetBCardInformation(BCardType.IncreaseElementProcent, (byte)AdditionalTypes.IncreaseElementProcent.LightElementDecrease).firstData)); + + break; + case ElementType.Shadow: + element += attacker.TryFindPartnerSkillInformation(BCardType.Element, (byte)AdditionalTypes.Element.DarkIncreased, skill).firstData; + element -= attacker.GetBCardInformation(BCardType.Element, (byte)AdditionalTypes.Element.DarkDecreased).firstData; + element += attacker.GetBCardInformation(BCardType.IncreaseDamage, (byte)AdditionalTypes.IncreaseDamage.DarkIncreased).firstData; + element -= attacker.GetBCardInformation(BCardType.IncreaseDamage, (byte)AdditionalTypes.IncreaseDamage.DarkDecreased).firstData; + element += attacker.GetShellWeaponEffectValue(ShellEffectType.IncreasedDarkProperties); + element += (int)(element * + attacker.GetMultiplier(attacker.GetBCardInformation(BCardType.IncreaseElementProcent, (byte)AdditionalTypes.IncreaseElementProcent.ShadowElementIncrease).firstData)); + element -= (int)(element * + attacker.GetMultiplier(attacker.GetBCardInformation(BCardType.IncreaseElementProcent, (byte)AdditionalTypes.IncreaseElementProcent.ShadowElementDecrease).firstData)); + break; + case ElementType.Neutral: + return new CalculationElementDamage(); + } + + element += attacker.GetBCardInformation(BCardType.Element, (byte)AdditionalTypes.Element.AllIncreased).firstData; + element -= attacker.GetBCardInformation(BCardType.Element, (byte)AdditionalTypes.Element.AllDecreased).firstData; + element += attacker.GetShellWeaponEffectValue(ShellEffectType.IncreasedElementalProperties); + element += (int)(element * attacker.GetMultiplier(attacker.GetBCardInformation(BCardType.IncreaseElementProcent, (byte)AdditionalTypes.IncreaseElementProcent.AllElementIncrease).firstData)); + element -= (int)(element * attacker.GetMultiplier(attacker.GetBCardInformation(BCardType.IncreaseElementProcent, (byte)AdditionalTypes.IncreaseElementProcent.AllElementDecrease).firstData)); + + elementMultiply = attacker.Element switch + { + ElementType.Fire => defender.Element switch + { + ElementType.Neutral => 1.3, + ElementType.Fire => 1, + ElementType.Water => 3, + ElementType.Light => 1, + ElementType.Shadow => 1.5 + }, + ElementType.Water => defender.Element switch + { + ElementType.Neutral => 1.3, + ElementType.Fire => 3, + ElementType.Water => 1, + ElementType.Light => 1.5, + ElementType.Shadow => 1 + }, + ElementType.Light => defender.Element switch + { + ElementType.Neutral => 1.3, + ElementType.Fire => 1.5, + ElementType.Water => 1, + ElementType.Light => 1, + ElementType.Shadow => 3 + }, + ElementType.Shadow => defender.Element switch + { + ElementType.Neutral => 1.3, + ElementType.Fire => 1, + ElementType.Water => 1.5, + ElementType.Light => 3, + ElementType.Shadow => 1 + }, + _ => elementMultiply + }; + + return new CalculationElementDamage + { + Element = element, + ElementMultiply = elementMultiply + }; + } + + public static CalculationDefense CalculationDefense(this IBattleEntityDump attacker, IBattleEntityDump defender) + { + double increaseDefense = 1; + double increaseDefenseAttackType = 1; + (int, double) increaseDefenseByLevel = (0, 1); + (int, double) increaseDefenseByLevelAttackType = (0, 1); + double increaseAllDefense = 1; + double increaseDefenseInPve = 1; + + if (defender.HasBCard(BCardType.Block, (byte)AdditionalTypes.Block.ChanceAllIncreased)) + { + (int firstData, int secondData, _) = defender.GetBCardInformation(BCardType.Block, (byte)AdditionalTypes.Block.ChanceAllIncreased); + if (defender.IsSucceededChance(firstData)) + { + increaseDefense += secondData * 0.01; + } + } + + if (defender.HasBCard(BCardType.Block, (byte)AdditionalTypes.Block.ChanceAllDecreased)) + { + (int firstData, int secondData, _) = defender.GetBCardInformation(BCardType.Block, (byte)AdditionalTypes.Block.ChanceAllDecreased); + if (defender.IsSucceededChance(firstData)) + { + increaseDefense -= secondData * 0.01; + } + } + + (int firstData, int secondData, int count) increaseDefenseAttackTypeBCard = defender.GetBCardInformation(BCardType.Block, (byte)AdditionalTypes.Block.ChanceMeleeIncreased); + switch (attacker.AttackType) + { + case AttackType.Melee when defender.HasBCard(BCardType.Block, (byte)AdditionalTypes.Block.ChanceMeleeIncreased): + if (defender.IsSucceededChance(increaseDefenseAttackTypeBCard.firstData)) + { + increaseDefenseAttackType += increaseDefenseAttackTypeBCard.secondData * 0.01; + defender.BroadcastEffect(EffectType.MeleeDefense); + } + + break; + case AttackType.Ranged when defender.HasBCard(BCardType.Block, (byte)AdditionalTypes.Block.ChanceRangedIncreased): + increaseDefenseAttackTypeBCard = defender.GetBCardInformation(BCardType.Block, (byte)AdditionalTypes.Block.ChanceRangedIncreased); + if (defender.IsSucceededChance(increaseDefenseAttackTypeBCard.firstData)) + { + increaseDefenseAttackType += increaseDefenseAttackTypeBCard.secondData * 0.01; + defender.BroadcastEffect(EffectType.RangeDefense); + } + + break; + case AttackType.Magical when defender.HasBCard(BCardType.Block, (byte)AdditionalTypes.Block.ChanceMagicalIncreased): + increaseDefenseAttackTypeBCard = defender.GetBCardInformation(BCardType.Block, (byte)AdditionalTypes.Block.ChanceMagicalIncreased); + if (defender.IsSucceededChance(increaseDefenseAttackTypeBCard.firstData)) + { + increaseDefenseAttackType += increaseDefenseAttackTypeBCard.secondData * 0.01; + defender.BroadcastEffect(EffectType.MagicDefense); + } + + break; + } + + increaseDefenseAttackTypeBCard = defender.GetBCardInformation(BCardType.Block, (byte)AdditionalTypes.Block.ChanceMeleeDecreased); + switch (attacker.AttackType) + { + case AttackType.Melee when defender.HasBCard(BCardType.Block, (byte)AdditionalTypes.Block.ChanceMeleeDecreased): + if (defender.IsSucceededChance(increaseDefenseAttackTypeBCard.firstData)) + { + increaseDefenseAttackType -= increaseDefenseAttackTypeBCard.secondData * 0.01; + defender.BroadcastEffect(EffectType.MeleeDefense); + } + + break; + case AttackType.Ranged when defender.HasBCard(BCardType.Block, (byte)AdditionalTypes.Block.ChanceRangedDecreased): + increaseDefenseAttackTypeBCard = defender.GetBCardInformation(BCardType.Block, (byte)AdditionalTypes.Block.ChanceRangedDecreased); + if (defender.IsSucceededChance(increaseDefenseAttackTypeBCard.firstData)) + { + increaseDefenseAttackType -= increaseDefenseAttackTypeBCard.secondData * 0.01; + defender.BroadcastEffect(EffectType.RangeDefense); + } + + break; + case AttackType.Magical when defender.HasBCard(BCardType.Block, (byte)AdditionalTypes.Block.ChanceMagicalDecreased): + increaseDefenseAttackTypeBCard = defender.GetBCardInformation(BCardType.Block, (byte)AdditionalTypes.Block.ChanceMagicalDecreased); + if (defender.IsSucceededChance(increaseDefenseAttackTypeBCard.firstData)) + { + increaseDefenseAttackType -= increaseDefenseAttackTypeBCard.secondData * 0.01; + defender.BroadcastEffect(EffectType.MagicDefense); + } + + break; + } + + increaseDefenseByLevel.Item1 += defender.GetBCardInformation(BCardType.Absorption, (byte)AdditionalTypes.Absorption.AllAttackDecreased).firstData; + increaseDefenseByLevel.Item1 -= defender.GetBCardInformation(BCardType.Absorption, (byte)AdditionalTypes.Absorption.AllAttackIncreased).firstData; + + increaseDefenseByLevel.Item2 -= defender.GetMultiplier(attacker.GetBCardInformation(BCardType.Absorption, (byte)AdditionalTypes.Absorption.AllAttackDecreased).secondData); + increaseDefenseByLevel.Item2 += defender.GetMultiplier(attacker.GetBCardInformation(BCardType.Absorption, (byte)AdditionalTypes.Absorption.AllAttackIncreased).secondData); + + increaseDefenseByLevelAttackType.Item1 += attacker.AttackType switch + { + AttackType.Melee => defender.GetBCardInformation(BCardType.Absorption, (byte)AdditionalTypes.Absorption.MeleeAttackDecreased).firstData, + AttackType.Ranged => defender.GetBCardInformation(BCardType.Absorption, (byte)AdditionalTypes.Absorption.RangedAttackDecreased).firstData, + AttackType.Magical => defender.GetBCardInformation(BCardType.Absorption, (byte)AdditionalTypes.Absorption.MagicalAttacksDecreased).firstData + }; + + increaseDefenseByLevelAttackType.Item1 -= attacker.AttackType switch + { + AttackType.Melee => defender.GetBCardInformation(BCardType.Absorption, (byte)AdditionalTypes.Absorption.MeleeAttackIncreased).firstData, + AttackType.Ranged => defender.GetBCardInformation(BCardType.Absorption, (byte)AdditionalTypes.Absorption.RangedAttackIncreased).firstData, + AttackType.Magical => defender.GetBCardInformation(BCardType.Absorption, (byte)AdditionalTypes.Absorption.MagicalAttackIncreased).firstData + }; + + increaseDefenseByLevelAttackType.Item2 -= attacker.AttackType switch + { + AttackType.Melee => defender.GetMultiplier(defender.GetBCardInformation(BCardType.Absorption, (byte)AdditionalTypes.Absorption.MeleeAttackDecreased).secondData), + AttackType.Ranged => defender.GetMultiplier(defender.GetBCardInformation(BCardType.Absorption, (byte)AdditionalTypes.Absorption.RangedAttackDecreased).secondData), + AttackType.Magical => defender.GetMultiplier(defender.GetBCardInformation(BCardType.Absorption, (byte)AdditionalTypes.Absorption.MagicalAttacksDecreased).secondData) + }; + + increaseDefenseByLevelAttackType.Item2 += attacker.AttackType switch + { + AttackType.Melee => defender.GetMultiplier(defender.GetBCardInformation(BCardType.Absorption, (byte)AdditionalTypes.Absorption.MeleeAttackIncreased).secondData), + AttackType.Ranged => defender.GetMultiplier(defender.GetBCardInformation(BCardType.Absorption, (byte)AdditionalTypes.Absorption.RangedAttackIncreased).secondData), + AttackType.Magical => defender.GetMultiplier(defender.GetBCardInformation(BCardType.Absorption, (byte)AdditionalTypes.Absorption.MagicalAttackIncreased).secondData) + }; + + increaseAllDefense = 1 + defender.GetMultiplier(defender.GetBCardInformation(BCardType.Item, (byte)AdditionalTypes.Item.DefenceIncreased).firstData); + + int maximumCriticalDamage = defender.GetBCardInformation(BCardType.VulcanoElementBuff, (byte)AdditionalTypes.VulcanoElementBuff.CriticalDefence).firstData; + + bool isPvP = attacker.IsPlayer() && defender.IsPlayer(); + double defenseInPvP = 1; + (int firstData, int secondData, int count) pvpBCard = defender.GetBCardInformation(BCardType.LeonaPassiveSkill, (byte)AdditionalTypes.LeonaPassiveSkill.DefenceIncreasedInPVP); + if (isPvP) + { + defenseInPvP -= pvpBCard.firstData * 0.01; + + pvpBCard = attacker.GetBCardInformation(BCardType.LeonaPassiveSkill, (byte)AdditionalTypes.LeonaPassiveSkill.DefenceDecreasedInPVP); + defenseInPvP += pvpBCard.firstData * 0.01; + defenseInPvP += attacker.GetShellWeaponEffectValue(ShellEffectType.ReducesPercentageEnemyDefenceInPVP) * 0.01; + } + else + { + if (pvpBCard.secondData == 1) + { + defenseInPvP += pvpBCard.firstData * 0.01; + } + } + + int multiplyDefenseInt = defender.GetBCardInformation(BCardType.MultDefence, (byte)AdditionalTypes.MultDefence.AllDefenceIncreased).firstData; + multiplyDefenseInt += attacker.AttackType switch + { + AttackType.Melee => defender.GetBCardInformation(BCardType.MultDefence, (byte)AdditionalTypes.MultDefence.MeleeDefenceIncreased).firstData, + AttackType.Ranged => defender.GetBCardInformation(BCardType.MultDefence, (byte)AdditionalTypes.MultDefence.RangedDefenceIncreased).firstData, + AttackType.Magical => defender.GetBCardInformation(BCardType.MultDefence, (byte)AdditionalTypes.MultDefence.MagicalDefenceIncreased).firstData + }; + + multiplyDefenseInt -= defender.GetBCardInformation(BCardType.MultDefence, (byte)AdditionalTypes.MultDefence.AllDefenceDecreased).firstData; + multiplyDefenseInt -= attacker.AttackType switch + { + AttackType.Melee => defender.GetBCardInformation(BCardType.MultDefence, (byte)AdditionalTypes.MultDefence.MeleeDefenceDecreased).firstData, + AttackType.Ranged => defender.GetBCardInformation(BCardType.MultDefence, (byte)AdditionalTypes.MultDefence.RangedDefenceDecreased).firstData, + AttackType.Magical => defender.GetBCardInformation(BCardType.MultDefence, (byte)AdditionalTypes.MultDefence.MagicalDefenceDecreased).firstData + }; + + int multiplyDefense = multiplyDefenseInt == 0 ? 1 : multiplyDefenseInt; + + return new CalculationDefense + { + IncreaseDefense = increaseDefense, + IncreaseDefenseAttackType = increaseDefenseAttackType, + IncreaseDefenseByLevel = increaseDefenseByLevel, + IncreaseDefenseByLevelAttackType = increaseDefenseByLevelAttackType, + IncreaseAllDefense = increaseAllDefense, + MaximumCriticalDamage = maximumCriticalDamage, + DefenseInPvP = defenseInPvP, + IncreaseDefenseInPve = increaseDefenseInPve, + MultiplyDefense = multiplyDefense + }; + } + + public static CalculationResult DamageResult( + this IBattleEntityDump attacker, + IBattleEntityDump defender, + CalculationBasicStatistics baseStatistics, + CalculationDefense defense, + CalculationPhysicalDamage physicalDamage, + CalculationElementDamage elementDamage, + SkillInfo skill) + { + bool isCritical = false; + + bool isSoftDamage = false; + bool isHighMonsterDamage = false; + double softDamageMultiplier = 0; + + int distance = attacker.Position.GetDistance(defender.Position); + + #region Attacker Values + + int attackerMorale = baseStatistics.AttackerMorale; + int attackerAttackUpgrade = baseStatistics.AttackerAttackUpgrade; + int attackerCriticalChance = baseStatistics.AttackerCriticalChance; + double attackerCriticalDamage = baseStatistics.AttackerCriticalDamage * 0.01; + double attackerElementRate = baseStatistics.AttackerElementRate * 0.01; + + ElementType attackerElement = attacker.Element; + AttackType attackType = attacker.AttackType; + + int basicDamage = _randomGenerator.RandomNumber(attacker.DamageMinimum + attacker.WeaponDamageMinimum, attacker.DamageMaximum + attacker.WeaponDamageMaximum); + + int weaponMin = attacker.WeaponDamageMinimum; + int weaponMax = attacker.WeaponDamageMaximum; + + double shellDamagePercentage = attacker.GetShellWeaponEffectValue(ShellEffectType.PercentageTotalDamage) * 0.01; + + #endregion + + #region Defender Values + + int defenderMorale = baseStatistics.DefenderMorale; + int defenderDefenseUpgrade = baseStatistics.DefenderDefenseUpgrade; + int defenderDefense = baseStatistics.DefenderDefense; + int defenderResistance = baseStatistics.DefenderResistance; + + #endregion + + #region Defense Values + + double increaseDefense = defense.IncreaseDefense; // 11, 1 + double increaseDefenseAttackType = defense.IncreaseDefenseAttackType; // 11, 3-5-7 + (int, double) increaseDefenseByLevel = defense.IncreaseDefenseByLevel; // 12, 1 + (int, double) increaseDefenseByLevelAttackType = defense.IncreaseDefenseByLevelAttackType; // 12, 3-5-7 + double increaseAllDefense = defense.IncreaseAllDefense; // 44, 4 + int maximumCriticalDamage = defense.MaximumCriticalDamage; // 66, 7 + double defenseInPvP = defense.DefenseInPvP; // 71, 7 + double increaseDefenseInPve = defense.IncreaseDefenseInPve; + double multiplyDefense = defense.MultiplyDefense; + + #endregion + + #region Psychical Damage + + int cleanDamage = physicalDamage.CleanDamage; + double damagePercentage = physicalDamage.DamagePercentage; // 5, 5 + double damagePercentageSecond = physicalDamage.DamagePercentageSecond; // 8, 5 + double multiplierDamage = physicalDamage.MultiplyDamage; // 34, 1 + double endCriticalDamage = physicalDamage.EndCriticalDamage; // 38, 5 + double ignoreEnemyDefense = physicalDamage.IgnoreEnemyDefense; // 84, 1 + double vesselLodDamage = physicalDamage.VesselLoDDamage; // 90, 3 + double vesselGlacernonDamage = physicalDamage.VesselGlacernonDamage; // 90, 7 + double increaseAllDamage = physicalDamage.IncreaseAllDamage; // 103, 1 + double increaseAllDamageAttackType = physicalDamage.IncreaseAllDamageAttackType; // 103, 3-5-7 + double increaseDamageMagicDefense = physicalDamage.IncreaseDamageMagicDefense; // 108, 9 + int increaseDamageRace = physicalDamage.IncreaseDamageRace; // 24, 1 + double increaseDamageRacePercentage = physicalDamage.IncreaseDamageRacePercentage; // 71, 1 + double increaseLodDamage = physicalDamage.IncreaseLoDDamage; // 101, 1 + double increaseVesselDamage = physicalDamage.IncreaseVesselDamage; // 101, 3 + double increaseDamageFaction = physicalDamage.IncreaseDamageFaction; // 85, 7-9 + int invisibleDamage = physicalDamage.InvisibleDamage; // 43, 7 + double increaseDamageInPvP = physicalDamage.IncreaseDamageInPvP; // 71, 9 + double increaseAttack = physicalDamage.IncreaseAttack; // 15, 1 + double increaseAttackAttackType = physicalDamage.IncreaseAttackAttackType; // 15, 3-5-7 + double increaseDamageShadowFairy = physicalDamage.IncreaseDamageShadowFairy; // 80, 9 + double increaseAllAttacks = physicalDamage.IncreaseAllAttacks; // 44, 3 + double increaseDamageByDebuffs = physicalDamage.IncreaseDamageByDebuffs; + double increaseDamageHighMonsters = physicalDamage.IncreaseDamageHighMonsters; // 86, 5 + double increaseDamageVersusMonsters = physicalDamage.IncreaseDamageVersusMonsters; + double increaseAllAttacksVersusMonsters = physicalDamage.IncreaseAllAttacksVersusMonsters; + + #endregion + + #region Element Damage + + int element = elementDamage.Element; + double elementMultiply = elementDamage.ElementMultiply; + + #endregion + + if (skill != null && skill.Element != (byte)attackerElement) + { + attackerElementRate = 0; + } + + if (attacker.HasBCard(BCardType.Mode, (byte)AdditionalTypes.Mode.EffectNoDamage)) + { + return new CalculationResult(0, false, false); + } + + basicDamage += cleanDamage; + basicDamage += invisibleDamage; + basicDamage += increaseDamageRace; + + basicDamage += attacker.GetShellWeaponEffectValue(ShellEffectType.DamageImproved); + + if (increaseDamageRacePercentage > 0) + { + basicDamage = (int)(basicDamage * increaseDamageRacePercentage); + } + + double damageFromMap = vesselLodDamage * vesselGlacernonDamage; + + if (increaseDamageMagicDefense > 0 && attackType == AttackType.Magical) + { + basicDamage += (int)Math.Floor(attacker.MagicalDefense * increaseDamageMagicDefense); + } + + if (increaseDamageHighMonsters > 0) + { + isHighMonsterDamage = true; + } + + int plusDifference = attackerAttackUpgrade - defenderDefenseUpgrade; + + bool countWeaponPlus = false; + int additionalDefense = 0; + + if (Math.Abs(plusDifference) > Math.Abs(increaseDefenseByLevel.Item1)) + { + increaseDefenseByLevel.Item2 = 1; + } + + if (Math.Abs(plusDifference) > Math.Abs(increaseDefenseByLevelAttackType.Item1)) + { + increaseDefenseByLevelAttackType.Item2 = 1; + } + + if (plusDifference > 0) + { + plusDifference = Math.Abs(plusDifference); + if (plusDifference > 10) + { + plusDifference = 10; + } + + weaponMin = (int)(weaponMin * _plus[plusDifference]); + weaponMax = (int)(weaponMax * _plus[plusDifference]); + countWeaponPlus = true; + } + else if (plusDifference < 0) + { + plusDifference = Math.Abs(plusDifference); + if (plusDifference > 10) + { + plusDifference = 10; + } + + additionalDefense += (int)Math.Floor(defenderDefense * _plus[plusDifference]); + } + + if (attackType != AttackType.Magical && attackerCriticalChance != 0 && attacker.IsSucceededChance(attackerCriticalChance)) + { + isCritical = true; + } + + if (damagePercentageSecond > 0 && isHighMonsterDamage) + { + softDamageMultiplier = increaseDamageHighMonsters + damagePercentageSecond + increaseDamageHighMonsters * damagePercentageSecond; + isSoftDamage = true; + } + else if (damagePercentageSecond > 0) + { + softDamageMultiplier = damagePercentageSecond; + isSoftDamage = true; + } + else if (isHighMonsterDamage) + { + softDamageMultiplier = increaseDamageHighMonsters; + isSoftDamage = true; + } + + double defenderResistanceMultiplier = defenderResistance >= 100 ? 0 : 1.0 - defenderResistance * 0.01; + + int minDamage = basicDamage + (countWeaponPlus ? weaponMin : 0); + int maxDamage = basicDamage + (countWeaponPlus ? weaponMax : 0); + + int calculatedDamage = _randomGenerator.RandomNumber(minDamage, maxDamage); + + #region Normal Damage + + int normalDmg = (int)((attacker.PhysicalDamageShell(calculatedDamage, shellDamagePercentage, increaseDefenseByLevel.Item2, increaseDefenseByLevelAttackType.Item2) + + attacker.PhysicalDamageIncreaseDefenseByLevel(calculatedDamage, increaseDefenseByLevel.Item2, increaseDefenseByLevelAttackType.Item2) + + attacker.PhysicalDamageIncreaseDefenseByLevelAttackType(calculatedDamage, increaseDefenseByLevelAttackType.Item2, increaseDefenseByLevel.Item2) + + attacker.PhysicalDamageIncreaseAttack(calculatedDamage, defenderDefense, additionalDefense, increaseAttack, increaseDefenseByLevel.Item2, increaseDefenseByLevelAttackType.Item2) + + attacker.PhysicalDamageIncreaseDefense(calculatedDamage, defenderDefense, additionalDefense, increaseDefenseAttackType, increaseDefenseByLevel.Item2, + increaseDefenseByLevelAttackType.Item2) + + attacker.PhysicalDamageMultiplier(calculatedDamage, multiplierDamage, increaseDefenseByLevel.Item2, increaseDefenseByLevelAttackType.Item2)) * increaseDefense); + + normalDmg = attacker.Penalties(defender, normalDmg, distance); + + normalDmg = (int)(normalDmg * damagePercentage); + normalDmg = (int)(normalDmg * increaseAttack); + normalDmg = (int)(normalDmg * increaseDefenseAttackType); + + #endregion + + #region Basic Damage + + int basicDmg = (int)(calculatedDamage * increaseAttack * increaseDefense); + basicDmg = attacker.Penalties(defender, basicDmg, distance); + + #endregion + + #region Critical Damage + + int criticalDmg = 0; + if (isCritical) + { + criticalDmg = (int)((attacker.PhysicalCriticalDamage(calculatedDamage, defenderDefense, attackerCriticalDamage, multiplierDamage) + + attacker.PhysicalCriticalDamageShell(calculatedDamage, defenderDefense, shellDamagePercentage, increaseDefenseByLevel.Item2, + increaseDefenseByLevelAttackType.Item2, attackerCriticalDamage, multiplierDamage) + + attacker.PhysicalCriticalDamageIncreaseDefenseByLevel(calculatedDamage, attackerCriticalDamage, increaseDefenseByLevel.Item2, increaseDefenseByLevelAttackType.Item2) + + attacker.PhysicalCriticalDamageIncreaseDefenseByLevelAttackType(calculatedDamage, attackerCriticalDamage, increaseDefenseByLevel.Item2, increaseDefenseByLevelAttackType.Item2) + + attacker.PhysicalCriticalIncreaseAttack(calculatedDamage, increaseAttack, attackerCriticalDamage, increaseDefenseByLevel.Item2, increaseDefenseByLevelAttackType.Item2, + multiplierDamage)) * increaseDefense); + + criticalDmg = attacker.Penalties(defender, criticalDmg, distance); + + criticalDmg = (int)(criticalDmg * damagePercentage); + criticalDmg = (int)(criticalDmg * increaseAttack); + criticalDmg = (int)(criticalDmg * increaseDefenseAttackType); + } + + #endregion + + #region Element Damage + + int elementDmg = 0; + if (attackerElement != ElementType.Neutral) + { + elementDmg = + attacker.ElementFairyDamage(calculatedDamage, attackerElementRate) + + attacker.ElementFairyDamageShell(calculatedDamage, shellDamagePercentage, attackerElementRate) + + attacker.ElementFairyDamageMultiplier(calculatedDamage, multiplierDamage, attackerElementRate); + } + + #endregion + + #region Physical Soft Damage + + int physicalSoft = 0; + if (isSoftDamage) + { + physicalSoft = (int)((attacker.SoftDamageMultiplier(calculatedDamage, softDamageMultiplier, multiplierDamage) + + attacker.SoftDamageShell(calculatedDamage, softDamageMultiplier, shellDamagePercentage, increaseDefenseByLevel.Item2, increaseDefenseByLevelAttackType.Item2) + + attacker.SoftDamageIncreaseDefenseByLevel(calculatedDamage, softDamageMultiplier, increaseDefenseByLevel.Item2, increaseDefenseByLevelAttackType.Item2) + + attacker.SoftDamageIncreaseDefenseByLevelAttackType(calculatedDamage, softDamageMultiplier, increaseDefenseByLevelAttackType.Item2, increaseDefenseByLevel.Item2) + + attacker.SoftDamageIncreaseAttack(calculatedDamage, increaseAttack, softDamageMultiplier, increaseDefense, increaseDefenseAttackType)) * increaseDefense); + + physicalSoft = attacker.Penalties(defender, physicalSoft, distance); + + physicalSoft = (int)(physicalSoft * damagePercentage); + physicalSoft = (int)(physicalSoft * increaseAttack); + physicalSoft = (int)(physicalSoft * increaseDefenseAttackType); + } + + #endregion + + #region Critical Soft Damage + + int criticalSoft = 0; + if (isCritical && isSoftDamage) + { + criticalSoft = (int)((attacker.PhysicalSoftCriticalDamage(calculatedDamage, softDamageMultiplier, attackerCriticalDamage, multiplierDamage) + + attacker.PhysicalSoftCriticalDamageShell(calculatedDamage, softDamageMultiplier, attackerCriticalDamage, + shellDamagePercentage, increaseDefenseByLevel.Item2, increaseDefenseByLevelAttackType.Item2) + + attacker.PhysicalSoftCriticalIncreaseDefenseByLevel(calculatedDamage, softDamageMultiplier, + attackerCriticalDamage, increaseDefenseByLevel.Item2, increaseDefenseByLevelAttackType.Item2) + + attacker.PhysicalSoftCriticalIncreaseDefenseByLevelAttackType(calculatedDamage, softDamageMultiplier, + attackerCriticalDamage, increaseDefenseByLevelAttackType.Item2, increaseDefenseByLevel.Item2) + + attacker.PhysicalSoftCriticalIncreaseAttack(calculatedDamage, softDamageMultiplier, attackerCriticalDamage, + increaseAttack, increaseDefenseByLevel.Item2, increaseDefenseByLevelAttackType.Item2)) * increaseDefense); + + criticalSoft = attacker.Penalties(defender, criticalSoft, distance); + + criticalSoft = (int)(criticalSoft * damagePercentage); + criticalSoft = (int)(criticalSoft * increaseAttack); + criticalSoft = (int)(criticalSoft * increaseDefenseAttackType); + } + + #endregion + + #region Element Soft Damage + + int elementSoft = 0; + if (attackerElement != ElementType.Neutral && isSoftDamage) + { + elementSoft = + attacker.SoftElementDamage(calculatedDamage, softDamageMultiplier, attackerElementRate, multiplierDamage) + + attacker.SoftElementDamageShell(calculatedDamage, shellDamagePercentage, softDamageMultiplier, attackerElementRate); + } + + #endregion + + /* FINAL PHYSICAL DAMAGE */ + + int physicalDmg = (int)(attackerMorale + (basicDmg + normalDmg + criticalDmg + physicalSoft + criticalSoft) * endCriticalDamage * increaseAllDamageAttackType * increaseAllDamage * + increaseAllAttacksVersusMonsters); + double magicialDamageReduction = defender is PlayerBattleEntityDump playerBattleEntityDump && attackType == AttackType.Magical ? playerBattleEntityDump.DecreaseMagicDamage : 1; + physicalDmg = (int)Math.Floor(physicalDmg * magicialDamageReduction); + + int defenceByLevel; + if (defender.IsPlayer()) + { + defenceByLevel = -15; + } + else + { + defenceByLevel = attacker.GetMonsterDamageBonus(defender.Level); + } + + int defenderDefenseDamage = (int)(defenderMorale + defenceByLevel + (defenderDefense + additionalDefense) * ignoreEnemyDefense * increaseAllDefense * increaseDefenseInPve * multiplyDefense); + + physicalDmg -= defenderDefenseDamage <= 0 ? 0 : defenderDefenseDamage; + + + /* FINAL ELEMENT DAMAGE */ + + int elementalDamage = (int)(element + (elementDmg + elementSoft) * increaseAllDamageAttackType * increaseAllDamage); + + if (elementalDamage != 0) + { + elementalDamage = (int)Math.Floor(elementalDamage * increaseDefense); + + elementalDamage = (int)Math.Floor(elementalDamage * increaseDamageShadowFairy); + + elementalDamage = (int)Math.Floor(elementalDamage * elementMultiply); + + elementalDamage = (int)Math.Floor(elementalDamage * defenderResistanceMultiplier); + } + + /* FINAL DAMAGE */ + + int finalDamage = physicalDmg + elementalDamage; + + finalDamage = (int)Math.Floor(finalDamage * increaseAllAttacks); + + finalDamage = (int)Math.Floor(finalDamage * increaseDamageByDebuffs); + + finalDamage = (int)Math.Floor(finalDamage * damageFromMap); + finalDamage = (int)Math.Floor(finalDamage * (1 + attacker.GetFamilyUpgradeValue(FamilyUpgradeType.INCREASE_ATTACK_DEFENSE) * 0.01)); + + finalDamage = (int)Math.Floor(finalDamage * increaseDamageInPvP); + + finalDamage = (int)Math.Floor(finalDamage * increaseLodDamage); + + finalDamage = (int)Math.Floor(finalDamage * increaseVesselDamage); + + finalDamage = (int)Math.Floor(finalDamage * increaseDamageFaction); + + finalDamage = (int)Math.Floor(finalDamage * increaseDamageVersusMonsters); + if (attacker is PlayerBattleEntityDump { IncreaseJajamaruDamage: true }) + { + finalDamage = (int)Math.Floor(finalDamage * 1.5); + } + + finalDamage = (int)Math.Floor(finalDamage * defenseInPvP); + + finalDamage = (int)Math.Floor(finalDamage * (1 - defender.GetFamilyUpgradeValue(FamilyUpgradeType.INCREASE_ATTACK_DEFENSE) * 0.01)); + + if (isCritical && maximumCriticalDamage != 0) + { + finalDamage = maximumCriticalDamage; + } + + if (attacker.HasBCard(BCardType.RecoveryAndDamagePercent, (byte)AdditionalTypes.RecoveryAndDamagePercent.DecreaseEnemyHP)) + { + double maxHpPercentage = attacker.GetBCardInformation(BCardType.RecoveryAndDamagePercent, (byte)AdditionalTypes.RecoveryAndDamagePercent.DecreaseEnemyHP).firstData * 0.01; + finalDamage = (int)(defender.MaxHp * maxHpPercentage); + } + + if (defender.HasBCard(BCardType.RecoveryAndDamagePercent, (byte)AdditionalTypes.RecoveryAndDamagePercent.DecreaseSelfHP)) + { + double maxHpPercentage = defender.GetBCardInformation(BCardType.RecoveryAndDamagePercent, (byte)AdditionalTypes.RecoveryAndDamagePercent.DecreaseSelfHP).firstData * 0.01; + finalDamage = (int)(defender.MaxHp * maxHpPercentage); + } + + if (finalDamage <= 0) + { + finalDamage = 1; + } + + finalDamage += (int)(GetMonsterBaseDamage(attacker, defender) * increaseAttack * increaseDefense); + + return new CalculationResult(finalDamage, isCritical, isSoftDamage); + } + + private static int GetMonsterBaseDamage(IBattleEntityDump attacker, IBattleEntityDump defender) + { + if (!attacker.IsMonster()) + { + return 0; + } + + int monsterLevel = attacker.Level; + int multiplier = monsterLevel switch + { + < 30 => 0, + <= 50 => 1, + < 60 => 2, + < 65 => 3, + < 70 => 4, + _ => 5 + }; + + double damageReduction = defender is PlayerBattleEntityDump playerBattleEntityDump ? playerBattleEntityDump.MinimalDamageReduction : 1; + + return (int)(monsterLevel * multiplier * damageReduction); + } + + private static int Penalties(this IBattleEntityDump attacker, IBattleEntityDump defender, int damage, int distance) + { + if (attacker.AttackType != AttackType.Ranged) + { + return damage; + } + + bool hasRangePenalty = !attacker.HasBCard(BCardType.GuarantedDodgeRangedAttack, (byte)AdditionalTypes.GuarantedDodgeRangedAttack.NoPenalty); + bool increaseDamageRangeDistance = attacker.HasBCard(BCardType.GuarantedDodgeRangedAttack, (byte)AdditionalTypes.GuarantedDodgeRangedAttack.DistanceDamageIncreasing); + + int returnDamage = damage; + returnDamage = distance switch + { + <= 2 when hasRangePenalty => (int)(returnDamage * 0.7), + > 2 when increaseDamageRangeDistance => (int)(returnDamage * (0.95 + 0.05 * distance)), + _ => returnDamage + }; + + return returnDamage; + } + + /* + * Calculation: + * 1. Clean damage + * 2. Element damage + * 3. Critical damage + * 4. Soft Damage + */ + + // Calculate all bonuses for physical damage + + private static int PhysicalDamageShell(this IBattleEntityDump attacker, int cleanDamage, double shellPercentage, double increaseDefenseByLevel, double increaseDefenseByLevelAttackType) + => (int)Math.Floor((cleanDamage + 15) * shellPercentage * increaseDefenseByLevel * increaseDefenseByLevelAttackType); + + private static int PhysicalDamageIncreaseAttack(this IBattleEntityDump attacker, int cleanDamage, int defense, int additionalDefense, double increaseAttack, double increaseDefenseByLevel, + double increaseDefenseByLevelAttackType) + => (int)Math.Floor((cleanDamage - defense - additionalDefense) / (1 / (increaseAttack - 1 < 0 ? 0 : increaseAttack - 1) + 1) * increaseDefenseByLevel * increaseDefenseByLevelAttackType); + + private static int PhysicalDamageIncreaseDefense(this IBattleEntityDump attacker, int cleanDamage, int defense, int additionalDefense, double increaseDefenseAttackType, + double increaseDefenseByLevel, + double increaseDefenseByLevelAttackType) + => (int)Math.Floor((cleanDamage + 15 - defense - additionalDefense) / (1 / (increaseDefenseAttackType - 1 < 0 ? 0 : increaseDefenseAttackType - 1) + 1) * increaseDefenseByLevel * + increaseDefenseByLevelAttackType); + + private static int PhysicalDamageIncreaseDefenseByLevel(this IBattleEntityDump attacker, int cleanDamage, double increaseDefenseByLevel, double increaseDefenseByLevelAttackType) + => increaseDefenseByLevel == 1.0 + ? 0 + : (int)Math.Floor((cleanDamage + 15) / (1 / (increaseDefenseByLevel - 1 < 0 ? 0 : increaseDefenseByLevel - 1) + 1) * increaseDefenseByLevel * increaseDefenseByLevelAttackType); + + private static int PhysicalDamageIncreaseDefenseByLevelAttackType(this IBattleEntityDump attacker, int cleanDamage, double increaseDefenseByLevelAttackType, double increaseDefenseByLevel) + => increaseDefenseByLevelAttackType == 1.0 + ? 0 + : (int)Math.Floor((cleanDamage + 15) / (1 / (increaseDefenseByLevelAttackType - 1 < 0 ? 0 : increaseDefenseByLevelAttackType - 1) + 1) * increaseDefenseByLevel * + increaseDefenseByLevelAttackType); + + private static int PhysicalDamageMultiplier(this IBattleEntityDump attacker, int cleanDamage, double multiplyDamage, double increaseDefenseByLevel, double increaseDefenseByLevelAttackType) + => (int)Math.Floor((cleanDamage + 15) * (multiplyDamage - 1) * increaseDefenseByLevel * increaseDefenseByLevelAttackType); + + // Calculate all bonuses for element damage + + private static int ElementFairyDamage(this IBattleEntityDump attacker, int cleanDamage, double fairyMultiplier) => (int)Math.Floor((cleanDamage + 115) * fairyMultiplier); + + private static int ElementFairyDamageShell(this IBattleEntityDump attacker, int cleanDamage, double shellPercentage, double fairyMultiplier) => + (int)Math.Floor((cleanDamage + 75) * shellPercentage * fairyMultiplier); + + private static int ElementFairyDamageMultiplier(this IBattleEntityDump attacker, int cleanDamage, double multiplierDamage, double fairyMultiplier) => + (int)Math.Floor((cleanDamage + 15) * fairyMultiplier * (multiplierDamage - 1)); + + // Calculate all bonuses for critical damage + + private static int PhysicalCriticalDamage(this IBattleEntityDump attacker, int cleanDamage, int defense, double criticalMultiplier, double multiplierDamage) + => (int)Math.Floor((cleanDamage + 15 - defense) * criticalMultiplier * multiplierDamage); + + private static int PhysicalCriticalDamageShell(this IBattleEntityDump attacker, int cleanDamage, int defense, double shellPercentage, double increaseDefenseByLevel, + double increaseDefenseByLevelAttackType, double criticalMultiplier, double multiplierDamage) + => (int)Math.Floor((cleanDamage + 15 - defense) * criticalMultiplier * shellPercentage * increaseDefenseByLevel * increaseDefenseByLevelAttackType * multiplierDamage); + + // Calculate all bonuses for critical soft damage + + private static int PhysicalCriticalIncreaseAttack(this IBattleEntityDump attacker, int cleanDamage, double increaseAttack, double criticalMultiplier, double increaseDefenseByLevel, + double increaseDefenseByLevelAttackType, + double multiplierDamage) => (int)Math.Floor(cleanDamage / (1 / (increaseAttack - 1 < 0 ? 0 : increaseAttack - 1) + 1) * criticalMultiplier * increaseDefenseByLevel * + increaseDefenseByLevelAttackType * multiplierDamage); + + private static int PhysicalCriticalDamageIncreaseDefenseByLevel(this IBattleEntityDump attacker, int cleanDamage, double criticalMultiplier, double increaseDefenseByLevel, + double increaseDefenseByLevelAttackType) + => increaseDefenseByLevel == 1.0 + ? 0 + : (int)Math.Floor((cleanDamage + 15) / (1 / (increaseDefenseByLevel - 1 < 0 ? 0 : increaseDefenseByLevel - 1) + 1) * criticalMultiplier * increaseDefenseByLevel * + increaseDefenseByLevelAttackType); + + private static int PhysicalCriticalDamageIncreaseDefenseByLevelAttackType(this IBattleEntityDump attacker, int cleanDamage, double criticalMultiplier, double increaseDefenseByLevel, + double increaseDefenseByLevelAttackType) + => increaseDefenseByLevelAttackType == 1.0 + ? 0 + : (int)Math.Floor((cleanDamage + 15) / (1 / (increaseDefenseByLevelAttackType - 1 < 0 ? 0 : increaseDefenseByLevelAttackType - 1) + 1) * criticalMultiplier * increaseDefenseByLevel * + increaseDefenseByLevelAttackType); + + private static int PhysicalSoftCriticalDamage(this IBattleEntityDump attacker, int cleanDamage, double softDamageMultiplier, double criticalMultiplier, double damageMultiplier) + => (int)Math.Floor((cleanDamage + 15) * softDamageMultiplier * criticalMultiplier * damageMultiplier); + + private static int PhysicalSoftCriticalDamageShell(this IBattleEntityDump attacker, int cleanDamage, double softDamageMultiplier, double criticalMultiplier, double shellPercentage, + double increaseDefenseByLevel, + double increaseDefenseByLevelAttackType) => + (int)Math.Floor((cleanDamage + 15) * softDamageMultiplier * criticalMultiplier * shellPercentage * increaseDefenseByLevel * increaseDefenseByLevelAttackType); + + private static int PhysicalSoftCriticalIncreaseAttack(this IBattleEntityDump attacker, int cleanDamage, double softDamageMultiplier, double criticalMultiplier, double increaseAttack, + double increaseDefenseByLevel, + double increaseDefenseByLevelAttackType) => (int)Math.Floor(cleanDamage / (1 / (increaseAttack - 1 < 0 ? 0 : increaseAttack - 1) + 1) * softDamageMultiplier * criticalMultiplier * + increaseDefenseByLevel * increaseDefenseByLevelAttackType); + + private static int PhysicalSoftCriticalIncreaseDefenseByLevel(this IBattleEntityDump attacker, int cleanDamage, double softDamageMultiplier, double criticalMultiplier, + double increaseDefenseByLevel, double increaseDefenseByLevelAttackType) + => increaseDefenseByLevel == 1.0 + ? 0 + : (int)Math.Floor(cleanDamage / (1 / (increaseDefenseByLevel - 1) + 1) * softDamageMultiplier * criticalMultiplier * increaseDefenseByLevel * increaseDefenseByLevelAttackType); + + private static int PhysicalSoftCriticalIncreaseDefenseByLevelAttackType(this IBattleEntityDump attacker, int cleanDamage, double softDamageMultiplier, double criticalMultiplier, + double increaseDefenseByLevelAttackType, double increaseDefenseByLevel) + => increaseDefenseByLevelAttackType == 1.0 + ? 0 + : (int)Math.Floor(cleanDamage * increaseDefenseByLevelAttackType * softDamageMultiplier * criticalMultiplier * increaseDefenseByLevelAttackType * increaseDefenseByLevel); + + // Calculate all bonuses for physical soft damage + + private static int SoftDamageMultiplier(this IBattleEntityDump attacker, int cleanDamage, double softDamageMultiplier, double multiplierDamage) + => (int)Math.Floor((cleanDamage + 15) * softDamageMultiplier * multiplierDamage); + + private static int SoftDamageShell(this IBattleEntityDump attacker, int cleanDamage, double softDamageMultiplier, double shellPercentage, double increaseDefenseByLevel, + double increaseDefenseByLevelAttackType) + => (int)Math.Floor((cleanDamage + 15) * softDamageMultiplier * shellPercentage * increaseDefenseByLevel * increaseDefenseByLevelAttackType); + + private static int SoftDamageIncreaseAttack(this IBattleEntityDump attacker, int cleanDamage, double increaseAttack, double softDamageMultiplier, double increaseDefense, + double increaseDefenseAttackType) + => (int)Math.Floor(cleanDamage * increaseAttack * softDamageMultiplier * increaseDefense * increaseDefenseAttackType); + + private static int SoftDamageIncreaseDefenseByLevel(this IBattleEntityDump attacker, int cleanDamage, double softDamageMultiplier, double increaseDefenseByLevel, + double increaseDefenseByLevelAttackType) + => increaseDefenseByLevel == 1.0 + ? 0 + : (int)Math.Floor((cleanDamage + 15) / (1 / (increaseDefenseByLevel - 1 < 0 ? 0 : increaseDefenseByLevel - 1) + 1) * softDamageMultiplier * increaseDefenseByLevel * + increaseDefenseByLevelAttackType); + + private static int SoftDamageIncreaseDefenseByLevelAttackType(this IBattleEntityDump attacker, int cleanDamage, double softDamageMultiplier, double increaseDefenseByLevelAttackType, + double increaseDefenseByLevel) + => increaseDefenseByLevelAttackType == 1.0 + ? 0 + : (int)Math.Floor((cleanDamage + 15) / (1 / (increaseDefenseByLevelAttackType - 1 < 0 ? 0 : increaseDefenseByLevelAttackType - 1) + 1) * softDamageMultiplier * increaseDefenseByLevel * + increaseDefenseByLevelAttackType); + + // Calculate all bonuses for element soft damage + + private static int SoftElementDamage(this IBattleEntityDump attacker, int cleanDamage, double softDamageMultiplier, double elementMultiplier, double multiplierDamage) + => (int)Math.Floor((cleanDamage + 15) * softDamageMultiplier * elementMultiplier * multiplierDamage); + + private static int SoftElementDamageShell(this IBattleEntityDump attacker, int cleanDamage, double shellPercentage, double softDamageMultiplier, double elementMultiplier) + => (int)Math.Floor((cleanDamage + 15) * softDamageMultiplier * shellPercentage * elementMultiplier); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Extensions/DateTimeExtension.cs b/srcs/WingsAPI.Game/Extensions/DateTimeExtension.cs new file mode 100644 index 0000000..a04af51 --- /dev/null +++ b/srcs/WingsAPI.Game/Extensions/DateTimeExtension.cs @@ -0,0 +1,8 @@ +using System; + +namespace WingsEmu.Core.Extensions; + +public static class DateTimeExtension +{ + public static int GetTotalMillisecondUntilNow(this DateTime source) => (int)(source - DateTime.UtcNow).TotalMilliseconds; +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Extensions/DictionaryExtension.cs b/srcs/WingsAPI.Game/Extensions/DictionaryExtension.cs new file mode 100644 index 0000000..186d7b2 --- /dev/null +++ b/srcs/WingsAPI.Game/Extensions/DictionaryExtension.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; + +namespace WingsEmu.Core.Extensions; + +public static class DictionaryExtension +{ + public static TValue GetOrDefault(this IDictionary dictionary, TKey key, TValue defaultValue = default) => + dictionary.TryGetValue(key, out TValue value) ? value : defaultValue; + + public static TValue GetOrSetDefault(this IDictionary dictionary, TKey key, TValue defaultValue = default) + { + if (dictionary.TryGetValue(key, out TValue value)) + { + return value; + } + + dictionary[key] = defaultValue; + return defaultValue; + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Extensions/EncodingExtensions.cs b/srcs/WingsAPI.Game/Extensions/EncodingExtensions.cs new file mode 100644 index 0000000..e7d6608 --- /dev/null +++ b/srcs/WingsAPI.Game/Extensions/EncodingExtensions.cs @@ -0,0 +1,32 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Text; +using PhoenixLib.MultiLanguage; + +namespace WingsEmu.Core.Extensions; + +public static class EncodingExtensions +{ + public static Encoding GetEncoding(this RegionLanguageType key) + { + switch (key) + { + case RegionLanguageType.EN: + case RegionLanguageType.FR: + case RegionLanguageType.ES: + return Encoding.GetEncoding(1252); + case RegionLanguageType.DE: + case RegionLanguageType.PL: + case RegionLanguageType.IT: + case RegionLanguageType.CZ: + return Encoding.GetEncoding(1250); + case RegionLanguageType.TR: + return Encoding.GetEncoding(1254); + default: + throw new ArgumentOutOfRangeException(nameof(key), key, null); + } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Extensions/EquipmentOptionExtensions.cs b/srcs/WingsAPI.Game/Extensions/EquipmentOptionExtensions.cs new file mode 100644 index 0000000..519e8f9 --- /dev/null +++ b/srcs/WingsAPI.Game/Extensions/EquipmentOptionExtensions.cs @@ -0,0 +1,97 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using System.Linq; +using WingsAPI.Packets.Enums.Shells; +using WingsEmu.DTOs.BCards; +using WingsEmu.DTOs.Items; +using WingsEmu.Game._enum; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Items; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Extensions; + +public static class EquipmentOptionExtensions +{ + public static int GetJewelsCellonsValue(this IPlayerEntity character, CellonType cellonType) + { + int ring = character.GetCellonValue(EquipmentType.Ring, cellonType); + int necklace = character.GetCellonValue(EquipmentType.Necklace, cellonType); + int bracelet = character.GetCellonValue(EquipmentType.Bracelet, cellonType); + + return ring + necklace + bracelet; + } + + /// + /// Picks the maximum values between primary weapon and secondary weapon for the given shell optionType + /// + /// + /// + /// + public static int GetMaxWeaponShellValue(this IPlayerEntity character, ShellEffectType shellEffectType) + { + int mainValue = character.GetMaxWeaponShellValue(shellEffectType, true); + int secondValue = character.GetMaxWeaponShellValue(shellEffectType, false); + if (mainValue == 0 && secondValue == 0) + { + return 0; + } + + return mainValue >= secondValue ? mainValue : secondValue; + } + + public static List ShellBuffs(this IPlayerEntity playerEntity, GameItemInstance gameItemInstance) + { + List options = gameItemInstance.EquipmentOptions; + if (options == null) + { + return null; + } + + if (!options.Any()) + { + return null; + } + + List buffBCards = new(); + foreach (EquipmentOptionDTO option in options) + { + if (option.EquipmentOptionType != EquipmentOptionType.WEAPON_SHELL) + { + continue; + } + + var type = (ShellEffectType)option.Type; + + BCardDTO newBCard = type.TryCreateBuffBCard(option.Value); + if (newBCard == null) + { + continue; + } + + buffBCards.Add(newBCard); + } + + return buffBCards; + } + + public static void TryAddShellBuffs(this IPlayerEntity playerEntity, GameItemInstance gameItemInstance) + { + List buffBCards = playerEntity.ShellBuffs(gameItemInstance); + if (buffBCards == null) + { + return; + } + + if (!buffBCards.Any()) + { + return; + } + + playerEntity.BCardComponent.AddShellTrigger(gameItemInstance.GameItem.EquipmentSlot == EquipmentType.MainWeapon, buffBCards); + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Extensions/Lists.cs b/srcs/WingsAPI.Game/Extensions/Lists.cs new file mode 100644 index 0000000..7521885 --- /dev/null +++ b/srcs/WingsAPI.Game/Extensions/Lists.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using System.Linq; + +namespace WingsEmu.Core.Extensions; + +public static class Lists +{ + public static List Create(params T[] values) => new(values); + + public static List SetFirst(this List list, T value) where T : class + { + T firstValue = list.FirstOrDefault(); + if (firstValue == value) + { + return list; + } + + int index = list.IndexOf(value); + if (index < 0) + { + list.Add(firstValue); + } + else + { + list[index] = firstValue; + } + + list[0] = value; + + return list; + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Extensions/MapItemExtension.cs b/srcs/WingsAPI.Game/Extensions/MapItemExtension.cs new file mode 100644 index 0000000..fe474fb --- /dev/null +++ b/srcs/WingsAPI.Game/Extensions/MapItemExtension.cs @@ -0,0 +1,18 @@ +using WingsEmu.Game.Maps; + +namespace WingsEmu.Game.Extensions; + +public static class MapItemExtension +{ + public static string GenerateIn(this MapItem mapItem) => $"in 9 {mapItem.ItemVNum} {mapItem.TransportId} {mapItem.PositionX} {mapItem.PositionY} {mapItem.Amount} {(mapItem.IsQuest ? 1 : 0)} 0 -1"; + + public static string GenerateOut(this MapItem mapItem) => $"out 9 {mapItem.TransportId}"; + + public static string GenerateSay(this MapItem mapItem) => $"say 9 {mapItem.TransportId} 2 Please, pick me up... quickly!"; + + public static void BroadcastSayDrop(this MapItem mapItem) => mapItem.MapInstance.Broadcast(mapItem.GenerateSay()); + + public static void BroadcastIn(this MapItem mapItem) => mapItem.MapInstance.Broadcast(mapItem.GenerateIn()); + + public static void BroadcastOut(this MapItem mapItem) => mapItem.MapInstance.Broadcast(mapItem.GenerateOut()); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Extensions/Mates/MateExtensions.cs b/srcs/WingsAPI.Game/Extensions/Mates/MateExtensions.cs new file mode 100644 index 0000000..acf06c5 --- /dev/null +++ b/srcs/WingsAPI.Game/Extensions/Mates/MateExtensions.cs @@ -0,0 +1,266 @@ +using System; +using System.Threading.Tasks; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Items; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Mates.Events; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Battle; + +namespace WingsEmu.Game.Extensions.Mates; + +public static class MateExtensions +{ + public static int GetModifier(this IMateEntity mate) + { + return mate.AttackType switch + { + AttackType.Melee => mate.MeleeHpFactor, + AttackType.Ranged => mate.RangeDodgeFactor, + AttackType.Magical => mate.MagicMpFactor, + _ => 0 + }; + } + + + public static void TeleportNearCharacter(this IMateEntity mateEntity) + { + IClientSession session = mateEntity.Owner?.Session; + if (session?.CurrentMapInstance == null) + { + return; + } + + mateEntity.ChangePosition(new Position((short)(session.PlayerEntity.PositionX + (mateEntity.MateType == MateType.Partner ? -1 : 1)), (short)(session.PlayerEntity.PositionY + 1))); + + if (mateEntity.MapInstance.HasMapFlag(MapFlags.IS_BASE_MAP)) + { + mateEntity.MapX = mateEntity.Position.X; + mateEntity.MapX = mateEntity.Position.Y; + } + + if (mateEntity.MapInstance.MapInstanceType == MapInstanceType.Miniland && mateEntity.MapInstance.Id == mateEntity.Owner.Miniland.Id) + { + mateEntity.MinilandX = mateEntity.Position.X; + mateEntity.MinilandX = mateEntity.Position.Y; + } + + bool isBlocked = session.PlayerEntity.MapInstance.IsBlockedZone(mateEntity.PositionX, mateEntity.PositionY); + + if (!isBlocked) + { + return; + } + + mateEntity.ChangePosition(new Position(session.PlayerEntity.PositionX, session.PlayerEntity.PositionY)); + + if (mateEntity.MapInstance.MapInstanceType == MapInstanceType.Miniland && mateEntity.MapInstance.Id == mateEntity.Owner.Miniland.Id) + { + mateEntity.MinilandX = mateEntity.Position.X; + mateEntity.MinilandX = mateEntity.Position.Y; + } + + if (!mateEntity.MapInstance.HasMapFlag(MapFlags.IS_BASE_MAP)) + { + return; + } + + mateEntity.MapX = mateEntity.Position.X; + mateEntity.MapX = mateEntity.Position.Y; + } + + public static void TeleportToCharacter(this IMateEntity mateEntity) + { + IClientSession session = mateEntity.Owner?.Session; + if (session == null || session.PlayerEntity.IsOnVehicle) + { + return; + } + + + mateEntity.ChangePosition(new Position(session.PlayerEntity.PositionX, session.PlayerEntity.PositionY)); + session.BroadcastMateTeleport(mateEntity); + } + + public static bool IsInCombat(this IMateEntity mateEntity, DateTime date) => mateEntity.LastDefence.AddSeconds(4) > date || mateEntity.LastSkillUse.AddSeconds(2) > date; + + public static bool CanAttack(this IMateEntity mateEntity) => !mateEntity.Owner.IsOnVehicle && mateEntity.Loyalty != 0 && mateEntity.CanPerformAttack(); + + public static bool CanMove(this IMateEntity mateEntity) + { + if (mateEntity.Loyalty <= 0) + { + return false; + } + + return mateEntity.CanPerformMove(); + } + + public static void RemoveLoyalty(this IMateEntity mateEntity, short loyalty, GameMinMaxConfiguration minMaxConfiguration, IGameLanguageService languageService) + { + if (mateEntity.Loyalty < minMaxConfiguration.MinMateLoyalty) + { + mateEntity.Loyalty = minMaxConfiguration.MinMateLoyalty; + return; + } + + mateEntity.Loyalty -= Math.Abs(loyalty); + + if (mateEntity.Loyalty < minMaxConfiguration.MinMateLoyalty) + { + mateEntity.Loyalty = minMaxConfiguration.MinMateLoyalty; + } + + mateEntity.Owner.Session.SendPetInfo(mateEntity, languageService); + } + + public static void AddLoyalty(this IMateEntity mateEntity, short loyalty, GameMinMaxConfiguration minMaxConfiguration, IGameLanguageService languageService) + { + if (mateEntity.Loyalty == minMaxConfiguration.MaxMateLoyalty) + { + return; + } + + if (mateEntity.Loyalty > minMaxConfiguration.MaxMateLoyalty) + { + mateEntity.Loyalty = minMaxConfiguration.MaxMateLoyalty; + return; + } + + mateEntity.Loyalty += Math.Abs(loyalty); + + if (mateEntity.Loyalty > minMaxConfiguration.MaxMateLoyalty) + { + mateEntity.Loyalty = minMaxConfiguration.MaxMateLoyalty; + } + + mateEntity.Owner.Session.SendPetInfo(mateEntity, languageService); + } + + public static bool CanWearItem(this IMateEntity entity, IGameItem gameItem) + { + if (gameItem.EquipmentSlot != EquipmentType.Sp && gameItem.EquipmentSlot != EquipmentType.Armor && gameItem.EquipmentSlot != EquipmentType.MainWeapon) + { + return true; + } + + if (gameItem.IsPartnerSpecialist) + { + AttackType mateSpecialist = gameItem.PartnerClass switch + { + 0 => AttackType.Melee, + 1 => AttackType.Ranged, + 2 => AttackType.Magical, + _ => AttackType.Other + }; + + return mateSpecialist == entity.AttackType; + } + + AttackType mateClass = gameItem.Class switch + { + 0 => AttackType.Melee, + 2 => AttackType.Melee, + 4 => AttackType.Ranged, + 8 => AttackType.Magical, + _ => AttackType.Other + }; + + return mateClass == entity.AttackType; + } + + public static bool CanWearSpecialist(this IMateEntity entity, IGameItem gameItem) => gameItem.IsPartnerSpecialist && entity.AttackType == (AttackType)gameItem.PartnerClass; + + public static int GetSpCooldown(this IMateEntity mate) => !mate.SpCooldownEnd.HasValue ? 0 : (int)(mate.SpCooldownEnd.Value - DateTime.UtcNow).TotalSeconds; + + public static bool IsSpCooldownElapsed(this IMateEntity mate) + { + if (!mate.SpCooldownEnd.HasValue) + { + return true; + } + + return mate.SpCooldownEnd.Value < DateTime.UtcNow; + } + + public static bool HavePartnerSkill(this IMateEntity mateEntity, byte slot) + { + bool skill = slot switch + { + 0 => mateEntity.Specialist.PartnerSkill1, + 1 => mateEntity.Specialist.PartnerSkill2, + 2 => mateEntity.Specialist.PartnerSkill3, + _ => false + }; + + return skill; + } + + public static async Task RemovePartnerSp(this IMateEntity mateEntity) + { + if (!mateEntity.IsUsingSp) + { + return; + } + + await mateEntity.Owner.Session.EmitEventAsync(new MateSpUntransformEvent + { + MateEntity = mateEntity + }); + } + + public static void RefreshPartnerSkills(this IMateEntity mateEntity) + { + if (mateEntity.MateType != MateType.Partner) + { + return; + } + + if (mateEntity.MonsterSkills?.Count == 0) + { + return; + } + + mateEntity.Owner.Session.RefreshSkillList(); + } + + public static bool SkillRankS(this IMateEntity mateEntity, byte slot) => mateEntity.Specialist?.PartnerSkills?.Find(s => s.Rank == 7 && s.Slot == slot) != null; + + public static byte GetFreeMateSlot(this IPlayerEntity player, bool isPartner) + { + byte slot; + byte maxCount = isPartner ? player.MaxPartnerCount : player.MaxPetCount; + for (slot = 0; slot < maxCount; slot++) + { + IMateEntity getMate = isPartner + ? player.MateComponent.GetMate(m => m.PetSlot == slot && m.MateType == MateType.Partner) + : player.MateComponent.GetMate(m => m.PetSlot == slot && m.MateType == MateType.Pet); + + if (getMate != null) + { + continue; + } + + break; + } + + return slot; + } + + public static void RefreshEquipmentValues(this IMateEntity mateEntity, GameItemInstance gameItemInstance, bool clearValues) + { + if (clearValues) + { + mateEntity.BCardComponent.ClearEquipmentBCards(gameItemInstance.GameItem.EquipmentSlot); + return; + } + + mateEntity.BCardComponent.AddEquipmentBCards(gameItemInstance.GameItem.EquipmentSlot, gameItemInstance.GameItem.BCards); + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Extensions/Mates/MatePacketExtensions.cs b/srcs/WingsAPI.Game/Extensions/Mates/MatePacketExtensions.cs new file mode 100644 index 0000000..7e8a10f --- /dev/null +++ b/srcs/WingsAPI.Game/Extensions/Mates/MatePacketExtensions.cs @@ -0,0 +1,368 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using PhoenixLib.MultiLanguage; +using WingsEmu.DTOs.Maps; +using WingsEmu.DTOs.Skills; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage.Configuration; +using WingsEmu.Game.Algorithm; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Items; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Skills; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Extensions.Mates; + +public static class MatePacketExtensions +{ + private static ICharacterAlgorithm _algorithm => StaticCharacterAlgorithmService.Instance; + + public static string GenerateMateControl(this IBattleEntity mateEntity) => $"ctl 2 {mateEntity.Id} 3 0"; + + public static string GeneratePartnerSkillResetCooldown(this IClientSession session, short slot) => $"psr {slot}"; + + public static string GeneratePetSkill(this IMateEntity mateEntity) + { + IBattleEntitySkill skill = mateEntity.Skills.FirstOrDefault(sk => + sk.Skill.SkillType == SkillType.PartnerSkill && mateEntity.IsTeamMember && mateEntity.MateType == MateType.Pet); + string packet = "petski "; + if (skill == null) + { + return packet + "-1"; + } + + return packet + $"{skill.Skill.Id.ToString()}"; + } + + public static string GenerateMateSkillResetCooldown(this IClientSession session) => "petsr 0"; + + public static string GenerateMateDelayPacket(this IMateEntity mateEntity, int delay, GuriType type, string argument) => $"pdelay {delay} {(byte)type} {argument}"; + + public static string GenerateCMode(this IMateEntity mateEntity, short morphId) => $"c_mode 2 {mateEntity.Id} {morphId} 0 0"; + + public static string GenerateCond(this IMateEntity mateEntity) => $"cond 2 {mateEntity.Id} {(mateEntity.CanAttack() ? 0 : 1)} {(mateEntity.CanMove() ? 0 : 1)} {mateEntity.Speed}"; + + public static string GenerateCond(this INpcEntity npcEntity) => $"cond 2 {npcEntity.Id} {(npcEntity.CanPerformAttack() ? 0 : 1)} {(npcEntity.CanPerformMove() ? 0 : 1)} {npcEntity.Speed}"; + + public static string GenerateEInfo(this IMateEntity mateEntity, IGameLanguageService gameLanguage, RegionLanguageType language, ISpPartnerConfiguration spPartnerConfiguration) + { + string name; + if (mateEntity.IsUsingSp && mateEntity.Specialist != null) + { + GameDialogKey specialistNameKey = Enum.Parse(spPartnerConfiguration.GetByMorph(mateEntity.Specialist.GameItem.Morph).Name); + name = gameLanguage.GetLanguage(specialistNameKey, language); + name = name.Replace(' ', '^'); + } + else + { + name = gameLanguage.GetLanguage(GameDataType.NpcMonster, mateEntity.Name, language); + name = string.IsNullOrEmpty(mateEntity.MateName) || mateEntity.Name == mateEntity.MateName ? name.Replace(' ', '^') : mateEntity.MateName.Replace(' ', '^'); + } + + return + "e_info " + + "10 " + + $"{mateEntity.NpcMonsterVNum} " + + $"{mateEntity.Level} " + + $"{mateEntity.Element} " + + $"{(byte)mateEntity.AttackType} " + + $"{mateEntity.ElementRate} " + + $"{mateEntity.Attack} " + + $"{mateEntity.StatisticsComponent.MinDamage} " + + $"{mateEntity.StatisticsComponent.MaxDamage} " + + $"{mateEntity.StatisticsComponent.HitRate} " + + $"{mateEntity.StatisticsComponent.CriticalChance} " + + $"{mateEntity.StatisticsComponent.CriticalDamage} " + + $"{mateEntity.Defence} " + + $"{mateEntity.StatisticsComponent.MeleeDefense} " + + $"{mateEntity.StatisticsComponent.MeleeDodge} " + + $"{mateEntity.StatisticsComponent.RangeDefense} " + + $"{mateEntity.StatisticsComponent.RangeDodge} " + + $"{mateEntity.StatisticsComponent.MagicDefense} " + + $"{mateEntity.StatisticsComponent.FireResistance} " + + $"{mateEntity.StatisticsComponent.WaterResistance} " + + $"{mateEntity.StatisticsComponent.LightResistance} " + + $"{mateEntity.StatisticsComponent.ShadowResistance} " + + $"{mateEntity.MaxHp} " + + $"{mateEntity.MaxMp} " + + "-1 " + + $"{name}"; + } + + public static string GetPartnerName(this IMateEntity mateEntity, IGameLanguageService gameLanguage, RegionLanguageType language, ISpPartnerConfiguration config, bool foe = false) + { + if (mateEntity.Specialist != null && mateEntity.IsUsingSp & !foe) + { + SpPartnerInfo partnerInfo = config?.GetByMorph(mateEntity.Specialist.GameItem.Morph); + if (partnerInfo != null && Enum.TryParse(partnerInfo.Name, out GameDialogKey key)) + { + return gameLanguage.GetLanguage(key, language); + } + } + + string name = string.IsNullOrEmpty(mateEntity.MateName) || mateEntity.MateName == mateEntity.Name + ? gameLanguage.GetLanguage(GameDataType.NpcMonster, mateEntity.Name, language) + : mateEntity.MateName; + name = name.Replace(' ', '^'); + if (foe) + { + name = "!§$%&/()=?*+~#"; + } + + return name; + } + + + public static string GenerateIn(this IMateEntity mateEntity, IGameLanguageService gameLanguage, RegionLanguageType language, ISpPartnerConfiguration config, + bool foe = false) => + mateEntity.GenerateIn(mateEntity.GetPartnerName(gameLanguage, language, config, foe)); + + public static string GenerateIn(this IMateEntity mateEntity, string name) + { + int faction = 0; + if (mateEntity.MapInstance != null && mateEntity.MapInstance.HasMapFlag(MapFlags.ACT_4)) + { + faction = (byte)mateEntity.Owner.Faction + 2; + } + + if (mateEntity.Specialist != null && mateEntity.IsUsingSp) + { + string mateSkills = mateEntity.Specialist.GenerateSkillInfo(0); + mateSkills = mateSkills.Remove(mateSkills.Length - 1); + + return + "in " + + "2 " + + $"{mateEntity.NpcMonsterVNum} " + + $"{mateEntity.Id} " + + $"{mateEntity.PositionX} " + + $"{mateEntity.PositionY} " + + $"{mateEntity.Direction} " + + $"{mateEntity.GetHpPercentage()} " + + $"{mateEntity.GetMpPercentage()} " + + "0 " + + $"{faction} " + + "3 " + + $"{mateEntity.CharacterId} " + + "1 " + + $"{(mateEntity.IsSitting ? 1 : 0)} " + + $"{(mateEntity.IsUsingSp ? mateEntity.Specialist.GameItem.Morph : mateEntity.Skin != 0 ? mateEntity.Skin : -1)} " + + $"{name.Replace(" ", "^")} " + + "1 " + + "0 " + + "1 " + + $"{mateSkills} " + + $"{(mateEntity.SkillRankS(0) ? "4237" : "0")} " + + $"{(mateEntity.SkillRankS(1) ? "4238" : "0")} " + + $"{(mateEntity.SkillRankS(2) ? "4239" : "0")} " + + "0 " + + "0"; + } + + return + "in " + + "2 " + + $"{mateEntity.NpcMonsterVNum} " + + $"{mateEntity.Id} " + + $"{mateEntity.PositionX} " + + $"{mateEntity.PositionY} " + + $"{mateEntity.Direction} " + + $"{mateEntity.GetHpPercentage()} " + + $"{mateEntity.GetMpPercentage()} " + + "0 " + + $"{faction} " + + "3 " + + $"{mateEntity.CharacterId} " + + "1 " + + $"{(mateEntity.IsSitting ? 1 : 0)} " + + $"{(mateEntity.IsUsingSp && mateEntity.Specialist != null ? mateEntity.Specialist.GameItem.Morph : mateEntity.Skin != 0 ? mateEntity.Skin : -1)} " + + $"{name} " + + $"{(byte)mateEntity.MateType + 1} " + + "-1 " + + "0 " + + "0 " + + "0 " + + "0 " + + "0 " + + "0 " + + "0 " + + "0 " + + "0 " + + "0 " + + "0"; + } + + public static string GenerateOut(this IMateEntity mateEntity) => $"out 2 {mateEntity.Id}"; + + public static string GeneratePst(this IMateEntity mateEntity) + { + IReadOnlyList buffs = mateEntity.BuffComponent.GetAllBuffs(); + string buffString = buffs.Aggregate(string.Empty, (current, buff) => current + $"{buff.CardId}.{buff.CasterLevel} "); + return $"pst 2 {mateEntity.Id} {(int)mateEntity.MateType} {mateEntity.GetHpPercentage()} {mateEntity.GetMpPercentage()} {mateEntity.Hp} {mateEntity.Mp} 0 0 0 {buffString}"; + } + + public static string GeneratePski(this IMateEntity mateEntity) + { + if (mateEntity.IsUsingSp && mateEntity.Specialist != null) + { + return $"pski {mateEntity.Specialist.GenerateSkillInfo(1)}"; + } + + return "dpski"; + } + + public static string GenerateRc(this IMateEntity mateEntity, int heal) => $"rc 2 {mateEntity.Id} {heal} 0"; + + public static string GenerateRest(this IMateEntity mateEntity) => $"rest 2 {mateEntity.Id} {(mateEntity.IsSitting ? 1 : 0)}"; + + public static string GenerateSayPacket(this IMateEntity mateEntity, string message, int type) => $"say 2 {mateEntity.Id} 2 {message}"; + + public static string GenerateScPacket(this IMateEntity mateEntity, IGameLanguageService gameLanguage, RegionLanguageType language) + { + mateEntity.RefreshStatistics(); + + string name = string.IsNullOrEmpty(mateEntity.MateName) || mateEntity.Name == mateEntity.MateName + ? gameLanguage.GetLanguage(GameDataType.NpcMonster, mateEntity.Name, language) + : mateEntity.MateName; + + if (mateEntity.MateType == MateType.Partner) + { + return + "sc_n " + + $"{mateEntity.PetSlot} " + + $"{mateEntity.NpcMonsterVNum} " + + $"{mateEntity.Id} " + + $"{mateEntity.Level} " + + $"{mateEntity.Loyalty} " + + $"{mateEntity.Experience} " + + $"{(mateEntity.Weapon != null ? $"{mateEntity.Weapon.ItemVNum}.{mateEntity.Weapon.Rarity}.{mateEntity.Weapon.Upgrade}" : "-1")} " + + $"{(mateEntity.Armor != null ? $"{mateEntity.Armor.ItemVNum}.{mateEntity.Armor.Rarity}.{mateEntity.Armor.Upgrade}" : "-1")} " + + $"{(mateEntity.Gloves != null ? $"{mateEntity.Gloves.ItemVNum}.0.0" : "-1")} " + + $"{(mateEntity.Boots != null ? $"{mateEntity.Boots.ItemVNum}.0.0" : "-1")} " + + "0 " + + "0 " + + $"{(byte)mateEntity.AttackType} " + + $"{mateEntity.GeneratePartnerEqStats()} " + + $"{mateEntity.Hp} " + + $"{mateEntity.MaxHp} " + + $"{mateEntity.Mp} " + + $"{mateEntity.MaxMp} " + + $"{(byte)(mateEntity.IsTeamMember ? 1 : 0)} " + + $"{_algorithm.GetLevelXp(mateEntity.Level, true, mateEntity.MateType)} " + + $"{name.Replace(' ', '^')} " + + $"{(mateEntity.IsUsingSp && mateEntity.Specialist != null ? mateEntity.Specialist.GameItem.Morph : mateEntity.Skin != 0 ? mateEntity.Skin : -1)} " + + $"{(mateEntity.IsSummonable ? 1 : 0)} " + + $"{(mateEntity.Specialist != null ? $"{mateEntity.Specialist.ItemVNum}.{mateEntity.Specialist.Agility}" : "-1")} " + + $"{mateEntity.Specialist.GenerateSkillInfo(2)} " + + "0 " + + "0"; + } + + return + "sc_p " + + $"{mateEntity.PetSlot} " + + $"{mateEntity.NpcMonsterVNum} " + + $"{mateEntity.Id} " + + $"{mateEntity.Level} " + + $"{mateEntity.Loyalty} " + + $"{mateEntity.Experience} " + + $"{(byte)mateEntity.AttackType} " + + $"{mateEntity.Attack} " + + $"{mateEntity.DamagesMinimum} " + + $"{mateEntity.DamagesMaximum} " + + $"{mateEntity.HitRate} " + + $"{mateEntity.HitCriticalChance} " + + $"{mateEntity.HitCriticalDamage} " + + $"{mateEntity.Defence} " + + $"{mateEntity.CloseDefence} " + + $"{mateEntity.DefenceDodge} " + + $"{mateEntity.DistanceDefence} " + + $"{mateEntity.DistanceDodge} " + + $"{mateEntity.MagicDefence} " + + $"{mateEntity.Element} " + + $"{mateEntity.FireResistance} " + + $"{mateEntity.WaterResistance} " + + $"{mateEntity.LightResistance} " + + $"{mateEntity.DarkResistance} " + + $"{mateEntity.Hp} " + + $"{mateEntity.MaxHp} " + + $"{mateEntity.Mp} " + + $"{mateEntity.MaxMp} " + + $"{(byte)(mateEntity.IsTeamMember ? 1 : 0)} " + + $"{_algorithm.GetLevelXp(mateEntity.Level, true, mateEntity.MateType)} " + + $"{(byte)(mateEntity.CanPickUp ? 1 : 0)} " + + $"{name.Replace(' ', '^')} " + + $"{(byte)(mateEntity.IsSummonable ? 1 : 0)} " + + "0"; + } + + public static string GeneratePartnerEqStats(this IMateEntity mateEntity) + { + GameItemInstance mateWeapon = mateEntity.Weapon; + GameItemInstance mateArmor = mateEntity.Armor; + + mateEntity.Attack = 0; + mateEntity.Defence = 0; + + if (mateWeapon != null) + { + mateEntity.Attack = mateWeapon.Upgrade; + } + + if (mateArmor != null) + { + mateEntity.Defence = mateArmor.Upgrade; + } + + return + $"{mateEntity.Attack} " + + $"{mateEntity.StatisticsComponent.MinDamage} " + + $"{mateEntity.StatisticsComponent.MaxDamage} " + + $"{mateEntity.StatisticsComponent.HitRate} " + + $"{mateEntity.StatisticsComponent.CriticalChance} " + + $"{mateEntity.StatisticsComponent.CriticalDamage} " + + $"{mateEntity.Defence} " + + $"{mateEntity.StatisticsComponent.MeleeDefense} " + + $"{mateEntity.StatisticsComponent.MeleeDodge} " + + $"{mateEntity.StatisticsComponent.RangeDefense} " + + $"{mateEntity.StatisticsComponent.RangeDodge} " + + $"{mateEntity.StatisticsComponent.MagicDefense} " + + $"{mateEntity.Element} " + + $"{mateEntity.StatisticsComponent.FireResistance} " + + $"{mateEntity.StatisticsComponent.WaterResistance} " + + $"{mateEntity.StatisticsComponent.LightResistance} " + + $"{mateEntity.StatisticsComponent.ShadowResistance}"; + } + + public static string GenerateStatInfo(this IMateEntity mateEntity) => + $"st 2 {mateEntity.Id} {mateEntity.Level} 0 {mateEntity.GetHpPercentage()} {mateEntity.GetMpPercentage()} {mateEntity.Hp} {mateEntity.Mp}"; + + public static string GenerateMateDance(this IMateEntity mateEntity) => $"guri 2 2 {mateEntity.Id}"; + + public static string GenerateMateSpCooldown(this IMateEntity mateEntity, short time) => $"psd {time}"; + + public static string GenerateRemoveMateSpSkills(this IMateEntity mateEntity) => "dpski"; + + public static void SendPetInfo(this IClientSession session, IMateEntity mate, IGameLanguageService gameLanguage) => + session.SendPacket(mate.GenerateScPacket(gameLanguage, session.UserLanguage)); + + public static void SendCondMate(this IClientSession session, IMateEntity mate) => session.SendPacket(mate.GenerateCond()); + public static void SendCondMate(this IClientSession session, INpcEntity npc) => session.SendPacket(npc.GenerateCond()); + public static void SendOutMate(this IClientSession session, IMateEntity mate) => session.SendPacket(mate.GenerateOut()); + public static void SendMatePskiPacket(this IClientSession session, IMateEntity mate) => session.SendPacket(mate.GeneratePski()); + public static void SendMateSkillPacket(this IClientSession session, IMateEntity mateEntity) => session.SendPacket(mateEntity.GeneratePetSkill()); + public static void SendMateSpCooldown(this IClientSession session, IMateEntity mate, short time) => session.SendPacket(mate.GenerateMateSpCooldown(time)); + + public static void SendMateDelay(this IClientSession session, IMateEntity mateEntity, int delay, GuriType type, string argument) => + session.SendPacket(mateEntity.GenerateMateDelayPacket(delay, type, argument)); + + public static void SendRemoveMateSpSkills(this IClientSession session, IMateEntity mateEntity) => session.SendPacket(mateEntity.GenerateRemoveMateSpSkills()); + public static void SendMateControl(this IClientSession session, IBattleEntity mateEntity) => session.SendPacket(mateEntity.GenerateMateControl()); + public static void SendMateSkillCooldownReset(this IClientSession session) => session.SendPacket(session.GenerateMateSkillResetCooldown()); + public static void SendPartnerSkillCooldown(this IClientSession session, short slot) => session.SendPacket(session.GeneratePartnerSkillResetCooldown(slot)); + public static void SendMateLife(this IClientSession session, IMateEntity mate) => session.SendPacket(mate.GeneratePst()); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Extensions/Mates/MateStatisticsExtensions.cs b/srcs/WingsAPI.Game/Extensions/Mates/MateStatisticsExtensions.cs new file mode 100644 index 0000000..957a337 --- /dev/null +++ b/srcs/WingsAPI.Game/Extensions/Mates/MateStatisticsExtensions.cs @@ -0,0 +1,24 @@ +using System; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Mates; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Extensions.Mates; + +public static class MateStatisticsExtensions +{ + public static int HealthHpLoad(this IMateEntity mateEntity) + { + int regen = mateEntity.BCardComponent.GetAllBCardsInformation(BCardType.Recovery, (byte)AdditionalTypes.Recovery.HPRecoveryIncreased, mateEntity.Level).firstData + - mateEntity.BCardComponent.GetAllBCardsInformation(BCardType.Recovery, (byte)AdditionalTypes.Recovery.HPRecoveryDecreased, mateEntity.Level).firstData; + return mateEntity.IsSitting ? regen + 50 : (DateTime.UtcNow - mateEntity.LastDefence).TotalSeconds > 4 ? regen + 20 : 0; + } + + public static int HealthMpLoad(this IMateEntity mateEntity) + { + int regen = mateEntity.BCardComponent.GetAllBCardsInformation(BCardType.Recovery, (byte)AdditionalTypes.Recovery.MPRecoveryIncreased, mateEntity.Level).firstData + - mateEntity.BCardComponent.GetAllBCardsInformation(BCardType.Recovery, (byte)AdditionalTypes.Recovery.MPRecoveryDecreased, mateEntity.Level).firstData; + return mateEntity.IsSitting ? regen + 50 : + (DateTime.UtcNow - mateEntity.LastDefence).TotalSeconds > 4 ? regen + 20 : 0; + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Extensions/NpcMonsterExtension.cs b/srcs/WingsAPI.Game/Extensions/NpcMonsterExtension.cs new file mode 100644 index 0000000..3249ed4 --- /dev/null +++ b/srcs/WingsAPI.Game/Extensions/NpcMonsterExtension.cs @@ -0,0 +1,233 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using PhoenixLib.MultiLanguage; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Entities.Event; +using WingsEmu.Game.Items; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Monster; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Npcs; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Battle; + +namespace WingsEmu.Game.Extensions; + +public static class NpcMonsterExtension +{ + public static string GenerateEInfo(this IMonsterData npcMonster, IGameLanguageService gameLanguage, RegionLanguageType languageType) + { + string name = gameLanguage.GetLanguage(GameDataType.NpcMonster, npcMonster.Name, languageType); + return "e_info 10 " + + $"{npcMonster.MonsterVNum} " + + $"{npcMonster.BaseLevel} " + + $"{npcMonster.BaseElement} " + + $"{(byte)npcMonster.AttackType} " + + $"{npcMonster.BaseElementRate} " + + $"{npcMonster.AttackUpgrade} " + + $"{npcMonster.BaseDamageMinimum} " + + $"{npcMonster.BaseDamageMaximum} " + + $"{npcMonster.BaseConcentrate} " + + $"{npcMonster.BaseCriticalChance} " + + $"{npcMonster.BaseCriticalRate} " + + $"{npcMonster.DefenceUpgrade} " + + $"{npcMonster.BaseCloseDefence} " + + $"{npcMonster.DefenceDodge} " + + $"{npcMonster.DistanceDefence} " + + $"{npcMonster.DistanceDefenceDodge} " + + $"{npcMonster.MagicDefence} " + + $"{npcMonster.BaseFireResistance} " + + $"{npcMonster.BaseWaterResistance} " + + $"{npcMonster.BaseLightResistance} " + + $"{npcMonster.BaseDarkResistance} " + + $"{npcMonster.BaseMaxHp} " + + $"{npcMonster.BaseMaxMp} " + + "-1 " + + $"{name.Replace(' ', '^')}"; + } + + public static void SendNpcInfo(this IClientSession session, IMonsterData npcMonster, IGameLanguageService gameLanguage) => + session.SendPacket(npcMonster.GenerateEInfo(gameLanguage, session.UserLanguage)); + + public static int GetModifier(this MonsterData monster) + { + return monster.AttackType switch + { + AttackType.Melee => monster.MeleeHpFactor, + AttackType.Ranged => monster.RangeDodgeFactor, + AttackType.Magical => monster.MagicMpFactor + }; + } + + public static bool CanHit(this IMonsterData npcMonster, IBattleEntity entity) + { + switch (entity) + { + case IPlayerEntity player when player.CheatComponent.IsInvisible: + return false; + case IMateEntity mateEntity: + + if (mateEntity.Owner == null) + { + break; + } + + if (mateEntity.Owner.IsInvisible()) + { + return false; + } + + if (mateEntity.Owner.Invisible) + { + return false; + } + + if (mateEntity.Owner.CheatComponent.IsInvisible) + { + return false; + } + + if (mateEntity.Owner.IsOnVehicle) + { + return false; + } + + break; + } + + return !(npcMonster.MonsterRaceType == MonsterRaceType.People && npcMonster.MonsterRaceSubType == 3 && entity.Faction == FactionType.Angel + | npcMonster.MonsterRaceType == MonsterRaceType.Angel && entity.Faction == FactionType.Demon); + } + + public static bool FindPhantomAmulet(this IMonsterData npcMonster, IBattleEntity entity) + { + if (npcMonster.RawHostility != (int)HostilityType.ATTACK_NOT_WEARING_PHANTOM_AMULET) + { + return false; + } + + switch (entity) + { + case IPlayerEntity character: + GameItemInstance amulet = character.Amulet; + if (amulet == null) + { + return false; + } + + return amulet.GameItem.Id == (short)ItemVnums.PHANTOM_AMULET; + case IMateEntity mateEntity: + IPlayerEntity owner = mateEntity.Owner; + GameItemInstance ownerAmulet = owner.Amulet; + if (ownerAmulet == null) + { + return false; + } + + return ownerAmulet.GameItem.Id == (short)ItemVnums.PHANTOM_AMULET; + } + + return true; + } + + public static MonsterRaceType GetMonsterRaceType(this IBattleEntity entity) + { + return entity switch + { + IPlayerEntity => MonsterRaceType.People, + IMateEntity mateEntity => mateEntity.MonsterRaceType, + INpcEntity mapNpc => mapNpc.MonsterRaceType, + IMonsterEntity monster => monster.MonsterRaceType, + _ => MonsterRaceType.People + }; + } + + public static Enum GetMonsterRaceSubType(this IMonsterData monster) + { + MonsterRaceType monsterRaceType = monster.MonsterRaceType; + byte subType = monster.MonsterRaceSubType; + + Enum raceSubType; + switch (monsterRaceType) + { + case MonsterRaceType.LowLevel: + raceSubType = (MonsterSubRace.LowLevel)subType; + break; + case MonsterRaceType.HighLevel: + raceSubType = (MonsterSubRace.HighLevel)subType; + break; + case MonsterRaceType.Furry: + raceSubType = (MonsterSubRace.Furry)subType; + break; + case MonsterRaceType.People: + raceSubType = (MonsterSubRace.People)subType; + break; + case MonsterRaceType.Angel: + raceSubType = (MonsterSubRace.Angels)subType; + break; + case MonsterRaceType.Undead: + raceSubType = (MonsterSubRace.Undead)subType; + break; + case MonsterRaceType.Spirit: + raceSubType = (MonsterSubRace.Spirits)subType; + break; + case MonsterRaceType.Other: + raceSubType = (MonsterSubRace.Other)subType; + break; + case MonsterRaceType.Fixed: + raceSubType = (MonsterSubRace.Fixed)subType; + break; + default: + return null; + } + + return raceSubType; + } + + public static string GenerateCMode(this IMonsterEntity monsterEntity, short? value = null) => $"c_mode 3 {monsterEntity.Id} {value ?? monsterEntity.Morph} 0 0"; + public static void BroadcastMonsterMorph(this IMonsterEntity monsterEntity, short? value = null) => monsterEntity.MapInstance.Broadcast(monsterEntity.GenerateCMode(value)); + + public static bool IsMonsterSpawningMonstersForQuest(this IMonsterEntity monsterEntity) => monsterEntity.RawHostility > 20000; + + public static string GenerateEnhancedGuri(this IMonsterEntity monsterEntity) => $"guri 21 3 {monsterEntity.Id}"; + public static void BroadcastEnhancedGuri(this IMonsterEntity monsterEntity) => monsterEntity.MapInstance.Broadcast(monsterEntity.GenerateEnhancedGuri()); + + public static bool IsMandra(this IBattleEntity battleEntity) + { + if (battleEntity is not IMonsterEntity monsterEntity) + { + return false; + } + + return monsterEntity.MonsterVNum is >= (short)MonsterVnum.HAPPY_MANDRA and <= (short)MonsterVnum.STRONG_GRASSMANDRA + or (short)MonsterVnum.GIANT_MANDRA or (short)MonsterVnum.DASHING_MANDRA; + } + + public static bool IsPhantom(this IMonsterEntity monsterEntity) => + (MonsterVnum)monsterEntity.MonsterVNum is MonsterVnum.RUBY_PHANTOM or MonsterVnum.EMERALD_PHANTOM or MonsterVnum.SAPPHIRE_PHANTOM; + + public static bool IsPhantom(this INpcEntity npcEntity) => + (MonsterVnum)npcEntity.MonsterVNum is MonsterVnum.RUBY_SHADOW_PHANTOM or MonsterVnum.EMERALD_SHADOW_PHANTOM or MonsterVnum.SAPPHIRE_SHADOW_PHANTOM; + + public static async Task TryDespawnTimeSpacePortal(this IClientSession session) + { + INpcEntity timeSpacePortal = session.CurrentMapInstance.GetPassiveNpcs().FirstOrDefault(x => x.TimeSpaceOwnerId.HasValue && x.TimeSpaceOwnerId.Value == session.PlayerEntity.Id); + if (timeSpacePortal == null) + { + return; + } + + string firstEffectPacket = timeSpacePortal.GenerateEffectGround(EffectType.BlueTimeSpace, timeSpacePortal.PositionX, timeSpacePortal.PositionY, true); + string secondEffectPacket = timeSpacePortal.GenerateEffectGround(EffectType.BlueRemoveTimeSpace, timeSpacePortal.PositionX, timeSpacePortal.PositionY, false); + + timeSpacePortal.MapInstance.Broadcast(_ => firstEffectPacket); + timeSpacePortal.MapInstance.Broadcast(_ => secondEffectPacket); + + await timeSpacePortal.EmitEventAsync(new MapLeaveNpcEntityEvent(timeSpacePortal)); + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Extensions/RarifyExtension.cs b/srcs/WingsAPI.Game/Extensions/RarifyExtension.cs new file mode 100644 index 0000000..d4287f9 --- /dev/null +++ b/srcs/WingsAPI.Game/Extensions/RarifyExtension.cs @@ -0,0 +1,79 @@ +using System.Threading.Tasks; +using WingsEmu.DTOs.Items; +using WingsEmu.Game._enum; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Items; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Extensions; + +public static class RarifyExtension +{ + public static async Task EmitRarifyEvent(this IClientSession sender, InventoryItem invItem, InventoryItem invAmulet, bool isDrop = false, bool isScroll = false) + { + RarifyMode mode = RarifyMode.Normal; + RarifyProtection protection = RarifyProtection.None; + + if (invItem.ItemInstance.Type != ItemInstanceType.WearableInstance) + { + return; + } + + GameItemInstance item = invItem.ItemInstance; + GameItemInstance amulet = invAmulet?.ItemInstance; + + if (isDrop) + { + mode = RarifyMode.Drop; + } + + if (isScroll) + { + protection = RarifyProtection.Scroll; + } + + if (amulet == null) + { + await sender.EmitEventAsync(new GamblingEvent(invItem, null, mode, protection)); + return; + } + + switch (amulet.ItemVNum) + { + case (int)ItemVnums.EQ_NORMAL_SCROLL: + protection = RarifyProtection.Scroll; + break; + case (int)ItemVnums.BLESSING_AMULET: + protection = RarifyProtection.BlessingAmulet; + break; + case (int)ItemVnums.PROTECTION_AMULET: + protection = RarifyProtection.ProtectionAmulet; + break; + case (int)ItemVnums.BLESSING_AMULET_DOUBLE: + protection = RarifyProtection.BlessingAmulet; + break; + case (int)ItemVnums.CHAMPION_AMULET: + protection = RarifyProtection.HeroicAmulet; + break; + case (int)ItemVnums.CHAMPION_AMULET_RANDOM: + protection = RarifyProtection.RandomHeroicAmulet; + break; + + case (int)ItemVnums.AMULET_INCREASE_NORMAL: + mode = RarifyMode.Increase; + break; + case (int)ItemVnums.CHAMPION_AMULET_INCREASE_1: + case (int)ItemVnums.CHAMPION_AMULET_INCREASE_2: + if (item.GameItem.IsHeroic) + { + mode = RarifyMode.Increase; + } + + break; + } + + await sender.EmitEventAsync(new GamblingEvent(invItem, invAmulet, mode, protection)); + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Extensions/SkillExtension.cs b/srcs/WingsAPI.Game/Extensions/SkillExtension.cs new file mode 100644 index 0000000..3bcab99 --- /dev/null +++ b/srcs/WingsAPI.Game/Extensions/SkillExtension.cs @@ -0,0 +1,409 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Foundatio.Utility; +using PhoenixLib.Events; +using WingsEmu.DTOs.BCards; +using WingsEmu.DTOs.Buffs; +using WingsEmu.DTOs.Skills; +using WingsEmu.Game._enum; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Monster.Event; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Skills; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Battle; +using WingsEmu.Packets.ServerPackets.Battle; + +namespace WingsEmu.Game.Extensions; + +public static class SkillExtension +{ + public static SkillInfo GetInfo(this SkillDTO skill, PartnerSkill partnerSkill = null, IBattleEntity battleEntity = null) + { + var dictionary = new Dictionary>(); + foreach (BCardDTO bCard in skill.BCards) + { + var key = (SkillCastType)bCard.CastType; + if (!dictionary.TryGetValue(key, out HashSet hashSet)) + { + hashSet = new HashSet(); + dictionary[key] = hashSet; + } + + hashSet.Add(bCard); + } + + if (battleEntity != null && !battleEntity.IsPlayer()) + { + IReadOnlyList onAttackBCards = battleEntity.BCardComponent.GetTriggerBCards(BCardTriggerType.ATTACK); + foreach (BCardDTO bCard in onAttackBCards) + { + var key = (SkillCastType)bCard.CastType; + if (!dictionary.TryGetValue(key, out HashSet hashSet)) + { + hashSet = new HashSet(); + dictionary[key] = hashSet; + } + + hashSet.Add(bCard); + } + } + + return new SkillInfo + { + Vnum = skill.Id, + AttackType = skill.AttackType, + AoERange = skill.AoERange, + BCards = skill.BCards, + CastAnimation = skill.CtAnimation, + CastEffect = skill.CtEffect, + SkillType = skill.SkillType, + CastTime = skill.CastTime, + Cooldown = skill.Cooldown, + CastId = skill.CastId, + Element = skill.Element, + HitAnimation = skill.SuAnimation, + HitEffect = skill.SuEffect, + HitType = skill.HitType, + TargetType = skill.TargetType, + Combos = skill.Combos, + TargetAffectedEntities = skill.TargetAffectedEntities, + Range = skill.Range, + IsUsingSecondWeapon = skill.IsUsingSecondWeapon, + IsComboSkill = skill.SpecialCost == 999, + ManaCost = skill.MpCost, + PartnerSkillRank = partnerSkill?.Rank, + BCardsType = dictionary + }; + } + + public static SkillInfo GetUpgradedSkill(this IPlayerEntity playerEntity, SkillDTO originalSkill, ICardsManager cardsManager, ISkillsManager skillsManager) + { + if (!playerEntity.SkillComponent.SkillUpgrades.TryGetValue((short)originalSkill.Id, out HashSet hashSet)) + { + return null; + } + + if (!hashSet.Any()) + { + return null; + } + + hashSet = hashSet.OrderBy(x => x.Skill.UpgradeType).ToHashSet(); + + BCardDTO[] upgradesBCards = hashSet.SelectMany(x => x.Skill.BCards).ToArray(); + + SkillDTO getBasicSkill = skillsManager.GetSkill(originalSkill.Id).DeepClone(); + + var bCards = new List(getBasicSkill.BCards); + var dictionary = new Dictionary>(); + + foreach (BCardDTO bCard in bCards) + { + var key = (SkillCastType)bCard.CastType; + if (!dictionary.TryGetValue(key, out HashSet hashSetBCards)) + { + hashSetBCards = new HashSet(); + dictionary[key] = hashSetBCards; + } + + hashSetBCards.Add(bCard); + } + + foreach (BCardDTO upgradeBCard in upgradesBCards) + { + var key = (SkillCastType)upgradeBCard.CastType; + if (!dictionary.TryGetValue(key, out HashSet hashSetBCards)) + { + hashSetBCards = new HashSet(); + dictionary[key] = hashSetBCards; + } + + if (upgradeBCard.Type != (short)BCardType.Buff) + { + if (upgradeBCard.FirstDataScalingType == BCardScalingType.NORMAL_VALUE && upgradeBCard.SecondDataScalingType == BCardScalingType.NORMAL_VALUE) + { + BCardDTO findOriginalBCard = getBasicSkill.BCards.FirstOrDefault(x => x.Type == upgradeBCard.Type && x.SubType == upgradeBCard.SubType); + if (findOriginalBCard == null) + { + bCards.Add(upgradeBCard); + if (!hashSetBCards.Contains(upgradeBCard)) + { + hashSetBCards.Add(upgradeBCard); + } + + continue; + } + + findOriginalBCard.FirstData += upgradeBCard.FirstData; + findOriginalBCard.SecondData += upgradeBCard.SecondData; + hashSetBCards.Add(findOriginalBCard); + continue; + } + + BCardDTO originalBCard = getBasicSkill.BCards.FirstOrDefault(x => x.Type == upgradeBCard.Type && x.SubType == upgradeBCard.SubType); + if (originalBCard == null) + { + bCards.Add(upgradeBCard); + hashSetBCards.Add(upgradeBCard); + continue; + } + + hashSetBCards.Remove(originalBCard); + bCards.Remove(originalBCard); + bCards.Add(upgradeBCard); + if (!hashSetBCards.Contains(upgradeBCard)) + { + hashSetBCards.Add(upgradeBCard); + } + + continue; + } + + BCardDTO[] buffBCards = getBasicSkill.BCards.Where(x => x.Type == (short)BCardType.Buff && x.SubType == (byte)AdditionalTypes.Buff.ChanceCausing).ToArray(); + + bool buffsExist = false; + + if (buffBCards.Any()) + { + buffsExist = true; + foreach (BCardDTO buffBCard in buffBCards) + { + int first = buffBCard.SecondData; + int second = upgradeBCard.SecondData; + + Card firstBuff = cardsManager.GetCardByCardId(first); + Card secondBuff = cardsManager.GetCardByCardId(second); + + if (firstBuff == null || secondBuff == null) + { + bCards.Add(upgradeBCard); + hashSetBCards.Add(upgradeBCard); + continue; + } + + if (firstBuff.GroupId != secondBuff.GroupId) + { + bCards.Add(upgradeBCard); + hashSetBCards.Add(upgradeBCard); + continue; + } + + hashSetBCards.Remove(buffBCard); + bCards.Remove(buffBCard); + bCards.Add(upgradeBCard); + hashSetBCards.Add(upgradeBCard); + } + } + + if (buffsExist) + { + continue; + } + + bCards.Add(upgradeBCard); + hashSetBCards.Add(upgradeBCard); + } + + SkillDTO[] skills = hashSet.Select(x => x.Skill).ToArray(); + int manaCost = originalSkill.MpCost + skills.Sum(x => x.MpCost); + byte aoeRange = (byte)(originalSkill.AoERange + skills.Sum(x => x.AoERange)); + byte range = (byte)(originalSkill.Range + skills.Sum(x => x.Range)); + short castTime = (short)(originalSkill.CastTime + skills.Sum(x => x.CastTime)); + short cooldown = (short)(originalSkill.Cooldown + skills.Sum(x => x.Cooldown)); + SkillDTO element = skills.LastOrDefault(x => x.Element != 0); + SkillDTO ctAnimation = skills.LastOrDefault(x => x.CtAnimation != -1); + SkillDTO ctEffect = skills.LastOrDefault(x => x.CtEffect != -1); + SkillDTO suAnimation = skills.LastOrDefault(x => x.SuAnimation != 0); + SkillDTO suEffect = skills.LastOrDefault(x => x.SuEffect != -1); + + return new SkillInfo + { + Vnum = originalSkill.Id, + AttackType = originalSkill.AttackType, + AoERange = aoeRange, + BCards = bCards, + CastAnimation = ctAnimation?.CtAnimation ?? originalSkill.CtAnimation, + CastEffect = ctEffect?.CtEffect ?? originalSkill.CtEffect, + SkillType = originalSkill.SkillType, + CastTime = castTime, + Cooldown = cooldown, + CastId = originalSkill.CastId, + Element = element?.Element ?? originalSkill.Element, + HitAnimation = suAnimation?.SuAnimation ?? originalSkill.SuAnimation, + HitEffect = suEffect?.SuEffect ?? originalSkill.SuEffect, + HitType = originalSkill.HitType, + TargetType = originalSkill.TargetType, + Combos = originalSkill.Combos, + TargetAffectedEntities = originalSkill.TargetAffectedEntities, + Range = range, + IsUsingSecondWeapon = originalSkill.IsUsingSecondWeapon, + IsComboSkill = originalSkill.SpecialCost == 999, + ManaCost = manaCost, + BCardsType = dictionary + }; + } + + public static void CancelCastingSkill(this IBattleEntity entity) + { + entity.RemoveCastingSkill(); + if (!(entity is IPlayerEntity character)) + { + return; + } + + character.Session.SendCancelPacket(CancelType.NotInCombatMode); + } + + public static void SendSpecialistGuri(this IClientSession session, int castId) + { + if (session.PlayerEntity.Specialist == null) + { + return; + } + + #region Types + + switch (session.PlayerEntity.Specialist.GameItem.Morph) + { + case 1: // Pajama + int type = 0; + if (castId == 1) + { + type = 14; + } + + if (castId >= 2 && castId <= 4) + { + type = 18 + castId; + } + + if (castId == 5) + { + type = 26; + } + + if (castId >= 6 && castId <= 10) + { + type = 24 + castId; + } + + session.CurrentMapInstance.Broadcast(session.GenerateGuriPacket(6, 1, session.PlayerEntity.Id, type)); + break; + case 16: // Pirate + session.CurrentMapInstance.Broadcast(session.GenerateGuriPacket(6, 1, session.PlayerEntity.Id, 43)); + break; + } + + #endregion + } + + public static List GetSavedBuffs(this IPlayerEntity character) + { + var staticBuffList = new List(); + if (!character.BuffComponent.HasAnyBuff()) + { + return staticBuffList; + } + + IReadOnlyList savedBuffs = character.BuffComponent.GetAllBuffs(x => x.BuffFlags is BuffFlag.BIG_AND_KEEP_ON_LOGOUT or BuffFlag.SAVING_ON_DISCONNECT); + + if (!savedBuffs.Any()) + { + return staticBuffList; + } + + foreach (Buff buff in savedBuffs) + { + var newBuff = new CharacterStaticBuffDto + { + CardId = buff.CardId, + CharacterId = character.Id, + RemainingTime = buff.RemainingTimeInMilliseconds() + }; + + staticBuffList.Add(newBuff); + } + + return staticBuffList; + } + + + public static short GetCannoneerHitEffect(this IPlayerEntity character, int castId) + { + return castId switch + { + 4 => 4522, + 6 => 4561, + 7 => 4573, + 8 => -1, + 10 => 4572 + }; + } + + public static bool SkillCanBeUsed(this IBattleEntity entity, CharacterSkill skill) => skill.LastUse <= DateTime.UtcNow; + + public static bool IsPassiveSkill(this SkillDTO skill) => skill.SkillType == SkillType.Passive; + + public static bool SkillCanBeUsed(this IBattleEntity entity, PartnerSkill skill) => skill != null && skill.LastUse <= DateTime.UtcNow; + + public static bool SkillCanBeUsed(this IBattleEntity entity, IBattleEntitySkill skill, in DateTime date) => skill != null && skill.LastUse <= date && entity.Mp >= skill.Skill.MpCost; + + public static string GenerateOnyxGuriPacket(this IBattleEntity entity, short x, short y) => $"guri 31 {(byte)entity.Type} {entity.Id} {x} {y}"; + + public static SkillInfo GetFakeTeleportSkill(this IBattleEntity entity) => + new() + { + Cooldown = 600, + CastId = 8 + }; + + public static SkillInfo GetFakeBombSkill(this IBattleEntity entity) => + new() + { + Cooldown = 600, + CastId = 4 + }; + + public static async Task TryDespawnBomb(this IPlayerEntity playerEntity, IAsyncEventPipeline asyncEventPipeline) + { + if (!playerEntity.SkillComponent.BombEntityId.HasValue) + { + return; + } + + IMonsterEntity bomb = playerEntity.MapInstance?.GetMonsterById(playerEntity.SkillComponent.BombEntityId.Value); + if (bomb?.MapInstance == null) + { + return; + } + + await asyncEventPipeline.ProcessEventAsync(new MonsterDeathEvent(bomb)); + + SkillInfo fakeBombSkill = playerEntity.GetFakeBombSkill(); + playerEntity.BroadcastSuPacket(playerEntity, fakeBombSkill, 0, SuPacketHitMode.NoDamageSuccess); + playerEntity.Session.SendSkillCooldownResetAfter(fakeBombSkill.CastId, (short)playerEntity.ApplyCooldownReduction(fakeBombSkill)); + playerEntity.CancelCastingSkill(); + playerEntity.SetSkillCooldown(fakeBombSkill); + playerEntity.SkillComponent.BombEntityId = null; + } + + public static ElementType? GetBuffElementType(this IBattleEntity entity, short skillVnum) + { + return skillVnum switch + { + (short)SkillsVnums.FLAME => ElementType.Fire, + (short)SkillsVnums.ICE => ElementType.Water, + (short)SkillsVnums.HALO => ElementType.Light, + (short)SkillsVnums.DARKNESS => ElementType.Shadow, + (short)SkillsVnums.NO_ELEMENT => ElementType.Neutral, + _ => null + }; + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Extensions/SpecialistExtension.cs b/srcs/WingsAPI.Game/Extensions/SpecialistExtension.cs new file mode 100644 index 0000000..13eff39 --- /dev/null +++ b/srcs/WingsAPI.Game/Extensions/SpecialistExtension.cs @@ -0,0 +1,93 @@ +using System.Linq; +using WingsEmu.DTOs.Skills; +using WingsEmu.Game.Items; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Skills; + +namespace WingsEmu.Game.Extensions; + +public static class SpecialistExtension +{ + public static string GenerateSkillInfo(this GameItemInstance specialistInstance, byte type) + { + // type = 0 - in packet + // type = 1 - pski packet + // type = 2 - sc_n packet + + if (specialistInstance == null && type == 2) + { + return "-1 -1 -1"; + } + + if (specialistInstance?.PartnerSkills == null) + { + return type switch + { + 0 => "0 0 0 ", + 1 => string.Empty, + 2 => "0.0 0.0 0.0", + _ => string.Empty + }; + } + + string generatePacket = string.Empty; + var generateSkillsPacket = Enumerable.Repeat(null, 3).ToList(); + + for (int i = 0; i < 3; i++) + { + PartnerSkill skill = specialistInstance.PartnerSkills?.ElementAtOrDefault(i); + if (skill == null) + { + continue; + } + + generateSkillsPacket[skill.Slot] = skill; + } + + for (int i = 0; i < 3; i++) + { + PartnerSkill skill = generateSkillsPacket.ElementAtOrDefault(i); + if (skill == null) + { + generatePacket += type switch + { + 0 => "0 ", + 1 => string.Empty, + 2 => "0.0 ", + _ => string.Empty + }; + + continue; + } + + generatePacket += type switch + { + 0 => $"{skill.SkillId} ", + 1 => $"{skill.SkillId} ", + 2 => $"{skill.SkillId}.{skill.Rank} ", + _ => "0 0 0" + }; + } + + return generatePacket; + } + + public static bool IsSpSkill(this GameItemInstance spInstance, SkillDTO ski) => + ski.UpgradeType == spInstance.GameItem.Morph && ski.SkillType == SkillType.NormalPlayerSkill && spInstance.SpLevel >= ski.LevelMinimum; + + public static void SendPartnerSpecialistInfo(this IClientSession session, GameItemInstance item) => + session.SendPacket(item.GeneratePslInfo()); + + public static string GeneratePslInfo(this GameItemInstance item) => + "pslinfo " + + $"{item.ItemVNum} " + + $"{item.GameItem.Element} " + + $"{item.GameItem.ElementRate} " + + $"{item.GameItem.LevelMinimum} " + + $"{item.GameItem.Speed} " + + $"{item.GameItem.FireResistance} " + + $"{item.GameItem.WaterResistance} " + + $"{item.GameItem.LightResistance} " + + $"{item.GameItem.DarkResistance} " + + $"{item.GenerateSkillInfo(2)}"; +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Extensions/StringExtensions.cs b/srcs/WingsAPI.Game/Extensions/StringExtensions.cs new file mode 100644 index 0000000..26a44c3 --- /dev/null +++ b/srcs/WingsAPI.Game/Extensions/StringExtensions.cs @@ -0,0 +1,21 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Linq; + +namespace WingsEmu.Core.Extensions; + +public static class StringExtensions +{ + #region Methods + + public static string Truncate(this string str, int length) => str.Length > length ? str.Substring(0, length) : str; + + public static string ToUnderscoreCase(this string str) + { + return string.Concat(str.Select((x, i) => i > 0 && char.IsUpper(x) ? "_" + x : x.ToString())).ToLower(); + } + + #endregion +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Extensions/UiPacketExtension.cs b/srcs/WingsAPI.Game/Extensions/UiPacketExtension.cs new file mode 100644 index 0000000..4100b43 --- /dev/null +++ b/srcs/WingsAPI.Game/Extensions/UiPacketExtension.cs @@ -0,0 +1,532 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using WingsAPI.Data.Character; +using WingsAPI.Packets.Enums.Act4; +using WingsEmu.DTOs.Items; +using WingsEmu.DTOs.Relations; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Act4; +using WingsEmu.Game.Algorithm; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions.Mates; +using WingsEmu.Game.Groups; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Networking.Broadcasting; +using WingsEmu.Game.Shops; +using WingsEmu.Packets; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Battle; +using WingsEmu.Packets.Enums.Chat; +using WingsEmu.Packets.Enums.Relations; + +namespace WingsEmu.Game.Extensions; + +public static class UiPacketExtension +{ + #region Generate Packets + + // family + + public static string GeneratePetBasketPacket(this IClientSession session, bool isOn) => $"ib 1278 {(isOn ? 1 : 0)}"; + + public static string GenerateQna(this IClientSession session, string packet, string message) => $"qna #{packet.Replace(' ', '^')} {message}"; + + public static string GenerateMapClear(this IMapInstance mapInstance) => "mapclear"; + + public static string GenerateAct6EmptyPacket(this IClientSession session) => "act6"; + + public static string GenerateEmptyRcScalc(this IClientSession session) => "rc_scalc 0 -1 -1 -1 -1 -1 "; + + public static string GenerateRcScalc(this IClientSession session, string name, byte type, long price, int amount, int bzAmount, long taxes, long priceTaxes) + => $"rc_scalc {type} {price} {amount} {bzAmount} {taxes} {priceTaxes} {name ?? ""}"; + + public static string GenerateBlinit(this IClientSession session) + { + string result = "blinit"; + + foreach (CharacterRelationDTO relation in session.PlayerEntity.GetBlockedRelations()) + { + result += $" {relation.RelatedCharacterId}|{relation.RelatedName}"; + } + + return result; + } + + public static string GenerateFinit(this IClientSession session, ISessionManager sessionManager) + { + string result = "finit"; + + foreach (CharacterRelationDTO relation in session.PlayerEntity.GetRelations().Where(x => x.RelationType != CharacterRelationType.Blocked)) + { + bool isOnline = sessionManager.IsOnline(relation.RelatedCharacterId); + result += $" {relation.RelatedCharacterId}|{(short)relation.RelationType}|{(isOnline ? 1 : 0)}|{relation.RelatedName}"; + } + + return result; + } + + public static string GenerateDir(this IBattleEntity entity) => $"dir {(byte)entity.Type} {entity.Id} {entity.Direction}"; + public static string GenerateDamage(this IBattleEntity entity, int damage) => $"dm {(byte)entity.Type} {entity.Id} {damage}"; + public static string GenerateHeal(this IBattleEntity entity, int heal) => $"rc {(byte)entity.Type} {entity.Id} {heal} 0"; + public static string GenerateSmemo(this IClientSession session, SmemoType type, string message) => $"s_memo {(byte)type} {message}"; + + public static string GenerateGb(this IClientSession session, BankType type, IReputationConfiguration reputationConfiguration, IBankReputationConfiguration bankReputationConfiguration, + IReadOnlyList topReputation) => + $"gb {(byte)type} {session.Account.BankMoney / 1000} {session.PlayerEntity.Gold} {(byte)session.PlayerEntity.GetBankRank(reputationConfiguration, bankReputationConfiguration, topReputation)} {session.PlayerEntity.GetBankPenalty(reputationConfiguration, bankReputationConfiguration, topReputation)}"; + + public static string GenerateRcPacket(this IBattleEntity entity, int health) => $"rc {(byte)entity.Type} {entity.Id} {health} 0"; + public static string GenerateSpectatorWindow(this IClientSession session) => "taw_open"; + public static string GenerateMovement(this IBattleEntity entity) => $"mv {(byte)entity.Type} {entity.Id} {entity.PositionX} {entity.PositionY} {entity.Speed}"; + public static string GenerateEffectObject(this IBattleEntity entity, bool first, EffectType effect) => $"eff_ob {(byte)entity.Type} {entity.Id} {(first ? 1 : 0)} {(int)effect}"; + + public static string GenerateEffectGround(this IBattleEntity entity, EffectType effectType, short x, short y, bool remove) + => $"eff_g {(short)effectType} {entity.Id} {x} {y} {(remove ? 1 : 0)}"; + + public static string GenerateEffectTarget(this IBattleEntity entity, IBattleEntity target, EffectType effectType) + => $"eff_t {(byte)entity.Type} {entity.Id} {(byte)target.Type} {target.Id} {(short)effectType}"; + + public static string GenerateSayPacket(this IClientSession session, string msg, ChatMessageColorType color) => + $"say {(byte)session.PlayerEntity.Type} {session.PlayerEntity.Id} {(byte)color} {msg}"; + + public static string GenerateSayNoIdPacket(string msg, ChatMessageColorType color) => + $"say 1 -1 {((byte)color).ToString()} {msg}"; + + public static string GenerateCancelPacket(this IClientSession session, CancelType cancelType, int id) => $"cancel {(byte)cancelType} {id} 1"; + + public static string GenerateInfoPacket(this IClientSession session, string message) => $"info {message}"; + + public static string GenerateMsgPacket(this IClientSession session, string message, MsgMessageType type) => $"msg {(byte)type} {message}"; + + public static string GenerateSpkPacket(this IClientSession session, string message, SpeakType type) => $"spk 1 {session.PlayerEntity.Id} {(byte)type} {session.PlayerEntity.Name} {message}"; + + public static string GenerateSpkPacket(long senderId, string senderName, string message, SpeakType type) => $"spk 1 {senderId.ToString()} {(byte)type} {senderName} {message}"; + + public static string GenerateGuriPacket(this IClientSession session, byte type, short argument = 0, long value = 0, int secondValue = 0) + { + switch (type) + { + case 2: + return $"guri 2 {argument} {session.PlayerEntity.Id}"; + + case 4: + return $"guri 4 {session.PlayerEntity.AdditionalHp} {session.PlayerEntity.AdditionalMp}"; + + case 6: + return $"guri 6 {argument} {value} {secondValue} 0"; + + case 10: + return $"guri 10 {argument} {value} {session.PlayerEntity.Id}"; + + case 12: + return $"guri 12 1 {session.PlayerEntity.Id} {value}"; + + case 15: + return $"guri 15 {argument} 0 0"; + + case (int)GuriType.ShellEffect: + return $"guri {type} 0 0 {argument}"; + + case 19: + return $"guri 19 0 0 {value}"; + + case 25: + return "guri 25"; + + default: + return $"guri {type} {argument} {value} {session.PlayerEntity.Id}"; + } + } + + public static string GenerateRestPacket(this IClientSession session) => $"rest 1 {session.PlayerEntity.Id} {(session.PlayerEntity.IsSitting ? 1 : 0)}"; + + public static string GenerateFcPacket(FactionType faction, Act4Status act4Status) => + $"fc {((byte)faction).ToString()} {((int)act4Status.TimeBeforeReset.TotalMinutes).ToString()} {GenerateSubFcPacket(FactionType.Angel, act4Status)} {GenerateSubFcPacket(FactionType.Demon, act4Status)}"; + + public static string GenerateGuriFactionOverridePacket(this IClientSession session) => + $"guri 5 1 {session.PlayerEntity.Id} {(session.PlayerEntity.Faction == FactionType.Angel ? 3 : 4).ToString()}"; + + public static string GenerateEndDancingGuriPacket(this IPlayerEntity playerEntity) => $"guri 6 1 {playerEntity.Id} 0 0"; + + private static string GenerateSubFcPacket(FactionType faction, Act4Status act4Status) + { + if (faction == act4Status.RelevantFaction) + { + return $"{(faction == FactionType.Angel ? act4Status.AngelPointsPercentage : act4Status.DemonPointsPercentage).ToString()} " + //percentage + $"{((byte)act4Status.FactionStateType).ToString()} " + //mode + $"{((int)act4Status.CurrentTimeBeforeMukrajuDespawn.TotalSeconds).ToString()} " + //currentTime + $"{((int)act4Status.TimeBeforeMukrajuDespawn.TotalSeconds).ToString()} " + //totalTime + "0 " + //$"{(act4Status.DungeonType == DungeonType.Morcos ? 1 : 0).ToString()} " + //morcos + "0 " + //$"{(act4Status.DungeonType == DungeonType.Hatus ? 1 : 0).ToString()} " + //hatus + "0 " + //$"{(act4Status.DungeonType == DungeonType.Calvinas ? 1 : 0).ToString()} " + //calvina + "0 " + //$"{(act4Status.DungeonType == DungeonType.Berios ? 1 : 0).ToString()} " + //berios + "0"; //no idea + } + + return $"{(faction == FactionType.Angel ? act4Status.AngelPointsPercentage : act4Status.DemonPointsPercentage).ToString()} 0 0 0 0 0 0 0 0"; + } + + public static string GenerateDungeonPacket(this IClientSession session, DungeonInstance dungeonInstance, DungeonSubInstance dungeonSubInstance, IAct4DungeonManager act4DungeonManager, + DateTime currentTime) + { + DungeonEventType dungeonEventType = AssertDungeonEventType(dungeonInstance, dungeonSubInstance); + int secondsBeforeEnd = (int)(act4DungeonManager.DungeonEnd - currentTime).TotalSeconds; + return $"dg {(byte)dungeonInstance.DungeonType} {(byte)dungeonEventType} {secondsBeforeEnd.ToString()} 0"; + } + + private static DungeonEventType AssertDungeonEventType(DungeonInstance dungeonInstance, DungeonSubInstance dungeonSubInstance) + { + //quick win + if (dungeonInstance.FinishSlowMoDate != null) + { + return DungeonEventType.BossRoomFinished; + } + + if (dungeonSubInstance.Bosses.Count > 0) + { + return DungeonEventType.InBossRoom; + } + + if (dungeonInstance.SpawnInstance.PortalGenerators.Count < 1) + { + return DungeonEventType.BossRoomOpen; + } + + return DungeonEventType.BossRoomClosed; + } + + public static string GenerateAct6Packet(this IClientSession session) => + "act6 " + + "1 " + + "0 " + + "0 " + + "0 " + + "0 " + + "0 " + + "0 " + + "0 " + + "0 " + + "0"; + + public static string GenerateDlgPacket(this IClientSession session, string yesPacket, string noPacket, string message) => + $"dlg #{yesPacket.Replace(' ', '^')} #{noPacket.Replace(' ', '^')} {message}"; + + public static string GenerateRpPacket(this IClientSession session, int mapId, int x, int y, string param) => $"rp {mapId} {x} {y} {param}"; + + public static string GenerateSpPointPacket(this IClientSession session) => + $"sp {session.PlayerEntity.SpPointsBonus} {StaticServerManager.Instance.MaxAdditionalSpPoints} {session.PlayerEntity.SpPointsBasic} {StaticServerManager.Instance.MaxBasicSpPoints}"; + + public static string GenerateEsfPacket(this IClientSession session, byte type) => $"esf {type}"; + + public static string GenerateDeletePost(this IClientSession session, byte type, int id) => $"post {type} {id}"; + + public static string GenerateNpcDialogSession(this IClientSession session, int value) => GenerateNpcDialog(session.PlayerEntity.Id, value); + + public static string GenerateNpcDialog(long characterId, int value) => $"npc_req 1 {characterId.ToString()} {value}"; + + public static string GenerateItemSpeaker(this IClientSession session, GameItemInstance item, string message, IItemsManager itemsManager, ICharacterAlgorithm algorithm) + { + string itemInfo = item.Type switch + { + ItemInstanceType.BoxInstance => $"{item.GenerateEInfo(itemsManager, algorithm)}", + ItemInstanceType.SpecialistInstance => $"{(item.GameItem.IsPartnerSpecialist ? item.GeneratePslInfo() : session.GenerateSlInfo(item, algorithm))}", + ItemInstanceType.WearableInstance => $"{item.GenerateEInfo(itemsManager, algorithm)}", + _ => $"IconInfo {item.ItemVNum}" + }; + + return $"sayitemt 1 {session.PlayerEntity.Id} 17 1 {item.ItemVNum} {session.PlayerEntity.Name} {message} {itemInfo}"; + } + + public static string GenerateInboxPacket(this IClientSession session, string message) => $"inbox {message}"; + + public static string GenerateMsCPacket(this IClientSession session, byte type) => $"ms_c {type}"; + + public static string GenerateMSlotPacket(this IClientSession session, byte slot) => $"mslot {slot} -1"; + + public static string GenerateScpPacket(this IClientSession session, byte type) => $"scp {type}"; + + public static string GenerateObArPacket(this IClientSession session) => "ob_ar"; + + public static string GenerateClockPacket(this IClientSession session, ClockType type, sbyte subType, TimeSpan time1, TimeSpan time2) => + $"evnt {(byte)type} {subType} {(int)time1.TotalMilliseconds / 100} {(int)time2.TotalMilliseconds / 100}"; + + public static string GenerateTsClockPacket(this IClientSession session, TimeSpan time1, bool isVisible) => + $"evnt {(byte)ClockType.TimeSpaceClock} {(isVisible ? 0 : -1)} {(int)time1.TotalMilliseconds / 100} 1"; + + public static string GenerateRemoveClockPacket(this IClientSession session) => "evnt 10 0 -1 -1"; + + public static string GenerateRemoveRedClock(this IClientSession session) => "evnt 3 1 -1 -1"; + + public static string GenerateInvisible(this IClientSession session) => + $"cl {session.PlayerEntity.Id} {(session.PlayerEntity.Invisible || session.PlayerEntity.CheatComponent.IsInvisible ? 1 : 0)} {(session.PlayerEntity.CheatComponent.IsInvisible ? 1 : 0)}"; + + public static string GenerateOppositeMove(this IClientSession session, bool enabled) => $"rv_m {session.PlayerEntity.Id} 1 {(enabled ? 1 : 0)}"; + + public static string GenerateBubble(this IClientSession session, string message) => $"csp {session.PlayerEntity.Id} {message.Replace(' ', (char)0xB)}"; + + public static string GenerateIncreaseRange(this IClientSession session, short range, bool enabled) => $"bf_d {range} {(enabled ? 1 : 0)}"; + + public static string GenerateGenderPacket(this IClientSession session) => $"p_sex {(byte)session.PlayerEntity.Gender}"; + + //pflag packet's argument doesn't seem useful as it only makes the client do "npc_req", without this argument that theoretically represents the dialog the server should return + public static string GeneratePlayerFlag(this IClientSession session, long flag) => $"pflag 1 {session.PlayerEntity.Id} {flag.ToString()}"; + + public static string GenerateShopPacket(this IClientSession session) + { + IEnumerable items = session.PlayerEntity.ShopComponent.Items; + return + $"shop {(byte)session.PlayerEntity.Type} {session.PlayerEntity.Id} {(items == null ? 0 : 1)} {(items == null ? 0 : 3)} {(items == null ? string.Empty : 0.ToString())} {(items == null ? string.Empty : session.PlayerEntity.ShopComponent.Name)}"; + } + + public static string GenerateGbexPacket(this IClientSession session, IReputationConfiguration reputationConfiguration, IBankReputationConfiguration bankReputationConfiguration, + IReadOnlyList topReputation) => + $"gbex {session.Account.BankMoney / 1000} {session.PlayerEntity.Gold} {(byte)session.PlayerEntity.GetBankRank(reputationConfiguration, bankReputationConfiguration, topReputation)} {session.PlayerEntity.GetBankPenalty(reputationConfiguration, bankReputationConfiguration, topReputation)}"; + + private static string GenerateScene(this IClientSession session, byte type, bool skip) => $"scene {type} {(skip ? 1 : 0)}"; + + public static string GenerateDragonPacket(this IBattleEntity entity, byte amountOfDragons) => $"eff_d 2 {amountOfDragons} "; + public static string GenerateEmptyHatusHeads(this IClientSession session) => "bc 0 0 0"; + + public static string GenerateArenaStatistics(this IClientSession session, bool leavingArena, PlayerGroup playerGroup) + { + CharacterLifetimeStatsDto lifetimeStats = session.PlayerEntity.LifetimeStats; + + var stringBuilder = new StringBuilder($"ascr {lifetimeStats.TotalArenaKills} {lifetimeStats.TotalArenaDeaths} 0 {session.PlayerEntity.ArenaKills} {session.PlayerEntity.ArenaDeaths} 0"); + + if (playerGroup == null) + { + stringBuilder.Append($" 0 0 {(leavingArena ? -1 : 0)}"); + return stringBuilder.ToString(); + } + + stringBuilder.Append($" {playerGroup.ArenaKills} {playerGroup.ArenaDeaths} {(leavingArena ? -1 : 1)}"); + return stringBuilder.ToString(); + } + + #endregion + + #region Send Packets + + /// + /// Qna packet is supposed to trigger a dialog box on the client side, which, once confirmed, will make the client send + /// the packet given in parameter + /// + /// + /// Packet you want the client to send when he will confirm the dialog box + /// + public static void SendQnaPacket(this IClientSession session, string packet, string message) => session.SendPacket(session.GenerateQna(packet, message)); + + public static void SendPlayerShopTitle(this IClientSession packetReceiver, IClientSession shopOwner) => packetReceiver.SendPacket(shopOwner.GenerateShopPacket()); + public static void SendPlayerFlag(this IClientSession receiverSession, IClientSession targetSession, long flag) => receiverSession.SendPacket(targetSession.GeneratePlayerFlag(flag)); + public static void SendInboxPacket(this IClientSession session, string message) => session.SendPacket(session.GenerateInboxPacket(message)); + + public static void SendGuriPacket(this IClientSession session, byte type, short argument = 0, long value = 0, int secondValue = 0) => + session.SendPacket(session.GenerateGuriPacket(type, argument, value, secondValue)); + + public static void SendEsfPacket(this IClientSession session, byte type) => session.SendPacket(session.GenerateEsfPacket(type)); + public static void RefreshSpPoint(this IClientSession session) => session.SendPacket(session.GenerateSpPointPacket()); + public static void SendRpPacket(this IClientSession session, int mapId, int x, int y, string param) => session.SendPacket(session.GenerateRpPacket(mapId, x, y, param)); + public static void SendEsfPacket(this IClientSession session) => session.SendPacket("esf 4"); + public static void SendDialog(this IClientSession session, string yesPacket, string noPacket, string dialog) => session.SendPacket(session.GenerateDlgPacket(yesPacket, noPacket, dialog)); + public static void SendSpeak(this IClientSession session, string message, SpeakType type) => session.SendPacket(session.GenerateSpkPacket(message, type)); + public static void SendSpeakToTarget(this IClientSession session, IClientSession target, string message, SpeakType type) => target.SendPacket(session.GenerateSpkPacket(message, type)); + + public static void ReceiveSpeakWhisper(this IClientSession receiver, long senderId, string senderName, string message, SpeakType type) => + receiver.SendPacket(GenerateSpkPacket(senderId, senderName, message, type)); + + public static void BroadcastRest(this IClientSession session) => session.Broadcast(session.GenerateRestPacket()); + public static void BroadcastRevive(this IClientSession session) => session.Broadcast(session.PlayerEntity.GenerateRevive()); + + public static void BroadcastGuri(this IClientSession session, byte type, byte argument, long value = 0, params IBroadcastRule[] rules) => + session.Broadcast(session.GenerateGuriPacket(type, argument, value), rules); + + public static void BroadcastIn(this IClientSession session, IReputationConfiguration reputationConfiguration, IReadOnlyList topReputation, params IBroadcastRule[] rules) => + session.Broadcast(session.GenerateInPacket(reputationConfiguration, topReputation), rules); + + public static void BroadcastOut(this IClientSession session, params IBroadcastRule[] rules) => session.Broadcast(session.GenerateOutPacket(), rules); + public static void BroadcastMateOut(this IMateEntity mateEntity) => mateEntity.MapInstance?.Broadcast(mateEntity.GenerateOut()); + + public static void BroadcastMateTeleport(this IClientSession session, IMateEntity mateEntity, params IBroadcastRule[] rules) => + session.Broadcast(mateEntity.GenerateTeleportPacket(mateEntity.PositionX, mateEntity.PositionY), rules); + + /// + /// By default it will send a TeleportPacket to where the character is, you can also define the coords manually. + /// + /// + /// + /// + /// + public static void BroadcastTeleportPacket(this IClientSession session, short? x = null, short? y = null, params IBroadcastRule[] rules) + { + short teleportX = session.PlayerEntity.PositionX; + short teleportY = session.PlayerEntity.PositionY; + if (x != null) + { + teleportX = (short)x; + } + + if (y != null) + { + teleportY = (short)y; + } + + session.Broadcast(session.PlayerEntity.GenerateTeleportPacket(teleportX, teleportY), rules); + } + + public static void BroadcastSpeak(this IClientSession session, string message, SpeakType type, params IBroadcastRule[] rules) => + session.PlayerEntity.MapInstance.Broadcast(session.GenerateSpkPacket(message, type), rules); + + public static void BroadcastTitleInfo(this IClientSession session) => session.CurrentMapInstance.Broadcast(session.GenerateTitInfoPacket()); + public static void BroadcastEffect(this IClientSession session, EffectType effectType, params IBroadcastRule[] rules) => session.Broadcast(session.GenerateEffectPacket(effectType), rules); + + public static void BroadcastEffectInRange(this IClientSession session, EffectType effectType) => + session.Broadcast(session.GenerateEffectPacket(effectType), new RangeBroadcast(session.PlayerEntity.PositionX, session.PlayerEntity.PositionY)); + + public static void BroadcastEffectInRange(this IClientSession session, int effectId) => + session.Broadcast(session.GenerateEffectPacket(effectId), new RangeBroadcast(session.PlayerEntity.PositionX, session.PlayerEntity.PositionY)); + + public static void BroadcastEffect(this IClientSession session, int effectId, params IBroadcastRule[] rules) => session.Broadcast(session.GenerateEffectPacket(effectId), rules); + public static void BroadcastCMode(this IClientSession session, params IBroadcastRule[] rules) => session.Broadcast(session.GenerateCModePacket(), rules); + public static void BroadcastEq(this IClientSession session, params IBroadcastRule[] rules) => session.Broadcast(session.GenerateEqPacket(), rules); + public static void BroadcastPairy(this IClientSession session, params IBroadcastRule[] rules) => session.Broadcast(session.GeneratePairyPacket(), rules); + + public static void BroadcastTargetConstBuffEffects(this IClientSession session, IMateEntity mateEntity, params IBroadcastRule[] rules) + => session.CurrentMapInstance?.Broadcast(mateEntity.GenerateConstBuffEffects(), rules); + + public static void SendTargetInPacket(this IClientSession session, IClientSession target, IReputationConfiguration reputationConfiguration, IReadOnlyList topReputation, + bool foe = false, bool showInEffect = false) + => session.SendPacket(target.GenerateInPacket(reputationConfiguration, topReputation, foe, showInEffect)); + + public static void BroadcastMovement(this IClientSession session, IBattleEntity entity, params IBroadcastRule[] rules) => session.Broadcast(GenerateMovement(entity), rules); + public static void Broadcast(this IClientSession session, string packet, params IBroadcastRule[] rules) => session.CurrentMapInstance?.Broadcast(packet, rules); + public static void Broadcast(this IClientSession session, T packet, params IBroadcastRule[] rules) where T : IServerPacket => session.CurrentMapInstance?.Broadcast(packet, rules); + public static void SendChatMessage(this IClientSession session, string msg, ChatMessageColorType color) => session.SendPacket(session.GenerateSayPacket(msg, color)); + + public static void SendChatMessageNoPlayer(this IClientSession session, string msg, ChatMessageColorType color) => + session.SendPacket($"say {(byte)session.PlayerEntity.Type} 0 {(byte)color} {msg}"); + + public static void SendChatMessageNoId(this IClientSession session, string msg, ChatMessageColorType color) => session.SendPacket(GenerateSayNoIdPacket(msg, color)); + public static void SendInformationChatMessage(this IClientSession session, string msg) => session.SendChatMessage(msg, ChatMessageColorType.Yellow); + public static void SendSuccessChatMessage(this IClientSession session, string msg) => session.SendChatMessage(msg, ChatMessageColorType.Green); + public static void SendErrorChatMessage(this IClientSession session, string msg) => session.SendChatMessage(msg, ChatMessageColorType.Red); + + public static void SendSpCooldownUi(this IClientSession session, int seconds) => session.SendPacket(session.GenerateSpCooldownPacket(seconds)); + public static void ResetSpCooldownUi(this IClientSession session) => session.SendPacket(session.GenerateSpCooldownPacket(0)); + public static string GenerateSpCooldownPacket(this IClientSession session, int seconds) => $"sd {seconds}"; + + public static void SendDebugMessage(this IClientSession session, string msg, ChatMessageColorType color = ChatMessageColorType.Yellow) + { + if (!session.DebugMode) + { + return; + } + + session.SendChatMessage($"[DEBUG] {msg}", color); + } + + public static void SendCancelPacket(this IClientSession session, CancelType cancelType, int id = 0) + { + session.SendPacket(session.GenerateCancelPacket(cancelType, id)); + session.SendDebugMessage("Battle cancel"); + } + + public static void SendGuriFactionOverridePacket(this IClientSession session) => session.SendPacket(session.GenerateGuriFactionOverridePacket()); + + public static void SendDungeonPacket(this IClientSession session, DungeonInstance dungeonInstance, DungeonSubInstance dungeonSubInstance, IAct4DungeonManager act4DungeonManager, + DateTime currentTime) + => session.SendPacket(session.GenerateDungeonPacket(dungeonInstance, dungeonSubInstance, act4DungeonManager, currentTime)); + + public static void SendInfo(this IClientSession session, string msg) => session.SendPacket(session.GenerateInfoPacket(msg)); + public static void SendInfo(this IClientSession session, GameDialogKey msg) => session.SendPacket(session.GenerateInfoPacket(session.GetLanguage(msg))); + public static void SendInfo(this IClientSession session, GameDialogKey msg, params object[] formatParams) => session.SendPacket(session.GenerateInfoPacket(session.GetLanguageFormat(msg))); + public static void SendMsg(this IClientSession session, string msg, MsgMessageType type) => session.SendPacket(session.GenerateMsgPacket(msg, type)); + public static void SendMsg(this IClientSession session, GameDialogKey msg, MsgMessageType type) => session.SendPacket(session.GenerateMsgPacket(session.GetLanguage(msg), type)); + + public static void BroadcastHeal(this IBattleEntity entity, int heal) => entity.MapInstance.Broadcast(entity.GenerateRcPacket(heal)); + public static void BroadcastDamage(this IBattleEntity entity, int damage) => entity.MapInstance.Broadcast(entity.GenerateDamage(damage)); + public static void SendPost(this IClientSession session, byte type, int id) => session.SendPacket(session.GenerateDeletePost(type, id)); + public static void SendSMemo(this IClientSession session, SmemoType type, string message) => session.SendPacket(session.GenerateSmemo(type, message)); + + public static void SendRcScalcPacket(this IClientSession session, byte type, long price, int amount, int bzAmount, long taxes, long priceTaxes, string name) + => session.SendPacket(session.GenerateRcScalc(name, type, price, amount, bzAmount, taxes, priceTaxes)); + + public static void SendEmptyRcScalcPacket(this IClientSession session) => session.SendPacket(session.GenerateEmptyRcScalc()); + public static void SendNpcDialog(this IClientSession session, int value) => session.SendPacket(session.GenerateNpcDialogSession(value)); + public static void SendTargetNpcDialog(this IClientSession session, long targetCharacterId, int value) => session.SendPacket(GenerateNpcDialog(targetCharacterId, value)); + public static void SendSpectatorWindow(this IClientSession session) => session.SendPacket(session.GenerateSpectatorWindow()); + public static void SendPslInfoPacket(this IClientSession session, GameItemInstance item) => session.SendPacket(item.GeneratePslInfo()); + public static void SendMsCPacket(this IClientSession session, byte type) => session.SendPacket(session.GenerateMsCPacket(type)); + public static void SendMSlotPacket(this IClientSession session, byte slot) => session.SendPacket(session.GenerateMSlotPacket(slot)); + public static void SendScpPacket(this IClientSession session, byte type) => session.SendPacket(session.GenerateScpPacket(type)); + public static void SendObArPacket(this IClientSession session) => session.SendPacket(session.GenerateObArPacket()); + public static void SendEffectEntity(this IClientSession session, IBattleEntity battleEntity, EffectType effectId) => session.SendPacket(battleEntity.GenerateEffectPacket(effectId)); + + public static void SendClockPacket(this IClientSession session, ClockType type, sbyte subType, TimeSpan time1, TimeSpan time2) => + session.SendPacket(session.GenerateClockPacket(type, subType, time1, time2)); + + public static void SendTsClockPacket(this IClientSession session, TimeSpan time, bool isVisible) => session.SendPacket(session.GenerateTsClockPacket(time, isVisible)); + public static void SendRemoveClockPacket(this IClientSession session) => session.SendPacket(session.GenerateRemoveClockPacket()); + public static void SendRemoveRedClockPacket(this IClientSession session) => session.SendPacket(session.GenerateRemoveRedClock()); + + public static void SendEffectObject(this IClientSession session, IBattleEntity entity, bool first, EffectType effect) => session.SendPacket(entity.GenerateEffectObject(first, effect)); + + public static void RefreshFriendList(this IClientSession session, ISessionManager sessionManager) => + session.SendPacket(session.GenerateFinit(sessionManager)); + + public static void RefreshBlackList(this IClientSession session) => + session.SendPacket(session.GenerateBlinit()); + + public static void SendOppositeMove(this IClientSession session, bool enabled) => session.SendPacket(session.GenerateOppositeMove(enabled)); + public static void BroadcastBubbleMessage(this IClientSession session, string message) => session.Broadcast(session.GenerateBubble(message)); + + public static void SendIncreaseRange(this IClientSession session) + { + int range = session.PlayerEntity.BCardComponent.GetAllBCardsInformation(BCardType.FearSkill, + (byte)AdditionalTypes.FearSkill.AttackRangedIncreased, session.PlayerEntity.Level).firstData; + + session.SendPacket(session.GenerateIncreaseRange((short)range, range > 0)); + + if (session.PlayerEntity.UseSp && session.PlayerEntity.Specialist != null) + { + } + } + + public static void BroadcastEffectGround(this IBattleEntity entity, EffectType effectType, short x, short y, bool remove) => + entity.MapInstance.Broadcast(entity.GenerateEffectGround(effectType, x, y, remove)); + + public static void SendGenderPacket(this IClientSession session) => session.SendPacket(session.GenerateGenderPacket()); + public static void BroadcastPlayerShopFlag(this IClientSession session, long flag) => session.Broadcast(session.GeneratePlayerFlag(flag), new ExceptSessionBroadcast(session)); + public static void BroadcastShop(this IClientSession session) => session.Broadcast(session.GenerateShopPacket()); + + public static void BroadcastEffectTarget(this IBattleEntity entity, IBattleEntity target, EffectType effectType) + => entity.MapInstance.Broadcast(entity.GenerateEffectTarget(target, effectType)); + + public static void SendGbexPacket(this IClientSession session, IReputationConfiguration reputationConfiguration, IBankReputationConfiguration bankReputationConfiguration, + IReadOnlyList topReputation) + => session.SendPacket(session.GenerateGbexPacket(reputationConfiguration, bankReputationConfiguration, topReputation)); + + public static void SendScene(this IClientSession session, byte type, bool skip) => session.SendPacket(session.GenerateScene(type, skip)); + + public static void SendEmptyHatusHeads(this IClientSession session) => session.SendPacket(session.GenerateEmptyHatusHeads()); + + public static void SendPetBasketPacket(this IClientSession session, bool isOn) => session.SendPacket(session.GeneratePetBasketPacket(isOn)); + + public static void BroadcastEndDancingGuriPacket(this IPlayerEntity playerEntity) => playerEntity.MapInstance.Broadcast(playerEntity.GenerateEndDancingGuriPacket()); + + public static void SendMapClear(this IClientSession session) => session.SendPacket(session.CurrentMapInstance.GenerateMapClear()); + + public static void SendArenaStatistics(this IClientSession session, bool leavingArena, PlayerGroup playerGroup = null) => + session.SendPacket(session.GenerateArenaStatistics(leavingArena, playerGroup)); + + #endregion +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Extensions/WearableInstanceExtensions.cs b/srcs/WingsAPI.Game/Extensions/WearableInstanceExtensions.cs new file mode 100644 index 0000000..3046d08 --- /dev/null +++ b/srcs/WingsAPI.Game/Extensions/WearableInstanceExtensions.cs @@ -0,0 +1,731 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Generic; +using System.Linq; +using WingsAPI.Packets.Enums.Shells; +using WingsEmu.DTOs.Items; +using WingsEmu.DTOs.Skills; +using WingsEmu.Game._enum; +using WingsEmu.Game.Algorithm; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Skills; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Extensions; + +public static class ItemInstanceExtensions +{ + public static string GenerateInventoryAdd(this InventoryItem inventoryItem) + { + GameItemInstance item = inventoryItem.ItemInstance; + switch (inventoryItem.InventoryType) + { + case InventoryType.Equipment: + + switch (item.GameItem.EquipmentSlot) + { + case EquipmentType.Sp: + return $"ivn 0 {inventoryItem.Slot}.{item.ItemVNum}.{item.Rarity}.{item.Upgrade}.{item.SpStoneUpgrade}.0"; + case EquipmentType.Armor: + case EquipmentType.MainWeapon: + case EquipmentType.SecondaryWeapon: + return $"ivn 0 {inventoryItem.Slot}.{item.ItemVNum}.{item.Rarity}.{(item.GameItem.IsColorable ? item.Design : item.Upgrade)}.0.{item.GetRunesCount()}"; + default: + if (item.GameItem.ItemSubType == 7) + { + return $"ivn 0 {inventoryItem.Slot}.{item.ItemVNum}.{item.Rarity}.{(item.GameItem.IsColorable ? item.Design : item.Upgrade)}.{(item.IsBound ? 1 : 0)}.0"; + } + + return $"ivn 0 {inventoryItem.Slot}.{item.ItemVNum}.{item.Rarity}.{(item.GameItem.IsColorable ? item.Design : item.Upgrade)}.0.0"; + } + + case InventoryType.Main: + return $"ivn 1 {inventoryItem.Slot}.{item.ItemVNum}.{item.Amount}.0"; + + case InventoryType.Etc: + return $"ivn 2 {inventoryItem.Slot}.{item.ItemVNum}.{item.Amount}.0"; + + case InventoryType.Miniland: + return $"ivn 3 {inventoryItem.Slot}.{item.ItemVNum}.{item.Amount}"; + + case InventoryType.Specialist: + return $"ivn 6 {inventoryItem.Slot}.{item.ItemVNum}.{item.Rarity}.{item.Upgrade}.{item.SpStoneUpgrade}"; + + case InventoryType.Costume: + return $"ivn 7 {inventoryItem.Slot}.{item.ItemVNum}.{item.Rarity}.{item.Upgrade}.0"; + } + + return string.Empty; + } + + public static string GenerateSortItems(this IClientSession session, InventoryType inventoryType) + { + string specialist = "ivn 6"; + string costume = "ivn 7"; + string specialistList = string.Empty; + string costumeList = string.Empty; + IOrderedEnumerable items = session.PlayerEntity.GetItemsByInventoryType(inventoryType).OrderBy(x => x?.ItemInstance.ItemVNum); + switch (inventoryType) + { + case InventoryType.Specialist: + foreach (InventoryItem item in items) + { + if (item == null) + { + continue; + } + + specialistList += $" {item.Slot}.{item.ItemInstance.ItemVNum}.{item.ItemInstance.Rarity}.{item.ItemInstance.Upgrade}.{item.ItemInstance.SpStoneUpgrade}"; + } + + return specialist + specialistList; + case InventoryType.Costume: + foreach (InventoryItem item in items) + { + if (item == null) + { + continue; + } + + costumeList += $" {item.Slot}.{item.ItemInstance.ItemVNum}.{item.ItemInstance.Rarity}.{item.ItemInstance.Upgrade}.0"; + } + + return costume + costumeList; + } + + return string.Empty; + } + + public static void SendSortedItems(this IClientSession session, InventoryType inventoryType) => session.SendPacket(session.GenerateSortItems(inventoryType)); + + public static int RarityPoint(this GameItemInstance item, short rarity, short lvl) + { + int p = rarity switch + { + 0 => 0, + 1 => 1, + 2 => 2, + 3 => 3, + 4 => 4, + 5 => 5, + 6 => 7, + 7 => 10, + 8 => 15, + _ => rarity * 2 + }; + + return p * (lvl / 5 + 1); + } + + public static string GenerateInventoryRemove(this InventoryItem item) => $"ivn {(byte)item.InventoryType} {item.Slot}.-1.0.0.0"; + + public static void SendSpecialistCardInfo(this IClientSession session, GameItemInstance specialistInstance, ICharacterAlgorithm characterAlgorithm) + => session.SendPacket(session.GenerateSlInfo(specialistInstance, characterAlgorithm)); + + public static string GenerateSlInfo(this IClientSession session, GameItemInstance specialistInstance, ICharacterAlgorithm algorithm) + { + int freePoint = specialistInstance.SpPointsBasic() - specialistInstance.SlDamage - specialistInstance.SlHP - specialistInstance.SlElement - + specialistInstance.SlDefence; + + int slHit = specialistInstance.SlPoint(specialistInstance.SlDamage, SpecialistPointsType.ATTACK); + int slDefence = specialistInstance.SlPoint(specialistInstance.SlDefence, SpecialistPointsType.DEFENCE); + int slElement = specialistInstance.SlPoint(specialistInstance.SlElement, SpecialistPointsType.ELEMENT); + int slHp = specialistInstance.SlPoint(specialistInstance.SlHP, SpecialistPointsType.HPMP); + + int shellHit = session.PlayerEntity.GetMaxWeaponShellValue(ShellEffectType.SLDamage) + + session.PlayerEntity.GetMaxWeaponShellValue(ShellEffectType.SLGlobal) + + session.PlayerEntity.BCardComponent.GetAllBCardsInformation(BCardType.IncreaseSpPoints, + (byte)AdditionalTypes.IncreaseSpPoints.SpCardAttackPointIncrease, session.PlayerEntity.Level).firstData; + + int shellDefense = session.PlayerEntity.GetMaxWeaponShellValue(ShellEffectType.SLDefence) + + session.PlayerEntity.GetMaxWeaponShellValue(ShellEffectType.SLGlobal) + + session.PlayerEntity.BCardComponent.GetAllBCardsInformation(BCardType.IncreaseSpPoints, + (byte)AdditionalTypes.IncreaseSpPoints.SpCardDefensePointIncrease, session.PlayerEntity.Level).firstData; + + int shellElement = session.PlayerEntity.GetMaxWeaponShellValue(ShellEffectType.SLElement) + + session.PlayerEntity.GetMaxWeaponShellValue(ShellEffectType.SLGlobal) + + session.PlayerEntity.BCardComponent.GetAllBCardsInformation(BCardType.IncreaseSpPoints, + (byte)AdditionalTypes.IncreaseSpPoints.SpCardElementPointIncrease, session.PlayerEntity.Level).firstData; + + int shellHp = session.PlayerEntity.GetMaxWeaponShellValue(ShellEffectType.SLHP) + + session.PlayerEntity.GetMaxWeaponShellValue(ShellEffectType.SLGlobal) + + session.PlayerEntity.BCardComponent.GetAllBCardsInformation(BCardType.IncreaseSpPoints, + (byte)AdditionalTypes.IncreaseSpPoints.SpCardHpMpPointIncrease, session.PlayerEntity.Level).firstData; + + string skill = string.Empty; + var skillsSp = StaticSkillsManager.Instance.GetSkills() + .Where(ski => + ski.UpgradeType == specialistInstance.GameItem.Morph + && ski.SkillType == SkillType.NormalPlayerSkill + && ski.LevelMinimum <= specialistInstance.SpLevel) + .Select(ski => new CharacterSkill { SkillVNum = ski.Id }).ToList(); + bool spDestroyed = specialistInstance.Rarity == -2; + + int firstskillvnum = 0; + if (skillsSp.Count == 0) + { + skill = "-1"; + } + else + { + firstskillvnum = skillsSp[0].SkillVNum; + } + + for (int i = 1; i < 11; i++) + { + if (skillsSp.Count < i + 1) + { + continue; + } + + if (skillsSp[i].SkillVNum <= firstskillvnum + 10) + { + skill += $"{skillsSp[i].SkillVNum}."; + } + } + + // 10 9 8 '0 0 0 0'<- bonusdamage bonusarmor bonuselement bonushpmp its after upgrade and + // 3 first values are not important + skill = skill.TrimEnd('.'); + bool isStuff = specialistInstance.GameItem.Type == InventoryType.Specialist || specialistInstance.GameItem.Type == InventoryType.Equipment || + specialistInstance.GameItem.Type == InventoryType.EquippedItems; + return + string.Format( + "slinfo {0} {1} {2} {3} {4} {5} 0 {35} 0 0 0 0 0 {6} {7} {8} {9} {10} {11} {12} {13} {14} {15} {16} {17} {18} {19} {20} 0 0 {21} {22} {23} {24} {25} {26} {27} {28} {29} {30} {31} {32} {33} {34}", + isStuff ? "0" : "2", + specialistInstance.ItemVNum, + specialistInstance.GameItem.Morph, + specialistInstance.SpLevel, + specialistInstance.GameItem.LevelJobMinimum, + specialistInstance.GameItem.ReputationMinimum, + specialistInstance.GameItem.SpPointsUsage, + specialistInstance.GameItem.FireResistance, + specialistInstance.GameItem.WaterResistance, + specialistInstance.GameItem.LightResistance, + specialistInstance.GameItem.DarkResistance, + specialistInstance.Xp, + algorithm.GetSpecialistJobXp(specialistInstance.SpLevel), + skill, + specialistInstance.TransportId, + freePoint, + slHit, + slDefence, + slElement, + slHp, + specialistInstance.Upgrade, + spDestroyed ? 1 : 0, + shellHit, + shellDefense, + shellElement, + shellHp, + specialistInstance.SpStoneUpgrade, + specialistInstance.SpDamage, + specialistInstance.SpDefence, + specialistInstance.SpElement, + specialistInstance.SpHP, + specialistInstance.SpFire, + specialistInstance.SpWater, + specialistInstance.SpLight, + specialistInstance.SpDark, + specialistInstance.GameItem.Speed); + } + + + public static int GetRunesCount(this ItemInstanceDTO itemInstance) + { + if (itemInstance.EquipmentOptions == null) + { + return default; + } + + int count = 0; + foreach (EquipmentOptionDTO equipmentOption in itemInstance.EquipmentOptions) + { + if (equipmentOption.EquipmentOptionType != EquipmentOptionType.RUNE) + { + continue; + } + + count += equipmentOption.Weight; + } + + return count; + } + + public static int GetInternalRunesCount(this ItemInstanceDTO itemInstance) => itemInstance.EquipmentOptions?.Count(s => s.EquipmentOptionType == EquipmentOptionType.RUNE) ?? 0; + + public static int GetShellCount(this GameItemInstance itemInstance) + { + List options = itemInstance.EquipmentOptions; + + if (options == null) + { + return 0; + } + + if (!options.Any()) + { + return 0; + } + + EquipmentOptionDTO option = options.FirstOrDefault(); + + if (option == null) + { + return 0; + } + + if (option.EquipmentOptionType == EquipmentOptionType.RUNE) + { + return 0; + } + + EquipmentType slot = option.EquipmentOptionType switch + { + EquipmentOptionType.ARMOR_SHELL => EquipmentType.Armor, + EquipmentOptionType.WEAPON_SHELL => EquipmentType.MainWeapon, + EquipmentOptionType.JEWELS => EquipmentType.Ring + }; + + int count = itemInstance.EquipmentOptions.Count(s => s.EquipmentOptionType == slot switch + { + EquipmentType.Armor => EquipmentOptionType.ARMOR_SHELL, + EquipmentType.MainWeapon => EquipmentOptionType.WEAPON_SHELL, + EquipmentType.SecondaryWeapon => EquipmentOptionType.WEAPON_SHELL, + EquipmentType.Bracelet => EquipmentOptionType.JEWELS, + EquipmentType.Necklace => EquipmentOptionType.JEWELS, + EquipmentType.Ring => EquipmentOptionType.JEWELS + }); + + return count; + } + + private static string GetRuneString(this GameItemInstance itemInstance) + { + if (itemInstance.EquipmentOptions == null) + { + return string.Empty; + } + + return itemInstance.EquipmentOptions + .Where(s => s.EquipmentOptionType == EquipmentOptionType.RUNE) + .OrderBy(s => s.Level) + .Aggregate(string.Empty, (current, option) => current + $" {option.EffectVnum}.{option.Type}.{option.Level * 4}.{option.Value * 4}.1"); + } + + private static string GetShellString(this GameItemInstance itemInstance) + { + if (itemInstance.EquipmentOptions == null) + { + return string.Empty; + } + + var options = itemInstance.EquipmentOptions.Where(s => s.Level <= (s.Level > 12 ? 20 : 8)) + .OrderBy(s => s.Level).ToList(); + options.AddRange(itemInstance.EquipmentOptions.Where(s => s.Level > (s.Level > 12 ? 20 : 8)) + .OrderByDescending(s => s.Level)); + + if (itemInstance.GameItem.ItemType == ItemType.Shell) + { + return options + .Where(s => s.EquipmentOptionType == EquipmentOptionType.ARMOR_SHELL || s.EquipmentOptionType == EquipmentOptionType.WEAPON_SHELL) + .Aggregate(string.Empty, + (current, option) => current + $" {(option.Level > 12 ? option.Level - 12 : option.Level)}.{(option.Type > 50 ? option.Type - 50 : option.Type)}.{option.Value}"); + } + + switch (itemInstance.GameItem.EquipmentSlot) + { + case EquipmentType.Armor: + return options + .Where(s => s.EquipmentOptionType == EquipmentOptionType.ARMOR_SHELL) + .OrderBy(s => s.Level) + .Aggregate(string.Empty, + (current, option) => current + $" {(option.Level > 12 ? option.Level - 12 : option.Level)}.{(option.Type > 50 ? option.Type - 50 : option.Type)}.{option.Value}"); + case EquipmentType.MainWeapon: + case EquipmentType.SecondaryWeapon: + return options + .Where(s => s.EquipmentOptionType == EquipmentOptionType.WEAPON_SHELL) + .OrderBy(s => s.Level) + .Aggregate(string.Empty, (current, option) => current + $" {option.Level}.{option.Type}.{option.Value}"); + case EquipmentType.Necklace: + case EquipmentType.Bracelet: + case EquipmentType.Ring: + { + return options + .Where(s => s.EquipmentOptionType == EquipmentOptionType.JEWELS) + .OrderBy(s => s.Level) + .Aggregate(string.Empty, (current, option) => current + $" {option.Type} {option.Level} {option.Value}"); + } + } + + return ""; + } + + public static void SendEInfoPacket(this IClientSession session, GameItemInstance item, IItemsManager itemsManager, ICharacterAlgorithm characterAlgorithm) => + session.SendPacket(item.GenerateEInfo(itemsManager, characterAlgorithm)); + + public static string GenerateEInfo(this GameItemInstance itemInstance, IItemsManager itemManager, ICharacterAlgorithm algorithm) + { + EquipmentType equipmentSlot = itemInstance.GameItem.EquipmentSlot; + ItemType itemType = itemInstance.GameItem.ItemType; + byte itemClass = itemInstance.GameItem.Class; + byte subtype = itemInstance.GameItem.ItemSubType; + + long seconds; + long hours; + if (itemInstance.IsBound) + { + if (itemInstance.ItemDeleteTime == null) + { + seconds = 0; + hours = 0; + } + else + { + seconds = (long)(itemInstance.ItemDeleteTime.Value - DateTime.UtcNow).TotalSeconds; + hours = (long)(itemInstance.ItemDeleteTime.Value - DateTime.UtcNow).TotalHours; + } + } + else + { + seconds = itemInstance.GameItem.ItemValidTime; + hours = itemInstance.GameItem.ItemValidTime; + } + + switch (itemType) + { + case ItemType.Weapon: + switch (equipmentSlot) + { + case EquipmentType.MainWeapon: + { + byte eInfoType = 0; + switch (itemClass) + { + case 4: + eInfoType = 1; + break; + case 8: + eInfoType = 5; + break; + } + + if (itemInstance.OriginalItemVnum != null) + { + return + $"e_info {eInfoType} {itemInstance.ItemVNum} {itemInstance.Rarity} {itemInstance.Upgrade} {(itemInstance.IsFixed ? 1 : 0)} {itemInstance.GameItem.LevelMinimum} {itemInstance.GameItem.DamageMinimum + itemInstance.DamageMinimum} {itemInstance.GameItem.DamageMaximum + itemInstance.DamageMaximum} {itemInstance.GameItem.HitRate + itemInstance.HitRate} {itemInstance.GameItem.CriticalLuckRate} {itemInstance.GameItem.CriticalRate} {itemInstance.GameItem.Price} 0 0 {itemInstance.OriginalItemVnum} 0 0 0"; + } + + string shellStr = GetShellString(itemInstance); + string runesStr = GetRuneString(itemInstance); + int isRuneFixed = 0; + return + $"e_info {eInfoType} {itemInstance.ItemVNum} {itemInstance.Rarity} {itemInstance.Upgrade} {(itemInstance.IsFixed ? 1 : 0)} {itemInstance.GameItem.LevelMinimum} {itemInstance.GameItem.DamageMinimum + itemInstance.DamageMinimum} {itemInstance.GameItem.DamageMaximum + itemInstance.DamageMaximum} {itemInstance.GameItem.HitRate + itemInstance.HitRate} {itemInstance.GameItem.CriticalLuckRate} {itemInstance.GameItem.CriticalRate} {itemInstance.Ammo} {itemInstance.GameItem.MaximumAmmo} {itemInstance.GameItem.Price} -1 {(itemInstance.ShellRarity == null ? "0" : $"{itemInstance.ShellRarity}")} {(itemInstance.BoundCharacterId == null ? "0" : $"{itemInstance.BoundCharacterId}")} {GetShellCount(itemInstance)}{shellStr} {GetRunesCount(itemInstance)} {isRuneFixed} {GetInternalRunesCount(itemInstance)}{runesStr}"; + } + + case EquipmentType.SecondaryWeapon: + { + byte eInfoType = 0; + switch (itemClass) + { + case 1: + case 2: + eInfoType = 1; + break; + } + + if (itemInstance.OriginalItemVnum != null) + { + return + $"e_info {eInfoType} {itemInstance.ItemVNum} {itemInstance.Rarity} {itemInstance.Upgrade} {(itemInstance.IsFixed ? 1 : 0)} {itemInstance.GameItem.LevelMinimum} {itemInstance.GameItem.DamageMinimum + itemInstance.DamageMinimum} {itemInstance.GameItem.DamageMaximum + itemInstance.DamageMaximum} {itemInstance.GameItem.HitRate + itemInstance.HitRate} {itemInstance.GameItem.CriticalLuckRate} {itemInstance.GameItem.CriticalRate} {itemInstance.GameItem.Price} 0 0 {itemInstance.OriginalItemVnum} 0 0 0"; + } + + string shellStr = GetShellString(itemInstance); + string runesStr = GetRuneString(itemInstance); + int isRuneFixed = 0; + return + $"e_info {eInfoType} {itemInstance.ItemVNum} {itemInstance.Rarity} {itemInstance.Upgrade} {(itemInstance.IsFixed ? 1 : 0)} {itemInstance.GameItem.LevelMinimum} {itemInstance.GameItem.DamageMinimum + itemInstance.DamageMinimum} {itemInstance.GameItem.DamageMaximum + itemInstance.DamageMaximum} {itemInstance.GameItem.HitRate + itemInstance.HitRate} {itemInstance.GameItem.CriticalLuckRate} {itemInstance.GameItem.CriticalRate} {itemInstance.Ammo} {itemInstance.GameItem.MaximumAmmo} {itemInstance.GameItem.Price} -1 {(itemInstance.ShellRarity == null ? "0" : $"{itemInstance.ShellRarity}")} {(itemInstance.BoundCharacterId == null ? "0" : $"{itemInstance.BoundCharacterId}")} {GetShellCount(itemInstance)}{shellStr} {GetRunesCount(itemInstance)} {isRuneFixed} {GetInternalRunesCount(itemInstance)}{runesStr}"; + } + } + + break; + + case ItemType.Armor: + { + byte eInfoType = 2; + + if (itemInstance.OriginalItemVnum != null) + { + return + $"e_info {eInfoType} {itemInstance.ItemVNum} {itemInstance.Rarity} {itemInstance.Upgrade} {(itemInstance.IsFixed ? 1 : 0)} {itemInstance.GameItem.LevelMinimum} {itemInstance.GameItem.CloseDefence + itemInstance.CloseDefence} {itemInstance.GameItem.DistanceDefence + itemInstance.DistanceDefence} {itemInstance.GameItem.MagicDefence + itemInstance.MagicDefence} {itemInstance.GameItem.DefenceDodge + itemInstance.DefenceDodge} {itemInstance.GameItem.Price} {itemInstance.OriginalItemVnum} 0 0 0"; + } + + string shellStr = GetShellString(itemInstance); + string runesStr = GetRuneString(itemInstance); + int isRuneFixed = 0; + return + $"e_info {eInfoType} {itemInstance.ItemVNum} {itemInstance.Rarity} {itemInstance.Upgrade} {(itemInstance.IsFixed ? 1 : 0)} {itemInstance.GameItem.LevelMinimum} {itemInstance.GameItem.CloseDefence + itemInstance.CloseDefence} {itemInstance.GameItem.DistanceDefence + itemInstance.DistanceDefence} {itemInstance.GameItem.MagicDefence + itemInstance.MagicDefence} {itemInstance.GameItem.DefenceDodge + itemInstance.DefenceDodge} {itemInstance.GameItem.Price} -1 {(itemInstance.ShellRarity == null ? "0" : $"{itemInstance.ShellRarity}")} {(itemInstance.BoundCharacterId == null ? "0" : $"{itemInstance.BoundCharacterId}")} {GetShellCount(itemInstance)}{shellStr} {GetRunesCount(itemInstance)} {isRuneFixed} {GetInternalRunesCount(itemInstance)}{runesStr}"; + } + + case ItemType.Fashion: + switch (equipmentSlot) + { + case EquipmentType.CostumeHat: + return + $"e_info 3 {itemInstance.ItemVNum} {itemInstance.GameItem.LevelMinimum} {itemInstance.GameItem.CloseDefence + itemInstance.CloseDefence} {itemInstance.GameItem.DistanceDefence + itemInstance.DistanceDefence} {itemInstance.GameItem.MagicDefence + itemInstance.MagicDefence} {itemInstance.GameItem.DefenceDodge + itemInstance.DefenceDodge} {itemInstance.GameItem.FireResistance + itemInstance.FireResistance} {itemInstance.GameItem.WaterResistance + itemInstance.WaterResistance} {itemInstance.GameItem.LightResistance + itemInstance.LightResistance} {itemInstance.GameItem.DarkResistance + itemInstance.DarkResistance} {itemInstance.GameItem.Price} -1 {(itemInstance.IsBound ? 0 : 1)} {(itemInstance.GameItem.ItemValidTime == -1 ? -1 : seconds / 3600)} 0"; + + case EquipmentType.CostumeSuit: + return + $"e_info 2 {itemInstance.ItemVNum} {itemInstance.Rarity} {itemInstance.Upgrade} {(itemInstance.IsFixed ? 1 : 0)} {itemInstance.GameItem.LevelMinimum} {itemInstance.GameItem.CloseDefence + itemInstance.CloseDefence} {itemInstance.GameItem.DistanceDefence + itemInstance.DistanceDefence} {itemInstance.GameItem.MagicDefence + itemInstance.MagicDefence} {itemInstance.GameItem.DefenceDodge + itemInstance.DefenceDodge} {itemInstance.GameItem.Price} -1 {(itemInstance.IsBound ? 0 : 1)} {(itemInstance.GameItem.ItemValidTime == -1 ? -1 : seconds / 3600)} 0"; // 1 = IsCosmetic -1 = no shells + + default: + return + $"e_info 3 {itemInstance.ItemVNum} {itemInstance.GameItem.LevelMinimum} {itemInstance.GameItem.CloseDefence + itemInstance.CloseDefence} {itemInstance.GameItem.DistanceDefence + itemInstance.DistanceDefence} {itemInstance.GameItem.MagicDefence + itemInstance.MagicDefence} {itemInstance.GameItem.DefenceDodge + itemInstance.DefenceDodge} {itemInstance.GameItem.FireResistance + itemInstance.FireResistance} {itemInstance.GameItem.WaterResistance + itemInstance.WaterResistance} {itemInstance.GameItem.LightResistance + itemInstance.LightResistance} {itemInstance.GameItem.DarkResistance + itemInstance.DarkResistance} {itemInstance.GameItem.Price} {itemInstance.Upgrade} 0 -1"; // after Item.Price theres TimesConnected {(Item.ItemValidTime == 0 ? -1 : Item.ItemValidTime / (3600))} + } + + case ItemType.Jewelry: + switch (equipmentSlot) + { + case EquipmentType.Amulet: + if (itemInstance.DurabilityPoint > 0) + { + return + $"e_info 4 {itemInstance.ItemVNum} {itemInstance.GameItem.LevelMinimum} {itemInstance.DurabilityPoint} {itemInstance.GameItem.ItemLeftType} 0 {itemInstance.GameItem.Price}"; + } + + return $"e_info 4 {itemInstance.ItemVNum} {itemInstance.GameItem.LevelMinimum} {seconds * 10} {itemInstance.GameItem.ItemLeftType} 0 {itemInstance.GameItem.Price}"; + + case EquipmentType.Fairy: + // 0 nothing + // 1 can trade + // 2 can't trade + int canTrade; + if (itemInstance.GameItem.MaxElementRate == 70 || itemInstance.GameItem.MaxElementRate == 80) + { + canTrade = itemInstance.IsBound ? 2 : 1; + } + else + { + canTrade = 0; + } + + return + $"e_info 4 {itemInstance.ItemVNum} {itemInstance.GameItem.Element} {itemInstance.ElementRate + itemInstance.GameItem.ElementRate} 0 {itemInstance.Xp} {itemInstance.GameItem.Price} {canTrade} 0"; // last IsNosmall + + case EquipmentType.Necklace: + case EquipmentType.Bracelet: + case EquipmentType.Ring: + return + $"e_info 4 {itemInstance.ItemVNum} {itemInstance.GameItem.LevelMinimum} {itemInstance.GameItem.MaxCellonLvl} {itemInstance.GameItem.MaxCellon} {GetShellCount(itemInstance)} {itemInstance.GameItem.Price}{GetShellString(itemInstance)}"; + + default: + return + $"e_info 4 {itemInstance.ItemVNum} {itemInstance.GameItem.LevelMinimum} {itemInstance.GameItem.MaxCellonLvl} {itemInstance.GameItem.MaxCellon} {itemInstance.Cellon} {itemInstance.GameItem.Price}"; + } + + case ItemType.Specialist: + return $"e_info 8 {itemInstance.ItemVNum}"; + + case ItemType.Box: + + // 0 = NOSMATE pearl 1= npc pearl 2 = sp box 3 = raid box 4= VEHICLE pearl + // 5=fairy pearl + switch (subtype) + { + case 0: + return itemInstance.HoldingVNum is null or 0 + ? $"e_info 7 {itemInstance.ItemVNum} 0" + : $"e_info 7 {itemInstance.ItemVNum} 1 {itemInstance.HoldingVNum} {itemInstance.SpLevel} {itemInstance.Xp} {algorithm.GetLevelXp(itemInstance.SpLevel, true, MateType.Pet)} {itemInstance.SpDamage} {itemInstance.SpDefence}"; + + case 1: + return itemInstance.HoldingVNum is null or 0 + ? $"e_info 7 {itemInstance.ItemVNum} 0" + : $"e_info 7 {itemInstance.ItemVNum} 1 {itemInstance.HoldingVNum} {itemInstance.SpLevel} {itemInstance.Xp} {algorithm.GetLevelXp(itemInstance.SpLevel, true)} {itemInstance.SpDamage} {itemInstance.SpDefence}"; + case 2: + if (itemInstance.HoldingVNum is null or 0) + { + return $"e_info 7 {itemInstance.ItemVNum} 0"; + } + + IGameItem spitem = itemManager.GetItem(itemInstance.HoldingVNum.Value); + return + $"e_info 7 {itemInstance.ItemVNum} 1 {itemInstance.HoldingVNum} {itemInstance.SpLevel} {itemInstance.Xp} {algorithm.GetSpecialistJobXp(itemInstance.SpLevel)} {itemInstance.Upgrade} {itemInstance.SlPoint(itemInstance.SlDamage, SpecialistPointsType.ATTACK)} {itemInstance.SlPoint(itemInstance.SlDefence, SpecialistPointsType.DEFENCE)} {itemInstance.SlPoint(itemInstance.SlElement, SpecialistPointsType.ELEMENT)} {itemInstance.SlPoint(itemInstance.SlHP, SpecialistPointsType.HPMP)} {itemInstance.SpPointsBasic() - itemInstance.SlDamage - itemInstance.SlHP - itemInstance.SlElement - itemInstance.SlDefence} {itemInstance.SpStoneUpgrade} {spitem.FireResistance} {spitem.WaterResistance} {spitem.LightResistance} {spitem.DarkResistance} {itemInstance.SpDamage} {itemInstance.SpDefence} {itemInstance.SpElement} {itemInstance.SpHP} {itemInstance.SpFire} {itemInstance.SpWater} {itemInstance.SpLight} {itemInstance.SpDark}"; + + case 4: + return itemInstance.HoldingVNum is null or 0 + ? $"e_info 11 {itemInstance.ItemVNum} 0" + : $"e_info 11 {itemInstance.ItemVNum} 1 {itemInstance.HoldingVNum}"; + + case 5: + if (itemInstance.HoldingVNum is null or 0) + { + return $"e_info 12 {itemInstance.ItemVNum} 0"; + } + + IGameItem fairyitem = itemManager.GetItem(itemInstance.HoldingVNum.Value); + return itemInstance.HoldingVNum == 0 + ? $"e_info 12 {itemInstance.ItemVNum} 0" + : $"e_info 12 {itemInstance.ItemVNum} 1 {itemInstance.HoldingVNum} {itemInstance.ElementRate + fairyitem.ElementRate}"; + + case 6: + if (itemInstance.HoldingVNum is null or 0) + { + return $"e_info 12 {itemInstance.ItemVNum} 0"; + } + + byte? itemElement = itemManager.GetItem(itemInstance.HoldingVNum.Value)?.Element; + return $"e_info 13 {itemInstance.ItemVNum} 1 {itemInstance.HoldingVNum} {itemElement} {itemInstance.GenerateSkillInfo(2).Replace('.', ' ')}"; + + case 7: + return $"e_info 11 {itemInstance.ItemVNum} {(itemInstance.IsBound ? 1 : 0)} {hours}"; + + default: + return $"e_info 8 {itemInstance.ItemVNum} -1 {itemInstance.Rarity}"; + } + + case ItemType.Shell: + return + $"e_info 9 {itemInstance.ItemVNum} {itemInstance.Upgrade} {itemInstance.Rarity} {itemInstance.GameItem.Price} {GetShellCount(itemInstance)}{GetShellString(itemInstance)}"; + } + + return string.Empty; + } + + public static void SetRarityPoint(this GameItemInstance itemInstance, IRandomGenerator randomGenerator) + { + if (itemInstance.Type != ItemInstanceType.WearableInstance || itemInstance.Rarity == 0) + { + return; + } + + int multiplier; + switch (itemInstance.Rarity) + { + case -2: + multiplier = -20; + break; + case -1: + multiplier = -10; + break; + case 0: + multiplier = 0; + break; + case 1: + multiplier = 1; + break; + case 2: + multiplier = 2; + break; + case 3: + multiplier = 3; + break; + case 4: + multiplier = 4; + break; + case 5: + multiplier = 5; + break; + case 6: + multiplier = 7; + break; + case 7: + case 8: + multiplier = 10; + break; + default: + multiplier = 0; + break; + } + + if (multiplier == 0) + { + return; + } + + double itemFix = itemInstance.GameItem.IsHeroic ? 0.05 : 0; + short itemLevel = itemInstance.GameItem.LevelMinimum; + + if (itemInstance.GameItem.IsHeroic) + { + itemLevel = (short)Math.Floor(100.0 + (itemInstance.GameItem.LevelMinimum - 20.0) / 4.0); + } + + switch (itemInstance.GameItem.EquipmentSlot) + { + case EquipmentType.MainWeapon: + case EquipmentType.SecondaryWeapon: + + if (itemInstance.Rarity > 0) + { + int additionalAttack = (int)Math.Floor((itemLevel / 5 + 1 + itemFix) * multiplier); + if (itemInstance.Rarity == 8) + { + additionalAttack += randomGenerator.RandomNumber(40, 101); + } + + int minMultiplier = (int)Math.Floor(itemLevel / 25.0 * multiplier); + int maxMultiplier = (int)Math.Floor((itemLevel / 20.0 + 1) * multiplier) + (int)Math.Floor(itemLevel / 40.0 * multiplier); + int hitRateMultiplier = randomGenerator.RandomNumber(minMultiplier, maxMultiplier); + + + itemInstance.WeaponMinDamageAdditionalValue = additionalAttack - hitRateMultiplier; + itemInstance.WeaponMaxDamageAdditionalValue = additionalAttack - hitRateMultiplier; + itemInstance.WeaponHitRateAdditionalValue = hitRateMultiplier; + } + else + { + int minMultiplier = multiplier; + int maxMultiplier = multiplier; + + itemInstance.WeaponMinDamageAdditionalValue = minMultiplier; + itemInstance.WeaponMaxDamageAdditionalValue = maxMultiplier; + } + + break; + + case EquipmentType.Armor: + + if (itemInstance.Rarity > 0) + { + int additionalArmor = (int)Math.Floor((itemLevel / 5 + 1 + itemFix) * multiplier); + if (itemInstance.Rarity == 8) + { + additionalArmor += randomGenerator.RandomNumber(40, 101); + } + + int minDifferent = (int)Math.Floor(itemLevel / 75.0 * multiplier); + int maxDifferent = (int)Math.Floor((itemLevel / 30.0 + 1.0) * multiplier); + int dodgeAdditionalValue = randomGenerator.RandomNumber(minDifferent, maxDifferent); + + int minDifferentMagic = (int)Math.Floor(itemLevel / 50.0 * multiplier); + int maxDifferentMagic = (int)Math.Floor((itemLevel / 25.0 + 1.0) * multiplier); + int armorMagicAdditionalValue = randomGenerator.RandomNumber(minDifferentMagic, maxDifferentMagic); + + int multiplierLeft = randomGenerator.RandomNumber(70, 131); + int left = additionalArmor - dodgeAdditionalValue - armorMagicAdditionalValue; + int armorRangeAdditionalValue = (int)Math.Floor(left / 2.0 * (multiplierLeft * 0.01)); + int armorMeleeAdditionalValue = left - armorRangeAdditionalValue; + + itemInstance.ArmorDodgeAdditionalValue = dodgeAdditionalValue; + itemInstance.ArmorMeleeAdditionalValue = armorMeleeAdditionalValue; + itemInstance.ArmorRangeAdditionalValue = armorRangeAdditionalValue; + itemInstance.ArmorMagicAdditionalValue = armorMagicAdditionalValue; + } + else + { + itemInstance.ArmorMeleeAdditionalValue = multiplier; + itemInstance.ArmorRangeAdditionalValue = multiplier; + itemInstance.ArmorMagicAdditionalValue = multiplier; + } + + break; + } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Families/Configuration/FamilyConfiguration.cs b/srcs/WingsAPI.Game/Families/Configuration/FamilyConfiguration.cs new file mode 100644 index 0000000..f9c4926 --- /dev/null +++ b/srcs/WingsAPI.Game/Families/Configuration/FamilyConfiguration.cs @@ -0,0 +1,73 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Generic; +using System.Linq; +using WingsEmu.Core; + +namespace WingsEmu.Game.Families.Configuration; + +public class FamilyConfiguration +{ + /* + * CREATION + */ + public bool CreationIsGroupRequired { get; set; } = false; + public int CreationGroupMembersRequired { get; set; } = 3; + public int CreationPrice { get; set; } = 200_000; + public int MinimumNameLength { get; set; } = 3; + public int MaximumNameLength { get; set; } = 20; + + public int DeputyLimit { get; set; } = 2; + public int KeeperLimit { get; set; } = 999; + + public TimeSpan TimeBetweenFamilyRejoin { get; set; } = TimeSpan.FromDays(1); + public byte DefaultMembershipCapacity { get; set; } = 20; + + public HashSet Upgrades { get; set; } = new(); + + public List Levels { get; set; } = new() + { + new() + { + Level = 1, + ExperienceRange = new Range + { + Minimum = 0, + Maximum = 99_999 + } + }, + new() + { + Level = 2, + ExperienceRange = new Range + { + Minimum = 100_000, + Maximum = 219_999 + } + }, + new() + { + Level = 3, + ExperienceRange = new Range + { + Minimum = 220_000, + Maximum = 369_999 + } + } + }; + + public byte GetLevelByFamilyXp(long familyXp) + { + LevelExperience levelInfo = Levels.FirstOrDefault(x => x.ExperienceRange.Minimum <= familyXp && familyXp <= x.ExperienceRange.Maximum); + return levelInfo?.Level ?? default; + } + + public Range GetRangeByFamilyXp(long familyXp) + { + LevelExperience levelInfo = Levels.FirstOrDefault(x => x.ExperienceRange.Minimum <= familyXp && familyXp <= x.ExperienceRange.Maximum); + return levelInfo?.ExperienceRange; + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Families/Configuration/FamilyUpgradesConfiguration.cs b/srcs/WingsAPI.Game/Families/Configuration/FamilyUpgradesConfiguration.cs new file mode 100644 index 0000000..31f28b1 --- /dev/null +++ b/srcs/WingsAPI.Game/Families/Configuration/FamilyUpgradesConfiguration.cs @@ -0,0 +1,10 @@ +using WingsAPI.Data.Families; + +namespace WingsEmu.Game.Families.Configuration; + +public class FamilyUpgradesConfiguration +{ + public FamilyUpgradeType UpgradeType { get; set; } + public byte UpgradeLevel { get; set; } + public short Value { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Families/Configuration/LevelExperience.cs b/srcs/WingsAPI.Game/Families/Configuration/LevelExperience.cs new file mode 100644 index 0000000..fb1cb07 --- /dev/null +++ b/srcs/WingsAPI.Game/Families/Configuration/LevelExperience.cs @@ -0,0 +1,10 @@ +using WingsEmu.Core; + +namespace WingsEmu.Game.Families.Configuration; + +public class LevelExperience +{ + public byte Level { get; set; } + + public Range ExperienceRange { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Families/Enum/FamXpObtainedFromType.cs b/srcs/WingsAPI.Game/Families/Enum/FamXpObtainedFromType.cs new file mode 100644 index 0000000..f9a6a50 --- /dev/null +++ b/srcs/WingsAPI.Game/Families/Enum/FamXpObtainedFromType.cs @@ -0,0 +1,9 @@ +namespace WingsEmu.Game.Families.Enum; + +public enum FamXpObtainedFromType +{ + Command = 0, + Raid = 1, + InstantCombat = 2, + LevelUp = 3 +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Families/Event/FamilyAddExperienceEvent.cs b/srcs/WingsAPI.Game/Families/Event/FamilyAddExperienceEvent.cs new file mode 100644 index 0000000..63b60ff --- /dev/null +++ b/srcs/WingsAPI.Game/Families/Event/FamilyAddExperienceEvent.cs @@ -0,0 +1,16 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Game.Families.Enum; + +namespace WingsEmu.Game.Families.Event; + +public class FamilyAddExperienceEvent : PlayerEvent +{ + public FamilyAddExperienceEvent(long experienceGained, FamXpObtainedFromType famXpObtainedFromType) + { + ExperienceGained = experienceGained; + FamXpObtainedFromType = famXpObtainedFromType; + } + + public long ExperienceGained { get; } + public FamXpObtainedFromType FamXpObtainedFromType { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Families/Event/FamilyAddLogEvent.cs b/srcs/WingsAPI.Game/Families/Event/FamilyAddLogEvent.cs new file mode 100644 index 0000000..572a60a --- /dev/null +++ b/srcs/WingsAPI.Game/Families/Event/FamilyAddLogEvent.cs @@ -0,0 +1,15 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsAPI.Data.Families; +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Families.Event; + +public class FamilyAddLogEvent : PlayerEvent +{ + public FamilyAddLogEvent(FamilyLogDto log) => Log = log; + + public FamilyLogDto Log { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Families/Event/FamilyAddMemberEvent.cs b/srcs/WingsAPI.Game/Families/Event/FamilyAddMemberEvent.cs new file mode 100644 index 0000000..509617e --- /dev/null +++ b/srcs/WingsAPI.Game/Families/Event/FamilyAddMemberEvent.cs @@ -0,0 +1,20 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Packets.Enums.Families; + +namespace WingsEmu.Game.Families.Event; + +public class FamilyAddMemberEvent : PlayerEvent +{ + public FamilyAddMemberEvent(long familyIdToJoin, long senderId, FamilyAuthority familyAuthority = FamilyAuthority.Member) + { + FamilyIdToJoin = familyIdToJoin; + FamilyAuthority = familyAuthority; + SenderId = senderId; + } + + public long SenderId { get; } + + public long FamilyIdToJoin { get; } + + public FamilyAuthority FamilyAuthority { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Families/Event/FamilyChangeAuthorityEvent.cs b/srcs/WingsAPI.Game/Families/Event/FamilyChangeAuthorityEvent.cs new file mode 100644 index 0000000..f8522f0 --- /dev/null +++ b/srcs/WingsAPI.Game/Families/Event/FamilyChangeAuthorityEvent.cs @@ -0,0 +1,23 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Packets.Enums.Families; + +namespace WingsEmu.Game.Families.Event; + +public class FamilyChangeAuthorityEvent : PlayerEvent +{ + public FamilyChangeAuthorityEvent(FamilyAuthority familyAuthority, long memberId, byte confirmed, string characterName = null) + { + MemberId = memberId; + Confirmed = confirmed; + CharacterName = characterName; + FamilyAuthority = familyAuthority; + } + + public FamilyAuthority FamilyAuthority { get; } + + public long MemberId { get; } + + public byte Confirmed { get; } + + public string CharacterName { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Families/Event/FamilyChangeDeputyEvent.cs b/srcs/WingsAPI.Game/Families/Event/FamilyChangeDeputyEvent.cs new file mode 100644 index 0000000..2f7b9ee --- /dev/null +++ b/srcs/WingsAPI.Game/Families/Event/FamilyChangeDeputyEvent.cs @@ -0,0 +1,15 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Families.Event; + +public class FamilyChangeDeputyEvent : PlayerEvent +{ + public FamilyChangeDeputyEvent(string sourceName, string targetName) + { + SourceName = sourceName; + TargetName = targetName; + } + + public string SourceName { get; } + public string TargetName { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Families/Event/FamilyChangeFactionEvent.cs b/srcs/WingsAPI.Game/Families/Event/FamilyChangeFactionEvent.cs new file mode 100644 index 0000000..fec367c --- /dev/null +++ b/srcs/WingsAPI.Game/Families/Event/FamilyChangeFactionEvent.cs @@ -0,0 +1,8 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Families.Event; + +public class FamilyChangeFactionEvent : PlayerEvent +{ + public int Faction { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Families/Event/FamilyChangeSettingsEvent.cs b/srcs/WingsAPI.Game/Families/Event/FamilyChangeSettingsEvent.cs new file mode 100644 index 0000000..652268b --- /dev/null +++ b/srcs/WingsAPI.Game/Families/Event/FamilyChangeSettingsEvent.cs @@ -0,0 +1,12 @@ +using WingsAPI.Packets.Enums.Families; +using WingsEmu.Game._packetHandling; +using WingsEmu.Packets.Enums.Families; + +namespace WingsEmu.Game.Families.Event; + +public class FamilyChangeSettingsEvent : PlayerEvent +{ + public FamilyAuthority Authority { get; set; } + public FamilyActionType FamilyActionType { get; set; } + public byte Value { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Families/Event/FamilyChangeSexEvent.cs b/srcs/WingsAPI.Game/Families/Event/FamilyChangeSexEvent.cs new file mode 100644 index 0000000..645efd4 --- /dev/null +++ b/srcs/WingsAPI.Game/Families/Event/FamilyChangeSexEvent.cs @@ -0,0 +1,10 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Families.Event; + +public class FamilyChangeSexEvent : PlayerEvent +{ + public FamilyChangeSexEvent(byte gender) => Gender = gender; + + public byte Gender { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Families/Event/FamilyChangeTitleEvent.cs b/srcs/WingsAPI.Game/Families/Event/FamilyChangeTitleEvent.cs new file mode 100644 index 0000000..52e0a13 --- /dev/null +++ b/srcs/WingsAPI.Game/Families/Event/FamilyChangeTitleEvent.cs @@ -0,0 +1,17 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Packets.Enums.Families; + +namespace WingsEmu.Game.Families.Event; + +public class FamilyChangeTitleEvent : PlayerEvent +{ + public FamilyChangeTitleEvent(string nickname, FamilyTitle title) + { + MemberNickname = nickname; + Title = title; + } + + public string MemberNickname { get; } + + public FamilyTitle Title { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Families/Event/FamilyCreateEvent.cs b/srcs/WingsAPI.Game/Families/Event/FamilyCreateEvent.cs new file mode 100644 index 0000000..aee1579 --- /dev/null +++ b/srcs/WingsAPI.Game/Families/Event/FamilyCreateEvent.cs @@ -0,0 +1,12 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Families.Event; + +public class FamilyCreateEvent : PlayerEvent +{ + public string Name { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Families/Event/FamilyCreatedEvent.cs b/srcs/WingsAPI.Game/Families/Event/FamilyCreatedEvent.cs new file mode 100644 index 0000000..b9c7b1a --- /dev/null +++ b/srcs/WingsAPI.Game/Families/Event/FamilyCreatedEvent.cs @@ -0,0 +1,7 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Families.Event; + +public class FamilyCreatedEvent : PlayerEvent +{ +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Families/Event/FamilyDisbandEvent.cs b/srcs/WingsAPI.Game/Families/Event/FamilyDisbandEvent.cs new file mode 100644 index 0000000..efff92b --- /dev/null +++ b/srcs/WingsAPI.Game/Families/Event/FamilyDisbandEvent.cs @@ -0,0 +1,7 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Families.Event; + +public class FamilyDisbandEvent : PlayerEvent +{ +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Families/Event/FamilyDisbandedEvent.cs b/srcs/WingsAPI.Game/Families/Event/FamilyDisbandedEvent.cs new file mode 100644 index 0000000..e446e12 --- /dev/null +++ b/srcs/WingsAPI.Game/Families/Event/FamilyDisbandedEvent.cs @@ -0,0 +1,8 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Families.Event; + +public class FamilyDisbandedEvent : PlayerEvent +{ + public long FamilyId { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Families/Event/FamilyInviteResponseEvent.cs b/srcs/WingsAPI.Game/Families/Event/FamilyInviteResponseEvent.cs new file mode 100644 index 0000000..9601640 --- /dev/null +++ b/srcs/WingsAPI.Game/Families/Event/FamilyInviteResponseEvent.cs @@ -0,0 +1,17 @@ +using WingsAPI.Packets.Enums.Families; +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Families.Event; + +public class FamilyInviteResponseEvent : PlayerEvent +{ + public FamilyInviteResponseEvent(FamilyJoinType familyJoinType, long senderCharacterId) + { + SenderCharacterId = senderCharacterId; + FamilyJoinType = familyJoinType; + } + + public FamilyJoinType FamilyJoinType { get; } + + public long SenderCharacterId { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Families/Event/FamilyInvitedEvent.cs b/srcs/WingsAPI.Game/Families/Event/FamilyInvitedEvent.cs new file mode 100644 index 0000000..d5665c9 --- /dev/null +++ b/srcs/WingsAPI.Game/Families/Event/FamilyInvitedEvent.cs @@ -0,0 +1,8 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Families.Event; + +public class FamilyInvitedEvent : PlayerEvent +{ + public long TargetId { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Families/Event/FamilyJoinedEvent.cs b/srcs/WingsAPI.Game/Families/Event/FamilyJoinedEvent.cs new file mode 100644 index 0000000..504cf3c --- /dev/null +++ b/srcs/WingsAPI.Game/Families/Event/FamilyJoinedEvent.cs @@ -0,0 +1,9 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Families.Event; + +public class FamilyJoinedEvent : PlayerEvent +{ + public long FamilyId { get; init; } + public long InviterId { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Families/Event/FamilyKickedMemberEvent.cs b/srcs/WingsAPI.Game/Families/Event/FamilyKickedMemberEvent.cs new file mode 100644 index 0000000..00a3888 --- /dev/null +++ b/srcs/WingsAPI.Game/Families/Event/FamilyKickedMemberEvent.cs @@ -0,0 +1,8 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Families.Event; + +public class FamilyKickedMemberEvent : PlayerEvent +{ + public long KickedMemberId { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Families/Event/FamilyLeaveEvent.cs b/srcs/WingsAPI.Game/Families/Event/FamilyLeaveEvent.cs new file mode 100644 index 0000000..b8db128 --- /dev/null +++ b/srcs/WingsAPI.Game/Families/Event/FamilyLeaveEvent.cs @@ -0,0 +1,7 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Families.Event; + +public class FamilyLeaveEvent : PlayerEvent +{ +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Families/Event/FamilyLeftEvent.cs b/srcs/WingsAPI.Game/Families/Event/FamilyLeftEvent.cs new file mode 100644 index 0000000..5fb5aeb --- /dev/null +++ b/srcs/WingsAPI.Game/Families/Event/FamilyLeftEvent.cs @@ -0,0 +1,8 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Families.Event; + +public class FamilyLeftEvent : PlayerEvent +{ + public long FamilyId { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Families/Event/FamilyListMembersEvent.cs b/srcs/WingsAPI.Game/Families/Event/FamilyListMembersEvent.cs new file mode 100644 index 0000000..36de17c --- /dev/null +++ b/srcs/WingsAPI.Game/Families/Event/FamilyListMembersEvent.cs @@ -0,0 +1,7 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Families.Event; + +public class FamilyListMembersEvent : PlayerEvent +{ +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Families/Event/FamilyMessageSentEvent.cs b/srcs/WingsAPI.Game/Families/Event/FamilyMessageSentEvent.cs new file mode 100644 index 0000000..2e4cb68 --- /dev/null +++ b/srcs/WingsAPI.Game/Families/Event/FamilyMessageSentEvent.cs @@ -0,0 +1,9 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Families.Event; + +public class FamilyMessageSentEvent : PlayerEvent +{ + public string Message { get; init; } + public FamilyMessageType MessageType { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Families/Event/FamilyNoticeMessageEvent.cs b/srcs/WingsAPI.Game/Families/Event/FamilyNoticeMessageEvent.cs new file mode 100644 index 0000000..3e21575 --- /dev/null +++ b/srcs/WingsAPI.Game/Families/Event/FamilyNoticeMessageEvent.cs @@ -0,0 +1,15 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Families.Event; + +public class FamilyNoticeMessageEvent : PlayerEvent +{ + public FamilyNoticeMessageEvent(string message, bool cleanMessage = false) + { + Message = message; + CleanMessage = cleanMessage; + } + + public string Message { get; } + public bool CleanMessage { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Families/Event/FamilyReceiveInviteEvent.cs b/srcs/WingsAPI.Game/Families/Event/FamilyReceiveInviteEvent.cs new file mode 100644 index 0000000..929de5f --- /dev/null +++ b/srcs/WingsAPI.Game/Families/Event/FamilyReceiveInviteEvent.cs @@ -0,0 +1,19 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Families.Event; + +public class FamilyReceiveInviteEvent : PlayerEvent +{ + public FamilyReceiveInviteEvent(string familyName, long senderCharacterId, long familyId) + { + FamilyName = familyName; + SenderCharacterId = senderCharacterId; + FamilyId = familyId; + } + + public long SenderCharacterId { get; } + + public long FamilyId { get; } + + public string FamilyName { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Families/Event/FamilyRemoveMemberEvent.cs b/srcs/WingsAPI.Game/Families/Event/FamilyRemoveMemberEvent.cs new file mode 100644 index 0000000..49c0be1 --- /dev/null +++ b/srcs/WingsAPI.Game/Families/Event/FamilyRemoveMemberEvent.cs @@ -0,0 +1,10 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Families.Event; + +public class FamilyRemoveMemberEvent : PlayerEvent +{ + public FamilyRemoveMemberEvent(string nickname) => Nickname = nickname; + + public string Nickname { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Families/Event/FamilySendInviteEvent.cs b/srcs/WingsAPI.Game/Families/Event/FamilySendInviteEvent.cs new file mode 100644 index 0000000..52bde2a --- /dev/null +++ b/srcs/WingsAPI.Game/Families/Event/FamilySendInviteEvent.cs @@ -0,0 +1,10 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Families.Event; + +public class FamilySendInviteEvent : PlayerEvent +{ + public FamilySendInviteEvent(string receiverNickname) => ReceiverNickname = receiverNickname; + + public string ReceiverNickname { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Families/Event/FamilyShoutEvent.cs b/srcs/WingsAPI.Game/Families/Event/FamilyShoutEvent.cs new file mode 100644 index 0000000..316c777 --- /dev/null +++ b/srcs/WingsAPI.Game/Families/Event/FamilyShoutEvent.cs @@ -0,0 +1,10 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Families.Event; + +public class FamilyShoutEvent : PlayerEvent +{ + public FamilyShoutEvent(string message) => Message = message; + + public string Message { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Families/Event/FamilyTodayEvent.cs b/srcs/WingsAPI.Game/Families/Event/FamilyTodayEvent.cs new file mode 100644 index 0000000..deec55a --- /dev/null +++ b/srcs/WingsAPI.Game/Families/Event/FamilyTodayEvent.cs @@ -0,0 +1,10 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Families.Event; + +public class FamilyTodayEvent : PlayerEvent +{ + public FamilyTodayEvent(string message) => Message = message; + + public string Message { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Families/Event/FamilyUpgradeBoughtEvent.cs b/srcs/WingsAPI.Game/Families/Event/FamilyUpgradeBoughtEvent.cs new file mode 100644 index 0000000..5dc7b66 --- /dev/null +++ b/srcs/WingsAPI.Game/Families/Event/FamilyUpgradeBoughtEvent.cs @@ -0,0 +1,12 @@ +using WingsAPI.Data.Families; +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Families.Event; + +public class FamilyUpgradeBoughtEvent : PlayerEvent +{ + public long FamilyId { get; init; } + public int UpgradeVnum { get; init; } + public FamilyUpgradeType FamilyUpgradeType { get; init; } + public short UpgradeValue { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Families/Event/FamilyWarehouseAddItemEvent.cs b/srcs/WingsAPI.Game/Families/Event/FamilyWarehouseAddItemEvent.cs new file mode 100644 index 0000000..ec1e420 --- /dev/null +++ b/srcs/WingsAPI.Game/Families/Event/FamilyWarehouseAddItemEvent.cs @@ -0,0 +1,11 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Game.Inventory; + +namespace WingsEmu.Game.Families.Event; + +public class FamilyWarehouseAddItemEvent : PlayerEvent +{ + public InventoryItem Item { get; init; } + public short Amount { get; init; } + public short DestinationSlot { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Families/Event/FamilyWarehouseCloseEvent.cs b/srcs/WingsAPI.Game/Families/Event/FamilyWarehouseCloseEvent.cs new file mode 100644 index 0000000..cc760d1 --- /dev/null +++ b/srcs/WingsAPI.Game/Families/Event/FamilyWarehouseCloseEvent.cs @@ -0,0 +1,7 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Families.Event; + +public class FamilyWarehouseCloseEvent : PlayerEvent +{ +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Families/Event/FamilyWarehouseItemPlacedEvent.cs b/srcs/WingsAPI.Game/Families/Event/FamilyWarehouseItemPlacedEvent.cs new file mode 100644 index 0000000..8df1c5f --- /dev/null +++ b/srcs/WingsAPI.Game/Families/Event/FamilyWarehouseItemPlacedEvent.cs @@ -0,0 +1,11 @@ +using WingsEmu.DTOs.Items; +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Families.Event; + +public class FamilyWarehouseItemPlacedEvent : PlayerEvent +{ + public ItemInstanceDTO ItemInstance { get; init; } + public int Amount { get; init; } + public short DestinationSlot { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Families/Event/FamilyWarehouseItemWithdrawnEvent.cs b/srcs/WingsAPI.Game/Families/Event/FamilyWarehouseItemWithdrawnEvent.cs new file mode 100644 index 0000000..950874c --- /dev/null +++ b/srcs/WingsAPI.Game/Families/Event/FamilyWarehouseItemWithdrawnEvent.cs @@ -0,0 +1,11 @@ +using WingsEmu.DTOs.Items; +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Families.Event; + +public class FamilyWarehouseItemWithdrawnEvent : PlayerEvent +{ + public ItemInstanceDTO ItemInstance { get; init; } + public int Amount { get; init; } + public short FromSlot { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Families/Event/FamilyWarehouseLogsOpenEvent.cs b/srcs/WingsAPI.Game/Families/Event/FamilyWarehouseLogsOpenEvent.cs new file mode 100644 index 0000000..0806b74 --- /dev/null +++ b/srcs/WingsAPI.Game/Families/Event/FamilyWarehouseLogsOpenEvent.cs @@ -0,0 +1,8 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Families.Event; + +public class FamilyWarehouseLogsOpenEvent : PlayerEvent +{ + public bool Refresh { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Families/Event/FamilyWarehouseMoveItemEvent.cs b/srcs/WingsAPI.Game/Families/Event/FamilyWarehouseMoveItemEvent.cs new file mode 100644 index 0000000..ea3c1a1 --- /dev/null +++ b/srcs/WingsAPI.Game/Families/Event/FamilyWarehouseMoveItemEvent.cs @@ -0,0 +1,12 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Families.Event; + +public class FamilyWarehouseMoveItemEvent : PlayerEvent +{ + public short OldSlot { get; init; } + + public short Amount { get; init; } + + public short NewSlot { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Families/Event/FamilyWarehouseOpenEvent.cs b/srcs/WingsAPI.Game/Families/Event/FamilyWarehouseOpenEvent.cs new file mode 100644 index 0000000..f43d591 --- /dev/null +++ b/srcs/WingsAPI.Game/Families/Event/FamilyWarehouseOpenEvent.cs @@ -0,0 +1,7 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Families.Event; + +public class FamilyWarehouseOpenEvent : PlayerEvent +{ +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Families/Event/FamilyWarehouseShowItemEvent.cs b/srcs/WingsAPI.Game/Families/Event/FamilyWarehouseShowItemEvent.cs new file mode 100644 index 0000000..1e81faf --- /dev/null +++ b/srcs/WingsAPI.Game/Families/Event/FamilyWarehouseShowItemEvent.cs @@ -0,0 +1,8 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Families.Event; + +public class FamilyWarehouseShowItemEvent : PlayerEvent +{ + public short Slot { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Families/Event/FamilyWarehouseWithdrawItemEvent.cs b/srcs/WingsAPI.Game/Families/Event/FamilyWarehouseWithdrawItemEvent.cs new file mode 100644 index 0000000..9041240 --- /dev/null +++ b/srcs/WingsAPI.Game/Families/Event/FamilyWarehouseWithdrawItemEvent.cs @@ -0,0 +1,9 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Families.Event; + +public class FamilyWarehouseWithdrawItemEvent : PlayerEvent +{ + public short Slot { get; init; } + public short Amount { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Families/ExperienceGainedSubMessage.cs b/srcs/WingsAPI.Game/Families/ExperienceGainedSubMessage.cs new file mode 100644 index 0000000..4f0a766 --- /dev/null +++ b/srcs/WingsAPI.Game/Families/ExperienceGainedSubMessage.cs @@ -0,0 +1,23 @@ +using System; +using WingsEmu.Game.Families.Enum; + +namespace WingsEmu.Game.Families; + +public class ExperienceGainedSubMessage +{ + public ExperienceGainedSubMessage(long characterId, long famXpGained, FamXpObtainedFromType famXpGainedFromType, DateTime dateOfReward) + { + CharacterId = characterId; + FamXpGained = famXpGained; + FamXpGainedFromType = famXpGainedFromType; + DateOfReward = dateOfReward; + } + + public long CharacterId { get; } + + public long FamXpGained { get; } + + public FamXpObtainedFromType FamXpGainedFromType { get; } + + public DateTime DateOfReward { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Families/Family.cs b/srcs/WingsAPI.Game/Families/Family.cs new file mode 100644 index 0000000..5ca5643 --- /dev/null +++ b/srcs/WingsAPI.Game/Families/Family.cs @@ -0,0 +1,148 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using System.Linq; +using WingsAPI.Data.Families; +using WingsAPI.Packets.Enums.Families; +using WingsEmu.Game.Managers; +using WingsEmu.Packets.Enums.Character; +using WingsEmu.Packets.Enums.Families; + +namespace WingsEmu.Game.Families; + +public class Family : IFamily +{ + public Family(FamilyDTO input, IReadOnlyCollection members, List logs, ISessionManager sessionManager) + { + Members = new List(); + Logs = logs ?? new List(); + Upgrades = new Dictionary(); + UpgradeValues = new Dictionary(); + Achievements = new Dictionary(); + AchievementProgress = new Dictionary(); + Mission = new Dictionary(); + if (input == null || members == null) + { + return; + } + + FamilyMembership head = null; + + foreach (FamilyMembershipDto membershipDto in members) + { + var membership = new FamilyMembership(membershipDto, sessionManager); + Members.Add(membership); + + if (membership.Authority == FamilyAuthority.Head) + { + head = membership; + } + } + + Experience = input.Experience; + Head = head; + HeadGender = input.HeadGender; + Id = input.Id; + Level = input.Level; + Message = input.Message; + Faction = input.Faction; + AssistantWarehouseAuthorityType = input.AssistantWarehouseAuthorityType; + AssistantCanGetHistory = input.AssistantCanGetHistory; + AssistantCanInvite = input.AssistantCanInvite; + AssistantCanNotice = input.AssistantCanNotice; + AssistantCanShout = input.AssistantCanShout; + MemberWarehouseAuthorityType = input.MemberWarehouseAuthorityType; + MemberCanGetHistory = input.MemberCanGetHistory; + Name = input.Name; + + + if (input.Upgrades != null) + { + if (input.Upgrades.UpgradesBought != null) + { + foreach (int upgradeId in input.Upgrades.UpgradesBought) + { + Upgrades[upgradeId] = new FamilyUpgrade { Id = upgradeId, State = FamilyUpgradeState.PASSIVE }; + } + } + + if (input.Upgrades.UpgradeValues != null) + { + foreach ((FamilyUpgradeType upgradeType, short value) in input.Upgrades.UpgradeValues) + { + UpgradeValues[upgradeType] = value; + } + } + } + + if (input.Achievements?.Achievements != null && input.Achievements.Achievements.Any()) + { + foreach ((int achievementId, FamilyAchievementCompletionDto achievement) in input.Achievements.Achievements) + { + Achievements[achievementId] = achievement; + } + } + + if (input.Achievements?.Progress != null && input.Achievements.Progress.Any()) + { + foreach ((int achievementId, FamilyAchievementProgressDto achievement) in input.Achievements.Progress) + { + AchievementProgress[achievementId] = achievement; + } + } + + if (input.Missions?.Missions != null && input.Missions.Missions.Any()) + { + foreach ((int achievementId, FamilyMissionDto achievement) in input.Missions.Missions) + { + Mission[achievementId] = achievement; + } + } + } + + public long Id { get; set; } + + public string Name { get; set; } + + public byte Level { get; set; } + + public long Experience { get; set; } + + public byte Faction { get; set; } + + public GenderType HeadGender { get; set; } + + public string Message { get; set; } + + public FamilyWarehouseAuthorityType AssistantWarehouseAuthorityType { get; set; } + + public FamilyWarehouseAuthorityType MemberWarehouseAuthorityType { get; set; } + + public bool AssistantCanGetHistory { get; set; } + public bool AssistantCanInvite { get; set; } + public bool AssistantCanNotice { get; set; } + + public bool AssistantCanShout { get; set; } + public bool MemberCanGetHistory { get; set; } + + public List Members { get; } + + public List Logs { get; } + + public FamilyMembership Head { get; set; } + + public Dictionary UpgradeValues { get; } + public Dictionary Mission { get; } + + public int GetMaximumMembershipCapacity() => UpgradeValues != null && UpgradeValues.TryGetValue(FamilyUpgradeType.INCREASE_FAMILY_MEMBERS_LIMIT, out short capacity) ? capacity : 50; + + public int GetWarehouseCapacity() => UpgradeValues != null && UpgradeValues.TryGetValue(FamilyUpgradeType.INCREASE_FAMILY_WAREHOUSE, out short capacity) ? capacity : 0; + + public bool HasAlreadyBoughtUpgrade(int upgradeId) => Upgrades != null && Upgrades.ContainsKey(upgradeId); + + public Dictionary Upgrades { get; } + public Dictionary Achievements { get; set; } + public Dictionary AchievementProgress { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Families/FamilyAchievementsVnum.cs b/srcs/WingsAPI.Game/Families/FamilyAchievementsVnum.cs new file mode 100644 index 0000000..3c5ed50 --- /dev/null +++ b/srcs/WingsAPI.Game/Families/FamilyAchievementsVnum.cs @@ -0,0 +1,75 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Game.Families; + +public enum FamilyAchievementsVnum +{ + FAMILY_LEVEL_2_UNLOCKED = 9018, + FAMILY_LEVEL_3_UNLOCKED = 9019, + FAMILY_LEVEL_4_UNLOCKED = 9020, + FAMILY_LEVEL_5_UNLOCKED = 9021, + FAMILY_LEVEL_6_UNLOCKED = 9022, + FAMILY_LEVEL_7_UNLOCKED = 9023, + FAMILY_LEVEL_8_UNLOCKED = 9024, + FAMILY_LEVEL_9_UNLOCKED = 9025, + FAMILY_LEVEL_10_UNLOCKED = 9026, + FAMILY_LEVEL_11_UNLOCKED = 9027, + FAMILY_LEVEL_12_UNLOCKED = 9028, + FAMILY_LEVEL_13_UNLOCKED = 9029, + FAMILY_LEVEL_14_UNLOCKED = 9030, + FAMILY_LEVEL_15_UNLOCKED = 9031, + FAMILY_LEVEL_16_UNLOCKED = 9032, + FAMILY_LEVEL_17_UNLOCKED = 9033, + FAMILY_LEVEL_18_UNLOCKED = 9034, + FAMILY_LEVEL_19_UNLOCKED = 9035, + FAMILY_LEVEL_20_UNLOCKED = 9036, + + + ENTER_20_QUOTES_OF_THE_DAY = 9037, + ENTER_100_QUOTES_OF_THE_DAY = 9038, + ENTER_300_QUOTES_OF_THE_DAY = 9039, + ENTER_1000_QUOTES_OF_THE_DAY = 9040, + ENTER_3000_QUOTES_OF_THE_DAY = 9041, + + COMPLETE_10_RAINBOW_BATTLE = 9042, + COMPLETE_100_RAINBOW_BATTLE = 9043, + COMPLETE_300_RAINBOW_BATTLE = 9044, + COMPLETE_1000_RAINBOW_BATTLE = 9045, + COMPLETE_3000_RAINBOW_BATTLE = 9046, + + WIN_5_RAINBOW_BATTLE = 9047, + WIN_50_RAINBOW_BATTLE = 9048, + WIN_150_RAINBOW_BATTLE = 9049, + WIN_500_RAINBOW_BATTLE = 9050, + WIN_1500_RAINBOW_BATTLE = 9051, + + DEFEAT_ANY_ACT4_DUNGEON_10_TIMES = 9052, + DEFEAT_ANY_ACT4_DUNGEON_100_TIMES = 9053, + DEFEAT_ANY_ACT4_DUNGEON_1000_TIMES = 9054, + + DEFEAT_MORCOS_ACT4_DUNGEON_1_TIME = 9055, + DEFEAT_MORCOS_ACT4_DUNGEON_10_TIMES = 9056, + DEFEAT_MORCOS_ACT4_DUNGEON_30_TIMES = 9057, + DEFEAT_MORCOS_ACT4_DUNGEON_100_TIMES = 9058, + DEFEAT_MORCOS_ACT4_DUNGEON_300_TIMES = 9059, + + DEFEAT_HATUS_ACT4_DUNGEON_1_TIME = 9060, + DEFEAT_HATUS_ACT4_DUNGEON_10_TIMES = 9061, + DEFEAT_HATUS_ACT4_DUNGEON_30_TIMES = 9062, + DEFEAT_HATUS_ACT4_DUNGEON_100_TIMES = 9063, + DEFEAT_HATUS_ACT4_DUNGEON_300_TIMES = 9064, + + DEFEAT_CALVINAS_ACT4_DUNGEON_1_TIME = 9065, + DEFEAT_CALVINAS_ACT4_DUNGEON_10_TIMES = 9066, + DEFEAT_CALVINAS_ACT4_DUNGEON_30_TIMES = 9067, + DEFEAT_CALVINAS_ACT4_DUNGEON_100_TIMES = 9068, + DEFEAT_CALVINAS_ACT4_DUNGEON_300_TIMES = 9069, + + DEFEAT_BERIOS_ACT4_DUNGEON_1_TIME = 9070, + DEFEAT_BERIOS_ACT4_DUNGEON_10_TIMES = 9071, + DEFEAT_BERIOS_ACT4_DUNGEON_30_TIMES = 9072, + DEFEAT_BERIOS_ACT4_DUNGEON_100_TIMES = 9073, + DEFEAT_BERIOS_ACT4_DUNGEON_300_TIMES = 9074 +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Families/FamilyComponent.cs b/srcs/WingsAPI.Game/Families/FamilyComponent.cs new file mode 100644 index 0000000..fcec2a9 --- /dev/null +++ b/srcs/WingsAPI.Game/Families/FamilyComponent.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using System.Linq; +using WingsEmu.Packets.Enums.Families; + +namespace WingsEmu.Game.Families; + +public class FamilyComponent : IFamilyComponent +{ + private readonly IFamilyManager _familyManager; + + public FamilyComponent(IFamilyManager familyManager) + { + _familyManager = familyManager; + FamilyMembership = null; + } + + public IFamily Family => FamilyMembership == null ? null : _familyManager.GetFamilyByFamilyId(FamilyMembership.FamilyId); + public FamilyMembership FamilyMembership { get; private set; } + public bool IsFamilyWarehouseOpen { get; set; } + public bool IsFamilyWarehouseLogsOpen { get; set; } + + public void SetFamilyMembership(FamilyMembership membership) => FamilyMembership = membership; + + public bool IsHeadOfFamily() => FamilyMembership != null && GetFamilyAuthority() == FamilyAuthority.Head; + public bool IsInFamily() => FamilyMembership != null && Family != null; + + public List GetFamilyMembers() + { + IFamily family = Family; + return family == null ? new List() : family.Members; + } + + public FamilyAuthority GetFamilyAuthority() => FamilyMembership.Authority; + public FamilyMembership GetMembershipByAuthority(FamilyAuthority familyAuthority) => Family?.Members.FirstOrDefault(x => x.Authority == familyAuthority); + public FamilyMembership GetMembershipById(long id) => Family?.Members.FirstOrDefault(x => x.CharacterId == id); + + public byte GetAmountOfMembersByType(FamilyAuthority type) + { + byte amount = 0; + foreach (FamilyMembership member in Family.Members.Where(member => member.Authority == type)) + { + amount++; + } + + return amount; + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Families/FamilyMembership.cs b/srcs/WingsAPI.Game/Families/FamilyMembership.cs new file mode 100644 index 0000000..2ffdcff --- /dev/null +++ b/srcs/WingsAPI.Game/Families/FamilyMembership.cs @@ -0,0 +1,33 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsAPI.Communication.Player; +using WingsAPI.Data.Families; +using WingsEmu.Game.Managers; + +namespace WingsEmu.Game.Families; + +public class FamilyMembership : FamilyMembershipDto +{ + private readonly ISessionManager _sessionManager; + + public FamilyMembership() + { + } + + public FamilyMembership(FamilyMembershipDto input, ISessionManager sessionManager) + { + _sessionManager = sessionManager; + FamilyId = input.FamilyId; + CharacterId = input.CharacterId; + Authority = input.Authority; + DailyMessage = input.DailyMessage; + Experience = input.Experience; + Title = input.Title; + JoinDate = input.JoinDate; + LastOnlineDate = input.LastOnlineDate; + } + + public ClusterCharacterInfo Character => _sessionManager.GetOnlineCharacterById(CharacterId).ConfigureAwait(false).GetAwaiter().GetResult(); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Families/FamilyMessageType.cs b/srcs/WingsAPI.Game/Families/FamilyMessageType.cs new file mode 100644 index 0000000..6e68bd8 --- /dev/null +++ b/srcs/WingsAPI.Game/Families/FamilyMessageType.cs @@ -0,0 +1,8 @@ +namespace WingsEmu.Game.Families; + +public enum FamilyMessageType +{ + Notice, + Quote, + Shout +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Families/FamilyMissionVnums.cs b/srcs/WingsAPI.Game/Families/FamilyMissionVnums.cs new file mode 100644 index 0000000..10ba15d --- /dev/null +++ b/srcs/WingsAPI.Game/Families/FamilyMissionVnums.cs @@ -0,0 +1,22 @@ +namespace WingsEmu.Game.Families; + +public enum FamilyMissionVnums +{ + DAILY_COMPLETE_10_RAINBOW_BATTLE = 9004, + DAILY_WIN_5_RAINBOW_BATTLE = 9005, + DAILY_DEFEAT_ANY_ACT4_DUNGEON_1_TIME = 9006, + DAILY_DEFEAT_5_CUBY_RAID = 9008, + DAILY_DEFEAT_5_GINSENG_RAID = 9009, + DAILY_DEFEAT_5_CASTRA_RAID = 9010, + DAILY_DEFEAT_5_GIANT_SPIDER_RAID = 9011, + DAILY_DEFEAT_5_GIANT_SLADE_RAID = 9012, + DAILY_DEFEAT_5_ROBBER_GANG_RAID = 9013, + DAILY_DEFEAT_5_KERTOS_RAID = 9014, + DAILY_DEFEAT_5_VALAKUS_RAID = 9015, + DAILY_DEFEAT_5_GRENIGAS_RAID = 9016, + DAILY_DEFEAT_10_INSTANT_BATTLES = 9017, + DAILY_DEFEAT_DUNGEON_BOSS_LESS_10MIN = 9075, + DAILY_DEFEAT_DUNGEON_BOSS_WITHOUT_DYING = 9076, + DAILY_DEFEAT_10_ENEMIES_ACT4 = 9078, + DAILY_DEFEAT_5_NAMAJU_RAID = 9079 +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Families/FamilyUpgrade.cs b/srcs/WingsAPI.Game/Families/FamilyUpgrade.cs new file mode 100644 index 0000000..2141978 --- /dev/null +++ b/srcs/WingsAPI.Game/Families/FamilyUpgrade.cs @@ -0,0 +1,9 @@ +using WingsAPI.Data.Families; + +namespace WingsEmu.Game.Families; + +public class FamilyUpgrade +{ + public int Id { get; set; } + public FamilyUpgradeState State { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Families/FamilyUpgradeBuyFromShopEvent.cs b/srcs/WingsAPI.Game/Families/FamilyUpgradeBuyFromShopEvent.cs new file mode 100644 index 0000000..2b2eef7 --- /dev/null +++ b/srcs/WingsAPI.Game/Families/FamilyUpgradeBuyFromShopEvent.cs @@ -0,0 +1,9 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Families; + +public class FamilyUpgradeBuyFromShopEvent : PlayerEvent +{ + public long NpcId { get; set; } + public short Slot { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Families/FamilyUpgradeBuyableState.cs b/srcs/WingsAPI.Game/Families/FamilyUpgradeBuyableState.cs new file mode 100644 index 0000000..35e3f42 --- /dev/null +++ b/srcs/WingsAPI.Game/Families/FamilyUpgradeBuyableState.cs @@ -0,0 +1,8 @@ +namespace WingsEmu.Game.Families; + +public enum FamilyUpgradeBuyableState +{ + AVAILABLE = 0, + ALREADY_OWNED = 1, + REQUIREMENTS_NOT_MET = 2 +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Families/IFamily.cs b/srcs/WingsAPI.Game/Families/IFamily.cs new file mode 100644 index 0000000..189244b --- /dev/null +++ b/srcs/WingsAPI.Game/Families/IFamily.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using WingsAPI.Data.Families; +using WingsAPI.Packets.Enums.Families; +using WingsEmu.Packets.Enums.Character; + +namespace WingsEmu.Game.Families; + +public interface IFamily +{ + long Id { get; set; } + string Name { get; set; } + byte Level { get; set; } + long Experience { get; set; } + byte Faction { get; set; } + GenderType HeadGender { get; set; } + string Message { get; set; } + FamilyWarehouseAuthorityType AssistantWarehouseAuthorityType { get; set; } + FamilyWarehouseAuthorityType MemberWarehouseAuthorityType { get; set; } + bool AssistantCanGetHistory { get; set; } + bool AssistantCanInvite { get; set; } + bool AssistantCanNotice { get; set; } + bool AssistantCanShout { get; set; } + bool MemberCanGetHistory { get; set; } + List Members { get; } + List Logs { get; } + FamilyMembership Head { get; set; } + Dictionary Upgrades { get; } + Dictionary UpgradeValues { get; } + Dictionary Mission { get; } + Dictionary Achievements { get; } + Dictionary AchievementProgress { get; } + + int GetMaximumMembershipCapacity(); + int GetWarehouseCapacity(); + bool HasAlreadyBoughtUpgrade(int upgradeId); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Families/IFamilyComponent.cs b/srcs/WingsAPI.Game/Families/IFamilyComponent.cs new file mode 100644 index 0000000..abcb860 --- /dev/null +++ b/srcs/WingsAPI.Game/Families/IFamilyComponent.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using WingsEmu.Packets.Enums.Families; + +namespace WingsEmu.Game.Families; + +public interface IFamilyComponent +{ + public IFamily Family { get; } + public FamilyMembership FamilyMembership { get; } + public bool IsFamilyWarehouseOpen { get; set; } + public bool IsFamilyWarehouseLogsOpen { get; set; } + + public void SetFamilyMembership(FamilyMembership membership); + public bool IsHeadOfFamily(); + public bool IsInFamily(); + public List GetFamilyMembers(); + public FamilyAuthority GetFamilyAuthority(); + public FamilyMembership GetMembershipByAuthority(FamilyAuthority familyAuthority); + public FamilyMembership GetMembershipById(long id); + public byte GetAmountOfMembersByType(FamilyAuthority type); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Families/IFamilyManager.cs b/srcs/WingsAPI.Game/Families/IFamilyManager.cs new file mode 100644 index 0000000..91e9dc5 --- /dev/null +++ b/srcs/WingsAPI.Game/Families/IFamilyManager.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using WingsAPI.Data.Families; + +namespace WingsEmu.Game.Families; + +public interface IFamilyManager +{ + void AddFamily(FamilyDTO family, IReadOnlyCollection members); + void RemoveFamily(long familyId); + IFamily GetFamilyByFamilyName(string familyName); + Family GetFamilyByFamilyId(long familyId); + Family GetFamilyByFamilyIdCache(long familyId); + void AddOrReplaceMember(FamilyMembershipDto membership); + void AddOrReplaceMember(FamilyMembershipDto membership, IFamily family); + void RemoveMember(long characterId, long familyId); + void MemberDisconnectionUpdate(long characterId, DateTime disconnectionTime); + FamilyMembership GetFamilyMembershipByCharacterId(long characterId); + void AddToFamilyLogs(IReadOnlyDictionary> logs); + void SendLogToFamilyServer(FamilyLogDto log); + IEnumerable AddToFamilyExperiences(Dictionary exps); + void SendExperienceToFamilyServer(ExperienceGainedSubMessage experienceGainedSubMessage); + Task CanJoinNewFamilyAsync(int playerEntityId); + Task RemovePlayerJoinCooldownAsync(int playerEntityId); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Families/Managers/FamilyAchievementManager.cs b/srcs/WingsAPI.Game/Families/Managers/FamilyAchievementManager.cs new file mode 100644 index 0000000..054fa0b --- /dev/null +++ b/srcs/WingsAPI.Game/Families/Managers/FamilyAchievementManager.cs @@ -0,0 +1,115 @@ +using System; +using System.Collections.Concurrent; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using PhoenixLib.Logging; +using PhoenixLib.ServiceBus; + +namespace Plugin.FamilyImpl.Achievements; + +public class FamilyAchievementManager : BackgroundService, IFamilyAchievementManager, IFamilyMissionManager +{ + private readonly IMessagePublisher _messagePublisher; + private readonly IMessagePublisher _missionMessagePublisher; + private readonly ConcurrentQueue _pendingMessages = new(); + + private readonly ConcurrentQueue _pendingMissionMessages = new(); + + public FamilyAchievementManager(IMessagePublisher messagePublisher, IMessagePublisher missionMessagePublisher) + { + _messagePublisher = messagePublisher; + _missionMessagePublisher = missionMessagePublisher; + } + + private static TimeSpan RefreshDelay => TimeSpan.FromSeconds(Convert.ToInt32(Environment.GetEnvironmentVariable("FAMILY_ACHIEVEMENT_REFRESH_IN_SECONDS") ?? "5")); + + public void IncrementFamilyAchievement(long familyId, int achievementId, int counterToAdd) + { + // later + _pendingMessages.Enqueue(new FamilyAchievementIncrementMessage + { + FamilyId = familyId, + AchievementId = achievementId, + ValueToAdd = counterToAdd + }); + } + + public void IncrementFamilyAchievement(long familyId, int achievementId) + { + IncrementFamilyAchievement(familyId, achievementId, 1); + } + + public void IncrementFamilyMission(long familyId, long? playerId, int missionId, int counterToAdd) + { + _pendingMissionMessages.Enqueue(new FamilyMissionIncrementMessage + { + FamilyId = familyId, + CharacterId = playerId, + MissionId = missionId, + ValueToAdd = counterToAdd + }); + } + + public void IncrementFamilyMission(long familyId, long? playerId, int missionId) + { + IncrementFamilyMission(familyId, playerId, missionId, 1); + } + + public void IncrementFamilyMission(long familyId, int missionId) + { + IncrementFamilyMission(familyId, null, missionId); + } + + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + await FlushPendingAchievementIncrements(); + await FlushPendingMissionIncrements(); + + await Task.Delay(RefreshDelay, stoppingToken); + } + } + + private async Task FlushPendingAchievementIncrements() + { + try + { + if (_pendingMessages.IsEmpty) + { + return; + } + + while (_pendingMessages.TryDequeue(out FamilyAchievementIncrementMessage msg)) + { + await _messagePublisher.PublishAsync(msg); + } + } + catch (Exception e) + { + Log.Error("[FamilyAchievementManager] ExecuteAsync", e); + } + } + + private async Task FlushPendingMissionIncrements() + { + try + { + if (_pendingMissionMessages.IsEmpty) + { + return; + } + + while (_pendingMissionMessages.TryDequeue(out FamilyMissionIncrementMessage msg)) + { + await _missionMessagePublisher.PublishAsync(msg); + } + } + catch (Exception e) + { + Log.Error("[FamilyAchievementManager] ExecuteAsync", e); + } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Families/Managers/IFamilyAchievementManager.cs b/srcs/WingsAPI.Game/Families/Managers/IFamilyAchievementManager.cs new file mode 100644 index 0000000..98b5fa7 --- /dev/null +++ b/srcs/WingsAPI.Game/Families/Managers/IFamilyAchievementManager.cs @@ -0,0 +1,7 @@ +namespace Plugin.FamilyImpl.Achievements; + +public interface IFamilyAchievementManager +{ + void IncrementFamilyAchievement(long familyId, int achievementId, int counterToAdd); + void IncrementFamilyAchievement(long familyId, int achievementId); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Families/Managers/IFamilyMissionManager.cs b/srcs/WingsAPI.Game/Families/Managers/IFamilyMissionManager.cs new file mode 100644 index 0000000..34231be --- /dev/null +++ b/srcs/WingsAPI.Game/Families/Managers/IFamilyMissionManager.cs @@ -0,0 +1,8 @@ +namespace Plugin.FamilyImpl.Achievements; + +public interface IFamilyMissionManager +{ + void IncrementFamilyMission(long familyId, long? playerId, int missionId, int counterToAdd); + void IncrementFamilyMission(long familyId, long? playerId, int missionId); + void IncrementFamilyMission(long familyId, int missionId); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Families/Messages/FamilyAchievementIncrementMessage.cs b/srcs/WingsAPI.Game/Families/Messages/FamilyAchievementIncrementMessage.cs new file mode 100644 index 0000000..629892d --- /dev/null +++ b/srcs/WingsAPI.Game/Families/Messages/FamilyAchievementIncrementMessage.cs @@ -0,0 +1,12 @@ +using PhoenixLib.ServiceBus; +using PhoenixLib.ServiceBus.Routing; + +namespace Plugin.FamilyImpl.Achievements; + +[MessageType("family.achievements.increment")] +public class FamilyAchievementIncrementMessage : IMessage +{ + public long FamilyId { get; set; } + public int AchievementId { get; set; } + public int ValueToAdd { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Families/Messages/FamilyMissionIncrementMessage.cs b/srcs/WingsAPI.Game/Families/Messages/FamilyMissionIncrementMessage.cs new file mode 100644 index 0000000..68fe110 --- /dev/null +++ b/srcs/WingsAPI.Game/Families/Messages/FamilyMissionIncrementMessage.cs @@ -0,0 +1,13 @@ +using PhoenixLib.ServiceBus; +using PhoenixLib.ServiceBus.Routing; + +namespace Plugin.FamilyImpl.Achievements; + +[MessageType("family.mission.increment")] +public class FamilyMissionIncrementMessage : IMessage +{ + public long FamilyId { get; set; } + public long? CharacterId { get; set; } + public int MissionId { get; set; } + public int ValueToAdd { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Families/StaticFamilyManager.cs b/srcs/WingsAPI.Game/Families/StaticFamilyManager.cs new file mode 100644 index 0000000..dbc31de --- /dev/null +++ b/srcs/WingsAPI.Game/Families/StaticFamilyManager.cs @@ -0,0 +1,11 @@ +namespace WingsEmu.Game.Families; + +public static class StaticFamilyManager +{ + public static IFamilyManager Instance { get; private set; } + + public static void Initialize(IFamilyManager generator) + { + Instance = generator; + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Features/GameFeature.cs b/srcs/WingsAPI.Game/Features/GameFeature.cs new file mode 100644 index 0000000..fd2f6e1 --- /dev/null +++ b/srcs/WingsAPI.Game/Features/GameFeature.cs @@ -0,0 +1,8 @@ +namespace WingsEmu.Game.Features; + +public enum GameFeature +{ + Warehouse, + FamilyWarehouse, + PartnerWarehouse +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Features/IGameFeatureToggleManager.cs b/srcs/WingsAPI.Game/Features/IGameFeatureToggleManager.cs new file mode 100644 index 0000000..ef9203e --- /dev/null +++ b/srcs/WingsAPI.Game/Features/IGameFeatureToggleManager.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; + +namespace WingsEmu.Game.Features; + +public interface IGameFeatureToggleManager +{ + Task IsDisabled(GameFeature serviceName); + Task Disable(GameFeature serviceName); + Task Enable(GameFeature serviceName); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Features/RedisGameFeatureToggleManager.cs b/srcs/WingsAPI.Game/Features/RedisGameFeatureToggleManager.cs new file mode 100644 index 0000000..a80528a --- /dev/null +++ b/srcs/WingsAPI.Game/Features/RedisGameFeatureToggleManager.cs @@ -0,0 +1,21 @@ +using System.Threading.Tasks; +using StackExchange.Redis; + +namespace WingsEmu.Game.Features; + +public class RedisGameFeatureToggleManager : IGameFeatureToggleManager +{ + private const string KeyPrefix = "game:disabled-feature"; + + private readonly IDatabase _database; + + public RedisGameFeatureToggleManager(IConnectionMultiplexer multiplexer) => _database = multiplexer.GetDatabase(0); + + public async Task IsDisabled(GameFeature serviceName) => await _database.KeyExistsAsync(CreateKey(serviceName)); + + public async Task Disable(GameFeature serviceName) => await _database.StringSetAsync(CreateKey(serviceName), "disabled"); + + public async Task Enable(GameFeature serviceName) => await _database.KeyDeleteAsync(CreateKey(serviceName)); + + private static string CreateKey(GameFeature serviceName) => $"{KeyPrefix}:{serviceName.ToString().ToLower()}"; +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/GameEvent/Configuration/IGameEventConfiguration.cs b/srcs/WingsAPI.Game/GameEvent/Configuration/IGameEventConfiguration.cs new file mode 100644 index 0000000..9aa1505 --- /dev/null +++ b/srcs/WingsAPI.Game/GameEvent/Configuration/IGameEventConfiguration.cs @@ -0,0 +1,12 @@ +using WingsEmu.Game.Maps; + +namespace WingsEmu.Game.GameEvent.Configuration; + +public interface IGameEventConfiguration +{ + public GameEventType GameEventType { get; } + + public short MapId { get; } + + public MapInstanceType MapInstanceType { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/GameEvent/Configuration/IGlobalGameEventConfiguration.cs b/srcs/WingsAPI.Game/GameEvent/Configuration/IGlobalGameEventConfiguration.cs new file mode 100644 index 0000000..12347e2 --- /dev/null +++ b/srcs/WingsAPI.Game/GameEvent/Configuration/IGlobalGameEventConfiguration.cs @@ -0,0 +1,10 @@ +using WingsEmu.Game.Characters; + +namespace WingsEmu.Game.GameEvent.Configuration; + +public interface IGlobalGameEventConfiguration +{ + IGameEventConfiguration GetConfiguration(IPlayerEntity character); + + public uint GetRegistrationCost(IPlayerEntity character); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/GameEvent/Event/GameEventInstanceProcessEvent.cs b/srcs/WingsAPI.Game/GameEvent/Event/GameEventInstanceProcessEvent.cs new file mode 100644 index 0000000..047d4af --- /dev/null +++ b/srcs/WingsAPI.Game/GameEvent/Event/GameEventInstanceProcessEvent.cs @@ -0,0 +1,11 @@ +using System; +using PhoenixLib.Events; + +namespace WingsEmu.Game.GameEvent.Event; + +public class GameEventInstanceProcessEvent : IAsyncEvent +{ + public GameEventInstanceProcessEvent(DateTime currentTime) => CurrentTime = currentTime; + + public DateTime CurrentTime { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/GameEvent/Event/GameEventInstanceStartEvent.cs b/srcs/WingsAPI.Game/GameEvent/Event/GameEventInstanceStartEvent.cs new file mode 100644 index 0000000..c086f33 --- /dev/null +++ b/srcs/WingsAPI.Game/GameEvent/Event/GameEventInstanceStartEvent.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using PhoenixLib.Events; +using WingsEmu.Game.GameEvent.Configuration; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Game.GameEvent.Event; + +public class GameEventInstanceStartEvent : IAsyncEvent +{ + public GameEventInstanceStartEvent(IEnumerable sessions, IGameEventConfiguration gameEventConfiguration) + { + Sessions = sessions; + GameEventConfiguration = gameEventConfiguration; + } + + public IEnumerable Sessions { get; } + + public IGameEventConfiguration GameEventConfiguration { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/GameEvent/GameEventType.cs b/srcs/WingsAPI.Game/GameEvent/GameEventType.cs new file mode 100644 index 0000000..0df264e --- /dev/null +++ b/srcs/WingsAPI.Game/GameEvent/GameEventType.cs @@ -0,0 +1,6 @@ +namespace WingsEmu.Game.GameEvent; + +public enum GameEventType +{ + InstantBattle = 1 +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/GameEvent/IGameEventInstance.cs b/srcs/WingsAPI.Game/GameEvent/IGameEventInstance.cs new file mode 100644 index 0000000..39c6b10 --- /dev/null +++ b/srcs/WingsAPI.Game/GameEvent/IGameEventInstance.cs @@ -0,0 +1,16 @@ +using System; +using WingsEmu.Game.GameEvent.Configuration; +using WingsEmu.Game.Maps; + +namespace WingsEmu.Game.GameEvent; + +public interface IGameEventInstance +{ + public IGameEventConfiguration Configuration { get; } + + public DateTime DestroyDate { get; } + + public IMapInstance MapInstance { get; } + + public GameEventType GameEventType { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/GameEvent/IGameEventInstanceManager.cs b/srcs/WingsAPI.Game/GameEvent/IGameEventInstanceManager.cs new file mode 100644 index 0000000..6cdb610 --- /dev/null +++ b/srcs/WingsAPI.Game/GameEvent/IGameEventInstanceManager.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace WingsEmu.Game.GameEvent; + +public interface IGameEventInstanceManager +{ + IReadOnlyCollection GetGameEventsByType(GameEventType gameEventType); + void AddGameEvent(IGameEventInstance gameEventInstance); + void RemoveGameEvent(IGameEventInstance gameEventInstance); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/GameEvent/IGameEventRegistrationManager.cs b/srcs/WingsAPI.Game/GameEvent/IGameEventRegistrationManager.cs new file mode 100644 index 0000000..7f10f5e --- /dev/null +++ b/srcs/WingsAPI.Game/GameEvent/IGameEventRegistrationManager.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using WingsEmu.Core.Generics; + +namespace WingsEmu.Game.GameEvent; + +public interface IGameEventRegistrationManager +{ + IReadOnlyCollection> GameEventRegistrations { get; } + + /// + /// Tries to add a registration, in the case of failing it will return false. + /// + /// + /// + /// + /// + bool AddGameEventRegistration(GameEventType gameEventType, DateTime currentTime, DateTime expiryDate); + + void RemoveGameEventRegistration(GameEventType gameEventType); + bool IsGameEventRegistrationOpen(GameEventType gameEventType, DateTime currentTime); + + void SetCharacterGameEventInclination(long id, GameEventType gameEventType); + ThreadSafeHashSet GetAndRemoveCharactersByGameEventInclination(GameEventType gameEventType); + void RemoveCharactersByGameEventInclination(GameEventType gameEventType); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/GameEvent/InstantBattle/InstantBattleWonEvent.cs b/srcs/WingsAPI.Game/GameEvent/InstantBattle/InstantBattleWonEvent.cs new file mode 100644 index 0000000..34e8a4d --- /dev/null +++ b/srcs/WingsAPI.Game/GameEvent/InstantBattle/InstantBattleWonEvent.cs @@ -0,0 +1,11 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.GameEvent.InstantBattle; + +public class InstantBattleWonEvent : PlayerEvent +{ +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/GameEvent/Matchmaking/Filter/FilterResult.cs b/srcs/WingsAPI.Game/GameEvent/Matchmaking/Filter/FilterResult.cs new file mode 100644 index 0000000..5c1ab4a --- /dev/null +++ b/srcs/WingsAPI.Game/GameEvent/Matchmaking/Filter/FilterResult.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Game.GameEvent.Matchmaking.Filter; + +public class FilterResult +{ + public FilterResult(List acceptedSessions, List refusedSessions) + { + AcceptedSessions = acceptedSessions; + RefusedSessions = refusedSessions; + } + + public List AcceptedSessions { get; } + public List RefusedSessions { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/GameEvent/Matchmaking/Filter/IMatchmakingFilter.cs b/srcs/WingsAPI.Game/GameEvent/Matchmaking/Filter/IMatchmakingFilter.cs new file mode 100644 index 0000000..956f0ae --- /dev/null +++ b/srcs/WingsAPI.Game/GameEvent/Matchmaking/Filter/IMatchmakingFilter.cs @@ -0,0 +1,8 @@ +using WingsEmu.Game.Networking; + +namespace WingsEmu.Game.GameEvent.Matchmaking.Filter; + +public interface IMatchmakingFilter +{ + bool IsAccepted(IClientSession session); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/GameEvent/Matchmaking/IMatchmaking.cs b/srcs/WingsAPI.Game/GameEvent/Matchmaking/IMatchmaking.cs new file mode 100644 index 0000000..9ccd1ee --- /dev/null +++ b/srcs/WingsAPI.Game/GameEvent/Matchmaking/IMatchmaking.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using WingsEmu.Game.GameEvent.Matchmaking.Filter; +using WingsEmu.Game.GameEvent.Matchmaking.Result; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Game.GameEvent.Matchmaking; + +public interface IMatchmaking +{ + IMatchmakingResult Matchmake(List sessions, GameEventType type); + FilterResult Filter(List sessions, params IMatchmakingFilter[] filters); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/GameEvent/Matchmaking/Matchmaker/IMatchmaker.cs b/srcs/WingsAPI.Game/GameEvent/Matchmaking/Matchmaker/IMatchmaker.cs new file mode 100644 index 0000000..e73e07c --- /dev/null +++ b/srcs/WingsAPI.Game/GameEvent/Matchmaking/Matchmaker/IMatchmaker.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using WingsEmu.Game.GameEvent.Matchmaking.Result; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Game.GameEvent.Matchmaking.Matchmaker; + +public interface IMatchmaker +{ + IMatchmakingResult Matchmake(List sessions); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/GameEvent/Matchmaking/Result/IMatchmakingResult.cs b/srcs/WingsAPI.Game/GameEvent/Matchmaking/Result/IMatchmakingResult.cs new file mode 100644 index 0000000..2a3d7f0 --- /dev/null +++ b/srcs/WingsAPI.Game/GameEvent/Matchmaking/Result/IMatchmakingResult.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using WingsEmu.Game._i18n; +using WingsEmu.Game.GameEvent.Configuration; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Game.GameEvent.Matchmaking.Result; + +public interface IMatchmakingResult +{ + public List>> Sessions { get; } + + public Dictionary> RefusedSessions { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Generics/ThreadSafeHashSet.cs b/srcs/WingsAPI.Game/Generics/ThreadSafeHashSet.cs new file mode 100644 index 0000000..0bcab17 --- /dev/null +++ b/srcs/WingsAPI.Game/Generics/ThreadSafeHashSet.cs @@ -0,0 +1,145 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Threading; + +namespace WingsEmu.Core.Generics; + +public sealed class ThreadSafeHashSet : IEnumerable, IDisposable +{ + private readonly HashSet _hashSet = new(); + private readonly ReaderWriterLockSlim _lock = new(LockRecursionPolicy.SupportsRecursion); + + public int Count + { + get + { + _lock.EnterReadLock(); + try + { + return _hashSet.Count; + } + finally + { + if (_lock.IsReadLockHeld) + { + _lock.ExitReadLock(); + } + } + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + public IEnumerator GetEnumerator() + { + _lock.EnterReadLock(); + try + { + return _hashSet.GetEnumerator(); + } + finally + { + if (_lock.IsReadLockHeld) + { + _lock.ExitReadLock(); + } + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public bool Add(T item) + { + _lock.EnterWriteLock(); + try + { + return _hashSet.Add(item); + } + finally + { + if (_lock.IsWriteLockHeld) + { + _lock.ExitWriteLock(); + } + } + } + + public void UnionWith(IEnumerable item) + { + _lock.EnterWriteLock(); + try + { + _hashSet.UnionWith(item); + } + finally + { + if (_lock.IsWriteLockHeld) + { + _lock.ExitWriteLock(); + } + } + } + + public void Clear() + { + _lock.EnterWriteLock(); + try + { + _hashSet.Clear(); + } + finally + { + if (_lock.IsWriteLockHeld) + { + _lock.ExitWriteLock(); + } + } + } + + public bool Contains(T item) + { + _lock.EnterReadLock(); + try + { + return _hashSet.Contains(item); + } + finally + { + if (_lock.IsReadLockHeld) + { + _lock.ExitReadLock(); + } + } + } + + public bool Remove(T item) + { + _lock.EnterWriteLock(); + try + { + return _hashSet.Remove(item); + } + finally + { + if (_lock.IsWriteLockHeld) + { + _lock.ExitWriteLock(); + } + } + } + + private void Dispose(bool disposing) + { + if (disposing) + { + _lock?.Dispose(); + } + } + + ~ThreadSafeHashSet() => Dispose(false); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Generics/ThreadSafeList.cs b/srcs/WingsAPI.Game/Generics/ThreadSafeList.cs new file mode 100644 index 0000000..012d040 --- /dev/null +++ b/srcs/WingsAPI.Game/Generics/ThreadSafeList.cs @@ -0,0 +1,666 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using PhoenixLib.Logging; + +namespace WingsEmu.Core.Generics; + +public class ThreadSafeList : IEnumerable, IDisposable +{ + #region Instantiation + + /// + /// Creates a new ThreadSafeList object. + /// + public ThreadSafeList() + { + _items = new List(); + _lock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); + } + + #endregion + + #region Properties + + /// + /// Gets the number of elements contained in the . + /// + public int Count + { + get + { + if (!_disposed) + { + _lock.EnterReadLock(); + try + { + return _items.Count; + } + finally + { + _lock.ExitReadLock(); + } + } + + return 0; + } + } + + #endregion + + public IEnumerator GetEnumerator() + { + _lock.EnterReadLock(); + try + { + return _items.GetEnumerator(); + } + finally + { + if (_lock.IsReadLockHeld) + { + _lock.ExitReadLock(); + } + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + ~ThreadSafeList() => Dispose(false); + + #region Members + + /// + /// private collection to store _items. + /// + private readonly List _items; + + /// + /// Used to synchronize access to _items list. + /// + private readonly ReaderWriterLockSlim _lock; + + private bool _disposed; + + #endregion + + #region Methods + + /// + /// Adds an object to the end of the . + /// + /// + public void Add(T value) + { + if (!_disposed) + { + _lock.EnterWriteLock(); + try + { + _items.Add(value); + } + finally + { + _lock.ExitWriteLock(); + } + } + } + + /// + /// Adds the elements of the specified collection to the end of the . + /// + /// + public void AddRange(List value) + { + if (!_disposed) + { + _lock.EnterWriteLock(); + try + { + _items.AddRange(value); + } + finally + { + _lock.ExitWriteLock(); + } + } + } + + /// + /// Determines whether all elements of a sequence satisfy a condition. + /// + /// + /// True; if elements satisfy the condition + public bool All(Func predicate) + { + if (!_disposed) + { + _lock.EnterReadLock(); + try + { + return _items.All(predicate); + } + finally + { + _lock.ExitReadLock(); + } + } + + return false; + } + + /// + /// Determines whether any element of a sequence satisfies a condition. + /// + /// + /// + /// + /// + public bool Any(Func predicate) + { + if (!_disposed) + { + _lock.EnterReadLock(); + try + { + return _items.Any(predicate); + } + finally + { + _lock.ExitReadLock(); + } + } + + return false; + } + + /// + /// Removes all elements from the . + /// + public void Clear() + { + if (!_disposed) + { + _lock.EnterWriteLock(); + try + { + _items.Clear(); + } + finally + { + _lock.ExitWriteLock(); + } + } + } + + /// + /// Copies the entire to a compatible one-dimensional array, starting at the + /// beginning of the target array. + /// + /// + public void CopyTo(T[] grpmembers) + { + if (!_disposed) + { + _lock.EnterReadLock(); + try + { + _items.CopyTo(grpmembers); + } + finally + { + _lock.ExitReadLock(); + } + } + } + + /// + /// Returns a number that represents how many elements in the specified sequence satisfy a condition. + /// + /// + /// number of found elements + public int CountLinq(Func predicate) + { + if (!_disposed) + { + _lock.EnterReadLock(); + try + { + return _items.Count(predicate); + } + finally + { + _lock.ExitReadLock(); + } + } + + return 0; + } + + /// + /// Disposes the current object. + /// + public void Dispose() + { + if (!_disposed) + { + _disposed = true; + Dispose(true); + GC.SuppressFinalize(this); + } + } + + /// + /// Returns the element at given index + /// + /// + /// object + public T ElementAt(int v) + { + if (_disposed) + { + return default; + } + + _lock.EnterReadLock(); + try + { + return _items.ElementAt(v); + } + catch (Exception ex) + { + Log.Error("ElementAt", ex); + } + finally + { + _lock.ExitReadLock(); + } + + return default; + } + + /// + /// Searches for an element that matches the conditions defined by the specified predicate, + /// and returns the first occurrence within the entire . + /// + /// + /// object + public T Find(Predicate predicate) + { + if (!_disposed) + { + _lock.EnterReadLock(); + try + { + return _items.Find(predicate); + } + finally + { + _lock.ExitReadLock(); + } + } + + return default; + } + + /// + /// Searches for an element that matches the conditions defined by the specified predicate, + /// and returns the first occurrence within the entire . + /// + /// object + public T FirstOrDefault() + { + if (!_disposed) + { + _lock.EnterReadLock(); + try + { + return _items.FirstOrDefault(); + } + finally + { + _lock.ExitReadLock(); + } + } + + return default; + } + + /// + /// Performs the specified action on each element of the . + /// + /// + public void ForEach(Action action) + { + if (!_disposed) + { + _lock.EnterReadLock(); + try + { + _items.ForEach(action); + } + finally + { + _lock.ExitReadLock(); + } + } + } + + /// + /// returns a list of all objects in current thread safe generic list + /// + /// + /// + /// + public List GetAllItems() + { + if (!_disposed) + { + _lock.EnterReadLock(); + try + { + return new List(_items); + } + finally + { + _lock.ExitReadLock(); + } + } + + return new List(); + } + + /// + /// Returns the last element of a sequence that satisfies a condition or a default value if + /// no such element is found. + /// + /// + /// object + public T LastOrDefault(Func predicate) + { + if (!_disposed) + { + _lock.EnterReadLock(); + try + { + return _items.LastOrDefault(predicate); + } + finally + { + _lock.ExitReadLock(); + } + } + + return default; + } + + /// + /// Removes the first occurrence of a specific object from the . + /// + /// + public void Remove(T match) + { + if (!_disposed) + { + _lock.EnterWriteLock(); + try + { + _items.Remove(match); + } + finally + { + _lock.ExitWriteLock(); + } + } + } + + /// + /// Removes all the elements that match the conditions defined by the specified predicate. + /// + /// + public void RemoveAll(Predicate match) + { + if (!_disposed) + { + _lock.EnterWriteLock(); + try + { + _items.RemoveAll(match); + } + finally + { + _lock.ExitWriteLock(); + } + } + } + + /// + /// Returns the only element of a sequence that satisfies a specified condition, and throws + /// an exception if more than one such element exists. + /// + /// + /// object + public T Single(Func predicate) + { + if (!_disposed) + { + _lock.EnterReadLock(); + try + { + return _items.Single(predicate); + } + finally + { + _lock.ExitReadLock(); + } + } + + return default; + } + + /// + /// Returns the only element of a sequence that satisfies a specified condition or a default + /// value if no such element exists; this method throws an exception if more than one element + /// satisfies the condition. + /// + /// + /// object + /// + public T SingleOrDefault(Func predicate) + { + if (!_disposed) + { + _lock.EnterReadLock(); + try + { + return _items.SingleOrDefault(predicate); + } + finally + { + _lock.ExitReadLock(); + } + } + + return default; + } + + /// + /// Returns a number that represents how many elements in the specified sequence satisfy a condition. + /// + /// + /// number of found elements + public int Sum(Func selector) + { + if (_disposed) + { + return 0; + } + + _lock.EnterReadLock(); + try + { + return _items.Sum(selector); + } + finally + { + _lock.ExitReadLock(); + } + + return 0; + } + + /// + /// Returns a number that represents how many elements in the specified sequence satisfy a condition. + /// + /// + /// int? number of found elements + public int? Sum(Func selector) + { + if (!_disposed) + { + _lock.EnterReadLock(); + try + { + return _items.Sum(selector); + } + finally + { + _lock.ExitReadLock(); + } + } + + return 0; + } + + /// + /// Returns a number that represents how many elements in the specified sequence satisfy a condition. + /// + /// + /// number of found elements + public long Sum(Func selector) + { + if (_disposed) + { + return 0; + } + + _lock.EnterReadLock(); + try + { + return _items.Sum(selector); + } + finally + { + _lock.ExitReadLock(); + } + } + + /// + /// Returns a number that represents how many elements in the specified sequence satisfy a condition. + /// + /// + /// long? number of found elements + public long? Sum(Func selector) + { + if (_disposed) + { + return 0; + } + + _lock.EnterReadLock(); + try + { + return _items.Sum(selector); + } + finally + { + _lock.ExitReadLock(); + } + } + + /// + /// Returns a number that represents how many elements in the specified sequence satisfy a condition. + /// + /// + /// number of found elements + public double Sum(Func selector) + { + if (_disposed) + { + return 0; + } + + _lock.EnterReadLock(); + try + { + return _items.Sum(selector); + } + finally + { + _lock.ExitReadLock(); + } + } + + /// + /// Returns a number that represents how many elements in the specified sequence satisfy a condition. + /// + /// + /// double? number of found elements + public double? Sum(Func selector) + { + if (_disposed) + { + return 0; + } + + _lock.EnterReadLock(); + try + { + return _items.Sum(selector); + } + finally + { + _lock.ExitReadLock(); + } + } + + /// + /// Filters a sequence of values based on a predicate. + /// + /// + /// + /// + /// + public List Where(Func predicate) + { + if (_disposed) + { + return new List(); + } + + _lock.EnterReadLock(); + try + { + return _items.Where(predicate).ToList(); + } + finally + { + _lock.ExitReadLock(); + } + } + + protected virtual void Dispose(bool disposing) + { + if (!disposing) + { + return; + } + + Clear(); + _lock.Dispose(); + } + + #endregion +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Generics/ThreadSafeSortedList.cs b/srcs/WingsAPI.Game/Generics/ThreadSafeSortedList.cs new file mode 100644 index 0000000..1579cc2 --- /dev/null +++ b/srcs/WingsAPI.Game/Generics/ThreadSafeSortedList.cs @@ -0,0 +1,786 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; + +namespace WingsEmu.Core.Generics; + +/// +/// This class is used to store key-value based items in a thread safe manner. It uses +/// publicly. +/// +/// Key type +/// Value type +public class ThreadSafeSortedList : IDisposable +{ + #region Instantiation + + /// + /// Creates a new ThreadSafeSortedList object. + /// + public ThreadSafeSortedList() + { + _items = new SortedList(); + _lock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion); + } + + #endregion + + #region Properties + + /// + /// Gets count of items in the collection. + /// + public int Count + { + get + { + if (_disposed) + { + return 0; + } + + _lock.EnterReadLock(); + try + { + return _items.Count; + } + finally + { + _lock.ExitReadLock(); + } + + return 0; + } + } + + #endregion + + #region Indexers + + /// + /// Gets/adds/replaces an item by key. + /// + /// Key to get/set value + /// Item associated with this key + public TV this[TK key] + { + get + { + if (_disposed) + { + return default; + } + + _lock.EnterReadLock(); + try + { + return _items.ContainsKey(key) ? _items[key] : default; + } + finally + { + _lock.ExitReadLock(); + } + } + + set + { + if (_disposed) + { + return; + } + + _lock.EnterWriteLock(); + try + { + _items[key] = value; + } + finally + { + _lock.ExitWriteLock(); + } + } + } + + #endregion + + #region Members + + /// + /// private collection to store _items. + /// + private readonly SortedList _items; + + /// + /// Used to synchronize access to _items list. + /// + private readonly ReaderWriterLockSlim _lock; + + private bool _disposed; + + #endregion + + #region Methods + + /// + /// Determines whether all elements of a sequence satisfy a condition. + /// + /// + /// True; if elements satisfy the condition + public bool All(Func predicate) + { + if (_disposed) + { + return false; + } + + _lock.EnterReadLock(); + try + { + return _items.Values.All(predicate); + } + finally + { + _lock.ExitReadLock(); + } + } + + /// + /// Determines whether any element of a sequence satisfies a condition. + /// + /// + /// + /// + /// + public bool Any(Func predicate) + { + if (_disposed) + { + return false; + } + + _lock.EnterReadLock(); + try + { + return _items.Values.Any(predicate); + } + finally + { + _lock.ExitReadLock(); + } + } + + /// + /// Removes all items from list. + /// + public void ClearAll() + { + if (_disposed) + { + return; + } + + _lock.EnterWriteLock(); + try + { + _items.Clear(); + } + finally + { + _lock.ExitWriteLock(); + } + } + + /// + /// Checks if collection contains spesified key. + /// + /// Key to check + /// True; if collection contains given key + public bool ContainsKey(TK key) + { + if (_disposed) + { + return false; + } + + _lock.EnterReadLock(); + try + { + return _items.ContainsKey(key); + } + finally + { + _lock.ExitReadLock(); + } + } + + /// + /// Checks if collection contains spesified item. + /// + /// Item to check + /// True; if collection contains given item + public bool ContainsValue(TV item) + { + if (_disposed) + { + return false; + } + + _lock.EnterReadLock(); + try + { + return _items.ContainsValue(item); + } + finally + { + _lock.ExitReadLock(); + } + } + + /// + /// Returns a number that represents how many elements in the specified sequence satisfy a condition. + /// + /// + /// number of found elements + public int CountLinq(Func predicate) + { + if (_disposed) + { + return 0; + } + + _lock.EnterReadLock(); + try + { + return _items.Values.Count(predicate); + } + finally + { + _lock.ExitReadLock(); + } + + return 0; + } + + /// + /// Disposes the current object. + /// + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Returns the first element of the sequence that satisfies a condition or a default value + /// if no such element is found. + /// + /// + /// object + public TV FirstOrDefault(Func predicate) + { + if (_disposed) + { + return default; + } + + _lock.EnterReadLock(); + try + { + return _items.Values.FirstOrDefault(predicate); + } + finally + { + _lock.ExitReadLock(); + } + + return default; + } + + /// + /// Performs the specified action on each element of the . + /// + /// + public void ForEach(Action action) + { + if (_disposed) + { + return; + } + + _lock.EnterReadLock(); + try + { + _items.Values.ToList().ForEach(action); + } + finally + { + _lock.ExitReadLock(); + } + } + + /// + /// Gets all items in collection. + /// + /// + /// + /// + public List GetAllItems() + { + if (_disposed) + { + return new List(); + } + + _lock.EnterReadLock(); + try + { + return new List(_items.Values); + } + finally + { + _lock.ExitReadLock(); + } + } + + /// + /// Gets then removes all items in collection. + /// + /// + /// + /// + public List GetAndClearAllItems() + { + if (_disposed) + { + return new List(); + } + + _lock.EnterWriteLock(); + try + { + var list = new List(_items.Values); + _items.Clear(); + return list; + } + finally + { + _lock.ExitWriteLock(); + } + } + + /// + /// Returns the last element of a sequence that satisfies a specified condition. + /// + /// + /// object + public TV Last(Func predicate) + { + if (_disposed) + { + return default; + } + + _lock.EnterReadLock(); + try + { + return _items.Values.Last(predicate); + } + finally + { + _lock.ExitReadLock(); + } + + return default; + } + + /// + /// Returns the last element of a sequence. + /// + /// object + public TV Last() + { + if (_disposed) + { + return default; + } + + _lock.EnterReadLock(); + try + { + return _items.Values.Last(); + } + finally + { + _lock.ExitReadLock(); + } + + return default; + } + + /// + /// Returns the last element of a sequence that satisfies a condition or a default value if + /// no such element is found. + /// + /// + /// object + public TV LastOrDefault(Func predicate) + { + if (_disposed) + { + return default; + } + + _lock.EnterReadLock(); + try + { + return _items.Values.LastOrDefault(predicate); + } + finally + { + _lock.ExitReadLock(); + } + + return default; + } + + /// + /// Returns the last element of a sequence, or a default value if the sequence contains no elements. + /// + /// object + public TV LastOrDefault() + { + if (_disposed) + { + return default; + } + + _lock.EnterReadLock(); + try + { + return _items.Values.LastOrDefault(); + } + finally + { + _lock.ExitReadLock(); + } + + return default; + } + + /// + /// Removes an item from collection. + /// + /// Key of item to remove + /// if removed + public bool Remove(TK key) + { + if (_disposed) + { + return false; + } + + _lock.EnterWriteLock(); + try + { + if (!_items.ContainsKey(key)) + { + return false; + } + + _items.Remove(key); + return true; + } + finally + { + _lock.ExitWriteLock(); + } + } + + /// + /// Removes an item from collection. + /// + /// Value of item to remove + /// if removed + public bool Remove(TV value) + { + if (_disposed) + { + return false; + } + + _lock.EnterWriteLock(); + try + { + if (!_items.ContainsValue(value)) + { + return false; + } + + _items.RemoveAt(_items.IndexOfValue(value)); + return true; + } + finally + { + _lock.ExitWriteLock(); + } + + return false; + } + + /// + /// Projects each element of a sequence into a new form. + /// + /// + /// + public IEnumerable Select(Func selector) + { + if (_disposed) + { + return default; + } + + _lock.EnterReadLock(); + try + { + return _items.Values.Select(selector); + } + finally + { + _lock.ExitReadLock(); + } + } + + /// + /// Returns the only element of a sequence that satisfies a specified condition, and throws + /// an exception if more than one such element exists. + /// + /// + /// object + public TV Single(Func predicate) + { + if (_disposed) + { + return default; + } + + _lock.EnterReadLock(); + try + { + return _items.Values.Single(predicate); + } + finally + { + _lock.ExitReadLock(); + } + } + + /// + /// Returns the only element of a sequence that satisfies a specified condition or a default + /// value if no such element exists; this method throws an exception if more than one element + /// satisfies the condition. + /// + /// + /// object + public TV SingleOrDefault(Func predicate) + { + if (!_disposed) + { + _lock.EnterReadLock(); + try + { + return _items.Values.SingleOrDefault(predicate); + } + finally + { + _lock.ExitReadLock(); + } + } + + return default; + } + + /// + /// Returns a number that represents how many elements in the specified sequence satisfy a condition. + /// + /// + /// number of found elements + public int Sum(Func selector) + { + if (!_disposed) + { + _lock.EnterReadLock(); + try + { + return _items.Values.Sum(selector); + } + finally + { + _lock.ExitReadLock(); + } + } + + return 0; + } + + /// + /// Returns a number that represents how many elements in the specified sequence satisfy a condition. + /// + /// + /// int? number of found elements + public int? Sum(Func selector) + { + if (!_disposed) + { + _lock.EnterReadLock(); + try + { + return _items.Values.Sum(selector); + } + finally + { + _lock.ExitReadLock(); + } + } + + return 0; + } + + /// + /// Returns a number that represents how many elements in the specified sequence satisfy a condition. + /// + /// + /// number of found elements + public long Sum(Func selector) + { + if (!_disposed) + { + _lock.EnterReadLock(); + try + { + return _items.Values.Sum(selector); + } + finally + { + _lock.ExitReadLock(); + } + } + + return 0; + } + + /// + /// Returns a number that represents how many elements in the specified sequence satisfy a condition. + /// + /// + /// long? number of found elements + public long? Sum(Func selector) + { + if (!_disposed) + { + _lock.EnterReadLock(); + try + { + return _items.Values.Sum(selector); + } + finally + { + _lock.ExitReadLock(); + } + } + + return 0; + } + + /// + /// Returns a number that represents how many elements in the specified sequence satisfy a condition. + /// + /// + /// number of found elements + public double Sum(Func selector) + { + if (!_disposed) + { + _lock.EnterReadLock(); + try + { + return _items.Values.Sum(selector); + } + finally + { + _lock.ExitReadLock(); + } + } + + return 0; + } + + /// + /// Returns a number that represents how many elements in the specified sequence satisfy a condition. + /// + /// + /// double? number of found elements + public double? Sum(Func selector) + { + if (!_disposed) + { + _lock.EnterReadLock(); + try + { + return _items.Values.Sum(selector); + } + finally + { + _lock.ExitReadLock(); + } + } + + return 0; + } + + /// + /// Filters a sequence of values based on a predicate. + /// + /// + /// + /// + /// + public List Where(Func predicate) + { + if (!_disposed) + { + _lock.EnterReadLock(); + try + { + return new List(_items.Values.Where(predicate)); + } + finally + { + _lock.ExitReadLock(); + } + } + + return new List(); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + ClearAll(); + _lock.Dispose(); + } + } + + #endregion +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/GmCommandEvent.cs b/srcs/WingsAPI.Game/GmCommandEvent.cs new file mode 100644 index 0000000..d884a09 --- /dev/null +++ b/srcs/WingsAPI.Game/GmCommandEvent.cs @@ -0,0 +1,11 @@ +using WingsEmu.DTOs.Account; +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game; + +public class GmCommandEvent : PlayerEvent +{ + public string Command { get; init; } + public AuthorityType PlayerAuthority { get; init; } + public AuthorityType CommandAuthority { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Groups/Events/GroupActionEvent.cs b/srcs/WingsAPI.Game/Groups/Events/GroupActionEvent.cs new file mode 100644 index 0000000..4be0140 --- /dev/null +++ b/srcs/WingsAPI.Game/Groups/Events/GroupActionEvent.cs @@ -0,0 +1,10 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Groups.Events; + +public class GroupActionEvent : PlayerEvent +{ + public GroupRequestType RequestType { get; init; } + public long CharacterId { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Groups/Events/GroupAddMemberEvent.cs b/srcs/WingsAPI.Game/Groups/Events/GroupAddMemberEvent.cs new file mode 100644 index 0000000..20b55d1 --- /dev/null +++ b/srcs/WingsAPI.Game/Groups/Events/GroupAddMemberEvent.cs @@ -0,0 +1,11 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Game.Characters; + +namespace WingsEmu.Game.Groups.Events; + +public class GroupAddMemberEvent : PlayerEvent +{ + public GroupAddMemberEvent(IPlayerEntity newMember) => NewMember = newMember; + + public IPlayerEntity NewMember { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Groups/Events/GroupInvitedEvent.cs b/srcs/WingsAPI.Game/Groups/Events/GroupInvitedEvent.cs new file mode 100644 index 0000000..8981384 --- /dev/null +++ b/srcs/WingsAPI.Game/Groups/Events/GroupInvitedEvent.cs @@ -0,0 +1,8 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Groups.Events; + +public class GroupInvitedEvent : PlayerEvent +{ + public long TargetId { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Groups/Events/GroupJoinEvent.cs b/srcs/WingsAPI.Game/Groups/Events/GroupJoinEvent.cs new file mode 100644 index 0000000..eb5266d --- /dev/null +++ b/srcs/WingsAPI.Game/Groups/Events/GroupJoinEvent.cs @@ -0,0 +1,10 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Groups.Events; + +public class JoinToGroupEvent : PlayerEvent +{ + public JoinToGroupEvent(PlayerGroup playerGroup) => PlayerGroup = playerGroup; + + public PlayerGroup PlayerGroup { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Groups/Events/GroupLeaveEvent.cs b/srcs/WingsAPI.Game/Groups/Events/GroupLeaveEvent.cs new file mode 100644 index 0000000..b5ddc3c --- /dev/null +++ b/srcs/WingsAPI.Game/Groups/Events/GroupLeaveEvent.cs @@ -0,0 +1,7 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Groups.Events; + +public class LeaveGroupEvent : PlayerEvent +{ +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Groups/Events/GroupRemoveMemberEvent.cs b/srcs/WingsAPI.Game/Groups/Events/GroupRemoveMemberEvent.cs new file mode 100644 index 0000000..4b47f98 --- /dev/null +++ b/srcs/WingsAPI.Game/Groups/Events/GroupRemoveMemberEvent.cs @@ -0,0 +1,11 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Game.Characters; + +namespace WingsEmu.Game.Groups.Events; + +public class RemoveMemberFromGroupEvent : PlayerEvent +{ + public RemoveMemberFromGroupEvent(IPlayerEntity memberToRemove) => MemberToRemove = memberToRemove; + + public IPlayerEntity MemberToRemove { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Groups/Events/GroupWeedingEvent.cs b/srcs/WingsAPI.Game/Groups/Events/GroupWeedingEvent.cs new file mode 100644 index 0000000..6076e90 --- /dev/null +++ b/srcs/WingsAPI.Game/Groups/Events/GroupWeedingEvent.cs @@ -0,0 +1,9 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Groups.Events; + +public class GroupWeedingEvent : PlayerEvent +{ + public bool RemoveBuff { get; init; } + public long? RelatedId { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Groups/GroupComponent.cs b/srcs/WingsAPI.Game/Groups/GroupComponent.cs new file mode 100644 index 0000000..28b92f2 --- /dev/null +++ b/srcs/WingsAPI.Game/Groups/GroupComponent.cs @@ -0,0 +1,39 @@ +using WingsEmu.Game.Characters; + +namespace WingsEmu.Game.Groups; + +public class GroupComponent : IGroupComponent +{ + private PlayerGroup _playerGroup; + + public GroupComponent() => _playerGroup = null; + + public long GetGroupId() => _playerGroup?.GroupId ?? 0; + + public PlayerGroup GetGroup() => _playerGroup; + + public void AddMember(IPlayerEntity member) => _playerGroup?.AddMember(member); + + public void RemoveMember(IPlayerEntity member) => _playerGroup?.RemoveMember(member); + + public void SetGroup(PlayerGroup playerGroup) + { + if (_playerGroup != null) + { + return; + } + + _playerGroup = playerGroup; + } + + public void RemoveGroup() + { + _playerGroup = null; + } + + public bool IsInGroup() => _playerGroup != null; + + public bool IsLeaderOfGroup(long characterId) => _playerGroup != null && _playerGroup.OwnerId == characterId; + + public bool IsGroupFull() => _playerGroup != null && _playerGroup.Members.Count >= _playerGroup.Slots; +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Groups/GroupFactory.cs b/srcs/WingsAPI.Game/Groups/GroupFactory.cs new file mode 100644 index 0000000..e6f7fe6 --- /dev/null +++ b/srcs/WingsAPI.Game/Groups/GroupFactory.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using WingsEmu.Game.Characters; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Groups; + +public class GroupFactory : IGroupFactory +{ + private readonly IGroupManager _groupManager; + + public GroupFactory(IGroupManager groupManager) => _groupManager = groupManager; + + public PlayerGroup CreateGroup(byte slots, long ownerId) => CreateGroup(slots, new List(), ownerId); + + public PlayerGroup CreateGroup(byte slots, List characters, long ownerId) => new PlayerGroup(_groupManager.GetNextGroupId(), slots, characters, ownerId); + + public PlayerGroup CreateGroup(byte slots, List characters, long ownerId, GroupSharingType type) => + new PlayerGroup(_groupManager.GetNextGroupId(), slots, characters, ownerId, type); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Groups/IGroupComponent.cs b/srcs/WingsAPI.Game/Groups/IGroupComponent.cs new file mode 100644 index 0000000..9399910 --- /dev/null +++ b/srcs/WingsAPI.Game/Groups/IGroupComponent.cs @@ -0,0 +1,19 @@ +using WingsEmu.Game.Characters; + +namespace WingsEmu.Game.Groups; + +public interface IGroupComponent +{ + public long GetGroupId(); + public PlayerGroup GetGroup(); + + public void AddMember(IPlayerEntity member); + public void RemoveMember(IPlayerEntity member); + + public void SetGroup(PlayerGroup playerGroup); + public void RemoveGroup(); + + public bool IsInGroup(); + public bool IsLeaderOfGroup(long characterId); + public bool IsGroupFull(); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Groups/IGroupFactory.cs b/srcs/WingsAPI.Game/Groups/IGroupFactory.cs new file mode 100644 index 0000000..5213552 --- /dev/null +++ b/srcs/WingsAPI.Game/Groups/IGroupFactory.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using WingsEmu.Game.Characters; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Groups; + +public interface IGroupFactory +{ + PlayerGroup CreateGroup(byte slots, long ownerId); + PlayerGroup CreateGroup(byte slots, List characters, long ownerId); + PlayerGroup CreateGroup(byte slots, List characters, long ownerId, GroupSharingType type); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Groups/IGroupManager.cs b/srcs/WingsAPI.Game/Groups/IGroupManager.cs new file mode 100644 index 0000000..d46a748 --- /dev/null +++ b/srcs/WingsAPI.Game/Groups/IGroupManager.cs @@ -0,0 +1,18 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsEmu.Game._ECS; +using WingsEmu.Game.Characters; + +namespace WingsEmu.Game.Groups; + +public interface IGroupManager : ITickProcessable +{ + int GetNextGroupId(); + void JoinGroup(PlayerGroup group, IPlayerEntity character); + void RemoveGroup(PlayerGroup group, IPlayerEntity character); + void AddMemberGroup(PlayerGroup group, IPlayerEntity character); + void RemoveMemberGroup(PlayerGroup group, IPlayerEntity character); + void ChangeLeader(PlayerGroup group, long newLeaderId); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Groups/PlayerGroup.cs b/srcs/WingsAPI.Game/Groups/PlayerGroup.cs new file mode 100644 index 0000000..fcab056 --- /dev/null +++ b/srcs/WingsAPI.Game/Groups/PlayerGroup.cs @@ -0,0 +1,111 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using WingsEmu.Game.Characters; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Groups; + +public class PlayerGroup +{ + private readonly List _characters; + private readonly ReaderWriterLockSlim _lock = new(); + private int _order; + + public PlayerGroup(int groupId, byte slots, List members, long ownerId, GroupSharingType sharingMode = GroupSharingType.ByOrder) + { + GroupId = groupId; + Slots = slots; + _characters = members; + SharingMode = sharingMode; + OwnerId = ownerId; + } + + public int GroupId { get; } + + public GroupSharingType SharingMode { get; set; } + + public byte Slots { get; } + + public IReadOnlyList Members + { + get + { + _lock.EnterReadLock(); + try + { + return _characters; + } + finally + { + _lock.ExitReadLock(); + } + } + } + + + public long OwnerId { get; set; } + + public long ArenaKills { get; set; } + public long ArenaDeaths { get; set; } + + public void AddMember(IPlayerEntity character) + { + _lock.EnterWriteLock(); + try + { + if (_characters.Contains(character)) + { + return; + } + + _characters.Add(character); + } + finally + { + _lock.ExitWriteLock(); + } + } + + public void RemoveMember(IPlayerEntity character) + { + _lock.EnterWriteLock(); + try + { + _characters.Remove(character); + } + finally + { + _lock.ExitWriteLock(); + } + } + + public long? GetNextOrderedCharacterId(IPlayerEntity character) + { + _lock.EnterReadLock(); + try + { + _order++; + IPlayerEntity[] characters = _characters.Where(x => x != null).ToArray(); + if (_characters.Count == 0) // group seems to be empty + { + return null; + } + + if (_order > _characters.Count - 1) // if order wents out of amount of ppl, reset it -> zero based index + { + _order = 0; + } + + return characters[_order].Id; + } + finally + { + _lock.ExitReadLock(); + } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Helpers/Damages/Calculation/CalculationBasicStatistics.cs b/srcs/WingsAPI.Game/Helpers/Damages/Calculation/CalculationBasicStatistics.cs new file mode 100644 index 0000000..bc24e2f --- /dev/null +++ b/srcs/WingsAPI.Game/Helpers/Damages/Calculation/CalculationBasicStatistics.cs @@ -0,0 +1,29 @@ +namespace WingsEmu.Game.Helpers.Damages.Calculation; + +public class CalculationBasicStatistics +{ + #region Attacker + + public int AttackerMorale { get; set; } + + public int AttackerAttackUpgrade { get; set; } + public int AttackerHitRate { get; set; } + public int AttackerCriticalChance { get; set; } + public int AttackerCriticalDamage { get; set; } + + public int AttackerElementRate { get; set; } + + #endregion + + #region Defender + + public int DefenderMorale { get; set; } + + public int DefenderDefenseUpgrade { get; set; } + public int DefenderDefense { get; set; } + public int DefenderDodge { get; set; } + + public int DefenderResistance { get; set; } + + #endregion +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Helpers/Damages/Calculation/CalculationDefense.cs b/srcs/WingsAPI.Game/Helpers/Damages/Calculation/CalculationDefense.cs new file mode 100644 index 0000000..53f15fb --- /dev/null +++ b/srcs/WingsAPI.Game/Helpers/Damages/Calculation/CalculationDefense.cs @@ -0,0 +1,23 @@ +namespace WingsEmu.Game.Helpers.Damages.Calculation; + +public class CalculationDefense +{ + // BCardDTO - Type, SubType + public double IncreaseDefense { get; set; } // 11, 1 + + public double IncreaseDefenseAttackType { get; set; } // 11, 3-5-7 + + public (int, double) IncreaseDefenseByLevel { get; set; } // 12, 1 + + public (int, double) IncreaseDefenseByLevelAttackType { get; set; } // 12, 3-5-7 + + public double IncreaseAllDefense { get; set; } // 44, 4 + + public int MaximumCriticalDamage { get; set; } // 66, 7 + + public double DefenseInPvP { get; set; } // 71, 7-8 + + public double IncreaseDefenseInPve { get; set; } + + public double MultiplyDefense { get; set; } // 35 +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Helpers/Damages/Calculation/CalculationElementDamage.cs b/srcs/WingsAPI.Game/Helpers/Damages/Calculation/CalculationElementDamage.cs new file mode 100644 index 0000000..183f297 --- /dev/null +++ b/srcs/WingsAPI.Game/Helpers/Damages/Calculation/CalculationElementDamage.cs @@ -0,0 +1,8 @@ +namespace WingsEmu.Game.Helpers.Damages.Calculation; + +public class CalculationElementDamage +{ + public int Element { get; set; } + + public double ElementMultiply { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Helpers/Damages/Calculation/CalculationPhysicalDamage.cs b/srcs/WingsAPI.Game/Helpers/Damages/Calculation/CalculationPhysicalDamage.cs new file mode 100644 index 0000000..d12813e --- /dev/null +++ b/srcs/WingsAPI.Game/Helpers/Damages/Calculation/CalculationPhysicalDamage.cs @@ -0,0 +1,57 @@ +namespace WingsEmu.Game.Helpers.Damages.Calculation; + +public class CalculationPhysicalDamage +{ + // BCardDTO - Type, SubType + public int CleanDamage { get; set; } + + public double DamagePercentage { get; set; } // 5, 5 + + public double DamagePercentageSecond { get; set; } // 8, 1 + + public double MultiplyDamage { get; set; } // 34, 1 + + public double EndCriticalDamage { get; set; } // 38, 5 + + public double IgnoreEnemyDefense { get; set; } // 84, 1 + + public double VesselLoDDamage { get; set; } // 90, 3 + + public double VesselGlacernonDamage { get; set; } // 90, 7 + + public double IncreaseAllDamage { get; set; } // 103, 1 + + public double IncreaseAllDamageAttackType { get; set; } // 103, 3-5-7 + + public double IncreaseDamageMagicDefense { get; set; } // 108, 9 + + public int IncreaseDamageRace { get; set; } // 24, 1 + + public double IncreaseDamageRacePercentage { get; set; } // 71, 1 + + public double IncreaseLoDDamage { get; set; } // 101, 1 + + public double IncreaseVesselDamage { get; set; } // 101, 3 + + public double IncreaseDamageFaction { get; set; } // 85, 7-9 + + public int InvisibleDamage { get; set; } // 43, 7 + + public double IncreaseDamageInPvP { get; set; } // 71, 9 + + public double IncreaseAttack { get; set; } // 15, 1 + + public double IncreaseAttackAttackType { get; set; } // 15, 3-5-7 + + public double IncreaseDamageShadowFairy { get; set; } // 80, 9 + + public double IncreaseAllAttacks { get; set; } // 44, 3 + + public double IncreaseDamageByDebuffs { get; set; } + + public double IncreaseDamageHighMonsters { get; set; } // 86, 5 + + public double IncreaseDamageVersusMonsters { get; set; } + + public double IncreaseAllAttacksVersusMonsters { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Helpers/Damages/Calculation/CalculationResult.cs b/srcs/WingsAPI.Game/Helpers/Damages/Calculation/CalculationResult.cs new file mode 100644 index 0000000..8c7d4e4 --- /dev/null +++ b/srcs/WingsAPI.Game/Helpers/Damages/Calculation/CalculationResult.cs @@ -0,0 +1,15 @@ +namespace WingsEmu.Game.Helpers.Damages.Calculation; + +public class CalculationResult +{ + public CalculationResult(int damage, bool isCritical, bool isSoftDamage) + { + Damage = damage; + IsCritical = isCritical; + IsSoftDamage = isSoftDamage; + } + + public int Damage { get; } + public bool IsCritical { get; } + public bool IsSoftDamage { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Helpers/Damages/DamageAlgorithmResult.cs b/srcs/WingsAPI.Game/Helpers/Damages/DamageAlgorithmResult.cs new file mode 100644 index 0000000..d8be69f --- /dev/null +++ b/srcs/WingsAPI.Game/Helpers/Damages/DamageAlgorithmResult.cs @@ -0,0 +1,24 @@ +namespace WingsEmu.Game.Helpers.Damages; + +public class DamageAlgorithmResult +{ + public DamageAlgorithmResult(int damages, HitType hitMode, bool onyxEffect, bool softDamageEffect) + { + Damages = damages; + HitType = hitMode; + OnyxEffect = onyxEffect; + SoftDamageEffect = softDamageEffect; + } + + public bool SoftDamageEffect { get; } + public int Damages { get; set; } + public HitType HitType { get; } + public bool OnyxEffect { get; } +} + +public enum HitType +{ + Normal, + Critical, + Miss +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Helpers/Damages/Position.cs b/srcs/WingsAPI.Game/Helpers/Damages/Position.cs new file mode 100644 index 0000000..d5a477f --- /dev/null +++ b/srcs/WingsAPI.Game/Helpers/Damages/Position.cs @@ -0,0 +1,32 @@ +namespace WingsEmu.Game.Helpers.Damages; + +public struct Position +{ + public Position(short x, short y) + { + X = x; + Y = y; + } + + public short X { get; } + public short Y { get; } + + public static bool operator ==(Position a, Position b) => a.Equals(b); + + public static bool operator !=(Position a, Position b) => !a.Equals(b); + + public bool Equals(Position other) => X == other.X && Y == other.Y; + + public override bool Equals(object other) => Equals((Position)other); + + public override int GetHashCode() + { + unchecked + { + int hash = 17; + hash = hash * 23 + X.GetHashCode(); + hash = hash * 23 + Y.GetHashCode(); + return hash; + } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Helpers/Damages/PositionExtensions.cs b/srcs/WingsAPI.Game/Helpers/Damages/PositionExtensions.cs new file mode 100644 index 0000000..42de35e --- /dev/null +++ b/srcs/WingsAPI.Game/Helpers/Damages/PositionExtensions.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections.Generic; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game._enum; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Mates; + +namespace WingsEmu.Game.Helpers.Damages; + +public static class PositionExtensions +{ + private static readonly double SQRT_2 = Math.Sqrt(2); + + public static IReadOnlyList GetAlliesInRange(this Position position, IBattleEntity caster, short range) + { + var allies = new List(); + + IReadOnlyList entities = caster.MapInstance.GetClosestBattleEntitiesInRange(position, range); + + foreach (IBattleEntity entity in entities) + { + if (entity is IPlayerEntity { IsSeal: true }) + { + continue; + } + + if (caster.IsAllyWith(entity) && entity.GetMonsterRaceType() != MonsterRaceType.Fixed) + { + allies.Add(entity); + } + } + + return allies; + } + + public static IReadOnlyList GetEnemiesInRange(this Position position, IBattleEntity caster, short range) + { + var enemies = new List(); + IEnumerable entities = caster.MapInstance.GetClosestBattleEntitiesInRange(position, range); + + foreach (IBattleEntity entity in entities) + { + if (caster.IsEnemyWith(entity) && entity.GetMonsterRaceType() != MonsterRaceType.Fixed) + { + enemies.Add(entity); + } + } + + return enemies; + } + + private static double Octile(int x, int y) + { + int min = Math.Min(x, y); + int max = Math.Max(x, y); + return min * SQRT_2 + max - min; + } + + public static bool IsInAoeZone(this Position start, Position position, int range) + { + int dx = Math.Abs(start.X - position.X); + int dy = Math.Abs(start.Y - position.Y); + bool s = dx <= range && dy <= range; + return range switch + { + < 2 => s, + < 5 => s && dx + dy < range + range, + _ => s && dx + dy <= range + range / 2 + }; + } + + public static bool IsInAoeZone(this Position start, IBattleEntity to, int range) => IsInAoeZone(start, to.Position, range); + public static bool IsInAoeZone(this IBattleEntity start, IBattleEntity to, int range) => IsInAoeZone(start.Position, to.Position, range); + public static bool IsInAoeZone(this IBattleEntityDump start, IBattleEntityDump to, int range) => IsInAoeZone(start.Position, to.Position, range); + + public static bool IsInPvpZone(this IBattleEntity target) => target?.MapInstance != null && !target.MapInstance.PvpZone(target.PositionX, target.PositionY); + public static bool IsInMateDollZone(this IBattleEntity entity) => entity.MapInstance != null && !entity.MapInstance.MateDollZone(entity.PositionX, entity.PositionY); + + public static bool IsInRange(this IBattleEntityDump from, IBattleEntityDump to, int range) => IsInRange(from.Position, to.Position, range); + + public static bool IsInRange(this Position src, short x, short y, int range) => GetDistance(src, x, y) <= range; + public static bool IsInRange(this Position src, Position pos, int range) => GetDistance(src, pos) <= range; + + public static int GetDistance(this IBattleEntityDump from, IBattleEntityDump to) => GetDistance(from.Position, to.Position); + public static int GetDistance(this Position src, Position dest) => GetDistance(src, dest.X, dest.Y); + public static double GetDoubleDistance(this Position src, Position dest) => GetDoubleDistance(src, dest.X, dest.Y); + public static int GetDistance(this Position src, int x, int y) => (int)Octile(Math.Abs(src.X - x), Math.Abs(src.Y - y)); + public static double GetDoubleDistance(this Position src, int x, int y) => Math.Sqrt(Math.Pow(src.X - x, 2) + Math.Pow(src.Y - y, 2)); + public static int GetDistance(this IBattleEntity src, IBattleEntity to) => GetDistance(src.Position, to.Position); + + public static Position NewMinilandMapCell(this IBattleEntity entity, IRandomGenerator randomGenerator) + { + short newX = (short)randomGenerator.RandomNumber(5, 15); + short newY = (short)randomGenerator.RandomNumber(3, 14); + return new Position(newX, newY); + } + + public static bool IsInLineX(this IBattleEntity entity, short x, short width) => Math.Abs(entity.Position.X - x) <= width; + public static bool IsInLineY(this IBattleEntity entity, short y, short height) => Math.Abs(entity.Position.Y - y) <= height; + public static bool IsMonsterAggroDisabled(this IBattleEntity entity) => entity.MapInstance != null && !entity.MapInstance.IsMonsterAggroDisabled(entity.PositionX, entity.PositionY); + public static bool IsMonsterAggroDisabled(this IBattleEntity entity, short x, short y) => entity.MapInstance != null && !entity.MapInstance.IsMonsterAggroDisabled(x, y); + + public static void ChangePosition(this IBattleEntity battleEntity, Position newPosition) + { + battleEntity.Position = newPosition; + + if (battleEntity.MapInstance == null) + { + return; + } + + switch (battleEntity) + { + case IPlayerEntity playerEntity: + + if (playerEntity.MapInstance.HasMapFlag(MapFlags.IS_BASE_MAP) && playerEntity.MapInstance.MapInstanceType != MapInstanceType.TimeSpaceInstance) + { + playerEntity.MapX = newPosition.X; + playerEntity.MapY = newPosition.Y; + } + + break; + case IMateEntity mateEntity: + + if (mateEntity.MapInstance.HasMapFlag(MapFlags.IS_BASE_MAP) && mateEntity.MapInstance.MapInstanceType != MapInstanceType.TimeSpaceInstance) + { + mateEntity.MapX = newPosition.X; + mateEntity.MapX = newPosition.Y; + } + + if (mateEntity.MapInstance is { MapInstanceType: MapInstanceType.Miniland } && mateEntity.MapInstance?.Id == mateEntity.Owner?.Miniland?.Id) + { + mateEntity.MinilandX = newPosition.X; + mateEntity.MinilandX = newPosition.Y; + } + + break; + } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Helpers/Location.cs b/srcs/WingsAPI.Game/Helpers/Location.cs new file mode 100644 index 0000000..f23a669 --- /dev/null +++ b/srcs/WingsAPI.Game/Helpers/Location.cs @@ -0,0 +1,15 @@ +namespace WingsEmu.Game.Helpers; + +public struct Location +{ + public Location(int mapId, short x, short y) + { + MapId = mapId; + X = x; + Y = y; + } + + public int MapId { get; } + public short X { get; } + public short Y { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Helpers/MapLocation.cs b/srcs/WingsAPI.Game/Helpers/MapLocation.cs new file mode 100644 index 0000000..bfe5365 --- /dev/null +++ b/srcs/WingsAPI.Game/Helpers/MapLocation.cs @@ -0,0 +1,10 @@ +using System; + +namespace WingsEmu.Game.Helpers; + +public class MapLocation +{ + public Guid MapInstanceId { get; init; } + public short X { get; init; } + public short Y { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Helpers/PathfindingHelper.cs b/srcs/WingsAPI.Game/Helpers/PathfindingHelper.cs new file mode 100644 index 0000000..bf4bf7c --- /dev/null +++ b/srcs/WingsAPI.Game/Helpers/PathfindingHelper.cs @@ -0,0 +1,182 @@ +using System; +using System.Collections.Generic; +using WingsEmu.Game.Helpers.Damages; + +namespace WingsEmu.Game.Helpers; + +public static class PathfindingAlgorithm +{ + [Flags] + public enum MapCellFlags + { + IsWalkingDisabled = 0x1, + IsAttackDisabledThrough = 0x2, + UnknownYet = 0x4, + IsMonsterAggroDisabled = 0x8, + IsPvpDisabled = 0x10, + MateDoll = 0x20 + } + + private static readonly sbyte[,] Neighbours = + { + { -1, -1 }, { 0, -1 }, { 1, -1 }, + { -1, 0 }, { 1, 0 }, + { -1, 1 }, { 0, 1 }, { 1, 1 } + }; + + public static bool IsWalkable(this IReadOnlyList array, int x, int y, int width, int height) + { + if (x < 0 || y < 0) + { + return false; + } + + if (x >= width || y >= height) + { + return false; + } + + return (array[x + y * width] & (int)MapCellFlags.IsWalkingDisabled) == 0; + } + + public static bool IsWalkable(this byte cell) => (cell & (int)MapCellFlags.IsWalkingDisabled) == 0; + + public static bool IsPvpZoneOff(this IReadOnlyList array, int x, int y, int width, int height) + { + if (x < 0 || y < 0) + { + return true; + } + + if (x >= width || y >= height) + { + return true; + } + + return (array[x + y * width] & (int)MapCellFlags.IsPvpDisabled) == (int)MapCellFlags.IsPvpDisabled; + } + + public static bool IsMateDollZone(this IReadOnlyList array, int x, int y, int width, int height) + { + if (x < 0 || y < 0) + { + return false; + } + + if (x >= width || y >= height) + { + return false; + } + + return (array[x + y * width] & (int)MapCellFlags.MateDoll) == (int)MapCellFlags.MateDoll; + } + + public static bool IsMonsterAggroDisabled(this IReadOnlyList array, int x, int y, int width, int height) + { + if (x < 0 || y < 0) + { + return true; + } + + if (x >= width || y >= height) + { + return true; + } + + return (array[x + y * width] & (int)MapCellFlags.IsMonsterAggroDisabled) == 0; + } + + /// + /// Returns an Array with the neighbors of the given position + /// + /// + /// + /// + /// + /// + public static IReadOnlyList GetNeighbors(this Position pos, IReadOnlyList map, int width, int height) + { + var neighbors = new List(8); + for (byte i = 0; i < 8; i++) + { + short x = (short)(pos.X + Neighbours[i, 0]); + short y = (short)(pos.Y + Neighbours[i, 1]); + if (x >= 0 && x < width && y >= 0 && y < height && map.IsWalkable(x, y, width, height)) + { + neighbors.Add(new Position(x, y)); + } + } + + return neighbors; + } + + public static Position Bresenham(Position from, Position to, float maxCalculationDistance, IReadOnlyList grid, int width, int height, bool ret) + { + short sX = from.X; + short sY = from.Y; + short tX = to.X; + short tY = to.Y; + + int dx = Math.Abs(tX - sX); + int sx = sX < tX ? 1 : -1; + int dy = -Math.Abs(tY - sY); + int sy = sY < tY ? 1 : -1; + int err = dx + dy; + + short lastX = -1; + short lastY = -1; + + short lastFreeX = from.X; + short lastFreeY = from.Y; + + while (true) // Bresenham + { + double distance = from.GetDoubleDistance(new Position(sX, sY)); + if (lastX != -1 && lastY != -1 && distance >= maxCalculationDistance) + { + return new Position(lastFreeX, lastFreeY); + } + + if (ret) + { + if (grid.IsWalkable(sX, sY, width, height)) + { + lastFreeX = sX; + lastFreeY = sY; + } + } + else + { + if (!grid.IsWalkable(sX, sY, width, height)) + { + return new Position(lastX, lastY); + } + } + + lastX = sX; + lastY = sY; + + if (sX == tX && sY == tY) + { + break; + } + + int e2 = 2 * err; + if (e2 >= dy) + { + err += dy; + sX += (short)sx; + } + + if (e2 > dx) + { + continue; + } + + err += dx; + sY += (short)sy; + } + + return new Position(lastFreeX, lastFreeY); + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/IRandomGenerator.cs b/srcs/WingsAPI.Game/IRandomGenerator.cs new file mode 100644 index 0000000..210a0f7 --- /dev/null +++ b/srcs/WingsAPI.Game/IRandomGenerator.cs @@ -0,0 +1,35 @@ +namespace WingsEmu.Game; + +public class StaticRandomGenerator +{ + public static IRandomGenerator Instance { get; private set; } + + public static void Initialize(IRandomGenerator generator) + { + Instance = generator; + } +} + +public interface IRandomGenerator +{ + /// + /// Generates a random number between min and max excluded + /// + /// + /// + /// + int RandomNumber(int min, int max); + + /// + /// Generates a random number between 0 and max excluded + /// + /// + /// + int RandomNumber(int max); + + /// + /// Generates a random number between 0 and 100 + /// + /// + int RandomNumber(); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/InterChannel/ChatShoutAdminEvent.cs b/srcs/WingsAPI.Game/InterChannel/ChatShoutAdminEvent.cs new file mode 100644 index 0000000..341d936 --- /dev/null +++ b/srcs/WingsAPI.Game/InterChannel/ChatShoutAdminEvent.cs @@ -0,0 +1,8 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.InterChannel; + +public class ChatShoutAdminEvent : PlayerEvent +{ + public string Message { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/InterChannel/FamilyChatMessageEvent.cs b/srcs/WingsAPI.Game/InterChannel/FamilyChatMessageEvent.cs new file mode 100644 index 0000000..a12d38e --- /dev/null +++ b/srcs/WingsAPI.Game/InterChannel/FamilyChatMessageEvent.cs @@ -0,0 +1,10 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.InterChannel; + +public class FamilyChatMessageEvent : PlayerEvent +{ + public FamilyChatMessageEvent(string message) => Message = message; + + public string Message { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/InterChannel/InterChannelChatMessageBroadcastEvent.cs b/srcs/WingsAPI.Game/InterChannel/InterChannelChatMessageBroadcastEvent.cs new file mode 100644 index 0000000..b131bd5 --- /dev/null +++ b/srcs/WingsAPI.Game/InterChannel/InterChannelChatMessageBroadcastEvent.cs @@ -0,0 +1,21 @@ +using PhoenixLib.Events; +using WingsEmu.Game._i18n; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Game.InterChannel; + +public class InterChannelChatMessageBroadcastEvent : IAsyncEvent +{ + public InterChannelChatMessageBroadcastEvent(GameDialogKey dialogKey, ChatMessageColorType chatMessageColorType, params string[] args) + { + DialogKey = dialogKey; + ChatMessageColorType = chatMessageColorType; + Args = args; + } + + public GameDialogKey DialogKey { get; } + + public ChatMessageColorType ChatMessageColorType { get; } + + public object?[] Args { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/InterChannel/InterChannelReceiveWhisperEvent.cs b/srcs/WingsAPI.Game/InterChannel/InterChannelReceiveWhisperEvent.cs new file mode 100644 index 0000000..bddf801 --- /dev/null +++ b/srcs/WingsAPI.Game/InterChannel/InterChannelReceiveWhisperEvent.cs @@ -0,0 +1,26 @@ +using WingsEmu.DTOs.Account; +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.InterChannel; + +public class InterChannelReceiveWhisperEvent : PlayerEvent +{ + public InterChannelReceiveWhisperEvent(long senderCharacterId, string senderNickname, int senderChannelId, AuthorityType authorityType, string message) + { + SenderNickname = senderNickname; + SenderChannelId = senderChannelId; + AuthorityType = authorityType; + Message = message; + SenderCharacterId = senderCharacterId; + } + + public long SenderCharacterId { get; } + + public string SenderNickname { get; } + + public int SenderChannelId { get; } + + public AuthorityType AuthorityType { get; } + + public string Message { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/InterChannel/InterChannelSendChatMsgByCharIdEvent.cs b/srcs/WingsAPI.Game/InterChannel/InterChannelSendChatMsgByCharIdEvent.cs new file mode 100644 index 0000000..22e835d --- /dev/null +++ b/srcs/WingsAPI.Game/InterChannel/InterChannelSendChatMsgByCharIdEvent.cs @@ -0,0 +1,21 @@ +using WingsEmu.Game._i18n; +using WingsEmu.Game._packetHandling; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Game.InterChannel; + +public class InterChannelSendChatMsgByCharIdEvent : PlayerEvent +{ + public InterChannelSendChatMsgByCharIdEvent(long characterId, GameDialogKey dialogKey, ChatMessageColorType chatMessageColorType) + { + DialogKey = dialogKey; + ChatMessageColorType = chatMessageColorType; + CharacterId = characterId; + } + + public long CharacterId { get; } + + public GameDialogKey DialogKey { get; } + + public ChatMessageColorType ChatMessageColorType { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/InterChannel/InterChannelSendChatMsgByNicknameEvent.cs b/srcs/WingsAPI.Game/InterChannel/InterChannelSendChatMsgByNicknameEvent.cs new file mode 100644 index 0000000..0edb20c --- /dev/null +++ b/srcs/WingsAPI.Game/InterChannel/InterChannelSendChatMsgByNicknameEvent.cs @@ -0,0 +1,21 @@ +using WingsEmu.Game._i18n; +using WingsEmu.Game._packetHandling; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Game.InterChannel; + +public class InterChannelSendChatMsgByNicknameEvent : PlayerEvent +{ + public InterChannelSendChatMsgByNicknameEvent(string nickname, GameDialogKey dialogKey, ChatMessageColorType chatMessageColorType) + { + DialogKey = dialogKey; + ChatMessageColorType = chatMessageColorType; + Nickname = nickname; + } + + public string Nickname { get; } + + public GameDialogKey DialogKey { get; } + + public ChatMessageColorType ChatMessageColorType { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/InterChannel/InterChannelSendInfoByCharIdEvent.cs b/srcs/WingsAPI.Game/InterChannel/InterChannelSendInfoByCharIdEvent.cs new file mode 100644 index 0000000..16474d0 --- /dev/null +++ b/srcs/WingsAPI.Game/InterChannel/InterChannelSendInfoByCharIdEvent.cs @@ -0,0 +1,17 @@ +using WingsEmu.Game._i18n; +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.InterChannel; + +public class InterChannelSendInfoByCharIdEvent : PlayerEvent +{ + public InterChannelSendInfoByCharIdEvent(long characterId, GameDialogKey dialogKey) + { + CharacterId = characterId; + DialogKey = dialogKey; + } + + public long CharacterId { get; } + + public GameDialogKey DialogKey { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/InterChannel/InterChannelSendInfoByNicknameEvent.cs b/srcs/WingsAPI.Game/InterChannel/InterChannelSendInfoByNicknameEvent.cs new file mode 100644 index 0000000..8b604e9 --- /dev/null +++ b/srcs/WingsAPI.Game/InterChannel/InterChannelSendInfoByNicknameEvent.cs @@ -0,0 +1,17 @@ +using WingsEmu.Game._i18n; +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.InterChannel; + +public class InterChannelSendInfoByNicknameEvent : PlayerEvent +{ + public InterChannelSendInfoByNicknameEvent(string nickname, GameDialogKey dialogKey) + { + Nickname = nickname; + DialogKey = dialogKey; + } + + public string Nickname { get; } + + public GameDialogKey DialogKey { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/InterChannel/InterChannelSendWhisperEvent.cs b/srcs/WingsAPI.Game/InterChannel/InterChannelSendWhisperEvent.cs new file mode 100644 index 0000000..6e19be5 --- /dev/null +++ b/srcs/WingsAPI.Game/InterChannel/InterChannelSendWhisperEvent.cs @@ -0,0 +1,16 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.InterChannel; + +public class InterChannelSendWhisperEvent : PlayerEvent +{ + public InterChannelSendWhisperEvent(string nickname, string message) + { + Nickname = nickname; + Message = message; + } + + public string Nickname { get; } + + public string Message { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Inventory/Event/DropMapItemEvent.cs b/srcs/WingsAPI.Game/Inventory/Event/DropMapItemEvent.cs new file mode 100644 index 0000000..b6deb7b --- /dev/null +++ b/srcs/WingsAPI.Game/Inventory/Event/DropMapItemEvent.cs @@ -0,0 +1,50 @@ +using PhoenixLib.Events; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Maps; + +namespace WingsEmu.Game.Inventory.Event; + +public class ThrowItemEvent : IAsyncEvent +{ + public ThrowItemEvent(IBattleEntity battleEntity, int itemVnum, int quantity, int minimumDistance, int maximumDistance) + { + BattleEntity = battleEntity; + ItemVnum = itemVnum; + Quantity = quantity; + MinimumDistance = minimumDistance; + MaximumDistance = maximumDistance; + } + + public IBattleEntity BattleEntity { get; } + public int ItemVnum { get; } + public int Quantity { get; } + public int MinimumDistance { get; } + public int MaximumDistance { get; } +} + +public class DropMapItemEvent : IAsyncEvent +{ + public DropMapItemEvent(IMapInstance map, Position position, short vnum, int amount, short design = 0, short rarity = 0, short upgrade = 0, long ownerId = -1, bool isQuest = false) + { + Map = map; + Position = position; + Vnum = vnum; + Amount = amount; + Design = design; + Rarity = rarity; + Upgrade = upgrade; + OwnerId = ownerId; + IsQuest = isQuest; + } + + public IMapInstance Map { get; } + public Position Position { get; } + public short Vnum { get; } + public int Amount { get; } + public short Design { get; } + public short Rarity { get; } + public short Upgrade { get; } + public long OwnerId { get; } + public bool IsQuest { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Inventory/Event/InventoryAddItemEvent.cs b/srcs/WingsAPI.Game/Inventory/Event/InventoryAddItemEvent.cs new file mode 100644 index 0000000..cd6ba57 --- /dev/null +++ b/srcs/WingsAPI.Game/Inventory/Event/InventoryAddItemEvent.cs @@ -0,0 +1,36 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Game.Inventory.Event; + +public class InventoryAddItemEvent : PlayerEvent +{ + public InventoryAddItemEvent(InventoryItem inventoryItem, bool showMessage = false, ChatMessageColorType addItemMessageType = ChatMessageColorType.Green, bool sendAsGiftIfFull = false, + MessageErrorType errorType = MessageErrorType.Chat, short? slot = null, InventoryType? inventoryType = null, bool isByMovePacket = false) + { + InventoryItem = inventoryItem; + ShowMessage = showMessage; + ItemMessageType = addItemMessageType; + SendAsGiftIfFull = sendAsGiftIfFull; + MessageErrorType = errorType; + Slot = slot; + InventoryType = inventoryType; + IsByMovePacket = isByMovePacket; + } + + public InventoryItem InventoryItem { get; } + public bool ShowMessage { get; } + public ChatMessageColorType ItemMessageType { get; } + public bool SendAsGiftIfFull { get; } + public MessageErrorType MessageErrorType { get; } + public short? Slot { get; } + public InventoryType? InventoryType { get; } + public bool IsByMovePacket { get; } +} + +public enum MessageErrorType +{ + Chat = 0, + Shop = 1 +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Inventory/Event/InventoryDropItemEvent.cs b/srcs/WingsAPI.Game/Inventory/Event/InventoryDropItemEvent.cs new file mode 100644 index 0000000..f7039e8 --- /dev/null +++ b/srcs/WingsAPI.Game/Inventory/Event/InventoryDropItemEvent.cs @@ -0,0 +1,18 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Inventory.Event; + +public class InventoryDropItemEvent : PlayerEvent +{ + public InventoryDropItemEvent(InventoryType inventoryType, short slot, short amount) + { + InventoryType = inventoryType; + Slot = slot; + Amount = amount; + } + + public InventoryType InventoryType { get; } + public short Slot { get; } + public short Amount { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Inventory/Event/InventoryEquipItemEvent.cs b/srcs/WingsAPI.Game/Inventory/Event/InventoryEquipItemEvent.cs new file mode 100644 index 0000000..b306583 --- /dev/null +++ b/srcs/WingsAPI.Game/Inventory/Event/InventoryEquipItemEvent.cs @@ -0,0 +1,20 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Inventory.Event; + +public class InventoryEquipItemEvent : PlayerEvent +{ + public InventoryEquipItemEvent(short slot, bool isSpecialType = false, InventoryType? inventoryType = null, bool boundItem = false) + { + Slot = slot; + IsSpecialType = isSpecialType; + InventoryType = inventoryType; + BoundItem = boundItem; + } + + public short Slot { get; } + public bool IsSpecialType { get; } + public InventoryType? InventoryType { get; } + public bool BoundItem { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Inventory/Event/InventoryItemDeletedEvent.cs b/srcs/WingsAPI.Game/Inventory/Event/InventoryItemDeletedEvent.cs new file mode 100644 index 0000000..69b090e --- /dev/null +++ b/srcs/WingsAPI.Game/Inventory/Event/InventoryItemDeletedEvent.cs @@ -0,0 +1,10 @@ +using WingsEmu.DTOs.Items; +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Inventory.Event; + +public class InventoryItemDeletedEvent : PlayerEvent +{ + public ItemInstanceDTO ItemInstance { get; init; } + public int ItemAmount { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Inventory/Event/InventoryItemUsedEvent.cs b/srcs/WingsAPI.Game/Inventory/Event/InventoryItemUsedEvent.cs new file mode 100644 index 0000000..4e664e3 --- /dev/null +++ b/srcs/WingsAPI.Game/Inventory/Event/InventoryItemUsedEvent.cs @@ -0,0 +1,8 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Inventory.Event; + +public class InventoryItemUsedEvent : PlayerEvent +{ + public int ItemVnum { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Inventory/Event/InventoryMoveItemEvent.cs b/srcs/WingsAPI.Game/Inventory/Event/InventoryMoveItemEvent.cs new file mode 100644 index 0000000..6ea5e22 --- /dev/null +++ b/srcs/WingsAPI.Game/Inventory/Event/InventoryMoveItemEvent.cs @@ -0,0 +1,24 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Inventory.Event; + +public class InventoryMoveItemEvent : PlayerEvent +{ + public InventoryMoveItemEvent(InventoryType inventoryType, short slot, short amount, short destinationSlot, InventoryType destinationType, bool sendPackets = true) + { + InventoryType = inventoryType; + Slot = slot; + Amount = amount; + DestinationSlot = destinationSlot; + DestinationType = destinationType; + SendPackets = sendPackets; + } + + public InventoryType InventoryType { get; } + public short Slot { get; } + public short Amount { get; } + public short DestinationSlot { get; } + public InventoryType DestinationType { get; } + public bool SendPackets { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Inventory/Event/InventoryPickUpItemEvent.cs b/srcs/WingsAPI.Game/Inventory/Event/InventoryPickUpItemEvent.cs new file mode 100644 index 0000000..d148174 --- /dev/null +++ b/srcs/WingsAPI.Game/Inventory/Event/InventoryPickUpItemEvent.cs @@ -0,0 +1,18 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Inventory.Event; + +public class InventoryPickUpItemEvent : PlayerEvent +{ + public InventoryPickUpItemEvent(VisualType pickerVisualType, long pickerId, long dropId) + { + PickerVisualType = pickerVisualType; + PickerId = pickerId; + DropId = dropId; + } + + public VisualType PickerVisualType { get; } + public long PickerId { get; } + public long DropId { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Inventory/Event/InventoryPickedUpItemEvent.cs b/srcs/WingsAPI.Game/Inventory/Event/InventoryPickedUpItemEvent.cs new file mode 100644 index 0000000..339c5c8 --- /dev/null +++ b/srcs/WingsAPI.Game/Inventory/Event/InventoryPickedUpItemEvent.cs @@ -0,0 +1,11 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Game.Helpers; + +namespace WingsEmu.Game.Inventory.Event; + +public class InventoryPickedUpItemEvent : PlayerEvent +{ + public int ItemVnum { get; init; } + public int Amount { get; init; } + public Location Location { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Inventory/Event/InventoryPickedUpPlayerItemEvent.cs b/srcs/WingsAPI.Game/Inventory/Event/InventoryPickedUpPlayerItemEvent.cs new file mode 100644 index 0000000..7a6d1df --- /dev/null +++ b/srcs/WingsAPI.Game/Inventory/Event/InventoryPickedUpPlayerItemEvent.cs @@ -0,0 +1,12 @@ +using WingsEmu.DTOs.Items; +using WingsEmu.Game._packetHandling; +using WingsEmu.Game.Helpers; + +namespace WingsEmu.Game.Inventory.Event; + +public class InventoryPickedUpPlayerItemEvent : PlayerEvent +{ + public ItemInstanceDTO ItemInstance { get; init; } + public int Amount { get; init; } + public Location Location { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Inventory/Event/InventoryRemoveItemEvent.cs b/srcs/WingsAPI.Game/Inventory/Event/InventoryRemoveItemEvent.cs new file mode 100644 index 0000000..6e616c5 --- /dev/null +++ b/srcs/WingsAPI.Game/Inventory/Event/InventoryRemoveItemEvent.cs @@ -0,0 +1,21 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Inventory.Event; + +public class InventoryRemoveItemEvent : PlayerEvent +{ + public InventoryRemoveItemEvent(int itemVnum, short amount = 1, bool isEquipped = false, InventoryItem inventoryItem = null, bool sendPackets = true) + { + ItemVnum = itemVnum; + Amount = amount; + IsEquipped = isEquipped; + InventoryItem = inventoryItem; + SendPackets = sendPackets; + } + + public int ItemVnum { get; } + public short Amount { get; } + public bool IsEquipped { get; } + public InventoryItem InventoryItem { get; } + public bool SendPackets { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Inventory/Event/InventorySortItemEvent.cs b/srcs/WingsAPI.Game/Inventory/Event/InventorySortItemEvent.cs new file mode 100644 index 0000000..7ca0af7 --- /dev/null +++ b/srcs/WingsAPI.Game/Inventory/Event/InventorySortItemEvent.cs @@ -0,0 +1,10 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Inventory.Event; + +public class InventorySortItemEvent : PlayerEvent +{ + public InventoryType InventoryType { get; init; } + public bool Confirm { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Inventory/Event/InventoryTakeOffItemEvent.cs b/srcs/WingsAPI.Game/Inventory/Event/InventoryTakeOffItemEvent.cs new file mode 100644 index 0000000..ce56431 --- /dev/null +++ b/srcs/WingsAPI.Game/Inventory/Event/InventoryTakeOffItemEvent.cs @@ -0,0 +1,12 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Inventory.Event; + +public class InventoryTakeOffItemEvent : PlayerEvent +{ + public InventoryTakeOffItemEvent(short slot) => Slot = slot; + + public short Slot { get; } + + public bool ForceToRandomSlot { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Inventory/Event/ItemSumEvent.cs b/srcs/WingsAPI.Game/Inventory/Event/ItemSumEvent.cs new file mode 100644 index 0000000..d74db8f --- /dev/null +++ b/srcs/WingsAPI.Game/Inventory/Event/ItemSumEvent.cs @@ -0,0 +1,15 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Inventory.Event; + +public class ItemSumEvent : PlayerEvent +{ + public ItemSumEvent(InventoryItem leftItem, InventoryItem rightItem) + { + LeftItem = leftItem; + RightItem = rightItem; + } + + public InventoryItem LeftItem { get; } + public InventoryItem RightItem { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Inventory/Event/PartnerInventoryEquipItemEvent.cs b/srcs/WingsAPI.Game/Inventory/Event/PartnerInventoryEquipItemEvent.cs new file mode 100644 index 0000000..05b1a17 --- /dev/null +++ b/srcs/WingsAPI.Game/Inventory/Event/PartnerInventoryEquipItemEvent.cs @@ -0,0 +1,18 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Inventory.Event; + +public class PartnerInventoryEquipItemEvent : PlayerEvent +{ + public PartnerInventoryEquipItemEvent(short partnerSlot, byte slot, InventoryType inventoryType = InventoryType.Equipment) + { + PartnerSlot = partnerSlot; + Slot = slot; + InventoryType = inventoryType; + } + + public short PartnerSlot { get; } + public byte Slot { get; } + public InventoryType InventoryType { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Inventory/Event/PartnerInventoryTakeOffItemEvent.cs b/srcs/WingsAPI.Game/Inventory/Event/PartnerInventoryTakeOffItemEvent.cs new file mode 100644 index 0000000..2af9bf6 --- /dev/null +++ b/srcs/WingsAPI.Game/Inventory/Event/PartnerInventoryTakeOffItemEvent.cs @@ -0,0 +1,15 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Inventory.Event; + +public class PartnerInventoryTakeOffItemEvent : PlayerEvent +{ + public PartnerInventoryTakeOffItemEvent(short petId, byte slot) + { + PetId = petId; + Slot = slot; + } + + public short PetId { get; } + public byte Slot { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Inventory/Event/PartnerSpecialistSkillEvent.cs b/srcs/WingsAPI.Game/Inventory/Event/PartnerSpecialistSkillEvent.cs new file mode 100644 index 0000000..5dd2308 --- /dev/null +++ b/srcs/WingsAPI.Game/Inventory/Event/PartnerSpecialistSkillEvent.cs @@ -0,0 +1,10 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Inventory.Event; + +public class PartnerSpecialistSkillEvent : PlayerEvent +{ + public byte PartnerSlot { get; init; } + public byte SkillSlot { get; init; } + public bool Roll { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Inventory/Event/PlayerItemToPartnerItemEvent.cs b/srcs/WingsAPI.Game/Inventory/Event/PlayerItemToPartnerItemEvent.cs new file mode 100644 index 0000000..9f5a4e1 --- /dev/null +++ b/srcs/WingsAPI.Game/Inventory/Event/PlayerItemToPartnerItemEvent.cs @@ -0,0 +1,16 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Inventory.Event; + +public class PlayerItemToPartnerItemEvent : PlayerEvent +{ + public PlayerItemToPartnerItemEvent(byte slot, InventoryType inventoryType) + { + Slot = slot; + InventoryType = inventoryType; + } + + public byte Slot { get; } + public InventoryType InventoryType { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Inventory/IInventoryComponent.cs b/srcs/WingsAPI.Game/Inventory/IInventoryComponent.cs new file mode 100644 index 0000000..b228a9d --- /dev/null +++ b/srcs/WingsAPI.Game/Inventory/IInventoryComponent.cs @@ -0,0 +1,61 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using WingsEmu.Game.Items; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Inventory; + +public interface IInventoryComponent +{ + public IEnumerable EquippedItems { get; } + + public GameItemInstance MainWeapon { get; } + public GameItemInstance SecondaryWeapon { get; } + public GameItemInstance Armor { get; } + + public GameItemInstance Amulet { get; } + public GameItemInstance Hat { get; } + public GameItemInstance Gloves { get; } + public GameItemInstance Ring { get; } + public GameItemInstance Necklace { get; } + public GameItemInstance Bracelet { get; } + public GameItemInstance Boots { get; } + + public GameItemInstance Fairy { get; } + public GameItemInstance Specialist { get; } + + public GameItemInstance Mask { get; } + public GameItemInstance CostumeSuit { get; } + public GameItemInstance CostumeHat { get; } + public GameItemInstance WeaponSkin { get; } + public GameItemInstance Wings { get; } + + public bool InventoryIsInitialized { get; } + public IEnumerable GetAllPlayerInventoryItems(); + public InventoryItem GetInventoryItemFromEquipmentSlot(EquipmentType type); + public InventoryItem GetItemBySlotAndType(short slot, InventoryType type); + public InventoryItem GetFirstItemByVnum(int vnum); + public InventoryItem FindItemWithoutFullStack(int vnum, short amount); + public IEnumerable GetItemsByInventoryType(InventoryType type); + public GameItemInstance GetItemInstanceFromEquipmentSlot(EquipmentType type); + public int CountItemWithVnum(int vnum); + public bool HasSpaceFor(int vnum, short amount = 1); + public bool HasItem(int vnum, short amount = 1); + public void AddItemToInventory(InventoryItem inventoryItem); + + /// + /// + /// + /// + /// + /// false if failed, true if succeeded + public bool RemoveItemFromSlotAndType(short slot, InventoryType type, out InventoryItem removedItem); + + public bool RemoveItemAmountByVnum(int vnum, short amount, out InventoryItem removedItem); + + public void EquipItem(InventoryItem item, EquipmentType type, bool force = false); + public void TakeOffItem(EquipmentType type, short? slot = null, InventoryType? inventoryType = null); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Inventory/IPartnerInventoryComponent.cs b/srcs/WingsAPI.Game/Inventory/IPartnerInventoryComponent.cs new file mode 100644 index 0000000..b37cd77 --- /dev/null +++ b/srcs/WingsAPI.Game/Inventory/IPartnerInventoryComponent.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using WingsEmu.Game.Items; +using WingsEmu.Game.Warehouse; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Inventory; + +public interface IPartnerInventoryComponent +{ + public IReadOnlyList PartnerGetEquippedItems(short partnerSlot); + public IReadOnlyList GetPartnersEquippedItems(); + public void PartnerEquipItem(InventoryItem item, short partnerSlot); + public void PartnerEquipItem(GameItemInstance item, short partnerSlot); + public void PartnerTakeOffItem(EquipmentType type, short partnerSlot); + public PartnerInventoryItem PartnerGetEquippedItem(EquipmentType type, short partnerSlot); + + public void AddPartnerWarehouseItem(GameItemInstance item, short slot); + public void RemovePartnerWarehouseItem(short slot); + public PartnerWarehouseItem GetPartnerWarehouseItem(short slot); + public IReadOnlyList PartnerWarehouseItems(); + public byte GetPartnerWarehouseSlots(); + public byte GetPartnerWarehouseSlotsWithoutBackpack(); + public bool HasSpaceForPartnerWarehouseItem(); + public bool HasSpaceForPartnerItemWarehouse(int itemVnum, short amount = 1); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Inventory/InventoryComponent.cs b/srcs/WingsAPI.Game/Inventory/InventoryComponent.cs new file mode 100644 index 0000000..156afd5 --- /dev/null +++ b/srcs/WingsAPI.Game/Inventory/InventoryComponent.cs @@ -0,0 +1,432 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using WingsEmu.Core.Extensions; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Inventory; + +public class InventoryComponent : IInventoryComponent +{ + private const int WEAR_SLOTS = 17; + private readonly IPlayerEntity _character; + private readonly Dictionary> _inventoryItemsByInventoryType = new(); + private readonly IItemsManager _itemsManager; + private readonly ReaderWriterLockSlim _lock = new(); + + private readonly InventoryItem[] _wear = new InventoryItem[WEAR_SLOTS]; + + public InventoryComponent(IPlayerEntity character, IItemsManager itemsManager) + { + _character = character; + _itemsManager = itemsManager; + } + + public IEnumerable GetAllPlayerInventoryItems() + { + var list = new List(); + _lock.EnterReadLock(); + try + { + foreach (KeyValuePair> keyValuePair in _inventoryItemsByInventoryType) + { + list.AddRange(keyValuePair.Value); + } + + foreach (InventoryItem inventoryItem in _wear) + { + if (inventoryItem == null) + { + continue; + } + + list.Add(inventoryItem); + } + + return list; + } + finally + { + _lock.ExitReadLock(); + } + } + + public InventoryItem GetFirstItemByVnum(int vnum) + { + IGameItem gameItem = _itemsManager.GetItem(vnum); + if (gameItem == null) + { + return null; + } + + _lock.EnterReadLock(); + try + { + if (!_inventoryItemsByInventoryType.TryGetValue(gameItem.Type, out List items)) + { + return null; + } + + return items.Where(s => s?.ItemInstance != null && s.ItemInstance.ItemVNum == vnum).OrderBy(x => x.Slot).FirstOrDefault(); + } + finally + { + _lock.ExitReadLock(); + } + } + + public InventoryItem FindItemWithoutFullStack(int vnum, short amount) + { + IGameItem gameItem = _itemsManager.GetItem(vnum); + if (gameItem == null) + { + return null; + } + + _lock.EnterReadLock(); + try + { + if (!_inventoryItemsByInventoryType.TryGetValue(gameItem.Type, out List items)) + { + return null; + } + + return items.Where(s => s?.ItemInstance != null && s.ItemInstance.ItemVNum == vnum && s.ItemInstance.Amount + amount <= 999).OrderBy(x => x.Slot).FirstOrDefault(); + } + finally + { + _lock.ExitReadLock(); + } + } + + public IEnumerable GetItemsByInventoryType(InventoryType type) + { + if (type == InventoryType.EquippedItems) + { + return Array.Empty(); + } + + _lock.EnterReadLock(); + try + { + if (_inventoryItemsByInventoryType.TryGetValue(type, out List items)) + { + return items; + } + + return Array.Empty(); + } + finally + { + _lock.ExitReadLock(); + } + } + + public IEnumerable EquippedItems => _wear; + public GameItemInstance GetItemInstanceFromEquipmentSlot(EquipmentType type) => _wear[(byte)type]?.ItemInstance; + + public GameItemInstance MainWeapon => _wear[(byte)EquipmentType.MainWeapon]?.ItemInstance; + public GameItemInstance SecondaryWeapon => _wear[(byte)EquipmentType.SecondaryWeapon]?.ItemInstance; + public GameItemInstance Armor => _wear[(byte)EquipmentType.Armor]?.ItemInstance; + public GameItemInstance Hat => _wear[(byte)EquipmentType.Hat]?.ItemInstance; + public GameItemInstance Amulet => _wear[(byte)EquipmentType.Amulet]?.ItemInstance; + public GameItemInstance Gloves => _wear[(byte)EquipmentType.Gloves]?.ItemInstance; + public GameItemInstance Ring => _wear[(byte)EquipmentType.Ring]?.ItemInstance; + public GameItemInstance Necklace => _wear[(byte)EquipmentType.Necklace]?.ItemInstance; + public GameItemInstance Bracelet => _wear[(byte)EquipmentType.Bracelet]?.ItemInstance; + public GameItemInstance Boots => _wear[(byte)EquipmentType.Boots]?.ItemInstance; + public GameItemInstance Fairy => _wear[(byte)EquipmentType.Fairy]?.ItemInstance; + public GameItemInstance Mask => _wear[(byte)EquipmentType.Mask]?.ItemInstance; + public GameItemInstance CostumeSuit => _wear[(byte)EquipmentType.CostumeSuit]?.ItemInstance; + public GameItemInstance CostumeHat => _wear[(byte)EquipmentType.CostumeHat]?.ItemInstance; + public GameItemInstance WeaponSkin => _wear[(byte)EquipmentType.WeaponSkin]?.ItemInstance; + public GameItemInstance Wings => _wear[(byte)EquipmentType.Wings]?.ItemInstance; + public GameItemInstance Specialist => _wear[(byte)EquipmentType.Sp]?.ItemInstance; + + public bool InventoryIsInitialized { get; } = false; + + public int CountItemWithVnum(int vnum) + { + IGameItem gameItem = _itemsManager.GetItem(vnum); + if (gameItem == null) + { + return default; + } + + _lock.EnterReadLock(); + try + { + if (!_inventoryItemsByInventoryType.TryGetValue(gameItem.Type, out List items)) + { + return 0; + } + + return items.Where(s => s?.ItemInstance != null && s.ItemInstance.ItemVNum == vnum).Sum(s => s.ItemInstance.Amount); + } + finally + { + _lock.ExitReadLock(); + } + } + + public bool HasSpaceFor(int vnum, short amount = 1) + { + IGameItem gameItem = _itemsManager.GetItem(vnum); + if (gameItem == null) + { + return false; + } + + _lock.EnterReadLock(); + try + { + if (!_inventoryItemsByInventoryType.TryGetValue(gameItem.Type, out List items)) + { + // no inventory in that InventoryType + return true; + } + + if (items == null || !items.Any()) + { + return true; + } + + // Find first free inventory slot - previous version was looking for by lists index, but now is by item slot + short slots = _character.GetInventorySlots(false, gameItem.Type); + + for (short i = 0; i < slots; i++) + { + InventoryItem freeSlot = items.FirstOrDefault(x => x != null && x.Slot == i); + if (freeSlot == null) + { + return true; + } + + if (freeSlot.ItemInstance == null) + { + continue; + } + + if (freeSlot.ItemInstance.GameItem.Id != vnum) + { + continue; + } + + if (freeSlot.ItemInstance.GameItem.IsNotStackableInventoryType() || freeSlot.ItemInstance.Amount + amount > 999) + { + continue; + } + + return true; + } + + return items.Count(s => s?.ItemInstance != null) <= _character.GetInventorySlots(true, gameItem.Type); + } + finally + { + _lock.ExitReadLock(); + } + } + + public bool HasItem(int vnum, short amount = 1) + { + IGameItem gameItem = _itemsManager.GetItem(vnum); + if (gameItem == null) + { + return false; + } + + _lock.EnterReadLock(); + try + { + if (!_inventoryItemsByInventoryType.TryGetValue(gameItem.Type, out List items)) + { + return false; + } + + return items.Where(s => s?.ItemInstance != null && s.ItemInstance.ItemVNum == vnum).Sum(s => s.ItemInstance.Amount) >= amount; + } + finally + { + _lock.ExitReadLock(); + } + } + + + public bool RemoveItemAmountByVnum(int vnum, short amount, out InventoryItem removedItem) + { + IGameItem gameItem = _itemsManager.GetItem(vnum); + if (gameItem == null) + { + removedItem = null; + return false; + } + + _lock.EnterWriteLock(); + try + { + InventoryItem getItem = GetFirstItemByVnum(vnum); + if (getItem == null) + { + removedItem = null; + return false; + } + + getItem.ItemInstance.Amount -= amount; + removedItem = getItem; + return true; + } + finally + { + _lock.ExitWriteLock(); + } + } + + public void EquipItem(InventoryItem item, EquipmentType type, bool force = false) + { + if (force == false) + { + RemoveItemFromSlotAndType(item.Slot, item.InventoryType, out InventoryItem _); + } + + item.Slot = (short)item.ItemInstance.GameItem.EquipmentSlot; + item.InventoryType = InventoryType.EquippedItems; + item.IsEquipped = true; + _wear[(byte)type] = item; + } + + public void TakeOffItem(EquipmentType type, short? slot = null, InventoryType? inventoryType = null) + { + InventoryItem item = _wear[(byte)type]; + if (item == null) + { + return; + } + + InventoryType invType = inventoryType ?? InventoryType.Equipment; + item.InventoryType = invType; + item.IsEquipped = false; + item.Slot = slot ?? _character.GetNextInventorySlot(invType); + AddItemToInventory(item); + _wear[(byte)type] = null; + } + + public void AddItemToInventory(InventoryItem inventoryItem) + { + InventoryType inventoryType = inventoryItem.InventoryType; + if (inventoryType == InventoryType.EquippedItems) + { + return; + } + + _lock.EnterReadLock(); + try + { + if (!_inventoryItemsByInventoryType.TryGetValue(inventoryType, out List item)) + { + item = new List(); + _inventoryItemsByInventoryType[inventoryType] = item; + } + + item.Add(inventoryItem); + } + finally + { + _lock.ExitReadLock(); + } + } + + public bool RemoveItemFromSlotAndType(short slot, InventoryType type, out InventoryItem removedItem) + { + if (type == InventoryType.EquippedItems) + { + removedItem = null; + return false; + } + + _lock.EnterReadLock(); + try + { + if (!_inventoryItemsByInventoryType.TryGetValue(type, out List inventoryItems)) + { + removedItem = null; + return false; + } + + InventoryItem item = inventoryItems.FirstOrDefault(x => x?.Slot == slot); + if (item == null) + { + removedItem = null; + return false; + } + + List items = _inventoryItemsByInventoryType.GetOrDefault(type); + if (items == null) + { + removedItem = null; + return false; + } + + items.Remove(item); + removedItem = item; + return true; + } + finally + { + _lock.ExitReadLock(); + } + } + + public InventoryItem GetInventoryItemFromEquipmentSlot(EquipmentType type) => _wear[(byte)type] == null ? null : _wear[(byte)type]; + + public InventoryItem GetItemBySlotAndType(short slot, InventoryType type) + { + if (type == InventoryType.EquippedItems) + { + return null; + } + + _lock.EnterReadLock(); + try + { + if (!_inventoryItemsByInventoryType.TryGetValue(type, out List item)) + { + return null; + } + + return item.FirstOrDefault(x => x?.Slot == slot); + } + finally + { + _lock.ExitReadLock(); + } + } + + public InventoryItem GetItemBySlotAndType(short slot, InventoryType type, bool force) + { + if (type == InventoryType.EquippedItems && force == false) + { + return null; + } + + _lock.EnterReadLock(); + try + { + if (!_inventoryItemsByInventoryType.TryGetValue(type, out List item)) + { + return null; + } + + return item.FirstOrDefault(x => x?.Slot == slot); + } + finally + { + _lock.ExitReadLock(); + } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Inventory/InventoryException.cs b/srcs/WingsAPI.Game/Inventory/InventoryException.cs new file mode 100644 index 0000000..5ea7c00 --- /dev/null +++ b/srcs/WingsAPI.Game/Inventory/InventoryException.cs @@ -0,0 +1,17 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace WingsEmu.Game.Inventory; + +public interface IItemUsageToggleManager +{ + Task IsItemBlocked(int vnum); + + Task BlockItemUsage(int vnum); + Task UnblockItemUsage(int vnum); + Task> GetBlockedItemUsages(); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Inventory/InventoryItem.cs b/srcs/WingsAPI.Game/Inventory/InventoryItem.cs new file mode 100644 index 0000000..4ec6aa3 --- /dev/null +++ b/srcs/WingsAPI.Game/Inventory/InventoryItem.cs @@ -0,0 +1,9 @@ +using WingsEmu.DTOs.Inventory; +using WingsEmu.Game.Items; + +namespace WingsEmu.Game.Inventory; + +public class InventoryItem : CharacterInventoryItemDto +{ + public GameItemInstance ItemInstance { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Inventory/PartnerInventoryComponent.cs b/srcs/WingsAPI.Game/Inventory/PartnerInventoryComponent.cs new file mode 100644 index 0000000..5b39c52 --- /dev/null +++ b/srcs/WingsAPI.Game/Inventory/PartnerInventoryComponent.cs @@ -0,0 +1,184 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using WingsEmu.DTOs.Bonus; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Items; +using WingsEmu.Game.Warehouse; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Inventory; + +public class PartnerInventoryComponent : IPartnerInventoryComponent +{ + private const int WEAR_SLOTS = 13; + private const byte PARTNER_SLOTS = 56; + + private readonly IPlayerEntity _character; + private readonly Dictionary _partnersInventory = new(); + private readonly PartnerWarehouseItem[] _warehouseItems = new PartnerWarehouseItem[PARTNER_SLOTS]; + + public PartnerInventoryComponent(IPlayerEntity character) => _character = character; + + public IReadOnlyList PartnerGetEquippedItems(short partnerSlot) => + !_partnersInventory.TryGetValue(partnerSlot, out PartnerInventoryItem[] items) ? Array.Empty() : items; + + public IReadOnlyList GetPartnersEquippedItems() + { + return _partnersInventory.Values.SelectMany(s => s).Where(item => item != null).ToList(); + } + + public void PartnerEquipItem(InventoryItem item, short partnerSlot) + { + if (item == null) + { + return; + } + + EquipmentType type = item.ItemInstance.GameItem.EquipmentSlot; + + if (type == EquipmentType.SecondaryWeapon) + { + type = EquipmentType.MainWeapon; + } + + var partnerItem = new PartnerInventoryItem + { + ItemInstance = item.ItemInstance, + PartnerSlot = partnerSlot + }; + + if (!_partnersInventory.TryGetValue(partnerSlot, out PartnerInventoryItem[] items)) + { + items = new PartnerInventoryItem[WEAR_SLOTS]; + _partnersInventory[partnerSlot] = items; + } + + items[(byte)type] = partnerItem; + } + + public void PartnerEquipItem(GameItemInstance item, short partnerSlot) + { + if (item == null) + { + return; + } + + EquipmentType type = item.GameItem.EquipmentSlot; + + if (type == EquipmentType.SecondaryWeapon) + { + type = EquipmentType.MainWeapon; + } + + var partnerItem = new PartnerInventoryItem + { + ItemInstance = item, + PartnerSlot = partnerSlot + }; + + if (!_partnersInventory.TryGetValue(partnerSlot, out PartnerInventoryItem[] items)) + { + items = new PartnerInventoryItem[WEAR_SLOTS]; + _partnersInventory[partnerSlot] = items; + } + + items[(byte)type] = partnerItem; + } + + public void PartnerTakeOffItem(EquipmentType type, short partnerSlot) + { + if (!_partnersInventory.TryGetValue(partnerSlot, out PartnerInventoryItem[] items)) + { + items = new PartnerInventoryItem[WEAR_SLOTS]; + _partnersInventory[partnerSlot] = items; + } + + items[(byte)type] = null; + } + + public PartnerInventoryItem PartnerGetEquippedItem(EquipmentType type, short partnerSlot) + { + if (!_partnersInventory.TryGetValue(partnerSlot, out PartnerInventoryItem[] items)) + { + items = new PartnerInventoryItem[WEAR_SLOTS]; + _partnersInventory[partnerSlot] = items; + } + + PartnerInventoryItem item = items[(byte)type]; + return item; + } + + public void AddPartnerWarehouseItem(GameItemInstance item, short slot) + { + var newWarehouseItem = new PartnerWarehouseItem + { + ItemInstance = item, + Slot = slot + }; + + _warehouseItems[newWarehouseItem.Slot] = newWarehouseItem; + } + + public void RemovePartnerWarehouseItem(short slot) + { + PartnerWarehouseItem partnerWarehouseItem = GetPartnerWarehouseItem(slot); + if (partnerWarehouseItem == null) + { + return; + } + + _warehouseItems[partnerWarehouseItem.Slot] = null; + } + + public PartnerWarehouseItem GetPartnerWarehouseItem(short slot) => _warehouseItems[slot]; + + public IReadOnlyList PartnerWarehouseItems() => _warehouseItems; + public byte GetPartnerWarehouseSlots() => _character.HaveStaticBonus(StaticBonusType.PartnerBackpack) ? PARTNER_SLOTS : (byte)0; + public byte GetPartnerWarehouseSlotsWithoutBackpack() => PARTNER_SLOTS; + + public bool HasSpaceForPartnerWarehouseItem() => _warehouseItems.Count(x => x != null) < PARTNER_SLOTS; + + public bool HasSpaceForPartnerItemWarehouse(int itemVnum, short amount = 1) + { + if (!_character.HaveStaticBonus(StaticBonusType.PartnerBackpack)) + { + return false; + } + + if (!_warehouseItems.Any()) + { + return true; + } + + PartnerWarehouseItem[] items = _warehouseItems.OrderBy(x => x?.Slot).ToArray(); + for (byte i = 0; i < PARTNER_SLOTS; i++) + { + PartnerWarehouseItem freeSlot = items.FirstOrDefault(x => x != null && x.Slot == i); + if (freeSlot == null) + { + return true; + } + + if (freeSlot.ItemInstance == null) + { + continue; + } + + if (freeSlot.ItemInstance.GameItem.Id != itemVnum) + { + continue; + } + + if (freeSlot.ItemInstance.GameItem.IsNotStackableInventoryType() || freeSlot.ItemInstance.Amount + amount > 999) + { + continue; + } + + return true; + } + + return HasSpaceForPartnerWarehouseItem(); + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Inventory/PartnerInventoryItem.cs b/srcs/WingsAPI.Game/Inventory/PartnerInventoryItem.cs new file mode 100644 index 0000000..b992cde --- /dev/null +++ b/srcs/WingsAPI.Game/Inventory/PartnerInventoryItem.cs @@ -0,0 +1,9 @@ +using WingsEmu.DTOs.Inventory; +using WingsEmu.Game.Items; + +namespace WingsEmu.Game.Inventory; + +public class PartnerInventoryItem : CharacterPartnerInventoryItemDto +{ + public GameItemInstance ItemInstance { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Items/CustomItem.cs b/srcs/WingsAPI.Game/Items/CustomItem.cs new file mode 100644 index 0000000..9494743 --- /dev/null +++ b/srcs/WingsAPI.Game/Items/CustomItem.cs @@ -0,0 +1,7 @@ +namespace WingsEmu.Game.Items; + +public class CustomItem +{ + public short Vnum { get; set; } + public ushort Amount { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Items/GameItem.cs b/srcs/WingsAPI.Game/Items/GameItem.cs new file mode 100644 index 0000000..6b1df69 --- /dev/null +++ b/srcs/WingsAPI.Game/Items/GameItem.cs @@ -0,0 +1,90 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsEmu.DTOs.Items; + +namespace WingsEmu.Game.Items; + +public class GameItem : ItemDTO, IGameItem +{ + public GameItem(ItemDTO item) + { + BCards = item.BCards; + Height = item.Height; + Width = item.Width; + MinilandObjectPoint = item.MinilandObjectPoint; + BasicUpgrade = item.BasicUpgrade; + CellonLvl = item.CellonLvl; + Class = item.Class; + CloseDefence = item.CloseDefence; + Color = item.Color; + Concentrate = item.Concentrate; + CriticalRate = item.CriticalRate; + CriticalLuckRate = item.CriticalLuckRate; + DamageMaximum = item.DamageMaximum; + DamageMinimum = item.DamageMinimum; + DarkElement = item.DarkElement; + DarkResistance = item.DarkResistance; + DefenceDodge = item.DefenceDodge; + DistanceDefence = item.DistanceDefence; + DistanceDefenceDodge = item.DistanceDefenceDodge; + Effect = item.Effect; + EffectValue = item.EffectValue; + Element = item.Element; + ElementRate = item.ElementRate; + EquipmentSlot = item.EquipmentSlot; + FireElement = item.FireElement; + FireResistance = item.FireResistance; + HitRate = item.HitRate; + Hp = item.Hp; + HpRegeneration = item.HpRegeneration; + IsMinilandActionable = item.IsMinilandActionable; + IsColorable = item.IsColorable; + IsConsumable = item.IsConsumable; + IsDroppable = item.IsDroppable; + IsHeroic = item.IsHeroic; + IsWarehouse = item.IsWarehouse; + IsSoldable = item.IsSoldable; + IsTradable = item.IsTradable; + ShowWarningOnUse = item.ShowWarningOnUse; + ItemSubType = item.ItemSubType; + ItemType = item.ItemType; + ItemValidTime = item.ItemValidTime; + LevelJobMinimum = item.LevelJobMinimum; + LevelMinimum = item.LevelMinimum; + LightElement = item.LightElement; + LightResistance = item.LightResistance; + MagicDefence = item.MagicDefence; + MaxCellon = item.MaxCellon; + MaxCellonLvl = item.MaxCellonLvl; + MaxElementRate = item.MaxElementRate; + MaximumAmmo = item.MaximumAmmo; + MoreHp = item.MoreHp; + MoreMp = item.MoreMp; + Morph = item.Morph; + Mp = item.Mp; + MpRegeneration = item.MpRegeneration; + Name = item.Name; + Price = item.Price; + ReputationMinimum = item.ReputationMinimum; + ReputPrice = item.ReputPrice; + Sex = item.Sex; + Speed = item.Speed; + SpPointsUsage = item.SpPointsUsage; + Type = item.Type; + Id = item.Id; + WaitDelay = item.WaitDelay; + WaterElement = item.WaterElement; + WaterResistance = item.WaterResistance; + IsPartnerSpecialist = item.IsPartnerSpecialist; + PartnerClass = item.PartnerClass; + SpMorphId = item.SpMorphId; + LeftUsages = item.LeftUsages; + ItemLeftType = item.ItemLeftType; + ShellType = item.ShellType; + ShellMinimumLevel = item.ShellMinimumLevel; + ShellMaximumLevel = item.ShellMaximumLevel; + Data = item.Data; + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Items/GameItemInstance.cs b/srcs/WingsAPI.Game/Items/GameItemInstance.cs new file mode 100644 index 0000000..bbdc9ff --- /dev/null +++ b/srcs/WingsAPI.Game/Items/GameItemInstance.cs @@ -0,0 +1,68 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Generic; +using WingsEmu.DTOs.Items; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Skills; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Items; + +public class GameItemInstance : ItemInstanceDTO +{ + private IGameItem _gameItem; + + private long _transportId; + + public GameItemInstance() + { + } + + public GameItemInstance(int vNum, int amount, byte upgrade, short rarity, short design) + { + ItemVNum = vNum; + Amount = amount; + Upgrade = upgrade; + Rarity = rarity; + Design = design; + } + + private static IItemsManager _itemManager => new Lazy(() => StaticItemsManager.Instance).Value; + + public long TransportId + { + get + { + if (_transportId == 0) + { + // create transportId thru factory + _transportId = TransportFactory.Instance.GenerateTransportId(); + } + + return _transportId; + } + } + + public int DamageMaximum => WeaponMaxDamageAdditionalValue; + public int DamageMinimum => WeaponMinDamageAdditionalValue; + public int CloseDefence => ArmorMeleeAdditionalValue; + public int DefenceDodge => ArmorDodgeAdditionalValue; + public int DistanceDefenceDodge => ArmorDodgeAdditionalValue; + public int HitRate => WeaponHitRateAdditionalValue; + public int DistanceDefence => ArmorRangeAdditionalValue; + public int MagicDefence => ArmorMagicAdditionalValue; + + public bool IsBound => BoundCharacterId.HasValue && GameItem.ItemType != ItemType.Armor && GameItem.ItemType != ItemType.Weapon; + + public IGameItem GameItem => !OriginalItemVnum.HasValue + ? _gameItem ??= _itemManager.GetItem(ItemVNum) + : _gameItem != null && _gameItem.Id == OriginalItemVnum.Value + ? _gameItem + : _gameItem = _itemManager.GetItem(OriginalItemVnum.Value); + + public List PartnerSkills { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Items/IDropRarityConfigurationProvider.cs b/srcs/WingsAPI.Game/Items/IDropRarityConfigurationProvider.cs new file mode 100644 index 0000000..a3e4bba --- /dev/null +++ b/srcs/WingsAPI.Game/Items/IDropRarityConfigurationProvider.cs @@ -0,0 +1,8 @@ +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Items; + +public interface IDropRarityConfigurationProvider +{ + sbyte GetRandomRarity(ItemType itemType); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Items/IGameItem.cs b/srcs/WingsAPI.Game/Items/IGameItem.cs new file mode 100644 index 0000000..4bade52 --- /dev/null +++ b/srcs/WingsAPI.Game/Items/IGameItem.cs @@ -0,0 +1,97 @@ +using System.Collections.Generic; +using WingsAPI.Packets.Enums.Shells; +using WingsEmu.DTOs.BCards; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Battle; + +namespace WingsEmu.Game.Items; + +public interface IGameItem +{ + List BCards { get; } + int Id { get; } + byte BasicUpgrade { get; } + byte CellonLvl { get; } + byte Class { get; } + short CloseDefence { get; } + byte Color { get; } + short Concentrate { get; } + sbyte CriticalLuckRate { get; } + short CriticalRate { get; } + short DamageMaximum { get; } + short DamageMinimum { get; } + byte DarkElement { get; } + short DarkResistance { get; } + short DefenceDodge { get; } + short DistanceDefence { get; } + short DistanceDefenceDodge { get; } + short Effect { get; } + int EffectValue { get; } + byte Element { get; } + short ElementRate { get; } + EquipmentType EquipmentSlot { get; } + short FireResistance { get; } + byte Height { get; } + short HitRate { get; } + short Hp { get; } + short HpRegeneration { get; } + bool IsMinilandActionable { get; } + bool IsColorable { get; } + bool IsTimeSpaceRewardBox { get; } + bool ShowDescriptionOnHover { get; } + bool Flag3 { get; } + bool FollowMouseOnUse { get; } + bool ShowSomethingOnHover { get; } + bool PlaySoundOnPickup { get; } + bool Flag7 { get; } + bool IsLimited { get; } + bool IsConsumable { get; } + bool IsDroppable { get; } + bool IsHeroic { get; } + bool ShowWarningOnUse { get; } + bool IsWarehouse { get; } + bool IsSoldable { get; } + bool IsTradable { get; } + byte ItemSubType { get; } + ItemType ItemType { get; } + long ItemValidTime { get; } + byte LevelJobMinimum { get; } + byte LevelMinimum { get; } + byte LightElement { get; } + short LightResistance { get; } + short MagicDefence { get; } + byte MaxCellon { get; } + byte MaxCellonLvl { get; } + short MaxElementRate { get; } + byte MaximumAmmo { get; } + int MinilandObjectPoint { get; } + short MoreHp { get; } + short MoreMp { get; } + short Morph { get; } + short Mp { get; } + short MpRegeneration { get; } + string Name { get; } + long Price { get; } + byte ReputationMinimum { get; } + long ReputPrice { get; } + byte Sex { get; } + byte Speed { get; } + byte SpPointsUsage { get; } + InventoryType Type { get; } + short WaitDelay { get; } + byte WaterElement { get; } + short WaterResistance { get; } + byte Width { get; } + AttackType AttackType { get; } + bool UseReputationAsPrice { get; } + byte PartnerClass { get; } + bool IsPartnerSpecialist { get; } + byte SpMorphId { get; } + short ItemLeftType { get; } + int LeftUsages { get; } + int IconId { get; } + short ShellMinimumLevel { get; } + short ShellMaximumLevel { get; } + ShellType ShellType { get; } + int[] Data { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Items/IGameItemInstanceFactory.cs b/srcs/WingsAPI.Game/Items/IGameItemInstanceFactory.cs new file mode 100644 index 0000000..bb99cb0 --- /dev/null +++ b/srcs/WingsAPI.Game/Items/IGameItemInstanceFactory.cs @@ -0,0 +1,19 @@ +using WingsEmu.DTOs.Items; + +namespace WingsEmu.Game.Items; + +public interface IGameItemInstanceFactory +{ + GameItemInstance CreateItem(ItemInstanceDTO dto); + ItemInstanceDTO CreateDto(GameItemInstance instance); + + GameItemInstance CreateItem(int itemVnum); + GameItemInstance CreateItem(int itemVnum, bool isMateLimited); + GameItemInstance CreateItem(int itemVnum, int amount); + GameItemInstance CreateItem(int itemVnum, int amount, byte upgrade); + GameItemInstance CreateItem(int itemVnum, int amount, byte upgrade, sbyte rare); + GameItemInstance CreateItem(int itemVnum, int amount, byte upgrade, sbyte rare, byte design, bool isMateLimited = false); + + GameItemInstance CreateSpecialistCard(int itemVnum, byte spLevel = 1, byte upgrade = 0, byte design = 0); + GameItemInstance DuplicateItem(GameItemInstance gameInstance); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Items/IItemBoxManager.cs b/srcs/WingsAPI.Game/Items/IItemBoxManager.cs new file mode 100644 index 0000000..217c173 --- /dev/null +++ b/srcs/WingsAPI.Game/Items/IItemBoxManager.cs @@ -0,0 +1,9 @@ +using WingsEmu.DTOs.ServerDatas; + +namespace WingsEmu.Game.Items; + +public interface IItemBoxManager +{ + ItemBoxDto GetItemBoxByItemVnumAndDesign(int itemVnum); + void Initialize(); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Logs/IPlayerActionLog.cs b/srcs/WingsAPI.Game/Logs/IPlayerActionLog.cs new file mode 100644 index 0000000..60aed20 --- /dev/null +++ b/srcs/WingsAPI.Game/Logs/IPlayerActionLog.cs @@ -0,0 +1,12 @@ +using System; + +namespace WingsEmu.Game.Logs; + +public interface IPlayerActionLog +{ + DateTime CreatedAt { get; init; } + int ChannelId { get; init; } + long CharacterId { get; init; } + string CharacterName { get; init; } + string IpAddress { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Logs/IPlayerActionManager.cs b/srcs/WingsAPI.Game/Logs/IPlayerActionManager.cs new file mode 100644 index 0000000..fbb04af --- /dev/null +++ b/srcs/WingsAPI.Game/Logs/IPlayerActionManager.cs @@ -0,0 +1,6 @@ +namespace WingsEmu.Game.Logs; + +public interface IPlayerLogManager +{ + void AddLog(T message) where T : IPlayerActionLog; +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Mails/CharacterMail.cs b/srcs/WingsAPI.Game/Mails/CharacterMail.cs new file mode 100644 index 0000000..80b4924 --- /dev/null +++ b/srcs/WingsAPI.Game/Mails/CharacterMail.cs @@ -0,0 +1,22 @@ +using WingsEmu.DTOs.Mails; +using WingsEmu.Game.Items; + +namespace WingsEmu.Game.Mails; + +public class CharacterMail : CharacterMailDto +{ + public CharacterMail(CharacterMailDto mailDto, byte mailSlot, GameItemInstance itemInstance) + { + MailSlot = mailSlot; + ItemInstance = itemInstance; + Id = mailDto.Id; + Date = mailDto.Date; + SenderName = mailDto.SenderName; + ReceiverId = mailDto.ReceiverId; + MailGiftType = mailDto.MailGiftType; + } + + public byte MailSlot { get; } + + public GameItemInstance ItemInstance { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Mails/CharacterNote.cs b/srcs/WingsAPI.Game/Mails/CharacterNote.cs new file mode 100644 index 0000000..28368ad --- /dev/null +++ b/srcs/WingsAPI.Game/Mails/CharacterNote.cs @@ -0,0 +1,29 @@ +using WingsEmu.DTOs.Mails; + +namespace WingsEmu.Game.Mails; + +public class CharacterNote : CharacterNoteDto +{ + public CharacterNote(CharacterNoteDto noteDto, byte noteSlot) + { + NoteSlot = noteSlot; + + Id = noteDto.Id; + Date = noteDto.Date; + SenderId = noteDto.SenderId; + ReceiverId = noteDto.ReceiverId; + Title = noteDto.Title; + Message = noteDto.Message; + EquipmentPackets = noteDto.EquipmentPackets; + IsSenderCopy = noteDto.IsSenderCopy; + IsOpened = noteDto.IsOpened; + SenderGender = noteDto.SenderGender; + SenderClass = noteDto.SenderClass; + SenderHairColor = noteDto.SenderHairColor; + SenderHairStyle = noteDto.SenderHairStyle; + SenderName = noteDto.SenderName; + ReceiverName = noteDto.ReceiverName; + } + + public byte NoteSlot { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Mails/Events/MailClaimedEvent.cs b/srcs/WingsAPI.Game/Mails/Events/MailClaimedEvent.cs new file mode 100644 index 0000000..eb4bb24 --- /dev/null +++ b/srcs/WingsAPI.Game/Mails/Events/MailClaimedEvent.cs @@ -0,0 +1,11 @@ +using WingsEmu.DTOs.Items; +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Mails.Events; + +public class MailClaimedEvent : PlayerEvent +{ + public long MailId { get; init; } + public string SenderName { get; init; } + public ItemInstanceDTO ItemInstance { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Mails/Events/MailCreateEvent.cs b/srcs/WingsAPI.Game/Mails/Events/MailCreateEvent.cs new file mode 100644 index 0000000..a98a19b --- /dev/null +++ b/srcs/WingsAPI.Game/Mails/Events/MailCreateEvent.cs @@ -0,0 +1,24 @@ +using WingsEmu.DTOs.Mails; +using WingsEmu.Game._packetHandling; +using WingsEmu.Game.Items; + +namespace WingsEmu.Game.Mails.Events; + +public class MailCreateEvent : PlayerEvent +{ + public MailCreateEvent(string senderName, long receiverId, MailGiftType mailGiftType, GameItemInstance itemInstance) + { + SenderName = senderName; + ReceiverId = receiverId; + MailGiftType = mailGiftType; + ItemInstance = itemInstance; + } + + public string SenderName { get; } + + public long ReceiverId { get; } + + public MailGiftType MailGiftType { get; } + + public GameItemInstance ItemInstance { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Mails/Events/MailOpenEvent.cs b/srcs/WingsAPI.Game/Mails/Events/MailOpenEvent.cs new file mode 100644 index 0000000..6b180c7 --- /dev/null +++ b/srcs/WingsAPI.Game/Mails/Events/MailOpenEvent.cs @@ -0,0 +1,10 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Mails.Events; + +public class MailOpenEvent : PlayerEvent +{ + public MailOpenEvent(long mailId) => MailId = mailId; + + public long MailId { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Mails/Events/MailRemoveEvent.cs b/srcs/WingsAPI.Game/Mails/Events/MailRemoveEvent.cs new file mode 100644 index 0000000..3108b71 --- /dev/null +++ b/srcs/WingsAPI.Game/Mails/Events/MailRemoveEvent.cs @@ -0,0 +1,10 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Mails.Events; + +public class MailRemoveEvent : PlayerEvent +{ + public MailRemoveEvent(long mailId) => MailId = mailId; + + public long MailId { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Mails/Events/MailRemovedEvent.cs b/srcs/WingsAPI.Game/Mails/Events/MailRemovedEvent.cs new file mode 100644 index 0000000..e376d09 --- /dev/null +++ b/srcs/WingsAPI.Game/Mails/Events/MailRemovedEvent.cs @@ -0,0 +1,11 @@ +using WingsEmu.DTOs.Items; +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Mails.Events; + +public class MailRemovedEvent : PlayerEvent +{ + public long MailId { get; init; } + public string SenderName { get; init; } + public ItemInstanceDTO ItemInstance { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Mails/Events/NoteCreateEvent.cs b/srcs/WingsAPI.Game/Mails/Events/NoteCreateEvent.cs new file mode 100644 index 0000000..cc3c838 --- /dev/null +++ b/srcs/WingsAPI.Game/Mails/Events/NoteCreateEvent.cs @@ -0,0 +1,19 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Mails.Events; + +public class NoteCreateEvent : PlayerEvent +{ + public NoteCreateEvent(string receiverName, string title, string message) + { + ReceiverName = receiverName; + Title = title; + Message = message; + } + + public string ReceiverName { get; } + + public string Title { get; } + + public string Message { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Mails/Events/NoteOpenEvent.cs b/srcs/WingsAPI.Game/Mails/Events/NoteOpenEvent.cs new file mode 100644 index 0000000..78ad24d --- /dev/null +++ b/srcs/WingsAPI.Game/Mails/Events/NoteOpenEvent.cs @@ -0,0 +1,15 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Mails.Events; + +public class NoteOpenEvent : PlayerEvent +{ + public NoteOpenEvent(long noteId, bool isSenderCopy) + { + NoteId = noteId; + IsSenderCopy = isSenderCopy; + } + + public long NoteId { get; } + public bool IsSenderCopy { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Mails/Events/NoteRemoveEvent.cs b/srcs/WingsAPI.Game/Mails/Events/NoteRemoveEvent.cs new file mode 100644 index 0000000..cb1b631 --- /dev/null +++ b/srcs/WingsAPI.Game/Mails/Events/NoteRemoveEvent.cs @@ -0,0 +1,15 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Mails.Events; + +public class NoteRemoveEvent : PlayerEvent +{ + public NoteRemoveEvent(long noteId, bool isSenderCopy) + { + NoteId = noteId; + IsSenderCopy = isSenderCopy; + } + + public long NoteId { get; } + public bool IsSenderCopy { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Mails/Events/NoteSentEvent.cs b/srcs/WingsAPI.Game/Mails/Events/NoteSentEvent.cs new file mode 100644 index 0000000..6794879 --- /dev/null +++ b/srcs/WingsAPI.Game/Mails/Events/NoteSentEvent.cs @@ -0,0 +1,11 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Mails.Events; + +public class NoteSentEvent : PlayerEvent +{ + public long NoteId { get; init; } + public string ReceiverName { get; init; } + public string Title { get; set; } + public string Message { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Mails/IMailNoteComponent.cs b/srcs/WingsAPI.Game/Mails/IMailNoteComponent.cs new file mode 100644 index 0000000..9d52575 --- /dev/null +++ b/srcs/WingsAPI.Game/Mails/IMailNoteComponent.cs @@ -0,0 +1,63 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using WingsEmu.Core.Generics; + +namespace WingsEmu.Game.Mails; + +public interface IMailNoteComponent +{ + void AddMail(CharacterMail mailDto); + void RemoveMail(CharacterMail mailDto); + CharacterMail GetMail(long mailId); + IEnumerable GetMails(); + + void AddNote(CharacterNote noteDto); + void RemoveNote(CharacterNote noteDto); + CharacterNote GetNote(long noteId, bool isSenderCopy); + IEnumerable GetNotes(); +} + +public class MailNoteComponent : IMailNoteComponent +{ + private readonly ThreadSafeList _mailLists = new(); + private readonly ConcurrentDictionary _mails = new(); + private readonly ThreadSafeList _noteLists = new(); + private readonly ConcurrentDictionary _notes = new(); + + public void AddMail(CharacterMail mailDto) + { + _mails.TryAdd(mailDto.MailSlot, mailDto); + _mailLists.Add(mailDto); + } + + public void RemoveMail(CharacterMail mailDto) + { + _mails.TryRemove(mailDto.MailSlot, out _); + _mailLists.Remove(mailDto); + } + + public CharacterMail GetMail(long mailId) => _mails.TryGetValue(mailId, out CharacterMail mail) ? mail : null; + + public IEnumerable GetMails() => _mailLists; + + public void AddNote(CharacterNote noteDto) + { + _notes.TryAdd(noteDto.NoteSlot, (noteDto, noteDto.IsSenderCopy)); + _noteLists.Add(noteDto); + } + + public void RemoveNote(CharacterNote noteDto) + { + _notes.TryRemove(noteDto.NoteSlot, out _); + _noteLists.Remove(noteDto); + } + + public CharacterNote GetNote(long noteId, bool isSenderCopy) + { + CharacterNote note = _noteLists.FirstOrDefault(x => x.NoteSlot == noteId && isSenderCopy == x.IsSenderCopy); + return note; + } + + public IEnumerable GetNotes() => _noteLists; +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Managers/BubbleComponent.cs b/srcs/WingsAPI.Game/Managers/BubbleComponent.cs new file mode 100644 index 0000000..9a6d0ef --- /dev/null +++ b/srcs/WingsAPI.Game/Managers/BubbleComponent.cs @@ -0,0 +1,14 @@ +namespace WingsEmu.Game.Managers; + +public class BubbleComponent : IBubbleComponent +{ + private string _bubbleMessage; + + public void SaveBubble(string message) => _bubbleMessage = message; + + public bool IsUsingBubble() => !string.IsNullOrEmpty(_bubbleMessage); + + public string GetMessage() => _bubbleMessage; + + public void RemoveBubble() => _bubbleMessage = null; +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Managers/IAct4Manager.cs b/srcs/WingsAPI.Game/Managers/IAct4Manager.cs new file mode 100644 index 0000000..e3c580a --- /dev/null +++ b/srcs/WingsAPI.Game/Managers/IAct4Manager.cs @@ -0,0 +1,30 @@ +using System; +using WingsAPI.Packets.Enums.Act4; +using WingsEmu.Game.Act4; +using WingsEmu.Game.Entities; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Managers; + +public interface IAct4Manager +{ + bool FactionPointsLocked { get; } + void AddFactionPoints(FactionType factionType, int amount); + + void ResetFactionPoints(FactionType factionType); + + void RegisterMukraju(DateTime current, IMonsterEntity mukraju, FactionType factionType); + + (DateTime deleteTime, IMonsterEntity mukraju, FactionType mukrajuFactionType) GetMukraju(); + + IMonsterEntity UnregisterMukraju(); + + FactionType MukrajuFaction(); + + FactionType GetTriumphantFaction(); + + Act4Status GetStatus(); +} + +public sealed record Act4Status(byte AngelPointsPercentage, byte DemonPointsPercentage, TimeSpan TimeBeforeReset, FactionType RelevantFaction, Act4FactionStateType FactionStateType, + TimeSpan CurrentTimeBeforeMukrajuDespawn, TimeSpan TimeBeforeMukrajuDespawn, DungeonType DungeonType); \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Managers/IBazaarManager.cs b/srcs/WingsAPI.Game/Managers/IBazaarManager.cs new file mode 100644 index 0000000..ae8c18d --- /dev/null +++ b/srcs/WingsAPI.Game/Managers/IBazaarManager.cs @@ -0,0 +1 @@ +namespace WingsEmu.Game.Managers; diff --git a/srcs/WingsAPI.Game/Managers/IBubbleComponent.cs b/srcs/WingsAPI.Game/Managers/IBubbleComponent.cs new file mode 100644 index 0000000..ed8e5ec --- /dev/null +++ b/srcs/WingsAPI.Game/Managers/IBubbleComponent.cs @@ -0,0 +1,9 @@ +namespace WingsEmu.Game.Managers; + +public interface IBubbleComponent +{ + public void SaveBubble(string message); + public bool IsUsingBubble(); + public string GetMessage(); + public void RemoveBubble(); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Managers/IDelayManager.cs b/srcs/WingsAPI.Game/Managers/IDelayManager.cs new file mode 100644 index 0000000..5cdfcdc --- /dev/null +++ b/srcs/WingsAPI.Game/Managers/IDelayManager.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using WingsEmu.Core.Extensions; +using WingsEmu.Game.Entities; + +namespace WingsEmu.Game.Managers; + +public interface IDelayConfiguration +{ + TimeSpan GetDelayByAction(DelayedActionType type); +} + +public class DelayConfiguration : IDelayConfiguration +{ + private static readonly TimeSpan Default = TimeSpan.FromSeconds(3); + + private readonly Dictionary _times = new() + { + [DelayedActionType.SummonPet] = Default, + [DelayedActionType.KickPet] = Default, + [DelayedActionType.EquipVehicle] = Default, + [DelayedActionType.IceBreakerUnfreeze] = Default, + [DelayedActionType.PartnerWearSp] = TimeSpan.FromSeconds(5), + [DelayedActionType.PartnerLearnSkill] = TimeSpan.FromSeconds(5), + [DelayedActionType.WearSp] = TimeSpan.FromSeconds(5), + [DelayedActionType.ReturnWing] = TimeSpan.FromSeconds(5), + [DelayedActionType.ReturnAmulet] = TimeSpan.FromSeconds(5), + [DelayedActionType.MinilandBell] = TimeSpan.FromSeconds(5), + [DelayedActionType.BaseTeleporter] = TimeSpan.FromSeconds(5), + [DelayedActionType.LodScroll] = TimeSpan.FromSeconds(5), + [DelayedActionType.PartnerResetSkill] = TimeSpan.FromSeconds(5), + [DelayedActionType.PartnerResetAllSkills] = TimeSpan.FromSeconds(5), + [DelayedActionType.WingOfFriendship] = Default, + [DelayedActionType.ButtonSwitch] = TimeSpan.FromSeconds(2), + [DelayedActionType.Mining] = default, + [DelayedActionType.SealedVessel] = TimeSpan.FromSeconds(2), + [DelayedActionType.RainbowBattleCaptureFlag] = TimeSpan.FromSeconds(5), + [DelayedActionType.RainbowBattleUnfreeze] = TimeSpan.FromSeconds(5) + }; + + public TimeSpan GetDelayByAction(DelayedActionType type) => _times.GetOrDefault(type, Default); +} + +public interface IDelayManager +{ + ValueTask RegisterAction(IBattleEntity entity, DelayedActionType action, TimeSpan time = default); + ValueTask CanPerformAction(IBattleEntity entity, DelayedActionType type); + ValueTask CompleteAction(IBattleEntity entity, DelayedActionType action); +} + +public enum DelayedActionType +{ + KickPet, + SummonPet, + EquipVehicle, + WearSp, + ReturnWing, + ReturnAmulet, + MinilandBell, + LodScroll, + ReturnScroll, + MorphScroll, + UseTeleporter, + BaseTeleporter, + PartnerWearSp, + PartnerLearnSkill, + PartnerResetSkill, + PartnerResetAllSkills, + IceBreakerUnfreeze, + WingOfFriendship, + ButtonSwitch, + Mining, + SealedVessel, + RainbowBattleCaptureFlag, + RainbowBattleUnfreeze +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Managers/IForbiddenNamesManager.cs b/srcs/WingsAPI.Game/Managers/IForbiddenNamesManager.cs new file mode 100644 index 0000000..ffff236 --- /dev/null +++ b/srcs/WingsAPI.Game/Managers/IForbiddenNamesManager.cs @@ -0,0 +1,13 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Threading.Tasks; + +namespace WingsEmu.Game.Managers; + +public interface IForbiddenNamesManager +{ + bool IsBanned(string name, out string s); + Task Reload(); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Managers/IItemUsageManager.cs b/srcs/WingsAPI.Game/Managers/IItemUsageManager.cs new file mode 100644 index 0000000..f263daa --- /dev/null +++ b/srcs/WingsAPI.Game/Managers/IItemUsageManager.cs @@ -0,0 +1,24 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; + +namespace WingsEmu.Game.Managers; + +public interface IItemUsageManager +{ + int GetLastItemUsed(long characterId); + void SetLastItemUsed(long characterId, int vNum); +} + +public class ItemUsageManager : IItemUsageManager +{ + private readonly ConcurrentDictionary _lastItemUsed; + + public ItemUsageManager() => _lastItemUsed = new ConcurrentDictionary(); + + public int GetLastItemUsed(long characterId) => _lastItemUsed.GetValueOrDefault(characterId); + + public void SetLastItemUsed(long characterId, int vNum) + { + _lastItemUsed[characterId] = vNum; + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Managers/IRankingManager.cs b/srcs/WingsAPI.Game/Managers/IRankingManager.cs new file mode 100644 index 0000000..554b7a4 --- /dev/null +++ b/srcs/WingsAPI.Game/Managers/IRankingManager.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using WingsAPI.Data.Character; + +namespace WingsEmu.Game.Managers; + +public interface IRankingManager +{ + IReadOnlyList TopCompliment { get; } + IReadOnlyList TopPoints { get; } + IReadOnlyList TopReputation { get; } + + Task TryRefreshRanking(); + void RefreshRanking(IReadOnlyList topComplimented, IReadOnlyList topPoints, IReadOnlyList topReputation); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Managers/IRevivalManager.cs b/srcs/WingsAPI.Game/Managers/IRevivalManager.cs new file mode 100644 index 0000000..41fa8fc --- /dev/null +++ b/srcs/WingsAPI.Game/Managers/IRevivalManager.cs @@ -0,0 +1,14 @@ +using System; + +namespace WingsEmu.Game.Managers; + +public interface IRevivalManager +{ + Guid RegisterRevival(long id); + + bool UnregisterRevival(long id, Guid guid); + + bool UnregisterRevival(long id); + + void TryUnregisterRevival(long id); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Managers/IScriptedInstanceManager.cs b/srcs/WingsAPI.Game/Managers/IScriptedInstanceManager.cs new file mode 100644 index 0000000..9664405 --- /dev/null +++ b/srcs/WingsAPI.Game/Managers/IScriptedInstanceManager.cs @@ -0,0 +1,15 @@ +namespace WingsEmu.Game.Managers; + +public class StaticScriptedInstanceManager +{ + public static IScriptedInstanceManager Instance { get; private set; } + + public static void Initialize(IScriptedInstanceManager manager) + { + Instance = manager; + } +} + +public interface IScriptedInstanceManager +{ +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Managers/IServerManager.cs b/srcs/WingsAPI.Game/Managers/IServerManager.cs new file mode 100644 index 0000000..d2b5556 --- /dev/null +++ b/srcs/WingsAPI.Game/Managers/IServerManager.cs @@ -0,0 +1,64 @@ +using System.Threading; + +namespace WingsEmu.Game.Managers; + +public class StaticServerManager +{ + public static IServerManager Instance { get; private set; } + + public static void Initialize(IServerManager instance) + { + Instance = instance; + } +} + +public enum GameServerState +{ + ERROR, + STARTING, + RUNNING, + IDLE, + STOPPING +} + +public interface IServerManager +{ + GameServerState State { get; } + bool IsRunning { get; } + int ChannelId { get; } + int MobDropRate { get; } + int MobDropChance { get; } + int FamilyExpRate { get; } + bool ExpEvent { get; set; } + int FairyXpRate { get; set; } + int GoldDropRate { get; set; } + int GoldRate { get; set; } + int GoldDropChance { get; set; } + int GenericDropRate { get; set; } + int GenericDropChance { get; set; } + int ReputRate { get; set; } + int HeroicStartLevel { get; set; } + int HeroXpRate { get; set; } + long MaxGold { get; set; } + long MaxBankGold { get; set; } + short MaxHeroLevel { get; set; } + short MaxJobLevel { get; set; } + short MaxLevel { get; set; } + short MaxSpLevel { get; set; } + int MateXpRate { get; set; } + int PartnerXpRate { get; set; } + short MaxMateLevel { get; set; } + short MaxNpcTalkRange { get; set; } + int MaxBasicSpPoints { get; set; } + int MaxAdditionalSpPoints { get; set; } + string ServerGroup { get; } + int MobXpRate { get; set; } + int AccountLimit { get; } + bool InShutdown { get; } + int JobXpRate { get; set; } + void InitializeAsync(); + void ListenCancellation(CancellationTokenSource stopServiceTokenSource); + void TryStart(); + void PutIdle(); + void Shutdown(); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Managers/ISessionManager.cs b/srcs/WingsAPI.Game/Managers/ISessionManager.cs new file mode 100644 index 0000000..65bdb54 --- /dev/null +++ b/srcs/WingsAPI.Game/Managers/ISessionManager.cs @@ -0,0 +1,76 @@ +using System.Threading.Tasks; +using WingsAPI.Communication.Player; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Game.Managers; + +public class StaticSessionManager +{ + public static ISessionManager Instance { get; private set; } + + public static void Initialize(ISessionManager manager) + { + Instance = manager; + } +} + +public interface ISessionManager : IBroadcaster +{ + int SessionsCount { get; } + + ValueTask GetOnlineCharacterById(long characterId); + ClusterCharacterInfo GetOnlineCharacterByName(string characterName); + bool IsOnline(string charName); + bool IsOnline(long characterId); + void AddOnline(ClusterCharacterInfo clusterCharacterInfo); + void RemoveOnline(string charName, long characterId); + + /// + /// Disconnects all sessions from the current channel + /// + /// + Task DisconnectAllAsync(); + + /// + /// Returns the IClientSession specified + /// by the character name passed as parameter + /// + /// + /// + IClientSession GetSessionByCharacterName(string name); + + /// + /// Kicks a player using the character name + /// passed as parameter + /// + /// + /// + Task KickAsync(string characterName); + + /// + /// Kicks a player using the account id + /// passed as parameter + /// + /// + /// + Task KickAsync(long accountId); + + /// + /// Returns the IClientSession based on the CharacterId provided as parameter + /// + /// + /// + IClientSession GetSessionByCharacterId(long id); + + /// + /// Registers a new session + /// + /// + void RegisterSession(IClientSession session); + + /// + /// Unregisters a session + /// + /// + void UnregisterSession(IClientSession session); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Managers/ServerData/IDropManager.cs b/srcs/WingsAPI.Game/Managers/ServerData/IDropManager.cs new file mode 100644 index 0000000..8f4be03 --- /dev/null +++ b/srcs/WingsAPI.Game/Managers/ServerData/IDropManager.cs @@ -0,0 +1,29 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using System.Threading.Tasks; +using WingsAPI.Data.Drops; +using WingsEmu.Game._enum; + +namespace WingsEmu.Game.Managers.ServerData; + +public class StaticDropManager +{ + public static IDropManager Instance { get; private set; } + + public static void Initialize(IDropManager dropManager) + { + Instance = dropManager; + } +} + +public interface IDropManager +{ + IEnumerable GetGeneralDrops(); + IReadOnlyList GetDropsByMapId(int mapId); + IReadOnlyList GetDropsByMonsterVnum(int monsterVnum); + IReadOnlyList GetDropsByMonsterRace(MonsterRaceType monsterRaceType, byte monsterSubRaceType); + Task InitializeAsync(); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Managers/ServerData/IMapMonsterManager.cs b/srcs/WingsAPI.Game/Managers/ServerData/IMapMonsterManager.cs new file mode 100644 index 0000000..22627af --- /dev/null +++ b/srcs/WingsAPI.Game/Managers/ServerData/IMapMonsterManager.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using WingsEmu.DTOs.Maps; + +namespace WingsEmu.Game.Managers.ServerData; + +public interface IMapMonsterManager +{ + /// + /// Gets the MapNpc from its id + /// + /// + MapMonsterDTO GetById(int mapNpcId); + + /// + /// Returns all the map npcs that are supposedly contained in this mapId + /// All the time you call this method, you'r going to get new map npcs + /// + /// + IReadOnlyList GetByMapId(int mapId); + + IReadOnlyList GetMapMonstersPerVNum(int vnum); + + /// + /// Loads all MapNpc's to the cache + /// + Task InitializeAsync(); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Managers/ServerData/IMapNpcManager.cs b/srcs/WingsAPI.Game/Managers/ServerData/IMapNpcManager.cs new file mode 100644 index 0000000..4f081ff --- /dev/null +++ b/srcs/WingsAPI.Game/Managers/ServerData/IMapNpcManager.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using WingsEmu.DTOs.Maps; + +namespace WingsEmu.Game.Managers.ServerData; + +public interface IMapNpcManager +{ + /// + /// Gets the MapNpc from its id + /// + /// + MapNpcDTO GetById(int mapNpcId); + + /// + /// Returns all the map npcs that are supposedly contained in this mapId + /// All the time you call this method, you'r going to get new map npcs + /// + /// + IReadOnlyList GetByMapId(int mapId); + + IReadOnlyList GetMapNpcsPerVNum(int vnum); + + /// + /// Loads all MapNpc's to the cache + /// + Task InitializeAsync(); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Managers/ServerData/IRecipeManager.cs b/srcs/WingsAPI.Game/Managers/ServerData/IRecipeManager.cs new file mode 100644 index 0000000..4ca1256 --- /dev/null +++ b/srcs/WingsAPI.Game/Managers/ServerData/IRecipeManager.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace WingsEmu.Game.Managers.ServerData; + +public interface IRecipeManager +{ + Task InitializeAsync(); + + IReadOnlyList GetRecipesByProducerItemVnum(int itemVnum); + + IReadOnlyList GetRecipesByNpcId(long mapNpcId); + + IReadOnlyList GetRecipesByNpcMonsterVnum(int npcVNum); + + IReadOnlyList GetRecipeByProducedItemVnum(int itemVnum); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Managers/ServerData/IShopManager.cs b/srcs/WingsAPI.Game/Managers/ServerData/IShopManager.cs new file mode 100644 index 0000000..b32e1fd --- /dev/null +++ b/srcs/WingsAPI.Game/Managers/ServerData/IShopManager.cs @@ -0,0 +1,14 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Threading.Tasks; +using WingsEmu.Game.Shops; + +namespace WingsEmu.Game.Managers.ServerData; + +public interface IShopManager +{ + Task InitializeAsync(); + ShopNpc GetShopByNpcId(int npcId); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Managers/StaticData/ICardsManager.cs b/srcs/WingsAPI.Game/Managers/StaticData/ICardsManager.cs new file mode 100644 index 0000000..13ca78f --- /dev/null +++ b/srcs/WingsAPI.Game/Managers/StaticData/ICardsManager.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using WingsEmu.Game.Buffs; + +namespace WingsEmu.Game.Managers.StaticData; + +public class StaticCardsManager +{ + public static ICardsManager Instance { get; private set; } + + public static void Initialize(ICardsManager manager) + { + Instance = manager; + } +} + +public interface ICardsManager +{ + /// + /// Loads the cards into the cache + /// + void Initialize(); + + /// + /// Returns a card with the specified + /// card Id + /// + /// + /// + Card GetCardByCardId(int cardId); + + /// + /// Returns the card with the specified name + /// + /// i18n key + /// + List GetCardByName(string name); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Managers/StaticData/IItemsManager.cs b/srcs/WingsAPI.Game/Managers/StaticData/IItemsManager.cs new file mode 100644 index 0000000..b56a3ec --- /dev/null +++ b/srcs/WingsAPI.Game/Managers/StaticData/IItemsManager.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using WingsEmu.Game.Items; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Managers.StaticData; + +public class StaticItemsManager +{ + public static IItemsManager Instance { get; private set; } + + public static void Initialize(IItemsManager manager) + { + Instance = manager; + } +} + +public interface IItemsManager +{ + /// + /// Loads the items into the cache + /// + void Initialize(); + + /// + /// Returns an item with the corresponding VNum + /// + /// + /// + IGameItem GetItem(int vnum); + + /// + /// Get item + /// + /// key + /// + List GetItem(string name); + + /// + /// Returns a list of items with the ItemType specified + /// as parameter + /// + /// + /// + IEnumerable GetItemsByType(ItemType type); + + /// + /// Returns a Title ID based on the VNum + /// + /// + /// + int GetTitleId(int itemVnum); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Managers/StaticData/INpcMonsterManager.cs b/srcs/WingsAPI.Game/Managers/StaticData/INpcMonsterManager.cs new file mode 100644 index 0000000..938ebb9 --- /dev/null +++ b/srcs/WingsAPI.Game/Managers/StaticData/INpcMonsterManager.cs @@ -0,0 +1,26 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using System.Threading.Tasks; +using WingsEmu.Game.Entities; + +namespace WingsEmu.Game.Managers.StaticData; + +public class StaticNpcMonsterManager +{ + public static INpcMonsterManager Instance { get; private set; } + + public static void Initialize(INpcMonsterManager manager) + { + Instance = manager; + } +} + +public interface INpcMonsterManager +{ + IMonsterData GetNpc(int vnum); + List GetNpc(string name); + Task InitializeAsync(); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Managers/StaticData/ISkillsManager.cs b/srcs/WingsAPI.Game/Managers/StaticData/ISkillsManager.cs new file mode 100644 index 0000000..7d8a926 --- /dev/null +++ b/srcs/WingsAPI.Game/Managers/StaticData/ISkillsManager.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using WingsEmu.DTOs.Skills; + +namespace WingsEmu.Game.Managers.StaticData; + +public class StaticSkillsManager +{ + public static ISkillsManager Instance { get; private set; } + + public static void Initialize(ISkillsManager manager) + { + Instance = manager; + } +} + +public interface ISkillsManager +{ + Task Initialize(); + SkillDTO GetSkill(int s); + List GetSkill(string name); + IEnumerable GetSkills(); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Managers/TransportFactory.cs b/srcs/WingsAPI.Game/Managers/TransportFactory.cs new file mode 100644 index 0000000..bd28104 --- /dev/null +++ b/srcs/WingsAPI.Game/Managers/TransportFactory.cs @@ -0,0 +1,32 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Threading; + +namespace WingsEmu.Game.Managers; + +public class TransportFactory +{ + private static TransportFactory _instance; + private long _lastTransportId = 100000; + + private TransportFactory() + { + // do nothing + } + + public static TransportFactory Instance => _instance ??= new TransportFactory(); + + public long GenerateTransportId() + { + Interlocked.Increment(ref _lastTransportId); + + if (_lastTransportId >= long.MaxValue) + { + _lastTransportId = 0; + } + + return _lastTransportId; + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Maps/CharacterMapItem.cs b/srcs/WingsAPI.Game/Maps/CharacterMapItem.cs new file mode 100644 index 0000000..9d9319e --- /dev/null +++ b/srcs/WingsAPI.Game/Maps/CharacterMapItem.cs @@ -0,0 +1,18 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsEmu.Game.Items; + +namespace WingsEmu.Game.Maps; + +public class CharacterMapItem : MapItem +{ + public CharacterMapItem(short x, short y, GameItemInstance itemInstance, IMapInstance mapInstance, bool isQuest = false) : base(x, y, isQuest, mapInstance) => ItemInstance = itemInstance; + + public override int Amount => ItemInstance.Amount; + + public override int ItemVNum => ItemInstance.ItemVNum; + + public override GameItemInstance GetItemInstance() => ItemInstance; +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Maps/Event/DisposeMapEvent.cs b/srcs/WingsAPI.Game/Maps/Event/DisposeMapEvent.cs new file mode 100644 index 0000000..e3ac5ef --- /dev/null +++ b/srcs/WingsAPI.Game/Maps/Event/DisposeMapEvent.cs @@ -0,0 +1,10 @@ +using PhoenixLib.Events; + +namespace WingsEmu.Game.Maps.Event; + +public class DisposeMapEvent : IAsyncEvent +{ + public DisposeMapEvent(IMapInstance map) => Map = map; + + public IMapInstance Map { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Maps/Event/JoinMapEndEvent.cs b/srcs/WingsAPI.Game/Maps/Event/JoinMapEndEvent.cs new file mode 100644 index 0000000..49919d3 --- /dev/null +++ b/srcs/WingsAPI.Game/Maps/Event/JoinMapEndEvent.cs @@ -0,0 +1,10 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Maps.Event; + +public class JoinMapEndEvent : PlayerEvent +{ + public JoinMapEndEvent(IMapInstance joinedMapInstance) => JoinedMapInstance = joinedMapInstance; + + public IMapInstance JoinedMapInstance { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Maps/Event/JoinMapEvent.cs b/srcs/WingsAPI.Game/Maps/Event/JoinMapEvent.cs new file mode 100644 index 0000000..d11da94 --- /dev/null +++ b/srcs/WingsAPI.Game/Maps/Event/JoinMapEvent.cs @@ -0,0 +1,42 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Maps.Event; + +public class JoinMapEvent : PlayerEvent +{ + public JoinMapEvent(int joinedMap, short? x = null, short? y = null) + { + JoinedMapId = joinedMap; + X = x; + Y = y; + } + + public JoinMapEvent(Guid joinedMap, short? x = null, short? y = null) + { + JoinedMapGuid = joinedMap; + X = x; + Y = y; + } + + public JoinMapEvent(IMapInstance joinedMap, short? x = null, short? y = null) + { + JoinedMapInstance = joinedMap; + X = x; + Y = y; + } + + public int JoinedMapId { get; } + + public Guid JoinedMapGuid { get; } + + public IMapInstance JoinedMapInstance { get; } + + public short? X { get; } + + public short? Y { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Maps/Event/LeaveMapEvent.cs b/srcs/WingsAPI.Game/Maps/Event/LeaveMapEvent.cs new file mode 100644 index 0000000..e1ea82c --- /dev/null +++ b/srcs/WingsAPI.Game/Maps/Event/LeaveMapEvent.cs @@ -0,0 +1,10 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Maps.Event; + +/// +/// Used internally in JoinMapEvent, if you want to change the map just use JoinMapEvent. +/// +public class LeaveMapEvent : PlayerEvent +{ +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Maps/Event/MapActivatedEvent.cs b/srcs/WingsAPI.Game/Maps/Event/MapActivatedEvent.cs new file mode 100644 index 0000000..fe1938a --- /dev/null +++ b/srcs/WingsAPI.Game/Maps/Event/MapActivatedEvent.cs @@ -0,0 +1,10 @@ +using PhoenixLib.Events; + +namespace WingsEmu.Game.Maps.Event; + +public class MapActivatedEvent : IAsyncEvent +{ + public MapActivatedEvent(IMapInstance mapInstance) => MapInstance = mapInstance; + + public IMapInstance MapInstance { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Maps/Event/MapDeactivatedEvent.cs b/srcs/WingsAPI.Game/Maps/Event/MapDeactivatedEvent.cs new file mode 100644 index 0000000..12580f9 --- /dev/null +++ b/srcs/WingsAPI.Game/Maps/Event/MapDeactivatedEvent.cs @@ -0,0 +1,10 @@ +using PhoenixLib.Events; + +namespace WingsEmu.Game.Maps.Event; + +public class MapDeactivatedEvent : IAsyncEvent +{ + public MapDeactivatedEvent(IMapInstance mapInstance) => MapInstance = mapInstance; + + public IMapInstance MapInstance { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Maps/Event/PortalRemoveEvent.cs b/srcs/WingsAPI.Game/Maps/Event/PortalRemoveEvent.cs new file mode 100644 index 0000000..2ac0305 --- /dev/null +++ b/srcs/WingsAPI.Game/Maps/Event/PortalRemoveEvent.cs @@ -0,0 +1,8 @@ +using PhoenixLib.Events; + +namespace WingsEmu.Game.Maps.Event; + +public class PortalRemoveEvent : IAsyncEvent +{ + public IPortalEntity Portal { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Maps/Event/PortalTriggerEvent.cs b/srcs/WingsAPI.Game/Maps/Event/PortalTriggerEvent.cs new file mode 100644 index 0000000..78eb6c5 --- /dev/null +++ b/srcs/WingsAPI.Game/Maps/Event/PortalTriggerEvent.cs @@ -0,0 +1,10 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Maps.Event; + +public class PortalTriggerEvent : PlayerEvent +{ + public IPortalEntity Portal { get; init; } + + public bool Confirmed { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Maps/Event/SpawnPortalEvent.cs b/srcs/WingsAPI.Game/Maps/Event/SpawnPortalEvent.cs new file mode 100644 index 0000000..043b751 --- /dev/null +++ b/srcs/WingsAPI.Game/Maps/Event/SpawnPortalEvent.cs @@ -0,0 +1,15 @@ +using PhoenixLib.Events; + +namespace WingsEmu.Game.Maps.Event; + +public class SpawnPortalEvent : IAsyncEvent +{ + public SpawnPortalEvent(IMapInstance map, IPortalEntity portal) + { + Map = map; + Portal = portal; + } + + public IMapInstance Map { get; } + public IPortalEntity Portal { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Maps/IMapAttribute.cs b/srcs/WingsAPI.Game/Maps/IMapAttribute.cs new file mode 100644 index 0000000..1719751 --- /dev/null +++ b/srcs/WingsAPI.Game/Maps/IMapAttribute.cs @@ -0,0 +1,6 @@ +namespace WingsEmu.Game.Maps; + +public interface IMapAttribute +{ + string Name { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Maps/IMapInstance.cs b/srcs/WingsAPI.Game/Maps/IMapInstance.cs new file mode 100644 index 0000000..7998671 --- /dev/null +++ b/srcs/WingsAPI.Game/Maps/IMapInstance.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using WingsEmu.DTOs.Maps; +using WingsEmu.DTOs.ServerDatas; +using WingsEmu.Game._ECS; +using WingsEmu.Game._ECS.Systems; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Items; +using WingsEmu.Game.Miniland; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Portals; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Maps; + +public interface IEntityIdManager +{ + int GenerateEntityId(); +} + +public interface IMapInstance : IMonsterSystem, IBroadcaster, ITickProcessable, ICharacterSystem, IMateSystem, IDropSystem, INpcSystem, IBattleSystem, IEntityIdManager +{ + Guid Id { get; } + MapInstanceType MapInstanceType { get; } + + public IReadOnlyList Grid { get; } + public int Width { get; } + public int Height { get; } + public int MapId { get; } + public int Music { get; } + public int MapVnum { get; } + public int MapNameId { get; } + + byte MapIndexX { get; set; } + byte MapIndexY { get; set; } + + bool IsDance { get; set; } + short? MapMusic { get; set; } + bool IsPvp { get; set; } + bool ShopAllowed { get; } + bool AIDisabled { get; set; } + + List Portals { get; } + List TimeSpacePortals { get; } + List MapDesignObjects { get; } + + public Position GetRandomPosition(); + IReadOnlyList GetEntitiesOnMapPackets(bool onlyItemsAndPortals = false); + + bool HasMapFlag(MapFlags flags); + + MapItem PutItem(ushort amount, ref GameItemInstance inv, IClientSession session); + + void DespawnMonster(IMonsterEntity monsterEntity); + + /// + /// Returns the characters in range + /// + /// + /// + /// + /// + IReadOnlyList GetNonMonsterBattleEntitiesInRange(Position pos, short distance); + + IReadOnlyList GetNonMonsterBattleEntitiesInRange(Position pos, short distance, Func predicate); + IReadOnlyList GetNonMonsterBattleEntities(); + IReadOnlyList GetNonMonsterBattleEntities(Func predicate); + IReadOnlyList GetBattleEntities(Func predicate); + IReadOnlyList GetBattleEntitiesInRange(Position pos, short distance); + IReadOnlyList GetClosestBattleEntitiesInRange(Position pos, short distance); + IBattleEntity GetBattleEntity(VisualType type, long id); + void RegisterSession(IClientSession session); + void UnregisterSession(IClientSession session); + void LoadPortals(IEnumerable value); + + void Initialize(DateTime date); + void Destroy(); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Maps/IMapInstanceFactory.cs b/srcs/WingsAPI.Game/Maps/IMapInstanceFactory.cs new file mode 100644 index 0000000..f363483 --- /dev/null +++ b/srcs/WingsAPI.Game/Maps/IMapInstanceFactory.cs @@ -0,0 +1,6 @@ +namespace WingsEmu.Game.Maps; + +public interface IMapInstanceFactory +{ + IMapInstance CreateMap(Map map, MapInstanceType mapInstanceType); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Maps/IMapManager.cs b/srcs/WingsAPI.Game/Maps/IMapManager.cs new file mode 100644 index 0000000..66159a7 --- /dev/null +++ b/srcs/WingsAPI.Game/Maps/IMapManager.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Game.Maps; + +public class StaticMapManager +{ + public static IMapManager Instance { get; private set; } + + public static void Initialize(IMapManager manager) + { + Instance = manager; + } +} + +public interface IMapManager +{ + Task Initialize(); + + bool HasMapFlagByMapId(int mapId, MapFlags mapFlag); + IReadOnlyList GetMapFlagByMapId(int mapId); + + MapDataDTO GetMapByMapId(int mapId); + + /// + /// Returns the MapInstance with the corresponding ID + /// + /// + /// + IMapInstance GetMapInstance(Guid id); + + /// + /// Generates a new MapInstance + /// + /// + /// + /// + IMapInstance GenerateMapInstanceByMapId(int mapId, MapInstanceType type); + + /// + /// Generates a new MapInstance + /// + /// + /// + /// + IMapInstance GenerateMapInstanceByMapVNum(ServerMapDto serverMapDto, MapInstanceType type); + + void RemoveMapInstance(Guid mapId); + + /// + /// Returns the mapinstance associated to the mapid + /// passed as parameter + /// + /// + /// + IMapInstance GetBaseMapInstanceByMapId(int mapId); + + Guid GetBaseMapInstanceIdByMapId(int mapId); + + /// + /// Teleports on a random place on the specified MapInstance + /// + /// + /// + /// + /// + Task TeleportOnRandomPlaceInMapAsync(IClientSession session, IMapInstance mapInstance, bool isSameMap = false); + + IEnumerable GetMapsWithFlag(MapFlags flags); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Maps/Map.cs b/srcs/WingsAPI.Game/Maps/Map.cs new file mode 100644 index 0000000..9fab553 --- /dev/null +++ b/srcs/WingsAPI.Game/Maps/Map.cs @@ -0,0 +1,20 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using WingsEmu.DTOs.Maps; + +namespace WingsEmu.Game.Maps; + +public class Map +{ + public List Flags { get; init; } + public IReadOnlyList Grid { get; init; } + public int MapId { get; init; } + public int Width { get; init; } + public int Height { get; init; } + public int MapVnum { get; init; } + public int MapNameId { get; init; } + public int Music { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Maps/MapExtensions.cs b/srcs/WingsAPI.Game/Maps/MapExtensions.cs new file mode 100644 index 0000000..806f0cf --- /dev/null +++ b/srcs/WingsAPI.Game/Maps/MapExtensions.cs @@ -0,0 +1,119 @@ +using System; +using WingsEmu.Game.Helpers; + +namespace WingsEmu.Game.Maps; + +public static class MapExtensions +{ + public static bool CanWalkAround(this IMapInstance mapInstance, int x, int y) + { + for (int dX = -1; dX <= 1; dX++) + { + for (int dY = -1; dY <= 1; dY++) + { + if (mapInstance.IsBlockedZone(x + dX, y + dY)) + { + return false; + } + } + } + + return true; + } + + + public static bool IsBlockedZone(this IMapInstance mapInstance, int x, int y) + { + try + { + if (mapInstance.Grid == null) + { + return false; + } + + return !mapInstance.Grid.IsWalkable(x, y, mapInstance.Width, mapInstance.Height); + } + catch + { + return true; + } + } + + public static bool PvpZone(this IMapInstance mapInstance, int x, int y) + { + try + { + return mapInstance.Grid != null && mapInstance.Grid.IsPvpZoneOff(x, y, mapInstance.Width, mapInstance.Height); + } + catch + { + return false; + } + } + + public static bool MateDollZone(this IMapInstance mapInstance, int x, int y) + { + try + { + return mapInstance.Grid != null && mapInstance.Grid.IsMateDollZone(x, y, mapInstance.Width, mapInstance.Height); + } + catch + { + return false; + } + } + + public static bool IsMonsterAggroDisabled(this IMapInstance mapInstance, int x, int y) + { + try + { + return mapInstance.Grid != null && mapInstance.Grid.IsMonsterAggroDisabled(x, y, mapInstance.Width, mapInstance.Height); + } + catch + { + return false; + } + } + + public static bool GetFreePosition(this IMapInstance mapInstance, IRandomGenerator randomGenerator, ref short firstX, ref short firstY, byte xpoint, byte ypoint) + { + short minX = (short)(-xpoint + firstX); + short maxX = (short)(xpoint + firstX); + + short minY = (short)(-ypoint + firstY); + short maxY = (short)(ypoint + firstY); + + short x = (short)randomGenerator.RandomNumber(minX, maxX + 1); + short y = (short)randomGenerator.RandomNumber(minY, maxY + 1); + + if (mapInstance.IsPathBlocked(firstX, firstY, x, y)) + { + return false; + } + + firstX = x; + firstY = y; + return true; + } + + private static bool IsPathBlocked(this IMapInstance mapInstance, int firstX, int firstY, int mapX, int mapY) + { + for (int i = 1; i <= Math.Abs(mapX - firstX); i++) + { + if (mapInstance.IsBlockedZone(firstX + Math.Sign(mapX - firstX) * i, firstY)) + { + return true; + } + } + + for (int i = 1; i <= Math.Abs(mapY - firstY); i++) + { + if (mapInstance.IsBlockedZone(firstX, firstY + Math.Sign(mapY - firstY) * i)) + { + return true; + } + } + + return false; + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Maps/MapInstancePortalHandler.cs b/srcs/WingsAPI.Game/Maps/MapInstancePortalHandler.cs new file mode 100644 index 0000000..b8706ca --- /dev/null +++ b/srcs/WingsAPI.Game/Maps/MapInstancePortalHandler.cs @@ -0,0 +1,31 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Portals; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Maps; + +public static class MapInstancePortalHandler +{ + public static List GenerateMinilandEntryPortals(this IMapInstance mapInstance, IMapInstance miniland, IPortalFactory portalFactory) + { + var list = new List(); + + switch (mapInstance.MapId) + { + case 1: + list.Add(portalFactory.CreatePortal(PortalType.Miniland, mapInstance, new Position(48, 132), miniland, new Position(5, 8))); + break; + + case 145: + list.Add(portalFactory.CreatePortal(PortalType.Miniland, mapInstance, new Position(9, 171), miniland, new Position(5, 8))); + break; + } + + return list; + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Maps/MapInstanceState.cs b/srcs/WingsAPI.Game/Maps/MapInstanceState.cs new file mode 100644 index 0000000..c8e80ac --- /dev/null +++ b/srcs/WingsAPI.Game/Maps/MapInstanceState.cs @@ -0,0 +1,7 @@ +namespace WingsEmu.Game.Maps; + +public enum MapInstanceState +{ + Idle, + Running +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Maps/MapInstanceType.cs b/srcs/WingsAPI.Game/Maps/MapInstanceType.cs new file mode 100644 index 0000000..1d69df0 --- /dev/null +++ b/srcs/WingsAPI.Game/Maps/MapInstanceType.cs @@ -0,0 +1,20 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Game.Maps; + +public enum MapInstanceType +{ + BaseMapInstance, + NormalInstance, + TimeSpaceInstance, + RaidInstance, + Act4Instance, + IceBreakerInstance, + ArenaInstance, + EventGameInstance, + Miniland, + Act4Dungeon, + RainbowBattle +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Maps/MapItem.cs b/srcs/WingsAPI.Game/Maps/MapItem.cs new file mode 100644 index 0000000..5cd4c23 --- /dev/null +++ b/srcs/WingsAPI.Game/Maps/MapItem.cs @@ -0,0 +1,68 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers; + +namespace WingsEmu.Game.Maps; + +public abstract class MapItem +{ + private long _transportId; + + protected GameItemInstance ItemInstance; + + public MapItem(short x, short y, bool isQuest, IMapInstance mapInstance) + { + PositionX = x; + PositionY = y; + IsQuest = isQuest; + MapInstance = mapInstance; + CreatedDate = DateTime.UtcNow; + ShowMessageEasterEgg = DateTime.UtcNow; + TransportId = 0; + } + + public virtual int Amount { get; set; } + + public DateTime? CreatedDate { get; set; } + + public DateTime ShowMessageEasterEgg { get; set; } + + public virtual int ItemVNum { get; set; } + + public short PositionX { get; set; } + + public short PositionY { get; set; } + + public bool IsQuest { get; } + + public IMapInstance MapInstance { get; } + + public long TransportId + { + get + { + if (_transportId == 0) + { + // create transportId thru factory + // TODO: Review has some problems, aka. issue corresponding to weird/multiple/missplaced drops + _transportId = TransportFactory.Instance.GenerateTransportId(); + } + + return _transportId; + } + + private set + { + if (value != _transportId) + { + _transportId = value; + } + } + } + + public abstract GameItemInstance GetItemInstance(); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Maps/TimeSpaceMapItem.cs b/srcs/WingsAPI.Game/Maps/TimeSpaceMapItem.cs new file mode 100644 index 0000000..c134245 --- /dev/null +++ b/srcs/WingsAPI.Game/Maps/TimeSpaceMapItem.cs @@ -0,0 +1,35 @@ +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Items; +using WingsEmu.Game.Triggers; + +namespace WingsEmu.Game.Maps; + +public class TimeSpaceMapItem : MapItem, IEventTriggerContainer +{ + private readonly IEventTriggerContainer _eventTriggerContainer; + + public TimeSpaceMapItem(short x, short y, bool isQuest, GameItemInstance gameItemInstance, IAsyncEventPipeline asyncEventPipeline, IMapInstance mapInstance, int? dancingTime, bool isObjective) + : base(x, y, isQuest, mapInstance) + { + ItemInstance = gameItemInstance; + _eventTriggerContainer = new EventTriggerContainer(asyncEventPipeline); + DancingTime = dancingTime; + IsObjective = isObjective; + Amount = 1; + ItemVNum = gameItemInstance.ItemVNum; + CreatedDate = null; + } + + public int? DancingTime { get; } + public bool IsObjective { get; } + + public void AddEvent(string key, IAsyncEvent notification, bool removedOnTrigger = false) + { + _eventTriggerContainer.AddEvent(key, notification, removedOnTrigger); + } + + public Task TriggerEvents(string key) => _eventTriggerContainer.TriggerEvents(key); + + public override GameItemInstance GetItemInstance() => ItemInstance; +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Mates/Events/MateBackToMinilandEvent.cs b/srcs/WingsAPI.Game/Mates/Events/MateBackToMinilandEvent.cs new file mode 100644 index 0000000..93fbc86 --- /dev/null +++ b/srcs/WingsAPI.Game/Mates/Events/MateBackToMinilandEvent.cs @@ -0,0 +1,17 @@ +using System; +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Mates.Events; + +public class MateBackToMinilandEvent : PlayerEvent +{ + public MateBackToMinilandEvent(IMateEntity mateEntity, Guid expectedGuid) + { + MateEntity = mateEntity; + ExpectedGuid = expectedGuid; + } + + public IMateEntity MateEntity { get; set; } + + public Guid ExpectedGuid { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Mates/Events/MateDeathEvent.cs b/srcs/WingsAPI.Game/Mates/Events/MateDeathEvent.cs new file mode 100644 index 0000000..ff275a8 --- /dev/null +++ b/srcs/WingsAPI.Game/Mates/Events/MateDeathEvent.cs @@ -0,0 +1,17 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Game.Entities; + +namespace WingsEmu.Game.Mates.Events; + +public class MateDeathEvent : PlayerEvent +{ + public MateDeathEvent(IBattleEntity killer, IMateEntity mateEntity) + { + Killer = killer; + MateEntity = mateEntity; + } + + public IBattleEntity Killer { get; set; } + + public IMateEntity MateEntity { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Mates/Events/MateHealEvent.cs b/srcs/WingsAPI.Game/Mates/Events/MateHealEvent.cs new file mode 100644 index 0000000..f810b2b --- /dev/null +++ b/srcs/WingsAPI.Game/Mates/Events/MateHealEvent.cs @@ -0,0 +1,10 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Mates.Events; + +public class MateHealEvent : PlayerEvent +{ + public IMateEntity MateEntity { get; set; } + public int HpHeal { get; set; } + public int MpHeal { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Mates/Events/MateInitializeEvent.cs b/srcs/WingsAPI.Game/Mates/Events/MateInitializeEvent.cs new file mode 100644 index 0000000..f548356 --- /dev/null +++ b/srcs/WingsAPI.Game/Mates/Events/MateInitializeEvent.cs @@ -0,0 +1,9 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Mates.Events; + +public class MateInitializeEvent : PlayerEvent +{ + public IMateEntity MateEntity { get; init; } + public bool IsOnCharacterEnter { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Mates/Events/MateJoinInsideMinilandEvent.cs b/srcs/WingsAPI.Game/Mates/Events/MateJoinInsideMinilandEvent.cs new file mode 100644 index 0000000..3c32dde --- /dev/null +++ b/srcs/WingsAPI.Game/Mates/Events/MateJoinInsideMinilandEvent.cs @@ -0,0 +1,8 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Mates.Events; + +public class MateJoinInMinilandEvent : PlayerEvent +{ + public IMateEntity MateEntity { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Mates/Events/MateJoinTeamEvent.cs b/srcs/WingsAPI.Game/Mates/Events/MateJoinTeamEvent.cs new file mode 100644 index 0000000..1d7a1a6 --- /dev/null +++ b/srcs/WingsAPI.Game/Mates/Events/MateJoinTeamEvent.cs @@ -0,0 +1,12 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Mates.Events; + +public class MateJoinTeamEvent : PlayerEvent +{ + public IMateEntity MateEntity { get; init; } + + public bool IsOnCharacterEnter { get; init; } + + public bool IsNewCreated { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Mates/Events/MateLeaveTeamEvent.cs b/srcs/WingsAPI.Game/Mates/Events/MateLeaveTeamEvent.cs new file mode 100644 index 0000000..f820318 --- /dev/null +++ b/srcs/WingsAPI.Game/Mates/Events/MateLeaveTeamEvent.cs @@ -0,0 +1,8 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Mates.Events; + +public class MateLeaveTeamEvent : PlayerEvent +{ + public IMateEntity MateEntity { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Mates/Events/MateProcessExperienceEvent.cs b/srcs/WingsAPI.Game/Mates/Events/MateProcessExperienceEvent.cs new file mode 100644 index 0000000..a967944 --- /dev/null +++ b/srcs/WingsAPI.Game/Mates/Events/MateProcessExperienceEvent.cs @@ -0,0 +1,15 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Mates.Events; + +public class MateProcessExperienceEvent : PlayerEvent +{ + public MateProcessExperienceEvent(IMateEntity mateEntity, long experience) + { + MateEntity = mateEntity; + Experience = experience; + } + + public IMateEntity MateEntity { get; } + public long Experience { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Mates/Events/MateRemoveEvent.cs b/srcs/WingsAPI.Game/Mates/Events/MateRemoveEvent.cs new file mode 100644 index 0000000..a894a41 --- /dev/null +++ b/srcs/WingsAPI.Game/Mates/Events/MateRemoveEvent.cs @@ -0,0 +1,8 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Mates.Events; + +public class MateRemoveEvent : PlayerEvent +{ + public IMateEntity MateEntity { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Mates/Events/MateRestEvent.cs b/srcs/WingsAPI.Game/Mates/Events/MateRestEvent.cs new file mode 100644 index 0000000..3ea219c --- /dev/null +++ b/srcs/WingsAPI.Game/Mates/Events/MateRestEvent.cs @@ -0,0 +1,10 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Mates.Events; + +public class MateRestEvent : PlayerEvent +{ + public IMateEntity MateEntity { get; init; } + public bool Rest { get; init; } + public bool Force { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Mates/Events/MateReviveEvent.cs b/srcs/WingsAPI.Game/Mates/Events/MateReviveEvent.cs new file mode 100644 index 0000000..b81c30a --- /dev/null +++ b/srcs/WingsAPI.Game/Mates/Events/MateReviveEvent.cs @@ -0,0 +1,26 @@ +using System; +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Mates.Events; + +public class MateReviveEvent : PlayerEvent +{ + public MateReviveEvent(IMateEntity mateEntity, bool delayed) + { + MateEntity = mateEntity; + Delayed = delayed; + } + + public MateReviveEvent(IMateEntity mateEntity, bool delayed, Guid expectedGuid) + { + MateEntity = mateEntity; + Delayed = delayed; + ExpectedGuid = expectedGuid; + } + + public IMateEntity MateEntity { get; set; } + + public bool Delayed { get; set; } + + public Guid ExpectedGuid { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Mates/Events/MateSpTransformEvent.cs b/srcs/WingsAPI.Game/Mates/Events/MateSpTransformEvent.cs new file mode 100644 index 0000000..fabb40a --- /dev/null +++ b/srcs/WingsAPI.Game/Mates/Events/MateSpTransformEvent.cs @@ -0,0 +1,8 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Mates.Events; + +public class MateSpTransformEvent : PlayerEvent +{ + public IMateEntity MateEntity { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Mates/Events/MateSpUntransformEvent.cs b/srcs/WingsAPI.Game/Mates/Events/MateSpUntransformEvent.cs new file mode 100644 index 0000000..dcaccc8 --- /dev/null +++ b/srcs/WingsAPI.Game/Mates/Events/MateSpUntransformEvent.cs @@ -0,0 +1,8 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Mates.Events; + +public class MateSpUntransformEvent : PlayerEvent +{ + public IMateEntity MateEntity { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Mates/Events/MateStayInsideMinilandEvent.cs b/srcs/WingsAPI.Game/Mates/Events/MateStayInsideMinilandEvent.cs new file mode 100644 index 0000000..bc43b64 --- /dev/null +++ b/srcs/WingsAPI.Game/Mates/Events/MateStayInsideMinilandEvent.cs @@ -0,0 +1,9 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Mates.Events; + +public class MateStayInsideMinilandEvent : PlayerEvent +{ + public IMateEntity MateEntity { get; init; } + public bool IsOnCharacterEnter { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Mates/Events/MateSummonEvent.cs b/srcs/WingsAPI.Game/Mates/Events/MateSummonEvent.cs new file mode 100644 index 0000000..4a76f7e --- /dev/null +++ b/srcs/WingsAPI.Game/Mates/Events/MateSummonEvent.cs @@ -0,0 +1,8 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Mates.Events; + +public class MateSummonEvent : PlayerEvent +{ + public IMateEntity MateEntity { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Mates/IMateComponent.cs b/srcs/WingsAPI.Game/Mates/IMateComponent.cs new file mode 100644 index 0000000..9317a44 --- /dev/null +++ b/srcs/WingsAPI.Game/Mates/IMateComponent.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; + +namespace WingsEmu.Game.Mates; + +public interface IMateComponent +{ + IReadOnlyList TeamMembers(); + IReadOnlyList TeamMembers(Func predicate); + IReadOnlyList GetMates(); + IReadOnlyList GetMates(Func predicate); + IMateEntity GetMate(Func predicate); + IMateEntity GetTeamMember(Func predicate); + void AddMate(IMateEntity mateEntity); + void RemoveMate(IMateEntity mateEntity); +} + +public class MateComponent : IMateComponent +{ + private readonly ReaderWriterLockSlim _lock = new(LockRecursionPolicy.SupportsRecursion); + private readonly List _mates = new(); + + public IReadOnlyList TeamMembers() + { + return GetMates(x => x.IsTeamMember); + } + + public IReadOnlyList TeamMembers(Func predicate) + { + return GetMates(x => x.IsTeamMember && predicate(x)); + } + + public IReadOnlyList GetMates() + { + _lock.EnterReadLock(); + try + { + return _mates.ToArray(); + } + finally + { + _lock.ExitReadLock(); + } + } + + public IReadOnlyList GetMates(Func predicate) + { + _lock.EnterReadLock(); + try + { + return _mates.FindAll(x => x != null && (predicate == null || predicate(x))); + } + finally + { + _lock.ExitReadLock(); + } + } + + public IMateEntity GetMate(Func predicate) + { + _lock.EnterReadLock(); + try + { + return _mates.FirstOrDefault(x => x != null && (predicate == null || predicate(x))); + } + finally + { + _lock.ExitReadLock(); + } + } + + public IMateEntity GetTeamMember(Func predicate) + { + return GetMate(x => x.IsTeamMember && predicate(x)); + } + + public void AddMate(IMateEntity mateEntity) + { + _lock.EnterWriteLock(); + try + { + _mates.Add(mateEntity); + } + finally + { + _lock.ExitWriteLock(); + } + } + + public void RemoveMate(IMateEntity mateEntity) + { + _lock.EnterWriteLock(); + try + { + _mates.Remove(mateEntity); + } + finally + { + _lock.ExitWriteLock(); + } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Mates/IMateEntity.cs b/srcs/WingsAPI.Game/Mates/IMateEntity.cs new file mode 100644 index 0000000..8c3433b --- /dev/null +++ b/srcs/WingsAPI.Game/Mates/IMateEntity.cs @@ -0,0 +1,62 @@ +using System; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Entities; +using WingsEmu.Game.EntityStatistics; +using WingsEmu.Game.Items; +using WingsEmu.Game.Revival; +using WingsEmu.Game.Skills; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Mates; + +public interface IMateEntity : IBattleEntity, IMateRevivalComponent, IMonsterData +{ + short CloseDefence { get; set; } + short DistanceDodge { get; set; } + bool IsSitting { get; set; } + bool IsUsingSp { get; set; } + DateTime LastDeath { get; set; } + DateTime LastDefence { get; set; } + DateTime LastLowLoyaltyEffect { get; set; } + DateTime LastHealth { get; set; } + DateTime LastLoyaltyRecover { get; set; } + DateTime LastSkillUse { get; set; } + DateTime LastBasicSkill { get; set; } + DateTime? SpawnMateByGuardian { get; set; } + IPlayerEntity Owner { get; set; } + byte PetSlot { get; set; } + IBattleEntitySkill LastUsedPartnerSkill { get; set; } + short HitRate { get; set; } + byte Attack { get; set; } + bool CanPickUp { get; set; } + long CharacterId { get; set; } + byte Defence { get; set; } + long Experience { get; set; } + bool IsSummonable { get; set; } + bool IsTeamMember { get; set; } + short Loyalty { get; set; } + short MapX { get; set; } + short MapY { get; set; } + short MinilandX { get; set; } + short MinilandY { get; set; } + MateType MateType { get; set; } + string MateName { get; set; } + int NpcMonsterVNum { get; set; } + short Skin { get; set; } + int HitCriticalChance { get; set; } + int HitCriticalDamage { get; set; } + bool IsLimited { get; init; } + DateTime? SpCooldownEnd { get; set; } + DateTime LastEffect { get; set; } + DateTime LastPetUpgradeEffect { get; set; } + GameItemInstance Weapon { get; } + GameItemInstance Armor { get; } + GameItemInstance Gloves { get; } + GameItemInstance Boots { get; } + GameItemInstance Specialist { get; } + SkillInfo BasicSkill { get; } + IMateStatisticsComponent StatisticsComponent { get; } + void RefreshStatistics(); + void Initialize(); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Mates/IMateEntityFactory.cs b/srcs/WingsAPI.Game/Mates/IMateEntityFactory.cs new file mode 100644 index 0000000..5532063 --- /dev/null +++ b/srcs/WingsAPI.Game/Mates/IMateEntityFactory.cs @@ -0,0 +1,17 @@ +using WingsEmu.DTOs.Mates; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Npcs; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Mates; + +public interface IMateEntityFactory +{ + public IMateEntity CreateMateEntity(IPlayerEntity playerEntity, MateDTO mateDto); + public IMateEntity CreateMateEntity(IPlayerEntity owner, int monsterVnum, MateType mateType); + public IMateEntity CreateMateEntity(IPlayerEntity owner, MonsterData monsterData, MateType mateType); + public IMateEntity CreateMateEntity(IPlayerEntity owner, MonsterData monsterData, MateType mateType, byte level); + public IMateEntity CreateMateEntity(IPlayerEntity owner, MonsterData monsterData, MateType mateType, byte level, bool isLimited); + + public MateDTO CreateMateDto(IMateEntity mateEntity); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Mates/IMateTransportFactory.cs b/srcs/WingsAPI.Game/Mates/IMateTransportFactory.cs new file mode 100644 index 0000000..d240e9f --- /dev/null +++ b/srcs/WingsAPI.Game/Mates/IMateTransportFactory.cs @@ -0,0 +1,6 @@ +namespace WingsEmu.Game.Mates; + +public interface IMateTransportFactory +{ + int GenerateTransportId(); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Mates/MateEntity.MonsterData.cs b/srcs/WingsAPI.Game/Mates/MateEntity.MonsterData.cs new file mode 100644 index 0000000..8f78495 --- /dev/null +++ b/srcs/WingsAPI.Game/Mates/MateEntity.MonsterData.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using WingsAPI.Data.Drops; +using WingsEmu.DTOs.BCards; +using WingsEmu.Game._enum; +using WingsEmu.Game.Skills; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Battle; + +namespace WingsEmu.Game.Mates; + +public partial class MateEntity +{ + public short AmountRequired { get; } + public byte ArmorLevel { get; } + public AttackType AttackType { get; } + public byte AttackUpgrade { get; } + public byte BasicCastTime { get; } + public short BasicCooldown { get; } + public byte BasicRange { get; } + public short AttackEffect { get; } + public short BasicHitChance { get; } + public bool CanBeCollected { get; } + public bool CanBeDebuffed { get; } + public bool CanBeCaught { get; } + public bool CanBePushed { get; } + public bool CanRegenMp { get; } + public bool CanWalk { get; } + public int CellSize { get; } + public int CleanDamageMin { get; } + public int CleanDamageMax { get; } + public int CleanHitRate { get; } + public int CleanMeleeDefence { get; } + public int CleanRangeDefence { get; } + public int CleanMagicDefence { get; } + public int CleanDodge { get; } + public int CleanHp { get; } + public int CleanMp { get; } + public short BaseCloseDefence { get; } + public short BaseConcentrate { get; } + public short BaseCriticalChance { get; } + public short BaseCriticalRate { get; } + public bool DamagedOnlyLastJajamaruSkill { get; } + public int BaseDamageMaximum { get; } + public int BaseDamageMinimum { get; } + public short BaseDarkResistance { get; } + public short DeathEffect { get; } + public int BaseMaxHp { get; } + public int BaseMaxMp { get; } + public byte DefenceUpgrade { get; } + public bool DisappearAfterHitting { get; } + public bool DisappearAfterSeconds { get; } + public bool DisappearAfterSecondsMana { get; } + public short DistanceDefenceDodge { get; } + public byte BaseElement { get; } + public short BaseElementRate { get; } + public short BaseFireResistance { get; } + public FactionType? SuggestedFaction { get; } + public int GiveDamagePercentage { get; } + public int GroupAttack { get; } + public bool HasMode { get; } + public int RawHostility { get; } + public int IconId { get; } + public bool IsPercent { get; } + public int JobXp { get; } + public byte BaseLevel { get; } + public short BaseLightResistance { get; } + public short MagicMpFactor { get; } + public short MeleeHpFactor { get; } + public sbyte MinimumAttackRange { get; } + public int MonsterVNum { get; } + public string Name { get; } + public byte NoticeRange { get; } + public bool OnDefenseOnlyOnce { get; } + public short PermanentEffect { get; } + public MonsterRaceType MonsterRaceType { get; } + public byte MonsterRaceSubType { get; } + public short RangeDodgeFactor { get; } + public TimeSpan BaseRespawnTime { get; } + public int SpawnMobOrColor { get; } + public byte BaseSpeed { get; } + public int SpriteSize { get; } + public int TakeDamages { get; } + public short VNumRequired { get; } + public short BaseWaterResistance { get; } + public byte WeaponLevel { get; } + public byte WinfoValue { get; } + public int Xp { get; } + public byte MaxTries { get; } + public short CollectionCooldown { get; } + public byte CollectionDanceTime { get; } + public bool TeleportRemoveFromInventory { get; } + public short BasicDashSpeed { get; } + public bool ModeIsHpTriggered { get; } + public byte ModeLimiterType { get; } + public short ModeRangeTreshold { get; } + public short ModeCModeVnum { get; } + public short ModeHpTresholdOrItemVnum { get; } + public short MidgardDamage { get; } + public bool HasDash { get; } + public bool DropToInventory { get; } + public IReadOnlyList Drops { get; } + public bool CanSeeInvisible { get; } + public IReadOnlyList BCards { get; } + public IReadOnlyList ModeBCards { get; } + public IReadOnlyList MonsterSkills { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Mates/MateEntity.Stats.cs b/srcs/WingsAPI.Game/Mates/MateEntity.Stats.cs new file mode 100644 index 0000000..fa28534 --- /dev/null +++ b/srcs/WingsAPI.Game/Mates/MateEntity.Stats.cs @@ -0,0 +1,108 @@ +using WingsEmu.Game._enum; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Extensions.Mates; +using WingsEmu.Game.Items; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Battle; + +namespace WingsEmu.Game.Mates; + +public partial class MateEntity +{ + private int _criticalChance; + private int _criticalDamage; + private int _damageMax; + private int _damageMin; + private short _hitRate; + private short _magicDefense; + private int _maxHp; + private int _maxMp; + private short _meleeDefense; + private short _meleeDodge; + private short _rangedDefense; + private short _rangedDodge; + private byte _speed; + + public void RefreshStatistics() + { + _maxHp = _algorithm.GetBasicHp((short)MonsterRaceType, Level, MeleeHpFactor, CleanHp, false); + _maxMp = _algorithm.GetBasicMp((short)MonsterRaceType, Level, MagicMpFactor, CleanMp, false); + + _hitRate = (short)_algorithm.GetHitrate((short)MonsterRaceType, AttackType, WeaponLevel, BaseLevel, RangeDodgeFactor, CleanHitRate, false, Level, MateType); + _damageMin = _algorithm.GetAttack(true, (short)MonsterRaceType, AttackType, WeaponLevel, WinfoValue, BaseLevel, this.GetModifier(), CleanDamageMin, false, Level, MateType); + _damageMax = _algorithm.GetAttack(false, (short)MonsterRaceType, AttackType, WeaponLevel, WinfoValue, BaseLevel, this.GetModifier(), CleanDamageMax, false, Level, MateType); + + _criticalChance = MateType == MateType.Partner ? 0 : BaseCriticalChance; + _criticalDamage = MateType == MateType.Partner ? 0 : BaseCriticalRate; + + _meleeDefense = (short)_algorithm.GetDefense((short)MonsterRaceType, AttackType.Melee, ArmorLevel, BaseLevel, MeleeHpFactor, CleanMeleeDefence, false, Level, MateType); + _rangedDefense = (short)_algorithm.GetDefense((short)MonsterRaceType, AttackType.Ranged, ArmorLevel, BaseLevel, RangeDodgeFactor, CleanRangeDefence, false, Level, MateType); + _magicDefense = (short)_algorithm.GetDefense((short)MonsterRaceType, AttackType.Magical, ArmorLevel, BaseLevel, MagicMpFactor, CleanMagicDefence, false, Level, MateType); + _meleeDodge = (short)_algorithm.GetDodge((short)MonsterRaceType, ArmorLevel, BaseLevel, RangeDodgeFactor, CleanDodge, false, Level, MateType); + _rangedDodge = (short)_algorithm.GetDodge((short)MonsterRaceType, ArmorLevel, BaseLevel, RangeDodgeFactor, CleanDodge, false, Level, MateType); + + StatisticsComponent.RefreshMateStatistics(this); + } + + private short GetMoreStats(StatisticType type, short baseStats) => (short)(this.FindMoreStats(type) + baseStats); + + private int GetMateDamage(int baseDamage, bool isMin) + { + GameItemInstance weapon = Weapon; + if (weapon == null) + { + return baseDamage; + } + + int toAdd; + if (isMin) + { + toAdd = weapon.DamageMinimum + weapon.GameItem.DamageMinimum; + } + else + { + toAdd = weapon.DamageMaximum + weapon.GameItem.DamageMaximum; + } + + return toAdd + baseDamage; + } + + private int GetMateCritical(int baseCritical, bool isChance) + { + if (MateType == MateType.Pet) + { + return baseCritical; + } + + GameItemInstance weapon = Weapon; + if (weapon == null) + { + return baseCritical; + } + + int toAdd; + if (isChance) + { + toAdd = weapon.GameItem.CriticalLuckRate; + } + else + { + toAdd = weapon.GameItem.CriticalRate; + } + + return toAdd + baseCritical; + } + + private int GetMateHitRate(short hitRate) + { + int stats = AttackType switch + { + AttackType.Melee => this.FindMoreStats(StatisticType.HITRATE_MELEE), + AttackType.Ranged => this.FindMoreStats(StatisticType.HITRATE_RANGED), + AttackType.Magical => this.FindMoreStats(StatisticType.HITRATE_MAGIC), + _ => 0 + }; + + return stats + hitRate; + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Mates/MateEntity.cs b/srcs/WingsAPI.Game/Mates/MateEntity.cs new file mode 100644 index 0000000..6ea15b4 --- /dev/null +++ b/srcs/WingsAPI.Game/Mates/MateEntity.cs @@ -0,0 +1,490 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Core.Generics; +using WingsEmu.DTOs.BCards; +using WingsEmu.DTOs.Skills; +using WingsEmu.Game._enum; +using WingsEmu.Game.Algorithm; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Battle.Managers; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Entities; +using WingsEmu.Game.EntityStatistics; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Npcs; +using WingsEmu.Game.Revival; +using WingsEmu.Game.Skills; +using WingsEmu.Game.Triggers; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Mates; + +public partial class MateEntity : IMateEntity +{ + private readonly IBattleEntityAlgorithmService _algorithm; + private readonly ICastingComponent _castingComponent; + private readonly IEndBuffDamageComponent _endBuffDamageComponent; + private readonly IAsyncEventPipeline _eventPipeline; + private readonly IEventTriggerContainer _eventTriggerContainer; + private readonly IReadOnlyList _npcMonsterSkills; + + public MateEntity(IPlayerEntity owner, MonsterData npcMonster, byte level, MateType mateType, + IMateTransportFactory transportFactory, IAsyncEventPipeline eventPipeline, IBattleEntityAlgorithmService algorithm, + IRandomGenerator randomGenerator) + { + Owner = owner; + NpcMonsterVNum = npcMonster.Id; + Level = level; + MateName = npcMonster.Name; + MateType = mateType; + Loyalty = 1000; + CharacterId = owner.Id; + Id = transportFactory.GenerateTransportId(); + + #region NpcMonsterData + + ArmorLevel = npcMonster.ArmorLevel; + AttackType = npcMonster.AttackType; + AttackUpgrade = npcMonster.AttackUpgrade; + AttackEffect = npcMonster.AttackEffect; + BasicCastTime = npcMonster.BasicCastTime; + BasicCooldown = npcMonster.BasicCooldown; + BasicRange = npcMonster.BasicRange; + BCards = npcMonster.BCards; + BasicHitChance = npcMonster.BasicHitChance; + CellSize = npcMonster.CellSize; + CleanDamageMin = npcMonster.CleanDamageMin; + CleanDamageMax = npcMonster.CleanDamageMax; + CleanHitRate = npcMonster.CleanHitRate; + CleanMeleeDefence = npcMonster.CleanMeleeDefence; + CleanRangeDefence = npcMonster.CleanRangeDefence; + CleanMagicDefence = npcMonster.CleanMagicDefence; + CleanDodge = npcMonster.CleanDodge; + CleanHp = npcMonster.CleanHp; + CleanMp = npcMonster.CleanMp; + BaseCloseDefence = npcMonster.BaseCloseDefence; + BaseConcentrate = npcMonster.BaseConcentrate; + BaseCriticalChance = npcMonster.BaseCriticalChance; + BaseCriticalRate = npcMonster.BaseCriticalRate; + BaseDamageMaximum = npcMonster.BaseDamageMaximum; + BaseDamageMinimum = npcMonster.BaseDamageMinimum; + BaseDarkResistance = npcMonster.BaseDarkResistance; + BaseMaxHp = npcMonster.BaseMaxHp; + BaseMaxMp = npcMonster.BaseMaxMp; + DefenceDodge = npcMonster.DefenceDodge; + DefenceUpgrade = npcMonster.DefenceUpgrade; + DistanceDefence = npcMonster.DistanceDefence; + DistanceDefenceDodge = npcMonster.DistanceDefenceDodge; + BaseElement = npcMonster.BaseElement; + BaseElementRate = npcMonster.BaseElementRate; + BaseFireResistance = npcMonster.BaseFireResistance; + BaseLevel = npcMonster.BaseLevel; + BaseLightResistance = npcMonster.BaseLightResistance; + MagicDefence = npcMonster.MagicDefence; + MagicMpFactor = npcMonster.MagicMpFactor; + MeleeHpFactor = npcMonster.MeleeHpFactor; + MinimumAttackRange = npcMonster.MinimumAttackRange; + MonsterVNum = npcMonster.MonsterVNum; + MateName = npcMonster.Name; + MonsterRaceType = npcMonster.MonsterRaceType; + MonsterRaceSubType = npcMonster.MonsterRaceSubType; + RangeDodgeFactor = npcMonster.RangeDodgeFactor; + _speed = npcMonster.BaseSpeed; + BaseWaterResistance = npcMonster.BaseWaterResistance; + WeaponLevel = npcMonster.WeaponLevel; + WinfoValue = npcMonster.WinfoValue; + Name = npcMonster.Name; + MonsterSkills = npcMonster.MonsterSkills; + BaseXp = npcMonster.BaseXp; + BaseJobXp = npcMonster.BaseJobXp; + _npcMonsterSkills = npcMonster.Skills; + + #endregion + + _eventPipeline = eventPipeline; + _algorithm = algorithm; + _revivalComponent = new MateRevivalComponent(); + BCardComponent = new BCardComponent(randomGenerator); + BuffComponent = new BuffComponent(); + _eventTriggerContainer = new EventTriggerContainer(_eventPipeline); + _castingComponent = new CastingComponent(); + _endBuffDamageComponent = new EndBuffDamageComponent(); + StatisticsComponent = new MateStatisticsComponent(this); + ChargeComponent = new ChargeComponent(); + } + + private IMateRevivalComponent _revivalComponent { get; } + + public bool TeleportBackOnNoticeRangeExceed { get; } + + public DateTime LastSpeedChange { get; set; } + + public byte Size { get; set; } = 10; + + public byte Element + { + get => IsUsingSp && Specialist != null ? Specialist.GameItem.Element : BaseElement; + set { } + } + + public int ElementRate + { + get => IsUsingSp && Specialist != null ? Specialist.GameItem.ElementRate : BaseElementRate; + set { } + } + + public int FireResistance + { + get => GetMoreStats(StatisticType.FIRE, BaseFireResistance); + set { } + } + + public int WaterResistance + { + get => GetMoreStats(StatisticType.WATER, BaseWaterResistance); + set { } + } + + public int LightResistance + { + get => GetMoreStats(StatisticType.LIGHT, BaseLightResistance); + set { } + } + + public int DarkResistance + { + get => GetMoreStats(StatisticType.DARK, BaseDarkResistance); + set { } + } + + public int DamagesMinimum + { + get => GetMateDamage(_damageMin, true); + set { } + } + + public int DamagesMaximum + { + get => GetMateDamage(_damageMax, false); + set { } + } + + public short CloseDefence + { + get => GetMoreStats(StatisticType.DEFENSE_MELEE, _meleeDefense); + set { } + } + + public short DistanceDefence + { + get => GetMoreStats(StatisticType.DEFENSE_RANGED, _rangedDefense); + set { } + } + + public short MagicDefence + { + get => GetMoreStats(StatisticType.DEFENSE_MAGIC, _magicDefense); + set { } + } + + public int BaseXp { get; } + public int BaseJobXp { get; } + + public short DefenceDodge + { + get => GetMoreStats(StatisticType.DODGE_MELEE, _meleeDodge); + set { } + } + + public short DistanceDodge + { + get => GetMoreStats(StatisticType.DODGE_RANGED, _rangedDodge); + set { } + } + + public short HitRate + { + get => (short)GetMateHitRate(_hitRate); + set { } + } + + public int HitCriticalChance + { + get => GetMateCritical(_criticalChance, true); + set { } + } + + public int HitCriticalDamage + { + get => GetMateCritical(_criticalDamage, false); + set { } + } + + public int MaxHp + { + get => this.GetMaxHp(_maxHp); + + set => _maxHp = value; + } + + public int MaxMp + { + get => this.GetMaxMp(_maxMp); + + set => _maxMp = value; + } + + public byte Speed + { + get => this.GetSpeed(_speed); + + set + { + LastSpeedChange = DateTime.UtcNow; + _speed = value > 59 ? (byte)59 : value; + } + } + + public IBCardComponent BCardComponent { get; } + public IChargeComponent ChargeComponent { get; } + public ThreadSafeHashSet AggroedEntities { get; } = new(); + public IBuffComponent BuffComponent { get; } + + public FactionType Faction => Owner?.Faction ?? FactionType.Neutral; + + public bool IsSitting { get; set; } + + public bool IsUsingSp { get; set; } + + public short MinilandY { get; set; } + + public DateTime LastHealth { get; set; } + + public DateTime LastDeath { get; set; } + + public DateTime LastDefence { get; set; } + + public DateTime LastBasicSkill { get; set; } + + public DateTime? SpawnMateByGuardian { get; set; } + + public DateTime LastSkillUse { get; set; } + + public IMapInstance MapInstance => IsTeamMember ? Owner?.MapInstance : Owner?.Miniland; + + public IPlayerEntity Owner { get; set; } + + public byte PetSlot { get; set; } + + public List Skills { get; set; } + + public Position Position + { + get => new(PositionX, PositionY); + set + { + PositionX = value.X; + PositionY = value.Y; + } + } + + public short PositionX { get; set; } + + public short PositionY { get; set; } + + public IBattleEntitySkill LastUsedPartnerSkill { get; set; } + + public IBattleEntity Killer { get; set; } + + public bool IsLimited { get; init; } + public DateTime? SpCooldownEnd { get; set; } + public DateTime LastEffect { get; set; } + public DateTime LastPetUpgradeEffect { get; set; } + public GameItemInstance Weapon => Owner?.PartnerGetEquippedItem(EquipmentType.MainWeapon, PetSlot)?.ItemInstance; + public GameItemInstance Armor => Owner?.PartnerGetEquippedItem(EquipmentType.Armor, PetSlot)?.ItemInstance; + public GameItemInstance Gloves => Owner?.PartnerGetEquippedItem(EquipmentType.Gloves, PetSlot)?.ItemInstance; + public GameItemInstance Boots => Owner?.PartnerGetEquippedItem(EquipmentType.Boots, PetSlot)?.ItemInstance; + public GameItemInstance Specialist => Owner?.PartnerGetEquippedItem(EquipmentType.Sp, PetSlot)?.ItemInstance; + + public void Initialize() + { + RefreshStatistics(); + + if (Hp == 0) + { + Hp = _maxHp; + Mp = _maxMp; + } + + Skills = new List(); + foreach (INpcMonsterSkill skill in MonsterSkills) + { + var tmp = new NpcMonsterSkill(skill.Skill, skill.Rate, skill.IsBasicAttack, skill.IsIgnoringHitChance); + Skills.Add(tmp); + } + + foreach (NpcMonsterSkillDTO skill in _npcMonsterSkills) + { + var monsterSkill = new NpcMonsterSkill(StaticSkillsManager.Instance.GetSkill(skill.SkillVNum), skill.Rate, skill.IsBasicAttack, skill.IsIgnoringHitChance); + Skills.Add(monsterSkill); + } + + + this.InitializeBCards(); + Killer = null; + LastBasicSkill = DateTime.MinValue; + + BasicSkill = new SkillInfo + { + Vnum = 0, + AttackType = AttackType, + SkillType = SkillType.PartnerSkill, + CastTime = BasicCastTime, + Cooldown = BasicCooldown, + Element = Element, + HitAnimation = 11, + HitEffect = AttackEffect, + HitChance = BasicHitChance, + TargetType = TargetType.Target, + TargetAffectedEntities = TargetAffectedEntities.Enemies, + Range = BasicRange + }; + + var dictionary = new Dictionary>(); + foreach (BCardDTO bCard in BCardComponent.GetTriggerBCards(BCardTriggerType.ATTACK)) + { + var key = (SkillCastType)bCard.CastType; + if (!dictionary.TryGetValue(key, out HashSet hashSet)) + { + hashSet = new HashSet(); + dictionary[key] = hashSet; + } + + hashSet.Add(bCard); + } + + BasicSkill.BCardsType = dictionary; + } + + public SkillInfo BasicSkill { get; private set; } + + public IMateStatisticsComponent StatisticsComponent { get; } + + public DateTime LastLowLoyaltyEffect { get; set; } = DateTime.MinValue; + + public DateTime LastLoyaltyRecover { get; set; } = DateTime.MinValue; + + public VisualType Type => VisualType.Npc; + + public int Id { get; } + + public void AddEvent(string key, IAsyncEvent notification, bool removedOnTrigger = false) => _eventTriggerContainer.AddEvent(key, notification, removedOnTrigger); + + public async Task TriggerEvents(string key) => await _eventTriggerContainer.TriggerEvents(key); + + public async Task EmitEventAsync(T eventArgs) where T : IBattleEntityEvent + { + if (eventArgs.Entity != this) + { + throw new ArgumentException("An event should be emitted only from the event sender"); + } + + await _eventPipeline.ProcessEventAsync(eventArgs); + } + + public void EmitEvent(T eventArgs) where T : IBattleEntityEvent + { + EmitEventAsync(eventArgs).ConfigureAwait(false).GetAwaiter().GetResult(); + } + + public byte Attack { get; set; } + + public bool CanPickUp { get; set; } + + public long CharacterId { get; set; } + + public byte Defence { get; set; } + + public byte Direction { get; set; } = 2; + + public long Experience { get; set; } + + public int Hp { get; set; } + + public bool IsSummonable { get; set; } + + public bool IsTeamMember { get; set; } + + public byte Level { get; set; } + + public short Loyalty { get; set; } + + public short MapX { get; set; } + + public short MapY { get; set; } + + public short MinilandX { get; set; } + + public MateType MateType { get; set; } + + public int Mp { get; set; } + + public string MateName { get; set; } + + public int NpcMonsterVNum { get; set; } + + public short Skin { get; set; } + + public DateTime RevivalDateTimeForExecution => _revivalComponent.RevivalDateTimeForExecution; + + public bool IsRevivalDelayed => _revivalComponent.IsRevivalDelayed; + + public void UpdateRevival(DateTime revivalDateTimeForExecution, bool isRevivalDelayed) + { + _revivalComponent.UpdateRevival(revivalDateTimeForExecution, isRevivalDelayed); + } + + public void DisableRevival() + { + _revivalComponent.DisableRevival(); + } + + public SkillCast SkillCast => _castingComponent.SkillCast; + + public bool IsCastingSkill => _castingComponent.IsCastingSkill; + + public void SetCastingSkill(SkillInfo skill, DateTime time) + { + _castingComponent.SetCastingSkill(skill, time); + } + + public void RemoveCastingSkill() + { + _castingComponent.RemoveCastingSkill(); + } + + public IReadOnlyDictionary EndBuffDamages => _endBuffDamageComponent.EndBuffDamages; + + public void AddEndBuff(short buffVnum, int damage) + { + _endBuffDamageComponent.AddEndBuff(buffVnum, damage); + } + + public int DecreaseDamageEndBuff(short buffVnum, int damage) => _endBuffDamageComponent.DecreaseDamageEndBuff(buffVnum, damage); + + public void RemoveEndBuffDamage(short buffVnum) + { + _endBuffDamageComponent.RemoveEndBuffDamage(buffVnum); + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Mates/MateEntityFactory.cs b/srcs/WingsAPI.Game/Mates/MateEntityFactory.cs new file mode 100644 index 0000000..482d772 --- /dev/null +++ b/srcs/WingsAPI.Game/Mates/MateEntityFactory.cs @@ -0,0 +1,103 @@ +using PhoenixLib.Events; +using WingsEmu.DTOs.Mates; +using WingsEmu.Game.Algorithm; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Npcs; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Mates; + +public class MateEntityFactory : IMateEntityFactory +{ + private readonly IBattleEntityAlgorithmService _algorithm; + private readonly IAsyncEventPipeline _eventPipeline; + private readonly IMateTransportFactory _mateTransportFactory; + private readonly INpcMonsterManager _npcMonsterManager; + private readonly IRandomGenerator _randomGenerator; + + public MateEntityFactory(INpcMonsterManager npcMonsterManager, IAsyncEventPipeline eventPipeline, IBattleEntityAlgorithmService algorithm, IRandomGenerator randomGenerator, + IMateTransportFactory mateTransportFactory) + { + _npcMonsterManager = npcMonsterManager; + _eventPipeline = eventPipeline; + _algorithm = algorithm; + _randomGenerator = randomGenerator; + _mateTransportFactory = mateTransportFactory; + } + + public IMateEntity CreateMateEntity(IPlayerEntity owner, MonsterData monsterData, MateType mateType) => CreateMateEntity(owner, monsterData, mateType, 1); + + public IMateEntity CreateMateEntity(IPlayerEntity playerEntity, MateDTO mateDto) + { + var monsterData = new MonsterData(_npcMonsterManager.GetNpc(mateDto.NpcMonsterVNum)); + var mate = new MateEntity(playerEntity, monsterData, mateDto.Level, mateDto.MateType, _mateTransportFactory, _eventPipeline, _algorithm, _randomGenerator) + { + Attack = mateDto.Attack, + CanPickUp = mateDto.CanPickUp, + CharacterId = mateDto.CharacterId, + Defence = mateDto.Defence, + Direction = mateDto.Direction, + Experience = mateDto.Experience, + Hp = mateDto.Hp, + Level = mateDto.Level, + Loyalty = mateDto.Loyalty, + Mp = mateDto.Mp, + MateName = mateDto.MateName, + Skin = mateDto.Skin, + IsSummonable = mateDto.IsSummonable, + MapX = mateDto.MapX, + MapY = mateDto.MapY, + MateType = mateDto.MateType, + PetSlot = mateDto.PetSlot, + MinilandX = mateDto.MinilandX, + MinilandY = mateDto.MinilandY, + IsTeamMember = mateDto.IsTeamMember, + IsLimited = mateDto.IsLimited + }; + + return mate; + } + + public IMateEntity CreateMateEntity(IPlayerEntity owner, int monsterVnum, MateType mateType) + { + IMonsterData monsterData = _npcMonsterManager.GetNpc(monsterVnum); + return monsterData == null ? null : CreateMateEntity(owner, new MonsterData(monsterData), mateType, 1); + } + + public IMateEntity CreateMateEntity(IPlayerEntity owner, MonsterData monsterData, MateType mateType, byte level) => CreateMateEntity(owner, monsterData, mateType, level, false); + + public IMateEntity CreateMateEntity(IPlayerEntity owner, MonsterData monsterData, MateType mateType, byte level, bool isLimited) => + new MateEntity(owner, monsterData, level, mateType, _mateTransportFactory, _eventPipeline, _algorithm, _randomGenerator) + { + IsLimited = isLimited + }; + + public MateDTO CreateMateDto(IMateEntity mateEntity) => new() + { + Id = mateEntity.Id, + Attack = mateEntity.Attack, + CanPickUp = mateEntity.CanPickUp, + CharacterId = mateEntity.CharacterId, + Defence = mateEntity.Defence, + Direction = mateEntity.Direction, + Experience = mateEntity.Experience, + Hp = mateEntity.Hp, + Level = mateEntity.Level, + Loyalty = mateEntity.Loyalty, + Mp = mateEntity.Mp, + MateName = mateEntity.MateName, + Skin = mateEntity.Skin, + IsSummonable = mateEntity.IsSummonable, + MapX = mateEntity.MapX, + MapY = mateEntity.MapY, + MateType = mateEntity.MateType, + PetSlot = mateEntity.PetSlot, + MinilandX = mateEntity.MinilandX, + MinilandY = mateEntity.MinilandY, + IsTeamMember = mateEntity.IsTeamMember, + NpcMonsterVNum = mateEntity.NpcMonsterVNum, + IsLimited = mateEntity.IsLimited + }; +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Mates/StaticMateTransportFactory.cs b/srcs/WingsAPI.Game/Mates/StaticMateTransportFactory.cs new file mode 100644 index 0000000..dcb316a --- /dev/null +++ b/srcs/WingsAPI.Game/Mates/StaticMateTransportFactory.cs @@ -0,0 +1,11 @@ +namespace WingsEmu.Game.Mates; + +public class StaticMateTransportFactory +{ + public static IMateTransportFactory Instance { get; private set; } + + public static void Initialize(IMateTransportFactory factory) + { + Instance = factory; + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Miniland/Events/AddObjMinilandEndLogicEvent.cs b/srcs/WingsAPI.Game/Miniland/Events/AddObjMinilandEndLogicEvent.cs new file mode 100644 index 0000000..d2e3b0b --- /dev/null +++ b/srcs/WingsAPI.Game/Miniland/Events/AddObjMinilandEndLogicEvent.cs @@ -0,0 +1,23 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Game.Maps; + +namespace WingsEmu.Game.Miniland.Events; + +public class AddObjMinilandEndLogicEvent : PlayerEvent +{ + public AddObjMinilandEndLogicEvent(MapDesignObject mapObject, IMapInstance miniland) + { + MapObject = mapObject; + Miniland = miniland; + } + + public MapDesignObject MapObject { get; } + public IMapInstance Miniland { get; } +} + +public class MinilandChestViewContentEvent : PlayerEvent +{ + public MinilandChestViewContentEvent(int chestVnum) => ChestVnum = chestVnum; + + public int ChestVnum { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Miniland/Events/AddObjMinilandEvent.cs b/srcs/WingsAPI.Game/Miniland/Events/AddObjMinilandEvent.cs new file mode 100644 index 0000000..cf171f4 --- /dev/null +++ b/srcs/WingsAPI.Game/Miniland/Events/AddObjMinilandEvent.cs @@ -0,0 +1,19 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Miniland.Events; + +public class AddObjMinilandEvent : PlayerEvent +{ + public AddObjMinilandEvent(short slot, short x, short y) + { + Slot = slot; + X = x; + Y = y; + } + + public short Slot { get; } + + public short X { get; } + + public short Y { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Miniland/Events/MinigameDurabilityCouponEvent.cs b/srcs/WingsAPI.Game/Miniland/Events/MinigameDurabilityCouponEvent.cs new file mode 100644 index 0000000..d43a1cf --- /dev/null +++ b/srcs/WingsAPI.Game/Miniland/Events/MinigameDurabilityCouponEvent.cs @@ -0,0 +1,10 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Miniland.Events; + +public class MinigameDurabilityCouponEvent : PlayerEvent +{ + public MinigameDurabilityCouponEvent(MapDesignObject mapObject) => MapObject = mapObject; + + public MapDesignObject MapObject { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Miniland/Events/MinigameDurabilityInfoEvent.cs b/srcs/WingsAPI.Game/Miniland/Events/MinigameDurabilityInfoEvent.cs new file mode 100644 index 0000000..d57569b --- /dev/null +++ b/srcs/WingsAPI.Game/Miniland/Events/MinigameDurabilityInfoEvent.cs @@ -0,0 +1,10 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Miniland.Events; + +public class MinigameDurabilityInfoEvent : PlayerEvent +{ + public MinigameDurabilityInfoEvent(MapDesignObject mapObject) => MapObject = mapObject; + + public MapDesignObject MapObject { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Miniland/Events/MinigameGetYieldInfoEvent.cs b/srcs/WingsAPI.Game/Miniland/Events/MinigameGetYieldInfoEvent.cs new file mode 100644 index 0000000..cc6c5f5 --- /dev/null +++ b/srcs/WingsAPI.Game/Miniland/Events/MinigameGetYieldInfoEvent.cs @@ -0,0 +1,10 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Miniland.Events; + +public class MinigameGetYieldInfoEvent : PlayerEvent +{ + public MinigameGetYieldInfoEvent(MapDesignObject mapObject) => MapObject = mapObject; + + public MapDesignObject MapObject { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Miniland/Events/MinigameGetYieldRewardEvent.cs b/srcs/WingsAPI.Game/Miniland/Events/MinigameGetYieldRewardEvent.cs new file mode 100644 index 0000000..e87ee5c --- /dev/null +++ b/srcs/WingsAPI.Game/Miniland/Events/MinigameGetYieldRewardEvent.cs @@ -0,0 +1,17 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Game.Configurations.Miniland; + +namespace WingsEmu.Game.Miniland.Events; + +public class MinigameGetYieldRewardEvent : PlayerEvent +{ + public MinigameGetYieldRewardEvent(MapDesignObject mapObject, RewardLevel rewardLevel) + { + MapObject = mapObject; + RewardLevel = rewardLevel; + } + + public MapDesignObject MapObject { get; } + + public RewardLevel RewardLevel { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Miniland/Events/MinigamePlayEvent.cs b/srcs/WingsAPI.Game/Miniland/Events/MinigamePlayEvent.cs new file mode 100644 index 0000000..1c19275 --- /dev/null +++ b/srcs/WingsAPI.Game/Miniland/Events/MinigamePlayEvent.cs @@ -0,0 +1,16 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Miniland.Events; + +public class MinigamePlayEvent : PlayerEvent +{ + public MinigamePlayEvent(MapDesignObject minigameObject, bool isForFun) + { + MinigameObject = minigameObject; + IsForFun = isForFun; + } + + public bool IsForFun { get; } + + public MapDesignObject MinigameObject { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Miniland/Events/MinigameRepairDurabilityEvent.cs b/srcs/WingsAPI.Game/Miniland/Events/MinigameRepairDurabilityEvent.cs new file mode 100644 index 0000000..4919d34 --- /dev/null +++ b/srcs/WingsAPI.Game/Miniland/Events/MinigameRepairDurabilityEvent.cs @@ -0,0 +1,16 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Miniland.Events; + +public class MinigameRepairDurabilityEvent : PlayerEvent +{ + public MinigameRepairDurabilityEvent(MapDesignObject mapObject, long goldToExpend) + { + MapObject = mapObject; + GoldToExpend = goldToExpend; + } + + public MapDesignObject MapObject { get; } + + public long GoldToExpend { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Miniland/Events/MinigameRewardClaimedEvent.cs b/srcs/WingsAPI.Game/Miniland/Events/MinigameRewardClaimedEvent.cs new file mode 100644 index 0000000..6b8d871 --- /dev/null +++ b/srcs/WingsAPI.Game/Miniland/Events/MinigameRewardClaimedEvent.cs @@ -0,0 +1,15 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Game.Configurations.Miniland; + +namespace WingsEmu.Game.Miniland.Events; + +public class MinigameRewardClaimedEvent : PlayerEvent +{ + public long OwnerId { get; init; } + public int MinigameVnum { get; init; } + public MinigameType MinigameType { get; init; } + public RewardLevel RewardLevel { get; init; } + public bool Coupon { get; init; } + public int ItemVnum { get; init; } + public short Amount { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Miniland/Events/MinigameRewardEvent.cs b/srcs/WingsAPI.Game/Miniland/Events/MinigameRewardEvent.cs new file mode 100644 index 0000000..c4b24ec --- /dev/null +++ b/srcs/WingsAPI.Game/Miniland/Events/MinigameRewardEvent.cs @@ -0,0 +1,20 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Game.Configurations.Miniland; + +namespace WingsEmu.Game.Miniland.Events; + +public class MinigameRewardEvent : PlayerEvent +{ + public MinigameRewardEvent(RewardLevel rewardLevel, MapDesignObject mapObject, bool coupon) + { + RewardLevel = rewardLevel; + MapObject = mapObject; + Coupon = coupon; + } + + public RewardLevel RewardLevel { get; } + + public MapDesignObject MapObject { get; } + + public bool Coupon { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Miniland/Events/MinigameScoreEvent.cs b/srcs/WingsAPI.Game/Miniland/Events/MinigameScoreEvent.cs new file mode 100644 index 0000000..c7f1663 --- /dev/null +++ b/srcs/WingsAPI.Game/Miniland/Events/MinigameScoreEvent.cs @@ -0,0 +1,19 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Miniland.Events; + +public class MinigameScoreEvent : PlayerEvent +{ + public MinigameScoreEvent(MapDesignObject mapObject, long score1, long score2) + { + MapObject = mapObject; + Score1 = score1; + Score2 = score2; + } + + public MapDesignObject MapObject { get; } + + public long Score1 { get; } + + public long Score2 { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Miniland/Events/MinigameScoreLogEvent.cs b/srcs/WingsAPI.Game/Miniland/Events/MinigameScoreLogEvent.cs new file mode 100644 index 0000000..5b5e962 --- /dev/null +++ b/srcs/WingsAPI.Game/Miniland/Events/MinigameScoreLogEvent.cs @@ -0,0 +1,15 @@ +using System; +using WingsEmu.Game._packetHandling; +using WingsEmu.Game.Configurations.Miniland; + +namespace WingsEmu.Game.Miniland.Events; + +public class MinigameScoreLogEvent : PlayerEvent +{ + public long OwnerId { get; init; } + public TimeSpan CompletionTime { get; init; } + public int MinigameVnum { get; init; } + public MinigameType MinigameType { get; init; } + public long Score1 { get; init; } + public long Score2 { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Miniland/Events/MinigameStopEvent.cs b/srcs/WingsAPI.Game/Miniland/Events/MinigameStopEvent.cs new file mode 100644 index 0000000..387b7e1 --- /dev/null +++ b/srcs/WingsAPI.Game/Miniland/Events/MinigameStopEvent.cs @@ -0,0 +1,10 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Miniland.Events; + +public class MinigameStopEvent : PlayerEvent +{ + public MinigameStopEvent(MapDesignObject minigameObject) => MinigameObject = minigameObject; + + public MapDesignObject MinigameObject { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Miniland/Events/MinilandIntroEvent.cs b/srcs/WingsAPI.Game/Miniland/Events/MinilandIntroEvent.cs new file mode 100644 index 0000000..de3d77b --- /dev/null +++ b/srcs/WingsAPI.Game/Miniland/Events/MinilandIntroEvent.cs @@ -0,0 +1,10 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Miniland.Events; + +public class MinilandIntroEvent : PlayerEvent +{ + public MinilandIntroEvent(string requestedMinilandIntro) => RequestedMinilandIntro = requestedMinilandIntro; + + public string RequestedMinilandIntro { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Miniland/Events/MinilandStateEvent.cs b/srcs/WingsAPI.Game/Miniland/Events/MinilandStateEvent.cs new file mode 100644 index 0000000..c3f124c --- /dev/null +++ b/srcs/WingsAPI.Game/Miniland/Events/MinilandStateEvent.cs @@ -0,0 +1,11 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Miniland.Events; + +public class MinilandStateEvent : PlayerEvent +{ + public MinilandStateEvent(MinilandState desiredMinilandState) => DesiredMinilandState = desiredMinilandState; + + public MinilandState DesiredMinilandState { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Miniland/Events/RmvObjMinilandEvent.cs b/srcs/WingsAPI.Game/Miniland/Events/RmvObjMinilandEvent.cs new file mode 100644 index 0000000..edbc734 --- /dev/null +++ b/srcs/WingsAPI.Game/Miniland/Events/RmvObjMinilandEvent.cs @@ -0,0 +1,10 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Miniland.Events; + +public class RmvObjMinilandEvent : PlayerEvent +{ + public RmvObjMinilandEvent(short slot) => Slot = slot; + + public short Slot { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Miniland/Events/UseObjMinilandEvent.cs b/srcs/WingsAPI.Game/Miniland/Events/UseObjMinilandEvent.cs new file mode 100644 index 0000000..d5a1edb --- /dev/null +++ b/srcs/WingsAPI.Game/Miniland/Events/UseObjMinilandEvent.cs @@ -0,0 +1,16 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Miniland.Events; + +public class UseObjMinilandEvent : PlayerEvent +{ + public UseObjMinilandEvent(string characterName, short slot) + { + CharacterName = characterName; + Slot = slot; + } + + public string CharacterName { get; } + + public short Slot { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Miniland/IMinigameManager.cs b/srcs/WingsAPI.Game/Miniland/IMinigameManager.cs new file mode 100644 index 0000000..c370425 --- /dev/null +++ b/srcs/WingsAPI.Game/Miniland/IMinigameManager.cs @@ -0,0 +1,20 @@ +using System.Threading.Tasks; +using WingsEmu.Game.Configurations.Miniland; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Game.Miniland; + +public interface IMinigameManager +{ + Task CanRefreshMinigamesFreeProductionPoints(long playerEntityId); + public MinigameScoresHolder GetScores(int minigameVnum); + + public Minigame GetSpecificMinigameConfiguration(int minigameVnum); + + public void RegisterInteraction(IClientSession session, MinilandInteractionInformationHolder minilandInteraction); + + public void ReportInteractionIncoherence(IClientSession session, MinigameInteraction lastInteraction, MapDesignObject lastMapObject, + MinigameInteraction actualInteraction, MapDesignObject actualMapObject); + + public MinilandInteractionInformationHolder GetLastInteraction(IClientSession session); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Miniland/IMinilandManager.cs b/srcs/WingsAPI.Game/Miniland/IMinilandManager.cs new file mode 100644 index 0000000..13429c1 --- /dev/null +++ b/srcs/WingsAPI.Game/Miniland/IMinilandManager.cs @@ -0,0 +1,33 @@ +using System.Threading.Tasks; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Game.Miniland; + +public interface IMinilandManager +{ + IMapInstance CreateMinilandByCharacterSession(IClientSession session); + + IMapInstance GetMinilandByCharacterId(long characterId); + + IClientSession GetSessionByMiniland(IMapInstance mapInstance); + + void SaveMinilandInvite(long senderId, long targetId); + + bool ContainsMinilandInvite(long senderId); + + bool ContainsTargetInvite(long senderId, long targetId); + + void RemoveMinilandInvite(long senderId, long targetId); + + int GetMinilandMaximumCapacity(long characterId); + + void RelativeUpdateMinilandCapacity(long characterId, int valueToAdd); + Task IncreaseMinilandVisitCounter(long characterId); + Task CanRefreshDailyVisitCounter(long characterId); + Task GetMinilandVisitCounter(long characterId); + + Configurations.Miniland.Miniland GetMinilandConfiguration(IMapInstance mapInstance); + + void RemoveMiniland(long characterId); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Miniland/InteractionInformationHolder.cs b/srcs/WingsAPI.Game/Miniland/InteractionInformationHolder.cs new file mode 100644 index 0000000..1007468 --- /dev/null +++ b/srcs/WingsAPI.Game/Miniland/InteractionInformationHolder.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using WingsEmu.Game.Configurations.Miniland; + +namespace WingsEmu.Game.Miniland; + +public class MinilandInteractionInformationHolder +{ + public MinilandInteractionInformationHolder(MinigameInteraction interaction, MapDesignObject mapObject) + { + Interaction = interaction; + TimeOfInteraction = DateTime.UtcNow; + MapObject = mapObject; + } + + public MinilandInteractionInformationHolder(MinigameInteraction interaction, MapDesignObject mapObject, (RewardLevel maxRewardLevel, List rewards) rewards) + { + Interaction = interaction; + TimeOfInteraction = DateTime.UtcNow; + MapObject = mapObject; + SavedRewards = rewards; + } + + public MinigameInteraction Interaction { get; set; } + + public DateTime TimeOfInteraction { get; set; } + + public MapDesignObject MapObject { get; set; } + + public (RewardLevel maxRewardLevel, List rewards) SavedRewards { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Miniland/MapDesignObject.cs b/srcs/WingsAPI.Game/Miniland/MapDesignObject.cs new file mode 100644 index 0000000..16dae56 --- /dev/null +++ b/srcs/WingsAPI.Game/Miniland/MapDesignObject.cs @@ -0,0 +1,14 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsAPI.Data.Miniland; +using WingsEmu.Game.Inventory; + +namespace WingsEmu.Game.Miniland; + +public class MapDesignObject : CharacterMinilandObjectDto +{ + public long CharacterId { get; set; } + public InventoryItem InventoryItem { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Miniland/MinigameInteraction.cs b/srcs/WingsAPI.Game/Miniland/MinigameInteraction.cs new file mode 100644 index 0000000..01c3879 --- /dev/null +++ b/srcs/WingsAPI.Game/Miniland/MinigameInteraction.cs @@ -0,0 +1,16 @@ +namespace WingsEmu.Game.Miniland; + +public enum MinigameInteraction +{ + None, + GetMinigameInformation, + DeclaratePlay, + DeclarateStop, + DeclarateScore, + GetReward, + GetMinigameDurability, + RepairMinigameDurability, + GetYieldInformation, + GetYieldReward, + UseDurabilityCoupon +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Miniland/Minigames/MinigameRefreshProductionEvent.cs b/srcs/WingsAPI.Game/Miniland/Minigames/MinigameRefreshProductionEvent.cs new file mode 100644 index 0000000..fd4ad52 --- /dev/null +++ b/srcs/WingsAPI.Game/Miniland/Minigames/MinigameRefreshProductionEvent.cs @@ -0,0 +1,14 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Miniland.Minigames; + +public class MinigameRefreshProductionEvent : PlayerEvent +{ + public MinigameRefreshProductionEvent(bool force) => Force = force; + + public bool Force { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Miniland/StaticMinilandManager.cs b/srcs/WingsAPI.Game/Miniland/StaticMinilandManager.cs new file mode 100644 index 0000000..c2bf6f3 --- /dev/null +++ b/srcs/WingsAPI.Game/Miniland/StaticMinilandManager.cs @@ -0,0 +1,11 @@ +namespace WingsEmu.Game.Miniland; + +public class StaticMinilandManager +{ + public static IMinilandManager Instance { get; private set; } + + public static void Initialize(IMinilandManager manager) + { + Instance = manager; + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Monster/Event/MonsterDeathEvent.cs b/srcs/WingsAPI.Game/Monster/Event/MonsterDeathEvent.cs new file mode 100644 index 0000000..7be9559 --- /dev/null +++ b/srcs/WingsAPI.Game/Monster/Event/MonsterDeathEvent.cs @@ -0,0 +1,15 @@ +using PhoenixLib.Events; +using WingsEmu.Game.Entities; + +namespace WingsEmu.Game.Monster.Event; + +public class MonsterDeathEvent : IAsyncEvent +{ + public MonsterDeathEvent(IMonsterEntity monsterEntity) => MonsterEntity = monsterEntity; + + public IMonsterEntity MonsterEntity { get; } + + public IBattleEntity Killer { get; set; } + + public bool IsByCommand { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Monster/Event/MonsterRespawnedEvent.cs b/srcs/WingsAPI.Game/Monster/Event/MonsterRespawnedEvent.cs new file mode 100644 index 0000000..9292ee1 --- /dev/null +++ b/srcs/WingsAPI.Game/Monster/Event/MonsterRespawnedEvent.cs @@ -0,0 +1,9 @@ +using PhoenixLib.Events; +using WingsEmu.Game.Entities; + +namespace WingsEmu.Game.Monster.Event; + +public class MonsterRespawnedEvent : IAsyncEvent +{ + public IMonsterEntity Monster { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Monster/Event/MonsterSummonEvent.cs b/srcs/WingsAPI.Game/Monster/Event/MonsterSummonEvent.cs new file mode 100644 index 0000000..e15cbf4 --- /dev/null +++ b/srcs/WingsAPI.Game/Monster/Event/MonsterSummonEvent.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using PhoenixLib.Events; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Maps; + +namespace WingsEmu.Game.Monster.Event; + +public class MonsterSummonEvent : IAsyncEvent +{ + public MonsterSummonEvent(IMapInstance map, IEnumerable monsters, IBattleEntity summoner = null, + bool getSummonerLevel = true, bool showEffect = false, short? scaledWithPlayerAmount = null) + { + Map = map; + Monsters = monsters; + Summoner = summoner; + GetSummonerLevel = getSummonerLevel; + ShowEffect = showEffect; + ScaledWithPlayerAmount = scaledWithPlayerAmount; + } + + public IMapInstance Map { get; } + public IEnumerable Monsters { get; } + public IBattleEntity Summoner { get; } + public bool GetSummonerLevel { get; } + public bool ShowEffect { get; } + public short? ScaledWithPlayerAmount { get; } + public Guid? NpcId { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Monster/Waypoint.cs b/srcs/WingsAPI.Game/Monster/Waypoint.cs new file mode 100644 index 0000000..1ba26b9 --- /dev/null +++ b/srcs/WingsAPI.Game/Monster/Waypoint.cs @@ -0,0 +1,8 @@ +namespace WingsEmu.Game.Monster; + +public class Waypoint +{ + public short X { get; init; } + public short Y { get; init; } + public int WaitTime { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/MonsterMapItem.cs b/srcs/WingsAPI.Game/MonsterMapItem.cs new file mode 100644 index 0000000..0ea613c --- /dev/null +++ b/srcs/WingsAPI.Game/MonsterMapItem.cs @@ -0,0 +1,29 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsEmu.Game.Items; +using WingsEmu.Game.Maps; + +namespace WingsEmu.Game; + +public sealed class MonsterMapItem : MapItem +{ + #region Instantiation + + public MonsterMapItem(short x, short y, GameItemInstance itemInstance, IMapInstance mapInstance, long ownerId = -1, bool isQuest = false) : base(x, y, isQuest, mapInstance) + { + OwnerId = ownerId; + ItemInstance = itemInstance; + } + + public override int Amount => ItemInstance.Amount; + + public override int ItemVNum => ItemInstance.ItemVNum; + + public long? OwnerId { get; } + + public override GameItemInstance GetItemInstance() => ItemInstance; + + #endregion +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Networking/BroadcastPacket.cs b/srcs/WingsAPI.Game/Networking/BroadcastPacket.cs new file mode 100644 index 0000000..5c752a8 --- /dev/null +++ b/srcs/WingsAPI.Game/Networking/BroadcastPacket.cs @@ -0,0 +1,44 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Networking; + +public class BroadcastPacket +{ + #region Instantiation + + public BroadcastPacket(IClientSession session, string packet, ReceiverType receiver, + string someonesCharacterName = "", long someonesCharacterId = -1, int xCoordinate = 0, int yCoordinate = 0) + { + Sender = session; + Packet = packet; + Receiver = receiver; + SomeonesCharacterName = someonesCharacterName; + SomeonesCharacterId = someonesCharacterId; + XCoordinate = xCoordinate; + YCoordinate = yCoordinate; + } + + #endregion + + #region Properties + + public string Packet { get; set; } + + public ReceiverType Receiver { get; set; } + + public IClientSession Sender { get; set; } + + public long SomeonesCharacterId { get; set; } + + public string SomeonesCharacterName { get; set; } + + public int XCoordinate { get; set; } + + public int YCoordinate { get; set; } + + #endregion +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Networking/Broadcasting/EmoticonsBroadcast.cs b/srcs/WingsAPI.Game/Networking/Broadcasting/EmoticonsBroadcast.cs new file mode 100644 index 0000000..7739026 --- /dev/null +++ b/srcs/WingsAPI.Game/Networking/Broadcasting/EmoticonsBroadcast.cs @@ -0,0 +1,6 @@ +namespace WingsEmu.Game.Networking.Broadcasting; + +public class EmoticonsBroadcast : IBroadcastRule +{ + public bool Match(IClientSession session) => !session.PlayerEntity.EmoticonsBlocked; +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Networking/Broadcasting/ExceptGroupBroadcast.cs b/srcs/WingsAPI.Game/Networking/Broadcasting/ExceptGroupBroadcast.cs new file mode 100644 index 0000000..203d7a9 --- /dev/null +++ b/srcs/WingsAPI.Game/Networking/Broadcasting/ExceptGroupBroadcast.cs @@ -0,0 +1,12 @@ +using WingsEmu.Game.Groups; + +namespace WingsEmu.Game.Networking.Broadcasting; + +public class ExceptGroupBroadcast : IBroadcastRule +{ + private readonly long _groupId; + + public ExceptGroupBroadcast(PlayerGroup playerGroup) => _groupId = playerGroup.GroupId; + + public bool Match(IClientSession session) => session.PlayerEntity.GetGroup()?.GroupId != _groupId; +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Networking/Broadcasting/ExceptRaidBroadcast.cs b/srcs/WingsAPI.Game/Networking/Broadcasting/ExceptRaidBroadcast.cs new file mode 100644 index 0000000..93a101c --- /dev/null +++ b/srcs/WingsAPI.Game/Networking/Broadcasting/ExceptRaidBroadcast.cs @@ -0,0 +1,12 @@ +using System; + +namespace WingsEmu.Game.Networking.Broadcasting; + +public class ExceptRaidBroadcast : IBroadcastRule +{ + private readonly Guid _id; + + public ExceptRaidBroadcast(Guid raidId) => _id = raidId; + + public bool Match(IClientSession session) => session != null && (!session.PlayerEntity.IsInRaidParty || session.PlayerEntity.Raid != null && session.PlayerEntity.Raid.Id != _id); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Networking/Broadcasting/ExceptSessionBroadcast.cs b/srcs/WingsAPI.Game/Networking/Broadcasting/ExceptSessionBroadcast.cs new file mode 100644 index 0000000..905875c --- /dev/null +++ b/srcs/WingsAPI.Game/Networking/Broadcasting/ExceptSessionBroadcast.cs @@ -0,0 +1,10 @@ +namespace WingsEmu.Game.Networking.Broadcasting; + +public class ExceptSessionBroadcast : IBroadcastRule +{ + private readonly long _sessionCharacterId; + + public ExceptSessionBroadcast(IClientSession session) => _sessionCharacterId = session.PlayerEntity.Id; + + public bool Match(IClientSession session) => session.PlayerEntity.Id != _sessionCharacterId; +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Networking/Broadcasting/ExpectBlockedPlayerBroadcast.cs b/srcs/WingsAPI.Game/Networking/Broadcasting/ExpectBlockedPlayerBroadcast.cs new file mode 100644 index 0000000..b100f4d --- /dev/null +++ b/srcs/WingsAPI.Game/Networking/Broadcasting/ExpectBlockedPlayerBroadcast.cs @@ -0,0 +1,9 @@ +namespace WingsEmu.Game.Networking.Broadcasting; + +public class ExpectBlockedPlayerBroadcast : IBroadcastRule +{ + private readonly long _senderId; + public ExpectBlockedPlayerBroadcast(long senderId) => _senderId = senderId; + + public bool Match(IClientSession session) => !session.PlayerEntity.IsBlocking(_senderId); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Networking/Broadcasting/ExpectRainbowEnemyTeamBroadcast.cs b/srcs/WingsAPI.Game/Networking/Broadcasting/ExpectRainbowEnemyTeamBroadcast.cs new file mode 100644 index 0000000..e2a7ea7 --- /dev/null +++ b/srcs/WingsAPI.Game/Networking/Broadcasting/ExpectRainbowEnemyTeamBroadcast.cs @@ -0,0 +1,23 @@ +namespace WingsEmu.Game.Networking.Broadcasting; + +public class RainbowTeamBroadcast : IBroadcastRule +{ + private readonly IClientSession _session; + + public RainbowTeamBroadcast(IClientSession session) => _session = session; + + public bool Match(IClientSession session) + { + if (!session.PlayerEntity.RainbowBattleComponent.IsInRainbowBattle) + { + return true; + } + + if (!_session.PlayerEntity.RainbowBattleComponent.IsInRainbowBattle) + { + return true; + } + + return session.PlayerEntity.RainbowBattleComponent.Team == _session.PlayerEntity.RainbowBattleComponent.Team; + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Networking/Broadcasting/FactionBroadcast.cs b/srcs/WingsAPI.Game/Networking/Broadcasting/FactionBroadcast.cs new file mode 100644 index 0000000..9e2a628 --- /dev/null +++ b/srcs/WingsAPI.Game/Networking/Broadcasting/FactionBroadcast.cs @@ -0,0 +1,31 @@ +using WingsEmu.Game.Extensions; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Networking.Broadcasting; + +public class FactionBroadcast : IBroadcastRule +{ + private readonly FactionType _faction; + private readonly bool _gmAffected; + + public FactionBroadcast(FactionType faction, bool gmAffected = false) + { + _faction = faction; + _gmAffected = gmAffected; + } + + public bool Match(IClientSession session) + { + if (session == null) + { + return false; + } + + if (_gmAffected || !session.IsGameMaster()) + { + return session.PlayerEntity.Faction == _faction; + } + + return true; + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Networking/Broadcasting/FamilyBroadcast.cs b/srcs/WingsAPI.Game/Networking/Broadcasting/FamilyBroadcast.cs new file mode 100644 index 0000000..a4097bc --- /dev/null +++ b/srcs/WingsAPI.Game/Networking/Broadcasting/FamilyBroadcast.cs @@ -0,0 +1,10 @@ +namespace WingsEmu.Game.Networking.Broadcasting; + +public class FamilyBroadcast : IBroadcastRule +{ + private readonly long _familyId; + + public FamilyBroadcast(long familyId) => _familyId = familyId; + + public bool Match(IClientSession session) => session.PlayerEntity.Family != null && session.PlayerEntity.Family.Id == _familyId; +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Networking/Broadcasting/GroupBroadcast.cs b/srcs/WingsAPI.Game/Networking/Broadcasting/GroupBroadcast.cs new file mode 100644 index 0000000..a99d39d --- /dev/null +++ b/srcs/WingsAPI.Game/Networking/Broadcasting/GroupBroadcast.cs @@ -0,0 +1,16 @@ +using WingsEmu.Game.Groups; + +namespace WingsEmu.Game.Networking.Broadcasting; + +public class GroupBroadcast : IBroadcastRule +{ + private readonly long _groupId; + + public GroupBroadcast(PlayerGroup playerGroup) => _groupId = playerGroup.GroupId; + + public bool Match(IClientSession session) + { + PlayerGroup group = session.PlayerEntity.GetGroup(); + return group != null && group.GroupId == _groupId; + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Networking/Broadcasting/IBroadcastRule.cs b/srcs/WingsAPI.Game/Networking/Broadcasting/IBroadcastRule.cs new file mode 100644 index 0000000..2408131 --- /dev/null +++ b/srcs/WingsAPI.Game/Networking/Broadcasting/IBroadcastRule.cs @@ -0,0 +1,15 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Game.Networking.Broadcasting; + +public interface IBroadcastRule +{ + /// + /// Tells whether or not the given session matches a broadcasting rule + /// + /// + /// true if the session should receive the packet + bool Match(IClientSession session); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Networking/Broadcasting/InBaseMapBroadcast.cs b/srcs/WingsAPI.Game/Networking/Broadcasting/InBaseMapBroadcast.cs new file mode 100644 index 0000000..95419d7 --- /dev/null +++ b/srcs/WingsAPI.Game/Networking/Broadcasting/InBaseMapBroadcast.cs @@ -0,0 +1,8 @@ +using WingsEmu.DTOs.Maps; + +namespace WingsEmu.Game.Networking.Broadcasting; + +public class InBaseMapBroadcast : IBroadcastRule +{ + public bool Match(IClientSession session) => session.CurrentMapInstance != null && session.CurrentMapInstance.HasMapFlag(MapFlags.IS_BASE_MAP); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Networking/Broadcasting/InRaidBroadcast.cs b/srcs/WingsAPI.Game/Networking/Broadcasting/InRaidBroadcast.cs new file mode 100644 index 0000000..43cf081 --- /dev/null +++ b/srcs/WingsAPI.Game/Networking/Broadcasting/InRaidBroadcast.cs @@ -0,0 +1,13 @@ +using System; +using WingsEmu.Game.Raids; + +namespace WingsEmu.Game.Networking.Broadcasting; + +public class InRaidBroadcast : IBroadcastRule +{ + private readonly Guid _id; + + public InRaidBroadcast(RaidParty raid) => _id = raid.Id; + + public bool Match(IClientSession session) => session.PlayerEntity.Raid != null && session.PlayerEntity.Raid.Id == _id; +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Networking/Broadcasting/LevelBroadcast.cs b/srcs/WingsAPI.Game/Networking/Broadcasting/LevelBroadcast.cs new file mode 100644 index 0000000..38e628f --- /dev/null +++ b/srcs/WingsAPI.Game/Networking/Broadcasting/LevelBroadcast.cs @@ -0,0 +1,10 @@ +namespace WingsEmu.Game.Networking.Broadcasting; + +public class LevelBroadcast : IBroadcastRule +{ + private readonly int _level; + + public LevelBroadcast(int level) => _level = level; + + public bool Match(IClientSession session) => session.PlayerEntity.Level >= _level; +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Networking/Broadcasting/NotMutedBroadcast.cs b/srcs/WingsAPI.Game/Networking/Broadcasting/NotMutedBroadcast.cs new file mode 100644 index 0000000..b3627b4 --- /dev/null +++ b/srcs/WingsAPI.Game/Networking/Broadcasting/NotMutedBroadcast.cs @@ -0,0 +1,8 @@ +using WingsEmu.Game.Extensions; + +namespace WingsEmu.Game.Networking.Broadcasting; + +public class NotMutedBroadcast : IBroadcastRule +{ + public bool Match(IClientSession session) => !session.IsMuted(); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Networking/Broadcasting/OnlyGameMasters.cs b/srcs/WingsAPI.Game/Networking/Broadcasting/OnlyGameMasters.cs new file mode 100644 index 0000000..7802076 --- /dev/null +++ b/srcs/WingsAPI.Game/Networking/Broadcasting/OnlyGameMasters.cs @@ -0,0 +1,8 @@ +using WingsEmu.DTOs.Account; + +namespace WingsEmu.Game.Networking.Broadcasting; + +public class OnlyGameMasters : IBroadcastRule +{ + public bool Match(IClientSession session) => session.PlayerEntity.Authority >= AuthorityType.GameMaster && session.GmMode; +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Networking/Broadcasting/OnlyPlayersBroadcast.cs b/srcs/WingsAPI.Game/Networking/Broadcasting/OnlyPlayersBroadcast.cs new file mode 100644 index 0000000..7e75703 --- /dev/null +++ b/srcs/WingsAPI.Game/Networking/Broadcasting/OnlyPlayersBroadcast.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; + +namespace WingsEmu.Game.Networking.Broadcasting; + +public class OnlyPlayersBroadcast : IBroadcastRule +{ + private readonly HashSet _players; + + public OnlyPlayersBroadcast(params long[] ids) => _players = new HashSet(ids); + + public bool Match(IClientSession session) => session.PlayerEntity != null && _players.Contains(session.PlayerEntity.Id); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Networking/Broadcasting/RaidBroadcast.cs b/srcs/WingsAPI.Game/Networking/Broadcasting/RaidBroadcast.cs new file mode 100644 index 0000000..50426c6 --- /dev/null +++ b/srcs/WingsAPI.Game/Networking/Broadcasting/RaidBroadcast.cs @@ -0,0 +1,12 @@ +using System; + +namespace WingsEmu.Game.Networking.Broadcasting; + +public class RaidBroadcast : IBroadcastRule +{ + private readonly Guid _raidId; + + public RaidBroadcast(Guid raidId) => _raidId = raidId; + + public bool Match(IClientSession session) => session != null && session.PlayerEntity.Raid != null && session.PlayerEntity.Raid.Id == _raidId; +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Networking/Broadcasting/RangeBroadcast.cs b/srcs/WingsAPI.Game/Networking/Broadcasting/RangeBroadcast.cs new file mode 100644 index 0000000..3286f77 --- /dev/null +++ b/srcs/WingsAPI.Game/Networking/Broadcasting/RangeBroadcast.cs @@ -0,0 +1,19 @@ +using WingsEmu.Game.Helpers.Damages; + +namespace WingsEmu.Game.Networking.Broadcasting; + +public class RangeBroadcast : IBroadcastRule +{ + private readonly int _range; + private readonly int _x; + private readonly int _y; + + public RangeBroadcast(int x, int y, int range = 20) + { + _x = x; + _y = y; + _range = range; + } + + public bool Match(IClientSession session) => session.PlayerEntity.Position.GetDistance(_x, _y) <= _range; +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Networking/Broadcasting/SpeakerHeroBroadcast.cs b/srcs/WingsAPI.Game/Networking/Broadcasting/SpeakerHeroBroadcast.cs new file mode 100644 index 0000000..a822706 --- /dev/null +++ b/srcs/WingsAPI.Game/Networking/Broadcasting/SpeakerHeroBroadcast.cs @@ -0,0 +1,6 @@ +namespace WingsEmu.Game.Networking.Broadcasting; + +public class SpeakerHeroBroadcast : IBroadcastRule +{ + public bool Match(IClientSession session) => !session.PlayerEntity.HeroChatBlocked; +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Networking/Broadcasting/TimeSpaceBroadcast.cs b/srcs/WingsAPI.Game/Networking/Broadcasting/TimeSpaceBroadcast.cs new file mode 100644 index 0000000..e371046 --- /dev/null +++ b/srcs/WingsAPI.Game/Networking/Broadcasting/TimeSpaceBroadcast.cs @@ -0,0 +1,18 @@ +namespace WingsEmu.Game.Networking.Broadcasting; + +public class TimeSpaceBroadcast : IBroadcastRule +{ + private readonly IClientSession _session; + + public TimeSpaceBroadcast(IClientSession session) => _session = session; + + public bool Match(IClientSession session) + { + if (session.PlayerEntity.Id == _session.PlayerEntity.Id) + { + return false; + } + + return session.PlayerEntity.TimeSpaceComponent.IsInTimeSpaceParty; + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Networking/IBroadcaster.cs b/srcs/WingsAPI.Game/Networking/IBroadcaster.cs new file mode 100644 index 0000000..e28c8af --- /dev/null +++ b/srcs/WingsAPI.Game/Networking/IBroadcaster.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using WingsEmu.Game.Networking.Broadcasting; +using WingsEmu.Packets; + +namespace WingsEmu.Game.Networking; + +public interface IBroadcaster +{ + IReadOnlyList Sessions { get; } + + void Broadcast(string packet); + + void Broadcast(T packet, params IBroadcastRule[] rules) where T : IServerPacket; + void Broadcast(string packet, params IBroadcastRule[] rules); + + void Broadcast(IEnumerable packets); + void Broadcast(IEnumerable packets, params IBroadcastRule[] rules); + + + void Broadcast(Func generatePacketCallback); + void Broadcast(Func generatePacketCallback, params IBroadcastRule[] rules); + + + Task BroadcastAsync(Func> lambdaAsync); + Task BroadcastAsync(Func> lambdaAsync, params IBroadcastRule[] rules); + + void Broadcast(T packet) where T : IPacket; +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Networking/IClientSession.cs b/srcs/WingsAPI.Game/Networking/IClientSession.cs new file mode 100644 index 0000000..486cd88 --- /dev/null +++ b/srcs/WingsAPI.Game/Networking/IClientSession.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using PhoenixLib.MultiLanguage; +using WingsAPI.Communication.Sessions.Model; +using WingsEmu.Game._i18n; +using WingsEmu.Game._packetHandling; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Maps; +using WingsEmu.Packets; + +namespace WingsEmu.Game.Networking; + +public interface IClientSession : IUserLanguageService +{ + RegionLanguageType UserLanguage { get; } + Account Account { get; } + IPlayerEntity PlayerEntity { get; } + IMapInstance CurrentMapInstance { get; set; } + bool HasCurrentMapInstance { get; } + bool HasSelectedCharacter { get; } + byte? SelectedCharacterSlot { get; set; } + string IpAddress { get; } + string HardwareId { get; } + string ClientVersion { get; } + bool IsAuthenticated { get; } + bool IsConnected { get; } + bool IsDisposing { get; } + int SessionId { get; } + bool DebugMode { get; set; } + bool GmMode { get; set; } + void ForceDisconnect(); + void InitializeAccount(Account account, Session session); + void InitializePlayerEntity(IPlayerEntity character); + + /* + * Events + */ + void EmitEvent(T e) where T : PlayerEvent; + Task EmitEventAsync(T e) where T : PlayerEvent; + + /* + * Game Network + */ + void SendPacket(T packet) where T : IPacket; + void SendPacket(string packet); + void SendPackets(IEnumerable packets); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Networking/SessionsContainer.cs b/srcs/WingsAPI.Game/Networking/SessionsContainer.cs new file mode 100644 index 0000000..2ea959d --- /dev/null +++ b/srcs/WingsAPI.Game/Networking/SessionsContainer.cs @@ -0,0 +1,303 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using WingsEmu.Game.Networking.Broadcasting; +using WingsEmu.Packets; + +namespace WingsEmu.Game.Networking; + +public abstract class SessionsContainer : IBroadcaster +{ + private static readonly IPacketSerializer Serializer = new PacketSerializer(); + + private readonly ReaderWriterLockSlim _lock = new(LockRecursionPolicy.SupportsRecursion); + private readonly ConcurrentQueue _queue = new(); + + private readonly List _sessionsAll = new(); + + private bool _disposed; + + public IReadOnlyList Sessions + { + get + { + _lock.EnterReadLock(); + try + { + return _sessionsAll.ToArray(); + } + finally + { + _lock.ExitReadLock(); + } + } + } + + public void Broadcast(string packet) + { + _queue.Enqueue(packet); + } + + public void Broadcast(T packet, params IBroadcastRule[] rules) where T : IServerPacket + { + _lock.EnterReadLock(); + try + { + foreach (IClientSession session in _sessionsAll) + { + bool all = true; + foreach (IBroadcastRule x in rules) + { + if (!x.Match(session)) + { + all = false; + break; + } + } + + if (!rules.Any() || all) + { + session.SendPacket(packet); + } + } + } + finally + { + _lock.ExitReadLock(); + } + } + + public void Broadcast(string packet, params IBroadcastRule[] rules) + { + _lock.EnterReadLock(); + try + { + foreach (IClientSession session in _sessionsAll) + { + bool all = true; + foreach (IBroadcastRule x in rules) + { + if (!x.Match(session)) + { + all = false; + break; + } + } + + if (!rules.Any() || all) + { + session.SendPacket(packet); + } + } + } + finally + { + _lock.ExitReadLock(); + } + } + + public void Broadcast(IEnumerable packets) + { + foreach (string packet in packets) + { + _queue.Enqueue(packet); + } + } + + public void Broadcast(IEnumerable packets, params IBroadcastRule[] rules) + { + _lock.EnterReadLock(); + try + { + foreach (IClientSession session in _sessionsAll) + { + bool all = true; + foreach (IBroadcastRule x in rules) + { + if (!x.Match(session)) + { + all = false; + break; + } + } + + if (!rules.Any() || all) + { + session.SendPackets(packets); + } + } + } + finally + { + _lock.ExitReadLock(); + } + } + + public void Broadcast(Func generatePacketCallback) + { + _lock.EnterReadLock(); + try + { + foreach (IClientSession session in _sessionsAll) + { + session.SendPacket(generatePacketCallback(session)); + } + } + finally + { + _lock.ExitReadLock(); + } + } + + public void Broadcast(Func generatePacketCallback, params IBroadcastRule[] rules) + { + _lock.EnterReadLock(); + try + { + foreach (IClientSession session in _sessionsAll) + { + bool all = true; + foreach (IBroadcastRule x in rules) + { + if (x.Match(session)) + { + continue; + } + + all = false; + break; + } + + if (!rules.Any() || all) + { + session.SendPacket(generatePacketCallback(session)); + } + } + } + finally + { + _lock.ExitReadLock(); + } + } + + public async Task BroadcastAsync(Func> lambdaAsync) + { + _lock.EnterReadLock(); + try + { + foreach (IClientSession session in _sessionsAll) + { + session.SendPacket(await lambdaAsync(session)); + } + } + finally + { + _lock.ExitReadLock(); + } + } + + public async Task BroadcastAsync(Func> generatePacketCallback, params IBroadcastRule[] rules) + { + _lock.EnterReadLock(); + try + { + foreach (IClientSession session in _sessionsAll) + { + bool all = true; + foreach (IBroadcastRule x in rules) + { + if (!x.Match(session)) + { + all = false; + break; + } + } + + if (!rules.Any() || all) + { + session.SendPacket(await generatePacketCallback(session)); + } + } + } + finally + { + _lock.ExitReadLock(); + } + } + + public void Broadcast(T packet) where T : IPacket + { + Broadcast(Serializer.Serialize(packet)); + } + + protected void FlushPackets() + { + var packets = new List(); + while (_queue.TryDequeue(out string packet)) + { + packets.Add(packet); + } + + _lock.EnterReadLock(); + try + { + foreach (IClientSession session in _sessionsAll) + { + session.SendPackets(packets); + } + } + finally + { + _lock.ExitReadLock(); + } + } + + public virtual void Dispose() + { + if (_disposed) + { + return; + } + + GC.SuppressFinalize(this); + _disposed = true; + } + + public virtual void RegisterSession(IClientSession session) + { + if (!session.HasSelectedCharacter) + { + return; + } + + _lock.EnterWriteLock(); + try + { + _sessionsAll.Add(session); + } + finally + { + _lock.ExitWriteLock(); + } + } + + public virtual void UnregisterSession(IClientSession session) + { + _lock.EnterWriteLock(); + try + { + _sessionsAll.Remove(session); + } + finally + { + _lock.ExitWriteLock(); + } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Npcs/Event/ItemProducedEvent.cs b/srcs/WingsAPI.Game/Npcs/Event/ItemProducedEvent.cs new file mode 100644 index 0000000..eb7214e --- /dev/null +++ b/srcs/WingsAPI.Game/Npcs/Event/ItemProducedEvent.cs @@ -0,0 +1,10 @@ +using WingsEmu.DTOs.Items; +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Npcs.Event; + +public class ItemProducedEvent : PlayerEvent +{ + public ItemInstanceDTO ItemInstance { get; init; } + public int ItemAmount { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Npcs/Event/MapNpcGenerateDeathEvent.cs b/srcs/WingsAPI.Game/Npcs/Event/MapNpcGenerateDeathEvent.cs new file mode 100644 index 0000000..3c8b292 --- /dev/null +++ b/srcs/WingsAPI.Game/Npcs/Event/MapNpcGenerateDeathEvent.cs @@ -0,0 +1,17 @@ +using PhoenixLib.Events; +using WingsEmu.Game.Entities; + +namespace WingsEmu.Game.Npcs.Event; + +public class MapNpcGenerateDeathEvent : IAsyncEvent +{ + public MapNpcGenerateDeathEvent(INpcEntity npcEntity, IBattleEntity killer) + { + NpcEntity = npcEntity; + Killer = killer; + } + + public INpcEntity NpcEntity { get; } + + public IBattleEntity Killer { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Npcs/Event/NpcSummonEvent.cs b/srcs/WingsAPI.Game/Npcs/Event/NpcSummonEvent.cs new file mode 100644 index 0000000..25b7c91 --- /dev/null +++ b/srcs/WingsAPI.Game/Npcs/Event/NpcSummonEvent.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using PhoenixLib.Events; +using WingsEmu.Game.Maps; + +namespace WingsEmu.Game.Npcs.Event; + +public class NpcSummonEvent : IAsyncEvent +{ + public IMapInstance Map { get; init; } + public IEnumerable Npcs { get; init; } + public Guid? MonsterId { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Npcs/MonsterData.cs b/srcs/WingsAPI.Game/Npcs/MonsterData.cs new file mode 100644 index 0000000..05dc52a --- /dev/null +++ b/srcs/WingsAPI.Game/Npcs/MonsterData.cs @@ -0,0 +1,147 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Generic; +using WingsAPI.Data.Drops; +using WingsEmu.DTOs.BCards; +using WingsEmu.DTOs.NpcMonster; +using WingsEmu.Game._enum; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Skills; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Npcs; + +public class MonsterData : NpcMonsterDto, IMonsterData +{ + // Do not remove! + public MonsterData() + { + } + + public MonsterData(IMonsterData monsterData) + { + AmountRequired = monsterData.AmountRequired; + ArmorLevel = monsterData.ArmorLevel; + AttackType = monsterData.AttackType; + AttackUpgrade = monsterData.AttackUpgrade; + BasicCastTime = monsterData.BasicCastTime; + BasicCooldown = monsterData.BasicCooldown; + BasicRange = monsterData.BasicRange; + AttackEffect = monsterData.AttackEffect; + BasicHitChance = monsterData.BasicHitChance; + CanBeCollected = monsterData.CanBeCollected; + CanBeDebuffed = monsterData.CanBeDebuffed; + CanBeCaught = monsterData.CanBeCaught; + CanBePushed = monsterData.CanBePushed; + CanRegenMp = monsterData.CanRegenMp; + CanWalk = monsterData.CanWalk; + CellSize = monsterData.CellSize; + CleanDamageMin = monsterData.CleanDamageMin; + CleanDamageMax = monsterData.CleanDamageMax; + CleanHitRate = monsterData.CleanHitRate; + CleanMeleeDefence = monsterData.CleanMeleeDefence; + CleanRangeDefence = monsterData.CleanRangeDefence; + CleanMagicDefence = monsterData.CleanMagicDefence; + CleanDodge = monsterData.CleanDodge; + CleanHp = monsterData.CleanHp; + CleanMp = monsterData.CleanMp; + CloseDefence = monsterData.BaseCloseDefence; + Concentrate = monsterData.BaseConcentrate; + CriticalChance = monsterData.BaseCriticalChance; + CriticalRate = monsterData.BaseCriticalRate; + DamagedOnlyLastJajamaruSkill = monsterData.DamagedOnlyLastJajamaruSkill; + DamageMaximum = monsterData.BaseDamageMaximum; + DamageMinimum = monsterData.BaseDamageMinimum; + DarkResistance = monsterData.BaseDarkResistance; + DeathEffect = monsterData.DeathEffect; + MaxHp = monsterData.BaseMaxHp; + MaxMp = monsterData.BaseMaxMp; + DefenceDodge = monsterData.DefenceDodge; + DefenceUpgrade = monsterData.DefenceUpgrade; + DisappearAfterHitting = monsterData.DisappearAfterHitting; + DisappearAfterSeconds = monsterData.DisappearAfterSeconds; + DisappearAfterSecondsMana = monsterData.DisappearAfterSecondsMana; + DistanceDefence = monsterData.DistanceDefence; + DistanceDefenceDodge = monsterData.DistanceDefenceDodge; + Element = monsterData.BaseElement; + ElementRate = monsterData.BaseElementRate; + SuggestedFaction = monsterData.SuggestedFaction; + FireResistance = monsterData.BaseFireResistance; + GiveDamagePercentage = monsterData.GiveDamagePercentage; + GroupAttack = monsterData.GroupAttack; + HasMode = monsterData.HasMode; + HostilityType = monsterData.RawHostility; + IconId = monsterData.IconId; + IsPercent = monsterData.IsPercent; + JobXp = monsterData.JobXp; + Level = monsterData.BaseLevel; + LightResistance = monsterData.BaseLightResistance; + MagicDefence = monsterData.MagicDefence; + MagicMpFactor = monsterData.MagicMpFactor; + MeleeHpFactor = monsterData.MeleeHpFactor; + MinimumAttackRange = monsterData.MinimumAttackRange; + Id = monsterData.MonsterVNum; + Name = monsterData.Name; + NoticeRange = monsterData.NoticeRange; + OnDefenseOnlyOnce = monsterData.OnDefenseOnlyOnce; + PermanentEffect = monsterData.PermanentEffect; + Race = (byte)monsterData.MonsterRaceType; + RaceType = monsterData.MonsterRaceSubType; + RangeDodgeFactor = monsterData.RangeDodgeFactor; + RespawnTime = (int)(monsterData.BaseRespawnTime.TotalMilliseconds / 100); + SpawnMobOrColor = monsterData.SpawnMobOrColor; + Speed = monsterData.BaseSpeed; + SpriteSize = monsterData.SpriteSize; + TakeDamages = monsterData.TakeDamages; + VNumRequired = monsterData.VNumRequired; + WaterResistance = monsterData.BaseWaterResistance; + WeaponLevel = monsterData.WeaponLevel; + WinfoValue = monsterData.WinfoValue; + Xp = monsterData.Xp; + MaxTries = monsterData.MaxTries; + CollectionCooldown = monsterData.CollectionCooldown; + CollectionDanceTime = monsterData.CollectionDanceTime; + TeleportRemoveFromInventory = monsterData.TeleportRemoveFromInventory; + HasDash = monsterData.HasDash; + BCards = monsterData.BCards; + ModeBCards = monsterData.ModeBCards; + Drops = monsterData.Drops; + MonsterSkills = monsterData.MonsterSkills; + if (monsterData is NpcMonsterDto npcMonsterDto) + { + Skills = npcMonsterDto.Skills; + } + } + + public short BaseCloseDefence => CloseDefence; + public short BaseConcentrate => Concentrate; + public short BaseCriticalChance => CriticalChance; + public short BaseCriticalRate => CriticalRate; + public int BaseDamageMaximum => DamageMaximum; + public int BaseDamageMinimum => DamageMinimum; + public short BaseDarkResistance => DarkResistance; + public int BaseMaxHp => MaxHp; + public int BaseMaxMp => MaxMp; + public byte BaseElement => Element; + public short BaseElementRate => ElementRate; + public short BaseFireResistance => FireResistance; + public int RawHostility => HostilityType; + public byte BaseLevel => Level; + public short BaseLightResistance => LightResistance; + public int MonsterVNum => Id; + public MonsterRaceType MonsterRaceType => (MonsterRaceType)Race; + public byte MonsterRaceSubType => RaceType; + public byte BaseSpeed => Speed; + public short BaseWaterResistance => WaterResistance; + public TimeSpan BaseRespawnTime => TimeSpan.FromMilliseconds(RespawnTime * 100); + + public FactionType? SuggestedFaction { get; set; } + public IReadOnlyList Drops { get; set; } + public bool CanSeeInvisible { get; set; } + public IReadOnlyList BCards { get; set; } + public IReadOnlyList ModeBCards { get; set; } + public IReadOnlyList MonsterSkills { get; set; } = new List(); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/PlayerCommandEvent.cs b/srcs/WingsAPI.Game/PlayerCommandEvent.cs new file mode 100644 index 0000000..a284b81 --- /dev/null +++ b/srcs/WingsAPI.Game/PlayerCommandEvent.cs @@ -0,0 +1,8 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game; + +public class PlayerCommandEvent : PlayerEvent +{ + public string Command { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Portals/IPortalEntity.cs b/srcs/WingsAPI.Game/Portals/IPortalEntity.cs new file mode 100644 index 0000000..03639cf --- /dev/null +++ b/srcs/WingsAPI.Game/Portals/IPortalEntity.cs @@ -0,0 +1,21 @@ +using WingsEmu.Game.Maps; +using WingsEmu.Game.TimeSpaces.Enums; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game; + +public interface IPortalEntity : IEventTriggerContainer +{ + int Id { get; set; } + PortalType Type { get; set; } + bool IsDisabled { get; set; } + IMapInstance MapInstance { get; } + short PositionX { get; set; } + short PositionY { get; set; } + PortalMinimapOrientation MinimapOrientation { get; set; } + short? RaidType { get; set; } + short? MapNameId { get; set; } + short? DestinationX { get; set; } + short? DestinationY { get; set; } + IMapInstance? DestinationMapInstance { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Portals/IPortalFactory.cs b/srcs/WingsAPI.Game/Portals/IPortalFactory.cs new file mode 100644 index 0000000..4f44006 --- /dev/null +++ b/srcs/WingsAPI.Game/Portals/IPortalFactory.cs @@ -0,0 +1,17 @@ +using System; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Maps; +using WingsEmu.Game.TimeSpaces.Enums; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Portals; + +public interface IPortalFactory +{ + IPortalEntity CreatePortal(PortalType portalType, IMapInstance source, Position sourcePos); + IPortalEntity CreatePortal(PortalType portalType, IMapInstance source, Position sourcePos, Guid destMapInstanceId, Position destPos); + IPortalEntity CreatePortal(PortalType portalType, IMapInstance source, Position sourcePos, int mapDestId, Position destPos); + IPortalEntity CreatePortal(PortalType portalType, IMapInstance source, Position sourcePos, int mapDestId, Position destPos, short? raidType, short? mapNameId); + IPortalEntity CreatePortal(PortalType portalType, IMapInstance source, Position sourcePos, IMapInstance destination, Position destPos); + IPortalEntity CreatePortal(PortalType portalType, IMapInstance source, Position sourcePos, IMapInstance destination, Position destPos, PortalMinimapOrientation minimapOrientation); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Portals/ITimeSpacePortalEntity.cs b/srcs/WingsAPI.Game/Portals/ITimeSpacePortalEntity.cs new file mode 100644 index 0000000..3e1661b --- /dev/null +++ b/srcs/WingsAPI.Game/Portals/ITimeSpacePortalEntity.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using WingsEmu.Game.Helpers.Damages; + +namespace WingsEmu.Game.Portals; + +public interface ITimeSpacePortalEntity +{ + public long TimeSpaceId { get; } + public Position Position { get; } + public bool IsHero { get; } + public bool IsSpecial { get; } + public bool IsHidden { get; } + public byte MinLevel { get; } + public byte MaxLevel { get; } + public byte SeedsOfPowerRequired { get; } + public string Name { get; } + public string Description { get; } + + public List<(short, short)> DrawRewards { get; } + public List<(short, short)> SpecialRewards { get; } + public List<(short, short)> BonusRewards { get; } + + public long? GroupId { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Portals/ITimeSpacePortalFactory.cs b/srcs/WingsAPI.Game/Portals/ITimeSpacePortalFactory.cs new file mode 100644 index 0000000..15a4875 --- /dev/null +++ b/srcs/WingsAPI.Game/Portals/ITimeSpacePortalFactory.cs @@ -0,0 +1,10 @@ +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Helpers.Damages; + +namespace WingsEmu.Game.Portals; + +public interface ITimeSpacePortalFactory +{ + ITimeSpacePortalEntity CreateTimeSpacePortal(TimeSpaceFileConfiguration timeSpaceFileConfiguration, Position position); + ITimeSpacePortalEntity CreateTimeSpacePortal(TimeSpaceFileConfiguration timeSpaceFileConfiguration, Position position, long? groupId); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Portals/PortalPacketExtensions.cs b/srcs/WingsAPI.Game/Portals/PortalPacketExtensions.cs new file mode 100644 index 0000000..b27d00e --- /dev/null +++ b/srcs/WingsAPI.Game/Portals/PortalPacketExtensions.cs @@ -0,0 +1,17 @@ +using WingsEmu.Game.Maps; + +namespace WingsEmu.Game; + +public static class PortalPacketExtensions +{ + public static string GenerateGp(this IPortalEntity portal) + { + if (portal.DestinationMapInstance?.MapInstanceType == MapInstanceType.TimeSpaceInstance) + { + return + $"gp {portal.PositionX} {portal.PositionY} {portal.MapNameId ?? portal.DestinationMapInstance.MapId} {(sbyte)portal.Type} {(byte)portal.MinimapOrientation} {(portal.IsDisabled ? 1 : 0)}"; + } + + return $"gp {portal.PositionX} {portal.PositionY} {portal.MapNameId ?? portal.DestinationMapInstance?.MapId ?? 0} {(sbyte)portal.Type} {portal.Id} {(portal.IsDisabled ? 1 : 0)}"; + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Quests/BasicQuestContainer.cs b/srcs/WingsAPI.Game/Quests/BasicQuestContainer.cs new file mode 100644 index 0000000..43fb1b3 --- /dev/null +++ b/srcs/WingsAPI.Game/Quests/BasicQuestContainer.cs @@ -0,0 +1,167 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using WingsEmu.Core.Extensions; +using WingsEmu.Core.Generics; +using WingsEmu.DTOs.Quests; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Quests; + +public class BasicQuestContainer : IQuestContainer +{ + private readonly ConcurrentDictionary _activeQuests = new(); + private readonly ConcurrentDictionary _completedPeriodicQuests = new(); + private readonly ConcurrentDictionary _completedQuests = new(); + private readonly ConcurrentDictionary<(int, int), CompletedScriptsDto> _completedScripts = new(); + private readonly ConcurrentDictionary> _completedScriptsByType = new(); + private readonly ConcurrentDictionary> _questsByQuestType = new(); + private int _soundFlowersPendingToStart; + + public bool HasCompletedScriptByIndex(int scriptId, int scriptIndex) => _completedScripts.ContainsKey((scriptId, scriptIndex)); + public bool HasCompletedQuest(int questId) => _completedQuests.ContainsKey(questId); + + public IEnumerable GetCurrentQuests() => _activeQuests.Values; + + public IEnumerable GetCompletedQuests() => _completedQuests.Values; + + public IEnumerable GetCompletedPeriodicQuests() => _completedPeriodicQuests.Values.Cast().ToArray(); + + public IEnumerable GetCurrentQuestsByType(QuestType questType) + { + return _activeQuests.Values.Where(s => s.Quest.QuestType == questType).ToArray(); + } + + public IEnumerable GetCurrentQuestsByTypes(IReadOnlyCollection questTypes) + { + return _activeQuests.Values.Where(s => questTypes.Contains(s.Quest.QuestType)).ToArray(); + } + + public IEnumerable GetQuestsProgress() => _activeQuests.Values.Cast().ToList(); + + public CharacterQuest GetCurrentQuest(int questId) => _activeQuests.GetOrDefault(questId); + + public bool HasQuestWithQuestType(QuestType questType) => _questsByQuestType.ContainsKey(questType); + + public bool HasQuestWithId(int questId) => _activeQuests.ContainsKey(questId); + + public void AddActiveQuest(CharacterQuest quest) + { + if (_activeQuests.ContainsKey(quest.QuestId)) + { + return; + } + + _activeQuests.TryAdd(quest.QuestId, quest); + + if (!_questsByQuestType.TryGetValue(quest.Quest.QuestType, out ThreadSafeList characterQuests)) + { + characterQuests = new ThreadSafeList(); + _questsByQuestType[quest.Quest.QuestType] = characterQuests; + } + + characterQuests.Add(quest); + } + + public void RemoveActiveQuest(int questId) + { + if (!_activeQuests.Remove(questId, out CharacterQuest quest)) + { + return; + } + + if (!_questsByQuestType.TryGetValue(quest.Quest.QuestType, out ThreadSafeList characterQuests)) + { + return; + } + + characterQuests.Remove(quest); + } + + public void AddCompletedQuest(CharacterQuest quest) + { + if (_completedQuests.ContainsKey(quest.QuestId)) + { + return; + } + + _completedQuests.TryAdd(quest.QuestId, quest); + } + + public void RemoveCompletedQuest(int questId) => _completedQuests.TryRemove(questId, out _); + + public void RemoveCompletedScript(int scriptId, int scriptIndex) => _completedScripts.TryRemove((scriptId, scriptIndex), out _); + + public void RemoveAllCompletedScripts() => _completedScripts.Clear(); + + public void AddCompletedPeriodicQuest(CharacterQuest quest) + { + if (_completedPeriodicQuests.ContainsKey(quest.QuestId)) + { + return; + } + + _completedPeriodicQuests.TryAdd(quest.QuestId, quest); + } + + public void ClearCompletedPeriodicQuests() => _completedPeriodicQuests.Clear(); + + public IEnumerable GetCompletedScripts() => _completedScripts.Values.ToArray(); + + public IEnumerable GetCompletedScriptsByType(TutorialActionType scriptType) + { + if (!_completedScriptsByType.TryGetValue(scriptType, out ThreadSafeList list)) + { + return Array.Empty(); + } + + return list; + } + + public void SaveScript(int scriptId, int scriptIndex, TutorialActionType scriptType, DateTime savingDate) + { + var completedScript = new CompletedScriptsDto + { + ScriptId = scriptId, + ScriptIndex = scriptIndex, + CompletedDate = savingDate + }; + + _completedScripts.TryAdd((scriptId, scriptIndex), completedScript); + + if (!_completedScriptsByType.TryGetValue(scriptType, out ThreadSafeList list)) + { + list = new ThreadSafeList(); + _completedScriptsByType[scriptType] = list; + } + + list.Add(completedScript); + } + + public CompletedScriptsDto GetLastCompletedScript() + { + if (!_completedScripts.Any()) + { + return null; + } + + return _completedScripts.OrderByDescending(s => s.Key.Item1).ThenByDescending(s => s.Key.Item2).FirstOrDefault().Value; + } + + public CompletedScriptsDto GetLastCompletedScriptByType(TutorialActionType scriptType) + { + if (!_completedScriptsByType.ContainsKey(scriptType) || !_completedScriptsByType[scriptType].Any()) + { + return null; + } + + return _completedScriptsByType[scriptType].OrderByDescending(s => s.ScriptId).ThenByDescending(s => s.ScriptIndex).FirstOrDefault(); + } + + public void IncreasePendingSoundFlowerQuests() => _soundFlowersPendingToStart++; + + public void DecreasePendingSoundFlowerQuests() => _soundFlowersPendingToStart--; + + public int GetPendingSoundFlowerQuests() => _soundFlowersPendingToStart; +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Quests/CharacterQuest.cs b/srcs/WingsAPI.Game/Quests/CharacterQuest.cs new file mode 100644 index 0000000..1ccd33a --- /dev/null +++ b/srcs/WingsAPI.Game/Quests/CharacterQuest.cs @@ -0,0 +1,102 @@ +using System.Collections.Generic; +using WingsEmu.DTOs.Quests; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Quests; + +public class CharacterQuest : CharacterQuestDto +{ + private readonly IRandomGenerator _randomGenerator; + + public CharacterQuest(QuestDto quest, QuestSlotType slotType, IRandomGenerator randomGenerator) + { + Quest = quest; + _randomGenerator = randomGenerator; + SlotType = slotType; + QuestId = quest.Id; + ObjectiveAmount = new Dictionary(); + + InitializeObjectiveAmount(); + } + + public QuestDto Quest { get; } + + private void InitializeObjectiveAmount() + { + foreach (QuestObjectiveDto objective in Quest.Objectives) + { + ObjectiveAmount.TryAdd(objective.ObjectiveIndex, new CharacterQuestObjectiveDto + { + CurrentAmount = 0, + RequiredAmount = RandomizeObjectiveAmount(objective) + }); + } + } + + public void ResetQuestProgress() + { + foreach (KeyValuePair objective in ObjectiveAmount) + { + ObjectiveAmount[objective.Key].CurrentAmount = 0; + } + } + + private int RandomizeObjectiveAmount(QuestObjectiveDto objective) + { + switch (Quest.QuestType) + { + // Random values + case QuestType.KILL_MONSTER_BY_VNUM: // 1 + case QuestType.DROP_HARDCODED: // 2 + case QuestType.COMPLETE_TIMESPACE: // 7 + case QuestType.CRAFT_WITHOUT_KEEPING: // 8 + case QuestType.KILL_PLAYER_IN_REGION: // 27 + return objective.Data1 != -1 && objective.Data2 != -1 ? _randomGenerator.RandomNumber(objective.Data1, objective.Data2 + 1) : + objective.Data1 == -1 ? objective.Data2 : objective.Data1; + case QuestType.DELIVER_ITEM_TO_NPC: // 4 + case QuestType.CAPTURE_WITHOUT_KEEPING: // 5 + case QuestType.CAPTURE_AND_KEEP: // 6 + return objective.Data2 != -1 && objective.Data3 != -1 ? _randomGenerator.RandomNumber(objective.Data2, objective.Data3 + 1) : + objective.Data2 == -1 ? objective.Data3 : objective.Data2; + + // Fixed values + case QuestType.DROP_CHANCE: // 3 + case QuestType.DROP_IN_TIMESPACE: // 13 + case QuestType.GIVE_ITEM_TO_NPC: // 14 + case QuestType.DIALOG_WHILE_HAVING_ITEM: //16 + case QuestType.DROP_CHANCE_2: // 17 + case QuestType.COLLECT: // 20 + case QuestType.GIVE_ITEM_TO_NPC_2: // 24 + return objective.Data2; + + case QuestType.COMPLETE_TIMESPACE_WITH_ATLEAST_X_POINTS: // 11 + return objective.Data3; + + case QuestType.GIVE_NPC_GOLD: //18 + return objective.Data1 * 10000; + + // Handled differently + case QuestType.DIALOG: // 12 + case QuestType.DIALOG_WHILE_WEARING: // 15 + case QuestType.GO_TO_MAP: // 19 + case QuestType.USE_ITEM_ON_TARGET: // 21 + case QuestType.DIALOG_2: // 22 + case QuestType.NOTHING: // 23 + break; + + case QuestType.WIN_RAID_AND_TALK_TO_NPC: // 25 + return objective.Data1; + + + case QuestType.KILL_X_MOBS_SOUND_FLOWER: // 26 + return objective.Data0; + + // Not used + case QuestType.DIE_X_TIMES: // 9 + case QuestType.EARN_REPUTATION: // 10 + return 1; + } + + return 0; + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Quests/Configurations/SoundFlowerConfiguration.cs b/srcs/WingsAPI.Game/Quests/Configurations/SoundFlowerConfiguration.cs new file mode 100644 index 0000000..326016a --- /dev/null +++ b/srcs/WingsAPI.Game/Quests/Configurations/SoundFlowerConfiguration.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace WingsEmu.Game.Quests.Configurations; + +public class SoundFlowerConfiguration +{ + public HashSet SoundFlowerQuestVnums { get; set; } + public HashSet WildSoundFlowerQuestVnums { get; set; } + public HashSet PossibleBuffs { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Quests/Event/AddQuestEvent.cs b/srcs/WingsAPI.Game/Quests/Event/AddQuestEvent.cs new file mode 100644 index 0000000..184d0a8 --- /dev/null +++ b/srcs/WingsAPI.Game/Quests/Event/AddQuestEvent.cs @@ -0,0 +1,16 @@ +using WingsEmu.DTOs.Quests; +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Quests.Event; + +public class AddQuestEvent : PlayerEvent +{ + public AddQuestEvent(int questId, QuestSlotType questSlotType) + { + QuestId = questId; + QuestSlotType = questSlotType; + } + + public int QuestId { get; } + public QuestSlotType QuestSlotType { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Quests/Event/AddSoundFlowerQuestEvent.cs b/srcs/WingsAPI.Game/Quests/Event/AddSoundFlowerQuestEvent.cs new file mode 100644 index 0000000..25cc6d0 --- /dev/null +++ b/srcs/WingsAPI.Game/Quests/Event/AddSoundFlowerQuestEvent.cs @@ -0,0 +1,9 @@ +using WingsEmu.Game._enum; +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Quests.Event; + +public class AddSoundFlowerQuestEvent : PlayerEvent +{ + public SoundFlowerType SoundFlowerType { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Quests/Event/QuestAbandonedEvent.cs b/srcs/WingsAPI.Game/Quests/Event/QuestAbandonedEvent.cs new file mode 100644 index 0000000..6e32bc6 --- /dev/null +++ b/srcs/WingsAPI.Game/Quests/Event/QuestAbandonedEvent.cs @@ -0,0 +1,10 @@ +using WingsEmu.DTOs.Quests; +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Quests.Event; + +public class QuestAbandonedEvent : PlayerEvent +{ + public int QuestId { get; init; } + public QuestSlotType QuestSlotType { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Quests/Event/QuestAddedEvent.cs b/srcs/WingsAPI.Game/Quests/Event/QuestAddedEvent.cs new file mode 100644 index 0000000..a0f63c6 --- /dev/null +++ b/srcs/WingsAPI.Game/Quests/Event/QuestAddedEvent.cs @@ -0,0 +1,10 @@ +using WingsEmu.DTOs.Quests; +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Quests.Event; + +public class QuestAddedEvent : PlayerEvent +{ + public int QuestId { get; init; } + public QuestSlotType QuestSlotType { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Quests/Event/QuestCompletedEvent.cs b/srcs/WingsAPI.Game/Quests/Event/QuestCompletedEvent.cs new file mode 100644 index 0000000..bc89761 --- /dev/null +++ b/srcs/WingsAPI.Game/Quests/Event/QuestCompletedEvent.cs @@ -0,0 +1,21 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Quests.Event; + +public class QuestCompletedEvent : PlayerEvent +{ + public QuestCompletedEvent(CharacterQuest characterQuest, bool claimReward = false, bool refreshProgress = true, bool giveNextQuest = true, bool ignoreNotCompletedQuest = false) + { + CharacterQuest = characterQuest; + ClaimReward = claimReward; + RefreshProgress = refreshProgress; + GiveNextQuest = giveNextQuest; + IgnoreNotCompletedQuest = ignoreNotCompletedQuest; + } + + public CharacterQuest CharacterQuest { get; } + public bool ClaimReward { get; } + public bool GiveNextQuest { get; } + public bool RefreshProgress { get; } + public bool IgnoreNotCompletedQuest { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Quests/Event/QuestCompletedLogEvent.cs b/srcs/WingsAPI.Game/Quests/Event/QuestCompletedLogEvent.cs new file mode 100644 index 0000000..502680e --- /dev/null +++ b/srcs/WingsAPI.Game/Quests/Event/QuestCompletedLogEvent.cs @@ -0,0 +1,10 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Game.Helpers; + +namespace WingsEmu.Game.Quests.Event; + +public class QuestCompletedLogEvent : PlayerEvent +{ + public CharacterQuest CharacterQuest { get; init; } + public Location Location { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Quests/Event/QuestDailyRefreshEvent.cs b/srcs/WingsAPI.Game/Quests/Event/QuestDailyRefreshEvent.cs new file mode 100644 index 0000000..a7518ba --- /dev/null +++ b/srcs/WingsAPI.Game/Quests/Event/QuestDailyRefreshEvent.cs @@ -0,0 +1,8 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Quests.Event; + +public class QuestDailyRefreshEvent : PlayerEvent +{ + public bool Force { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Quests/Event/QuestHarvestEvent.cs b/srcs/WingsAPI.Game/Quests/Event/QuestHarvestEvent.cs new file mode 100644 index 0000000..ba6c875 --- /dev/null +++ b/srcs/WingsAPI.Game/Quests/Event/QuestHarvestEvent.cs @@ -0,0 +1,9 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Quests.Event; + +public class QuestHarvestEvent : PlayerEvent +{ + public int ItemVnum { get; init; } + public int NpcVnum { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Quests/Event/QuestItemPickUpEvent.cs b/srcs/WingsAPI.Game/Quests/Event/QuestItemPickUpEvent.cs new file mode 100644 index 0000000..9264f3b --- /dev/null +++ b/srcs/WingsAPI.Game/Quests/Event/QuestItemPickUpEvent.cs @@ -0,0 +1,10 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Quests.Event; + +public class QuestItemPickUpEvent : PlayerEvent +{ + public int ItemVnum { get; init; } + public int Amount { get; init; } + public bool SendMessage { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Quests/Event/QuestMonsterDeathEvent.cs b/srcs/WingsAPI.Game/Quests/Event/QuestMonsterDeathEvent.cs new file mode 100644 index 0000000..3427ac9 --- /dev/null +++ b/srcs/WingsAPI.Game/Quests/Event/QuestMonsterDeathEvent.cs @@ -0,0 +1,9 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Game.Entities; + +namespace WingsEmu.Game.Quests.Event; + +public class QuestMonsterDeathEvent : PlayerEvent +{ + public IMonsterEntity MonsterEntity { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Quests/Event/QuestNpcTalkEvent.cs b/srcs/WingsAPI.Game/Quests/Event/QuestNpcTalkEvent.cs new file mode 100644 index 0000000..193d871 --- /dev/null +++ b/srcs/WingsAPI.Game/Quests/Event/QuestNpcTalkEvent.cs @@ -0,0 +1,18 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Game.Entities; + +namespace WingsEmu.Game.Quests.Event; + +public class QuestNpcTalkEvent : PlayerEvent +{ + public QuestNpcTalkEvent(CharacterQuest characterQuest, INpcEntity npcEntity, bool isByBlueAlertNrun = false) + { + CharacterQuest = characterQuest; + NpcEntity = npcEntity; + IsByBlueAlertNrun = isByBlueAlertNrun; + } + + public CharacterQuest CharacterQuest { get; } + public INpcEntity NpcEntity { get; } + public bool IsByBlueAlertNrun { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Quests/Event/QuestObjectiveUpdatedEvent.cs b/srcs/WingsAPI.Game/Quests/Event/QuestObjectiveUpdatedEvent.cs new file mode 100644 index 0000000..caac2b0 --- /dev/null +++ b/srcs/WingsAPI.Game/Quests/Event/QuestObjectiveUpdatedEvent.cs @@ -0,0 +1,8 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Quests.Event; + +public class QuestObjectiveUpdatedEvent : PlayerEvent +{ + public CharacterQuest CharacterQuest { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Quests/Event/QuestRemoveEvent.cs b/srcs/WingsAPI.Game/Quests/Event/QuestRemoveEvent.cs new file mode 100644 index 0000000..7615cd3 --- /dev/null +++ b/srcs/WingsAPI.Game/Quests/Event/QuestRemoveEvent.cs @@ -0,0 +1,15 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Quests.Event; + +public class QuestRemoveEvent : PlayerEvent +{ + public QuestRemoveEvent(CharacterQuest characterQuest, bool isCompleted) + { + CharacterQuest = characterQuest; + IsCompleted = isCompleted; + } + + public CharacterQuest CharacterQuest { get; } + public bool IsCompleted { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Quests/Event/QuestRewardEvent.cs b/srcs/WingsAPI.Game/Quests/Event/QuestRewardEvent.cs new file mode 100644 index 0000000..0e470b4 --- /dev/null +++ b/srcs/WingsAPI.Game/Quests/Event/QuestRewardEvent.cs @@ -0,0 +1,9 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Quests.Event; + +public class QuestRewardEvent : PlayerEvent +{ + public int QuestId { get; init; } + public bool ClaimReward { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Quests/Event/RunScriptEvent.cs b/srcs/WingsAPI.Game/Quests/Event/RunScriptEvent.cs new file mode 100644 index 0000000..ae23b66 --- /dev/null +++ b/srcs/WingsAPI.Game/Quests/Event/RunScriptEvent.cs @@ -0,0 +1,8 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Quests.Event; + +public class RunScriptEvent : PlayerEvent +{ + public int RunId { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Quests/IQuestContainer.cs b/srcs/WingsAPI.Game/Quests/IQuestContainer.cs new file mode 100644 index 0000000..7d3f510 --- /dev/null +++ b/srcs/WingsAPI.Game/Quests/IQuestContainer.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using WingsEmu.DTOs.Quests; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Quests; + +public interface IQuestContainer +{ + public bool HasQuestWithQuestType(QuestType questType); + public bool HasQuestWithId(int questId); + public bool HasCompletedScriptByIndex(int scriptId, int scriptIndex); + public bool HasCompletedQuest(int questId); + public IEnumerable GetCurrentQuests(); + public IEnumerable GetCompletedQuests(); + public IEnumerable GetCompletedPeriodicQuests(); + public IEnumerable GetCurrentQuestsByType(QuestType questType); + public IEnumerable GetCurrentQuestsByTypes(IReadOnlyCollection questTypes); + public IEnumerable GetQuestsProgress(); + public CharacterQuest GetCurrentQuest(int questId); + public void AddActiveQuest(CharacterQuest quest); + public void RemoveActiveQuest(int questId); + public void AddCompletedQuest(CharacterQuest quest); + public void RemoveCompletedQuest(int questId); + public void RemoveCompletedScript(int scriptId, int scriptIndex); + public void RemoveAllCompletedScripts(); + public void AddCompletedPeriodicQuest(CharacterQuest quest); + public void ClearCompletedPeriodicQuests(); + + public IEnumerable GetCompletedScripts(); + public IEnumerable GetCompletedScriptsByType(TutorialActionType scriptType); + public void SaveScript(int scriptId, int scriptIndex, TutorialActionType scriptType, DateTime savingDate); + public CompletedScriptsDto GetLastCompletedScript(); + public CompletedScriptsDto GetLastCompletedScriptByType(TutorialActionType scriptType); + + + public void IncreasePendingSoundFlowerQuests(); + public void DecreasePendingSoundFlowerQuests(); + public int GetPendingSoundFlowerQuests(); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Quests/IQuestFactory.cs b/srcs/WingsAPI.Game/Quests/IQuestFactory.cs new file mode 100644 index 0000000..218a3d8 --- /dev/null +++ b/srcs/WingsAPI.Game/Quests/IQuestFactory.cs @@ -0,0 +1,8 @@ +using WingsEmu.DTOs.Quests; + +namespace WingsEmu.Game.Quests; + +public interface IQuestFactory +{ + CharacterQuest NewQuest(long characterId, int questId, QuestSlotType questSlotType); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Quests/IQuestManager.cs b/srcs/WingsAPI.Game/Quests/IQuestManager.cs new file mode 100644 index 0000000..9f71430 --- /dev/null +++ b/srcs/WingsAPI.Game/Quests/IQuestManager.cs @@ -0,0 +1,33 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using WingsEmu.DTOs.Quests; + +namespace WingsEmu.Game.Quests; + +public interface IQuestManager +{ + Task InitializeAsync(); + QuestDto GetQuestById(int questId); + IReadOnlyCollection GetScriptsTutorialByType(TutorialActionType type); + IReadOnlyCollection GetScriptsTutorialByScriptId(int scriptId); + IReadOnlyCollection GetScriptsTutorial(); + IReadOnlyCollection GetScriptsTutorialUntilIndex(int scriptId, int scriptIndex); + IReadOnlyCollection GetQuestlines(int questId); + TutorialDto GetQuestPayScriptByQuestId(int questId); + TutorialDto GetScriptTutorialById(int scriptId); + TutorialDto GetScriptTutorialByIndex(int scriptId, int index); + TutorialDto GetFirstScriptFromIdByType(int scriptId, TutorialActionType type); + QuestNpcDto GetQuestNpcByScriptId(int scriptId); + IReadOnlyCollection GetNpcBlueAlertQuests(); + QuestNpcDto GetNpcBlueAlertQuestByQuestId(int questId); + Task CanRefreshDailyQuests(long characterId); + Task TryTakeDailyQuest(Guid masterAccId, int questPackId); + Task TryTakeDailyQuest(long characterId, int questPackId); + List GetQuestByName(string name); + bool IsNpcBlueAlertQuest(int questId); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Quests/IRunScriptHandler.cs b/srcs/WingsAPI.Game/Quests/IRunScriptHandler.cs new file mode 100644 index 0000000..6319664 --- /dev/null +++ b/srcs/WingsAPI.Game/Quests/IRunScriptHandler.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Quests.Event; + +namespace WingsEmu.Game.Quests; + +public interface IRunScriptHandler +{ + public int[] RunIds { get; } + + Task ExecuteAsync(IClientSession session, RunScriptEvent e); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Quests/IRunScriptHandlerContainer.cs b/srcs/WingsAPI.Game/Quests/IRunScriptHandlerContainer.cs new file mode 100644 index 0000000..4f50300 --- /dev/null +++ b/srcs/WingsAPI.Game/Quests/IRunScriptHandlerContainer.cs @@ -0,0 +1,16 @@ +using System.Threading.Tasks; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Quests.Event; + +namespace WingsEmu.Game.Quests; + +public interface IRunScriptHandlerContainer +{ + Task RegisterAsync(IRunScriptHandler handler); + + Task UnregisterAsync(long runId); + + void Handle(IClientSession player, RunScriptEvent args); + + Task HandleAsync(IClientSession player, RunScriptEvent args); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Quicklist/IQuicklistComponent.cs b/srcs/WingsAPI.Game/Quicklist/IQuicklistComponent.cs new file mode 100644 index 0000000..96e90c7 --- /dev/null +++ b/srcs/WingsAPI.Game/Quicklist/IQuicklistComponent.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using WingsEmu.DTOs.Quicklist; + +namespace WingsEmu.Game.Quicklist; + +public interface IQuicklistComponent +{ + List GetQuicklist(); + IReadOnlyList GetQuicklistByTab(short tab, int morphId); + CharacterQuicklistEntryDto GetQuicklistByTabSlotAndMorph(short tab, short slot, int morphId); + + void AddQuicklist(CharacterQuicklistEntryDto quicklist); + void RemoveQuicklist(short tab, short slot, int morphId); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Quicklist/QuicklistAddEvent.cs b/srcs/WingsAPI.Game/Quicklist/QuicklistAddEvent.cs new file mode 100644 index 0000000..c868273 --- /dev/null +++ b/srcs/WingsAPI.Game/Quicklist/QuicklistAddEvent.cs @@ -0,0 +1,17 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsEmu.DTOs.Quicklist; +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Quicklist; + +public class QuicklistAddEvent : PlayerEvent +{ + public short Tab { get; init; } + public short Slot { get; init; } + public QuicklistType Type { get; init; } + public short DestinationType { get; init; } + public short DestinationSlotOrVnum { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Quicklist/QuicklistComponent.cs b/srcs/WingsAPI.Game/Quicklist/QuicklistComponent.cs new file mode 100644 index 0000000..ba07d52 --- /dev/null +++ b/srcs/WingsAPI.Game/Quicklist/QuicklistComponent.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using WingsEmu.Core.Extensions; +using WingsEmu.DTOs.Quicklist; + +namespace WingsEmu.Game.Quicklist; + +public class QuicklistComponent : IQuicklistComponent +{ + private const int QUICKLIST_SLOTS = 30; + private readonly List _quicklist = new(); + private readonly Dictionary<(short, int), CharacterQuicklistEntryDto[]> _quicklistByTabAndMorphId = new(); + + + public List GetQuicklist() => _quicklist; + + public IReadOnlyList GetQuicklistByTab(short tab, int morphId) => _quicklistByTabAndMorphId.GetOrDefault((tab, morphId)); + + public CharacterQuicklistEntryDto GetQuicklistByTabSlotAndMorph(short tab, short slot, int morphId) + { + CharacterQuicklistEntryDto[] entries = _quicklistByTabAndMorphId.GetOrDefault((tab, morphId)); + return entries?[slot]; + } + + public void AddQuicklist(CharacterQuicklistEntryDto quicklist) + { + if (!_quicklistByTabAndMorphId.TryGetValue((quicklist.QuicklistTab, quicklist.Morph), out CharacterQuicklistEntryDto[] dtos)) + { + _quicklistByTabAndMorphId[(quicklist.QuicklistTab, quicklist.Morph)] = dtos = new CharacterQuicklistEntryDto[QUICKLIST_SLOTS]; + } + + dtos[quicklist.QuicklistSlot] = quicklist; + _quicklist.Add(quicklist); + } + + public void RemoveQuicklist(short tab, short slot, int morphId) + { + CharacterQuicklistEntryDto[] quicklist = _quicklistByTabAndMorphId.GetOrDefault((tab, morphId)); + quicklist[slot] = null; + _quicklist.RemoveAll(s => s.QuicklistTab == tab && s.QuicklistSlot == slot && s.Morph == morphId); + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Quicklist/QuicklistRemoveEvent.cs b/srcs/WingsAPI.Game/Quicklist/QuicklistRemoveEvent.cs new file mode 100644 index 0000000..307025d --- /dev/null +++ b/srcs/WingsAPI.Game/Quicklist/QuicklistRemoveEvent.cs @@ -0,0 +1,9 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Quicklist; + +public class QuicklistRemoveEvent : PlayerEvent +{ + public short Tab { get; init; } + public short Slot { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Quicklist/QuicklistSwapEvent.cs b/srcs/WingsAPI.Game/Quicklist/QuicklistSwapEvent.cs new file mode 100644 index 0000000..995f9a7 --- /dev/null +++ b/srcs/WingsAPI.Game/Quicklist/QuicklistSwapEvent.cs @@ -0,0 +1,10 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Quicklist; + +public class QuicklistSwapEvent : PlayerEvent +{ + public short Tab { get; init; } + public short FromSlot { get; init; } + public short ToSlot { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Raids/ButtonMapItem.cs b/srcs/WingsAPI.Game/Raids/ButtonMapItem.cs new file mode 100644 index 0000000..32fa4a9 --- /dev/null +++ b/srcs/WingsAPI.Game/Raids/ButtonMapItem.cs @@ -0,0 +1,66 @@ +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Items; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Triggers; + +namespace WingsEmu.Game.Raids; + +public sealed class ButtonMapItem : MapItem, IEventTriggerContainer +{ + private readonly IEventTriggerContainer _eventTriggerContainer; + + /// + /// + /// + /// + /// + /// + /// + /// Initial state of the button, when switching to this state it won't trigger the + /// 'switchedToNonDefaultState' + /// + /// Map instance in which the button has been added + /// + /// + public ButtonMapItem(short x, short y, int deactivatedStateVNum, int activatedStateVNum, bool activatedState, IMapInstance mapInstance, IAsyncEventPipeline asyncEventPipeline, + bool? onlyOnce = null, bool isObjective = false, bool isQuest = false, int? customDanceDuration = null) : base(x, y, isQuest, mapInstance) + { + DeactivatedStateVNum = deactivatedStateVNum; + ActivatedStateVNum = activatedStateVNum; + DefaultState = activatedState; + State = DefaultState; + IsObjective = isObjective; + ItemVNum = State ? ActivatedStateVNum : DeactivatedStateVNum; + _eventTriggerContainer = new EventTriggerContainer(asyncEventPipeline); + CustomDanceDuration = customDanceDuration; + + Amount = 1; + CreatedDate = null; + CanBeMovedOnlyOnce = onlyOnce; + } + + public bool AlreadyMoved { get; set; } + + public bool IsObjective { get; } + + public int DeactivatedStateVNum { get; } + + public int ActivatedStateVNum { get; } + + public bool DefaultState { get; } + + public bool State { get; set; } + + public bool? CanBeMovedOnlyOnce { get; set; } + + public bool NonDefaultState => State != DefaultState; + + public int? CustomDanceDuration { get; set; } + + public void AddEvent(string key, IAsyncEvent notification, bool removedOnTrigger = false) => _eventTriggerContainer.AddEvent(key, notification, removedOnTrigger); + + public Task TriggerEvents(string key) => _eventTriggerContainer.TriggerEvents(key); + + public override GameItemInstance GetItemInstance() => ItemInstance; +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Raids/Configuration/RaidConfiguration.cs b/srcs/WingsAPI.Game/Raids/Configuration/RaidConfiguration.cs new file mode 100644 index 0000000..531f5f2 --- /dev/null +++ b/srcs/WingsAPI.Game/Raids/Configuration/RaidConfiguration.cs @@ -0,0 +1,14 @@ +using System; + +namespace WingsEmu.Game.Raids.Configuration; + +public class RaidConfiguration +{ + public TimeSpan RaidMapDestroyDelay { get; set; } = TimeSpan.FromSeconds(30); + + public TimeSpan RaidDeathRevivalDelay { get; set; } = TimeSpan.FromSeconds(20); + + public TimeSpan RaidSlowMoDelay { get; set; } = TimeSpan.FromSeconds(7); + + public int LivesPerCharacter { get; set; } = 3; +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Raids/DropChance.cs b/srcs/WingsAPI.Game/Raids/DropChance.cs new file mode 100644 index 0000000..add3edf --- /dev/null +++ b/srcs/WingsAPI.Game/Raids/DropChance.cs @@ -0,0 +1,15 @@ +namespace WingsEmu.Game.Raids; + +public class DropChance +{ + public DropChance(int chance, int itemVnum, int amount) + { + Chance = chance; + ItemVnum = itemVnum; + Amount = amount; + } + + public int Chance { get; } + public int ItemVnum { get; } + public int Amount { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Raids/Enum/RaidFinishType.cs b/srcs/WingsAPI.Game/Raids/Enum/RaidFinishType.cs new file mode 100644 index 0000000..0c37872 --- /dev/null +++ b/srcs/WingsAPI.Game/Raids/Enum/RaidFinishType.cs @@ -0,0 +1,9 @@ +namespace WingsEmu.Game.Raids.Enum; + +public enum RaidFinishType +{ + Disbanded, + MissionClear, + TimeIsUp, + NoLivesLeft +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Raids/Enum/RaidJoinType.cs b/srcs/WingsAPI.Game/Raids/Enum/RaidJoinType.cs new file mode 100644 index 0000000..603f551 --- /dev/null +++ b/srcs/WingsAPI.Game/Raids/Enum/RaidJoinType.cs @@ -0,0 +1,7 @@ +namespace WingsEmu.Game.Raids.Enum; + +public enum RaidJoinType +{ + RAID_LIST, + INVITATION +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Raids/Events/RaidAbandonedEvent.cs b/srcs/WingsAPI.Game/Raids/Events/RaidAbandonedEvent.cs new file mode 100644 index 0000000..825d751 --- /dev/null +++ b/srcs/WingsAPI.Game/Raids/Events/RaidAbandonedEvent.cs @@ -0,0 +1,9 @@ +using System; +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Raids.Events; + +public class RaidAbandonedEvent : PlayerEvent +{ + public Guid RaidId { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Raids/Events/RaidCreatedEvent.cs b/srcs/WingsAPI.Game/Raids/Events/RaidCreatedEvent.cs new file mode 100644 index 0000000..85f2774 --- /dev/null +++ b/srcs/WingsAPI.Game/Raids/Events/RaidCreatedEvent.cs @@ -0,0 +1,7 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Raids.Events; + +public class RaidCreatedEvent : PlayerEvent +{ +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Raids/Events/RaidDiedEvent.cs b/srcs/WingsAPI.Game/Raids/Events/RaidDiedEvent.cs new file mode 100644 index 0000000..38532d8 --- /dev/null +++ b/srcs/WingsAPI.Game/Raids/Events/RaidDiedEvent.cs @@ -0,0 +1,7 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Raids.Events; + +public class RaidDiedEvent : PlayerEvent +{ +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Raids/Events/RaidGiveRewardsEvent.cs b/srcs/WingsAPI.Game/Raids/Events/RaidGiveRewardsEvent.cs new file mode 100644 index 0000000..48cd00a --- /dev/null +++ b/srcs/WingsAPI.Game/Raids/Events/RaidGiveRewardsEvent.cs @@ -0,0 +1,18 @@ +using PhoenixLib.Events; +using WingsEmu.Game.Entities; + +namespace WingsEmu.Game.Raids.Events; + +public class RaidGiveRewardsEvent : IAsyncEvent +{ + public RaidGiveRewardsEvent(RaidParty raidParty, IMonsterEntity mapBoss, RaidReward raidReward) + { + RaidParty = raidParty; + MapBoss = mapBoss; + RaidReward = raidReward; + } + + public RaidParty RaidParty { get; } + public IMonsterEntity MapBoss { get; } + public RaidReward RaidReward { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Raids/Events/RaidInstanceDestroyEvent.cs b/srcs/WingsAPI.Game/Raids/Events/RaidInstanceDestroyEvent.cs new file mode 100644 index 0000000..d61019d --- /dev/null +++ b/srcs/WingsAPI.Game/Raids/Events/RaidInstanceDestroyEvent.cs @@ -0,0 +1,10 @@ +using PhoenixLib.Events; + +namespace WingsEmu.Game.Raids.Events; + +public class RaidInstanceDestroyEvent : IAsyncEvent +{ + public RaidInstanceDestroyEvent(RaidParty raidParty) => RaidParty = raidParty; + + public RaidParty RaidParty { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Raids/Events/RaidInstanceFinishEvent.cs b/srcs/WingsAPI.Game/Raids/Events/RaidInstanceFinishEvent.cs new file mode 100644 index 0000000..d0d4209 --- /dev/null +++ b/srcs/WingsAPI.Game/Raids/Events/RaidInstanceFinishEvent.cs @@ -0,0 +1,17 @@ +using PhoenixLib.Events; +using WingsEmu.Game.Raids.Enum; + +namespace WingsEmu.Game.Raids.Events; + +public class RaidInstanceFinishEvent : IAsyncEvent +{ + public RaidInstanceFinishEvent(RaidParty raidParty, RaidFinishType raidFinishType) + { + RaidFinishType = raidFinishType; + RaidParty = raidParty; + } + + public RaidParty RaidParty { get; } + + public RaidFinishType RaidFinishType { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Raids/Events/RaidInstanceLivesIncDecEvent.cs b/srcs/WingsAPI.Game/Raids/Events/RaidInstanceLivesIncDecEvent.cs new file mode 100644 index 0000000..6f29839 --- /dev/null +++ b/srcs/WingsAPI.Game/Raids/Events/RaidInstanceLivesIncDecEvent.cs @@ -0,0 +1,10 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Raids.Events; + +public class RaidInstanceLivesIncDecEvent : PlayerEvent +{ + public RaidInstanceLivesIncDecEvent(short amount) => Amount = amount; + + public short Amount { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Raids/Events/RaidInstanceRefreshInfoEvent.cs b/srcs/WingsAPI.Game/Raids/Events/RaidInstanceRefreshInfoEvent.cs new file mode 100644 index 0000000..0365a4f --- /dev/null +++ b/srcs/WingsAPI.Game/Raids/Events/RaidInstanceRefreshInfoEvent.cs @@ -0,0 +1,10 @@ +using PhoenixLib.Events; + +namespace WingsEmu.Game.Raids.Events; + +public class RaidInstanceRefreshInfoEvent : IAsyncEvent +{ + public RaidInstanceRefreshInfoEvent(RaidParty raidParty) => RaidParty = raidParty; + + public RaidParty RaidParty { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Raids/Events/RaidInstanceStartEvent.cs b/srcs/WingsAPI.Game/Raids/Events/RaidInstanceStartEvent.cs new file mode 100644 index 0000000..7e2da6b --- /dev/null +++ b/srcs/WingsAPI.Game/Raids/Events/RaidInstanceStartEvent.cs @@ -0,0 +1,12 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Raids.Events; + +public class RaidInstanceStartEvent : PlayerEvent +{ + public bool ForceTeleport { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Raids/Events/RaidInvitedEvent.cs b/srcs/WingsAPI.Game/Raids/Events/RaidInvitedEvent.cs new file mode 100644 index 0000000..cdb2891 --- /dev/null +++ b/srcs/WingsAPI.Game/Raids/Events/RaidInvitedEvent.cs @@ -0,0 +1,8 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Raids.Events; + +public class RaidInvitedEvent : PlayerEvent +{ + public long TargetId { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Raids/Events/RaidJoinedEvent.cs b/srcs/WingsAPI.Game/Raids/Events/RaidJoinedEvent.cs new file mode 100644 index 0000000..554304f --- /dev/null +++ b/srcs/WingsAPI.Game/Raids/Events/RaidJoinedEvent.cs @@ -0,0 +1,9 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Game.Raids.Enum; + +namespace WingsEmu.Game.Raids.Events; + +public class RaidJoinedEvent : PlayerEvent +{ + public RaidJoinType JoinType { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Raids/Events/RaidLeftEvent.cs b/srcs/WingsAPI.Game/Raids/Events/RaidLeftEvent.cs new file mode 100644 index 0000000..e01dae5 --- /dev/null +++ b/srcs/WingsAPI.Game/Raids/Events/RaidLeftEvent.cs @@ -0,0 +1,9 @@ +using System; +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Raids.Events; + +public class RaidLeftEvent : PlayerEvent +{ + public Guid RaidId { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Raids/Events/RaidListJoinEvent.cs b/srcs/WingsAPI.Game/Raids/Events/RaidListJoinEvent.cs new file mode 100644 index 0000000..c65dd64 --- /dev/null +++ b/srcs/WingsAPI.Game/Raids/Events/RaidListJoinEvent.cs @@ -0,0 +1,10 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Raids.Events; + +public class RaidListJoinEvent : PlayerEvent +{ + public RaidListJoinEvent(string nickname) => Nickname = nickname; + + public string Nickname { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Raids/Events/RaidListOpenEvent.cs b/srcs/WingsAPI.Game/Raids/Events/RaidListOpenEvent.cs new file mode 100644 index 0000000..9cc2052 --- /dev/null +++ b/srcs/WingsAPI.Game/Raids/Events/RaidListOpenEvent.cs @@ -0,0 +1,7 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Raids.Events; + +public class RaidListOpenEvent : PlayerEvent +{ +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Raids/Events/RaidListRegisterEvent.cs b/srcs/WingsAPI.Game/Raids/Events/RaidListRegisterEvent.cs new file mode 100644 index 0000000..022382a --- /dev/null +++ b/srcs/WingsAPI.Game/Raids/Events/RaidListRegisterEvent.cs @@ -0,0 +1,7 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Raids.Events; + +public class RaidListRegisterEvent : PlayerEvent +{ +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Raids/Events/RaidListUnregisterEvent.cs b/srcs/WingsAPI.Game/Raids/Events/RaidListUnregisterEvent.cs new file mode 100644 index 0000000..bb0dd8c --- /dev/null +++ b/srcs/WingsAPI.Game/Raids/Events/RaidListUnregisterEvent.cs @@ -0,0 +1,7 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Raids.Events; + +public class RaidListUnregisterEvent : PlayerEvent +{ +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Raids/Events/RaidLostEvent.cs b/srcs/WingsAPI.Game/Raids/Events/RaidLostEvent.cs new file mode 100644 index 0000000..6d08094 --- /dev/null +++ b/srcs/WingsAPI.Game/Raids/Events/RaidLostEvent.cs @@ -0,0 +1,7 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Raids.Events; + +public class RaidLostEvent : PlayerEvent +{ +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Raids/Events/RaidMonsterThrowEvent.cs b/srcs/WingsAPI.Game/Raids/Events/RaidMonsterThrowEvent.cs new file mode 100644 index 0000000..9340af4 --- /dev/null +++ b/srcs/WingsAPI.Game/Raids/Events/RaidMonsterThrowEvent.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using PhoenixLib.Events; +using WingsEmu.Core; +using WingsEmu.Game.Entities; + +namespace WingsEmu.Game.Raids.Events; + +public class RaidMonsterThrowEvent : IAsyncEvent +{ + public RaidMonsterThrowEvent(IMonsterEntity monsterEntity, List drops, byte itemDropsAmount, Range goldDropRange, byte goldDropsAmount) + { + MonsterEntity = monsterEntity; + Drops = drops; + ItemDropsAmount = itemDropsAmount; + GoldDropRange = goldDropRange; + GoldDropsAmount = goldDropsAmount; + } + + public IMonsterEntity MonsterEntity { get; } + + public List Drops { get; } + + public byte ItemDropsAmount { get; } + + public Range GoldDropRange { get; } + + public byte GoldDropsAmount { get; } +} + +public class Drop +{ + public int ItemVNum { get; set; } + + public int Amount { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Raids/Events/RaidObjectiveIncreaseEvent.cs b/srcs/WingsAPI.Game/Raids/Events/RaidObjectiveIncreaseEvent.cs new file mode 100644 index 0000000..f70b7a1 --- /dev/null +++ b/srcs/WingsAPI.Game/Raids/Events/RaidObjectiveIncreaseEvent.cs @@ -0,0 +1,22 @@ +using PhoenixLib.Events; + +namespace WingsEmu.Game.Raids.Events; + +public class RaidObjectiveIncreaseEvent : IAsyncEvent +{ + public RaidObjectiveIncreaseEvent(RaidTargetType raidTargetType, RaidSubInstance raidSubInstance) + { + RaidTargetType = raidTargetType; + RaidSubInstance = raidSubInstance; + } + + public RaidTargetType RaidTargetType { get; } + + public RaidSubInstance RaidSubInstance { get; } +} + +public enum RaidTargetType +{ + Monster, + Button +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Raids/Events/RaidPartyCreateEvent.cs b/srcs/WingsAPI.Game/Raids/Events/RaidPartyCreateEvent.cs new file mode 100644 index 0000000..338e9d6 --- /dev/null +++ b/srcs/WingsAPI.Game/Raids/Events/RaidPartyCreateEvent.cs @@ -0,0 +1,20 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsEmu.Game._packetHandling; +using WingsEmu.Game.Inventory; + +namespace WingsEmu.Game.Raids.Events; + +public class RaidPartyCreateEvent : PlayerEvent +{ + public RaidPartyCreateEvent(byte raidType, InventoryItem itemToRemove) + { + RaidType = raidType; + ItemToRemove = itemToRemove; + } + + public byte RaidType { get; } + public InventoryItem ItemToRemove { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Raids/Events/RaidPartyDisbandEvent.cs b/srcs/WingsAPI.Game/Raids/Events/RaidPartyDisbandEvent.cs new file mode 100644 index 0000000..0039ff1 --- /dev/null +++ b/srcs/WingsAPI.Game/Raids/Events/RaidPartyDisbandEvent.cs @@ -0,0 +1,10 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Raids.Events; + +public class RaidPartyDisbandEvent : PlayerEvent +{ + public RaidPartyDisbandEvent(bool isByRdPacket = false) => IsByRdPacket = isByRdPacket; + + public bool IsByRdPacket { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Raids/Events/RaidPartyInvitePlayerEvent.cs b/srcs/WingsAPI.Game/Raids/Events/RaidPartyInvitePlayerEvent.cs new file mode 100644 index 0000000..633888d --- /dev/null +++ b/srcs/WingsAPI.Game/Raids/Events/RaidPartyInvitePlayerEvent.cs @@ -0,0 +1,10 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Raids.Events; + +public class RaidPartyInvitePlayerEvent : PlayerEvent +{ + public RaidPartyInvitePlayerEvent(long targetId) => TargetId = targetId; + + public long TargetId { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Raids/Events/RaidPartyJoinEvent.cs b/srcs/WingsAPI.Game/Raids/Events/RaidPartyJoinEvent.cs new file mode 100644 index 0000000..e27b2a5 --- /dev/null +++ b/srcs/WingsAPI.Game/Raids/Events/RaidPartyJoinEvent.cs @@ -0,0 +1,15 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Raids.Events; + +public class RaidPartyJoinEvent : PlayerEvent +{ + public RaidPartyJoinEvent(long raidOwnerId, bool isByRaidList) + { + RaidOwnerId = raidOwnerId; + IsByRaidList = isByRaidList; + } + + public long RaidOwnerId { get; } + public bool IsByRaidList { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Raids/Events/RaidPartyKickPlayerEvent.cs b/srcs/WingsAPI.Game/Raids/Events/RaidPartyKickPlayerEvent.cs new file mode 100644 index 0000000..232e298 --- /dev/null +++ b/srcs/WingsAPI.Game/Raids/Events/RaidPartyKickPlayerEvent.cs @@ -0,0 +1,10 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Raids.Events; + +public class RaidPartyKickPlayerEvent : PlayerEvent +{ + public RaidPartyKickPlayerEvent(long characterId) => CharacterId = characterId; + + public long CharacterId { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Raids/Events/RaidPartyLeaveEvent.cs b/srcs/WingsAPI.Game/Raids/Events/RaidPartyLeaveEvent.cs new file mode 100644 index 0000000..3b035b6 --- /dev/null +++ b/srcs/WingsAPI.Game/Raids/Events/RaidPartyLeaveEvent.cs @@ -0,0 +1,16 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Raids.Events; + +public class RaidPartyLeaveEvent : PlayerEvent +{ + public RaidPartyLeaveEvent(bool byKick, bool removeLife = true) + { + ByKick = byKick; + RemoveLife = removeLife; + } + + public bool ByKick { get; } + + public bool RemoveLife { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Raids/Events/RaidPlayerSwitchButtonEvent.cs b/srcs/WingsAPI.Game/Raids/Events/RaidPlayerSwitchButtonEvent.cs new file mode 100644 index 0000000..2ca2775 --- /dev/null +++ b/srcs/WingsAPI.Game/Raids/Events/RaidPlayerSwitchButtonEvent.cs @@ -0,0 +1,10 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Raids.Events; + +public class RaidPlayerSwitchButtonEvent : PlayerEvent +{ + public RaidPlayerSwitchButtonEvent(ButtonMapItem buttonMapItem) => ButtonMapItem = buttonMapItem; + + public ButtonMapItem ButtonMapItem { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Raids/Events/RaidPortalOpenEvent.cs b/srcs/WingsAPI.Game/Raids/Events/RaidPortalOpenEvent.cs new file mode 100644 index 0000000..c3b0069 --- /dev/null +++ b/srcs/WingsAPI.Game/Raids/Events/RaidPortalOpenEvent.cs @@ -0,0 +1,16 @@ +using PhoenixLib.Events; + +namespace WingsEmu.Game.Raids.Events; + +public class RaidPortalOpenEvent : IAsyncEvent +{ + public RaidPortalOpenEvent(RaidSubInstance raidSubInstance, IPortalEntity portal) + { + RaidSubInstance = raidSubInstance; + Portal = portal; + } + + public RaidSubInstance RaidSubInstance { get; } + + public IPortalEntity Portal { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Raids/Events/RaidResetRestrictionEvent.cs b/srcs/WingsAPI.Game/Raids/Events/RaidResetRestrictionEvent.cs new file mode 100644 index 0000000..a4a4fef --- /dev/null +++ b/srcs/WingsAPI.Game/Raids/Events/RaidResetRestrictionEvent.cs @@ -0,0 +1,7 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Raids.Events; + +public class RaidResetRestrictionEvent : PlayerEvent +{ +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Raids/Events/RaidRevivedEvent.cs b/srcs/WingsAPI.Game/Raids/Events/RaidRevivedEvent.cs new file mode 100644 index 0000000..102aeef --- /dev/null +++ b/srcs/WingsAPI.Game/Raids/Events/RaidRevivedEvent.cs @@ -0,0 +1,8 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Raids.Events; + +public class RaidRevivedEvent : PlayerEvent +{ + public bool RestoredLife { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Raids/Events/RaidRewardReceivedEvent.cs b/srcs/WingsAPI.Game/Raids/Events/RaidRewardReceivedEvent.cs new file mode 100644 index 0000000..25229c8 --- /dev/null +++ b/srcs/WingsAPI.Game/Raids/Events/RaidRewardReceivedEvent.cs @@ -0,0 +1,8 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Raids.Events; + +public class RaidRewardReceivedEvent : PlayerEvent +{ + public byte BoxRarity { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Raids/Events/RaidStartedEvent.cs b/srcs/WingsAPI.Game/Raids/Events/RaidStartedEvent.cs new file mode 100644 index 0000000..c3c700c --- /dev/null +++ b/srcs/WingsAPI.Game/Raids/Events/RaidStartedEvent.cs @@ -0,0 +1,7 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Raids.Events; + +public class RaidStartedEvent : PlayerEvent +{ +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Raids/Events/RaidSwitchButtonToggledEvent.cs b/srcs/WingsAPI.Game/Raids/Events/RaidSwitchButtonToggledEvent.cs new file mode 100644 index 0000000..9a76a56 --- /dev/null +++ b/srcs/WingsAPI.Game/Raids/Events/RaidSwitchButtonToggledEvent.cs @@ -0,0 +1,8 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Raids.Events; + +public class RaidSwitchButtonToggledEvent : PlayerEvent +{ + public long LeverId { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Raids/Events/RaidTargetKilledEvent.cs b/srcs/WingsAPI.Game/Raids/Events/RaidTargetKilledEvent.cs new file mode 100644 index 0000000..9b793a7 --- /dev/null +++ b/srcs/WingsAPI.Game/Raids/Events/RaidTargetKilledEvent.cs @@ -0,0 +1,8 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Raids.Events; + +public class RaidTargetKilledEvent : PlayerEvent +{ + public long[] DamagerCharactersIds { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Raids/Events/RaidTeleportMemberEvent.cs b/srcs/WingsAPI.Game/Raids/Events/RaidTeleportMemberEvent.cs new file mode 100644 index 0000000..161a098 --- /dev/null +++ b/srcs/WingsAPI.Game/Raids/Events/RaidTeleportMemberEvent.cs @@ -0,0 +1,21 @@ +using PhoenixLib.Events; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Maps; + +namespace WingsEmu.Game.Raids.Events; + +public class RaidTeleportMemberEvent : IAsyncEvent +{ + public RaidTeleportMemberEvent(IMapInstance mapInstance, Position sourcePosition, Position destinationPosition, byte range) + { + MapInstance = mapInstance; + SourcePosition = sourcePosition; + DestinationPosition = destinationPosition; + Range = range; + } + + public IMapInstance MapInstance { get; } + public Position SourcePosition { get; } + public Position DestinationPosition { get; } + public byte Range { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Raids/Events/RaidWonEvent.cs b/srcs/WingsAPI.Game/Raids/Events/RaidWonEvent.cs new file mode 100644 index 0000000..0d7cc7c --- /dev/null +++ b/srcs/WingsAPI.Game/Raids/Events/RaidWonEvent.cs @@ -0,0 +1,7 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Raids.Events; + +public class RaidWonEvent : PlayerEvent +{ +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Raids/IRaidComponent.cs b/srcs/WingsAPI.Game/Raids/IRaidComponent.cs new file mode 100644 index 0000000..807cd76 --- /dev/null +++ b/srcs/WingsAPI.Game/Raids/IRaidComponent.cs @@ -0,0 +1,16 @@ +namespace WingsEmu.Game.Raids; + +public interface IRaidComponent +{ + public RaidParty Raid { get; } + public byte RaidDeaths { get; } + + public bool IsInRaidParty { get; } + public bool HasRaidStarted { get; } + public bool RaidTeamIsFull { get; } + public bool IsRaidLeader(long characterId); + + public void SetRaidParty(RaidParty raidParty); + public void AddRaidDeath(); + public void RemoveRaidDeath(); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Raids/IRaidFactory.cs b/srcs/WingsAPI.Game/Raids/IRaidFactory.cs new file mode 100644 index 0000000..516d304 --- /dev/null +++ b/srcs/WingsAPI.Game/Raids/IRaidFactory.cs @@ -0,0 +1,6 @@ +namespace WingsEmu.Game.Raids; + +public interface IRaidFactory +{ + RaidInstance CreateRaid(RaidParty raidType); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Raids/IRaidManager.cs b/srcs/WingsAPI.Game/Raids/IRaidManager.cs new file mode 100644 index 0000000..94ee63a --- /dev/null +++ b/srcs/WingsAPI.Game/Raids/IRaidManager.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; + +namespace WingsEmu.Game.Raids; + +public interface IRaidManager +{ + IReadOnlyCollection Raids { get; } + void AddRaid(RaidParty raidParty); + void RemoveRaid(RaidParty raidParty); + + + #region RAID_LIST + + IReadOnlyCollection RaidPublishList { get; } + bool ContainsRaidInRaidPublishList(RaidParty raid); + void RegisterRaidInRaidPublishList(RaidParty raidParty); + void UnregisterRaidFromRaidPublishList(RaidParty raid); + + #endregion +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Raids/RaidBox.cs b/srcs/WingsAPI.Game/Raids/RaidBox.cs new file mode 100644 index 0000000..03edf4b --- /dev/null +++ b/srcs/WingsAPI.Game/Raids/RaidBox.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; + +namespace WingsEmu.Game.Raids; + +public class RaidBox +{ + public RaidBox(int rewardBox, IEnumerable raidBoxRarities) + { + RewardBox = rewardBox; + RaidBoxRarities = raidBoxRarities; + } + + public int RewardBox { get; } + public IEnumerable RaidBoxRarities { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Raids/RaidBoxRarity.cs b/srcs/WingsAPI.Game/Raids/RaidBoxRarity.cs new file mode 100644 index 0000000..ba63ee5 --- /dev/null +++ b/srcs/WingsAPI.Game/Raids/RaidBoxRarity.cs @@ -0,0 +1,13 @@ +namespace WingsEmu.Game.Raids; + +public class RaidBoxRarity +{ + public RaidBoxRarity(byte rarity, int chance) + { + Rarity = rarity; + Chance = chance; + } + + public byte Rarity { get; } + public int Chance { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Raids/RaidComponent.cs b/srcs/WingsAPI.Game/Raids/RaidComponent.cs new file mode 100644 index 0000000..6bfe366 --- /dev/null +++ b/srcs/WingsAPI.Game/Raids/RaidComponent.cs @@ -0,0 +1,53 @@ +using WingsEmu.Game.Networking; + +namespace WingsEmu.Game.Raids; + +public class RaidComponent : IRaidComponent +{ + public RaidParty Raid { get; private set; } + public byte RaidDeaths { get; private set; } + + public bool IsRaidLeader(long characterId) + { + if (!IsInRaidParty) + { + return false; + } + + if (Raid?.Members == null || Raid.Members.Count < 1) + { + return false; + } + + IClientSession leader = Raid.Members[0]; + return leader?.PlayerEntity.Id == characterId; + } + + public bool RaidTeamIsFull => Raid != null && Raid.Members.Count >= Raid.MaximumMembers; + + public bool IsInRaidParty => Raid != null; + + public bool HasRaidStarted => Raid is { Started: true }; + + public void SetRaidParty(RaidParty raidParty) + { + Raid = raidParty; + RaidDeaths = 0; + } + + public void AddRaidDeath() + { + RaidDeaths++; + } + + public void RemoveRaidDeath() + { + if (RaidDeaths < 1) + { + RaidDeaths = 0; + return; + } + + RaidDeaths--; + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Raids/RaidInstance.cs b/srcs/WingsAPI.Game/Raids/RaidInstance.cs new file mode 100644 index 0000000..a25ff29 --- /dev/null +++ b/srcs/WingsAPI.Game/Raids/RaidInstance.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using WingsEmu.Game.Helpers.Damages; + +namespace WingsEmu.Game.Raids; + +public class RaidInstance +{ + public RaidInstance(IReadOnlyCollection raidSubInstances, RaidSubInstance spawnInstance, Position spawnPoint, TimeSpan timeToComplete, byte maxLives, RaidReward raidReward) + { + foreach (RaidSubInstance raidSubInstance in raidSubInstances) + { + raidSubInstance.MapInstance.Initialize(DateTime.UtcNow.AddMilliseconds(-500)); + } + + DateTime now = DateTime.UtcNow; + + RaidSubInstances = GetDictionary(raidSubInstances); + FinishDate = now.Add(timeToComplete); + StartDate = now; + RemoveDate = FinishDate; + Lives = maxLives; + MaxLives = maxLives; + SpawnPoint = spawnPoint; + SpawnInstance = spawnInstance; + RaidReward = raidReward; + } + + public IReadOnlyDictionary RaidSubInstances { get; } + + public int Lives { get; private set; } + + public byte MaxLives { get; } + + public Position SpawnPoint { get; } + + public RaidSubInstance SpawnInstance { get; } + + public RaidReward RaidReward { get; } + + public DateTime FinishDate { get; } + + public DateTime StartDate { get; } + + public DateTime RemoveDate { get; private set; } + + public DateTime? FinishSlowMoDate { get; private set; } + + public TimeSpan TimeUntilEnd => FinishDate - DateTime.UtcNow; + + private static IReadOnlyDictionary GetDictionary(IEnumerable raidSubInstances) + { + var dictionary = new Dictionary(); + foreach (RaidSubInstance raidSubInstance in raidSubInstances) + { + dictionary.TryAdd(raidSubInstance.MapInstance.Id, raidSubInstance); + } + + return dictionary; + } + + public void IncreaseOrDecreaseLives(short amount) + { + int futureValue = Lives + amount; + if (futureValue > MaxLives) + { + Lives = MaxLives; + return; + } + + Lives = futureValue; + } + + public void SetDestroyDate(DateTime dateTime) + { + RemoveDate = dateTime; + } + + public void SetFinishSlowMoDate(DateTime? dateTime) + { + FinishSlowMoDate = dateTime; + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Raids/RaidPacketType.cs b/srcs/WingsAPI.Game/Raids/RaidPacketType.cs new file mode 100644 index 0000000..35fcc68 --- /dev/null +++ b/srcs/WingsAPI.Game/Raids/RaidPacketType.cs @@ -0,0 +1,11 @@ +namespace WingsEmu.Game.Raids; + +public enum RaidPacketType +{ + LIST_MEMBERS = 0, + LEAVE = 1, + LEADER_RELATED = 2, + REFRESH_MEMBERS_HP_MP = 3, + AFTER_INSTANCE_START_BUT_BEFORE_REFRESH_MEMBERS = 4, + INSTANCE_START = 5 +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Raids/RaidParty.cs b/srcs/WingsAPI.Game/Raids/RaidParty.cs new file mode 100644 index 0000000..db525a9 --- /dev/null +++ b/srcs/WingsAPI.Game/Raids/RaidParty.cs @@ -0,0 +1,172 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using WingsAPI.Packets.Enums; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Game.Raids; + +public class RaidParty +{ + private readonly ReaderWriterLockSlim _lock = new(); + private readonly List _members; + + public RaidParty(Guid id, RaidType type, + byte minimumLevel, byte maximumLevel, + byte minimumHeroLevel, byte maximumHeroLevel, + byte minimumMembers, byte maximumMembers) + { + Id = id; + Type = type; + MinimumLevel = minimumLevel; + MaximumLevel = maximumLevel; + MinimumHeroLevel = minimumHeroLevel; + MaximumHeroLevel = maximumHeroLevel; + MinimumMembers = minimumMembers; + MaximumMembers = maximumMembers; + _members = new List(MaximumMembers); + } + + public Guid Id { get; } + + public bool Started { get; private set; } + public bool Finished { get; private set; } + public bool Destroy { get; set; } + public RaidType Type { get; } + public byte MinimumLevel { get; } + public byte MaximumLevel { get; } + public byte MinimumHeroLevel { get; } + public byte MaximumHeroLevel { get; } + public byte MaximumMembers { get; } + public byte MinimumMembers { get; } + + public IReadOnlyList Members + { + get + { + _lock.EnterReadLock(); + try + { + return _members.FindAll(x => x != null); + } + finally + { + _lock.ExitReadLock(); + } + } + } + + public RaidInstance Instance { get; private set; } + + public IClientSession Leader + { + get + { + _lock.EnterReadLock(); + try + { + return _members.FirstOrDefault(); + } + finally + { + _lock.ExitReadLock(); + } + } + } + + public static IEqualityComparer IdComparer { get; } = new IdEqualityComparer(); + + public void AddMember(IClientSession session) + { + _lock.EnterWriteLock(); + try + { + _members.Add(session); + } + finally + { + _lock.ExitWriteLock(); + } + } + + public void RemoveMember(IClientSession session) + { + _lock.EnterWriteLock(); + try + { + _members.Remove(session); + } + finally + { + _lock.ExitWriteLock(); + } + } + + public void StartRaid(RaidInstance raid) + { + Started = true; + Instance = raid; + } + + public void FinishRaid(DateTime toRemove) + { + Finished = true; + Instance?.SetDestroyDate(toRemove); + } + + protected bool Equals(RaidParty other) => Id.Equals(other.Id); + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != GetType()) + { + return false; + } + + return Equals((RaidParty)obj); + } + + public override int GetHashCode() => Id.GetHashCode(); + + + private sealed class IdEqualityComparer : IEqualityComparer + { + public bool Equals(RaidParty x, RaidParty y) + { + if (ReferenceEquals(x, y)) + { + return true; + } + + if (ReferenceEquals(x, null)) + { + return false; + } + + if (ReferenceEquals(y, null)) + { + return false; + } + + if (x.GetType() != y.GetType()) + { + return false; + } + + return x.Id.Equals(y.Id); + } + + public int GetHashCode(RaidParty obj) => obj.Id.GetHashCode(); + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Raids/RaidReward.cs b/srcs/WingsAPI.Game/Raids/RaidReward.cs new file mode 100644 index 0000000..623046c --- /dev/null +++ b/srcs/WingsAPI.Game/Raids/RaidReward.cs @@ -0,0 +1,15 @@ +namespace WingsEmu.Game.Raids; + +public class RaidReward +{ + public RaidReward(RaidBox raidBox, bool defaultReputation, int? fixedReputation) + { + RaidBox = raidBox; + DefaultReputation = defaultReputation; + FixedReputation = fixedReputation; + } + + public RaidBox RaidBox { get; } + public bool DefaultReputation { get; } + public int? FixedReputation { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Raids/RaidSubInstance.cs b/srcs/WingsAPI.Game/Raids/RaidSubInstance.cs new file mode 100644 index 0000000..9d0f1c1 --- /dev/null +++ b/srcs/WingsAPI.Game/Raids/RaidSubInstance.cs @@ -0,0 +1,135 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Triggers; + +namespace WingsEmu.Game.Raids; + +public class RaidSubInstance : IEventTriggerContainer +{ + private readonly List _bossMonsters = new(); + + private readonly List _deadBossMonsters = new(); + + private readonly IEventTriggerContainer _eventTriggerContainer; + + public RaidSubInstance(IMapInstance mapInstance, IAsyncEventPipeline asyncEventPipeline) + { + MapInstance = mapInstance; + _eventTriggerContainer = new EventTriggerContainer(asyncEventPipeline); + } + + public IDictionary RaidWaves { get; private set; } + + public bool RaidWavesActivated { get; set; } + public byte RaidWaveState { get; set; } + public DateTime LastRaidWave { get; set; } = DateTime.MaxValue; + + public bool TargetsCompleted => CurrentCompletedTargetMonsters >= CurrentTargetMonsters && CurrentCompletedTargetButtons >= CurrentTargetButtons; + + public IMapInstance MapInstance { get; } + + public IReadOnlyCollection BossMonsters + { + get + { + CleanMonsters(); + return _bossMonsters; + } + } + + public IReadOnlyCollection DeadBossMonsters + { + get + { + CleanMonsters(); + return _deadBossMonsters; + } + } + + public int CurrentCompletedTargetMonsters { get; set; } + public int CurrentTargetMonsters { get; private set; } + + public int CurrentCompletedTargetButtons { get; set; } + + public int CurrentTargetButtons { get; private set; } + public bool IsDiscoveredByLeader { get; set; } + + public void AddEvent(string key, IAsyncEvent notification, bool removedOnTrigger = false) => _eventTriggerContainer.AddEvent(key, notification, removedOnTrigger); + + public Task TriggerEvents(string key) => _eventTriggerContainer.TriggerEvents(key); + + public void AddRaidMonster(IMonsterEntity monsterEntity) + { + if (monsterEntity.IsTarget || monsterEntity.IsBoss) + { + CurrentTargetMonsters++; + } + + if (monsterEntity.IsBoss) + { + _bossMonsters.Add(monsterEntity); + } + } + + public void RemoveRaidMonster(IMonsterEntity monsterEntity) + { + if (monsterEntity.IsTarget) + { + CurrentTargetMonsters--; + } + + if (monsterEntity.IsBoss) + { + _bossMonsters.Remove(monsterEntity); + } + + MapInstance.RemoveMonster(monsterEntity); + } + + public void AddRaidButton(ButtonMapItem buttonMapItem) + { + MapInstance.AddDrop(buttonMapItem); + if (!buttonMapItem.IsObjective) + { + return; + } + + CurrentTargetButtons++; + } + + public void RemoveRaidButton(ButtonMapItem buttonMapItem) + { + MapInstance.RemoveDrop(buttonMapItem.TransportId); + if (!buttonMapItem.IsObjective) + { + return; + } + + CurrentTargetButtons--; + } + + public void AddRaidWave(Dictionary raidWaves) + { + RaidWaves = raidWaves; + RaidWaveState = 0; + } + + private void CleanMonsters() + { + foreach (IMonsterEntity monsterEntity in _bossMonsters.ToArray()) + { + if (monsterEntity.IsAlive()) + { + continue; + } + + _bossMonsters.Remove(monsterEntity); + _deadBossMonsters.Add(monsterEntity); + } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Raids/RaidWave.cs b/srcs/WingsAPI.Game/Raids/RaidWave.cs new file mode 100644 index 0000000..f9e41fc --- /dev/null +++ b/srcs/WingsAPI.Game/Raids/RaidWave.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; + +namespace WingsEmu.Game.Raids; + +public class RaidWave +{ + public RaidWave(IEnumerable monsters, short timeInSeconds) + { + Monsters = monsters; + TimeInSeconds = timeInSeconds; + } + + public IEnumerable Monsters { get; } + public short TimeInSeconds { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Raids/RaidWindowType.cs b/srcs/WingsAPI.Game/Raids/RaidWindowType.cs new file mode 100644 index 0000000..49024c6 --- /dev/null +++ b/srcs/WingsAPI.Game/Raids/RaidWindowType.cs @@ -0,0 +1,11 @@ +namespace WingsEmu.Game.Raids; + +public enum RaidWindowType : byte +{ + MISSION_START = 0, + MISSION_CLEAR = 1, + TIMES_UP = 2, + LEADER_DEATH = 3, + NO_LIVES_LEFT = 4, + MISSION_FAIL = 5 // Used in Rainbow Battle +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Raids/RaidsPacketExtensions.cs b/srcs/WingsAPI.Game/Raids/RaidsPacketExtensions.cs new file mode 100644 index 0000000..ee9af59 --- /dev/null +++ b/srcs/WingsAPI.Game/Raids/RaidsPacketExtensions.cs @@ -0,0 +1,261 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using System.Linq; +using System.Text; +using WingsAPI.Packets.Enums; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Game.Raids; + +public static class RaidsPacketExtensions +{ + public static string GenerateRaidName(this IClientSession session, IGameLanguageService gameLanguageService, RaidType raidType) + { + string raidKey = $"RAID_NAME_{raidType.ToString().ToUpper()}"; + if (!System.Enum.TryParse(raidKey, out GameDialogKey key)) + { + return raidKey; + } + + return gameLanguageService.GetLanguage(key, session.UserLanguage); + } + + public static string GenerateRdList(this IClientSession session) + { + var stringBuilder = new StringBuilder("rdlst"); + stringBuilder.Append($" {session.PlayerEntity.Raid.MinimumLevel}"); + stringBuilder.Append($" {session.PlayerEntity.Raid.MaximumLevel}"); + stringBuilder.Append($" {(byte)session.PlayerEntity.Raid.Type}"); + foreach (IClientSession targetSession in session.PlayerEntity.Raid.Members) + { + stringBuilder.Append( + $" {targetSession.PlayerEntity.Level}.{(targetSession.PlayerEntity.Specialist != null && targetSession.PlayerEntity.UseSp ? targetSession.PlayerEntity.Specialist.GameItem.Morph : -1)}" + + $".{(byte)targetSession.PlayerEntity.Class}.{targetSession.PlayerEntity.RaidDeaths}." + + $"{targetSession.PlayerEntity.Name}.{(byte)targetSession.PlayerEntity.Gender}.{targetSession.PlayerEntity.Id}.{targetSession.PlayerEntity.HeroLevel}"); + } + + return stringBuilder.ToString(); + } + + public static string GenerateRl(this IClientSession session, byte type, IRaidManager raidManager) + { + IReadOnlyCollection registeredRaids = raidManager.RaidPublishList; + if (!registeredRaids.Any()) + { + return $"rl {type}"; + } + + string header = $"rl {type} "; + foreach (RaidParty raid in registeredRaids) + { + IClientSession leader = raid.Members.First(); + header += $" {(byte)raid.Type}.{raid.MinimumLevel}.{raid.MaximumLevel}.{leader.PlayerEntity.Name}.{leader.PlayerEntity.Level}." + + $"{(leader.PlayerEntity.Morph == 0 ? -1 : leader.PlayerEntity.Morph)}.{(byte)leader.PlayerEntity.Class}.{(byte)leader.PlayerEntity.Gender}.{raid.Members.Count}.{leader.PlayerEntity.HeroLevel}"; + } + + return header; + } + + public static string GenerateRaidPacket(this IClientSession session, RaidPacketType packetType, bool isLeaving = false) + { + var stringBuilder = new StringBuilder("raid"); + switch (packetType) + { + case RaidPacketType.LIST_MEMBERS: + stringBuilder.Append(" 0"); + + if (session.PlayerEntity.Raid?.Members != null) + { + foreach (IClientSession targetSession in session.PlayerEntity.Raid.Members.OrderByDescending(s => s.PlayerEntity.Level)) + { + stringBuilder.Append(" "); + stringBuilder.Append(targetSession.PlayerEntity.Id); + } + } + + break; + case RaidPacketType.LEAVE: + return isLeaving ? "raid 1 0" : "raid 1 1"; + case RaidPacketType.LEADER_RELATED: + return "raid 2 " + (isLeaving + ? "-1" + : $"{(session.PlayerEntity.Raid?.Members != null && session.PlayerEntity.Raid.Members.Any() ? session.PlayerEntity.Raid.Members.First().PlayerEntity.Id : 0)}"); + case RaidPacketType.REFRESH_MEMBERS_HP_MP: + stringBuilder.Append(" 3"); + + if (session.PlayerEntity.Raid?.Members != null) + { + foreach (IClientSession targetSession in session.PlayerEntity.Raid.Members.OrderByDescending(s => s.PlayerEntity.Level)) + { + stringBuilder.Append($" {targetSession.PlayerEntity.Id}.{targetSession.PlayerEntity.GetHpPercentage()}.{targetSession.PlayerEntity.GetMpPercentage()}"); + } + } + + break; + case RaidPacketType.AFTER_INSTANCE_START_BUT_BEFORE_REFRESH_MEMBERS: + return "raid 4"; + case RaidPacketType.INSTANCE_START: + return "raid 5 1"; + } + + return stringBuilder.ToString(); + } + + public static string GenerateRaidBossPacket(this IMonsterEntity entity, bool secondBoss) + => $"rboss {(secondBoss ? 4 : 3)} {entity.Id} {entity.Hp} {entity.MaxHp} {entity.MonsterVNum}"; + + public static string GenerateEmptyRaidBoss(this IClientSession session) => "rboss -1 -1 0 0"; + + public static void SendEmptyRaidBoss(this IClientSession session) + { + session.SendPacket(session.GenerateEmptyRaidBoss()); + } + + public static string GenerateRaidUiPacket(this IClientSession session, RaidType raidType, RaidWindowType windowType) => + $"raidbf 0 {(byte)windowType} {(raidType == RaidType.Laurena ? 40 : 25)}"; + + public static string GenerateThrowPacket(this IBattleEntity entity, MonsterMapItem mapItem) + => $"throw {mapItem.ItemVNum} {mapItem.TransportId} {entity.PositionX} {entity.PositionY} {mapItem.PositionX} {mapItem.PositionY} {mapItem.Amount}"; + + public static string GeneraterRaidmbf(this IClientSession session) + { + RaidSubInstance raidSubInstance = session.PlayerEntity.Raid.Instance.RaidSubInstances[session.PlayerEntity.MapInstanceId]; + + return "raidmbf " + + $"{raidSubInstance.CurrentTargetMonsters.ToString()} " + // initialMonstersToKill + $"{(raidSubInstance.CurrentTargetMonsters - raidSubInstance.CurrentCompletedTargetMonsters).ToString()} " + // currentMonsterToKill + $"{raidSubInstance.CurrentTargetButtons.ToString()} " + // initialButtonsToUse + $"{(raidSubInstance.CurrentTargetButtons - raidSubInstance.CurrentCompletedTargetButtons).ToString()} " + // currentButtonsToUse + $"{session.PlayerEntity.Raid.Instance.Lives.ToString()} " + + $"{session.PlayerEntity.Raid.Instance.MaxLives.ToString()} " + + $"{(session.PlayerEntity.Raid.Type == RaidType.Laurena ? 40 : 25).ToString()}"; + } + + public static void SendRaidPacket(this IClientSession session, RaidPacketType type, bool isLeaving = false) + { + session.SendPacket(session.GenerateRaidPacket(type, isLeaving)); + } + + public static void RefreshRaidMemberList(this IClientSession session) + { + if (!session.PlayerEntity.IsInRaidParty) + { + return; + } + + session.SendPacket(session.GenerateRdList()); + } + + public static void SendRlPacket(this IClientSession session, byte type, IRaidManager raidManager) => session.SendPacket(session.GenerateRl(type, raidManager)); + + public static void TrySendRaidBossPackets(this IClientSession session) + { + if (!session.PlayerEntity.IsInRaidParty) + { + return; + } + + if (session.CurrentMapInstance == null) + { + return; + } + + if (session.PlayerEntity.Raid?.Instance == null) + { + return; + } + + if (!session.PlayerEntity.Raid.Instance.RaidSubInstances.TryGetValue(session.CurrentMapInstance.Id, out RaidSubInstance subInstance)) + { + return; + } + + bool secondBoss = false; + foreach (IMonsterEntity boss in subInstance.BossMonsters) + { + session.SendPacket(boss.GenerateRaidBossPacket(secondBoss)); + secondBoss = true; + } + } + + public static void TrySendRaidBossDeadPackets(this IClientSession session) + { + if (!session.PlayerEntity.Raid.Instance.RaidSubInstances.ContainsKey(session.CurrentMapInstance.Id)) + { + return; + } + + RaidSubInstance subInstance = session.PlayerEntity.Raid.Instance.RaidSubInstances[session.CurrentMapInstance.Id]; + + bool secondBoss = false; + foreach (IMonsterEntity boss in subInstance.DeadBossMonsters) + { + session.SendPacket(boss.GenerateRaidBossPacket(secondBoss)); + secondBoss = true; + } + } + + public static void SendRaidUiPacket(this IClientSession session, RaidType raidType, RaidWindowType raidWindowType) => + session.SendPacket(session.GenerateRaidUiPacket(raidType, raidWindowType)); + + public static void SendRaidmbf(this IClientSession session) + { + if (session.PlayerEntity.Raid?.Instance == null) + { + return; + } + + if (!session.PlayerEntity.Raid.Instance.RaidSubInstances.ContainsKey(session.PlayerEntity.MapInstanceId)) + { + return; + } + + session.SendPacket(session.GeneraterRaidmbf()); + } + + public static void BroadcastThrow(this IBattleEntity entity, MonsterMapItem mapItem) => entity.MapInstance.Broadcast(entity.GenerateThrowPacket(mapItem)); + + public static bool IsRaidTypeRestricted(this IClientSession session, RaidType raidType) + { + switch (raidType) + { + case RaidType.Glacerus: + case RaidType.LordDraco: + return true; + default: + return false; + } + } + + public static bool CanPlayerJoinToRestrictedRaid(this IClientSession session, RaidType raidType) + { + return raidType switch + { + RaidType.LordDraco => session.PlayerEntity.RaidRestrictionDto.LordDraco > 0, + RaidType.Glacerus => session.PlayerEntity.RaidRestrictionDto.Glacerus > 0, + _ => true + }; + } + + public static bool IsPlayerWearingRaidAmulet(this IClientSession session, RaidType raidType) + { + if (session.PlayerEntity.Amulet == null) + { + return false; + } + + return raidType switch + { + RaidType.LordDraco => session.PlayerEntity.Amulet.ItemVNum == (short)ItemVnums.DRACO_AMULET, + RaidType.Glacerus => session.PlayerEntity.Amulet.ItemVNum == (short)ItemVnums.GLACERUS_AMULET, + _ => true + }; + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Raids/RdClientPacketType.cs b/srcs/WingsAPI.Game/Raids/RdClientPacketType.cs new file mode 100644 index 0000000..fe9d6c9 --- /dev/null +++ b/srcs/WingsAPI.Game/Raids/RdClientPacketType.cs @@ -0,0 +1,9 @@ +namespace WingsEmu.Game.Raids; + +public enum RdClientPacketType +{ + JOIN_RAID = 1, + LEAVE_RAID = 2, + KICK_FROM_RAID = 3, + DISSOLVE_RAID = 4 +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Raids/RdServerPacketType.cs b/srcs/WingsAPI.Game/Raids/RdServerPacketType.cs new file mode 100644 index 0000000..61840fe --- /dev/null +++ b/srcs/WingsAPI.Game/Raids/RdServerPacketType.cs @@ -0,0 +1,8 @@ +namespace WingsEmu.Game.Raids; + +public enum RdServerPacketType +{ + Invite = 1, + Leave = 2, + Kick = 3 +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Raids/RlClientPacketType.cs b/srcs/WingsAPI.Game/Raids/RlClientPacketType.cs new file mode 100644 index 0000000..e653871 --- /dev/null +++ b/srcs/WingsAPI.Game/Raids/RlClientPacketType.cs @@ -0,0 +1,9 @@ +namespace WingsEmu.Game.Raids; + +public enum RlClientPacketType +{ + SHOW = 0, + REGISTER = 1, + UNREGISTER = 2, + JOIN = 3 +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Raids/RlServerPacketType.cs b/srcs/WingsAPI.Game/Raids/RlServerPacketType.cs new file mode 100644 index 0000000..e2c6fc7 --- /dev/null +++ b/srcs/WingsAPI.Game/Raids/RlServerPacketType.cs @@ -0,0 +1,9 @@ +namespace WingsEmu.Game.Raids; + +public enum RlServerPacketType +{ + CAN_JOIN = 0, + IS_LEADER = 1, + IS_IN_RAID = 2, + IS_IN_A_GROUP = 3 +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleCaptureFlagEvent.cs b/srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleCaptureFlagEvent.cs new file mode 100644 index 0000000..80f530f --- /dev/null +++ b/srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleCaptureFlagEvent.cs @@ -0,0 +1,10 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Game.Entities; + +namespace WingsEmu.Game.RainbowBattle.Event; + +public class RainbowBattleCaptureFlagEvent : PlayerEvent +{ + public INpcEntity NpcEntity { get; init; } + public bool IsConfirm { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleDestroyEvent.cs b/srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleDestroyEvent.cs new file mode 100644 index 0000000..86e07a9 --- /dev/null +++ b/srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleDestroyEvent.cs @@ -0,0 +1,8 @@ +using PhoenixLib.Events; + +namespace WingsEmu.Game.RainbowBattle.Event; + +public class RainbowBattleDestroyEvent : IAsyncEvent +{ + public RainbowBattleParty RainbowBattleParty { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleEndEvent.cs b/srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleEndEvent.cs new file mode 100644 index 0000000..0fb92bb --- /dev/null +++ b/srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleEndEvent.cs @@ -0,0 +1,8 @@ +using PhoenixLib.Events; + +namespace WingsEmu.Game.RainbowBattle.Event; + +public class RainbowBattleEndEvent : IAsyncEvent +{ + public RainbowBattleParty RainbowBattleParty { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleEnterEvent.cs b/srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleEnterEvent.cs new file mode 100644 index 0000000..85cc138 --- /dev/null +++ b/srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleEnterEvent.cs @@ -0,0 +1,8 @@ +using PhoenixLib.Events; + +namespace WingsEmu.Game.RainbowBattle.Event; + +public class RainbowBattleEnterEvent : IAsyncEvent +{ + public long[] Players { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleFreezeEvent.cs b/srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleFreezeEvent.cs new file mode 100644 index 0000000..76fbf81 --- /dev/null +++ b/srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleFreezeEvent.cs @@ -0,0 +1,9 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Game.Entities; + +namespace WingsEmu.Game.RainbowBattle.Event; + +public class RainbowBattleFreezeEvent : PlayerEvent +{ + public IBattleEntity Killer { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleFrozenEvent.cs b/srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleFrozenEvent.cs new file mode 100644 index 0000000..f143876 --- /dev/null +++ b/srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleFrozenEvent.cs @@ -0,0 +1,11 @@ +using System; +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.RainbowBattle.Event; + +public class RainbowBattleFrozenEvent : PlayerEvent +{ + public Guid Id { get; init; } + public RainbowBattlePlayerDump Killer { get; init; } + public RainbowBattlePlayerDump Killed { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleJoinEvent.cs b/srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleJoinEvent.cs new file mode 100644 index 0000000..6e672af --- /dev/null +++ b/srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleJoinEvent.cs @@ -0,0 +1,7 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.RainbowBattle.Event; + +public class RainbowBattleJoinEvent : PlayerEvent +{ +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleLeaveEvent.cs b/srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleLeaveEvent.cs new file mode 100644 index 0000000..d5a58f7 --- /dev/null +++ b/srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleLeaveEvent.cs @@ -0,0 +1,10 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.RainbowBattle.Event; + +public class RainbowBattleLeaveEvent : PlayerEvent +{ + public bool SendMessage { get; init; } + public bool CheckIfFinished { get; init; } + public bool AddLeaverBuster { get; init; } = true; +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleLeaverBusterRefreshEvent.cs b/srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleLeaverBusterRefreshEvent.cs new file mode 100644 index 0000000..fe85cb4 --- /dev/null +++ b/srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleLeaverBusterRefreshEvent.cs @@ -0,0 +1,8 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.RainbowBattle.Event; + +public class RainbowBattleLeaverBusterRefreshEvent : PlayerEvent +{ + public bool Force { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleLoseEvent.cs b/srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleLoseEvent.cs new file mode 100644 index 0000000..6eb12f1 --- /dev/null +++ b/srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleLoseEvent.cs @@ -0,0 +1,10 @@ +using System; +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.RainbowBattle.Event; + +public class RainbowBattleLoseEvent : PlayerEvent +{ + public Guid Id { get; init; } + public int[] Players { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleProcessActivityPointsEvent.cs b/srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleProcessActivityPointsEvent.cs new file mode 100644 index 0000000..ecc68b2 --- /dev/null +++ b/srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleProcessActivityPointsEvent.cs @@ -0,0 +1,8 @@ +using PhoenixLib.Events; + +namespace WingsEmu.Game.RainbowBattle.Event; + +public class RainbowBattleProcessActivityPointsEvent : IAsyncEvent +{ + public RainbowBattleParty RainbowBattleParty { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleProcessFlagPointsEvent.cs b/srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleProcessFlagPointsEvent.cs new file mode 100644 index 0000000..90e4b40 --- /dev/null +++ b/srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleProcessFlagPointsEvent.cs @@ -0,0 +1,8 @@ +using PhoenixLib.Events; + +namespace WingsEmu.Game.RainbowBattle.Event; + +public class RainbowBattleProcessFlagPointsEvent : IAsyncEvent +{ + public RainbowBattleParty RainbowBattleParty { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleProcessLifeEvent.cs b/srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleProcessLifeEvent.cs new file mode 100644 index 0000000..2e08085 --- /dev/null +++ b/srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleProcessLifeEvent.cs @@ -0,0 +1,8 @@ +using PhoenixLib.Events; + +namespace WingsEmu.Game.RainbowBattle.Event; + +public class RainbowBattleProcessLifeEvent : IAsyncEvent +{ + public RainbowBattleParty RainbowBattleParty { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleRefreshScoreEvent.cs b/srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleRefreshScoreEvent.cs new file mode 100644 index 0000000..8ed3588 --- /dev/null +++ b/srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleRefreshScoreEvent.cs @@ -0,0 +1,8 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.RainbowBattle.Event; + +public class RainbowBattleRefreshScoreEvent : PlayerEvent +{ + public RainbowBattleParty RainbowBattleParty { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleStartEvent.cs b/srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleStartEvent.cs new file mode 100644 index 0000000..4b9a44b --- /dev/null +++ b/srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleStartEvent.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using WingsEmu.Game._packetHandling; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Game.RainbowBattle.Event; + +public class RainbowBattleStartEvent : PlayerEvent +{ + public List RedTeam { get; init; } + public List BlueTeam { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleStartProcessRegistrationEvent.cs b/srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleStartProcessRegistrationEvent.cs new file mode 100644 index 0000000..4859236 --- /dev/null +++ b/srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleStartProcessRegistrationEvent.cs @@ -0,0 +1,7 @@ +using PhoenixLib.Events; + +namespace WingsEmu.Game.RainbowBattle.Event; + +public class RainbowBattleStartProcessRegistrationEvent : IAsyncEvent +{ +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleStartRegisterEvent.cs b/srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleStartRegisterEvent.cs new file mode 100644 index 0000000..be215d0 --- /dev/null +++ b/srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleStartRegisterEvent.cs @@ -0,0 +1,7 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.RainbowBattle.Event; + +public class RainbowBattleStartRegisterEvent : PlayerEvent +{ +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleTieEvent.cs b/srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleTieEvent.cs new file mode 100644 index 0000000..8fac395 --- /dev/null +++ b/srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleTieEvent.cs @@ -0,0 +1,9 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.RainbowBattle.Event; + +public class RainbowBattleTieEvent : PlayerEvent +{ + public int[] RedTeam { get; init; } + public int[] BlueTeam { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleUnfreezeEvent.cs b/srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleUnfreezeEvent.cs new file mode 100644 index 0000000..3f1d804 --- /dev/null +++ b/srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleUnfreezeEvent.cs @@ -0,0 +1,9 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Game.Characters; + +namespace WingsEmu.Game.RainbowBattle.Event; + +public class RainbowBattleUnfreezeEvent : PlayerEvent +{ + public IPlayerEntity Unfreezer { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleUnfreezeProcessEvent.cs b/srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleUnfreezeProcessEvent.cs new file mode 100644 index 0000000..3403cf8 --- /dev/null +++ b/srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleUnfreezeProcessEvent.cs @@ -0,0 +1,8 @@ +using PhoenixLib.Events; + +namespace WingsEmu.Game.RainbowBattle.Event; + +public class RainbowBattleUnfreezeProcessEvent : IAsyncEvent +{ + public RainbowBattleParty RainbowBattleParty { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleWonEvent.cs b/srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleWonEvent.cs new file mode 100644 index 0000000..b511dff --- /dev/null +++ b/srcs/WingsAPI.Game/RainbowBattle/Event/RainbowBattleWonEvent.cs @@ -0,0 +1,10 @@ +using System; +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.RainbowBattle.Event; + +public class RainbowBattleWonEvent : PlayerEvent +{ + public Guid Id { get; init; } + public int[] Players { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/RainbowBattle/IRainbowBattleManager.cs b/srcs/WingsAPI.Game/RainbowBattle/IRainbowBattleManager.cs new file mode 100644 index 0000000..db6a33b --- /dev/null +++ b/srcs/WingsAPI.Game/RainbowBattle/IRainbowBattleManager.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using WingsEmu.Game._enum; +using WingsEmu.Game.Configurations; + +namespace WingsEmu.Game.RainbowBattle; + +public interface IRainbowBattleManager +{ + bool IsActive { get; set; } + bool IsRegistrationActive { get; } + DateTime RegistrationStartTime { get; } + DateTime? RainbowBattleProcessTime { get; set; } + + IReadOnlyList<(TimeSpan, int, TimeType)> Warnings { get; } + IEnumerable RegisteredPlayers { get; } + + IEnumerable RainbowBattleParties { get; } + void EnableBattleRainbowRegistration(); + void DisableBattleRainbowRegistration(); + + void RegisterPlayer(long id); + void UnregisterPlayer(long id); + void ClearRegisteredPlayers(); + + void AddRainbowBattle(RainbowBattleParty rainbowBattleParty); + void RemoveRainbowBattle(RainbowBattleParty rainbowBattleParty); +} + +public class RainbowBattleManager : IRainbowBattleManager +{ + private readonly ReaderWriterLockSlim _lock = new(); + private readonly ConcurrentDictionary _rainbowBattleParties = new(); + private readonly HashSet _registeredPlayers = new(); + + public RainbowBattleManager(RainbowBattleConfiguration rainbowBattleConfiguration) + { + var warnings = new List<(TimeSpan, int, TimeType)>(); + foreach (TimeSpan warning in rainbowBattleConfiguration.Warnings) + { + TimeSpan time = TimeSpan.FromMinutes(5) - warning; + bool isSec = time.TotalMinutes < 1; + + warnings.Add(new ValueTuple(warning, (int)(isSec ? time.TotalSeconds : time.TotalMinutes), isSec ? TimeType.SECONDS : TimeType.MINUTES)); + } + + Warnings = warnings; + } + + public bool IsActive { get; set; } + public bool IsRegistrationActive { get; private set; } + + public void EnableBattleRainbowRegistration() + { + IsRegistrationActive = true; + RegistrationStartTime = DateTime.UtcNow.AddSeconds(30); + } + + public void DisableBattleRainbowRegistration() + { + IsRegistrationActive = false; + RegistrationStartTime = DateTime.MinValue; + } + + public DateTime RegistrationStartTime { get; private set; } + + public DateTime? RainbowBattleProcessTime { get; set; } + + public IReadOnlyList<(TimeSpan, int, TimeType)> Warnings { get; } + + public void RegisterPlayer(long id) + { + _lock.EnterWriteLock(); + try + { + _registeredPlayers.Add(id); + } + finally + { + _lock.ExitWriteLock(); + } + } + + public void UnregisterPlayer(long id) + { + _lock.EnterWriteLock(); + try + { + _registeredPlayers.Remove(id); + } + finally + { + _lock.ExitWriteLock(); + } + } + + public IEnumerable RegisteredPlayers + { + get + { + _lock.EnterReadLock(); + try + { + return _registeredPlayers.ToArray(); + } + finally + { + _lock.ExitReadLock(); + } + } + } + + public void ClearRegisteredPlayers() + { + _lock.EnterWriteLock(); + try + { + _registeredPlayers.Clear(); + } + finally + { + _lock.ExitWriteLock(); + } + } + + public void AddRainbowBattle(RainbowBattleParty rainbowBattleParty) + { + _rainbowBattleParties.TryAdd(rainbowBattleParty.Id, rainbowBattleParty); + } + + public void RemoveRainbowBattle(RainbowBattleParty rainbowBattleParty) + { + _rainbowBattleParties.Remove(rainbowBattleParty.Id, out _); + } + + public IEnumerable RainbowBattleParties => _rainbowBattleParties.Values.ToArray(); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/RainbowBattle/RainBowFlag.cs b/srcs/WingsAPI.Game/RainbowBattle/RainBowFlag.cs new file mode 100644 index 0000000..9c31470 --- /dev/null +++ b/srcs/WingsAPI.Game/RainbowBattle/RainBowFlag.cs @@ -0,0 +1,11 @@ +using System; +using WingsAPI.Packets.Enums.Rainbow; + +namespace WingsEmu.Game.RainbowBattle; + +public class RainBowFlag +{ + public RainbowBattleFlagTeamType FlagTeamType { get; set; } + public RainbowBattleFlagType FlagType { get; set; } + public DateTime RainbowBattleLastTakeOver { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/RainbowBattle/RainbowBattleComponent.cs b/srcs/WingsAPI.Game/RainbowBattle/RainbowBattleComponent.cs new file mode 100644 index 0000000..96d4b3f --- /dev/null +++ b/srcs/WingsAPI.Game/RainbowBattle/RainbowBattleComponent.cs @@ -0,0 +1,52 @@ +using System; +using WingsAPI.Packets.Enums.Rainbow; + +namespace WingsEmu.Game.RainbowBattle; + +public interface IRainbowBattleComponent +{ + int Kills { get; set; } + int Deaths { get; set; } + bool IsInRainbowBattle { get; } + bool IsFrozen { get; set; } + DateTime? FrozenTime { get; set; } + RainbowBattleParty RainbowBattleParty { get; } + int ActivityPoints { get; set; } + + RainbowBattleTeamType Team { get; } + + void SetRainbowBattle(RainbowBattleParty rainbowBattleParty, RainbowBattleTeamType team); + void RemoveRainbowBattle(); +} + +public class RainbowBattleComponent : IRainbowBattleComponent +{ + public int Kills { get; set; } + public int Deaths { get; set; } + public bool IsInRainbowBattle => RainbowBattleParty != null; + public bool IsFrozen { get; set; } + public DateTime? FrozenTime { get; set; } + + public RainbowBattleParty RainbowBattleParty { get; private set; } + public int ActivityPoints { get; set; } + public RainbowBattleTeamType Team { get; private set; } + + public void SetRainbowBattle(RainbowBattleParty rainbowBattleParty, RainbowBattleTeamType team) + { + RainbowBattleParty = rainbowBattleParty; + Team = team; + Kills = 0; + Deaths = 0; + IsFrozen = false; + ActivityPoints = 0; + } + + public void RemoveRainbowBattle() + { + RainbowBattleParty = null; + Kills = 0; + Deaths = 0; + IsFrozen = false; + ActivityPoints = 0; + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/RainbowBattle/RainbowBattleExtensions.cs b/srcs/WingsAPI.Game/RainbowBattle/RainbowBattleExtensions.cs new file mode 100644 index 0000000..9c2fee6 --- /dev/null +++ b/srcs/WingsAPI.Game/RainbowBattle/RainbowBattleExtensions.cs @@ -0,0 +1,157 @@ +using System; +using System.Collections.Generic; +using System.Text; +using WingsAPI.Packets.Enums.Rainbow; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Game.RainbowBattle; + +public static class RainbowBattleExtensions +{ + public static string GenerateRainbowMembers(this RainbowBattleParty rainbowBattle, RainbowBattleTeamType team) + { + var packet = new StringBuilder("fbt 0 "); + + IReadOnlyList members = team == RainbowBattleTeamType.Red ? rainbowBattle.RedTeam : rainbowBattle.BlueTeam; + + foreach (IClientSession member in members) + { + packet.Append($" {member.PlayerEntity.Id}"); + } + + return packet.ToString(); + } + + public static string GenerateRainbowBattleWidget(this RainbowBattleParty rainbowBattle, RainbowBattleTeamType team) + { + var packet = new StringBuilder("fblst"); + + IReadOnlyList members = team == RainbowBattleTeamType.Red ? rainbowBattle.RedTeam : rainbowBattle.BlueTeam; + + foreach (IClientSession member in members) + { + packet.Append(" " + + $"{member.PlayerEntity.Level}" + + $".{(member.PlayerEntity.Specialist != null && member.PlayerEntity.UseSp ? member.PlayerEntity.Specialist.GameItem.Morph : -1)}" + + $".{(byte)member.PlayerEntity.Class}" + + $".{member.PlayerEntity.RainbowBattleComponent.Kills}" + + $".{member.PlayerEntity.Name}" + + $".{(byte)member.PlayerEntity.Gender}" + + $".{member.PlayerEntity.Id}" + + $".{member.PlayerEntity.HeroLevel}"); + } + + return packet.ToString(); + } + + public static string GenerateRainbowBattleLive(this RainbowBattleParty rainbowBattle, RainbowBattleTeamType team) + { + var packet = new StringBuilder("fbt 3"); + + IReadOnlyList members = team == RainbowBattleTeamType.Red ? rainbowBattle.RedTeam : rainbowBattle.BlueTeam; + + foreach (IClientSession member in members) + { + packet.Append($" {member.PlayerEntity.Id}.{member.PlayerEntity.HpPercentage}.{member.PlayerEntity.MpPercentage}"); + } + + return packet.ToString(); + } + + public static string GenerateFlagPacket(this INpcEntity entity) => $"fbt 6 {entity.Id} {(byte)entity.RainbowFlag.FlagType} {(byte)entity.RainbowFlag.FlagTeamType}"; + + public static string GenerateRainBowEnter(bool isEnter) => $"fbt 1 {(isEnter ? 1 : 0)}"; + public static string GenerateRainBowExit() => "fbt 2 -1"; + + public static string GenerateRainbowScore(this RainbowBattleParty party, RainbowBattleTeamType teamType) + { + var packet = new StringBuilder("fbs "); + + int redPoints = party.RedPoints; + int bluePoints = party.BluePoints; + + switch (teamType) + { + case RainbowBattleTeamType.Red: + + int redTeamCount = party.RedTeam.Count; + int redBigFlags = party.RedFlags.TryGetValue(RainbowBattleFlagType.Big, out byte bigRedCount) ? bigRedCount : 0; + int redMediumFlags = party.RedFlags.TryGetValue(RainbowBattleFlagType.Medium, out byte mediumRedCount) ? mediumRedCount : 0; + int redSmallFlags = party.RedFlags.TryGetValue(RainbowBattleFlagType.Small, out byte smallRedCount) ? smallRedCount : 0; + + packet.Append($"1 {redTeamCount} {redPoints} {bluePoints} {redSmallFlags} {redMediumFlags} {redBigFlags} RED"); + + break; + case RainbowBattleTeamType.Blue: + + int blueTeamCount = party.BlueTeam.Count; + int blueBigFlags = party.BlueFlags.TryGetValue(RainbowBattleFlagType.Big, out byte bigBlueCount) ? bigBlueCount : 0; + int blueMediumFlags = party.BlueFlags.TryGetValue(RainbowBattleFlagType.Medium, out byte mediumBlueCount) ? mediumBlueCount : 0; + int blueSmallFlags = party.BlueFlags.TryGetValue(RainbowBattleFlagType.Small, out byte smallBlueCount) ? smallBlueCount : 0; + + packet.Append($"2 {blueTeamCount} {redPoints} {bluePoints} {blueSmallFlags} {blueMediumFlags} {blueBigFlags} BLUE"); + + break; + } + + return packet.ToString(); + } + + public static string GenerateRainbowTime(RainbowTimeType timeType, short? seconds = null) + { + switch (timeType) + { + case RainbowTimeType.End: + return "fbt 5 0 0"; + case RainbowTimeType.Start: + return $"fbt 5 1 {seconds ?? 0}"; + case RainbowTimeType.Enter: + return "fbt 5 2 0"; + default: + throw new ArgumentOutOfRangeException(nameof(timeType), timeType, null); + } + } + + public static string GenerateRainbowTeamType(this IClientSession session) => + $"guri 5 1 {session.PlayerEntity.Id} {(session.PlayerEntity.RainbowBattleComponent.Team == RainbowBattleTeamType.Red ? 1 : 2).ToString()}"; + + public static void BroadcastRainbowTeamType(this IClientSession session) => session.CurrentMapInstance.Broadcast(session.GenerateRainbowTeamType()); + + public static bool CanJoinToRainbowBattle(this IClientSession session) + { + if (session.PlayerEntity.RainbowBattleComponent.IsInRainbowBattle) + { + return false; + } + + if (session.PlayerEntity.IsInGroup()) + { + return false; + } + + if (session.PlayerEntity.TimeSpaceComponent.IsInTimeSpaceParty) + { + return false; + } + + if (session.PlayerEntity.IsInRaidParty) + { + return false; + } + + if (session.PlayerEntity.HasShopOpened) + { + return false; + } + + if (session.IsMuted()) + { + return false; + } + + return session.CurrentMapInstance != null && session.CurrentMapInstance.HasMapFlag(MapFlags.IS_BASE_MAP); + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/RainbowBattle/RainbowBattleParty.cs b/srcs/WingsAPI.Game/RainbowBattle/RainbowBattleParty.cs new file mode 100644 index 0000000..743ffc7 --- /dev/null +++ b/srcs/WingsAPI.Game/RainbowBattle/RainbowBattleParty.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; +using WingsAPI.Packets.Enums.Rainbow; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Game.RainbowBattle; + +public class RainbowBattleParty +{ + private readonly List _blueTeam; + + private readonly ReaderWriterLockSlim _lock = new(); + + private readonly List _redTeam; + + public RainbowBattleParty(List redTeam, List blueTeam) + { + DateTime now = DateTime.UtcNow; + Id = Guid.NewGuid(); + _redTeam = redTeam; + _blueTeam = blueTeam; + EndTime = now.AddMinutes(6).AddSeconds(59); + StartTime = now; + } + + public Guid Id { get; } + + public DateTime EndTime { get; } + public DateTime StartTime { get; } + public DateTime LastMembersLife { get; set; } + public DateTime? FinishTime { get; set; } + public DateTime LastPointsTeamAdd { get; set; } + public DateTime LastActivityPointsTeamAdd { get; set; } + public bool Started { get; set; } + + public IMapInstance MapInstance { get; init; } + + public int RedPoints { get; private set; } + public int BluePoints { get; private set; } + + public ConcurrentDictionary RedFlags { get; set; } = new(); + public ConcurrentDictionary BlueFlags { get; set; } = new(); + + public IReadOnlyList RedTeam + { + get + { + _lock.EnterReadLock(); + try + { + return _redTeam.ToArray(); + } + finally + { + _lock.ExitReadLock(); + } + } + } + + public IReadOnlyList BlueTeam + { + get + { + _lock.EnterReadLock(); + try + { + return _blueTeam.ToArray(); + ; + } + finally + { + _lock.ExitReadLock(); + } + } + } + + public void IncreaseRedPoints(int count) + { + _lock.EnterWriteLock(); + try + { + RedPoints += count; + + if (RedPoints < 0) + { + RedPoints = 0; + } + } + finally + { + _lock.ExitWriteLock(); + } + } + + public void IncreaseBluePoints(int count) + { + _lock.EnterWriteLock(); + try + { + BluePoints += count; + + if (BluePoints < 0) + { + BluePoints = 0; + } + } + finally + { + _lock.ExitWriteLock(); + } + } + + public void RemoveRedPlayer(IClientSession session) + { + _lock.EnterWriteLock(); + try + { + _redTeam.Remove(session); + } + finally + { + _lock.ExitWriteLock(); + } + } + + public void RemoveBluePlayer(IClientSession session) + { + _lock.EnterWriteLock(); + try + { + _blueTeam.Remove(session); + } + finally + { + _lock.ExitWriteLock(); + } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/RainbowBattle/RainbowBattlePlayerDump.cs b/srcs/WingsAPI.Game/RainbowBattle/RainbowBattlePlayerDump.cs new file mode 100644 index 0000000..bdd8c2b --- /dev/null +++ b/srcs/WingsAPI.Game/RainbowBattle/RainbowBattlePlayerDump.cs @@ -0,0 +1,9 @@ +using WingsEmu.Game.Characters; + +namespace WingsEmu.Game.RainbowBattle; + +public class RainbowBattlePlayerDump : BasicPlayerDump +{ + public int Score { get; init; } + public string Team { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/RandomBag.cs b/srcs/WingsAPI.Game/RandomBag.cs new file mode 100644 index 0000000..335d68e --- /dev/null +++ b/srcs/WingsAPI.Game/RandomBag.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; + +namespace WingsEmu.Game; + +public class RandomBag +{ + private readonly List _entries = new(); + + private readonly IRandomGenerator _random; + private double _accumulator; + + public RandomBag(IRandomGenerator random) => _random = random; + + public void AddEntry(T item, double weight) + { + _accumulator += weight; + _entries.Add(new Entry { Item = item, AccumulatedWeight = _accumulator }); + } + + public T GetRandom() + { + double rnd = _random.RandomNumber((int)_accumulator); + + foreach (Entry entry in _entries) + { + if (rnd >= entry.AccumulatedWeight) + { + continue; + } + + return entry.Item; + } + + return default; //should only happen when there are no entries + } + + private struct Entry + { + public T Item { get; init; } + public double AccumulatedWeight { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Range.cs b/srcs/WingsAPI.Game/Range.cs new file mode 100644 index 0000000..3ef9484 --- /dev/null +++ b/srcs/WingsAPI.Game/Range.cs @@ -0,0 +1,8 @@ +namespace WingsEmu.Core; + +public class Range +{ + public T Minimum { get; set; } + + public T Maximum { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Recipes/IRecipeFactory.cs b/srcs/WingsAPI.Game/Recipes/IRecipeFactory.cs new file mode 100644 index 0000000..25dc679 --- /dev/null +++ b/srcs/WingsAPI.Game/Recipes/IRecipeFactory.cs @@ -0,0 +1,8 @@ +using WingsEmu.DTOs.Recipes; + +namespace WingsEmu.Game; + +public interface IRecipeFactory +{ + Recipe CreateRecipe(RecipeDTO recipeDto); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Recipes/Recipe.cs b/srcs/WingsAPI.Game/Recipes/Recipe.cs new file mode 100644 index 0000000..4caeb14 --- /dev/null +++ b/srcs/WingsAPI.Game/Recipes/Recipe.cs @@ -0,0 +1,38 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using WingsEmu.DTOs.Recipes; +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game; + +public class Recipe +{ + public Recipe(int id, int amount, int? producerMapNpcId, int? producerItemVnum, int? producerNpcVnum, int producedItemVnum, IReadOnlyList items) + { + Id = id; + Amount = amount; + ProducerMapNpcId = producerMapNpcId; + ProducerItemVnum = producerItemVnum; + ProducerNpcVnum = producerNpcVnum; + ProducedItemVnum = producedItemVnum; + Items = items; + } + + public int Id { get; } + public int Amount { get; } + public int? ProducerMapNpcId { get; } + public int? ProducerItemVnum { get; } + public int? ProducerNpcVnum { get; } + public int ProducedItemVnum { get; } + public IReadOnlyList Items { get; } +} + +public class RecipeOpenWindowEvent : PlayerEvent +{ + public RecipeOpenWindowEvent(int itemVnum) => ItemVnum = itemVnum; + + public int ItemVnum { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Relations/AddRelationEvent.cs b/srcs/WingsAPI.Game/Relations/AddRelationEvent.cs new file mode 100644 index 0000000..dd7a9e8 --- /dev/null +++ b/srcs/WingsAPI.Game/Relations/AddRelationEvent.cs @@ -0,0 +1,20 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsEmu.Game._packetHandling; +using WingsEmu.Packets.Enums.Relations; + +namespace WingsEmu.Game.Relations; + +public class AddRelationEvent : PlayerEvent +{ + public AddRelationEvent(long targetCharacterId, CharacterRelationType relationType) + { + TargetCharacterId = targetCharacterId; + RelationType = relationType; + } + + public long TargetCharacterId { get; } + public CharacterRelationType RelationType { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Relations/IInvitationManager.cs b/srcs/WingsAPI.Game/Relations/IInvitationManager.cs new file mode 100644 index 0000000..1ec2f12 --- /dev/null +++ b/srcs/WingsAPI.Game/Relations/IInvitationManager.cs @@ -0,0 +1,16 @@ +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Relations; + +public interface IInvitationManager +{ + bool ContainsPendingInvitation(long invitationSender, long invitationTarget, InvitationType type); + void AddPendingInvitation(long invitationSender, long invitationTarget, InvitationType type); + void RemovePendingInvitation(long invitationSender, long invitationTarget, InvitationType type); + + /// + /// Mainly used on disconnect + /// + /// + void RemoveAllPendingInvitations(long invitationSender); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Relations/IRelationComponent.cs b/srcs/WingsAPI.Game/Relations/IRelationComponent.cs new file mode 100644 index 0000000..f10aa1a --- /dev/null +++ b/srcs/WingsAPI.Game/Relations/IRelationComponent.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using WingsEmu.DTOs.Relations; +using WingsEmu.Packets.Enums.Relations; + +namespace WingsEmu.Game.Relations; + +public interface IRelationComponent +{ + public IReadOnlyList GetRelations(); + public IEnumerable GetFriendRelations(); + public IEnumerable GetBlockedRelations(); + + public bool IsBlocking(long targetId); + public bool IsFriend(long targetId); + public bool IsMarried(long targetId); + public bool IsFriendsListFull(); + public void AddRelation(CharacterRelationDTO relation); + public void RemoveRelation(long targetCharacterId, CharacterRelationType relationType); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Relations/InvitationManager.cs b/srcs/WingsAPI.Game/Relations/InvitationManager.cs new file mode 100644 index 0000000..5bc0bab --- /dev/null +++ b/srcs/WingsAPI.Game/Relations/InvitationManager.cs @@ -0,0 +1,107 @@ +using System.Collections.Generic; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Relations; + +public class InvitationManager : IInvitationManager +{ + // sender <--> type/targets + private readonly Dictionary> _senderInvitations = new(); + + // target <--> type/senders + private readonly Dictionary> _targetInvitations = new(); + + public bool ContainsPendingInvitation(long invitationSender, long invitationTarget, InvitationType type) + { + if (_senderInvitations.TryGetValue(invitationSender, out HashSet<(long, InvitationType)> senderDictionary)) + { + return senderDictionary != null && senderDictionary.Contains((invitationTarget, type)); + } + + if (!_targetInvitations.TryGetValue(invitationTarget, out HashSet<(long, InvitationType)> targetDictionary)) + { + return false; + } + + return targetDictionary != null && targetDictionary.Contains((invitationTarget, type)); + } + + public void AddPendingInvitation(long invitationSender, long invitationTarget, InvitationType type) + { + AddSenderInvitation(invitationSender, invitationTarget, type); + AddTargetInvitation(invitationSender, invitationTarget, type); + } + + public void RemovePendingInvitation(long invitationSender, long invitationTarget, InvitationType type) + { + RemoveSenderInvitation(invitationSender, invitationTarget, type); + RemoveTargetInvitation(invitationSender, invitationTarget, type); + } + + public void RemoveAllPendingInvitations(long invitationSender) + { + if (!_senderInvitations.TryGetValue(invitationSender, out HashSet<(long, InvitationType)> dictionary)) + { + return; + } + + foreach ((long target, InvitationType key) in dictionary) + { + RemoveTargetInvitation(invitationSender, target, key); + } + + dictionary.Clear(); + } + + private void AddSenderInvitation(long invitationSender, long invitationTarget, InvitationType type) + { + if (!_senderInvitations.TryGetValue(invitationSender, out HashSet<(long, InvitationType)> senderDictionary)) + { + senderDictionary = new HashSet<(long, InvitationType)>(); + _senderInvitations[invitationSender] = senderDictionary; + } + + if (senderDictionary.Contains((invitationTarget, type))) + { + return; + } + + senderDictionary.Add((invitationTarget, type)); + } + + private void AddTargetInvitation(long invitationSender, long invitationTarget, InvitationType type) + { + if (!_targetInvitations.TryGetValue(invitationTarget, out HashSet<(long, InvitationType)> targetDictionary)) + { + targetDictionary = new HashSet<(long, InvitationType)>(); + _targetInvitations[invitationTarget] = targetDictionary; + } + + if (targetDictionary.Contains((invitationSender, type))) + { + return; + } + + targetDictionary.Add((invitationSender, type)); + } + + private void RemoveSenderInvitation(long invitationSender, long invitationTarget, InvitationType type) + { + if (!_senderInvitations.TryGetValue(invitationSender, out HashSet<(long, InvitationType)> dictionary)) + { + return; + } + + dictionary.Remove((invitationTarget, type)); + } + + private void RemoveTargetInvitation(long invitationSender, long invitationTarget, InvitationType type) + { + if (!_targetInvitations.TryGetValue(invitationTarget, out HashSet<(long, InvitationType)> dictionary)) + { + return; + } + + dictionary.Remove((invitationSender, type)); + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Relations/RelationBlockEvent.cs b/srcs/WingsAPI.Game/Relations/RelationBlockEvent.cs new file mode 100644 index 0000000..6c3774c --- /dev/null +++ b/srcs/WingsAPI.Game/Relations/RelationBlockEvent.cs @@ -0,0 +1,8 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Relations; + +public class RelationBlockEvent : PlayerEvent +{ + public long CharacterId { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Relations/RelationComponent.cs b/srcs/WingsAPI.Game/Relations/RelationComponent.cs new file mode 100644 index 0000000..366f490 --- /dev/null +++ b/srcs/WingsAPI.Game/Relations/RelationComponent.cs @@ -0,0 +1,181 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using WingsEmu.DTOs.Relations; +using WingsEmu.Packets.Enums.Relations; + +namespace WingsEmu.Game.Relations; + +public class RelationComponent : IRelationComponent +{ + private const int FRIEND_LIST_LIMIT = 50; + + private readonly ReaderWriterLockSlim _lock = new(); + private readonly Dictionary> _relations = new(); + private readonly Dictionary _relationsByTargetCharacterId = new(); + + public IReadOnlyList GetRelations() + { + _lock.EnterReadLock(); + try + { + return _relations.Values.SelectMany(s => s).ToArray(); + } + finally + { + _lock.ExitReadLock(); + } + } + + public IEnumerable GetFriendRelations() + { + _lock.EnterReadLock(); + try + { + if (!_relations.TryGetValue(CharacterRelationType.Friend, out List relations)) + { + return Array.Empty(); + } + + return relations; + } + finally + { + _lock.ExitReadLock(); + } + } + + public IEnumerable GetBlockedRelations() + { + _lock.EnterReadLock(); + try + { + if (!_relations.TryGetValue(CharacterRelationType.Blocked, out List relations)) + { + return Array.Empty(); + } + + return relations; + } + finally + { + _lock.ExitReadLock(); + } + } + + public bool IsBlocking(long targetId) + { + _lock.EnterReadLock(); + try + { + if (!_relationsByTargetCharacterId.TryGetValue(targetId, out CharacterRelationDTO relationDto)) + { + return false; + } + + return relationDto.RelationType == CharacterRelationType.Blocked; + } + finally + { + _lock.ExitReadLock(); + } + } + + public bool IsFriend(long targetId) + { + _lock.EnterReadLock(); + try + { + if (!_relationsByTargetCharacterId.TryGetValue(targetId, out CharacterRelationDTO relationDto)) + { + return false; + } + + return relationDto.RelationType == CharacterRelationType.Friend; + } + finally + { + _lock.ExitReadLock(); + } + } + + public bool IsMarried(long targetId) + { + _lock.EnterReadLock(); + try + { + if (!_relationsByTargetCharacterId.TryGetValue(targetId, out CharacterRelationDTO relationDto)) + { + return false; + } + + return relationDto.RelationType == CharacterRelationType.Spouse; + } + finally + { + _lock.ExitReadLock(); + } + } + + public bool IsFriendsListFull() + { + _lock.EnterReadLock(); + try + { + if (!_relations.TryGetValue(CharacterRelationType.Friend, out List list) || list == null) + { + return false; + } + + return list.Count >= FRIEND_LIST_LIMIT; + } + finally + { + _lock.ExitReadLock(); + } + } + + public void AddRelation(CharacterRelationDTO relation) + { + _lock.EnterWriteLock(); + try + { + _relationsByTargetCharacterId.TryAdd(relation.RelatedCharacterId, relation); + if (!_relations.TryGetValue(relation.RelationType, out List relations)) + { + _relations[relation.RelationType] = relations = new List(); + } + + relations.Add(relation); + } + finally + { + _lock.ExitWriteLock(); + } + } + + public void RemoveRelation(long targetCharacterId, CharacterRelationType relationType) + { + _lock.EnterWriteLock(); + try + { + if (!_relationsByTargetCharacterId.TryGetValue(targetCharacterId, out CharacterRelationDTO relationDto)) + { + return; + } + + _relationsByTargetCharacterId.Remove(targetCharacterId, out _); + if (!_relations.TryGetValue(relationType, out List relations) || relations == null) + { + return; + } + + relations.Remove(relationDto); + } + finally + { + _lock.ExitWriteLock(); + } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Relations/RelationFriendEvent.cs b/srcs/WingsAPI.Game/Relations/RelationFriendEvent.cs new file mode 100644 index 0000000..5aa287c --- /dev/null +++ b/srcs/WingsAPI.Game/Relations/RelationFriendEvent.cs @@ -0,0 +1,10 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Game.Relations; + +public class RelationFriendEvent : PlayerEvent +{ + public FInsPacketType RequestType { get; init; } + public long CharacterId { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Relations/RemoveRelationEvent.cs b/srcs/WingsAPI.Game/Relations/RemoveRelationEvent.cs new file mode 100644 index 0000000..50e9326 --- /dev/null +++ b/srcs/WingsAPI.Game/Relations/RemoveRelationEvent.cs @@ -0,0 +1,16 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Packets.Enums.Relations; + +namespace WingsEmu.Game.Relations; + +public class RemoveRelationEvent : PlayerEvent +{ + public RemoveRelationEvent(long targetCharacterId, CharacterRelationType type) + { + TargetCharacterId = targetCharacterId; + Type = type; + } + + public long TargetCharacterId { get; } + public CharacterRelationType Type { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/RespawnReturn/Event/RespawnChangeEvent.cs b/srcs/WingsAPI.Game/RespawnReturn/Event/RespawnChangeEvent.cs new file mode 100644 index 0000000..133dfaf --- /dev/null +++ b/srcs/WingsAPI.Game/RespawnReturn/Event/RespawnChangeEvent.cs @@ -0,0 +1,8 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.RespawnReturn.Event; + +public class RespawnChangeEvent : PlayerEvent +{ + public int MapId { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/RespawnReturn/Event/RespawnPlayerEvent.cs b/srcs/WingsAPI.Game/RespawnReturn/Event/RespawnPlayerEvent.cs new file mode 100644 index 0000000..1b8577e --- /dev/null +++ b/srcs/WingsAPI.Game/RespawnReturn/Event/RespawnPlayerEvent.cs @@ -0,0 +1,7 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.RespawnReturn.Event; + +public class RespawnPlayerEvent : PlayerEvent +{ +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/RespawnReturn/Event/ReturnChangeEvent.cs b/srcs/WingsAPI.Game/RespawnReturn/Event/ReturnChangeEvent.cs new file mode 100644 index 0000000..b8473d9 --- /dev/null +++ b/srcs/WingsAPI.Game/RespawnReturn/Event/ReturnChangeEvent.cs @@ -0,0 +1,12 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.RespawnReturn.Event; + +public class ReturnChangeEvent : PlayerEvent +{ + public int MapId { get; init; } + public short MapX { get; init; } + public short MapY { get; init; } + + public bool IsByGroup { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/RespawnReturn/IHomeComponent.cs b/srcs/WingsAPI.Game/RespawnReturn/IHomeComponent.cs new file mode 100644 index 0000000..ebad4ef --- /dev/null +++ b/srcs/WingsAPI.Game/RespawnReturn/IHomeComponent.cs @@ -0,0 +1,42 @@ +using WingsAPI.Packets.Enums; +using WingsEmu.DTOs.Respawns; +using WingsEmu.Game._enum; + +namespace WingsEmu.Game.RespawnReturn; + +public interface IHomeComponent +{ + public RespawnType RespawnType { get; } + public Act5RespawnType Act5RespawnType { get; } + + public CharacterReturnDto Return { get; } + public void ChangeRespawn(RespawnType type); + public void ChangeAct5Respawn(Act5RespawnType type); + + public void ChangeReturn(CharacterReturnDto returnDto); +} + +public class HomeComponent : IHomeComponent +{ + public HomeComponent(CharacterReturnDto characterDtoReturn) => Return = characterDtoReturn ?? new CharacterReturnDto(); + + public RespawnType RespawnType { get; private set; } + public Act5RespawnType Act5RespawnType { get; private set; } + + public void ChangeRespawn(RespawnType type) + { + RespawnType = type; + } + + public void ChangeAct5Respawn(Act5RespawnType type) + { + Act5RespawnType = type; + } + + public CharacterReturnDto Return { get; private set; } + + public void ChangeReturn(CharacterReturnDto returnDto) + { + Return = returnDto; + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Revival/Act4KillEvent.cs b/srcs/WingsAPI.Game/Revival/Act4KillEvent.cs new file mode 100644 index 0000000..d7b71d8 --- /dev/null +++ b/srcs/WingsAPI.Game/Revival/Act4KillEvent.cs @@ -0,0 +1,8 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Revival; + +public class Act4KillEvent : PlayerEvent +{ + public long TargetId { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Revival/CharacterRevivalComponent.cs b/srcs/WingsAPI.Game/Revival/CharacterRevivalComponent.cs new file mode 100644 index 0000000..7f40228 --- /dev/null +++ b/srcs/WingsAPI.Game/Revival/CharacterRevivalComponent.cs @@ -0,0 +1,52 @@ +using System; + +namespace WingsEmu.Game.Revival; + +public interface ICharacterRevivalComponent +{ + public DateTime RevivalDateTimeForExecution { get; } + public RevivalType RevivalType { get; } + public ForcedType ForcedType { get; } + public DateTime AskRevivalDateTimeForExecution { get; } + public AskRevivalType AskRevivalType { get; } + public void UpdateRevival(DateTime revivalDateTimeForExecution, RevivalType revivalType, ForcedType forcedType); + public void DisableRevival(); + public void UpdateAskRevival(DateTime askRevivalDateTimeForExecution, AskRevivalType askRevivalType); + public void DisableAskRevival(); +} + +public class CharacterRevivalComponent : ICharacterRevivalComponent +{ + public DateTime RevivalDateTimeForExecution { get; private set; } = DateTime.MaxValue; + + public RevivalType RevivalType { get; private set; } + + public ForcedType ForcedType { get; private set; } + + public DateTime AskRevivalDateTimeForExecution { get; private set; } = DateTime.MaxValue; + + public AskRevivalType AskRevivalType { get; private set; } + + public void UpdateRevival(DateTime revivalDateTimeForExecution, RevivalType revivalType, ForcedType forcedType) + { + RevivalDateTimeForExecution = revivalDateTimeForExecution; + RevivalType = revivalType; + ForcedType = forcedType; + } + + public void DisableRevival() + { + RevivalDateTimeForExecution = DateTime.MaxValue; + } + + public void UpdateAskRevival(DateTime askRevivalDateTimeForExecution, AskRevivalType askRevivalType) + { + AskRevivalDateTimeForExecution = askRevivalDateTimeForExecution; + AskRevivalType = askRevivalType; + } + + public void DisableAskRevival() + { + AskRevivalDateTimeForExecution = DateTime.MaxValue; + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Revival/MateRevivalComponent.cs b/srcs/WingsAPI.Game/Revival/MateRevivalComponent.cs new file mode 100644 index 0000000..82e0c56 --- /dev/null +++ b/srcs/WingsAPI.Game/Revival/MateRevivalComponent.cs @@ -0,0 +1,28 @@ +using System; + +namespace WingsEmu.Game.Revival; + +public interface IMateRevivalComponent +{ + DateTime RevivalDateTimeForExecution { get; } + bool IsRevivalDelayed { get; } + void UpdateRevival(DateTime revivalDateTimeForExecution, bool isRevivalDelayed); + void DisableRevival(); +} + +public class MateRevivalComponent : IMateRevivalComponent +{ + public DateTime RevivalDateTimeForExecution { get; private set; } = DateTime.MaxValue; + public bool IsRevivalDelayed { get; private set; } + + public void UpdateRevival(DateTime revivalDateTimeForExecution, bool isRevivalDelayed) + { + RevivalDateTimeForExecution = revivalDateTimeForExecution; + IsRevivalDelayed = isRevivalDelayed; + } + + public void DisableRevival() + { + RevivalDateTimeForExecution = DateTime.MaxValue; + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Revival/RevivalAskEvent.cs b/srcs/WingsAPI.Game/Revival/RevivalAskEvent.cs new file mode 100644 index 0000000..7e76b53 --- /dev/null +++ b/srcs/WingsAPI.Game/Revival/RevivalAskEvent.cs @@ -0,0 +1,18 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Revival; + +public class RevivalAskEvent : PlayerEvent +{ + public RevivalAskEvent(AskRevivalType askRevivalType) => AskRevivalType = askRevivalType; + + public AskRevivalType AskRevivalType { get; } +} + +public enum AskRevivalType +{ + BasicRevival, + ArenaRevival, + DungeonRevival, + TimeSpaceRevival +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Revival/RevivalReviveEvent.cs b/srcs/WingsAPI.Game/Revival/RevivalReviveEvent.cs new file mode 100644 index 0000000..176a12a --- /dev/null +++ b/srcs/WingsAPI.Game/Revival/RevivalReviveEvent.cs @@ -0,0 +1,38 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Revival; + +public class RevivalReviveEvent : PlayerEvent +{ + public RevivalReviveEvent() + { + } + + public RevivalReviveEvent(RevivalType revivalType) => RevivalType = revivalType; + + public RevivalReviveEvent(RevivalType revivalType, ForcedType forced) + { + RevivalType = revivalType; + Forced = forced; + } + + public RevivalType RevivalType { get; set; } + + public ForcedType Forced { get; set; } +} + +public enum RevivalType +{ + TryPayRevival = 0, + DontPayRevival = 1, + TryPayArenaRevival = 2 +} + +public enum ForcedType +{ + NoForced, + Forced, + Reconnect, + Act4SealRevival, + HolyRevival +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Revival/RevivalStartProcedureEvent.cs b/srcs/WingsAPI.Game/Revival/RevivalStartProcedureEvent.cs new file mode 100644 index 0000000..ba4414c --- /dev/null +++ b/srcs/WingsAPI.Game/Revival/RevivalStartProcedureEvent.cs @@ -0,0 +1,10 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Game.Entities; + +namespace WingsEmu.Game.Revival; + +public class RevivalStartProcedureEvent : PlayerEvent +{ + public RevivalStartProcedureEvent(IBattleEntity killer) => Killer = killer; + public IBattleEntity Killer { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Ship/Configuration/IShipConfigurationProvider.cs b/srcs/WingsAPI.Game/Ship/Configuration/IShipConfigurationProvider.cs new file mode 100644 index 0000000..edd2754 --- /dev/null +++ b/srcs/WingsAPI.Game/Ship/Configuration/IShipConfigurationProvider.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; + +namespace WingsEmu.Game.Ship.Configuration; + +public interface IShipConfigurationProvider +{ + IReadOnlyList GetShips(); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Ship/Configuration/ShipConfiguration.cs b/srcs/WingsAPI.Game/Ship/Configuration/ShipConfiguration.cs new file mode 100644 index 0000000..5f7ca84 --- /dev/null +++ b/srcs/WingsAPI.Game/Ship/Configuration/ShipConfiguration.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using WingsEmu.Core; + +namespace WingsEmu.Game.Ship.Configuration; + +public class ShipConfiguration : List +{ +} + +public class Ship +{ + public ShipType ShipType { get; set; } + public long ShipCost { get; set; } + public byte ShipLevelRestriction { get; set; } + + public short ShipMapId { get; set; } + public Range ShipMapX { get; set; } + public Range ShipMapY { get; set; } + + public TimeSpan Departure { get; set; } + public List DepartureWarnings { get; set; } + + public int DestinationMapId { get; set; } + public Range DestinationMapX { get; set; } + public Range DestinationMapY { get; set; } +} + +public enum ShipType +{ + Act4Angels, + Act4Demons, + Act5 +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Ship/Event/ShipEnterEvent.cs b/srcs/WingsAPI.Game/Ship/Event/ShipEnterEvent.cs new file mode 100644 index 0000000..f481f62 --- /dev/null +++ b/srcs/WingsAPI.Game/Ship/Event/ShipEnterEvent.cs @@ -0,0 +1,11 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Game.Ship.Configuration; + +namespace WingsEmu.Game.Ship.Event; + +public class ShipEnterEvent : PlayerEvent +{ + public ShipEnterEvent(ShipType shipType) => ShipType = shipType; + + public ShipType ShipType { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Ship/Event/ShipLeaveEvent.cs b/srcs/WingsAPI.Game/Ship/Event/ShipLeaveEvent.cs new file mode 100644 index 0000000..f124541 --- /dev/null +++ b/srcs/WingsAPI.Game/Ship/Event/ShipLeaveEvent.cs @@ -0,0 +1,7 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Ship.Event; + +public class ShipLeaveEvent : PlayerEvent +{ +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Ship/Event/ShipProcessEvent.cs b/srcs/WingsAPI.Game/Ship/Event/ShipProcessEvent.cs new file mode 100644 index 0000000..316bef8 --- /dev/null +++ b/srcs/WingsAPI.Game/Ship/Event/ShipProcessEvent.cs @@ -0,0 +1,16 @@ +using System; +using PhoenixLib.Events; + +namespace WingsEmu.Game.Ship.Event; + +public class ShipProcessEvent : IAsyncEvent +{ + public ShipProcessEvent(ShipInstance shipInstance, DateTime currentTime) + { + ShipInstance = shipInstance; + CurrentTime = currentTime; + } + + public ShipInstance ShipInstance { get; } + public DateTime CurrentTime { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Ship/IShipManager.cs b/srcs/WingsAPI.Game/Ship/IShipManager.cs new file mode 100644 index 0000000..6e53b64 --- /dev/null +++ b/srcs/WingsAPI.Game/Ship/IShipManager.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using WingsEmu.Game.Ship.Configuration; + +namespace WingsEmu.Game.Ship; + +public interface IShipManager +{ + void AddShip(ShipInstance shipInstance); + ShipInstance GetShip(ShipType shipType); + IReadOnlyCollection GetShips(); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Ship/ShipInstance.cs b/srcs/WingsAPI.Game/Ship/ShipInstance.cs new file mode 100644 index 0000000..8c6d031 --- /dev/null +++ b/srcs/WingsAPI.Game/Ship/ShipInstance.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Ship.Configuration; + +namespace WingsEmu.Game.Ship; + +public class ShipInstance +{ + public ShipInstance(IMapInstance mapInstance, Configuration.Ship configuration, DateTime currentTime) + { + MapInstance = mapInstance; + Configuration = configuration; + DepartureWarnings = Configuration.DepartureWarnings.ToList(); + LastDeparture = currentTime; + } + + public ShipType ShipType => Configuration.ShipType; + public IMapInstance MapInstance { get; } + public Configuration.Ship Configuration { get; } + public List DepartureWarnings { get; set; } + public DateTime LastDeparture { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Shops/Event/BuyItemNpcShopEvent.cs b/srcs/WingsAPI.Game/Shops/Event/BuyItemNpcShopEvent.cs new file mode 100644 index 0000000..df0952d --- /dev/null +++ b/srcs/WingsAPI.Game/Shops/Event/BuyItemNpcShopEvent.cs @@ -0,0 +1,11 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Shops.Event; + +public class BuyItemNpcShopEvent : PlayerEvent +{ + public long OwnerId { get; set; } + public short Slot { get; set; } + public short Amount { get; set; } + public bool Accept { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Shops/Event/BuyShopItemEvent.cs b/srcs/WingsAPI.Game/Shops/Event/BuyShopItemEvent.cs new file mode 100644 index 0000000..24189e2 --- /dev/null +++ b/srcs/WingsAPI.Game/Shops/Event/BuyShopItemEvent.cs @@ -0,0 +1,10 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Shops.Event; + +public class BuyShopItemEvent : PlayerEvent +{ + public long OwnerId { get; set; } + public short Slot { get; set; } + public short Amount { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Shops/Event/BuyShopSkillEvent.cs b/srcs/WingsAPI.Game/Shops/Event/BuyShopSkillEvent.cs new file mode 100644 index 0000000..972712b --- /dev/null +++ b/srcs/WingsAPI.Game/Shops/Event/BuyShopSkillEvent.cs @@ -0,0 +1,10 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Shops.Event; + +public class BuyShopSkillEvent : PlayerEvent +{ + public long OwnerId { get; set; } + public short Slot { get; set; } + public bool Accept { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Shops/Event/CurrencyType.cs b/srcs/WingsAPI.Game/Shops/Event/CurrencyType.cs new file mode 100644 index 0000000..af39bdc --- /dev/null +++ b/srcs/WingsAPI.Game/Shops/Event/CurrencyType.cs @@ -0,0 +1,7 @@ +namespace WingsEmu.Game.Shops.Event; + +public enum CurrencyType +{ + GOLD, + REPUTATION +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Shops/Event/ShopClosedEvent.cs b/srcs/WingsAPI.Game/Shops/Event/ShopClosedEvent.cs new file mode 100644 index 0000000..1ad89fb --- /dev/null +++ b/srcs/WingsAPI.Game/Shops/Event/ShopClosedEvent.cs @@ -0,0 +1,7 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Shops.Event; + +public class ShopClosedEvent : PlayerEvent +{ +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Shops/Event/ShopNpcBoughtItemEvent.cs b/srcs/WingsAPI.Game/Shops/Event/ShopNpcBoughtItemEvent.cs new file mode 100644 index 0000000..e973d0c --- /dev/null +++ b/srcs/WingsAPI.Game/Shops/Event/ShopNpcBoughtItemEvent.cs @@ -0,0 +1,13 @@ +using WingsEmu.DTOs.Items; +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Shops.Event; + +public class ShopNpcBoughtItemEvent : PlayerEvent +{ + public long SellerId { get; init; } + public CurrencyType CurrencyType { get; init; } + public long TotalPrice { get; init; } + public ItemInstanceDTO ItemInstance { get; init; } + public int Quantity { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Shops/Event/ShopNpcListItemsEvent.cs b/srcs/WingsAPI.Game/Shops/Event/ShopNpcListItemsEvent.cs new file mode 100644 index 0000000..a2c584d --- /dev/null +++ b/srcs/WingsAPI.Game/Shops/Event/ShopNpcListItemsEvent.cs @@ -0,0 +1,9 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Shops.Event; + +public class ShopNpcListItemsEvent : PlayerEvent +{ + public int NpcId { get; set; } + public byte ShopType { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Shops/Event/ShopNpcSoldItemEvent.cs b/srcs/WingsAPI.Game/Shops/Event/ShopNpcSoldItemEvent.cs new file mode 100644 index 0000000..8fc0375 --- /dev/null +++ b/srcs/WingsAPI.Game/Shops/Event/ShopNpcSoldItemEvent.cs @@ -0,0 +1,11 @@ +using WingsEmu.DTOs.Items; +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Shops.Event; + +public class ShopNpcSoldItemEvent : PlayerEvent +{ + public ItemInstanceDTO ItemInstance { get; init; } + public short Amount { get; init; } + public long PricePerItem { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Shops/Event/ShopOpenedEvent.cs b/srcs/WingsAPI.Game/Shops/Event/ShopOpenedEvent.cs new file mode 100644 index 0000000..ffc3ddd --- /dev/null +++ b/srcs/WingsAPI.Game/Shops/Event/ShopOpenedEvent.cs @@ -0,0 +1,8 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Shops.Event; + +public class ShopOpenedEvent : PlayerEvent +{ + public string ShopName { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Shops/Event/ShopPlayerBoughtItemEvent.cs b/srcs/WingsAPI.Game/Shops/Event/ShopPlayerBoughtItemEvent.cs new file mode 100644 index 0000000..8f75dae --- /dev/null +++ b/srcs/WingsAPI.Game/Shops/Event/ShopPlayerBoughtItemEvent.cs @@ -0,0 +1,13 @@ +using WingsEmu.DTOs.Items; +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Shops.Event; + +public class ShopPlayerBoughtItemEvent : PlayerEvent +{ + public long SellerId { get; init; } + public string SellerName { get; init; } + public long TotalPrice { get; init; } + public int Quantity { get; init; } + public ItemInstanceDTO ItemInstance { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Shops/Event/ShopPlayerBuyItemEvent.cs b/srcs/WingsAPI.Game/Shops/Event/ShopPlayerBuyItemEvent.cs new file mode 100644 index 0000000..36af5c2 --- /dev/null +++ b/srcs/WingsAPI.Game/Shops/Event/ShopPlayerBuyItemEvent.cs @@ -0,0 +1,10 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Shops.Event; + +public class ShopPlayerBuyItemEvent : PlayerEvent +{ + public long OwnerId { get; init; } + public short Slot { get; init; } + public short Amount { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Shops/Event/ShopPlayerCloseEvent.cs b/srcs/WingsAPI.Game/Shops/Event/ShopPlayerCloseEvent.cs new file mode 100644 index 0000000..fd6e42d --- /dev/null +++ b/srcs/WingsAPI.Game/Shops/Event/ShopPlayerCloseEvent.cs @@ -0,0 +1,7 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Shops.Event; + +public class ShopPlayerCloseEvent : PlayerEvent +{ +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Shops/Event/ShopPlayerOpenEvent.cs b/srcs/WingsAPI.Game/Shops/Event/ShopPlayerOpenEvent.cs new file mode 100644 index 0000000..42ffab1 --- /dev/null +++ b/srcs/WingsAPI.Game/Shops/Event/ShopPlayerOpenEvent.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Shops.Event; + +public class ShopPlayerOpenEvent : PlayerEvent +{ + public List Items { get; init; } + + public string ShopTitle { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Shops/Event/ShopSkillBoughtEvent.cs b/srcs/WingsAPI.Game/Shops/Event/ShopSkillBoughtEvent.cs new file mode 100644 index 0000000..73ee08b --- /dev/null +++ b/srcs/WingsAPI.Game/Shops/Event/ShopSkillBoughtEvent.cs @@ -0,0 +1,8 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Shops.Event; + +public class ShopSkillBoughtEvent : PlayerEvent +{ + public short SkillVnum { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Shops/Event/ShopSkillSoldEvent.cs b/srcs/WingsAPI.Game/Shops/Event/ShopSkillSoldEvent.cs new file mode 100644 index 0000000..166ae2e --- /dev/null +++ b/srcs/WingsAPI.Game/Shops/Event/ShopSkillSoldEvent.cs @@ -0,0 +1,8 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Shops.Event; + +public class ShopSkillSoldEvent : PlayerEvent +{ + public int SkillVnum { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Shops/ShopComponent.cs b/srcs/WingsAPI.Game/Shops/ShopComponent.cs new file mode 100644 index 0000000..9966595 --- /dev/null +++ b/srcs/WingsAPI.Game/Shops/ShopComponent.cs @@ -0,0 +1,57 @@ +using System.Collections.Generic; +using System.Linq; + +namespace WingsEmu.Game.Shops; + +public interface IShopComponent +{ + IEnumerable Items { get; } + string Name { get; set; } + long Sell { get; set; } + ShopPlayerItem GetItem(short slot); + void AddShop(IEnumerable items); + void RemoveShopItem(ShopPlayerItem shopPlayerItem); + void RemoveShop(); +} + +public class ShopComponent : IShopComponent +{ + private ShopPlayerItem[] _items; + public string Name { get; set; } + + public long Sell { get; set; } + public IEnumerable Items => _items; + + public ShopPlayerItem GetItem(short slot) => _items?[slot] == null ? null : _items[slot]; + + public void RemoveShopItem(ShopPlayerItem shopPlayerItem) + { + if (_items == null) + { + return; + } + + if (shopPlayerItem == null) + { + return; + } + + _items[shopPlayerItem.ShopSlot] = null; + } + + public void AddShop(IEnumerable items) + { + _items = new ShopPlayerItem[20]; + foreach (ShopPlayerItem item in items.Where(item => item != null)) + { + _items[item.ShopSlot] = item; + } + } + + public void RemoveShop() + { + _items = null; + Name = null; + Sell = 0; + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Shops/ShopNpc.cs b/srcs/WingsAPI.Game/Shops/ShopNpc.cs new file mode 100644 index 0000000..5af162b --- /dev/null +++ b/srcs/WingsAPI.Game/Shops/ShopNpc.cs @@ -0,0 +1,28 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using WingsEmu.DTOs.Shops; + +namespace WingsEmu.Game.Shops; + +public class ShopNpc +{ + public ShopNpc(int mapNpcId, ShopNpcMenuType menuType, string name, byte shopType, IReadOnlyList shopItems, IReadOnlyList shopSkills) + { + MapNpcId = mapNpcId; + MenuType = menuType; + Name = name; + ShopType = shopType; + ShopItems = shopItems; + ShopSkills = shopSkills; + } + + public int MapNpcId { get; } + public ShopNpcMenuType MenuType { get; } + public string Name { get; } + public byte ShopType { get; } + public IReadOnlyList ShopItems { get; } + public IReadOnlyList ShopSkills { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Shops/ShopNpcMenuType.cs b/srcs/WingsAPI.Game/Shops/ShopNpcMenuType.cs new file mode 100644 index 0000000..caffa55 --- /dev/null +++ b/srcs/WingsAPI.Game/Shops/ShopNpcMenuType.cs @@ -0,0 +1,9 @@ +namespace WingsEmu.Game.Shops; + +public enum ShopNpcMenuType +{ + ITEMS = 0, + SKILLS = 1, + MINILAND = 4, + FAMILIES = 5 +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Shops/ShopPlayerItem.cs b/srcs/WingsAPI.Game/Shops/ShopPlayerItem.cs new file mode 100644 index 0000000..117655a --- /dev/null +++ b/srcs/WingsAPI.Game/Shops/ShopPlayerItem.cs @@ -0,0 +1,18 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsEmu.Game.Inventory; + +namespace WingsEmu.Game.Shops; + +public class ShopPlayerItem +{ + public InventoryItem InventoryItem { get; set; } + + public long PricePerUnit { get; set; } + + public short SellAmount { get; set; } + + public short ShopSlot { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Skills/AngelElementBuffComponent.cs b/srcs/WingsAPI.Game/Skills/AngelElementBuffComponent.cs new file mode 100644 index 0000000..a4ed9e9 --- /dev/null +++ b/srcs/WingsAPI.Game/Skills/AngelElementBuffComponent.cs @@ -0,0 +1,18 @@ +using WingsEmu.Game.Battle; + +namespace WingsEmu.Game.Skills; + +public class AngelElementBuffComponent : IAngelElementBuffComponent +{ + public ElementType? AngelElement { get; private set; } + + public void AddAngelElement(ElementType elementType) + { + AngelElement = elementType; + } + + public void RemoveAngelElement() + { + AngelElement = null; + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Skills/CharacterSkill.cs b/srcs/WingsAPI.Game/Skills/CharacterSkill.cs new file mode 100644 index 0000000..bfb5f28 --- /dev/null +++ b/srcs/WingsAPI.Game/Skills/CharacterSkill.cs @@ -0,0 +1,37 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Generic; +using System.Linq; +using WingsEmu.DTOs.Skills; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Game.Skills; + +public static class CharacterSkillsExtensions +{ + public static IEnumerable GetSkills(this IPlayerEntity character) => character.UseSp ? character.SkillsSp.Values.ToArray() : character.CharacterSkills.Values.ToArray(); + + public static string GenerateSkillCooldownResetPacket(this IClientSession session, int castId) => $"sr {castId}"; + + public static string GenerateSkillCooldownResetAfter(this IClientSession session, short castId, short time) => $"sr -10 {castId} {time}"; + public static void SendSkillCooldownResetAfter(this IClientSession session, short castId, short time) => session.SendPacket(session.GenerateSkillCooldownResetAfter(castId, time)); + + public static void SendSkillCooldownReset(this IClientSession session, int castId) => session.SendPacket(session.GenerateSkillCooldownResetPacket(castId)); +} + +public class CharacterSkill : CharacterSkillDTO, IBattleEntitySkill +{ + private SkillDTO _skill; + + public CharacterSkill() => LastUse = DateTime.UtcNow.AddHours(-1); + + public DateTime LastUse { get; set; } + public short Rate { get; } = 100; + + public SkillDTO Skill => _skill ??= StaticSkillsManager.Instance.GetSkill(SkillVNum); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Skills/ComboSkillComponent.cs b/srcs/WingsAPI.Game/Skills/ComboSkillComponent.cs new file mode 100644 index 0000000..23fca37 --- /dev/null +++ b/srcs/WingsAPI.Game/Skills/ComboSkillComponent.cs @@ -0,0 +1,32 @@ +namespace WingsEmu.Game.Skills; + +public class ComboSkillComponent : IComboSkillComponent +{ + private ComboSkillState _comboSkillState; + + public void SaveComboSkill(ComboSkillState comboSkillState) => _comboSkillState = comboSkillState; + + public ComboSkillState GetComboState() => _comboSkillState; + + public void IncreaseComboState(byte castId) + { + if (_comboSkillState == null) + { + return; + } + + _comboSkillState.State++; + _comboSkillState.LastSkillByCastId = castId; + } + + public void CleanComboState() => _comboSkillState = null; +} + +public class ComboSkillState +{ + public byte State { get; set; } + + public byte OriginalSkillCastId { get; set; } + + public byte LastSkillByCastId { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Skills/IAngelElementBuffComponent.cs b/srcs/WingsAPI.Game/Skills/IAngelElementBuffComponent.cs new file mode 100644 index 0000000..2c2d6b6 --- /dev/null +++ b/srcs/WingsAPI.Game/Skills/IAngelElementBuffComponent.cs @@ -0,0 +1,10 @@ +using WingsEmu.Game.Battle; + +namespace WingsEmu.Game.Skills; + +public interface IAngelElementBuffComponent +{ + public ElementType? AngelElement { get; } + public void AddAngelElement(ElementType elementType); + public void RemoveAngelElement(); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Skills/IBattleEntitySkill.cs b/srcs/WingsAPI.Game/Skills/IBattleEntitySkill.cs new file mode 100644 index 0000000..a6b6bf4 --- /dev/null +++ b/srcs/WingsAPI.Game/Skills/IBattleEntitySkill.cs @@ -0,0 +1,11 @@ +using System; +using WingsEmu.DTOs.Skills; + +namespace WingsEmu.Game.Skills; + +public interface IBattleEntitySkill +{ + SkillDTO Skill { get; } + DateTime LastUse { get; set; } + short Rate { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Skills/IComboSkillComponent.cs b/srcs/WingsAPI.Game/Skills/IComboSkillComponent.cs new file mode 100644 index 0000000..939b051 --- /dev/null +++ b/srcs/WingsAPI.Game/Skills/IComboSkillComponent.cs @@ -0,0 +1,9 @@ +namespace WingsEmu.Game.Skills; + +public interface IComboSkillComponent +{ + public void SaveComboSkill(ComboSkillState comboSkillState); + public ComboSkillState GetComboState(); + public void IncreaseComboState(byte castId); + public void CleanComboState(); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Skills/IEndBuffDamageComponent.cs b/srcs/WingsAPI.Game/Skills/IEndBuffDamageComponent.cs new file mode 100644 index 0000000..61de268 --- /dev/null +++ b/srcs/WingsAPI.Game/Skills/IEndBuffDamageComponent.cs @@ -0,0 +1,68 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; + +namespace WingsEmu.Game.Skills; + +public interface IEndBuffDamageComponent +{ + public IReadOnlyDictionary EndBuffDamages { get; } + + public void AddEndBuff(short buffVnum, int damage); + public int DecreaseDamageEndBuff(short buffVnum, int damage); + public void RemoveEndBuffDamage(short buffVnum); +} + +public class EndBuffDamageComponent : IEndBuffDamageComponent +{ + private readonly ConcurrentDictionary _buffs = new(); + private readonly ReaderWriterLockSlim _lock = new(LockRecursionPolicy.SupportsRecursion); + + public IReadOnlyDictionary EndBuffDamages => _buffs; + + public void AddEndBuff(short buffVnum, int damage) + { + _lock.EnterWriteLock(); + try + { + _buffs.TryAdd(buffVnum, damage); + } + finally + { + _lock.ExitWriteLock(); + } + } + + public int DecreaseDamageEndBuff(short buffVnum, int damage) + { + _lock.EnterWriteLock(); + try + { + if (!_buffs.TryGetValue(buffVnum, out int damageCounter)) + { + return 0; + } + + damageCounter -= damage; + _buffs[buffVnum] = damageCounter; + return damageCounter; + } + finally + { + _lock.ExitWriteLock(); + } + } + + public void RemoveEndBuffDamage(short buffVnum) + { + _lock.EnterWriteLock(); + try + { + _buffs.TryRemove(buffVnum, out _); + } + finally + { + _lock.ExitWriteLock(); + } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Skills/IScoutComponent.cs b/srcs/WingsAPI.Game/Skills/IScoutComponent.cs new file mode 100644 index 0000000..464af58 --- /dev/null +++ b/srcs/WingsAPI.Game/Skills/IScoutComponent.cs @@ -0,0 +1,31 @@ +namespace WingsEmu.Game.Skills; + +public interface IScoutComponent +{ + public ScoutStateType ScoutStateType { get; } + public void ChangeScoutState(ScoutStateType stateType); +} + +public class ScoutComponent : IScoutComponent +{ + public ScoutComponent() => ScoutStateType = ScoutStateType.None; + + public ScoutStateType ScoutStateType { get; private set; } + + public void ChangeScoutState(ScoutStateType stateType) + { + if (ScoutStateType == stateType) + { + return; + } + + ScoutStateType = stateType; + } +} + +public enum ScoutStateType : byte +{ + None = 0, + FirstState = 1, + SecondState = 2 +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Skills/ISkillComponent.cs b/srcs/WingsAPI.Game/Skills/ISkillComponent.cs new file mode 100644 index 0000000..a352303 --- /dev/null +++ b/srcs/WingsAPI.Game/Skills/ISkillComponent.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; + +namespace WingsEmu.Game.Skills; + +public interface ISkillComponent +{ + public bool CanBeInterrupted { get; set; } + public bool IsSkillInterrupted { get; set; } + public DateTime? SendTeleportPacket { get; set; } + public long? BombEntityId { get; set; } + public DateTime? ResetSkillCooldowns { get; set; } + public DateTime? ResetSpSkillCooldowns { get; set; } + public byte Zoom { get; set; } + public Dictionary> SkillUpgrades { get; } + public ConcurrentDictionary SkillsResets { get; } + public int? LastSkillVnum { get; set; } +} + +public class SkillComponent : ISkillComponent +{ + public bool CanBeInterrupted { get; set; } + public bool IsSkillInterrupted { get; set; } + public DateTime? SendTeleportPacket { get; set; } + public long? BombEntityId { get; set; } + public DateTime? ResetSkillCooldowns { get; set; } + public DateTime? ResetSpSkillCooldowns { get; set; } + public byte Zoom { get; set; } + public Dictionary> SkillUpgrades { get; } = new(); + public ConcurrentDictionary SkillsResets { get; } = new(); + public int? LastSkillVnum { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Skills/ISkillCooldownComponent.cs b/srcs/WingsAPI.Game/Skills/ISkillCooldownComponent.cs new file mode 100644 index 0000000..71d6a1a --- /dev/null +++ b/srcs/WingsAPI.Game/Skills/ISkillCooldownComponent.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Concurrent; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Skills; + +public interface ISkillCooldownComponent +{ + public ConcurrentQueue<(DateTime time, short castId)> SkillCooldowns { get; } + public ConcurrentQueue<(DateTime time, short castId, MateType mateType)> MatesSkillCooldowns { get; } + + public void AddSkillCooldown(DateTime time, short castId); + public void ClearSkillCooldowns(); + + public void AddMateSkillCooldown(DateTime time, short castId, MateType mateType); + public void ClearMateSkillCooldowns(); +} + +public interface IEntitySkillFactory +{ + INpcMonsterSkill CreateNpcMonsterSkill(int skillVnum, short rate, bool isBasicAttack, bool isIgnoringHitChance); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Skills/ISpyOutManager.cs b/srcs/WingsAPI.Game/Skills/ISpyOutManager.cs new file mode 100644 index 0000000..d52d347 --- /dev/null +++ b/srcs/WingsAPI.Game/Skills/ISpyOutManager.cs @@ -0,0 +1,11 @@ +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Skills; + +public interface ISpyOutManager +{ + void AddSpyOutSkill(long characterId, long targetId, VisualType targetType); + bool ContainsSpyOut(long characterId); + (long targetId, VisualType targetType) GetSpyOutTarget(long characterId); + void RemoveSpyOutSkill(long characterId); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Skills/IWildKeeperComponent.cs b/srcs/WingsAPI.Game/Skills/IWildKeeperComponent.cs new file mode 100644 index 0000000..9908d96 --- /dev/null +++ b/srcs/WingsAPI.Game/Skills/IWildKeeperComponent.cs @@ -0,0 +1,73 @@ +using System.Threading; + +namespace WingsEmu.Game.Skills; + +public interface IWildKeeperComponent +{ + public void IncreaseElementStacks(); + public void DecreaseElementStacks(); + public void ResetElementStacks(); + public short ElementStacks(); +} + +public class WildKeeperComponent : IWildKeeperComponent +{ + private readonly ReaderWriterLockSlim _lock = new(); + private short _elementsCounter; + + public void IncreaseElementStacks() + { + _lock.EnterWriteLock(); + try + { + _elementsCounter++; + } + finally + { + _lock.ExitWriteLock(); + } + } + + public void DecreaseElementStacks() + { + _lock.EnterWriteLock(); + try + { + _elementsCounter--; + if (_elementsCounter < 0) + { + _elementsCounter = 0; + } + } + finally + { + _lock.ExitWriteLock(); + } + } + + public void ResetElementStacks() + { + _lock.EnterWriteLock(); + try + { + _elementsCounter = 0; + } + finally + { + _lock.ExitWriteLock(); + } + } + + public short ElementStacks() + { + _lock.EnterReadLock(); + try + { + return _elementsCounter; + } + finally + { + _lock.ExitReadLock(); + } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Skills/NpcMonsterSkill.cs b/srcs/WingsAPI.Game/Skills/NpcMonsterSkill.cs new file mode 100644 index 0000000..67edf77 --- /dev/null +++ b/srcs/WingsAPI.Game/Skills/NpcMonsterSkill.cs @@ -0,0 +1,32 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using WingsEmu.DTOs.Skills; + +namespace WingsEmu.Game.Skills; + +public interface INpcMonsterSkill : IBattleEntitySkill +{ + bool IsBasicAttack { get; } + bool IsIgnoringHitChance { get; } +} + +public class NpcMonsterSkill : INpcMonsterSkill +{ + public NpcMonsterSkill(SkillDTO skill, short rate, bool isBasicAttack, bool isIgnoringHitChance) + { + Skill = skill; + Rate = rate; + IsBasicAttack = isBasicAttack; + IsIgnoringHitChance = isIgnoringHitChance; + } + + public short Rate { get; } + public bool IsIgnoringHitChance { get; } + public bool IsBasicAttack { get; } + + public DateTime LastUse { get; set; } = DateTime.MinValue; + public SkillDTO Skill { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Skills/PartnerSkill.cs b/srcs/WingsAPI.Game/Skills/PartnerSkill.cs new file mode 100644 index 0000000..b0b63e0 --- /dev/null +++ b/srcs/WingsAPI.Game/Skills/PartnerSkill.cs @@ -0,0 +1,15 @@ +using System; +using WingsEmu.DTOs.Skills; +using WingsEmu.Game.Managers.StaticData; + +namespace WingsEmu.Game.Skills; + +public class PartnerSkill : PartnerSkillDTO, IBattleEntitySkill +{ + private SkillDTO _skill; + + public DateTime LastUse { get; set; } = DateTime.MinValue; + public short Rate { get; } = 100; + + public SkillDTO Skill => _skill ??= StaticSkillsManager.Instance.GetSkill(SkillId); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Skills/SkillCooldownComponent.cs b/srcs/WingsAPI.Game/Skills/SkillCooldownComponent.cs new file mode 100644 index 0000000..47b70a0 --- /dev/null +++ b/srcs/WingsAPI.Game/Skills/SkillCooldownComponent.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Concurrent; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Skills; + +public class SkillCooldownComponent : ISkillCooldownComponent +{ + public SkillCooldownComponent() + { + SkillCooldowns = new ConcurrentQueue<(DateTime time, short castId)>(); + MatesSkillCooldowns = new ConcurrentQueue<(DateTime time, short castId, MateType mateType)>(); + } + + public ConcurrentQueue<(DateTime time, short castId)> SkillCooldowns { get; } + public ConcurrentQueue<(DateTime time, short castId, MateType mateType)> MatesSkillCooldowns { get; } + + public void AddSkillCooldown(DateTime time, short castId) + { + SkillCooldowns.Enqueue((time, castId)); + } + + public void ClearSkillCooldowns() + { + SkillCooldowns.Clear(); + } + + public void AddMateSkillCooldown(DateTime time, short castId, MateType mateType) + { + MatesSkillCooldowns.Enqueue((time, castId, mateType)); + } + + public void ClearMateSkillCooldowns() + { + MatesSkillCooldowns.Clear(); + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Skills/SpyOutManager.cs b/srcs/WingsAPI.Game/Skills/SpyOutManager.cs new file mode 100644 index 0000000..ee87676 --- /dev/null +++ b/srcs/WingsAPI.Game/Skills/SpyOutManager.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using WingsEmu.Core.Extensions; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Skills; + +public class SpyOutManager : ISpyOutManager +{ + private readonly Dictionary _spyOut = new(); + + public void AddSpyOutSkill(long characterId, long targetId, VisualType targetType) + { + _spyOut[characterId] = (targetId, targetType); + } + + public bool ContainsSpyOut(long characterId) => _spyOut.ContainsKey(characterId); + + public (long targetId, VisualType targetType) GetSpyOutTarget(long characterId) => _spyOut.GetOrDefault(characterId); + + public void RemoveSpyOutSkill(long characterId) + { + if (!ContainsSpyOut(characterId)) + { + return; + } + + _spyOut.Remove(characterId); + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/SnackFood/AdditionalFoodProgress.cs b/srcs/WingsAPI.Game/SnackFood/AdditionalFoodProgress.cs new file mode 100644 index 0000000..37fb2ee --- /dev/null +++ b/srcs/WingsAPI.Game/SnackFood/AdditionalFoodProgress.cs @@ -0,0 +1,17 @@ +using System; + +namespace WingsEmu.Game.SnackFood; + +public class AdditionalFoodProgress +{ + public int HpCap { get; set; } + public int MpCap { get; set; } + + public int FoodAdditionalHpBuffer { get; set; } + public int FoodAdditionalMpBuffer { get; set; } + + public int FoodAdditionalHpBufferSize { get; set; } + public int FoodAdditionalMpBufferSize { get; set; } + + public DateTime LastTick { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/SnackFood/AdditionalSnackProgress.cs b/srcs/WingsAPI.Game/SnackFood/AdditionalSnackProgress.cs new file mode 100644 index 0000000..26bb884 --- /dev/null +++ b/srcs/WingsAPI.Game/SnackFood/AdditionalSnackProgress.cs @@ -0,0 +1,17 @@ +using System; + +namespace WingsEmu.Game.SnackFood; + +public class AdditionalSnackProgress +{ + public int HpCap { get; set; } + public int MpCap { get; set; } + + public int SnackAdditionalHpBuffer { get; set; } + public int SnackAdditionalMpBuffer { get; set; } + + public int SnackAdditionalHpBufferSize { get; set; } + public int SnackAdditionalMpBufferSize { get; set; } + + public DateTime LastTick { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/SnackFood/Events/AddAdditionalHpMpEvent.cs b/srcs/WingsAPI.Game/SnackFood/Events/AddAdditionalHpMpEvent.cs new file mode 100644 index 0000000..b04dbc9 --- /dev/null +++ b/srcs/WingsAPI.Game/SnackFood/Events/AddAdditionalHpMpEvent.cs @@ -0,0 +1,12 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.SnackFood.Events; + +public class AddAdditionalHpMpEvent : PlayerEvent +{ + public int Hp { get; init; } + public int Mp { get; init; } + + public int MaxHpPercentage { get; set; } + public int MaxMpPercentage { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/SnackFood/FoodProgress.cs b/srcs/WingsAPI.Game/SnackFood/FoodProgress.cs new file mode 100644 index 0000000..5c1f393 --- /dev/null +++ b/srcs/WingsAPI.Game/SnackFood/FoodProgress.cs @@ -0,0 +1,21 @@ +using System; + +namespace WingsEmu.Game.SnackFood; + +public class FoodProgress +{ + public int FoodHpBuffer { get; set; } + public int FoodMpBuffer { get; set; } + + public int FoodHpBufferSize { get; set; } + public int FoodMpBufferSize { get; set; } + + public int FoodSpBuffer { get; set; } + public int FoodSpBufferSize { get; set; } + + public int FoodMateMaxHpBuffer { get; set; } + public int FoodMateMaxHpBufferSize { get; set; } + + public DateTime LastTick { get; set; } + public int IncreaseTick { get; set; } = 1; +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/SnackFood/FoodSnackComponent.cs b/srcs/WingsAPI.Game/SnackFood/FoodSnackComponent.cs new file mode 100644 index 0000000..bde6b57 --- /dev/null +++ b/srcs/WingsAPI.Game/SnackFood/FoodSnackComponent.cs @@ -0,0 +1,177 @@ +using System; +using WingsEmu.Game.Items; + +namespace WingsEmu.Game.SnackFood; + +public class FoodSnackComponent : IFoodSnackComponent +{ + private readonly SnackFoodConfiguration _configuration; + + public FoodSnackComponent(SnackFoodConfiguration configuration) => _configuration = configuration; + + public FoodProgress GetFoodProgress { get; private set; } = new(); + + public SnackProgress GetSnackProgress { get; private set; } = new(); + + public AdditionalSnackProgress GetAdditionalSnackProgress { get; private set; } = new(); + + public AdditionalFoodProgress GetAdditionalFoodProgress { get; private set; } = new(); + + public bool AddSnack(IGameItem gameItem) + { + SnackProgress progress = GetSnackProgress ??= new SnackProgress(); + + if (progress.SnackHpBuffer >= _configuration.SnackSoftCap || progress.SnackMpBuffer >= _configuration.SnackSoftCap || progress.SnackSpBuffer >= _configuration.SnackSoftCap) + { + return true; + } + + progress.SnackHpBuffer += gameItem.Hp; + progress.SnackHpBufferSize = progress.SnackHpBuffer; + progress.SnackMpBuffer += gameItem.Mp; + progress.SnackMpBufferSize = progress.SnackMpBuffer; + progress.SnackSpBuffer += gameItem.Data[4]; + progress.SnackSpBufferSize = progress.SnackSpBuffer; + + if (progress.SnackHpBuffer >= _configuration.SnackHardCap) + { + progress.SnackHpBuffer = _configuration.SnackHardCap; + progress.SnackHpBufferSize = _configuration.SnackHardCap; + } + + if (progress.SnackMpBuffer >= _configuration.SnackHardCap) + { + progress.SnackMpBuffer = _configuration.SnackHardCap; + progress.SnackMpBufferSize = _configuration.SnackHardCap; + } + + if (progress.SnackSpBuffer >= _configuration.SnackHardCap) + { + progress.SnackSpBuffer = _configuration.SnackHardCap; + progress.SnackSpBufferSize = _configuration.SnackHardCap; + } + + return false; + } + + public void AddAdditionalSnack(int max, int amount, bool isHp, int cap = 100) + { + AdditionalSnackProgress additionalProgress = GetAdditionalSnackProgress ??= new AdditionalSnackProgress(); + + if (isHp) + { + int hpCap = (int)Math.Round(max * cap / 100.0); + + if (additionalProgress.HpCap != 0 && additionalProgress.HpCap > cap) + { + return; + } + + additionalProgress.SnackAdditionalHpBuffer += amount; + additionalProgress.SnackAdditionalHpBufferSize = additionalProgress.SnackAdditionalHpBuffer; + additionalProgress.HpCap = (int)Math.Round(hpCap * 100.0 / max); + + return; + } + + int mpCap = (int)Math.Round(max * cap / 100.0); + + if (additionalProgress.MpCap != 0 && additionalProgress.MpCap > cap) + { + return; + } + + additionalProgress.SnackAdditionalMpBuffer += amount; + additionalProgress.SnackAdditionalMpBufferSize = additionalProgress.SnackAdditionalMpBuffer; + additionalProgress.MpCap = (int)Math.Round(mpCap * 100.0 / max); + } + + public bool AddFood(IGameItem gameItem) + { + FoodProgress progress = GetFoodProgress ??= new FoodProgress(); + + if (progress.FoodHpBuffer >= _configuration.FoodSoftCap || progress.FoodMpBuffer >= _configuration.FoodSoftCap || progress.FoodSpBuffer >= _configuration.FoodSoftCap) + { + return true; + } + + progress.FoodHpBuffer += gameItem.Hp; + progress.FoodHpBufferSize = progress.FoodHpBuffer; + progress.FoodMpBuffer += gameItem.Mp; + progress.FoodMpBufferSize = progress.FoodMpBuffer; + progress.FoodSpBuffer += gameItem.Data[4]; + progress.FoodSpBufferSize = progress.FoodSpBuffer; + + if (progress.FoodHpBuffer >= _configuration.FoodHardCap) + { + progress.FoodHpBuffer = _configuration.FoodHardCap; + progress.FoodHpBufferSize = _configuration.FoodHardCap; + } + + if (progress.FoodMpBuffer >= _configuration.FoodHardCap) + { + progress.FoodMpBuffer = _configuration.FoodHardCap; + progress.FoodMpBufferSize = _configuration.FoodHardCap; + } + + if (progress.FoodSpBuffer >= _configuration.FoodHardCap) + { + progress.FoodSpBuffer = _configuration.FoodHardCap; + progress.FoodSpBufferSize = _configuration.FoodHardCap; + } + + int mateProgress = gameItem.Data[5]; + if (mateProgress != 0) + { + progress.IncreaseTick += mateProgress; + progress.FoodMateMaxHpBuffer = 100; + progress.FoodMateMaxHpBufferSize = 100; + } + + return false; + } + + public void AddAdditionalFood(int max, int amount, bool isHp, int cap = 100) + { + AdditionalFoodProgress additionalProgress = GetAdditionalFoodProgress ??= new AdditionalFoodProgress(); + + if (isHp) + { + int hpCap = (int)Math.Round(max * cap / 100.0); + + if (additionalProgress.HpCap != 0 && additionalProgress.HpCap > cap) + { + return; + } + + additionalProgress.FoodAdditionalHpBuffer += amount; + additionalProgress.FoodAdditionalHpBufferSize = additionalProgress.FoodAdditionalHpBuffer; + additionalProgress.HpCap = (int)Math.Round(hpCap * 100.0 / max); + + return; + } + + int mpCap = (int)Math.Round(max * cap / 100.0); + + if (additionalProgress.MpCap != 0 && additionalProgress.MpCap > cap) + { + return; + } + + additionalProgress.FoodAdditionalMpBuffer += amount; + additionalProgress.FoodAdditionalMpBufferSize = additionalProgress.FoodAdditionalMpBuffer; + additionalProgress.MpCap = (int)Math.Round(mpCap * 100.0 / max); + } + + public void ClearFoodBuffer() + { + GetFoodProgress = null; + GetAdditionalFoodProgress = null; + } + + public void ClearSnackBuffer() + { + GetSnackProgress = null; + GetAdditionalSnackProgress = null; + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/SnackFood/FoodSnackComponentFactory.cs b/srcs/WingsAPI.Game/SnackFood/FoodSnackComponentFactory.cs new file mode 100644 index 0000000..4fa9daa --- /dev/null +++ b/srcs/WingsAPI.Game/SnackFood/FoodSnackComponentFactory.cs @@ -0,0 +1,10 @@ +namespace WingsEmu.Game.SnackFood; + +public class FoodSnackComponentFactory : IFoodSnackComponentFactory +{ + private readonly SnackFoodConfiguration _snackFoodConfiguration; + + public FoodSnackComponentFactory(SnackFoodConfiguration snackFoodConfiguration) => _snackFoodConfiguration = snackFoodConfiguration; + + public IFoodSnackComponent CreateFoodSnackComponent() => new FoodSnackComponent(_snackFoodConfiguration); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/SnackFood/IFoodSnackComponent.cs b/srcs/WingsAPI.Game/SnackFood/IFoodSnackComponent.cs new file mode 100644 index 0000000..534ba44 --- /dev/null +++ b/srcs/WingsAPI.Game/SnackFood/IFoodSnackComponent.cs @@ -0,0 +1,19 @@ +using WingsEmu.Game.Items; + +namespace WingsEmu.Game.SnackFood; + +public interface IFoodSnackComponent +{ + public FoodProgress GetFoodProgress { get; } + public SnackProgress GetSnackProgress { get; } + public AdditionalSnackProgress GetAdditionalSnackProgress { get; } + public AdditionalFoodProgress GetAdditionalFoodProgress { get; } + + public bool AddSnack(IGameItem gameItem); + public void AddAdditionalSnack(int max, int amount, bool isHp, int cap = 100); + public bool AddFood(IGameItem gameItem); + public void AddAdditionalFood(int max, int amount, bool isHp, int cap = 100); + + public void ClearFoodBuffer(); + public void ClearSnackBuffer(); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/SnackFood/IFoodSnackComponentFactory.cs b/srcs/WingsAPI.Game/SnackFood/IFoodSnackComponentFactory.cs new file mode 100644 index 0000000..6adec97 --- /dev/null +++ b/srcs/WingsAPI.Game/SnackFood/IFoodSnackComponentFactory.cs @@ -0,0 +1,6 @@ +namespace WingsEmu.Game.SnackFood; + +public interface IFoodSnackComponentFactory +{ + IFoodSnackComponent CreateFoodSnackComponent(); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/SnackFood/SnackFoodConfiguration.cs b/srcs/WingsAPI.Game/SnackFood/SnackFoodConfiguration.cs new file mode 100644 index 0000000..98f91b7 --- /dev/null +++ b/srcs/WingsAPI.Game/SnackFood/SnackFoodConfiguration.cs @@ -0,0 +1,12 @@ +namespace WingsEmu.Game.SnackFood; + +public class SnackFoodConfiguration +{ + public int DelayBetweenSnack { get; set; } + public int DelayBetweenFood { get; set; } + public int SnackSoftCap { get; set; } + public int FoodSoftCap { get; set; } + + public int SnackHardCap { get; set; } + public int FoodHardCap { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/SnackFood/SnackProgress.cs b/srcs/WingsAPI.Game/SnackFood/SnackProgress.cs new file mode 100644 index 0000000..9865644 --- /dev/null +++ b/srcs/WingsAPI.Game/SnackFood/SnackProgress.cs @@ -0,0 +1,17 @@ +using System; + +namespace WingsEmu.Game.SnackFood; + +public class SnackProgress +{ + public int SnackHpBuffer { get; set; } + public int SnackMpBuffer { get; set; } + + public int SnackHpBufferSize { get; set; } + public int SnackMpBufferSize { get; set; } + + public int SnackSpBuffer { get; set; } + public int SnackSpBufferSize { get; set; } + + public DateTime LastTick { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Specialists/ISpecialistStatsComponent.cs b/srcs/WingsAPI.Game/Specialists/ISpecialistStatsComponent.cs new file mode 100644 index 0000000..36a1fcd --- /dev/null +++ b/srcs/WingsAPI.Game/Specialists/ISpecialistStatsComponent.cs @@ -0,0 +1,40 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Game.Specialists; + +public interface ISpecialistStatsComponent +{ + int DamageMinimum { get; } + int DamageMaximum { get; } + int HitRate { get; } + int CriticalLuckRate { get; } + int CriticalRate { get; } + int DefenceDodge { get; } + int DistanceDefenceDodge { get; } + int ElementRate { get; } + int DarkResistance { get; } + int LightResistance { get; } + int FireResistance { get; } + int WaterResistance { get; } + int CriticalDodge { get; } + int CloseDefence { get; } + int DistanceDefence { get; } + int MagicDefence { get; } + int Hp { get; } + int Mp { get; } + int SpDamage { get; } + int SpDefence { get; } + int SpElement { get; } + int SpHP { get; } + int SpDark { get; } + int SpFire { get; } + int SpWater { get; } + int SpLight { get; } + int GetSlHit(); + int GetSlDefense(); + int GetSlElement(); + int GetSlHp(); + void RefreshSlStats(); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Specialists/SpecialistStatsComponent.cs b/srcs/WingsAPI.Game/Specialists/SpecialistStatsComponent.cs new file mode 100644 index 0000000..67c9f3b --- /dev/null +++ b/srcs/WingsAPI.Game/Specialists/SpecialistStatsComponent.cs @@ -0,0 +1,618 @@ +using WingsAPI.Packets.Enums.Shells; +using WingsEmu.Game._enum; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Items; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Specialists; + +public class SpecialistStatsComponent : ISpecialistStatsComponent +{ + private readonly IPlayerEntity _playerEntity; + + public SpecialistStatsComponent(IPlayerEntity playerEntity) => _playerEntity = playerEntity; + + private GameItemInstance Specialist => _playerEntity.Specialist; + + + public int GetSlHit() + { + if (Specialist == null) + { + return 0; + } + + int slHit = Specialist.SlPoint(Specialist.SlDamage, SpecialistPointsType.ATTACK); + slHit += _playerEntity.GetMaxWeaponShellValue(ShellEffectType.SLDamage) + + _playerEntity.GetMaxWeaponShellValue(ShellEffectType.SLGlobal) + + _playerEntity.BCardComponent.GetAllBCardsInformation(BCardType.IncreaseSpPoints, + (byte)AdditionalTypes.IncreaseSpPoints.SpCardAttackPointIncrease, _playerEntity.Level).firstData; + ; + + slHit = slHit > 100 ? 100 : slHit; + + return slHit; + } + + public int GetSlDefense() + { + if (Specialist == null) + { + return 0; + } + + int slDefence = Specialist.SlPoint(Specialist.SlDefence, SpecialistPointsType.DEFENCE); + + slDefence += _playerEntity.GetMaxWeaponShellValue(ShellEffectType.SLDefence) + + _playerEntity.GetMaxWeaponShellValue(ShellEffectType.SLGlobal) + + _playerEntity.BCardComponent.GetAllBCardsInformation(BCardType.IncreaseSpPoints, + (byte)AdditionalTypes.IncreaseSpPoints.SpCardDefensePointIncrease, _playerEntity.Level).firstData; + ; + + slDefence = slDefence > 100 ? 100 : slDefence; + + return slDefence; + } + + public int GetSlElement() + { + if (Specialist == null) + { + return 0; + } + + int slElement = Specialist.SlPoint(Specialist.SlElement, SpecialistPointsType.ELEMENT); + + slElement += _playerEntity.GetMaxWeaponShellValue(ShellEffectType.SLElement) + + _playerEntity.GetMaxWeaponShellValue(ShellEffectType.SLGlobal) + + _playerEntity.BCardComponent.GetAllBCardsInformation(BCardType.IncreaseSpPoints, + (byte)AdditionalTypes.IncreaseSpPoints.SpCardElementPointIncrease, _playerEntity.Level).firstData; + ; + + slElement = slElement > 100 ? 100 : slElement; + + return slElement; + } + + public int GetSlHp() + { + if (Specialist == null) + { + return 0; + } + + int slHp = Specialist.SlPoint(Specialist.SlHP, SpecialistPointsType.HPMP); + + slHp += _playerEntity.GetMaxWeaponShellValue(ShellEffectType.SLHP) + + _playerEntity.GetMaxWeaponShellValue(ShellEffectType.SLGlobal) + + _playerEntity.BCardComponent.GetAllBCardsInformation(BCardType.IncreaseSpPoints, + (byte)AdditionalTypes.IncreaseSpPoints.SpCardHpMpPointIncrease, _playerEntity.Level).firstData; + ; + + slHp = slHp > 100 ? 100 : slHp; + + return slHp; + } + + public int DamageMinimum { get; set; } + public int DamageMaximum { get; set; } + public int HitRate { get; set; } + public int CriticalLuckRate { get; set; } + public int CriticalRate { get; set; } + public int DefenceDodge { get; set; } + public int DistanceDefenceDodge { get; set; } + public int ElementRate { get; set; } + public int DarkResistance { get; set; } + public int LightResistance { get; set; } + public int FireResistance { get; set; } + public int WaterResistance { get; set; } + public int CriticalDodge { get; set; } + public int CloseDefence { get; set; } + public int DistanceDefence { get; set; } + public int MagicDefence { get; set; } + public int Hp { get; set; } + public int Mp { get; set; } + public int SpDamage => Specialist?.SpDamage ?? 0; + public int SpDefence => Specialist?.SpDefence ?? 0; + public int SpHP => Specialist?.SpHP ?? 0; + public int SpElement => Specialist?.SpElement ?? 0; + public int SpDark => Specialist?.SpDark ?? 0; + public int SpFire => Specialist?.SpFire ?? 0; + public int SpWater => Specialist?.SpWater ?? 0; + public int SpLight => Specialist?.SpLight ?? 0; + + public void RefreshSlStats() + { + DamageMinimum = 0; + DamageMaximum = 0; + HitRate = 0; + CriticalLuckRate = 0; + CriticalRate = 0; + DefenceDodge = 0; + DistanceDefenceDodge = 0; + ElementRate = 0; + DarkResistance = 0; + LightResistance = 0; + FireResistance = 0; + WaterResistance = 0; + CriticalDodge = 0; + CloseDefence = 0; + DistanceDefence = 0; + MagicDefence = 0; + Hp = 0; + Mp = 0; + + if (Specialist == null) + { + return; + } + + int slHit = GetSlHit(); + int slDefence = GetSlDefense(); + int slElement = GetSlElement(); + int slHp = GetSlHp(); + + + #region slHit + + if (slHit >= 1) + { + DamageMinimum += 5; + DamageMaximum += 5; + } + + if (slHit >= 10) + { + HitRate += 10; + } + + if (slHit >= 20) + { + CriticalLuckRate += 2; + } + + if (slHit >= 30) + { + DamageMinimum += 5; + DamageMaximum += 5; + HitRate += 10; + } + + if (slHit >= 40) + { + CriticalRate += 10; + } + + if (slHit >= 50) + { + Hp += 200; + Mp += 200; + } + + if (slHit >= 60) + { + HitRate += 15; + } + + if (slHit >= 70) + { + HitRate += 15; + DamageMinimum += 5; + DamageMaximum += 5; + } + + if (slHit >= 80) + { + CriticalLuckRate += 3; + } + + if (slHit >= 90) + { + CriticalRate += 20; + } + + if (slHit >= 100) + { + CriticalLuckRate += 3; + CriticalRate += 20; + Hp += 200; + Mp += 200; + DamageMinimum += 5; + DamageMaximum += 5; + HitRate += 20; + } + + #endregion + + #region slDefence + + if (slDefence >= 10) + { + DefenceDodge += 5; + DistanceDefenceDodge += 5; + } + + if (slDefence >= 20) + { + CloseDefence += 1; + DistanceDefence += 1; + MagicDefence += 1; + CriticalDodge += 2; + } + + if (slDefence >= 30) + { + Hp += 100; + } + + if (slDefence >= 40) + { + CloseDefence += 1; + DistanceDefence += 1; + MagicDefence += 1; + CriticalDodge += 2; + } + + if (slDefence >= 50) + { + DefenceDodge += 5; + DistanceDefenceDodge += 5; + } + + if (slDefence >= 60) + { + Hp += 200; + } + + if (slDefence >= 70) + { + CriticalDodge += 3; + } + + if (slDefence >= 75) + { + FireResistance += 2; + WaterResistance += 2; + LightResistance += 2; + DarkResistance += 2; + CloseDefence += 1; + DistanceDefence += 1; + MagicDefence += 1; + } + + if (slDefence >= 80) + { + DefenceDodge += 10; + DistanceDefenceDodge += 10; + CriticalDodge += 3; + } + + if (slDefence >= 90) + { + FireResistance += 3; + WaterResistance += 3; + LightResistance += 3; + DarkResistance += 3; + CloseDefence += 1; + DistanceDefence += 1; + MagicDefence += 1; + } + + if (slDefence >= 95) + { + Hp += 300; + } + + if (slDefence >= 100) + { + DefenceDodge += 20; + DistanceDefenceDodge += 20; + FireResistance += 5; + WaterResistance += 5; + LightResistance += 5; + DarkResistance += 5; + CloseDefence += 1; + DistanceDefence += 1; + MagicDefence += 1; + } + + #endregion + + #region slHp + + if (slHp >= 5) + { + DamageMinimum += 5; + DamageMaximum += 5; + } + + if (slHp >= 10) + { + DamageMinimum += 5; + DamageMaximum += 5; + } + + if (slHp >= 15) + { + DamageMinimum += 5; + DamageMaximum += 5; + } + + if (slHp >= 20) + { + DamageMinimum += 5; + DamageMaximum += 5; + CloseDefence += 10; + DistanceDefence += 10; + MagicDefence += 10; + } + + if (slHp >= 25) + { + DamageMinimum += 5; + DamageMaximum += 5; + } + + if (slHp >= 30) + { + DamageMinimum += 5; + DamageMaximum += 5; + } + + if (slHp >= 35) + { + DamageMinimum += 5; + DamageMaximum += 5; + } + + if (slHp >= 40) + { + DamageMinimum += 5; + DamageMaximum += 5; + CloseDefence += 15; + DistanceDefence += 15; + MagicDefence += 15; + } + + if (slHp >= 45) + { + DamageMinimum += 10; + DamageMaximum += 10; + } + + if (slHp >= 50) + { + DamageMinimum += 10; + DamageMaximum += 10; + FireResistance += 2; + WaterResistance += 2; + LightResistance += 2; + DarkResistance += 2; + } + + if (slHp >= 55) + { + DamageMinimum += 10; + DamageMaximum += 10; + } + + if (slHp >= 60) + { + DamageMinimum += 10; + DamageMaximum += 10; + } + + if (slHp >= 65) + { + DamageMinimum += 10; + DamageMaximum += 10; + } + + if (slHp >= 70) + { + DamageMinimum += 10; + DamageMaximum += 10; + CloseDefence += 20; + DistanceDefence += 20; + MagicDefence += 20; + } + + if (slHp >= 75) + { + DamageMinimum += 15; + DamageMaximum += 15; + } + + if (slHp >= 80) + { + DamageMinimum += 15; + DamageMaximum += 15; + } + + if (slHp >= 85) + { + DamageMinimum += 15; + DamageMaximum += 15; + CriticalDodge += 1; + } + + if (slHp >= 86) + { + CriticalDodge += 1; + } + + if (slHp >= 87) + { + CriticalDodge += 1; + } + + if (slHp >= 88) + { + CriticalDodge += 1; + } + + if (slHp >= 90) + { + DamageMinimum += 15; + DamageMaximum += 15; + CloseDefence += 25; + DistanceDefence += 25; + MagicDefence += 25; + } + + if (slHp >= 91) + { + DefenceDodge += 2; + DistanceDefenceDodge += 2; + } + + if (slHp >= 92) + { + DefenceDodge += 2; + DistanceDefenceDodge += 2; + } + + if (slHp >= 93) + { + DefenceDodge += 2; + DistanceDefenceDodge += 2; + } + + if (slHp >= 94) + { + DefenceDodge += 2; + DistanceDefenceDodge += 2; + } + + if (slHp >= 95) + { + DamageMinimum += 20; + DamageMaximum += 20; + DefenceDodge += 2; + DistanceDefenceDodge += 2; + } + + if (slHp >= 96) + { + DefenceDodge += 2; + DistanceDefenceDodge += 2; + } + + if (slHp >= 97) + { + DefenceDodge += 2; + DistanceDefenceDodge += 2; + } + + if (slHp >= 98) + { + DefenceDodge += 2; + DistanceDefenceDodge += 2; + } + + if (slHp >= 99) + { + DefenceDodge += 2; + DistanceDefenceDodge += 2; + } + + if (slHp >= 100) + { + FireResistance += 3; + WaterResistance += 3; + LightResistance += 3; + DarkResistance += 3; + CloseDefence += 30; + DistanceDefence += 30; + MagicDefence += 30; + DamageMinimum += 20; + DamageMaximum += 20; + DefenceDodge += 2; + DistanceDefenceDodge += 2; + CriticalDodge += 1; + } + + #endregion + + #region slElement + + if (slElement >= 1) + { + ElementRate += 2; + } + + if (slElement >= 10) + { + Mp += 100; + } + + if (slElement >= 20) + { + MagicDefence += 5; + } + + if (slElement >= 30) + { + FireResistance += 2; + WaterResistance += 2; + LightResistance += 2; + DarkResistance += 2; + ElementRate += 2; + } + + if (slElement >= 40) + { + Mp += 100; + } + + if (slElement >= 50) + { + MagicDefence += 5; + } + + if (slElement >= 60) + { + FireResistance += 3; + WaterResistance += 3; + LightResistance += 3; + DarkResistance += 3; + ElementRate += 2; + } + + if (slElement >= 70) + { + Mp += 100; + } + + if (slElement >= 80) + { + MagicDefence += 5; + } + + if (slElement >= 90) + { + FireResistance += 4; + WaterResistance += 4; + LightResistance += 4; + DarkResistance += 4; + ElementRate += 2; + } + + if (slElement < 100) + { + return; + } + + FireResistance += 6; + WaterResistance += 6; + LightResistance += 6; + DarkResistance += 6; + MagicDefence += 5; + Mp += 200; + ElementRate += 2; + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/TimeSpaces/Enums/PortalMinimapOrientation.cs b/srcs/WingsAPI.Game/TimeSpaces/Enums/PortalMinimapOrientation.cs new file mode 100644 index 0000000..fc7cf46 --- /dev/null +++ b/srcs/WingsAPI.Game/TimeSpaces/Enums/PortalMinimapOrientation.cs @@ -0,0 +1,9 @@ +namespace WingsEmu.Game.TimeSpaces.Enums; + +public enum PortalMinimapOrientation : byte +{ + NORTH = 0, + EAST = 1, + SOUTH = 2, + WEST = 3 +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/TimeSpaces/Enums/TimeSpaceAction.cs b/srcs/WingsAPI.Game/TimeSpaces/Enums/TimeSpaceAction.cs new file mode 100644 index 0000000..c0370a2 --- /dev/null +++ b/srcs/WingsAPI.Game/TimeSpaces/Enums/TimeSpaceAction.cs @@ -0,0 +1,10 @@ +namespace WingsEmu.Game.TimeSpaces.Enums; + +public enum TimeSpaceAction : byte +{ + CAMERA_ADJUST = 0, + TIMESPACE_COMPLETE = 1, + WARNING_WITH_SOUND = 2, + WARNING_WITHOUT_SOUND = 3, + CAMERA_ADJUST_2 = 4 +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/TimeSpaces/Enums/TimeSpaceFinishType.cs b/srcs/WingsAPI.Game/TimeSpaces/Enums/TimeSpaceFinishType.cs new file mode 100644 index 0000000..0675e09 --- /dev/null +++ b/srcs/WingsAPI.Game/TimeSpaces/Enums/TimeSpaceFinishType.cs @@ -0,0 +1,11 @@ +namespace WingsEmu.Game.TimeSpaces.Enums; + +public enum TimeSpaceFinishType : byte +{ + TIME_IS_UP = 1, + NPC_DIED = 2, + OUT_OF_LIVES = 3, + TEAM_MEMBER_OUT_OF_LIVES = 4, + SUCCESS = 5, + SUCCESS_HIGH_SCORE = 6 +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/TimeSpaces/Enums/TimeSpaceTaskType.cs b/srcs/WingsAPI.Game/TimeSpaces/Enums/TimeSpaceTaskType.cs new file mode 100644 index 0000000..f3f6e92 --- /dev/null +++ b/srcs/WingsAPI.Game/TimeSpaces/Enums/TimeSpaceTaskType.cs @@ -0,0 +1,8 @@ +namespace WingsEmu.Game.TimeSpaces.Enums; + +public enum TimeSpaceTaskType +{ + None = 0, + KillAllMonsters = 1, + Survive = 2 +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceAddTimeToTimerEvent.cs b/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceAddTimeToTimerEvent.cs new file mode 100644 index 0000000..921c8b7 --- /dev/null +++ b/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceAddTimeToTimerEvent.cs @@ -0,0 +1,10 @@ +using System; +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.TimeSpaces.Events; + +public class TimeSpaceAddTimeToTimerEvent : PlayerEvent +{ + public TimeSpaceParty TimeSpaceParty { get; init; } + public TimeSpan Time { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceBonusMonsterEvent.cs b/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceBonusMonsterEvent.cs new file mode 100644 index 0000000..19dc374 --- /dev/null +++ b/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceBonusMonsterEvent.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using PhoenixLib.Events; +using WingsEmu.Game.Entities; + +namespace WingsEmu.Game.TimeSpaces.Events; + +public class TimeSpaceBonusMonsterEvent : IAsyncEvent +{ + public IReadOnlyList MonsterEntities { get; init; } + public TimeSpaceSubInstance TimeSpaceSubInstance { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceCheckForTasksCompletedEvent.cs b/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceCheckForTasksCompletedEvent.cs new file mode 100644 index 0000000..0154ebc --- /dev/null +++ b/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceCheckForTasksCompletedEvent.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using PhoenixLib.Events; + +namespace WingsEmu.Game.TimeSpaces.Events; + +public class TimeSpaceCheckForTasksCompletedEvent : TimeSpaceTaskCheckEvent +{ + public IEnumerable TimeSpaceSubInstances { get; init; } + public IEnumerable Events { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceCheckMonsterEvent.cs b/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceCheckMonsterEvent.cs new file mode 100644 index 0000000..b4540af --- /dev/null +++ b/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceCheckMonsterEvent.cs @@ -0,0 +1,11 @@ +using PhoenixLib.Events; +using WingsEmu.Game.Entities; + +namespace WingsEmu.Game.TimeSpaces.Events; + +public class TimeSpaceCheckMonsterEvent : IAsyncEvent +{ + public TimeSpaceCheckMonsterEvent(IMonsterEntity monsterEntity) => MonsterEntity = monsterEntity; + + public IMonsterEntity MonsterEntity { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceCheckObjectivesEvent.cs b/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceCheckObjectivesEvent.cs new file mode 100644 index 0000000..8c5059b --- /dev/null +++ b/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceCheckObjectivesEvent.cs @@ -0,0 +1,10 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.TimeSpaces.Events; + +public class TimeSpaceCheckObjectivesEvent : PlayerEvent +{ + public TimeSpaceParty TimeSpaceParty { get; init; } + public bool PlayerEnteredToEndPortal { get; init; } + public bool SendMessageWithNotFinishedObjects { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceClosePortalEvent.cs b/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceClosePortalEvent.cs new file mode 100644 index 0000000..14063f8 --- /dev/null +++ b/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceClosePortalEvent.cs @@ -0,0 +1,8 @@ +using PhoenixLib.Events; + +namespace WingsEmu.Game.TimeSpaces.Events; + +public class TimeSpaceClosePortalEvent : IAsyncEvent +{ + public IPortalEntity PortalEntity { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceDeathEvent.cs b/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceDeathEvent.cs new file mode 100644 index 0000000..85e9fbf --- /dev/null +++ b/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceDeathEvent.cs @@ -0,0 +1,7 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.TimeSpaces.Events; + +public class TimeSpaceDeathEvent : PlayerEvent +{ +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceDecreaseLiveEvent.cs b/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceDecreaseLiveEvent.cs new file mode 100644 index 0000000..dc70966 --- /dev/null +++ b/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceDecreaseLiveEvent.cs @@ -0,0 +1,7 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.TimeSpaces.Events; + +public class TimeSpaceDecreaseLiveEvent : PlayerEvent +{ +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceDespawnMonstersInRoomEvent.cs b/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceDespawnMonstersInRoomEvent.cs new file mode 100644 index 0000000..2531dff --- /dev/null +++ b/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceDespawnMonstersInRoomEvent.cs @@ -0,0 +1,8 @@ +using PhoenixLib.Events; + +namespace WingsEmu.Game.TimeSpaces.Events; + +public class TimeSpaceDespawnMonstersInRoomEvent : IAsyncEvent +{ + public TimeSpaceSubInstance TimeSpaceSubInstance { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceDestroyEvent.cs b/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceDestroyEvent.cs new file mode 100644 index 0000000..d90363f --- /dev/null +++ b/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceDestroyEvent.cs @@ -0,0 +1,10 @@ +using PhoenixLib.Events; + +namespace WingsEmu.Game.TimeSpaces.Events; + +public class TimeSpaceDestroyEvent : IAsyncEvent +{ + public TimeSpaceDestroyEvent(TimeSpaceParty timeSpace) => TimeSpace = timeSpace; + + public TimeSpaceParty TimeSpace { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceGroupTryJoinEvent.cs b/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceGroupTryJoinEvent.cs new file mode 100644 index 0000000..0c1de04 --- /dev/null +++ b/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceGroupTryJoinEvent.cs @@ -0,0 +1,10 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Game.Portals; + +namespace WingsEmu.Game.TimeSpaces.Events; + +public class TimeSpaceGroupTryJoinEvent : PlayerEvent +{ + public ITimeSpacePortalEntity PortalEntity { get; init; } + public long CharacterId { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceIncreaseScoreEvent.cs b/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceIncreaseScoreEvent.cs new file mode 100644 index 0000000..34a68bb --- /dev/null +++ b/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceIncreaseScoreEvent.cs @@ -0,0 +1,9 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.TimeSpaces.Events; + +public class TimeSpaceIncreaseScoreEvent : PlayerEvent +{ + public int AmountToIncrease { get; init; } + public TimeSpaceParty TimeSpaceParty { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceInstanceFinishEvent.cs b/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceInstanceFinishEvent.cs new file mode 100644 index 0000000..070e510 --- /dev/null +++ b/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceInstanceFinishEvent.cs @@ -0,0 +1,18 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Game.TimeSpaces.Enums; + +namespace WingsEmu.Game.TimeSpaces.Events; + +public class TimeSpaceInstanceFinishEvent : PlayerEvent +{ + public TimeSpaceInstanceFinishEvent(TimeSpaceParty timeSpaceParty, TimeSpaceFinishType timeSpaceFinishType, long? victimId = null) + { + TimeSpaceParty = timeSpaceParty; + TimeSpaceFinishType = timeSpaceFinishType; + VictimId = victimId; + } + + public TimeSpaceParty TimeSpaceParty { get; } + public TimeSpaceFinishType TimeSpaceFinishType { get; } + public long? VictimId { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceInstanceStartEvent.cs b/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceInstanceStartEvent.cs new file mode 100644 index 0000000..fa88482 --- /dev/null +++ b/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceInstanceStartEvent.cs @@ -0,0 +1,7 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.TimeSpaces.Events; + +public class TimeSpaceInstanceStartEvent : PlayerEvent +{ +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceLeavePartyEvent.cs b/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceLeavePartyEvent.cs new file mode 100644 index 0000000..57ac2bf --- /dev/null +++ b/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceLeavePartyEvent.cs @@ -0,0 +1,10 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.TimeSpaces.Events; + +public class TimeSpaceLeavePartyEvent : PlayerEvent +{ + public bool RemoveLive { get; init; } + public bool CheckForSeeds { get; init; } + public bool CheckFinished { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpacePartyCreateEvent.cs b/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpacePartyCreateEvent.cs new file mode 100644 index 0000000..2581f03 --- /dev/null +++ b/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpacePartyCreateEvent.cs @@ -0,0 +1,20 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Game.Inventory; + +namespace WingsEmu.Game.TimeSpaces.Events; + +public class TimeSpacePartyCreateEvent : PlayerEvent +{ + public TimeSpacePartyCreateEvent(long timeSpaceId, InventoryItem itemToRemove = null, bool isEasyMode = false, bool isChallengeMode = false) + { + TimeSpaceId = timeSpaceId; + ItemToRemove = itemToRemove; + IsEasyMode = isEasyMode; + IsChallengeMode = isChallengeMode; + } + + public long TimeSpaceId { get; } + public bool IsEasyMode { get; } + public bool IsChallengeMode { get; } + public InventoryItem ItemToRemove { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpacePickUpItemEvent.cs b/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpacePickUpItemEvent.cs new file mode 100644 index 0000000..568d694 --- /dev/null +++ b/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpacePickUpItemEvent.cs @@ -0,0 +1,11 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Mates; + +namespace WingsEmu.Game.TimeSpaces.Events; + +public class TimeSpacePickUpItemEvent : PlayerEvent +{ + public TimeSpaceMapItem TimeSpaceMapItem { get; init; } + public IMateEntity MateEntity { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpacePortalOpenEvent.cs b/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpacePortalOpenEvent.cs new file mode 100644 index 0000000..062e230 --- /dev/null +++ b/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpacePortalOpenEvent.cs @@ -0,0 +1,8 @@ +using PhoenixLib.Events; + +namespace WingsEmu.Game.TimeSpaces.Events; + +public class TimeSpacePortalOpenEvent : IAsyncEvent +{ + public IPortalEntity PortalEntity { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceRefreshObjectiveProgressEvent.cs b/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceRefreshObjectiveProgressEvent.cs new file mode 100644 index 0000000..b4d7faa --- /dev/null +++ b/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceRefreshObjectiveProgressEvent.cs @@ -0,0 +1,9 @@ +using System; +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.TimeSpaces.Events; + +public class TimeSpaceRefreshObjectiveProgressEvent : PlayerEvent +{ + public Guid MapInstanceId { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceRemoveItemsEvent.cs b/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceRemoveItemsEvent.cs new file mode 100644 index 0000000..f27b85d --- /dev/null +++ b/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceRemoveItemsEvent.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using PhoenixLib.Events; +using WingsEmu.Game.Maps; + +namespace WingsEmu.Game.TimeSpaces.Events; + +public class TimeSpaceRemoveItemsEvent : IAsyncEvent +{ + public IEnumerable ItemsToRemove { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceSelectRewardEvent.cs b/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceSelectRewardEvent.cs new file mode 100644 index 0000000..15bac6a --- /dev/null +++ b/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceSelectRewardEvent.cs @@ -0,0 +1,8 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.TimeSpaces.Events; + +public class TimeSpaceSelectRewardEvent : PlayerEvent +{ + public bool SendRepayPacket { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceSetTimeEvent.cs b/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceSetTimeEvent.cs new file mode 100644 index 0000000..8317788 --- /dev/null +++ b/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceSetTimeEvent.cs @@ -0,0 +1,10 @@ +using System; +using PhoenixLib.Events; + +namespace WingsEmu.Game.TimeSpaces.Events; + +public class TimeSpaceSetTimeEvent : IAsyncEvent +{ + public TimeSpaceParty TimeSpaceParty { get; init; } + public TimeSpan Time { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceStartClockEvent.cs b/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceStartClockEvent.cs new file mode 100644 index 0000000..4b0dc32 --- /dev/null +++ b/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceStartClockEvent.cs @@ -0,0 +1,15 @@ +using PhoenixLib.Events; + +namespace WingsEmu.Game.TimeSpaces.Events; + +public class TimeSpaceStartClockEvent : IAsyncEvent +{ + public TimeSpaceStartClockEvent(TimeSpaceParty timeSpaceParty, bool isVisible) + { + TimeSpaceParty = timeSpaceParty; + IsVisible = isVisible; + } + + public TimeSpaceParty TimeSpaceParty { get; set; } + public bool IsVisible { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceStartPortalEvent.cs b/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceStartPortalEvent.cs new file mode 100644 index 0000000..eaa14c4 --- /dev/null +++ b/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceStartPortalEvent.cs @@ -0,0 +1,7 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.TimeSpaces.Events; + +public class TimeSpaceStartPortalEvent : PlayerEvent +{ +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceStartTaskEvent.cs b/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceStartTaskEvent.cs new file mode 100644 index 0000000..7507805 --- /dev/null +++ b/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceStartTaskEvent.cs @@ -0,0 +1,8 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.TimeSpaces.Events; + +public class TimeSpaceStartTaskEvent : PlayerEvent +{ + public TimeSpaceSubInstance TimeSpaceSubInstance { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceTaskCheckEvent.cs b/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceTaskCheckEvent.cs new file mode 100644 index 0000000..d926da4 --- /dev/null +++ b/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceTaskCheckEvent.cs @@ -0,0 +1,8 @@ +using PhoenixLib.Events; + +namespace WingsEmu.Game.TimeSpaces.Events; + +public class TimeSpaceTaskCheckEvent : IAsyncEvent +{ + public bool Completed { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceTogglePortalEvent.cs b/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceTogglePortalEvent.cs new file mode 100644 index 0000000..1d47a7f --- /dev/null +++ b/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceTogglePortalEvent.cs @@ -0,0 +1,8 @@ +using PhoenixLib.Events; + +namespace WingsEmu.Game.TimeSpaces.Events; + +public class TimeSpaceTogglePortalEvent : IAsyncEvent +{ + public IPortalEntity PortalEntity { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceTryFinishTaskEvent.cs b/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceTryFinishTaskEvent.cs new file mode 100644 index 0000000..9a1c83a --- /dev/null +++ b/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceTryFinishTaskEvent.cs @@ -0,0 +1,15 @@ +using PhoenixLib.Events; + +namespace WingsEmu.Game.TimeSpaces.Events; + +public class TimeSpaceTryFinishTaskEvent : IAsyncEvent +{ + public TimeSpaceTryFinishTaskEvent(TimeSpaceSubInstance timeSpaceSubInstance, TimeSpaceParty timeSpaceParty) + { + TimeSpaceSubInstance = timeSpaceSubInstance; + TimeSpaceParty = timeSpaceParty; + } + + public TimeSpaceSubInstance TimeSpaceSubInstance { get; } + public TimeSpaceParty TimeSpaceParty { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceTryStartHiddenEvent.cs b/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceTryStartHiddenEvent.cs new file mode 100644 index 0000000..5c7e3da --- /dev/null +++ b/srcs/WingsAPI.Game/TimeSpaces/Events/TimeSpaceTryStartHiddenEvent.cs @@ -0,0 +1,10 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Game.Entities; + +namespace WingsEmu.Game.TimeSpaces.Events; + +public class TimeSpaceTryStartHiddenEvent : PlayerEvent +{ + public INpcEntity TimeSpacePortal { get; init; } + public bool IsChallengeMode { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/TimeSpaces/Events/TryStartTaskEvent.cs b/srcs/WingsAPI.Game/TimeSpaces/Events/TryStartTaskEvent.cs new file mode 100644 index 0000000..0034ebf --- /dev/null +++ b/srcs/WingsAPI.Game/TimeSpaces/Events/TryStartTaskEvent.cs @@ -0,0 +1,8 @@ +using PhoenixLib.Events; + +namespace WingsEmu.Game.TimeSpaces.Events; + +public class TryStartTaskEvent : IAsyncEvent +{ + public TimeSpaceSubInstance Map { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/TimeSpaces/ITimeSpaceComponent.cs b/srcs/WingsAPI.Game/TimeSpaces/ITimeSpaceComponent.cs new file mode 100644 index 0000000..9986d04 --- /dev/null +++ b/srcs/WingsAPI.Game/TimeSpaces/ITimeSpaceComponent.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using WingsEmu.Game.Entities; + +namespace WingsEmu.Game.TimeSpaces; + +public interface ITimeSpaceComponent +{ + public TimeSpaceParty TimeSpace { get; } + public List Partners { get; set; } + + public bool IsInTimeSpaceParty { get; } + public bool TimeSpaceTeamIsFull { get; } + + public void SetTimeSpaceParty(TimeSpaceParty timeSpaceParty); + public void RemoveTimeSpaceParty(); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/TimeSpaces/ITimeSpaceFactory.cs b/srcs/WingsAPI.Game/TimeSpaces/ITimeSpaceFactory.cs new file mode 100644 index 0000000..7a432d6 --- /dev/null +++ b/srcs/WingsAPI.Game/TimeSpaces/ITimeSpaceFactory.cs @@ -0,0 +1,6 @@ +namespace WingsEmu.Game.TimeSpaces; + +public interface ITimeSpaceFactory +{ + TimeSpaceInstance Create(TimeSpaceParty timeSpaceParty); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/TimeSpaces/ITimeSpaceManager.cs b/srcs/WingsAPI.Game/TimeSpaces/ITimeSpaceManager.cs new file mode 100644 index 0000000..58302d6 --- /dev/null +++ b/srcs/WingsAPI.Game/TimeSpaces/ITimeSpaceManager.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; + +namespace WingsEmu.Game.TimeSpaces; + +public interface ITimeSpaceManager +{ + IReadOnlyCollection GetTimeSpaces(); + TimeSpaceParty GetTimeSpace(Guid id); + TimeSpaceParty GetTimeSpaceByMapInstanceId(Guid id); + TimeSpaceSubInstance GetSubInstance(Guid mapInstanceId); + void AddTimeSpace(TimeSpaceParty timeSpaceParty); + void AddTimeSpaceSubInstance(Guid mapInstanceId, TimeSpaceSubInstance subInstance); + void AddTimeSpaceByMapInstanceId(Guid mapInstanceId, TimeSpaceParty timeSpaceParty); + void RemoveTimeSpaceSubInstance(Guid mapInstanceId); + void RemoveTimeSpacePartyByMapInstanceId(Guid mapInstanceId); + void RemoveTimeSpace(TimeSpaceParty timeSpaceParty); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/TimeSpaces/TimeSpaceComponent.cs b/srcs/WingsAPI.Game/TimeSpaces/TimeSpaceComponent.cs new file mode 100644 index 0000000..761c982 --- /dev/null +++ b/srcs/WingsAPI.Game/TimeSpaces/TimeSpaceComponent.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using WingsEmu.Game.Entities; + +namespace WingsEmu.Game.TimeSpaces; + +public class TimeSpaceComponent : ITimeSpaceComponent +{ + public TimeSpaceParty TimeSpace { get; private set; } + public List Partners { get; set; } = new(); + public bool TimeSpaceTeamIsFull => TimeSpace != null && TimeSpace.Members.Count >= TimeSpace.TimeSpaceInformation.MaxPlayers; + + public bool IsInTimeSpaceParty => TimeSpace != null; + + public void SetTimeSpaceParty(TimeSpaceParty timeSpaceParty) + { + TimeSpace = timeSpaceParty; + } + + public void RemoveTimeSpaceParty() + { + TimeSpace = null; + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/TimeSpaces/TimeSpaceInstance.cs b/srcs/WingsAPI.Game/TimeSpaces/TimeSpaceInstance.cs new file mode 100644 index 0000000..d56f2c6 --- /dev/null +++ b/srcs/WingsAPI.Game/TimeSpaces/TimeSpaceInstance.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using WingsEmu.Core.Generics; +using WingsEmu.Game.Helpers.Damages; + +namespace WingsEmu.Game.TimeSpaces; + +public class TimeSpaceInstance +{ + public TimeSpaceInstance(IReadOnlyCollection timeSpaceSubInstances, TimeSpaceSubInstance spawnInstance, Position spawnPoint, TimeSpan timeToComplete, + byte maxLives, TimeSpaceObjective timeSpaceObjective, int bonusPointItemDropChance, HashSet protectedNpcs, short? obtainablePartnerVnum, + bool infiniteDuration, int? preFinishDialog, bool preFinishDialogIsObjective) + { + foreach (TimeSpaceSubInstance timeSpaceSubInstance in timeSpaceSubInstances) + { + timeSpaceSubInstance.MapInstance.Initialize(DateTime.UtcNow.AddMilliseconds(-500)); + } + + TimeSpaceSubInstances = timeSpaceSubInstances.ToDictionary(s => s.MapInstance.Id); + FinishDate = DateTime.UtcNow.Add(timeToComplete); + Lives = maxLives; + TimeSpaceObjective = timeSpaceObjective; + MaxLives = maxLives; + SpawnPoint = spawnPoint; + SpawnInstance = spawnInstance; + TimeToComplete = timeToComplete; + BonusPointItemDropChance = bonusPointItemDropChance; + KilledMonsters = 0; + SavedNpcs = 0; + EnteredRooms = 0; + Score = 0; + ProtectedNpcs = protectedNpcs; + ObtainablePartnerVnum = obtainablePartnerVnum; + InfiniteDuration = infiniteDuration; + PreFinishDialog = preFinishDialog; + PreFinishDialogIsObjective = preFinishDialogIsObjective; + } + + public IReadOnlyDictionary TimeSpaceSubInstances { get; } + + public int Lives { get; private set; } + public int Score { get; private set; } + public int KilledMonsters { get; private set; } + public int EnteredRooms { get; private set; } + public int SavedNpcs { get; set; } + public byte MaxLives { get; } + public int BonusPointItemDropChance { get; } + public int KilledProtectedNpcs { get; set; } + public int? PreFinishDialog { get; set; } + public bool PreFinishDialogShown { get; set; } + public bool PreFinishDialogIsObjective { get; set; } + public DateTime? PreFinishDialogTime { get; set; } + public Position SpawnPoint { get; } + public TimeSpaceSubInstance SpawnInstance { get; } + public DateTime FinishDate { get; private set; } + public DateTime RemoveDate { get; private set; } + public DateTime? StartTimeFreeze { get; set; } + public TimeSpan TimeToComplete { get; } + public TimeSpaceObjective TimeSpaceObjective { get; } + public TimeSpan TimeUntilEnd => FinishDate - DateTime.UtcNow; + public ThreadSafeHashSet VisitedRooms { get; } = new(); + public HashSet ProtectedNpcs { get; } + public short? ObtainablePartnerVnum { get; } + public bool InfiniteDuration { get; set; } + + public void SetDestroyDate(DateTime dateTime) + { + RemoveDate = dateTime; + } + + public void UpdateFinishDate(TimeSpan? toFinish = null) + { + FinishDate = DateTime.UtcNow.Add(toFinish ?? TimeToComplete); + } + + public void AddTimeToFinishDate(TimeSpan timeSpan) + { + FinishDate += timeSpan; + } + + public void IncreaseOrDecreaseLives(short amount) + { + int futureValue = Lives + amount; + if (futureValue > MaxLives) + { + Lives = MaxLives; + return; + } + + Lives = futureValue; + } + + public void IncreaseKilledMonsters() => KilledMonsters++; + + public void IncreaseEnteredRooms() => EnteredRooms++; + + public void IncreaseScoreByAmount(int amount) => Score += amount; + + public void UpdateFinalScore(int score) => Score = score; +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/TimeSpaces/TimeSpaceObjective.cs b/srcs/WingsAPI.Game/TimeSpaces/TimeSpaceObjective.cs new file mode 100644 index 0000000..c685853 --- /dev/null +++ b/srcs/WingsAPI.Game/TimeSpaces/TimeSpaceObjective.cs @@ -0,0 +1,23 @@ +namespace WingsEmu.Game.TimeSpaces; + +public class TimeSpaceObjective +{ + public bool KillAllMonsters { get; set; } + public bool GoToExit { get; set; } + public bool ProtectNPC { get; set; } + + public short? KillMonsterVnum { get; set; } + public short? KillMonsterAmount { get; set; } + public short KilledMonsterAmount { get; set; } + + public short? CollectItemVnum { get; set; } + public short? CollectItemAmount { get; set; } + public short CollectedItemAmount { get; set; } + + public byte? Conversation { get; set; } + public byte ConversationsHad { get; set; } + + public short? InteractObjectsVnum { get; set; } + public short? InteractObjectsAmount { get; set; } + public short InteractedObjectsAmount { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/TimeSpaces/TimeSpacePacketExtensions.cs b/srcs/WingsAPI.Game/TimeSpaces/TimeSpacePacketExtensions.cs new file mode 100644 index 0000000..3d906f0 --- /dev/null +++ b/srcs/WingsAPI.Game/TimeSpaces/TimeSpacePacketExtensions.cs @@ -0,0 +1,526 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using WingsAPI.Data.TimeSpace; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Portals; +using WingsEmu.Game.TimeSpaces.Enums; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.TimeSpaces; + +public static class TimeSpacePacketExtensions +{ + public static string GenerateRsfn(this IMapInstance mapInstance, bool isStartMap = false, bool isVisit = true) + { + int number = isStartMap ? 0 : mapInstance.GetAliveMonsters(x => x.SummonerType == null || x.SummonerType != VisualType.Player).Any() && isVisit ? 3 : isVisit ? 0 : 1; + + return $"rsfn {mapInstance.MapIndexX} {mapInstance.MapIndexY} {number}"; + } + + public static string GenerateMinfo(this IClientSession session) + { + TimeSpaceInstance timeSpace = session.PlayerEntity.TimeSpaceComponent.TimeSpace.Instance; + TimeSpaceObjective timeSpaceObjective = session.PlayerEntity.TimeSpaceComponent.TimeSpace.Instance.TimeSpaceObjective; + return "minfo " + + $"{(timeSpaceObjective.KillAllMonsters ? 1 : 0)} " + + $"{(timeSpaceObjective.GoToExit ? 1 : 0)} " + + $"{timeSpaceObjective.KillMonsterVnum ?? -1}.{timeSpaceObjective.KilledMonsterAmount}/{timeSpaceObjective.KillMonsterAmount ?? 0} " + + $"{timeSpaceObjective.CollectItemVnum ?? -1}.{timeSpaceObjective.CollectedItemAmount}/{timeSpaceObjective.CollectItemAmount ?? 0} " + + $"{(timeSpaceObjective.Conversation == null ? "-1/0" : $"{timeSpaceObjective.ConversationsHad}/{timeSpaceObjective.Conversation}")} " + + $"{timeSpaceObjective.InteractObjectsVnum ?? -1}.{timeSpaceObjective.InteractedObjectsAmount}/{timeSpaceObjective.InteractObjectsAmount ?? 0} " + + $"{(timeSpaceObjective.ProtectNPC ? 1 : 0)} " + + $"{timeSpace.MaxLives} 0"; + } + + public static string GenerateMissionTargetMessage(this IClientSession session, string message) => $"sinfo {message}"; + + public static void SendMissionTargetMessage(this IClientSession session, string message) => session.SendPacket(session.GenerateMissionTargetMessage(message)); + + public static void SendMinfo(this IClientSession session) + { + if (!session.PlayerEntity.TimeSpaceComponent.IsInTimeSpaceParty) + { + return; + } + + if (session.PlayerEntity.TimeSpaceComponent.TimeSpace.Instance == null) + { + return; + } + + session.SendPacket(session.GenerateMinfo()); + } + + public static void RefreshTimespaceScoreUi(this IClientSession session) + { + session.SendPacket(session.GenerateRnscPacket()); + } + + public static string GenerateRnscPacket(this IClientSession session) => $"rnsc {session.PlayerEntity.TimeSpaceComponent.TimeSpace.Instance.Score}"; + + public static string GenerateRsfpPacket(this IClientSession session) + { + if (session.CurrentMapInstance.MapInstanceType == MapInstanceType.RaidInstance) + { + return "rsfp 0 0"; + } + + if (session.CurrentMapInstance.MapInstanceType != MapInstanceType.TimeSpaceInstance) + { + return "rsfp 0 -1"; + } + + return $"rsfp {session.CurrentMapInstance.MapIndexX} {session.CurrentMapInstance.MapIndexY}"; + } + + public static string GenerateRsfmPacket(this IClientSession session, TimeSpaceAction action, byte spiralFloor) + { + TimeSpaceParty timeSpaceParty = session.PlayerEntity.TimeSpaceComponent.TimeSpace; + byte maxIndexX = timeSpaceParty.Instance.TimeSpaceSubInstances.Values.Max(s => s.MapInstance.MapIndexX); + byte maxIndexY = timeSpaceParty.Instance.TimeSpaceSubInstances.Values.Max(s => s.MapInstance.MapIndexY); + + return $"rsfm {timeSpaceParty.TimeSpaceId} {(byte)action} {maxIndexX + 1} {maxIndexY + 1} {spiralFloor}"; + } + + public static string GenerateRepayPacket(this IClientSession session, IReadOnlyList rewards, TimeSpaceRewardItem selectedDrawItem) + { + string repayPacket = "repay"; + foreach (TimeSpaceRewardItem bonusReward in rewards.Where(x => x.Type == TimeSpaceRewardType.BONUS)) + { + repayPacket += $" {bonusReward.ItemVnum}.{bonusReward.Rarity}.{bonusReward.Amount}"; + } + + for (int i = 0; i < 3 - rewards.Count(x => x.Type == TimeSpaceRewardType.BONUS); i++) + { + repayPacket += " -1.0.0"; + } + + foreach (TimeSpaceRewardItem specialReward in rewards.Where(x => x.Type == TimeSpaceRewardType.SPECIAL)) + { + repayPacket += $" {specialReward.ItemVnum}.{specialReward.Rarity}.{specialReward.Amount}"; + } + + for (int i = 0; i < 2 - rewards.Count(x => x.Type == TimeSpaceRewardType.SPECIAL); i++) + { + repayPacket += " -1.0.0"; + } + + repayPacket += selectedDrawItem == null ? " -1.0.0" : $" {selectedDrawItem.ItemVnum}.{selectedDrawItem.Rarity}.{selectedDrawItem.Amount}"; + return repayPacket; + } + + public static string GenerateScorePacket(this IClientSession session, TimeSpaceFinishType timeSpaceFinishType, + bool isMonsterPerfect = false, bool isNpcPerfect = false, bool isMapsPerfect = false) + { + TimeSpaceParty timeSpace = session.PlayerEntity.TimeSpaceComponent.TimeSpace; + TimeSpaceInstance timeSpaceInstance = session.PlayerEntity.TimeSpaceComponent.TimeSpace.Instance; + return "score " + + $"{(int)timeSpaceFinishType} " + + $"{timeSpaceInstance.Score} " + + "500 " + + "500 " + + "500 " + + $"{(timeSpace.IsEasyMode ? 0 : timeSpace.TimeSpaceInformation.Rewards.Draw?.Count ?? 0)} " + + $"{timeSpaceInstance.KilledMonsters} " + + $"{timeSpaceInstance.EnteredRooms} " + + $"{timeSpaceInstance.SavedNpcs} " + + $"{(isMonsterPerfect ? 1 : 0)}{(isMapsPerfect ? 1 : 0)}{(isNpcPerfect ? 1 : 0)} " + + "1 " + + "15"; + } + + public static IEnumerable GenerateTimeSpacePortals(this IClientSession session) + { + var packets = new List(); + + foreach (ITimeSpacePortalEntity timeSpace in session.CurrentMapInstance.TimeSpacePortals) + { + byte type = (byte)(session.PlayerEntity.Level < timeSpace.MinLevel ? 0 : 1); + + if (session.PlayerEntity.CompletedTimeSpaces.Contains(timeSpace.TimeSpaceId)) + { + type = 4; + } + + if (timeSpace.IsHero) + { + type = 8; + } + + packets.Add($"wp {timeSpace.Position.X} {timeSpace.Position.Y} {timeSpace.TimeSpaceId} {type} {timeSpace.MinLevel} {timeSpace.MaxLevel}"); + } + + return packets; + } + + public static void SendTimeSpacePortals(this IClientSession session) + { + session.SendPackets(session.GenerateTimeSpacePortals()); + } + + public static string GenerateTimeSpaceInfo(this IClientSession session, ITimeSpacePortalEntity portalEntity, TimeSpaceRecordDto timeSpaceRecordDto) + { + byte type = (byte)(session.PlayerEntity.Level < portalEntity.MinLevel ? 0 : 1); + bool completed = session.PlayerEntity.CompletedTimeSpaces.Contains(portalEntity.TimeSpaceId); + + if (completed) + { + type = 4; + } + + string name = session.GetLanguage(portalEntity.Name); + string desc = session.GetLanguage(portalEntity.Description); + + var drawRewards = new StringBuilder(); + var specialRewards = new StringBuilder(); + var bonusRewards = new StringBuilder(); + + byte drawCount = 5; + byte specialCount = 2; + byte bonusCount = 3; + + foreach ((short vnum, short amount) in portalEntity.DrawRewards) + { + drawCount--; + drawRewards.Append($"{vnum}.{amount} "); + } + + for (int i = 0; i < drawCount; i++) + { + drawRewards.Append("-1.0 "); + } + + foreach ((short vnum, short amount) in portalEntity.SpecialRewards) + { + specialCount--; + specialRewards.Append($"{vnum}.{amount} "); + } + + for (int i = 0; i < specialCount; i++) + { + specialRewards.Append("-1.0 "); + } + + foreach ((short vnum, short amount) in portalEntity.BonusRewards) + { + bonusCount--; + bonusRewards.Append($"{vnum}.{amount} "); + } + + for (int i = 0; i < bonusCount; i++) + { + bonusRewards.Append("-1.0 "); + } + + return + $"rbr {portalEntity.TimeSpaceId}.{(portalEntity.IsHero ? 1 : portalEntity.IsHidden || portalEntity.IsSpecial ? 2 : 0)}.0 {type} {(completed ? 15 : 0)} " + + $"{portalEntity.MinLevel}.{portalEntity.MaxLevel} {portalEntity.SeedsOfPowerRequired} " + + $"{drawRewards}{specialRewards}{bonusRewards}{(timeSpaceRecordDto == null ? "-1.0" : $"{timeSpaceRecordDto.Record}.{timeSpaceRecordDto.CharacterName}")} 0 " + + $"{(portalEntity.IsHidden || portalEntity.IsSpecial ? 0 : 1)} {name}\n{desc}"; + } + + public static string GenerateTimeSpaceInfo(this IClientSession session, INpcEntity npcEntity, + List<(short, short)> drawRewards, List<(short, short)> specialRewards, List<(short, short)> bonusRewards, TimeSpaceRecordDto timeSpaceRecordDto) + { + byte type = (byte)(session.PlayerEntity.Level < npcEntity.TimeSpaceInfo.MinLevel ? 0 : 1); + bool completed = session.PlayerEntity.CompletedTimeSpaces.Contains(npcEntity.TimeSpaceInfo.TsId); + + if (completed) + { + type = 4; + } + + string name = session.GetLanguage(npcEntity.TimeSpaceInfo.Name); + string desc = session.GetLanguage(npcEntity.TimeSpaceInfo.Description); + + var drawRewardsPacket = new StringBuilder(); + var specialRewardsPacket = new StringBuilder(); + var bonusRewardsPacket = new StringBuilder(); + + byte drawCount = 5; + byte specialCount = 2; + byte bonusCount = 3; + + foreach ((short vnum, short amount) in drawRewards) + { + drawCount--; + drawRewardsPacket.Append($"{vnum}.{amount} "); + } + + for (int i = 0; i < drawCount; i++) + { + drawRewardsPacket.Append("-1.0 "); + } + + foreach ((short vnum, short amount) in specialRewards) + { + specialCount--; + specialRewardsPacket.Append($"{vnum}.{amount} "); + } + + for (int i = 0; i < specialCount; i++) + { + specialRewardsPacket.Append("-1.0 "); + } + + foreach ((short vnum, short amount) in bonusRewards) + { + bonusCount--; + bonusRewardsPacket.Append($"{vnum}.{amount} "); + } + + for (int i = 0; i < bonusCount; i++) + { + bonusRewardsPacket.Append("-1.0 "); + } + + return + $"rbr {npcEntity.TimeSpaceInfo.TsId}.{(npcEntity.TimeSpaceInfo.IsHero ? 1 : npcEntity.TimeSpaceInfo.IsHidden || npcEntity.TimeSpaceInfo.IsSpecial ? 2 : 0)}.0 {type} {(completed ? 15 : 0)} " + + $"{npcEntity.TimeSpaceInfo.MinLevel}.{npcEntity.TimeSpaceInfo.MaxLevel} {npcEntity.TimeSpaceInfo.SeedsOfPowerRequired} " + + $"{drawRewardsPacket}{specialRewardsPacket}{bonusRewardsPacket}{(timeSpaceRecordDto == null ? "-1.0" : $"{timeSpaceRecordDto.Record}.{timeSpaceRecordDto.CharacterName}")} 0 " + + $"{(npcEntity.TimeSpaceInfo.IsHidden || npcEntity.TimeSpaceInfo.IsSpecial ? 0 : 1)} {name}\n{desc}"; + } + + public static void SendTimeSpaceInfo(this IClientSession session, ITimeSpacePortalEntity portal, TimeSpaceRecordDto timeSpaceRecordDto) + { + session.SendPacket(session.GenerateTimeSpaceInfo(portal, timeSpaceRecordDto)); + } + + public static void SendTimeSpaceInfo(this IClientSession session, INpcEntity npc, TimeSpaceRecordDto timeSpaceRecordDto) + { + TimeSpaceFileConfiguration timeSpaceFileConfiguration = npc.TimeSpaceInfo; + List<(short, short)> drawRewards = new(); + List<(short, short)> specialRewards = new(); + List<(short, short)> bonusRewards = new(); + if (timeSpaceFileConfiguration.Rewards?.Draw != null) + { + foreach (TimeSpaceItemConfiguration draw in timeSpaceFileConfiguration.Rewards.Draw) + { + drawRewards.Add((draw.ItemVnum, draw.Amount)); + } + } + + if (timeSpaceFileConfiguration.Rewards?.Special != null) + { + foreach (TimeSpaceItemConfiguration special in timeSpaceFileConfiguration.Rewards.Special) + { + specialRewards.Add((special.ItemVnum, special.Amount)); + } + } + + if (timeSpaceFileConfiguration.Rewards?.Bonus != null) + { + foreach (TimeSpaceItemConfiguration bonus in timeSpaceFileConfiguration.Rewards.Bonus) + { + bonusRewards.Add((bonus.ItemVnum, bonus.Amount)); + } + } + + session.SendPacket(session.GenerateTimeSpaceInfo(npc, drawRewards, specialRewards, bonusRewards, timeSpaceRecordDto)); + } + + public static string GenerateRsfiPacket(this IClientSession session, ISubActConfiguration subActConfiguration, ITimeSpaceConfiguration timeSpaceConfiguration) + { + long lastCompletedTimeSpace = 0; + + foreach (long tsId in session.PlayerEntity.CompletedTimeSpaces.Reverse()) + { + TimeSpaceFileConfiguration getTimeSpace = timeSpaceConfiguration.GetTimeSpaceConfiguration(tsId); + if (getTimeSpace == null) + { + continue; + } + + if (getTimeSpace.IsSpecial || getTimeSpace.IsHidden) + { + continue; + } + + lastCompletedTimeSpace = tsId; + break; + } + + SubActsConfiguration getAct = subActConfiguration.GetConfigurationByTimeSpaceId(lastCompletedTimeSpace); + + if (getAct == null) + { + return "rsfi 0 0 0 0 0 0"; + } + + int timeSpaceCount = getAct.TimeSpaces.Length; + int timeSpacesDone = getAct.TimeSpaces.Count(timeSpace => session.PlayerEntity.CompletedTimeSpaces.Contains(timeSpace)); + + // Get next act + if (timeSpaceCount == timeSpacesDone) + { + SubActsConfiguration newAct = null; + int counter = subActConfiguration.GetConfigurations().Count(); + int count = 0; + while (counter > 0) + { + counter--; + count++; + newAct = subActConfiguration.GetConfigurationById(getAct.Id + count); + if (newAct != null) + { + break; + } + } + + if (newAct != null) + { + getAct = newAct; + timeSpaceCount = getAct.TimeSpaces.Length; + timeSpacesDone = getAct.TimeSpaces.Count(timeSpace => session.PlayerEntity.CompletedTimeSpaces.Contains(timeSpace)); + } + } + + return $"rsfi {getAct.Act} {getAct.SubAct} {timeSpacesDone} {timeSpaceCount} {timeSpacesDone} {timeSpaceCount}"; + } + + public static void SendRsfiPacket(this IClientSession session, ISubActConfiguration subActConfiguration, ITimeSpaceConfiguration timeSpaceConfiguration) + { + session.SendPacket(session.GenerateRsfiPacket(subActConfiguration, timeSpaceConfiguration)); + } + + public static string GenerateTimerFreeze(this IClientSession session) => "guri 8 8"; + + public static void SendTimerFreeze(this IClientSession session) => session.SendPacket(session.GenerateTimerFreeze()); + + public static string GenerateNpcReqPacket(this IClientSession session, int dialog) => $"npc_req 1 {session.PlayerEntity.Id} {dialog}"; + + public static void SendNpcReqPacket(this IClientSession session, int dialog) => session.SendPacket(session.GenerateNpcReqPacket(dialog)); + + public static int CalculateGoldReward(this TimeSpaceParty timeSpaceParty) => (int)(timeSpaceParty.TimeSpaceInformation.MinLevel * + (2 + Math.Floor(timeSpaceParty.TimeSpaceInformation.MinLevel / 10.0) / 5) * timeSpaceParty.Instance.KilledMonsters); + + public static long CalculateExperience(this TimeSpaceParty timeSpaceParty) => timeSpaceParty.TimeSpaceInformation.MinLevel * 3 * timeSpaceParty.Instance.KilledMonsters; + + public static int GetTimeSpacePenalty(this IClientSession session) + { + TimeSpaceParty timeSpaceParty = session.PlayerEntity.TimeSpaceComponent.TimeSpace; + + int difference = session.PlayerEntity.Level - timeSpaceParty.TimeSpaceInformation.MinLevel; + + switch (difference) + { + case <= 0: + return 0; + case > 50: + difference = 50; + break; + } + + return difference * 2; + } + + public static int GetTimeSpaceScorePenalty(this TimeSpaceParty timeSpace) + { + int difference = timeSpace.HigherLevel - timeSpace.TimeSpaceInformation.MinLevel; + + switch (difference) + { + case <= 0: + return 0; + case > 50: + difference = 50; + break; + } + + return difference * 2; + } + + public static bool CanJoinToTimeSpace(this IClientSession session, long timeSpaceId, ISubActConfiguration subActConfiguration, ITimeSpaceConfiguration timeSpaceConfig) + { + TimeSpaceFileConfiguration timeSpaceConfigInfo = timeSpaceConfig.GetTimeSpaceConfiguration(timeSpaceId); + SubActsConfiguration timeSpaceConfiguration = subActConfiguration.GetConfigurationByTimeSpaceId(timeSpaceId); + if (timeSpaceConfiguration == null || timeSpaceConfigInfo == null) + { + return false; + } + + bool canJoin = true; + SubActsConfiguration getPreviousTimeSpaceConfiguration = null; + int count = 0; + int counter = subActConfiguration.GetConfigurations().Count(); + + while (counter > count) + { + count++; + getPreviousTimeSpaceConfiguration = subActConfiguration.GetConfigurationById(timeSpaceConfiguration.Id - count); + if (getPreviousTimeSpaceConfiguration?.TimeSpaces == null || !getPreviousTimeSpaceConfiguration.TimeSpaces.Any()) + { + continue; + } + + break; + } + + if (getPreviousTimeSpaceConfiguration?.TimeSpaces != null) + { + long getLastTimeSpace = getPreviousTimeSpaceConfiguration.TimeSpaces.LastOrDefault(); + if (!session.PlayerEntity.CompletedTimeSpaces.Contains(getLastTimeSpace)) + { + canJoin = false; + } + } + + if (!timeSpaceConfigInfo.IsHero) + { + return canJoin; + } + + foreach (long timeSpace in timeSpaceConfiguration.TimeSpaces) + { + if (timeSpace >= timeSpaceId) + { + continue; + } + + if (session.PlayerEntity.CompletedTimeSpaces.Contains(timeSpace)) + { + continue; + } + + canJoin = false; + break; + } + + return canJoin; + } + + public static void SendScorePacket(this IClientSession session, TimeSpaceFinishType timeSpaceFinishType, bool isMonsterPerfect = false, bool isNpcPerfect = false, bool isMapsPerfect = false) + => session.SendPacket(session.GenerateScorePacket(timeSpaceFinishType, isMonsterPerfect, isNpcPerfect, isMapsPerfect)); + + public static void SendRsfpPacket(this IClientSession session) => session.SendPacket(session.GenerateRsfpPacket()); + + public static void SendRsfmPacket(this IClientSession session, TimeSpaceAction action, byte spiralFloor = 0) + { + TimeSpaceParty timeSpaceParty = session.PlayerEntity.TimeSpaceComponent.TimeSpace; + if (timeSpaceParty?.Instance == null) + { + return; + } + + session.SendPacket(session.GenerateRsfmPacket(action, spiralFloor)); + } + + public static void SendRepayPacket(this IClientSession session, IReadOnlyList rewards, TimeSpaceRewardItem selectedDrawItem) => + session.SendPacket(session.GenerateRepayPacket(rewards, selectedDrawItem)); + + public static bool IsInSpecialOrHiddenTimeSpace(this IClientSession session) + { + TimeSpaceParty timeSpace = session.PlayerEntity.TimeSpaceComponent.TimeSpace; + if (timeSpace == null) + { + return false; + } + + return timeSpace.TimeSpaceInformation.IsSpecial || timeSpace.TimeSpaceInformation.IsHidden; + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/TimeSpaces/TimeSpaceParty.cs b/srcs/WingsAPI.Game/TimeSpaces/TimeSpaceParty.cs new file mode 100644 index 0000000..470630e --- /dev/null +++ b/srcs/WingsAPI.Game/TimeSpaces/TimeSpaceParty.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using WingsEmu.Core.Generics; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Game.TimeSpaces; + +public class TimeSpaceParty +{ + private readonly ReaderWriterLockSlim _lock = new(); + private readonly List _members; + + public TimeSpaceParty(TimeSpaceFileConfiguration timeSpaceInformation, bool isEasyMode, bool isChallengeMode) + { + Id = Guid.NewGuid(); + TimeSpaceInformation = timeSpaceInformation; + IsEasyMode = isEasyMode; + IsChallengeMode = isChallengeMode; + TimeSpaceId = timeSpaceInformation.TsId; + _members = new List(timeSpaceInformation.MaxPlayers); + } + + public Guid Id { get; } + public bool Entered { get; private set; } + public bool Started { get; private set; } + public bool Finished { get; private set; } + public bool Destroy { get; set; } + public long TimeSpaceId { get; } + + public IReadOnlyList Members + { + get + { + _lock.EnterReadLock(); + try + { + return _members.FindAll(x => x != null); + } + finally + { + _lock.ExitReadLock(); + } + } + } + + public TimeSpaceInstance Instance { get; private set; } + public IClientSession Leader => Members.FirstOrDefault(); + public ThreadSafeHashSet ClaimedRewards { get; } = new(); + public ThreadSafeHashSet FirstCompletedTimeSpaceIds { get; } = new(); + public TimeSpaceFileConfiguration TimeSpaceInformation { get; } + public bool IsEasyMode { get; } + public bool IsChallengeMode { get; } + public byte HigherLevel { get; set; } + public DateTime LastObjectivesCheck { get; set; } + public int? ItemVnumToRemove { get; set; } + + public void SetEnteredTimeSpace(TimeSpaceInstance timeSpace) + { + Entered = true; + Instance = timeSpace; + } + + public void StartTimeSpace() + { + Started = true; + Instance.UpdateFinishDate(); + } + + public void AddMember(IClientSession session) + { + _lock.EnterWriteLock(); + try + { + _members.Add(session); + } + finally + { + _lock.ExitWriteLock(); + } + } + + public void RemoveMember(IClientSession session) + { + _lock.EnterWriteLock(); + try + { + _members.Remove(session); + } + finally + { + _lock.ExitWriteLock(); + } + } + + public void FinishTimeSpace(DateTime toRemove) + { + Finished = true; + Instance?.SetDestroyDate(toRemove); + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/TimeSpaces/TimeSpaceRewardItem.cs b/srcs/WingsAPI.Game/TimeSpaces/TimeSpaceRewardItem.cs new file mode 100644 index 0000000..bcc3c17 --- /dev/null +++ b/srcs/WingsAPI.Game/TimeSpaces/TimeSpaceRewardItem.cs @@ -0,0 +1,16 @@ +namespace WingsEmu.Game.TimeSpaces; + +public class TimeSpaceRewardItem +{ + public TimeSpaceRewardType Type { get; set; } + public int ItemVnum { get; set; } + public int Amount { get; set; } + public sbyte Rarity { get; set; } +} + +public enum TimeSpaceRewardType +{ + DRAW, + BONUS, + SPECIAL +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/TimeSpaces/TimeSpaceSubInstance.cs b/srcs/WingsAPI.Game/TimeSpaces/TimeSpaceSubInstance.cs new file mode 100644 index 0000000..1565408 --- /dev/null +++ b/srcs/WingsAPI.Game/TimeSpaces/TimeSpaceSubInstance.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Triggers; + +namespace WingsEmu.Game.TimeSpaces; + +public class TimeSpaceSubInstance : IEventTriggerContainer +{ + private readonly IEventTriggerContainer _eventTriggerContainer; + + public TimeSpaceSubInstance(IMapInstance mapInstance, IAsyncEventPipeline asyncEventPipeline) + { + MapInstance = mapInstance; + _eventTriggerContainer = new EventTriggerContainer(asyncEventPipeline); + } + + public IMapInstance MapInstance { get; } + + public DateTime? TimeSpaceWave { get; set; } + public List TimeSpaceWaves { get; set; } = new(); + + public TimeSpaceTask Task { get; set; } + public long? MonsterBonusId { get; set; } + public int MonsterBonusCombo { get; set; } + public bool SendPortalOpenMessage { get; set; } + public DateTime LastTryFinishTime { get; set; } + + public Dictionary> SpawnAfterMobsKilled { get; } = new(); + + public void AddEvent(string key, IAsyncEvent notification, bool removedOnTrigger = false) => _eventTriggerContainer.AddEvent(key, notification, removedOnTrigger); + + public Task TriggerEvents(string key) => _eventTriggerContainer.TriggerEvents(key); +} + +public class TimeSpaceWave +{ + public IReadOnlyList Monsters { get; init; } + public TimeSpan Delay { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/TimeSpaces/TimeSpaceTask.cs b/srcs/WingsAPI.Game/TimeSpaces/TimeSpaceTask.cs new file mode 100644 index 0000000..d2feaf8 --- /dev/null +++ b/srcs/WingsAPI.Game/TimeSpaces/TimeSpaceTask.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using WingsEmu.Game.Entities; +using WingsEmu.Game.TimeSpaces.Enums; + +namespace WingsEmu.Game.TimeSpaces; + +public class TimeSpaceTask +{ + public TimeSpaceTaskType TaskType { get; set; } + public TimeSpan? Time { get; set; } + public string GameDialogKey { get; set; } + public bool IsActivated { get; set; } + public bool IsFinished { get; set; } + public DateTime TaskStart { get; set; } + public DateTime? TimeLeft { get; set; } + public List<(int?, IMonsterEntity)> MonstersAfterTaskStart { get; } = new(); + public int? StartDialog { get; set; } + public bool StartDialogIsObjective { get; set; } + public bool DialogStartTask { get; set; } + public int? EndDialog { get; set; } + public bool EndDialogIsObjective { get; set; } + public string StartDialogShout { get; set; } + public string EndDialogShout { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/ToSummon.cs b/srcs/WingsAPI.Game/ToSummon.cs new file mode 100644 index 0000000..5f36251 --- /dev/null +++ b/srcs/WingsAPI.Game/ToSummon.cs @@ -0,0 +1,44 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using PhoenixLib.Events; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Monster; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game; + +public class ToSummon +{ + public int VNum { get; init; } + public Position? SpawnCell { get; init; } + public IBattleEntity Target { get; init; } + public bool IsMoving { get; init; } + public byte SummonChance { get; init; } = 100; + public bool RemoveTick { get; init; } + public bool IsTarget { get; init; } + public bool IsBonusOrProtected { get; init; } + public bool IsHostile { get; init; } + public bool IsBossOrMate { get; init; } + public bool IsVesselMonster { get; init; } + public SummonType? SummonType { get; init; } + public IMonsterEntity MonsterEntity { get; init; } + public IReadOnlyCollection<(string key, IAsyncEvent asyncEvent, bool removeOnUse)> TriggerEvents { get; init; } + public bool IsMateTrainer { get; init; } + public short SetHitChance { get; init; } + public Position? GoToBossPosition { get; init; } + public bool IsInstantBattle { get; init; } + public byte Direction { get; init; } = 2; + public byte? Level { get; init; } + public float? HpMultiplier { get; init; } + public float? MpMultiplier { get; init; } + public FactionType? FactionType { get; init; } + public Guid? AtAroundMobId { get; init; } + public byte? AtAroundMobRange { get; init; } + public ConcurrentDictionary Waypoints { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Triggers/BattleTriggers.cs b/srcs/WingsAPI.Game/Triggers/BattleTriggers.cs new file mode 100644 index 0000000..ce6bf53 --- /dev/null +++ b/srcs/WingsAPI.Game/Triggers/BattleTriggers.cs @@ -0,0 +1,10 @@ +namespace WingsEmu.Game.Triggers; + +public static class BattleTriggers +{ + public const string OnDeath = "OnDeath"; + public const string OnHit = "OnHit"; + public const string OnHalfHp = "OnHalfHp"; + public const string OnQuarterHp = "OnQuarterHp"; + public const string OnThreeFourthsHp = "OnThreeFourthsHp"; +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Triggers/EventTriggerContainer.cs b/srcs/WingsAPI.Game/Triggers/EventTriggerContainer.cs new file mode 100644 index 0000000..9b3bce2 --- /dev/null +++ b/srcs/WingsAPI.Game/Triggers/EventTriggerContainer.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Concurrent; +using System.Linq; +using System.Threading.Tasks; +using PhoenixLib.Events; +using PhoenixLib.Logging; +using WingsEmu.Core.Generics; + +namespace WingsEmu.Game.Triggers; + +public class EventTriggerContainer : IEventTriggerContainer +{ + private readonly IAsyncEventPipeline _eventPipeline; + private readonly ConcurrentDictionary> _events = new(); + + public EventTriggerContainer(IAsyncEventPipeline eventPipeline) => _eventPipeline = eventPipeline; + + public void AddEvent(string key, IAsyncEvent notification, bool removedOnTrigger = false) + { + if (!_events.TryGetValue(key, out ThreadSafeHashSet registeredEvents)) + { + registeredEvents = new ThreadSafeHashSet(); + _events[key] = registeredEvents; + } + + registeredEvents.Add(new RegisteredEvent(notification, removedOnTrigger)); + } + + public async Task TriggerEvents(string key) + { + if (string.IsNullOrWhiteSpace(key)) + { + return; + } + + if (!_events.TryGetValue(key, out ThreadSafeHashSet registeredEvents)) + { + return; + } + + if (registeredEvents == null || !registeredEvents.Any()) + { + return; + } + + try + { + var removeEvents = new ThreadSafeHashSet(); + foreach (RegisteredEvent registeredEvent in registeredEvents.Reverse().ToArray()) + { + if (registeredEvent == null) + { + continue; + } + + await _eventPipeline.ProcessEventAsync(registeredEvent.Notification); + if (registeredEvent.RemovedOnTrigger) + { + removeEvents.Add(registeredEvent); + } + } + + foreach (RegisteredEvent registeredEvent in removeEvents) + { + registeredEvents.Remove(registeredEvent); + } + } + catch (Exception e) + { + Log.Error("TriggerEvents", e); + } + } + + private class RegisteredEvent + { + public RegisteredEvent(IAsyncEvent notification, bool removedOnTrigger) + { + Notification = notification; + RemovedOnTrigger = removedOnTrigger; + } + + public IAsyncEvent Notification { get; } + public bool RemovedOnTrigger { get; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Triggers/IEventTriggerContainer.cs b/srcs/WingsAPI.Game/Triggers/IEventTriggerContainer.cs new file mode 100644 index 0000000..3908d9e --- /dev/null +++ b/srcs/WingsAPI.Game/Triggers/IEventTriggerContainer.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using PhoenixLib.Events; + +namespace WingsEmu.Game; + +public interface IEventTriggerContainer +{ + public void AddEvent(string key, IAsyncEvent notification, bool removedOnTrigger = false); + public Task TriggerEvents(string key); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Upgrades/Cellons/CellonChances.cs b/srcs/WingsAPI.Game/Upgrades/Cellons/CellonChances.cs new file mode 100644 index 0000000..67fa53e --- /dev/null +++ b/srcs/WingsAPI.Game/Upgrades/Cellons/CellonChances.cs @@ -0,0 +1,7 @@ +namespace WingsEmu.Game.Cellons; + +public class CellonChances +{ + public int CellonAmount { get; set; } + public double SuccessChance { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Upgrades/Cellons/CellonOption.cs b/srcs/WingsAPI.Game/Upgrades/Cellons/CellonOption.cs new file mode 100644 index 0000000..82bee15 --- /dev/null +++ b/srcs/WingsAPI.Game/Upgrades/Cellons/CellonOption.cs @@ -0,0 +1,14 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsEmu.Core; +using WingsEmu.Game._enum; + +namespace WingsEmu.Game.Cellons; + +public class CellonOption +{ + public CellonType Type { get; set; } + public Range Range { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Upgrades/Cellons/CellonPossibilities.cs b/srcs/WingsAPI.Game/Upgrades/Cellons/CellonPossibilities.cs new file mode 100644 index 0000000..c295a80 --- /dev/null +++ b/srcs/WingsAPI.Game/Upgrades/Cellons/CellonPossibilities.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace WingsEmu.Game.Cellons; + +public class CellonPossibilities +{ + public int CellonLevel { get; set; } + public int Price { get; set; } + public HashSet Options { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Upgrades/Cellons/CellonSystemConfiguration.cs b/srcs/WingsAPI.Game/Upgrades/Cellons/CellonSystemConfiguration.cs new file mode 100644 index 0000000..269a4e0 --- /dev/null +++ b/srcs/WingsAPI.Game/Upgrades/Cellons/CellonSystemConfiguration.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace WingsEmu.Game.Cellons; + +public class CellonSystemConfiguration +{ + public HashSet Options { get; set; } + public HashSet ChancesToSuccess { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Warehouse/Events/AccountWarehouseAddItemEvent.cs b/srcs/WingsAPI.Game/Warehouse/Events/AccountWarehouseAddItemEvent.cs new file mode 100644 index 0000000..e9e1b1f --- /dev/null +++ b/srcs/WingsAPI.Game/Warehouse/Events/AccountWarehouseAddItemEvent.cs @@ -0,0 +1,11 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Game.Inventory; + +namespace WingsEmu.Game.Warehouse.Events; + +public class AccountWarehouseAddItemEvent : PlayerEvent +{ + public InventoryItem Item { get; init; } + public short Amount { get; init; } + public short SlotDestination { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Warehouse/Events/AccountWarehouseMoveEvent.cs b/srcs/WingsAPI.Game/Warehouse/Events/AccountWarehouseMoveEvent.cs new file mode 100644 index 0000000..081aae3 --- /dev/null +++ b/srcs/WingsAPI.Game/Warehouse/Events/AccountWarehouseMoveEvent.cs @@ -0,0 +1,17 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Warehouse.Events; + +public class AccountWarehouseMoveEvent : PlayerEvent +{ + public AccountWarehouseMoveEvent(short originalSlot, short amount, short newSlot) + { + OriginalSlot = originalSlot; + Amount = amount; + NewSlot = newSlot; + } + + public short OriginalSlot { get; } + public short Amount { get; } + public short NewSlot { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Warehouse/Events/AccountWarehouseOpenEvent.cs b/srcs/WingsAPI.Game/Warehouse/Events/AccountWarehouseOpenEvent.cs new file mode 100644 index 0000000..1d96a1b --- /dev/null +++ b/srcs/WingsAPI.Game/Warehouse/Events/AccountWarehouseOpenEvent.cs @@ -0,0 +1,7 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Warehouse.Events; + +public class AccountWarehouseOpenEvent : PlayerEvent +{ +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Warehouse/Events/AccountWarehouseShowItemEvent.cs b/srcs/WingsAPI.Game/Warehouse/Events/AccountWarehouseShowItemEvent.cs new file mode 100644 index 0000000..73d1b73 --- /dev/null +++ b/srcs/WingsAPI.Game/Warehouse/Events/AccountWarehouseShowItemEvent.cs @@ -0,0 +1,8 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Warehouse.Events; + +public class AccountWarehouseShowItemEvent : PlayerEvent +{ + public short Slot { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Warehouse/Events/AccountWarehouseWithdrawItemEvent.cs b/srcs/WingsAPI.Game/Warehouse/Events/AccountWarehouseWithdrawItemEvent.cs new file mode 100644 index 0000000..1ea3060 --- /dev/null +++ b/srcs/WingsAPI.Game/Warehouse/Events/AccountWarehouseWithdrawItemEvent.cs @@ -0,0 +1,16 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Warehouse.Events; + +public class AccountWarehouseWithdrawItemEvent : PlayerEvent +{ + public AccountWarehouseWithdrawItemEvent(short slot, short amount) + { + Slot = slot; + Amount = amount; + } + + public short Slot { get; } + + public short Amount { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Warehouse/Events/PartnerWarehouseAddItemEvent.cs b/srcs/WingsAPI.Game/Warehouse/Events/PartnerWarehouseAddItemEvent.cs new file mode 100644 index 0000000..50695e4 --- /dev/null +++ b/srcs/WingsAPI.Game/Warehouse/Events/PartnerWarehouseAddItemEvent.cs @@ -0,0 +1,9 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Game.Items; + +namespace WingsEmu.Game.Warehouse.Events; + +public class PartnerWarehouseAddItemEvent : PlayerEvent +{ + public GameItemInstance ItemInstance { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Warehouse/Events/PartnerWarehouseDepositEvent.cs b/srcs/WingsAPI.Game/Warehouse/Events/PartnerWarehouseDepositEvent.cs new file mode 100644 index 0000000..0ec31e1 --- /dev/null +++ b/srcs/WingsAPI.Game/Warehouse/Events/PartnerWarehouseDepositEvent.cs @@ -0,0 +1,20 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Game.Warehouse.Events; + +public class PartnerWarehouseDepositEvent : PlayerEvent +{ + public PartnerWarehouseDepositEvent(InventoryType inventoryType, short inventorySlot, short amount, short slotDestination) + { + InventoryType = inventoryType; + InventorySlot = inventorySlot; + Amount = amount; + SlotDestination = slotDestination; + } + + public InventoryType InventoryType { get; } + public short InventorySlot { get; } + public short Amount { get; } + public short SlotDestination { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Warehouse/Events/PartnerWarehouseMoveEvent.cs b/srcs/WingsAPI.Game/Warehouse/Events/PartnerWarehouseMoveEvent.cs new file mode 100644 index 0000000..0e26921 --- /dev/null +++ b/srcs/WingsAPI.Game/Warehouse/Events/PartnerWarehouseMoveEvent.cs @@ -0,0 +1,17 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Warehouse.Events; + +public class PartnerWarehouseMoveEvent : PlayerEvent +{ + public PartnerWarehouseMoveEvent(short originalSlot, short amount, short newSlot) + { + OriginalSlot = originalSlot; + Amount = amount; + NewSlot = newSlot; + } + + public short OriginalSlot { get; } + public short Amount { get; } + public short NewSlot { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Warehouse/Events/PartnerWarehouseWithdrawEvent.cs b/srcs/WingsAPI.Game/Warehouse/Events/PartnerWarehouseWithdrawEvent.cs new file mode 100644 index 0000000..fc3e3a4 --- /dev/null +++ b/srcs/WingsAPI.Game/Warehouse/Events/PartnerWarehouseWithdrawEvent.cs @@ -0,0 +1,15 @@ +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Warehouse.Events; + +public class PartnerWarehouseWithdrawEvent : PlayerEvent +{ + public PartnerWarehouseWithdrawEvent(short slot, short amount) + { + Slot = slot; + Amount = amount; + } + + public short Slot { get; } + public short Amount { get; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Warehouse/Events/WarehouseItemPlacedEvent.cs b/srcs/WingsAPI.Game/Warehouse/Events/WarehouseItemPlacedEvent.cs new file mode 100644 index 0000000..4c3d5ac --- /dev/null +++ b/srcs/WingsAPI.Game/Warehouse/Events/WarehouseItemPlacedEvent.cs @@ -0,0 +1,11 @@ +using WingsEmu.DTOs.Items; +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Warehouse.Events; + +public class WarehouseItemPlacedEvent : PlayerEvent +{ + public ItemInstanceDTO ItemInstance { get; init; } + public int Amount { get; init; } + public short DestinationSlot { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Warehouse/Events/WarehouseItemWithdrawnEvent.cs b/srcs/WingsAPI.Game/Warehouse/Events/WarehouseItemWithdrawnEvent.cs new file mode 100644 index 0000000..b55c062 --- /dev/null +++ b/srcs/WingsAPI.Game/Warehouse/Events/WarehouseItemWithdrawnEvent.cs @@ -0,0 +1,11 @@ +using WingsEmu.DTOs.Items; +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Game.Warehouse.Events; + +public class WarehouseItemWithdrawnEvent : PlayerEvent +{ + public ItemInstanceDTO ItemInstance { get; init; } + public int Amount { get; init; } + public short FromSlot { get; init; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Warehouse/IAccountWarehouseManager.cs b/srcs/WingsAPI.Game/Warehouse/IAccountWarehouseManager.cs new file mode 100644 index 0000000..5691de7 --- /dev/null +++ b/srcs/WingsAPI.Game/Warehouse/IAccountWarehouseManager.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using WingsAPI.Data.Account; +using WingsEmu.DTOs.Items; +using WingsEmu.Game._enum; + +namespace WingsEmu.Game.Warehouse; + +public interface IAccountWarehouseManager +{ + public Task<(IDictionary accountWarehouseItemDtos, ManagerResponseType?)> GetWarehouse(long accountId); + public Task<(AccountWarehouseItemDto, ManagerResponseType?)> GetWarehouseItem(long accountId, short slot); + public Task<(AccountWarehouseItemDto, ManagerResponseType?)> AddWarehouseItem(AccountWarehouseItemDto warehouseItemDtoToAdd); + public Task<(AccountWarehouseItemDto, ItemInstanceDTO, ManagerResponseType?)> WithdrawWarehouseItem(AccountWarehouseItemDto warehouseItemDtoToWithdraw, int amount); + public Task<(AccountWarehouseItemDto oldItem, AccountWarehouseItemDto newItem, ManagerResponseType?)> MoveWarehouseItem(AccountWarehouseItemDto warehouseItemDtoToMove, int amount, short newSlot); + public void CleanCache(long accountId); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Warehouse/IWarehouse.cs b/srcs/WingsAPI.Game/Warehouse/IWarehouse.cs new file mode 100644 index 0000000..7ac4f09 --- /dev/null +++ b/srcs/WingsAPI.Game/Warehouse/IWarehouse.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace WingsEmu.Game.Warehouse; + +public interface IWarehouse +{ + public void AddWarehouseItem(WarehouseItem item, bool force = false); + public void RemoveWarehouseItem(short slot); + public WarehouseItem GetWarehouseItem(short slot); + public IReadOnlyList WarehouseItems(); + public int GetWarehouseSlots(); + public bool HasSpaceForWarehouseItem(); +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Warehouse/PartnerWarehouseItem.cs b/srcs/WingsAPI.Game/Warehouse/PartnerWarehouseItem.cs new file mode 100644 index 0000000..d5e98a1 --- /dev/null +++ b/srcs/WingsAPI.Game/Warehouse/PartnerWarehouseItem.cs @@ -0,0 +1,9 @@ +using WingsEmu.DTOs.Inventory; +using WingsEmu.Game.Items; + +namespace WingsEmu.Game.Warehouse; + +public class PartnerWarehouseItem : PartnerWarehouseItemDto +{ + public GameItemInstance ItemInstance { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/Warehouse/WarehouseItem.cs b/srcs/WingsAPI.Game/Warehouse/WarehouseItem.cs new file mode 100644 index 0000000..c468e73 --- /dev/null +++ b/srcs/WingsAPI.Game/Warehouse/WarehouseItem.cs @@ -0,0 +1,9 @@ +using WingsAPI.Data.Account; +using WingsEmu.Game.Items; + +namespace WingsEmu.Game.Warehouse; + +public class WarehouseItem : AccountWarehouseItemDto +{ + public GameItemInstance ItemInstance { get; set; } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/WingsAPI.Game.csproj b/srcs/WingsAPI.Game/WingsAPI.Game.csproj new file mode 100644 index 0000000..fb5d37b --- /dev/null +++ b/srcs/WingsAPI.Game/WingsAPI.Game.csproj @@ -0,0 +1,22 @@ + + + + net5.0 + latest + WingsEmu.Game + + + + + + + + + + + + + + + + diff --git a/srcs/WingsAPI.Game/_enum/ActRaidTypes.cs b/srcs/WingsAPI.Game/_enum/ActRaidTypes.cs new file mode 100644 index 0000000..77dcef3 --- /dev/null +++ b/srcs/WingsAPI.Game/_enum/ActRaidTypes.cs @@ -0,0 +1,12 @@ +namespace WingsEmu.Game._enum; + +public enum ActRaidTypes : byte +{ + None = 0, + Morcos = 1, + Hatus = 2, + Calvina = 3, + Berios = 4, + Erenia = 5, + Zenas = 6 +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/_enum/ArmorItemSubType.cs b/srcs/WingsAPI.Game/_enum/ArmorItemSubType.cs new file mode 100644 index 0000000..f1df3d5 --- /dev/null +++ b/srcs/WingsAPI.Game/_enum/ArmorItemSubType.cs @@ -0,0 +1,13 @@ +namespace WingsEmu.Game._enum; + +/// +/// +public enum ArmorItemSubType +{ + AdventurerArmor = 0, + MageArmor = 1, + ArcherArmor = 2, + SwordmanArmor = 3, + PartnerArmor = 4, + MartialArtistArmor = 5 +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/_enum/BankRankType.cs b/srcs/WingsAPI.Game/_enum/BankRankType.cs new file mode 100644 index 0000000..1e23f5c --- /dev/null +++ b/srcs/WingsAPI.Game/_enum/BankRankType.cs @@ -0,0 +1,19 @@ +namespace WingsEmu.Game._enum; + +public enum BankRankType +{ + VIP, + DIAMOND, + + GOLD_1, + GOLD_2, + GOLD_3, + + SILVER_1, + SILVER_2, + SILVER_3, + + BRONZE_1, + BRONZE_2, + BRONZE_3 +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/_enum/BankType.cs b/srcs/WingsAPI.Game/_enum/BankType.cs new file mode 100644 index 0000000..0c5f72e --- /dev/null +++ b/srcs/WingsAPI.Game/_enum/BankType.cs @@ -0,0 +1,9 @@ +namespace WingsEmu.Game._enum; + +public enum BankType +{ + BankMoney = 0, + Deposit = 1, + Withdraw = 2, + OpenBank = 3 +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/_enum/BuffFlag.cs b/srcs/WingsAPI.Game/_enum/BuffFlag.cs new file mode 100644 index 0000000..c5e0f68 --- /dev/null +++ b/srcs/WingsAPI.Game/_enum/BuffFlag.cs @@ -0,0 +1,19 @@ +using System; + +namespace WingsEmu.Game._enum; + +[Flags] +public enum BuffFlag +{ + NORMAL = 1 << 0, // Normal buff, ends after x sec/min + BIG = 1 << 1, // Use vb packet instead of bf, doesn't disappear after death/transform SP + NO_DURATION = 1 << 2, // No duration - refreshes automatically if it somehow ends + SAVING_ON_DISCONNECT = 1 << 3, // The duration is kept after logging out + REFRESH_AT_EXPIRATION = 1 << 4, + NOT_REMOVED_ON_DEATH = 1 << 5, + NOT_REMOVED_ON_SP_CHANGE = 1 << 6, + DISAPPEAR_ON_PVP = 1 << 7, + + BIG_AND_KEEP_ON_LOGOUT = BIG | SAVING_ON_DISCONNECT, + PARTNER = NORMAL | NO_DURATION +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/_enum/BuffGroup.cs b/srcs/WingsAPI.Game/_enum/BuffGroup.cs new file mode 100644 index 0000000..0c1de30 --- /dev/null +++ b/srcs/WingsAPI.Game/_enum/BuffGroup.cs @@ -0,0 +1,12 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Game._enum; + +public enum BuffGroup : byte +{ + Good = 0, + Neutral = 1, + Bad = 2 +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/_enum/BuffGroupIds.cs b/srcs/WingsAPI.Game/_enum/BuffGroupIds.cs new file mode 100644 index 0000000..a939bf8 --- /dev/null +++ b/srcs/WingsAPI.Game/_enum/BuffGroupIds.cs @@ -0,0 +1,7 @@ +namespace WingsEmu.Game._enum; + +public enum BuffGroupIds +{ + BLEEDING = 1, + STUNS = 4 +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/_enum/BuffVnums.cs b/srcs/WingsAPI.Game/_enum/BuffVnums.cs new file mode 100644 index 0000000..d525392 --- /dev/null +++ b/srcs/WingsAPI.Game/_enum/BuffVnums.cs @@ -0,0 +1,80 @@ +namespace WingsEmu.Game._enum; + +public enum BuffVnums : short +{ + CHARGE = 0, + MINOR_BLEEDING = 1, + BLACKOUT = 7, + PVP = 15, + BLEEDING = 21, + FREEZE = 27, + BLIND = 37, + HEAVY_BLEEDING = 42, + RESURRECTION_SIDE_EFFECTS = 44, + HAND_OF_DEATH = 59, + DEADLY_BLACKOUT = 66, + WEAKEN_DEFENCE_POWER = 68, + SHOCK = 70, + BIND = 99, + PARALYSIS = 102, + + RAINBOW_ENERGY = 115, + ENERGY_ENHANCEMENT = 118, + ANCELLOAN_BLESS = 121, + GUARDIAN_BLESS = 122, + FAIRY_BOOSTER = 131, + PRAYER_OF_DEFENCE = 138, + SOULSTONE_BLESSING = 146, + BEAR_SPIRIT = 155, + + INVICIBLE_IN_PVP = 169, + + WEDDING = 319, + SPEED_BOOSTER = 336, + ACT_52_FIRE_DEBUFF = 339, + ICE_FLOWER = 340, + FAMILY_BUFF_XP = 360, + + SOUND_FLOWER_BLESSING = 378, + SOUND_FLOWER_BLESSING_BETTER = 379, + + FAIRYXP_POTION = 393, + + ANGRY = 445, + + KNOCKDOWN = 500, + LOSER_SIGH = 505, + EXPLOSIVE_ENCHACMENT = 507, + NOBLE_GESTURE = 531, + SPIRIT_OF_ENLIGHTENMENT = 532, + SPIRIT_OF_TEMPERANCE = 533, + SPIRIT_OF_STRENGTH = 534, + KUNDALINI_SYNDROME = 542, + SPIRIT_OF_SACRIFICE = 546, + + SONG_OF_THE_SIRENS = 548, + AMBUSH = 559, + AMBUSH_RAID = 560, + ETERNAL_ICE = 569, + SONG_OF_THE_SIRENS_PVP = 581, + + AMBUSH_POSITION_1 = 562, + AMBUSH_POSITION_2 = 563, + SNIPER_POSITION_1 = 564, + SNIPER_POSITION_2 = 565, + AMBUSH_PREPARATION_1 = 567, + AMBUSH_PREPARATION_2 = 568, + SPIRIT_ABSORPTION = 596, + MARK_OF_DEATH = 597, + + MAGICAL_FETTERS = 608, + FLAME = 609, + DARKNESS = 613, + MAGIC_SPELL = 617, + + ILLUMINATING_POWDER = 619, + MEMORIAL = 620, + CORRUPTION = 628, + + DAZZLE = 706 +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/_enum/CastType.cs b/srcs/WingsAPI.Game/_enum/CastType.cs new file mode 100644 index 0000000..1b64eec --- /dev/null +++ b/srcs/WingsAPI.Game/_enum/CastType.cs @@ -0,0 +1,10 @@ +namespace WingsEmu.Game._enum; + +public enum CastType +{ + SELF = 0, + ATTACK = 1, + DEFENSE = 2, + ALL_TARGETS = 3, + TARGET_SELF = 4 +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/_enum/CellonType.cs b/srcs/WingsAPI.Game/_enum/CellonType.cs new file mode 100644 index 0000000..8c770c6 --- /dev/null +++ b/srcs/WingsAPI.Game/_enum/CellonType.cs @@ -0,0 +1,15 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Game._enum; + +public enum CellonType : byte +{ + Hp, + Mp, + HpRecovery, + MpRecovery, + MpConsumption, + CriticalDamageDecrease +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/_enum/CostumeItemSubType.cs b/srcs/WingsAPI.Game/_enum/CostumeItemSubType.cs new file mode 100644 index 0000000..75087b0 --- /dev/null +++ b/srcs/WingsAPI.Game/_enum/CostumeItemSubType.cs @@ -0,0 +1,12 @@ +namespace WingsEmu.Game._enum; + +public enum CostumeItemSubType +{ + Hats = 0, + Masks = 1, + Gloves = 2, + Boots = 3, + CostumeBottom = 4, + CostumeTop = 5, + CostumeWeapon = 6 +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/_enum/DialogVnums.cs b/srcs/WingsAPI.Game/_enum/DialogVnums.cs new file mode 100644 index 0000000..6311145 --- /dev/null +++ b/srcs/WingsAPI.Game/_enum/DialogVnums.cs @@ -0,0 +1,15 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Game._enum; + +public enum DialogVnums +{ + NPC_REQ = 99, + SHOP_PLAYER = 501, + MINILAND_SIGN = 10000, + SMALL_CAMPFIRE = 10023, + BIG_CAMPFIRE = 10024, + ICE_MACHINE = 10026 +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/_enum/EffectType.cs b/srcs/WingsAPI.Game/_enum/EffectType.cs new file mode 100644 index 0000000..19536a8 --- /dev/null +++ b/srcs/WingsAPI.Game/_enum/EffectType.cs @@ -0,0 +1,91 @@ +namespace WingsEmu.Game._enum; + +public enum EffectType : short +{ + Respawn = 4, + ShinyStars = 5, + NormalLevelUp = 6, // Looks yellow + Summon = 7, + JobLevelUp = 8, // Looks white + BigWhiteLines = 11, // It's like when you take pvp snack or snack who give you an hp/mp excess but much bigger + BoostedAttack = 15, + ShinyDust = 18, + SpecialTeleport = 23, + Frozen = 35, + FireExplosion = 36, + EquipAmulet = 39, + DemonDignityRestore = 47, + AngelDignityRestore = 48, + SmallCritical = 191, + BlueMiss = 192, + RedMiss = 193, + SmokePuff = 194, + Transform = 196, + CatchSuccess = 197, + NormalLevelUpSubEffect = 198, // A sub effect sent with "NormalLevelUp" + BigCritical = 199, + PushSmoke = 212, + SmallWhiteLines = 203, // Hp/Atk/Def/Exp/Pvp etc... + BlueTimeSpace = 822, + BlueRemoveTimeSpace = 823, + TsTarget = 824, + TsMate = 825, + TsBonus = 826, + OtherRaidMember = 828, + OtherRaidLeader = 829, + OwnRaidMember = 830, + OwnRaidLeader = 831, + Heart = 854, + SmallHearth = 881, + MediumHearth = 882, + LargeHearth = 883, + SpeedBoost = 885, + UnfixedItem = 3003, + UpgradeFail = 3004, + UpgradeSuccess = 3005, + PetPickupEnabled = 3007, + YellowArrow = 3008, + NoneFlag = 3009, + RedFlag = 3010, + BlueFlag = 3011, + RedTeam = 3012, + BlueTeam = 3013, + MagicDefense = 3020, + MeleeDefense = 3021, + RangeDefense = 3022, + MateAttackDefenceUpgrade = 3035, + MateAttackUpgrade = 3038, + MateDefenceUpgrade = 3039, + IgnoreDefence = 3037, + MovingAura = 3415, + VehicleTeleportation = 3625, + FairyResetBuff = 4013, + Sp6ArcherTargetFalcon = 4269, + MateHealByMonster = 4328, + MeditationFirstStage = 4343, + MeditationFinalStage = 4344, + TalentArenaCall = 4432, + RedCircle = 4660, + AngelProtection = 4800, + DemonProtection = 4801, + Taunt = 4968, + ArchmageTeleport = 4482, + ArchmageTeleportSet = 4497, + ArchmageTeleportWhiteEffect = 4498, + ArchmageTeleportAfter = 4499, + Targeted = 5000, + TargetedByOthers = 5001, + PetLove = 5002, + PetLoveBroke = 5003, + PetPickUp = 5004, + PetAttack = 5005, + MinigameQuarry = 5102, + MinigameSawmill = 5103, + MinigameShooting = 5105, + MinigameFishing = 5104, + MinigameTypewritter = 5113, + MinigameMemory = 5112, + Eat = 6000, + DecreaseHp = 6004, + DoubleChanceDrop = 7552 +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/_enum/EtcItemType.cs b/srcs/WingsAPI.Game/_enum/EtcItemType.cs new file mode 100644 index 0000000..1c7b239 --- /dev/null +++ b/srcs/WingsAPI.Game/_enum/EtcItemType.cs @@ -0,0 +1,13 @@ +namespace WingsEmu.Game._enum; + +public enum EtcItemType +{ + Sellable = 0, + Food = 1, + Snacks = 2, + Magic = 4, + Materials = 5, + PetOrPartnerItem = 6, + Ammo = 7, + UsableQuestItem = 8 +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/_enum/GroupAttackType.cs b/srcs/WingsAPI.Game/_enum/GroupAttackType.cs new file mode 100644 index 0000000..159367b --- /dev/null +++ b/srcs/WingsAPI.Game/_enum/GroupAttackType.cs @@ -0,0 +1,8 @@ +namespace WingsEmu.Game._enum; + +public enum GroupAttackType +{ + None = 0, + ByRace = 1, + ByVnum = 5 +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/_enum/ItemClassType.cs b/srcs/WingsAPI.Game/_enum/ItemClassType.cs new file mode 100644 index 0000000..02bc6fb --- /dev/null +++ b/srcs/WingsAPI.Game/_enum/ItemClassType.cs @@ -0,0 +1,14 @@ +using System; + +namespace WingsEmu.Game._enum; + +[Flags] +public enum ItemClassType +{ + Neutral = 0, + Adventurer = 1 << 0, + Swordsman = 1 << 1, + Archer = 1 << 2, + Mage = 1 << 3, + MartialArtist = 1 << 4 +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/_enum/ItemEffectVnums.cs b/srcs/WingsAPI.Game/_enum/ItemEffectVnums.cs new file mode 100644 index 0000000..ae2b388 --- /dev/null +++ b/srcs/WingsAPI.Game/_enum/ItemEffectVnums.cs @@ -0,0 +1,12 @@ +namespace WingsEmu.Game._enum; + +public enum ItemEffectVnums : short +{ + BLESSING_AMULET = 282, + + SPAWN_NPC = 550, + + CHAMPION_AMULET_RANDOM = 4262, + CHAMPION_AMULET_INCREASE = 4263, + CHAMPION_AMULET = 4264 +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/_enum/ItemVnums.cs b/srcs/WingsAPI.Game/_enum/ItemVnums.cs new file mode 100644 index 0000000..2d5ecf5 --- /dev/null +++ b/srcs/WingsAPI.Game/_enum/ItemVnums.cs @@ -0,0 +1,339 @@ +namespace WingsEmu.Game._enum; + +public enum ItemVnums : short +{ + ADVENTURER_SHOES = 62, + + AMULET_INCREASE_NORMAL = 259, + + SOLARIS_FAIRY = 273, + + BLESSING_AMULET = 282, + PROTECTION_AMULET = 283, + AMULET_OF_REINFORCEMENT = 284, + + PET_BEAD = 285, + PARTNER_BEAD = 286, + + SPECIALIST_CARD_HOLDER = 287, + + HELLHOUND_NECKLACE = 331, + PHANTOM_AMULET = 332, + + MINIGAME_REWARD_CHEST_1 = 392, + MINIGAME_REWARD_CHEST_2 = 393, + MINIGAME_REWARD_CHEST_3 = 394, + MINIGAME_REWARD_CHEST_4 = 395, + MINIGAME_REWARD_CHEST_5 = 396, + + AZURIS_FAIRY = 425, + + WIG = 428, + SPIKY_HAIRSTYLE = 429, + COLORFUL_WIG = 441, + RARE_SPIKY_HAIRSTYLE = 442, + BROWN_WIG = 443, + + BLESSING_AMULET_DOUBLE = 498, + + FIRE_FAIRY = 800, + WATER_FAIRY = 801, + LIGHT_FAIRY = 802, + SHADOW_FAIRY = 803, + + PYJAMA_SP = 900, + CHICKEN_SP = 907, + JAJAMARU_SP = 908, + ICE_MAGE = 913, + DARK_GUNNER = 914, + + MAGMARIS_FAIRY = 920, + + PARTNER_WEAPON_MELEE = 990, + PARTNER_WEAPON_RANGED = 991, + PARTNER_WEAPON_MAGIC = 992, + PARTNER_ARMOR_MAGIC = 995, + PARTNER_ARMOR_RANGED = 996, + PARTNER_ARMOR_MELEE = 997, + + SEED_OF_POWER = 1012, + GILLION = 1013, + CELLA = 1014, + + SOUL_GEM = 1015, + COMPLETE_SOUL_GEM = 1016, + + CELLON_1 = 1017, + CELLON_2 = 1018, + CELLON_3 = 1019, + CELLON_4 = 1020, + CELLON_5 = 1021, + CELLON_6 = 1022, + CELLON_7 = 1023, + CELLON_8 = 1024, + CELLON_9 = 1025, + CELLON_10 = 1026, + DONA_RIVER_SAND = 1027, + + NEW_MOON_CRYSTAL = 1028, + HALF_MOON_CRYSTAL = 1029, + FULL_MOON_CRYSTAL = 1030, + + RED_CRYSTAL_PIECE = 1031, + BLUE_CRYSTAL_PIECE = 1032, + WHITE_CRYSTAL_PIECE = 1033, + BLACK_CRYSTAL_PIECE = 1034, + + GOLD = 1046, + + FAKE_MIMIC_POTION = 1076, + + WILD_SOUND_FLOWER = 1086, + SOUND_FLOWER = 1087, + + BONUS_POINTS = 1097, + + SKIN_FOR_BOB = 1103, + LILY_OF_PURITY = 1113, + SELF_INTRODUCTION = 1117, + SKIN_FOR_TOM = 1141, + SKIN_FOR_KLIFF = 1142, + + SMALL_BLOOD_RED_RUBY = 1161, + + KENKO_BEAD = 1174, + + NAMAJU_RAID = 1226, + + LOWER_SP_SCROLL = 1363, + HIGHER_SP_SCROLL = 1364, + RESET_SP_POINT = 1366, + + POTION_OF_SALVATION = 1211, + + FULL_HP_POTION = 1242, + FULL_MP_POTION = 1243, + FULL_HP_MP_POTION = 1244, + + ATTACK_POTION = 1246, + DEFENCE_POTION = 1247, + ENERGY_POTION = 1248, + EXPERIENCE_POTION = 1249, + + DURABILITY_COUPON = 1269, + REWARD_COUPON = 1270, + PRODUCTION_COUPON = 1271, + + EQ_NORMAL_SCROLL = 1218, + SOULSTONE_BLESSING = 1362, + + PERFUME = 1428, + RAINBOW_PEARL = 1429, + MAGIC_ERASER = 1430, + + ANGEL_BASE_FLAG = 1446, + DEMON_BASE_FLAG = 1447, + + FAMILY_SUMMONING_HORN = 1458, + + ANGEL_EGG_FAMILY = 1626, + DEMON_EGG_FAMILY = 1627, + + UNKNOWN_RELICT = 1878, + MYSTERIOUS_RELICT = 1879, + + SEALED_TAROT_FOOL = 1894, + SEALED_TAROT_MAGICIAN = 1895, + SEALED_TAROT_LOVERS = 1896, + SEALED_TAROT_HERMIT = 1897, + SEALED_TAROT_DEATH = 1898, + SEALED_TAROT_DEVIL = 1899, + SEALED_TAROT_TOWER = 1900, + SEALED_TAROT_STAR = 1901, + SEALED_TAROT_MOON = 1902, + SEALED_TAROT_SUN = 1903, + TAROT_CARD_GAME = 1904, + + DELICIOUS_LUNCH = 2018, + + BATTLE_POTION = 2028, + YELLOW_DYE = 2050, + BLUE_DYE = 2051, + PURPLE_DYE = 2052, + ORANGE_DYE = 2053, + BROWN_DYE = 2054, + GREEN_DYE = 2055, + DARK_DYE = 2056, + GREY_DYE = 2057, + RED_DYE = 2058, + HAIR_DYE_11 = 2060, + HAIR_DYE_12 = 2061, + HAIR_DYE_13 = 2062, + HAIR_DYE_14 = 2063, + HAIR_DYE_15 = 2064, + HAIR_DYE_16 = 2065, + HAIR_DYE_17 = 2066, + HAIR_DYE_18 = 2067, + HAIR_DYE_19 = 2068, + HAIR_DYE_20 = 2069, + + PET_FOOD = 2077, + DELICIOUS_PET_FOOD = 2078, + + AMMO_ADVENTURER = 2081, + AMMO_SWORDSMAN = 2082, + AMMO_ARCHER = 2083, + BANDAGE = 2087, + + NOSMATE_GUARDIAN_ANGEL = 2089, + + LUINIA_OF_RESTORATION = 2127, + NAME_TAG = 2157, + WING_OF_FRIENDSHIP = 2160, + + SUPER_HAIR_WAX = 2161, + SUPER_HAIR_GEL = 2162, + PINK_DYE = 2163, + LIGHT_GREEN_DYE = 2164, + WHITE_DYE = 2165, + LIGHT_BLUE_DYE = 2166, + BLACK_DYE = 2167, + + SPEAKER = 2173, + BUBBLE = 2174, + + ANGEL_FEATHER = 2282, + + ICE_CUBE = 2307, + PARTNER_GUARDIAN_ANGEL = 2329, + + RAINBOW_COIN = 2361, + + MYSTERIOUS_HAIR_DYE = 2418, + + SMALL_RUBY_COMP = 2514, + SMALL_SAPPHIRE_COMP = 2515, + SMALL_OBSIDIAN_COMP = 2516, + SMALL_TOPAZ_COMP = 2517, + RUBY_COMP = 2518, + SAPPHIRE_COMP = 2519, + OBSIDIAN_COMP = 2520, + TOPAZ_COMP = 2521, + + DRACO_CLAW = 2522, + GLACERUS_MANE = 2523, + SAVAGE_DYES = 2533, + BANK_CARD = 2539, + + MINILAND_THEME_SPRING = 3800, + + TAROT_FOOL = 4046, + TAROT_MAGICIAN = 4047, + TAROT_LOVERS = 4048, + TAROT_HERMIT = 4049, + TAROT_DEATH = 4050, + TAROT_DEVIL = 4051, + TAROT_TOWER = 4052, + TAROT_STAR = 4053, + TAROT_MOON = 4054, + TAROT_SUN = 4055, + + PIRATE_SP = 4099, + + MOUNT_BEAD = 4106, + FAIRY_BEAD = 4194, + GOLDEN_SP_HOLDER = 4240, + + CHAMPION_AMULET_INCREASE_1 = 4261, + CHAMPION_AMULET_RANDOM = 4262, + CHAMPION_AMULET_INCREASE_2 = 4263, + CHAMPION_AMULET = 4264, + + GLADIATOR = 4500, + + DRACO_AMULET = 4503, + GLACERUS_AMULET = 4504, + + PSP_HOLDER = 4801, + + MAGIC_LAMP = 5105, + SCROLL_CHICKEN = 5107, + SPEED_BOOSTER = 5119, + PET_SLOT_EXPANSION = 5130, + SCROLL_PYJAMA = 5207, + SCROLL_PIRATE = 5519, + + EQ_GOLD_SCROLL = 5369, + FAIRY_EXPERIENCE_POTION = 5370, + + LORD_DRACO_SEAL = 5500, + GLACERUS_SEAL = 5512, + + INVENTORY_EXPANSION_TICKET_30_DAYS = 5795, + INVENTORY_EXPANSION_TICKET_60_DAYS = 5796, + INVENTORY_EXPANSION_TICKET_PERMANENT = 5797, + + SKIN_FOR_FRIGG = 5827, + SKIN_FOR_RAGNAR = 5828, + SKIN_FOR_ERDIMIEN = 5829, + SKIN_FOR_YERTIRAND = 5830, + SKIN_FOR_JENNIFER = 5831, + + CUARRY_BANK_SAVINGS_BOOK = 5836, + CUARRY_BANK_VIP_10 = 5838, + CUARRY_BANK_VIP_30 = 5839, + + PARTNER_SLOT_EXPANSION = 5856, + + AKAMUR_COUPON = 5910, + ICE_FLOWER = 5911, + MAGIC_CAMEL = 5914, + MAGIC_CAMEL_BOX = 5915, + STRONG_ICE_FLOWER_OIL = 5916, + + LEFT_SIDE_GRENIGAS_SEAL = 5917, + RIGHT_SIDE_GRENIGAS_SEAL = 5918, + RUNE_PIECE = 5919, + GRENIGAS_SEAL = 5922, + + LARGE_HEAT_RESISTANCE_POTION = 5927, + ICE_FLOWER_OIL = 5929, + HEAT_RESISTANCE_POTION = 5030, + + PARTNER_SKILL_TICKET_SINGLE = 5931, + PARTNER_SKILL_TICKET_ALL = 5932, + + LAURENA_SEAL = 5977, + CLEANSING_POWDER = 5984, + SOUL_SLIVER = 5986, + SEED_OF_DAMNATION = 5987, + + /* LIMITED ITEMS */ + SELF_INTRODUCTION_LIMITED = 9013, + + ATTACK_POTION_LIMITED = 9020, + DEFENCE_POTION_LIMITED = 9021, + ENERGY_POTION_LIMITED = 9022, + EXPERIENCE_POTION_LIMITED = 9023, + + PET_SLOT_EXPANSION_LIMITED = 9072, + + PARTNER_SKILL_TICKET_SINGLE_LIMITED = 9109, + PARTNER_SKILL_TICKET_ALL_LIMITED = 9110, + + PARTNER_SLOT_EXPANSION_LIMITED = 9113, + + FAIRY_EXPERIENCE_POTION_LIMITED = 9116, + SPEED_BOOSTER_LIMITED = 9071, + SOULSTONE_BLESSING_LIMITED = 9075, + + NOSMATE_GUARDIAN_ANGEL_LIMITED = 10016, + SPEAKER_LIMITED = 10028, + BUBBLE_LIMITED = 10029, + WING_OF_FRIENDSHIP_LIMITED = 10048, + PARTNER_GUARDIAN_ANGEL_LIMITED = 10050, + MYSTERIOUS_HAIR_DYE_LIMITED = 10059, + LIMITED_BANK_CARD = 10066, + + FULL_HP_MP_POTION_LIMIT = 9129 +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/_enum/ManagerResponseType.cs b/srcs/WingsAPI.Game/_enum/ManagerResponseType.cs new file mode 100644 index 0000000..941bbe4 --- /dev/null +++ b/srcs/WingsAPI.Game/_enum/ManagerResponseType.cs @@ -0,0 +1,8 @@ +namespace WingsEmu.Game._enum; + +public enum ManagerResponseType +{ + Success, + Error, + Maintenance +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/_enum/MapIds.cs b/srcs/WingsAPI.Game/_enum/MapIds.cs new file mode 100644 index 0000000..e2c690c --- /dev/null +++ b/srcs/WingsAPI.Game/_enum/MapIds.cs @@ -0,0 +1,31 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Game._enum; + +public enum MapIds +{ + NOSVILLE = 1, + KREM = 20, + ACT4_ANGEL_CITADEL = 130, + ACT4_DEMON_CITADEL = 131, + ACT4_TUNDRA = 134, + HATUS_BOSS_MAP = 138, + ALVEUS = 145, + ACT4_SHIP = 149, + LAND_OF_DEATH = 150, + UNKNOWN_LAND = 153, + ACT4_CALIGOR = 154, + MORTAZ_DESERT_PORT = 170, + AKAMUR_CAMP = 177, + DESERT_EAGLY_CITY = 189, + ARENA_INDIVIDUAL = 2006, + ARENA_FAMILY = 2106, + SP_STONE_PAJAMA = 2107, + SP_STONE_1 = 2108, + SP_STONE_2 = 2109, + SP_STONE_3 = 2111, + SP_STONE_4 = 2112, + MINILAND = 20001 +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/_enum/MateNrunType.cs b/srcs/WingsAPI.Game/_enum/MateNrunType.cs new file mode 100644 index 0000000..fa6a38c --- /dev/null +++ b/srcs/WingsAPI.Game/_enum/MateNrunType.cs @@ -0,0 +1,12 @@ +namespace WingsEmu.Game._enum; + +public enum MateNrunType +{ + CompanyOrSendBack = 2, // Only in his Mini-Land + Stay = 3, // Only in his Mini-Land + KickPetFromAnywhere = 4, + TriggerPetKick = 5, + KickPet = 6, + TriggerSummon = 7, + Summon = 9 +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/_enum/MinilandItemSubType.cs b/srcs/WingsAPI.Game/_enum/MinilandItemSubType.cs new file mode 100644 index 0000000..27faefa --- /dev/null +++ b/srcs/WingsAPI.Game/_enum/MinilandItemSubType.cs @@ -0,0 +1,8 @@ +namespace WingsEmu.Game._enum; + +public enum MinilandItemSubType +{ + HOUSE = 0, + SMALL_HOUSE = 1, + WAREHOUSE = 2 +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/_enum/MonsterRaceType.cs b/srcs/WingsAPI.Game/_enum/MonsterRaceType.cs new file mode 100644 index 0000000..585b978 --- /dev/null +++ b/srcs/WingsAPI.Game/_enum/MonsterRaceType.cs @@ -0,0 +1,14 @@ +namespace WingsEmu.Game._enum; + +public enum MonsterRaceType : byte +{ + LowLevel = 0, + HighLevel = 1, + Furry = 2, + People = 3, + Angel = 4, + Undead = 5, + Spirit = 6, + Other = 7, + Fixed = 8 +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/_enum/MonsterSubRace.cs b/srcs/WingsAPI.Game/_enum/MonsterSubRace.cs new file mode 100644 index 0000000..e0e7dd0 --- /dev/null +++ b/srcs/WingsAPI.Game/_enum/MonsterSubRace.cs @@ -0,0 +1,75 @@ +namespace WingsEmu.Game.Monster; + +public class MonsterSubRace +{ + public enum Angels : byte + { + Angel = 0, + Demon = 1, + HalfAngel = 2 + } + + public enum Fixed : byte + { + FixedTrap = 0, + EnergyBall = 1, + CannonBall = 2, + MiniLandStructure = 3, + Unknown0 = 4, + Unknown1 = 5, + Unknown2 = 6, + Unknown3 = 7, + Unknown4 = 8 + } + + public enum Furry : byte + { + Kovolt = 0, + Bushtail = 1, + Catsy = 2 + } + + public enum HighLevel : byte + { + Plant = 0, + Animal = 1, + Monster = 2 + } + + public enum LowLevel : byte + { + Plant = 0, + Animal = 1, + Monster = 2 + } + + public enum Other : byte + { + Machine = 0, + Doll = 1 + } + + public enum People : byte + { + Humanlike = 0, + Elf = 1, + Half = 2, + Demon = 3, + Orc = 4 + } + + public enum Spirits : byte + { + LowLevelSpirit = 0, + HighLevelSpirit = 1, + LowLevelGhost = 2, + HighLevelGhost = 3 + } + + public enum Undead : byte + { + LowLevelUndead = 0, + HighLevelUndead = 1, + Vampire = 2 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/_enum/MonsterVnum.cs b/srcs/WingsAPI.Game/_enum/MonsterVnum.cs new file mode 100644 index 0000000..d4b4cd7 --- /dev/null +++ b/srcs/WingsAPI.Game/_enum/MonsterVnum.cs @@ -0,0 +1,78 @@ +namespace WingsEmu.Game._enum; + +/// +/// Monster vnums +/// +public enum MonsterVnum : short +{ + HAPPY_MANDRA = 18, + STRONG_GRASSMANDRA = 23, + + BOMB = 945, + ROOKIE_KENKO_SWORDSMAN = 145, + KENKO_SWORDSMAN = 146, + ELITE_KENKO_SWORDSMAN = 147, + ROOKIE_KENKO_SPEARMAN = 148, + KENKO_SPEARMAN = 149, + ELITE_KENKO_SPEARMAN = 150, + ROOKIE_KENKO_RAIDER = 151, + KENKO_RAIDER = 152, + ELITE_KENKO_RAIDER = 153, + GIANT_MANDRA = 161, + DASHING_MANDRA = 162, + TRAINING_STAKE = 195, + MINI_JAJAMARU = 416, + SAKURA = 417, + LOD_FIRE_HORN = 443, + + LORD_MUKRAJU = 556, + LORD_HATUS = 577, + + EMERALD_PHANTOM = 621, + SAPPHIRE_PHANTOM = 622, + RUBY_PHANTOM = 623, + + SMALL_MINILAND_SIGN = 920, + BIG_MINILAND_SIGN = 921, + + BIG_FLAG = 922, + MEDIUM_FLAG = 923, + SMALL_FLAG = 924, + + TIME_SPACE_NPC = 930, + + SMALL_CAMPFIRE = 956, + GIANT_CAMPFIRE = 957, + ICE_MACHINE = 959, + + ANGEL_CAMP = 965, + DEMON_CAMP = 966, + + ROBBER_GANG_CHEST = 1346, + + EMERALD_SHADOW_PHANTOM = 1408, + SAPPHIRE_SHADOW_PHANTOM = 1409, + RUBY_SHADOW_PHANTOM = 1410, + + HALLOWEEN_MINILAND_SIGN = 1385, + + EASTER_MINILAND_SIGN = 1428, + PIRATE_MINILAND_SIGN = 1499, + CHRISTMAS_MINILAND_SIGN = 1519, + ICE_FLOWER = 2004, + POISON_PLANT_OF_DAMNATION = 2350, + FIRST_METEORITE = 2352, + SECOND_METEORITE = 2353, + ONYX_MONSTER = 2371, + + DOLL_TRAIN_BEG = 20004, + DOLL_TRAIN_INTER = 20005, + DOLL_TRAIN_ADV = 20006, + DOLL_XP_FIREBALL = 20007, + DOLL_XP_KOVOLT = 20008, + DOLL_XP_KENKO = 20009, + DOLL_XP_WAREPARD = 20010, + DOLL_XP_CATSY = 20011, + DOLL_XP_SKULL_WARRIOR = 20012, + DOLL_XP_BUSHTAIL = 20013 +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/_enum/MorphIdType.cs b/srcs/WingsAPI.Game/_enum/MorphIdType.cs new file mode 100644 index 0000000..bcf60a0 --- /dev/null +++ b/srcs/WingsAPI.Game/_enum/MorphIdType.cs @@ -0,0 +1,38 @@ +namespace WingsEmu.Game._enum; + +public enum MorphIdType +{ + Pyjama = 1, + Warrior = 2, + Ninja = 3, + Ranger = 4, + Assassin = 5, + RedMagician = 6, + HolyMage = 7, + Chicken = 8, + Jajamaru = 9, + Crusader = 10, + Berserker = 11, + Destroyer = 12, + WildKeeper = 13, + BlueMagician = 14, + DarkGunner = 15, + Pirate = 16, + Gladiator = 17, + FireCannoneer = 18, + Volcano = 19, + BattleMonk = 20, + Scout = 21, + TideLord = 22, + DeathReaper = 23, + DemonHunter = 24, + Seer = 25, + Renegade = 26, + AvengingAngel = 27, + Archmage = 28, + DraconicFist = 29, + MysticArts = 31, + WeddingCostume = 32, + MasterWolf = 33, + DemonWarrior = 34 +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/_enum/NpcMonsterRaceType.cs b/srcs/WingsAPI.Game/_enum/NpcMonsterRaceType.cs new file mode 100644 index 0000000..5840dbd --- /dev/null +++ b/srcs/WingsAPI.Game/_enum/NpcMonsterRaceType.cs @@ -0,0 +1,12 @@ +namespace WingsEmu.Game._enum; + +public enum NpcMonsterRaceType +{ + Race0UnknownYet, + Race1, + Race2, + Race3, + Race4, + Race5, + Race6 +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/_enum/NpcShopType.cs b/srcs/WingsAPI.Game/_enum/NpcShopType.cs new file mode 100644 index 0000000..376e14d --- /dev/null +++ b/srcs/WingsAPI.Game/_enum/NpcShopType.cs @@ -0,0 +1,7 @@ +namespace WingsEmu.Game._enum; + +public enum NpcShopType +{ + NOS_BAZAAR = 45, + BANK = 81 +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/_enum/QuestsVnums.cs b/srcs/WingsAPI.Game/_enum/QuestsVnums.cs new file mode 100644 index 0000000..61f912d --- /dev/null +++ b/srcs/WingsAPI.Game/_enum/QuestsVnums.cs @@ -0,0 +1,16 @@ +namespace WingsEmu.Game._enum; + +public enum QuestsVnums : short +{ + CALVIN_ADVENTURER_TRAINING_SKILLS = 1503, + TALK_WEARING_KOVOLT_MASK_1 = 1541, + GIVE_MALCOM_ADVENTURER_SHOES = 1544, + TALK_WEARING_KOVOLT_MASK_2 = 1546, + SORAYA_LUNCH_TO_CALVIN = 1517, + LILIES_SP2 = 2014, + FORTUNE_TELLER_1 = 2251, + FORTUNE_TELLER_2 = 2252, + FORTUNE_TELLER_3 = 2253, + FORTUNE_TELLER_4 = 2254, + FORTUNE_TELLER_5 = 2255 +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/_enum/ReputationType.cs b/srcs/WingsAPI.Game/_enum/ReputationType.cs new file mode 100644 index 0000000..dc517d0 --- /dev/null +++ b/srcs/WingsAPI.Game/_enum/ReputationType.cs @@ -0,0 +1,46 @@ +namespace WingsEmu.Game._enum; + +public enum ReputationType : byte +{ + BEGINNER_1 = 1, + BEGINNER_2 = 2, + BEGINNER_3 = 3, + GREEN_TRAINEE = 4, + BLUE_TRAINEE = 5, + RED_TRAINEE = 6, + + GREEN_EXPERIENCED = 7, + BLUE_EXPERIENCED = 8, + RED_EXPERIENCED = 9, + + GREEN_BATTLE_SOLDIER = 10, + BLUE_BATTLE_SOLDIER = 11, + RED_BATTLE_SOLDIER = 12, + + GREEN_EXPERT = 13, + BLUE_EXPERT = 14, + RED_EXPERT = 15, + + GREEN_LEADER = 16, + BLUE_LEADER = 17, + RED_LEADER = 18, + + GREEN_MASTER = 19, + BLUE_MASTER = 20, + RED_MASTER = 21, + + GREEN_NOS = 22, + BLUE_NOS = 23, + RED_NOS = 24, + + GREEN_ELITE = 25, + BLUE_ELITE = 26, + RED_ELITE = 27, + + SECOND_LEGEND = 28, + FIRST_LEGEND = 29, + + ANCIENT_HERO = 30, + MYSTERIOUS_HERO = 31, + LEGENDARY_HERO = 32 +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/_enum/ShellItemSubType.cs b/srcs/WingsAPI.Game/_enum/ShellItemSubType.cs new file mode 100644 index 0000000..4c6c2c8 --- /dev/null +++ b/srcs/WingsAPI.Game/_enum/ShellItemSubType.cs @@ -0,0 +1,7 @@ +namespace WingsEmu.Game._enum; + +public enum ShellItemSubType +{ + Weapon = 0, + Armor = 1 +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/_enum/SkillCastType.cs b/srcs/WingsAPI.Game/_enum/SkillCastType.cs new file mode 100644 index 0000000..e0ed4a1 --- /dev/null +++ b/srcs/WingsAPI.Game/_enum/SkillCastType.cs @@ -0,0 +1,10 @@ +namespace WingsEmu.Game._enum; + +public enum SkillCastType : byte +{ + BEFORE_ATTACK_SELF = 0, + BEFORE_ATTACK_ON_MAIN_TARGET = 1, + AFTER_ATTACK_ALL_ALLIES = 2, + AFTER_ATTACK_ALL_TARGETS = 3, + BEFORE_ATTACK_ALL_TARGETS = 4 +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/_enum/SkillsVnums.cs b/srcs/WingsAPI.Game/_enum/SkillsVnums.cs new file mode 100644 index 0000000..c35d1f3 --- /dev/null +++ b/srcs/WingsAPI.Game/_enum/SkillsVnums.cs @@ -0,0 +1,39 @@ +namespace WingsEmu.Game._enum; + +public enum SkillsVnums +{ + // Passive skills + BEGINNER_PHYSICAL_STRENGTH = 20, + BEGINNER_SPEED = 24, + BEGINNER_INTELLIGENCE = 28, + BEGINNER_HP_RECOVERY = 52, + + FLAME = 1191, + ICE = 1192, + HALO = 1193, + DARKNESS = 1194, + NO_ELEMENT = 1195, + + EXECUTION = 1066, + SPIRIT_SPLITTER = 1178, + ADV_CAPTURE = 209, + SWOR_CAPTURE = 235, + ARC_CAPUTRE = 236, + MAG_CAPTURE = 237, + JAJAMARU_LAST_SKILL = 888, + BOMB = 915, + FIRE_MINE = 920, + TAUNT = 1061, + SACRIFICE = 1095, + GIANT_SWIRL = 1108, + SPY_OUT = 1117, + FOG_ARROW = 1120, + SNIPER = 1124, + SPY_OUT_SKILL = 1249, + HOLY_EXPLOSION = 1326, + ARCH_TELEPORT = 1330, + DOUBLE_RIPPER = 1342, + CONVERT = 1348, + MA_BASIC_ATTACK = 1525, + MA_SECOND_ATTACK = 1529 +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/_enum/SoundFlowerType.cs b/srcs/WingsAPI.Game/_enum/SoundFlowerType.cs new file mode 100644 index 0000000..9b7557b --- /dev/null +++ b/srcs/WingsAPI.Game/_enum/SoundFlowerType.cs @@ -0,0 +1,7 @@ +namespace WingsEmu.Game._enum; + +public enum SoundFlowerType +{ + SOUND_FLOWER, + WILD_SOUND_FLOWER +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/_enum/SoundType.cs b/srcs/WingsAPI.Game/_enum/SoundType.cs new file mode 100644 index 0000000..d14effe --- /dev/null +++ b/srcs/WingsAPI.Game/_enum/SoundType.cs @@ -0,0 +1,29 @@ +namespace WingsEmu.Game._enum; + +public enum SoundType : short +{ + CLICK_BUTTON = 1, + CAMERA_SHOT = 2, + ADVENTURER_DEATH = 3, + WALKING = 5, + WALKING_2 = 6, + TS_DANGER = 7, + WALKING_3 = 8, + LEVEL_UP = 9, + PICK_UP_ITEM = 10, + TS_ENDING_TIME = 12, + ATTACK = 13, + ITEM_MOVING = 16, + JOB_LEVEL_UP = 23, + TARGET = 26, + SECOND_CHANCE = 334, + WARNING = 928, + NO_MANA = 1323, + CRAFTING_SUCCESS = 1324, + BUY_SKILL = 1326, + CRAFTING_FAILED = 1332, + TAUNT_SKILL = 1691, + MEDITATION_FIRST = 1824, + MEDITATION_SECOND = 1825, + MEDITATION_THIRD = 1826 +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/_enum/SpecialistItemSubType.cs b/srcs/WingsAPI.Game/_enum/SpecialistItemSubType.cs new file mode 100644 index 0000000..fc25765 --- /dev/null +++ b/srcs/WingsAPI.Game/_enum/SpecialistItemSubType.cs @@ -0,0 +1,8 @@ +namespace WingsEmu.Game._enum; + +public enum SpecialistItemSubType +{ + EquippabbleByAdventurer = 0, + SpClassSpecific = 1, + PartnerSp = 4 +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/_enum/SpecialistPointsType.cs b/srcs/WingsAPI.Game/_enum/SpecialistPointsType.cs new file mode 100644 index 0000000..27f1325 --- /dev/null +++ b/srcs/WingsAPI.Game/_enum/SpecialistPointsType.cs @@ -0,0 +1,9 @@ +namespace WingsEmu.Game._enum; + +public enum SpecialistPointsType +{ + ATTACK, + DEFENCE, + ELEMENT, + HPMP +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/_enum/StatisticType.cs b/srcs/WingsAPI.Game/_enum/StatisticType.cs new file mode 100644 index 0000000..867baa9 --- /dev/null +++ b/srcs/WingsAPI.Game/_enum/StatisticType.cs @@ -0,0 +1,29 @@ +namespace WingsEmu.Game._enum; + +public enum StatisticType +{ + /* Attack */ + ATTACK_MELEE, + ATTACK_RANGED, + ATTACK_MAGIC, + + /* HitRate */ + HITRATE_MELEE, + HITRATE_RANGED, + HITRATE_MAGIC, + + /* Defense */ + DEFENSE_MELEE, + DEFENSE_RANGED, + DEFENSE_MAGIC, + + /* Dodge */ + DODGE_MELEE, + DODGE_RANGED, + + /* Resistance */ + FIRE, + WATER, + LIGHT, + DARK +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/_enum/TimeType.cs b/srcs/WingsAPI.Game/_enum/TimeType.cs new file mode 100644 index 0000000..626e9ff --- /dev/null +++ b/srcs/WingsAPI.Game/_enum/TimeType.cs @@ -0,0 +1,8 @@ +namespace WingsEmu.Game._enum; + +public enum TimeType +{ + SECONDS, + MINUTES, + HOURS +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/_enum/UsableItemSubType.cs b/srcs/WingsAPI.Game/_enum/UsableItemSubType.cs new file mode 100644 index 0000000..b432ac9 --- /dev/null +++ b/srcs/WingsAPI.Game/_enum/UsableItemSubType.cs @@ -0,0 +1,12 @@ +namespace WingsEmu.Game._enum; + +public enum UsableItemSubType +{ + PetBead = 0, + PartnerBead = 1, + SpecialistHolder = 2, + RaidBoxOrSealedJajamaruSpOrSealedSakuraBead = 3, + VehicleBead = 4, + FairyBead = 5, + PartnerSpHolder = 6 +} \ No newline at end of file diff --git a/srcs/WingsAPI.Game/_enum/WeaponItemSubType.cs b/srcs/WingsAPI.Game/_enum/WeaponItemSubType.cs new file mode 100644 index 0000000..749a8bb --- /dev/null +++ b/srcs/WingsAPI.Game/_enum/WeaponItemSubType.cs @@ -0,0 +1,19 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Game._enum; + +public enum WeaponItemSubType +{ + AdventurerSword = 0, + SwordsmanSword = 1, + Daggers = 3, + Fists = 4, + Singleshot = 5, + Bow = 6, + MagicSpellGun = 8, + Wands = 9, + Tokens = 11, + PartnerWeapons = 12 +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets.Handling/GenericGamePacketHandlerBase.cs b/srcs/WingsAPI.Packets.Handling/GenericGamePacketHandlerBase.cs new file mode 100644 index 0000000..b4fdd69 --- /dev/null +++ b/srcs/WingsAPI.Packets.Handling/GenericGamePacketHandlerBase.cs @@ -0,0 +1,25 @@ +using System.Threading.Tasks; +using WingsEmu.Game._packetHandling; +using WingsEmu.Game.Networking; +using WingsEmu.Packets; + +namespace WingsAPI.Packets.Handling +{ + public abstract class GenericGamePacketHandlerBase : IGamePacketHandler where T : IPacket + { + public async Task HandleAsync(IClientSession session, IPacket packet) + { + if (packet is T typedPacket && session.IsAuthenticated) + { + await HandlePacketAsync(session, typedPacket); + } + } + + public void Handle(IClientSession session, IPacket packet) + { + HandleAsync(session, packet).ConfigureAwait(false).GetAwaiter().GetResult(); + } + + protected abstract Task HandlePacketAsync(IClientSession session, T packet); + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets.Handling/PacketHandlingExtensions.cs b/srcs/WingsAPI.Packets.Handling/PacketHandlingExtensions.cs new file mode 100644 index 0000000..413971a --- /dev/null +++ b/srcs/WingsAPI.Packets.Handling/PacketHandlingExtensions.cs @@ -0,0 +1,35 @@ +using System; +using System.Reflection; +using Microsoft.Extensions.DependencyInjection; +using PhoenixLib.Extensions; + +namespace WingsAPI.Packets.Handling +{ + public static class PacketHandlingExtensions + { + public static void AddGamePacketHandlersInAssembly(this IServiceCollection services) + { + services.AddGamePacketHandlersInAssembly(typeof(T).Assembly); + } + + public static void AddGamePacketHandlersInAssembly(this IServiceCollection services, Assembly assembly) + { + Type[] types = assembly.GetTypesImplementingGenericClass(typeof(GenericGamePacketHandlerBase<>)); + services.AddGamePacketHandlersInAssembly(types); + } + + public static void AddGamePacketHandlersInAssembly(this IServiceCollection services, Type[] types) + { + foreach (Type type in types) + { + if (type.IsAbstract) + { + continue; + } + + services.AddSingleton(new RegisteredPacketHandler { HandlerType = type }); + services.AddTransient(type); + } + } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets.Handling/RegisteredPacketHandler.cs b/srcs/WingsAPI.Packets.Handling/RegisteredPacketHandler.cs new file mode 100644 index 0000000..f8221db --- /dev/null +++ b/srcs/WingsAPI.Packets.Handling/RegisteredPacketHandler.cs @@ -0,0 +1,9 @@ +using System; + +namespace WingsAPI.Packets.Handling +{ + public class RegisteredPacketHandler + { + public Type HandlerType { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets.Handling/WingsAPI.Packets.Handling.csproj b/srcs/WingsAPI.Packets.Handling/WingsAPI.Packets.Handling.csproj new file mode 100644 index 0000000..514a06b --- /dev/null +++ b/srcs/WingsAPI.Packets.Handling/WingsAPI.Packets.Handling.csproj @@ -0,0 +1,14 @@ + + + + net5.0 + + + + + + + + + + diff --git a/srcs/WingsAPI.Packets/ClientPacket.cs b/srcs/WingsAPI.Packets/ClientPacket.cs new file mode 100644 index 0000000..c77b6e1 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPacket.cs @@ -0,0 +1,14 @@ +namespace WingsEmu.Packets +{ + public abstract class ClientPacket : IClientPacket + { + /// + /// tells + /// + public bool IsReturnPacket { get; set; } + + public string OriginalContent { get; set; } + + public string OriginalHeader { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPacketRegistered.cs b/srcs/WingsAPI.Packets/ClientPacketRegistered.cs new file mode 100644 index 0000000..243d174 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPacketRegistered.cs @@ -0,0 +1,9 @@ +using System; + +namespace WingsEmu.Packets +{ + public class ClientPacketRegistered + { + public Type PacketType { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/AddobjPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/AddobjPacket.cs new file mode 100644 index 0000000..3ef798b --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/AddobjPacket.cs @@ -0,0 +1,23 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("addobj")] + public class AddobjPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public short Slot { get; set; } + + [PacketIndex(1)] + public short PositionX { get; set; } + + [PacketIndex(2)] + public short PositionY { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/ArenaPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/ArenaPacket.cs new file mode 100644 index 0000000..fdb3218 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/ArenaPacket.cs @@ -0,0 +1,9 @@ +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("arena")] + public class ArenaPacket : ClientPacket + { + [PacketIndex(0)] + public byte ArenaType { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/BIPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/BIPacket.cs new file mode 100644 index 0000000..899b021 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/BIPacket.cs @@ -0,0 +1,25 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("b_i")] + public class BiPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public InventoryType InventoryType { get; set; } + + [PacketIndex(1)] + public byte Slot { get; set; } + + [PacketIndex(2)] + public byte? Option { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/BrawlerCreatePacket.cs b/srcs/WingsAPI.Packets/ClientPackets/BrawlerCreatePacket.cs new file mode 100644 index 0000000..4134cfe --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/BrawlerCreatePacket.cs @@ -0,0 +1,30 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Character; + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("Char_NEW_JOB")] + public class BrawlerCreatePacket : ClientPacket + { + [PacketIndex(0)] + public string Name { get; set; } + + [PacketIndex(1)] + public byte Slot { get; set; } + + [PacketIndex(2)] + public GenderType Gender { get; set; } + + [PacketIndex(3)] + public HairStyleType HairStyle { get; set; } + + [PacketIndex(4)] + public HairColorType HairColor { get; set; } + + public override string ToString() => $"Create Character Name: {Name} Slot: {Slot}"; + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/BscPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/BscPacket.cs new file mode 100644 index 0000000..b692fdc --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/BscPacket.cs @@ -0,0 +1,20 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("bsc")] + public class BscPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public byte Type { get; set; } + + [PacketIndex(1)] + public byte? Option { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/BtkPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/BtkPacket.cs new file mode 100644 index 0000000..3d4ad56 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/BtkPacket.cs @@ -0,0 +1,20 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("btk")] + public class BtkPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public long CharacterId { get; set; } + + [PacketIndex(1, serializeToEnd: true)] + public string Message { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/BuyPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/BuyPacket.cs new file mode 100644 index 0000000..7ad100f --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/BuyPacket.cs @@ -0,0 +1,24 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("buy")] + public class BuyPacket : ClientPacket + { + [PacketIndex(0)] + public BuyShopType Type { get; set; } + + [PacketIndex(1)] + public long OwnerId { get; set; } + + [PacketIndex(2)] + public short Slot { get; set; } + + [PacketIndex(3)] + public short Amount { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/CBlistPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/CBlistPacket.cs new file mode 100644 index 0000000..5a9773c --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/CBlistPacket.cs @@ -0,0 +1,40 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsAPI.Packets.Enums.Bazaar.Filter; + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("c_blist")] + public class CbListPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public int Index { get; set; } + + [PacketIndex(1)] + public BazaarCategoryFilterType CategoryFilterType { get; set; } + + [PacketIndex(2)] + public byte SubTypeFilter { get; set; } + + [PacketIndex(3)] + public BazaarLevelFilterType LevelFilter { get; set; } + + [PacketIndex(4)] + public BazaarRarityFilterType RareFilter { get; set; } + + [PacketIndex(5)] + public BazaarUpgradeFilterType UpgradeFilter { get; set; } + + [PacketIndex(6)] + public BazaarSortFilterType OrderFilter { get; set; } + + [PacketIndex(7, serializeToEnd: true)] + public string ItemVNumFilter { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/CBuyPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/CBuyPacket.cs new file mode 100644 index 0000000..87891c9 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/CBuyPacket.cs @@ -0,0 +1,26 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("c_buy")] + public class CBuyPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public long BazaarItemId { get; set; } + + [PacketIndex(1)] + public short VNum { get; set; } + + [PacketIndex(2)] + public short Amount { get; set; } + + [PacketIndex(3)] + public long Price { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/CClosePacket.cs b/srcs/WingsAPI.Packets/ClientPackets/CClosePacket.cs new file mode 100644 index 0000000..8ebfa96 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/CClosePacket.cs @@ -0,0 +1,14 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("c_close")] + public class CClosePacket : ClientPacket + { + #region Properties + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/CModPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/CModPacket.cs new file mode 100644 index 0000000..87c833f --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/CModPacket.cs @@ -0,0 +1,29 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("c_mod")] + public class CmodPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public long BazaarId { get; set; } + + [PacketIndex(1)] + public short VNum { get; set; } + + [PacketIndex(2)] + public int Amount { get; set; } + + [PacketIndex(3)] + public long NewPricePerItem { get; set; } + + [PacketIndex(4)] + public byte Confirmed { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/CRegPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/CRegPacket.cs new file mode 100644 index 0000000..06d63e9 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/CRegPacket.cs @@ -0,0 +1,49 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("c_reg")] + public class CRegPacket : ClientPacket + { + //c_reg 0 1 0 9 1 4 1 99 63 90 2 + + #region Properties + + [PacketIndex(0)] + public int Type { get; set; } + + [PacketIndex(1)] + public byte Inventory { get; set; } + + [PacketIndex(2)] + public byte Slot { get; set; } + + [PacketIndex(3)] + public int Unknown1 { get; set; } + + [PacketIndex(4)] + public int Unknown2 { get; set; } + + [PacketIndex(5)] + public byte Durability { get; set; } + + [PacketIndex(6)] + public int IsPackage { get; set; } + + [PacketIndex(7)] + public short Amount { get; set; } + + [PacketIndex(8)] + public long PricePerItem { get; set; } + + [PacketIndex(9)] + public int Taxe { get; set; } + + [PacketIndex(10)] + public byte MedalUsed { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/CScalcPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/CScalcPacket.cs new file mode 100644 index 0000000..b03f666 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/CScalcPacket.cs @@ -0,0 +1,29 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("c_scalc")] + public class CScalcPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public long BazaarId { get; set; } + + [PacketIndex(1)] + public short VNum { get; set; } + + [PacketIndex(2)] + public short Amount { get; set; } + + [PacketIndex(3)] + public short MaxAmount { get; set; } + + [PacketIndex(4)] + public long Price { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/CSkillPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/CSkillPacket.cs new file mode 100644 index 0000000..7a920bb --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/CSkillPacket.cs @@ -0,0 +1,14 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("c_skill")] + public class CSkillPacket : ClientPacket + { + #region Properties + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/CSlistPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/CSlistPacket.cs new file mode 100644 index 0000000..c8f7dfa --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/CSlistPacket.cs @@ -0,0 +1,20 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("c_slist")] + public class CsListPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public ushort Index { get; set; } + + [PacketIndex(1)] + public byte Filter { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/CharacterCreatePacket.cs b/srcs/WingsAPI.Packets/ClientPackets/CharacterCreatePacket.cs new file mode 100644 index 0000000..2637e7e --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/CharacterCreatePacket.cs @@ -0,0 +1,34 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Character; + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("Char_NEW")] + public class CharacterCreatePacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public string Name { get; set; } + + [PacketIndex(1)] + public byte Slot { get; set; } + + [PacketIndex(2)] + public GenderType Gender { get; set; } + + [PacketIndex(3)] + public HairStyleType HairStyle { get; set; } + + [PacketIndex(4)] + public HairColorType HairColor { get; set; } + + public override string ToString() => $"Create Character Name: {Name} Slot: {Slot}"; + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/CharacterDeletePacket.cs b/srcs/WingsAPI.Packets/ClientPackets/CharacterDeletePacket.cs new file mode 100644 index 0000000..59f0376 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/CharacterDeletePacket.cs @@ -0,0 +1,16 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("Char_DEL")] + public class CharacterDeletePacket : ClientPacket + { + [PacketIndex(0)] + public byte Slot { get; set; } + + [PacketIndex(1)] + public string AccountPassword { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/CharacterOptionPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/CharacterOptionPacket.cs new file mode 100644 index 0000000..a40dfad --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/CharacterOptionPacket.cs @@ -0,0 +1,18 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsEmu.Packets.Enums.Character; + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("gop")] + public class CharacterOptionPacket : ClientPacket + { + [PacketIndex(0)] + public CharacterOption Option { get; set; } + + [PacketIndex(1)] + public bool IsActive { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/CharacterRenamePacket.cs b/srcs/WingsAPI.Packets/ClientPackets/CharacterRenamePacket.cs new file mode 100644 index 0000000..d1de996 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/CharacterRenamePacket.cs @@ -0,0 +1,20 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("Char_REN")] + public class CharacterRenamePacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public byte Slot { get; set; } + + [PacketIndex(1)] + public string Name { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/ComplimentPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/ComplimentPacket.cs new file mode 100644 index 0000000..119f261 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/ComplimentPacket.cs @@ -0,0 +1,17 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("compl")] + public class ComplimentPacket : ClientPacket + { + #region Properties + + [PacketIndex(1)] + public long CharacterId { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/CreateFamilyPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/CreateFamilyPacket.cs new file mode 100644 index 0000000..2a39538 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/CreateFamilyPacket.cs @@ -0,0 +1,17 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("glmk")] + public class CreateFamilyPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public string FamilyName { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/CrossServerEntrypointPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/CrossServerEntrypointPacket.cs new file mode 100644 index 0000000..7e61273 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/CrossServerEntrypointPacket.cs @@ -0,0 +1,12 @@ +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("DAC")] + public class CrossServerEntrypointPacket : ClientPacket + { + [PacketIndex(0)] + public string AccountName { get; set; } + + [PacketIndex(1)] + public byte CharacterSlot { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/CspServerPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/CspServerPacket.cs new file mode 100644 index 0000000..f549f47 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/CspServerPacket.cs @@ -0,0 +1,20 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("csp")] + public class CspServerPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public long CharacterId { get; set; } + + [PacketIndex(1)] + public string Message { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/CspePacket.cs b/srcs/WingsAPI.Packets/ClientPackets/CspePacket.cs new file mode 100644 index 0000000..0447f60 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/CspePacket.cs @@ -0,0 +1,9 @@ +using WingsEmu.Packets; + +namespace WingsAPI.Packets.ClientPackets +{ + [PacketHeader("csp_e")] + public class CspePacket : ClientPacket + { + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/CsprPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/CsprPacket.cs new file mode 100644 index 0000000..9ef86af --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/CsprPacket.cs @@ -0,0 +1,17 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("csp_r")] + public class CsprPacket : ClientPacket + { + #region Properties + + [PacketIndex(0, SerializeToEnd = true)] + public string Message { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/DepositPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/DepositPacket.cs new file mode 100644 index 0000000..dd1c65f --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/DepositPacket.cs @@ -0,0 +1,31 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("deposit")] + public class DepositPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public InventoryType Inventory { get; set; } + + [PacketIndex(1)] + public byte Slot { get; set; } + + [PacketIndex(2)] + public short Amount { get; set; } + + [PacketIndex(3)] + public byte NewSlot { get; set; } + + [PacketIndex(4)] + public bool PartnerBackpack { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/DirectionPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/DirectionPacket.cs new file mode 100644 index 0000000..0328cbf --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/DirectionPacket.cs @@ -0,0 +1,25 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("dir")] + public class DirectionPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public byte Direction { get; set; } + + [PacketIndex(1)] + public VisualType VisualType { get; set; } + + [PacketIndex(2)] + public long Id { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/EntryPointPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/EntryPointPacket.cs new file mode 100644 index 0000000..8169f88 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/EntryPointPacket.cs @@ -0,0 +1,18 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader(EntryPointPacketHeader)] + public class EntryPointPacket : ClientPacket + { + public const string EntryPointPacketHeader = "WingsEmu.EntryPoint"; + + [PacketIndex(0)] + public string SessionName { get; set; } + + [PacketIndex(1)] + public string Password { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/EquipmentInfoPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/EquipmentInfoPacket.cs new file mode 100644 index 0000000..121be07 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/EquipmentInfoPacket.cs @@ -0,0 +1,26 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("eqinfo")] + public class EquipmentInfoPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public byte Type { get; set; } + + [PacketIndex(1)] + public short Slot { get; set; } + + [PacketIndex(2)] + public byte PartnerEqSlot { get; set; } + + [PacketIndex(3)] + public long? ShopOwnerId { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/EscapePacket.cs b/srcs/WingsAPI.Packets/ClientPackets/EscapePacket.cs new file mode 100644 index 0000000..8bb3421 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/EscapePacket.cs @@ -0,0 +1,11 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("escape")] + public class EscapePacket : ClientPacket + { + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/ExcListPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/ExcListPacket.cs new file mode 100644 index 0000000..f432ff8 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/ExcListPacket.cs @@ -0,0 +1,17 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("exc_list")] + public class ExcListPacket : ClientPacket + { + #region Properties + + [PacketIndex(0, SerializeToEnd = true)] + public string PacketData { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/ExchangeRequestPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/ExchangeRequestPacket.cs new file mode 100644 index 0000000..1936244 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/ExchangeRequestPacket.cs @@ -0,0 +1,18 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("req_exc")] + public class ExchangeRequestPacket : ClientPacket + { + [PacketIndex(0)] + public RequestExchangeType RequestType { get; set; } + + [PacketIndex(1)] + public long CharacterId { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/FDelPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/FDelPacket.cs new file mode 100644 index 0000000..195522b --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/FDelPacket.cs @@ -0,0 +1,17 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("fdel")] + public class FDelPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public long CharacterId { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/FDepositPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/FDepositPacket.cs new file mode 100644 index 0000000..d4bde9a --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/FDepositPacket.cs @@ -0,0 +1,31 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("f_deposit")] + public class FDepositPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public InventoryType Inventory { get; set; } + + [PacketIndex(1)] + public short SourceSlot { get; set; } + + [PacketIndex(2)] + public short Amount { get; set; } + + [PacketIndex(3)] + public short DestinationSlot { get; set; } + + [PacketIndex(4)] + public byte? Unknown { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/FInsPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/FInsPacket.cs new file mode 100644 index 0000000..2fe5ab8 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/FInsPacket.cs @@ -0,0 +1,20 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("fins")] + public class FInsPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public byte Type { get; set; } + + [PacketIndex(1)] + public long CharacterId { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/FInsPacketType.cs b/srcs/WingsAPI.Packets/ClientPackets/FInsPacketType.cs new file mode 100644 index 0000000..ec10f98 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/FInsPacketType.cs @@ -0,0 +1,9 @@ +namespace WingsEmu.Packets.ClientPackets +{ + public enum FInsPacketType + { + INVITE = 1, + ACCEPT = 2, + REFUSE = 0 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/FReposPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/FReposPacket.cs new file mode 100644 index 0000000..c3b7727 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/FReposPacket.cs @@ -0,0 +1,26 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("f_repos")] + public class FReposPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public short OldSlot { get; set; } + + [PacketIndex(1)] + public short Amount { get; set; } + + [PacketIndex(2)] + public short NewSlot { get; set; } + + [PacketIndex(3)] + public byte? Unknown { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/FStashEndPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/FStashEndPacket.cs new file mode 100644 index 0000000..aa1c8d9 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/FStashEndPacket.cs @@ -0,0 +1,14 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("f_stash_end")] + public class FStashEndPacket : ClientPacket + { + #region Properties + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/FWithdrawPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/FWithdrawPacket.cs new file mode 100644 index 0000000..989bdcc --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/FWithdrawPacket.cs @@ -0,0 +1,23 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("f_withdraw")] + public class FWithdrawPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public short Slot { get; set; } + + [PacketIndex(1)] + public short Amount { get; set; } + + [PacketIndex(2)] + public byte? Unknown { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/FamilyChatPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/FamilyChatPacket.cs new file mode 100644 index 0000000..2af48f1 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/FamilyChatPacket.cs @@ -0,0 +1,17 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader(":")] + public class FamilyChatPacket : ClientPacket + { + #region Properties + + [PacketIndex(0, serializeToEnd: true)] + public string Message { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/FamilyDisbandPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/FamilyDisbandPacket.cs new file mode 100644 index 0000000..162507a --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/FamilyDisbandPacket.cs @@ -0,0 +1,14 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("glrm")] + public class FamilyDisbandPacket : ClientPacket + { + #region Properties + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/FamilyManagementPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/FamilyManagementPacket.cs new file mode 100644 index 0000000..e33cc25 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/FamilyManagementPacket.cs @@ -0,0 +1,25 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsEmu.Packets.Enums.Families; + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("fmg")] + public class FamilyManagementPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public FamilyAuthority FamilyAuthorityType { get; set; } + + [PacketIndex(1)] + public long TargetId { get; set; } + + [PacketIndex(2)] + public byte? Confirmed { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/FauthPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/FauthPacket.cs new file mode 100644 index 0000000..1fa568b --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/FauthPacket.cs @@ -0,0 +1,25 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsEmu.Packets.Enums.Families; + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("fauth")] + public class FAuthPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public FamilyAuthority MemberType { get; set; } + + [PacketIndex(1)] + public byte AuthorityId { get; set; } + + [PacketIndex(2)] + public byte Value { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/FbPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/FbPacket.cs new file mode 100644 index 0000000..02d1586 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/FbPacket.cs @@ -0,0 +1,9 @@ +using WingsEmu.Packets; + +namespace WingsAPI.Packets.ClientPackets +{ + [PacketHeader("fb")] + public class FbPacket : ClientPacket + { + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/FhistCtsPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/FhistCtsPacket.cs new file mode 100644 index 0000000..aeba1ec --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/FhistCtsPacket.cs @@ -0,0 +1,16 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsEmu.Packets; + +namespace WingsAPI.Packets.ClientPackets +{ + [PacketHeader("fhis_cts")] + public class FhistCtsPacket : ClientPacket + { + #region Properties + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/FlPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/FlPacket.cs new file mode 100644 index 0000000..fb858fc --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/FlPacket.cs @@ -0,0 +1,17 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("$fl")] + public class FlPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public string CharName { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/FrankCtsPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/FrankCtsPacket.cs new file mode 100644 index 0000000..6a87ad2 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/FrankCtsPacket.cs @@ -0,0 +1,17 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("frank_cts")] + public class FrankCtsPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public byte Type { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/FsLogCtsPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/FsLogCtsPacket.cs new file mode 100644 index 0000000..8431d0c --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/FsLogCtsPacket.cs @@ -0,0 +1,9 @@ +using WingsEmu.Packets; + +namespace WingsAPI.Packets.ClientPackets +{ + [PacketHeader("fslog_cts")] + public class FsLogCtsPacket : ClientPacket + { + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/GLeavePacket.cs b/srcs/WingsAPI.Packets/ClientPackets/GLeavePacket.cs new file mode 100644 index 0000000..c29e70d --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/GLeavePacket.cs @@ -0,0 +1,7 @@ +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("gleave")] + public class GLeavePacket : ClientPacket + { + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/GListPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/GListPacket.cs new file mode 100644 index 0000000..df40352 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/GListPacket.cs @@ -0,0 +1,17 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("glist")] + public class GListPacket : ClientPacket + { + #region Properties + + [PacketIndex(1)] + public GListPacketType Type { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/GListPacketType.cs b/srcs/WingsAPI.Packets/ClientPackets/GListPacketType.cs new file mode 100644 index 0000000..e5dc9d7 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/GListPacketType.cs @@ -0,0 +1,8 @@ +namespace WingsEmu.Packets.ClientPackets +{ + public enum GListPacketType : byte + { + RefreshFamilyInfo = 2, + RefreshFamilyMembers = 0 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/GameStartPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/GameStartPacket.cs new file mode 100644 index 0000000..eb199cd --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/GameStartPacket.cs @@ -0,0 +1,11 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("game_start")] + public class GameStartPacket : ClientPacket + { + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/GboxPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/GboxPacket.cs new file mode 100644 index 0000000..a4c40f7 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/GboxPacket.cs @@ -0,0 +1,21 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("gbox")] + public class GboxPacket : ClientPacket + { + [PacketIndex(0)] + public BankActionType Type { get; set; } + + [PacketIndex(1)] + public long Amount { get; set; } + + [PacketIndex(2)] + public byte Option { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/GetGiftPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/GetGiftPacket.cs new file mode 100644 index 0000000..eda2764 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/GetGiftPacket.cs @@ -0,0 +1,20 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("pcl")] + public class GetGiftPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public byte Type { get; set; } + + [PacketIndex(1)] + public int GiftId { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/GetGiftType.cs b/srcs/WingsAPI.Packets/ClientPackets/GetGiftType.cs new file mode 100644 index 0000000..ca36563 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/GetGiftType.cs @@ -0,0 +1,8 @@ +namespace WingsEmu.Packets.ClientPackets +{ + public enum GetGiftType : byte + { + OpenMail = 2, + RemoveMail = 5 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/GetPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/GetPacket.cs new file mode 100644 index 0000000..07ebe57 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/GetPacket.cs @@ -0,0 +1,23 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("get")] + public class GetPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public byte PickerType { get; set; } + + [PacketIndex(1)] + public int PickerId { get; set; } + + [PacketIndex(2)] + public long TransportId { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/GitPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/GitPacket.cs new file mode 100644 index 0000000..f941d22 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/GitPacket.cs @@ -0,0 +1,17 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("git")] + public class GitPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public int ButtonId { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/GroupSayPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/GroupSayPacket.cs new file mode 100644 index 0000000..65ffd86 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/GroupSayPacket.cs @@ -0,0 +1,17 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader(";")] + public class GroupSayPacket : ClientPacket + { + #region Properties + + [PacketIndex(0, SerializeToEnd = true)] + public string Message { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/GuriPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/GuriPacket.cs new file mode 100644 index 0000000..254ebcd --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/GuriPacket.cs @@ -0,0 +1,29 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("guri")] + public class GuriPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public int Type { get; set; } + + [PacketIndex(1)] + public int Argument { get; set; } + + [PacketIndex(2)] + public long? User { get; set; } + + [PacketIndex(3)] + public long? Data { get; set; } + + [PacketIndex(4, serializeToEnd: true)] + public string Value { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/HeroPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/HeroPacket.cs new file mode 100644 index 0000000..f08fca9 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/HeroPacket.cs @@ -0,0 +1,17 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("hero")] + public class HeroPacket : ClientPacket + { + #region Properties + + [PacketIndex(0, serializeToEnd: true)] + public string Message { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/ISortPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/ISortPacket.cs new file mode 100644 index 0000000..2cbcac7 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/ISortPacket.cs @@ -0,0 +1,14 @@ +using WingsEmu.Packets; + +namespace WingsAPI.Packets.ClientPackets +{ + [PacketHeader("isort")] + public class ISortPacket : ClientPacket + { + [PacketIndex(0)] + public byte InventoryType { get; set; } + + [PacketIndex(1)] + public bool Confirm { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/JoinFamilyPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/JoinFamilyPacket.cs new file mode 100644 index 0000000..c49f119 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/JoinFamilyPacket.cs @@ -0,0 +1,20 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("gjoin")] + public class JoinFamilyPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public byte Type { get; set; } + + [PacketIndex(1)] + public long CharacterId { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/LbsPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/LbsPacket.cs new file mode 100644 index 0000000..31b7c74 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/LbsPacket.cs @@ -0,0 +1,17 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("lbs")] + public class LbsPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public int Type { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/MJoinPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/MJoinPacket.cs new file mode 100644 index 0000000..bcc65fd --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/MJoinPacket.cs @@ -0,0 +1,23 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("mjoin")] + public class MJoinPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public byte Type { get; set; } + + [PacketIndex(1)] + public long CharacterId { get; set; } + + [PacketIndex(2)] + public byte OptionType { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/MLEditPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/MLEditPacket.cs new file mode 100644 index 0000000..e483cac --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/MLEditPacket.cs @@ -0,0 +1,20 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("mledit")] + public class MlEditPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public byte Type { get; set; } + + [PacketIndex(1, SerializeToEnd = true)] + public string Parameters { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/MShopPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/MShopPacket.cs new file mode 100644 index 0000000..7485c9d --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/MShopPacket.cs @@ -0,0 +1,19 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("m_shop")] + public class MShopPacket : ClientPacket + { + [PacketIndex(0)] + public MShopPacketType Type { get; set; } + + //TODO: With the actual deserialization it is impossible to represent this packet's structure + [PacketIndex(1, SerializeToEnd = true)] + public string PacketData { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/MallPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/MallPacket.cs new file mode 100644 index 0000000..614c3b0 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/MallPacket.cs @@ -0,0 +1,17 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("mall")] + public class MallPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public int Type { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/MinigamePacket.cs b/srcs/WingsAPI.Packets/ClientPackets/MinigamePacket.cs new file mode 100644 index 0000000..cceb123 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/MinigamePacket.cs @@ -0,0 +1,29 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("mg")] + public class MinigamePacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public byte Type { get; set; } + + [PacketIndex(1)] + public byte Id { get; set; } + + [PacketIndex(2)] + public short MinigameVNum { get; set; } + + [PacketIndex(3)] + public long? Point { get; set; } + + [PacketIndex(4)] + public long? Point2 { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/MkraidPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/MkraidPacket.cs new file mode 100644 index 0000000..b0616e2 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/MkraidPacket.cs @@ -0,0 +1,20 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("mkraid")] + public class MkraidPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public byte RaidId { get; set; } + + [PacketIndex(1)] + public short MapInstanceId { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/MultiTargetListPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/MultiTargetListPacket.cs new file mode 100644 index 0000000..753ddd8 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/MultiTargetListPacket.cs @@ -0,0 +1,22 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("mtlist")] + public class MultiTargetListPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public byte TargetsAmount { get; set; } + + [PacketIndex(1, RemoveSeparator = true)] + public List Targets { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/MultiTargetListSubPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/MultiTargetListSubPacket.cs new file mode 100644 index 0000000..1c305aa --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/MultiTargetListSubPacket.cs @@ -0,0 +1,18 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("multi_target_list_sub_packet")] // header will be ignored for serializing just sub list packets + public class MultiTargetListSubPacket : ClientPacket + { + [PacketIndex(0)] + public VisualType TargetType { get; set; } + + [PacketIndex(1)] + public int TargetId { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/MvePacket.cs b/srcs/WingsAPI.Packets/ClientPackets/MvePacket.cs new file mode 100644 index 0000000..cb85d88 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/MvePacket.cs @@ -0,0 +1,28 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("mve")] + public class MvePacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public InventoryType InventoryType { get; set; } + + [PacketIndex(1)] + public short Slot { get; set; } + + [PacketIndex(2)] + public InventoryType DestinationInventoryType { get; set; } + + [PacketIndex(3)] + public short DestinationSlot { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/MviPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/MviPacket.cs new file mode 100644 index 0000000..d0d2f7c --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/MviPacket.cs @@ -0,0 +1,28 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("mvi")] + public class MviPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public InventoryType InventoryType { get; set; } + + [PacketIndex(1)] + public short Slot { get; set; } + + [PacketIndex(2)] + public short Amount { get; set; } + + [PacketIndex(3)] + public byte DestinationSlot { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/MzPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/MzPacket.cs new file mode 100644 index 0000000..910bf2a --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/MzPacket.cs @@ -0,0 +1,23 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("mz")] + public class MzPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public string Ip { get; set; } + + [PacketIndex(1)] + public short Port { get; set; } + + [PacketIndex(2)] + public byte Slot { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/NRunPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/NRunPacket.cs new file mode 100644 index 0000000..c57b394 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/NRunPacket.cs @@ -0,0 +1,27 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("n_run")] + public class NRunPacket : ClientPacket + { + [PacketIndex(0)] + public NpcRunType Type { get; set; } + + [PacketIndex(1)] + public short Argument { get; set; } + + [PacketIndex(2)] + public VisualType Value { get; set; } + + [PacketIndex(3)] + public int NpcId { get; set; } + + [PacketIndex(4)] + public byte? Confirmation { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/NcifPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/NcifPacket.cs new file mode 100644 index 0000000..32d4aab --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/NcifPacket.cs @@ -0,0 +1,20 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("ncif")] + public class NcifPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public byte Type { get; set; } + + [PacketIndex(1)] + public long TargetId { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/Nos0575Packet.cs b/srcs/WingsAPI.Packets/ClientPackets/Nos0575Packet.cs new file mode 100644 index 0000000..e128bef --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/Nos0575Packet.cs @@ -0,0 +1,22 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("NoS0575")] + public class Nos0575Packet : ClientPacket + { + [PacketIndex(0)] + public int Number { get; set; } + + [PacketIndex(1)] + public string Name { get; set; } + + [PacketIndex(2)] + public string Password { get; set; } + + [PacketIndex(3)] + public string ClientData { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/NpinfoPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/NpinfoPacket.cs new file mode 100644 index 0000000..506f9e3 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/NpinfoPacket.cs @@ -0,0 +1,17 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("npinfo")] + public class NpinfoPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public byte Page { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/ObaPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/ObaPacket.cs new file mode 100644 index 0000000..b1c9cc8 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/ObaPacket.cs @@ -0,0 +1,11 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("ob_a")] + public class ObaPacket : ClientPacket + { + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/PJoinPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/PJoinPacket.cs new file mode 100644 index 0000000..8220d2d --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/PJoinPacket.cs @@ -0,0 +1,22 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("pjoin")] + public class PJoinPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public GroupRequestType RequestType { get; set; } + + [PacketIndex(1)] + public long CharacterId { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/PdtClosePacket.cs b/srcs/WingsAPI.Packets/ClientPackets/PdtClosePacket.cs new file mode 100644 index 0000000..187229c --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/PdtClosePacket.cs @@ -0,0 +1,11 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("pdtclose")] + public class PdtClosePacket : ClientPacket + { + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/PdtsePacket.cs b/srcs/WingsAPI.Packets/ClientPackets/PdtsePacket.cs new file mode 100644 index 0000000..0d4b5e1 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/PdtsePacket.cs @@ -0,0 +1,29 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("pdtse")] + public class PdtsePacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public byte Type { get; set; } + + [PacketIndex(1)] + public short VNum { get; set; } + + [PacketIndex(2)] + public int? Unknown { get; set; } + + [PacketIndex(3)] + public short? EqItemSlot { get; set; } + + [PacketIndex(4)] + public int? AnotherUnknown { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/PleavePacket.cs b/srcs/WingsAPI.Packets/ClientPackets/PleavePacket.cs new file mode 100644 index 0000000..5b63915 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/PleavePacket.cs @@ -0,0 +1,14 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("pleave")] + public class PLeavePacket : ClientPacket + { + #region Properties + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/PreqPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/PreqPacket.cs new file mode 100644 index 0000000..b5eee01 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/PreqPacket.cs @@ -0,0 +1,17 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("preq")] + public class PreqPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public bool Confirmed { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/PslPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/PslPacket.cs new file mode 100644 index 0000000..c098294 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/PslPacket.cs @@ -0,0 +1,13 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("psl")] + public class PslPacket : ClientPacket + { + [PacketIndex(0)] + public int Type { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/PsopServerPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/PsopServerPacket.cs new file mode 100644 index 0000000..d2bbf5e --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/PsopServerPacket.cs @@ -0,0 +1,23 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("ps_op")] + public class PsopServerPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public byte PetSlot { get; set; } + + [PacketIndex(1)] + public byte SkillSlot { get; set; } + + [PacketIndex(2)] + public byte Option { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/PstPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/PstPacket.cs new file mode 100644 index 0000000..68acb62 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/PstPacket.cs @@ -0,0 +1,35 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("pst")] + public class PstPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public int Argument { get; set; } + + [PacketIndex(1)] + public int Type { get; set; } + + [PacketIndex(2)] + public long Id { get; set; } + + [PacketIndex(3)] + public int? Unknow1 { get; set; } + + [PacketIndex(4)] + public int Unknow2 { get; set; } + + [PacketIndex(5)] + public string Receiver { get; set; } + + [PacketIndex(6, SerializeToEnd = true)] + public string Data { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/PstPacketType.cs b/srcs/WingsAPI.Packets/ClientPackets/PstPacketType.cs new file mode 100644 index 0000000..196d16b --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/PstPacketType.cs @@ -0,0 +1,9 @@ +namespace WingsEmu.Packets.ClientPackets +{ + public enum PstPacketType : byte + { + SendNote = 1, + RemoveNote = 2, + ReadNote = 3 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/PtCtlPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/PtCtlPacket.cs new file mode 100644 index 0000000..3275ec8 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/PtCtlPacket.cs @@ -0,0 +1,23 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("ptctl")] + public class PtCtlPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public short MapId { get; set; } + + [PacketIndex(1)] + public byte Amount { get; set; } + + [PacketIndex(2, SerializeToEnd = true)] + public string PacketEnd { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/PtCtlSubPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/PtCtlSubPacket.cs new file mode 100644 index 0000000..762d24d --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/PtCtlSubPacket.cs @@ -0,0 +1,19 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("pt_ctl_sub_packet")] // header will be ignored for serializing just sub list packets + public class PtCtlSubPacket : ClientPacket + { + [PacketIndex(0)] + public long UserId { get; set; } + + [PacketIndex(1)] + public short UserX { get; set; } + + [PacketIndex(2)] + public short UserY { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/PulsePacket.cs b/srcs/WingsAPI.Packets/ClientPackets/PulsePacket.cs new file mode 100644 index 0000000..f1c3e9a --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/PulsePacket.cs @@ -0,0 +1,17 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("pulse")] + public class PulsePacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public int Tick { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/PutPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/PutPacket.cs new file mode 100644 index 0000000..f2287de --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/PutPacket.cs @@ -0,0 +1,25 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("put")] + public class PutPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public InventoryType InventoryType { get; set; } + + [PacketIndex(1)] + public byte Slot { get; set; } + + [PacketIndex(2)] + public short Amount { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/QSetPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/QSetPacket.cs new file mode 100644 index 0000000..58d56fb --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/QSetPacket.cs @@ -0,0 +1,25 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("qset")] + public class QSetPacket : ClientPacket + { + [PacketIndex(0)] + public QsetPacketType Type { get; set; } + + [PacketIndex(1)] + public short QuicklistTab { get; set; } + + [PacketIndex(2)] + public short QuicklistSlot { get; set; } + + [PacketIndex(3)] + public short? DestinationType { get; set; } + + [PacketIndex(4)] + public short? DestinationSlotOrVnum { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/QsetPacketType.cs b/srcs/WingsAPI.Packets/ClientPackets/QsetPacketType.cs new file mode 100644 index 0000000..6de5c53 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/QsetPacketType.cs @@ -0,0 +1,10 @@ +namespace WingsEmu.Packets.ClientPackets +{ + public enum QsetPacketType + { + SET_ITEM = 0, + SET_SKILL = 1, + SWAP = 2, + REMOVE = 3 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/QtPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/QtPacket.cs new file mode 100644 index 0000000..0e2ced2 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/QtPacket.cs @@ -0,0 +1,20 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("qt")] + public class QtPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public short Action { get; set; } + + [PacketIndex(1)] + public int Slot { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/RInfoPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/RInfoPacket.cs new file mode 100644 index 0000000..64487af --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/RInfoPacket.cs @@ -0,0 +1,20 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("r_info")] + public class RInfoPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public short? VNum { get; set; } + + [PacketIndex(1)] + public short? Slot { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/RankSkPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/RankSkPacket.cs new file mode 100644 index 0000000..f99ed86 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/RankSkPacket.cs @@ -0,0 +1,17 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("ranksk")] + public class RankSkPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public byte slot { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/RdPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/RdPacket.cs new file mode 100644 index 0000000..a2ea88b --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/RdPacket.cs @@ -0,0 +1,23 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("rd")] + public class RdPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public short Type { get; set; } + + [PacketIndex(1)] + public long CharacterId { get; set; } + + [PacketIndex(2)] + public short? Parameter { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/Relations/BlDelPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/Relations/BlDelPacket.cs new file mode 100644 index 0000000..2bd63cc --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/Relations/BlDelPacket.cs @@ -0,0 +1,17 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("bldel")] + public class BlDelPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public long CharacterId { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/Relations/BlInsPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/Relations/BlInsPacket.cs new file mode 100644 index 0000000..7d01c29 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/Relations/BlInsPacket.cs @@ -0,0 +1,17 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("blins")] + public class BlInsPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public long CharacterId { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/RemovePacket.cs b/srcs/WingsAPI.Packets/ClientPackets/RemovePacket.cs new file mode 100644 index 0000000..8a3f89c --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/RemovePacket.cs @@ -0,0 +1,20 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("remove")] + public class RemovePacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public byte InventorySlot { get; set; } + + [PacketIndex(1)] + public short PartnerSlot { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/ReposPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/ReposPacket.cs new file mode 100644 index 0000000..33c60ac --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/ReposPacket.cs @@ -0,0 +1,26 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("repos")] + public class ReposPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public short OldSlot { get; set; } + + [PacketIndex(1)] + public short Amount { get; set; } + + [PacketIndex(2)] + public short NewSlot { get; set; } + + [PacketIndex(3)] + public bool IsPartnerBackpack { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/ReqInfoPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/ReqInfoPacket.cs new file mode 100644 index 0000000..88c5d51 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/ReqInfoPacket.cs @@ -0,0 +1,23 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("req_info")] + public class ReqInfoPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public byte Type { get; set; } + + [PacketIndex(1)] + public long TargetVNum { get; set; } + + [PacketIndex(2)] + public int? MateVNum { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/RequestNpcPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/RequestNpcPacket.cs new file mode 100644 index 0000000..1e873c7 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/RequestNpcPacket.cs @@ -0,0 +1,22 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("npc_req")] + public class RequestNpcPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public VisualType VisualType { get; set; } + + [PacketIndex(1)] + public long TargetNpcId { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/RevivalPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/RevivalPacket.cs new file mode 100644 index 0000000..d10a2a1 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/RevivalPacket.cs @@ -0,0 +1,17 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("revival")] + public class RevivalPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public byte Type { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/RlPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/RlPacket.cs new file mode 100644 index 0000000..4ab06a7 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/RlPacket.cs @@ -0,0 +1,20 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("rl")] + public class RlPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public short Type { get; set; } + + [PacketIndex(1)] + public string CharacterName { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/RmvobjPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/RmvobjPacket.cs new file mode 100644 index 0000000..2d86765 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/RmvobjPacket.cs @@ -0,0 +1,17 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("rmvobj")] + public class RmvobjPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public short Slot { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/RselPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/RselPacket.cs new file mode 100644 index 0000000..9227182 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/RselPacket.cs @@ -0,0 +1,14 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("rsel")] + public class RSelPacket : ClientPacket + { + #region Properties + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/RstartPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/RstartPacket.cs new file mode 100644 index 0000000..006d8a3 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/RstartPacket.cs @@ -0,0 +1,17 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("rstart")] + public class RStartPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public byte? Type { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/RxitPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/RxitPacket.cs new file mode 100644 index 0000000..e9ece2c --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/RxitPacket.cs @@ -0,0 +1,17 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("rxit")] + public class RxitPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public byte State { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/SayPPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/SayPPacket.cs new file mode 100644 index 0000000..a42eae4 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/SayPPacket.cs @@ -0,0 +1,20 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("say_p")] + public class SayPPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public int PetId { get; set; } + + [PacketIndex(1, SerializeToEnd = true)] + public string Message { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/SayPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/SayPacket.cs new file mode 100644 index 0000000..01af845 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/SayPacket.cs @@ -0,0 +1,17 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("say")] + public class SayPacket : ClientPacket + { + #region Properties + + [PacketIndex(0, SerializeToEnd = true)] + public string Message { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/ScpCtsPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/ScpCtsPacket.cs new file mode 100644 index 0000000..94e9ff9 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/ScpCtsPacket.cs @@ -0,0 +1,14 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("sc_p_cts")] + public class ScpCtsPacket : ClientPacket + { + #region Properties + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/ScriptPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/ScriptPacket.cs new file mode 100644 index 0000000..24ffac5 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/ScriptPacket.cs @@ -0,0 +1,16 @@ +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("script")] + public class ScriptPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public short Type { get; set; } + + [PacketIndex(1, serializeToEnd: true)] + public string Data { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/SelectPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/SelectPacket.cs new file mode 100644 index 0000000..67cd7eb --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/SelectPacket.cs @@ -0,0 +1,17 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("select")] + public class SelectPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public byte Slot { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/SellPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/SellPacket.cs new file mode 100644 index 0000000..a748763 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/SellPacket.cs @@ -0,0 +1,23 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("sell")] + public class SellPacket : ClientPacket + { + #region Properties + + [PacketIndex(2)] + public short Data { get; set; } + + [PacketIndex(3)] + public byte? Slot { get; set; } + + [PacketIndex(4)] + public ushort? Amount { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/ShopClosePacket.cs b/srcs/WingsAPI.Packets/ClientPackets/ShopClosePacket.cs new file mode 100644 index 0000000..165453f --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/ShopClosePacket.cs @@ -0,0 +1,14 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("shopclose")] + public class ShopClosePacket : ClientPacket + { + #region Properties + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/ShoppingPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/ShoppingPacket.cs new file mode 100644 index 0000000..9c7c04d --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/ShoppingPacket.cs @@ -0,0 +1,20 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("shopping")] + public class ShoppingPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public byte Type { get; set; } + + [PacketIndex(3)] + public int NpcId { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/SitPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/SitPacket.cs new file mode 100644 index 0000000..96ee3b4 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/SitPacket.cs @@ -0,0 +1,22 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("rest")] + public class SitPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public byte Amount { get; set; } + + [PacketIndex(1, RemoveSeparator = true)] + public List Users { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/SitSubPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/SitSubPacket.cs new file mode 100644 index 0000000..bc1180f --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/SitSubPacket.cs @@ -0,0 +1,18 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("sit_sub_packet")] // header will be ignored for serializing just sub list packets + public class SitSubPacket : ClientPacket + { + [PacketIndex(0)] + public VisualType VisualType { get; set; } + + [PacketIndex(1)] + public long UserId { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/SnapPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/SnapPacket.cs new file mode 100644 index 0000000..d52047d --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/SnapPacket.cs @@ -0,0 +1,14 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("snap")] + public class SnapPacket : ClientPacket + { + #region Properties + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/SortOpenPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/SortOpenPacket.cs new file mode 100644 index 0000000..fe37bb7 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/SortOpenPacket.cs @@ -0,0 +1,11 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("sortopen")] + public class SortOpenPacket : ClientPacket + { + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/SpTransformPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/SpTransformPacket.cs new file mode 100644 index 0000000..c4c6cce --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/SpTransformPacket.cs @@ -0,0 +1,32 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("sl")] + public class SpTransformPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public byte Type { get; set; } + + [PacketIndex(3)] + public int TransportId { get; set; } + + [PacketIndex(4)] + public short SpecialistDamage { get; set; } + + [PacketIndex(5)] + public short SpecialistDefense { get; set; } + + [PacketIndex(6)] + public short SpecialistElement { get; set; } + + [PacketIndex(7)] + public short SpecialistHp { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/SpecialistHolderPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/SpecialistHolderPacket.cs new file mode 100644 index 0000000..7bbafe8 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/SpecialistHolderPacket.cs @@ -0,0 +1,23 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("s_carrier")] + public class SpecialistHolderPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public short Slot { get; set; } + + [PacketIndex(1)] + public byte HolderSlot { get; set; } + + [PacketIndex(2)] + public byte HolderType { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/StashEndPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/StashEndPacket.cs new file mode 100644 index 0000000..cc0a4fb --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/StashEndPacket.cs @@ -0,0 +1,14 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("stash_end")] + public class StashEndPacket : ClientPacket + { + #region Properties + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/SuctlPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/SuctlPacket.cs new file mode 100644 index 0000000..7d46a69 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/SuctlPacket.cs @@ -0,0 +1,29 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("suctl")] + public class SuctlPacket : ClientPacket + { + [PacketIndex(0)] + public int CastId { get; set; } + + [PacketIndex(1)] + public int Unknown2 { get; set; } + + [PacketIndex(2)] + public int MateTransportId { get; set; } + + [PacketIndex(3)] + public VisualType TargetType { get; set; } + + [PacketIndex(4)] + public long TargetId { get; set; } + + public override string ToString() => $"{CastId} {Unknown2} {MateTransportId} {TargetType} {TargetId}"; + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/TaCallPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/TaCallPacket.cs new file mode 100644 index 0000000..3050311 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/TaCallPacket.cs @@ -0,0 +1,17 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("ta_call")] + public class TaCallPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public byte CalledIndex { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/TawPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/TawPacket.cs new file mode 100644 index 0000000..18824ff --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/TawPacket.cs @@ -0,0 +1,17 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("taw")] + public class TawPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public string Username { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/TitEqPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/TitEqPacket.cs new file mode 100644 index 0000000..42596ce --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/TitEqPacket.cs @@ -0,0 +1,14 @@ +using WingsEmu.Packets.Enums.Titles; + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("tit_eq")] + public class TitEqPacket : ClientPacket + { + [PacketIndex(0)] + public TitEqPacketType Type { get; set; } + + [PacketIndex(1)] + public int ItemVnum { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/TodayPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/TodayPacket.cs new file mode 100644 index 0000000..532c201 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/TodayPacket.cs @@ -0,0 +1,14 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("today_cts")] + public class TodayPacket : ClientPacket + { + #region Properties + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/TreqClientPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/TreqClientPacket.cs new file mode 100644 index 0000000..a1a3d98 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/TreqClientPacket.cs @@ -0,0 +1,25 @@ +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("treq")] + public class TreqClientPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public int X { get; set; } + + [PacketIndex(1)] + public int Y { get; set; } + + [PacketIndex(2)] + public byte StartPress { get; set; } + + [PacketIndex(3)] + public long RecordPressAndCharacterId { get; set; } + + [PacketIndex(4)] + public bool RecordPressConfirm { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/UpetPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/UpetPacket.cs new file mode 100644 index 0000000..7b7c97e --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/UpetPacket.cs @@ -0,0 +1,26 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("u_pet")] + public class UpetPacket : ClientPacket + { + [PacketIndex(0)] + public int MateTransportId { get; set; } + + [PacketIndex(1)] + public VisualType TargetType { get; set; } + + [PacketIndex(2)] + public int TargetId { get; set; } + + [PacketIndex(3)] + public int Unknown2 { get; set; } + + public override string ToString() => $"{MateTransportId} {TargetType} {TargetId} 0"; + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/UpgradePacket.cs b/srcs/WingsAPI.Packets/ClientPackets/UpgradePacket.cs new file mode 100644 index 0000000..aa9c786 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/UpgradePacket.cs @@ -0,0 +1,37 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("up_gr")] + public class UpgradePacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public byte UpgradeType { get; set; } + + [PacketIndex(1)] + public InventoryType InventoryType { get; set; } + + [PacketIndex(2)] + public byte Slot { get; set; } + + [PacketIndex(3)] + public InventoryType? InventoryType2 { get; set; } + + [PacketIndex(4)] + public byte? Slot2 { get; set; } + + [PacketIndex(5)] + public InventoryType? CellonInventoryType { get; set; } + + [PacketIndex(6)] + public byte? CellonSlot { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/UpgradePacketType.cs b/srcs/WingsAPI.Packets/ClientPackets/UpgradePacketType.cs new file mode 100644 index 0000000..af85fb7 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/UpgradePacketType.cs @@ -0,0 +1,21 @@ +namespace WingsEmu.Packets.ClientPackets +{ + public enum UpgradePacketType : byte + { + FREE_CHICKEN_UPGRADE = 35, + FREE_PAJAMA_UPGRADE = 38, + FREE_PIRATE_UPGRADFE = 42, + PLAYER_ITEM_TO_PARTNER = 0, + ITEM_UPGRADE = 1, + CELLON_UPGRADE = 3, + ITEM_RARITY = 7, + ITEM_SUM = 8, + SP_UPGRADE = 9, + ITEM_UPGRADE_SCROLL = 20, + ITEM_RARITY_SCROLL = 21, + SP_UPGRADE_SCROLL_BLUE = 25, + SP_UPGRADE_SCROLL_RED = 26, + SP_PERFECTION = 41, + ITEM_UPGRADE_GOLD_SCROLL = 43 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/UpsPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/UpsPacket.cs new file mode 100644 index 0000000..4347128 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/UpsPacket.cs @@ -0,0 +1,30 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("u_ps")] + public class UpsPacket : ClientPacket + { + [PacketIndex(0)] + public int MateTransportId { get; set; } + + [PacketIndex(1)] + public VisualType TargetType { get; set; } + + [PacketIndex(2)] + public int TargetId { get; set; } + + [PacketIndex(3)] + public int SkillSlot { get; set; } + + [PacketIndex(4)] + public short MapX { get; set; } + + [PacketIndex(5)] + public short MapY { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/UseAtSkillPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/UseAtSkillPacket.cs new file mode 100644 index 0000000..f6e3878 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/UseAtSkillPacket.cs @@ -0,0 +1,23 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("u_as")] + public class UseAtSkillPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public int CastId { get; set; } + + [PacketIndex(1)] + public short MapX { get; set; } + + [PacketIndex(2)] + public short MapY { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/UseItemPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/UseItemPacket.cs new file mode 100644 index 0000000..da5eb31 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/UseItemPacket.cs @@ -0,0 +1,18 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("u_i")] + public class UseItemPacket : ClientPacket + { + [PacketIndex(2)] + public InventoryType Type { get; set; } + + [PacketIndex(3)] + public short Slot { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/UseSkillPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/UseSkillPacket.cs new file mode 100644 index 0000000..75f1765 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/UseSkillPacket.cs @@ -0,0 +1,33 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("u_s")] + public class UseSkillPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public int CastId { get; set; } + + [PacketIndex(1)] + public VisualType VisualType { get; set; } + + [PacketIndex(2)] + public int MapMonsterId { get; set; } + + [PacketIndex(3)] + public short? MapX { get; set; } + + [PacketIndex(4)] + public short? MapY { get; set; } + + public override string ToString() => $"{CastId} {VisualType} {MapMonsterId} {MapX} {MapY}"; + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/UseobjPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/UseobjPacket.cs new file mode 100644 index 0000000..845671d --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/UseobjPacket.cs @@ -0,0 +1,20 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("useobj")] + public class UseobjPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public string CharacterName { get; set; } + + [PacketIndex(1)] + public short Slot { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/WalkPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/WalkPacket.cs new file mode 100644 index 0000000..e6aacc6 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/WalkPacket.cs @@ -0,0 +1,26 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("walk")] + public class WalkPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public short XCoordinate { get; set; } + + [PacketIndex(1)] + public short YCoordinate { get; set; } + + [PacketIndex(2)] + public short Unknown { get; set; } + + [PacketIndex(3)] + public short Speed { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/WearPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/WearPacket.cs new file mode 100644 index 0000000..9c41924 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/WearPacket.cs @@ -0,0 +1,23 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("wear")] + public class WearPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public byte InventorySlot { get; set; } + + [PacketIndex(1)] + public byte PetId { get; set; } + + [PacketIndex(2)] + public bool BoundItem { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/WearPartnerCardPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/WearPartnerCardPacket.cs new file mode 100644 index 0000000..1fdfeb6 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/WearPartnerCardPacket.cs @@ -0,0 +1,12 @@ +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("wear_pc")] + public class WearPartnerCardPacket : ClientPacket + { + [PacketIndex(0)] + public byte InventorySlot { get; set; } + + [PacketIndex(1)] + public byte PetId { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/WhisperPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/WhisperPacket.cs new file mode 100644 index 0000000..256bba1 --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/WhisperPacket.cs @@ -0,0 +1,17 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("/")] + public class WhisperPacket : ClientPacket + { + #region Properties + + [PacketIndex(0, SerializeToEnd = true)] + public string Message { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/WithdrawPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/WithdrawPacket.cs new file mode 100644 index 0000000..1d1c90a --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/WithdrawPacket.cs @@ -0,0 +1,19 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("withdraw")] + public class WithdrawPacket : ClientPacket + { + [PacketIndex(0)] + public short Slot { get; set; } + + [PacketIndex(1)] + public short Amount { get; set; } + + [PacketIndex(2)] + public bool PetBackpack { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ClientPackets/WreqPacket.cs b/srcs/WingsAPI.Packets/ClientPackets/WreqPacket.cs new file mode 100644 index 0000000..495a14d --- /dev/null +++ b/srcs/WingsAPI.Packets/ClientPackets/WreqPacket.cs @@ -0,0 +1,16 @@ +namespace WingsEmu.Packets.ClientPackets +{ + [PacketHeader("wreq")] + public class WreqPacket : ClientPacket + { + #region Properties + + [PacketIndex(0)] + public byte Value { get; set; } + + [PacketIndex(1)] + public long? Param { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/Act4/Act4FactionStateType.cs b/srcs/WingsAPI.Packets/Enums/Act4/Act4FactionStateType.cs new file mode 100644 index 0000000..b6cb858 --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/Act4/Act4FactionStateType.cs @@ -0,0 +1,9 @@ +namespace WingsAPI.Packets.Enums.Act4 +{ + public enum Act4FactionStateType : byte + { + Nothing = 0, + Mukraju = 1, + RaidDungeon = 3 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/Act4/DungeonEventType.cs b/srcs/WingsAPI.Packets/Enums/Act4/DungeonEventType.cs new file mode 100644 index 0000000..780d7a0 --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/Act4/DungeonEventType.cs @@ -0,0 +1,10 @@ +namespace WingsAPI.Packets.Enums.Act4 +{ + public enum DungeonEventType + { + BossRoomClosed = 1, + BossRoomOpen = 2, + BossRoomFinished = 3, + InBossRoom = 4 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/Act5RespawnType.cs b/srcs/WingsAPI.Packets/Enums/Act5RespawnType.cs new file mode 100644 index 0000000..1ab0636 --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/Act5RespawnType.cs @@ -0,0 +1,9 @@ +namespace WingsAPI.Packets.Enums +{ + public enum Act5RespawnType + { + MORTAZ_DESERT_PORT = 0, + AKAMUR_CAMP = 1, + DESERT_EAGLE_CITY = 2 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/AdditionalTypes.cs b/srcs/WingsAPI.Packets/Enums/AdditionalTypes.cs new file mode 100644 index 0000000..e61bbcc --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/AdditionalTypes.cs @@ -0,0 +1,1281 @@ +namespace WingsEmu.Packets.Enums +{ + public class AdditionalTypes + { + public enum AbsorbedSpirit : byte + { + ApplyEffectIfPresent = 11, + ApplyEffectIfNotPresent = 12, + ResistForcedMovement = 21, + ResistForcedMovementNegated = 22, + MagicCooldownIncreased = 31, + MagicCooldownDecreased = 32 + } + + public enum Absorption : byte + { + AllAttackIncreased = 11, + AllAttackDecreased = 12, + MeleeAttackIncreased = 21, + MeleeAttackDecreased = 22, + RangedAttackIncreased = 31, + RangedAttackDecreased = 32, + MagicalAttackIncreased = 41, + MagicalAttacksDecreased = 42 + } + + public enum AbsorptionAndPowerSkill : byte + { + AddDamageToHP = 11, + RemoveDamnageFromHP = 12, + DamageIncreasedSkill = 41, + DamageDecreasedSkill = 42, + CriticalIncreasedSkill = 51, + CriticalDecreasedSkill = 52 + } + + public enum AngerSkill : byte + { + AttackInRangeNotLocation = 11, + AttackInRangeNotLocationNegated = 12, + ReduceEnemyHPChance = 21, + ReduceEnemyHPByDamageChance = 22, + BlockGoodEffect = 31, + BlockGoodEffectNegated = 32, + OnlyNormalAttacks = 41, + OnlyNormalAttacksNegated = 42 + } + + public enum ArenaCamera : byte + { + CallParticipant1 = 11, + CallParticipant2 = 12, + CallParticipant2Negated = 21, + CallParticipant2NegatedNegated = 22, + CallParticipant3 = 31, + CallParticipant3Negated = 32, + SwitchView = 41, + SwitchViewNegated = 42, + SeeHiddenAllies = 51, + SeeHiddenAlliesNegated = 52 + } + + public enum AttackPower : byte + { + AllAttacksIncreased = 11, + AllAttacksDecreased = 12, + MeleeAttacksIncreased = 21, + MeleeAttacksDecreased = 22, + RangedAttacksIncreased = 31, + RangedAttacksDecreased = 32, + MagicalAttacksIncreased = 41, + MagicalAttacksDecreased = 42, + AttackLevelIncreased = 51, + AttackLevelDecreased = 52 + } + + public enum BackToMiniland : byte + { + BackToMinilandHpRegen = 11, + HiddenEnemyAdditionalDamage = 21, + HiddenEnemyDamageReceivedIncreased = 31, + HiddenEnemyDamageReceivedDecreased = 32, + HiddenEnemyDetection = 41, + UseSkillManyTimes = 51 + } + + public enum BearSpirit : byte + { + IncreaseMaximumHP = 11, + DecreaseMaximumHP = 12, + IncreaseMaximumMP = 31, + DecreaseMaximumMP = 32 + } + + public enum Block : byte + { + ChanceAllIncreased = 11, + ChanceAllDecreased = 12, + ChanceMeleeIncreased = 21, + ChanceMeleeDecreased = 22, + ChanceRangedIncreased = 31, + ChanceRangedDecreased = 32, + ChanceMagicalIncreased = 41, + ChanceMagicalDecreased = 42 + } + + public enum Buff : byte + { + ChanceCausing = 11, + ChanceRemoving = 12, + PreventingBadEffect = 21, + PreventingBadEffectNegated = 22, + NearbyObjectsAboveLevel = 31, + NearbyObjectsBelowLevel = 32, + EffectResistance = 41, + EffectResistanceNegated = 42, + CancelGroupOfEffects = 51, + CounteractPoison = 52 + } + + public enum CalculatingLevel : byte + { + CalculatedAttackLevel = 11, + CalculatedAttackLevelNegated = 12, + CalculatedDefenceLevel = 21, + CalculatedDefenceLevelNegated = 22 + } + + public enum Capture : byte + { + CaptureAnimal = 11, + CaptureAnimalNegated = 12 + } + + public enum Casting : byte + { + EffectDurationIncreased = 11, + EffectDurationDecreased = 12, + ManaForSkillsIncreased = 21, + ManaForSkillsDecreased = 22, + AttackSpeedIncreased = 31, + AttackSpeedDecreased = 32, + CastingSkillFailed = 41, + CastingSkillFailedNegated = 42, + InterruptCasting = 51, + InterruptCastingNegated = 52 + } + + public enum ChangingPlace : byte + { + ReplaceTargetPosition = 11, + ReplaceTargetPositionNegated = 12, + IncreaseReputationLostAfterDeath = 21, + DecreaseReputationLostAfterDeath = 22, + IncreaseXpLostAfterDeath = 31, + DecreaseXpLostAfterDeath = 32, + IncreaseDamageVersusAngels = 41, + DecreaseDamageVersusAngels = 42, + IncreaseDamageVersusDemons = 51, + DecreaseDamageVersusDemons = 52 + } + + public enum Count : byte + { + Summon = 11, + SummonChance = 12, + BelialBound = 21, + BelialBoundNegated = 22, + IncreaseCritAttackOnEnd = 31, + DecreaseCritAttackOnEnd = 32, + IncreaseCritAttackOnDefenseEnd = 41, + DecreaseCritAttackOnDefenseEnd = 42 + } + + public enum Critical : byte + { + InflictingIncreased = 11, + InflictingReduced = 12, + DamageIncreased = 21, + DamageIncreasedInflictingReduced = 22, + DamageIncreasingChance = 31, + DamageReducingChance = 32, + ReceivingIncreased = 41, + ReceivingDecreased = 42, + DamageFromCriticalIncreased = 51, + DamageFromCriticalDecreased = 52 + } + + public enum Damage : byte + { + DamageIncreased = 11, + DamageDecreased = 12, + MeleeIncreased = 21, + MeleeDecreased = 22, + RangedIncreased = 31, + RangedDecreased = 32, + MagicalIncreased = 41, + MagicalDecreased = 42 + } + + public enum DamageConvertingSkill : byte + { + TransferInflictedDamage = 11, + TransferInflictedDamageNegated = 12, + IncreaseDamageTransfered = 21, + DecreaseDamageTransfered = 22, + HPRecoveryIncreased = 31, + HPRecoveryDecreased = 32, + AdditionalDamageCombo = 41, + AdditionalDamageComboNegated = 42, + ReflectMaximumReceivedDamage = 51, + ReflectMaximumReceivedDamageNegated = 52 + } + + public enum DarkCloneSummon : byte + { + SummonDarkCloneChance = 11, + SummonDarkCloneChanceNegated = 12, + ConvertRecoveryToDamage = 21, + ConvertRecoveryToDamageNegated = 22, + ConvertDamageToHPChance = 31, + ConvertDamageToHPChanceNegated = 32, + IncreaseEnemyCooldownChance = 41, + IncreaseEnemyCooldownChanceNegated = 42, + DarkElementDamageIncreaseChance = 51, + DarkElementDamageDecreaseChance = 52 + } + + public enum DealDamageAround : byte + { + DamageDeflect = 11, + DamageDeflectNegated = 12, + DealAreaDamagePerSecond = 21, + DealAreaDamagePerSecondNegated = 22, + SummonOnDefend = 31, + SummonOnDefendDouble = 32, + NosmateAttackIncrease = 41, + NosmateAttackIncreaseNegated = 42, + EffectiveOnEnemyInAreaPerSecond = 51, + EffectiveOnEnemyInAreaPerSecondNegated = 52 + } + + public enum DebuffResistance : byte + { + IncreaseBadEffectChance = 11, + NeverBadEffectChance = 12, + IncreaseBadGeneralEffectChance = 21, + NeverBadGeneralEffectChance = 22, + IncreaseBadMagicEffectChance = 31, + NeverBadMagicEffectChance = 32, + IncreaseBadToxicEffectChance = 41, + NeverBadToxicEffectChance = 42, + IncreaseBadDiseaseEffectChance = 51, + NeverBadDiseaseEffectChance = 52 + } + + public enum Defence : byte + { + AllIncreased = 11, + AllDecreased = 12, + MeleeIncreased = 21, + MeleeDecreased = 22, + RangedIncreased = 31, + RangedDecreased = 32, + MagicalIncreased = 41, + MagicalDecreased = 42, + DefenceLevelIncreased = 51, + DefenceLevelDecreased = 52 + } + + public enum DodgeAndDefencePercent : byte + { + DodgeIncreased = 11, + DodgeDecreased = 12, + DodgingMeleeIncreased = 21, + DodgingMeleeDecreased = 22, + DodgingRangedIncreased = 31, + DodgingRangedDecreased = 32, + DefenceIncreased = 41, + DefenceReduced = 42 + } + + public enum DragonSkills : byte + { + ChangeIntoDragon = 11, + ChangeIntoHaetae = 12, + CooldownResetChance = 21, + CannotUseBuffChance = 31, + CannotUseBuffChanceNegated = 32, + ReceivedExpAndJobIncrease = 41, + ReceivedExpAndJobIncreaseNegated = 42, + MagicArrowChance = 51, + MagicArrowChanceNegated = 52 + } + + public enum Drain : byte + { + CastDrain = 11, + CastDrainNegated = 12, + TransferEnemyHP = 21, + TransferEnemyHPNegated = 22 + } + + // 21-40 + public enum DrainAndSteal : byte + { + ReceiveHpFromMP = 11, + ReceiveHpFromMPNegated = 12, + ReceiveMpFromHP = 21, + ReceiveMpFromHPNegated = 22, + GiveEnemyHP = 31, + LeechEnemyHP = 32, + GiveEnemyMP = 41, + LeechEnemyMP = 42, + ConvertEnemyMPToHP = 51, + ConvertEnemyHPToMP = 52 + } + + public enum DropItemTwice : byte + { + DoubleDropChance = 11, + DoubleDropChanceNegated = 12, + EffectOnEnemyWhileAttackingChance = 21, + EffectOnEnemyWhileAttackingChanceNegated = 22, + EffectOnSelfWhileAttackingChance = 31, + EffectOnSelfWhileAttackingChanceNegated = 32, + EffectOnEnemyWhileDefendingChance = 41, + EffectOnEnemyWhileDefendingChanceNegated = 42, + EffectOnSelfWhileDefendingChance = 51, + EffectOnSelfWhileDefendingChanceNegated = 52 + } + + public enum EffectSummon : byte + { + CooldownResetChance = 11, + CooldownResetChanceNegated = 12, + TeamEffectAppliedChance = 21, + TeamEffectDeletedChance = 22, + IfMobHigherLevelDamageIncrease = 31, + IfMobHigherLevelDamageDecrease = 32, + BlockNegativeEffect = 41, + BlockNegativeEffectNegated = 42, + ChanceToGive = 51, + ChanceToDelete = 52 + } + + public enum Element : byte + { + FireIncreased = 11, + FireDecreased = 12, + WaterIncreased = 21, + WaterDecreased = 22, + LightIncreased = 31, + LightDecreased = 32, + DarkIncreased = 41, + DarkDecreased = 42, + AllIncreased = 51, + AllDecreased = 52 + } + + public enum ElementResistance : byte + { + AllIncreased = 11, + AllDecreased = 12, + FireIncreased = 21, + FireDecreased = 22, + WaterIncreased = 31, + WaterDecreased = 32, + LightIncreased = 41, + LightDecreased = 42, + DarkIncreased = 51, + DarkDecreased = 52 + } + + public enum EnemyElementResistance : byte + { + AllIncreased = 11, + AllDecreased = 12, + FireIncreased = 21, + FireDecreased = 22, + WaterIncreased = 31, + WaterDecreased = 32, + LightIncreased = 41, + LightDecreased = 42, + DarkIncreased = 51, + DarkDecreased = 52 + } + + public enum FairyXPIncrease : byte + { + TeleportToLocation = 11, + TeleportToLocationNegated = 12, + IncreaseFairyXPPoints = 21, + IncreaseFairyXPPointsNegated = 22 + } + + public enum FalconSkill : byte + { + CausingChanceLocation = 11, + RemovingChanceLocation = 12, + Hide = 21, + HideNegated = 22, + Ambush = 31, + AmbushNegated = 32, + FalconFollowing = 41, + FalconFollowingNegated = 42, + FalconFocusLowestHP = 51, + FalconFocusLowestHPNegated = 52 + } + + public enum FearSkill : byte + { + RestoreRemainingEnemyHP = 11, + DecreaseRemainingEnemyHP = 12, + TimesUsed = 21, + TimesUsedNegated = 22, + AttackRangedIncreased = 31, + AttackRangedDecreased = 32, + MoveAgainstWill = 41, + MoveAgainstWillNegated = 42, + ProduceWhenAmbushe = 51, + ProduceWhenAmbushNegated = 52 + } + + public enum FireCannoneerRangeBuff : byte + { + AOEIncreased = 11, + AOEDecreased = 12, + Flinch = 21, + FlinchNegated = 22 + } + + public enum FocusEnemyAttentionSkill : byte + { + FocusEnemyAttention = 11 + + // Unknown = 12, Unknown2 = 21, Unknown3 = 22, + } + + public enum FourthGlacernonFamilyRaid : byte + { + AllInFieldReceiveDamage = 11, // Look nearly the same as 12 + AllInFieldsReceiveDamage = 12 // Look nearly the same as 11 + } + + public enum FrozenDebuff : byte + { + MovementLocked = 11, + MovementLockedNegated = 12 + + // Unknown = 21, Unknown2 = 22 + } + + public enum GuarantedDodgeRangedAttack : byte + { + AttackHitChance = 11, + AttackHitChanceNegated = 12, + AlwaysDodgeProbability = 21, + AlwaysDodgeProbabilityNegated = 22, + NoPenalty = 31, + NoPenaltyNegated = 32, + DistanceDamageIncreasing = 41, + DistanceDamageIncreasingNegated = 42 + } + + public enum HealingBurningAndCasting : byte + { + RestoreHP = 11, + DecreaseHP = 12, + RestoreMP = 21, + DecreaseMP = 22, + RestoreHPWhenCasting = 31, + DecreaseHPWhenCasting = 32, + RestoreHPWhenCastingInterrupted = 41, + DecreaseHPWhenCastingInterrupted = 42, + HPIncreasedByConsumingMP = 51, + HPDecreasedByConsumingMP = 52 + } + + public enum HideBarrelSkill : byte + { + NoHPConsumption = 11, + NoHPRecovery = 12 + } + + public enum HPMP : byte + { + RestoreDecreasedHP = 11, + DecreaseRemainingHP = 12, + RestoreDecreasedMP = 21, + DecreaseRemainingMP = 22, + HPRestored = 31, + HPReduced = 32, + MPRestored = 41, + MPReduced = 42, + ReceiveAdditionalHP = 51, + ReceiveAdditionalMP = 52 + } + + public enum HugeSnowman : byte + { + SnowStorm = 11, + SnowStormNegated = 12, + EarthQuake = 21, + EarthQuakeNegated = 22 + } + + public enum IncreaseAllDamage : byte + { + AllAttackIncrease = 11, + AllAttackDecrease = 12, + MeleeAttackIncrease = 21, + MeleeAttackDecrease = 22, + RangeAttackIncrease = 31, + RangeAttackDecrease = 32, + MagicAttackIncrease = 41, + MagicAttackDecrease = 42, + ConcentrationIncrease = 51, + ConcentrationDecrease = 52 + } + + public enum IncreaseDamage : byte + { + IncreasingProbability = 11, + DecreasingProbability = 12, + FireIncreased = 21, + FireDecreased = 22, + WaterIncreased = 31, + WaterDecreased = 32, + LightIncreased = 41, + LightDecreased = 42, + DarkIncreased = 51, + DarkDecreased = 52 + } + + public enum IncreaseDamageDebuffs : byte + { + RuneHpEffect = 11, + RuneHpEffectNegated = 12, + MASP4AttackSuccussedPoints = 21, + MASP4DefenseSuccussedPoints = 22, + MASP4CanUseAbility = 31, + MASP4CanUseAbilityNegated = 32, + EffectOnShadowAttack = 41, + EffectOnShadowAttackNegated = 42, + AttackAndDefIncreaseOnDebuff = 51, + AttackAndDefIncreasOnBuff = 52 + } + + public enum IncreaseDamageInLoD : byte + { + LodMonstersAttackIncrease = 11, + LodMonstersAttackDecrease = 12, + VesselMonstersAttackIncrease = 21, + VesselMonstersAttackDecrease = 22, + ExpGainIncrease = 31, + ExpGainDecrease = 32, + JobGainIncrease = 41, + JobGainDecrease = 42, + DodgeIncrease = 51, + DodgeDecrease = 52 + } + + public enum IncreaseDamageVersus : byte + { + FireDamageIncreaseChance = 11, + FireDamageDecreaseChance = 12, + VesselAndLodMobDamageIncrease = 21, + VesselAndLodMobDamageDecrease = 22, + PvpDamageAndSpeedRainbowBattleIncrease = 31, + PvpDamageAndSpeedRainbowBattleDecrease = 32, + VesselAndFrozenCrownMobDamageIncrease = 41, + VesselAndFrozenCrownMobDamageDecrease = 42 + } + + public enum IncreaseDamageVersusMonsters : byte + { + LowLevelPlantAttackIncrease = 11, + LowLevelPlantAttackDecrease = 12, + LowLevelAnimalAttackIncrease = 21, + LowLevelAnimalAttackDecrease = 22, + LowLevelMonsterAttackIncrease = 31, + LowLevelMonsterAttackDecrease = 32, + KovoltAttackIncrease = 41, + KovoltAttackDecrease = 42, + CatsyAttackIncrease = 51, + CatsyAttackDecrease = 52 + } + + public enum IncreaseDamageVersusMonsters_2 : byte + { + LowLevelSpiritAttackIncrease = 11, + LowLevelSpiritAttackDecrease = 12, + AngelAttackIncrease = 21, + AngelAttackDecrease = 22, + DemonAttackIncrease = 31, + DemonAttackDecrease = 32, + LowLevelUndeadAttackIncrease = 41, + LowLevelUndeadAttackDecrease = 42, + ProductionPointsUseDecrease = 51, + ProductionPointsUseDecreaseNegated = 52 + } + + public enum IncreaseElementByResis : byte + { + AllElementResisIncrease = 11, + AllElementResisDecrease = 12, + FireElementResisIncrease = 21, + FireElementResisDecrease = 22, + WaterElementResisIncrease = 31, + WaterElementResisDecrease = 32, + LightElementResisIncrease = 41, + LightElementResisDecrease = 42, + ShadowElementResisIncrease = 51, + ShadowElementResisDecrease = 52 + } + + public enum IncreaseElementDamage : byte + { + AllElementAttackIncrease = 11, + AllElementAttackDecrease = 12, + FireElementAttackIncrease = 21, + FireElementAttackDecrease = 22, + WaterElementAttackIncrease = 31, + WaterElementAttackDecrease = 32, + LightElementAttackIncrease = 41, + LightElementAttackDecrease = 42, + ShadowElementAttackIncrease = 51, + ShadowElementAttackDecrease = 52 + } + + public enum IncreaseElementFairy : byte + { + FairyElementIncrease = 11, + FairyElementDecrease = 12, + FairyElementIncreaseWhileAttackingChance = 21, + FairyElementDecreaseWhileAttackingChance = 22, + DamageToMonstersIncrease = 31, + DamageToMonstersDecrease = 32 + } + + public enum IncreaseElementProcent : byte + { + FireElementIncrease = 11, + FireElementDecrease = 12, + WaterElementIncrease = 21, + WaterElementDecrease = 22, + LightElementIncrease = 31, + LightElementDecrease = 32, + ShadowElementIncrease = 41, + ShadowElementDecrease = 42, + AllElementIncrease = 51, + AllElementDecrease = 52 + } + + public enum IncreaseSpPoints : byte + { + SpCardAttackPointIncrease = 11, + SpCardAttackPointDecrease = 12, + SpCardDefensePointIncrease = 21, + SpCardDefensePointDecrease = 22, + SpCardElementPointIncrease = 31, + SpCardElementPointDecrease = 32, + SpCardHpMpPointIncrease = 41, + SpCardHpMpPointDecrease = 42, + AccuracyIncrease = 51, + AccuracyDecrease = 52 + } + + public enum InflictSkill : byte + { + InflictDamageAtLocation = 11, + InflictDamageAtLocationNegated = 12 + } + + public enum Item : byte + { + EXPIncreased = 11, + EXPIncreasedNegated = 12, + AttackIncreased = 21, + DefenceIncreased = 22, + DropItemsWhenAttacked = 31, + DropItemsWhenAttackedNegated = 32, + ScrollPower = 41, + ScrollPowerNegated = 42, + IncreaseEarnedGold = 51, + IncreaseEarnedGoldNegated = 52 + } + + public enum JumpBackPush : byte + { + JumpBackChance = 11, + PushBackChance = 21, + PushBackChanceNegated = 22, + MeleeDurationIncreased = 31, + MeleeDurationDecreased = 32, + RangedDurationIncreased = 41, + RangedDurationDecreased = 42, + MagicalDurationIncreased = 51, + MagicalDurationDecreased = 52 + } + + public enum LeonaPassiveSkill : byte + { + IncreaseDamageAgainst = 11, + DecreaseDamageAgainst = 12, + IncreaseRecoveryItems = 21, + DecreaseRecoveryItems = 22, + OnSPWearCausing = 31, + OnSPWearRemoving = 32, + DefenceIncreasedInPVP = 41, + DefenceDecreasedInPVP = 42, + AttackIncreasedInPVP = 51, + AttackDecreasedInPVP = 52 + } + + public enum LightAndShadow : byte + { + InflictDamageToMP = 11, + IncreaseMPByAbsorbedDamage = 12, + RemoveBadEffects = 21, + RemoveGoodEffects = 22, + InflictDamageOnUndead = 31, + HealUndead = 32, + AdditionalDamageWhenHidden = 41, + AdditionalDamageOnHiddenEnemy = 42 + } + + public enum LordBerios : byte + { + CauseDamage = 11, + CauseDamageNegated = 12 + } + + public enum LordCalvinas : byte + { + InflictDamageAtLocation = 11, + InflictDamageAtLocationNegated = 12 + } + + public enum LordHatus : byte + { + InflictDamageAtLocation = 11, + InflictDamageAtLocationNegated = 12 + } + + public enum LordMorcos : byte + { + InflictDamageAfter = 11, + InflictDamageAfterNegated = 12 + } + + public enum MagicShield : byte + { + MagicShieldDefend = 11, + MagicShieldDefendNegated = 12, + IncreaseElementResisOnDamageTaken = 21, + DecreaseElementResisOnDamageTaken = 22, + MaxAdditionalHpIncrease = 31, + MaxAdditionalHpDecrease = 32, + IgnoreBlock = 41, + IgnoreBlockNegated = 42, + DodgeIncrease = 51, + AccuracyIncrease = 52 + } + + public enum MaxHPMP : byte + { + MaximumHPIncreased = 11, + MaximumHPDecreased = 12, + MaximumMPIncreased = 21, + MaximumMPDecreased = 22, + IncreasesMaximumHP = 31, + DecreasesMaximumHP = 32, + IncreasesMaximumMP = 41, + DecreasesMaximumMP = 42, + MaximumHPMPIncreased = 51, + MaximumHPMPDecreased = 52 + } + + public enum MeditationSkill : byte + { + CausingChance = 11, + RemovingChance = 12, + ShortMeditation = 21, + ShortMeditationNegated = 22, + RegularMeditation = 31, + RegularMeditationNegated = 32, + LongMeditation = 41, + LongMeditationNegated = 42, + Sacrifice = 51, + SacrificeNegated = 52 + } + + public enum MeteoriteTeleport : byte + { + SummonInVisualRange = 11, + SummonInVisualRangeNegated = 12, + TransformTarget = 21, + TransformTargetNegated = 22, + TeleportForward = 31, + TeleportForwardNegated = 32, + CauseMeteoriteFall = 41, + CauseMeteoriteFallNegated = 42, + TeleportYouAndGroupToSavedLocation = 51, + TeleportYouAndGroupToSavedLocationNegated = 52 + } + + // 41-60 + public enum Mode : byte + { + Range = 11, + ReturnRange = 12, + EffectNoDamage = 21, + DirectDamage = 22, + AttackTimeIncreased = 31, + AttackTimeDecreased = 32, + ModeChance = 41, + ModeChanceNegated = 42, + OccuringChance = 51, + OccuringChanceNegated = 52 + } + + public enum Morale : byte + { + MoraleIncreased = 11, + MoraleDecreased = 12, + MoraleDoubled = 21, + MoraleHalved = 22, + LockMorale = 31, + LockMoraleNegated = 32, + SkillCooldownIncreased = 41, + SkillCooldownDecreased = 42, + IgnoreEnemyMorale = 51, + IgnoreEnemyMoraleNegated = 52 + } + + public enum Move : byte + { + MovementImpossible = 11, + MovementImpossibleNegated = 12, + MoveSpeedIncreasedPercentage = 21, + MoveSpeedDecreasedPercentage = 22, + InvisibleMovement = 31, + InvisibleMovementNegated = 32, + MovementSpeedIncreased = 41, + MovementSpeedDecreased = 42, + TempMaximized = 51, + TempMaximizedNegated = 52 + } + + public enum MultAttack : byte + { + AllAttackIncreased = 11, + AllAttackDecreased = 12, + MeleeAttackIncreased = 21, + MeleeAttackDecreased = 22, + RangedAttackIncreased = 31, + RangedAttackDecreased = 32, + MagicalAttackIncreased = 41, + MagicalAttackDecreased = 42 + } + + public enum MultDefence : byte + { + AllDefenceIncreased = 11, + AllDefenceDecreased = 12, + MeleeDefenceIncreased = 21, + MeleeDefenceDecreased = 22, + RangedDefenceIncreased = 31, + RangedDefenceDecreased = 32, + MagicalDefenceIncreased = 41, + MagicalDefenceDecreased = 42 + } + + public enum NoCharacteristicValue : byte + { + AllPowersNullified = 11, + AllResistancesNullified = 12, + FireElementNullified = 21, + FireResistanceNullified = 22, + WaterElementNullified = 31, + WaterResistanceNullified = 32, + LightElementNullified = 41, + LightResistanceNullified = 42, + DarkElementNullified = 51, + DarkResistanceNullified = 52 + } + + public enum NoDefeatAndNoDamage : byte + { + DecreaseHPNoDeath = 11, + DecreaseHPNoKill = 12, + NeverReceiveDamage = 21, + NeverCauseDamage = 22, + TransferAttackPower = 31, + TransferAttackPowerNegated = 32 + } + + public enum Quest : byte + { + SummonMonsterBased = 11, + SummonMonsterBasedNegated = 12 + } + + public enum Recovery : byte + { + HPRecoveryIncreased = 11, + HPRecoveryDecreased = 12, + MPRecoveryIncreased = 21, + MPRecoveryDecreased = 22 + } + + public enum RecoveryAndDamagePercent : byte + { + HPRecovered = 11, + HPReduced = 12, + MPRecovered = 21, + MPReduced = 22, + DecreaseEnemyHP = 31, + DecreaseSelfHP = 32 + } + + public enum ReflectDamage : byte + { + DeflectDamageOnCrit = 11, + DeflectDamageOnCritNegated = 12, + DamageDodge = 21, + DamageDodgeNegated = 22, + CritAttackIncrease = 31, + CritAttackIncreaseNegated = 32, + TakeMpOnDamage = 41, + TakeMpOnDamageNegated = 42, + AllAttackIncreasePerMagicDefense = 51, + AllAttackIncreasePerMagicDefenseNegated = 52 + } + + public enum Reflection : byte + { + HPIncreased = 11, + HPDecreased = 12, + MPIncreased = 21, + MPDecreased = 22, + EnemyHPIncreased = 31, + EnemyHPDecreased = 32, + EnemyMPIncreased = 41, + EnemyMPDecreased = 42, + ChanceMpLost = 52 + } + + public enum ReputHeroLevel : byte + { + ReputIncreased = 11, + ReputDecreased = 12, + ReceivedHeroExpIncrease = 21, + ReceivedHeroExpDecrease = 22, + IfInTeamGetItemPerHours = 31, + IfInTeamGetItemPerHoursNegated = 32, + EnemyEffectDeleteChance = 41, + EnemyEffectDeleteChanceNegated = 42, + CreateBuffDragonVitality = 51, + CreateBuffStrongDragonVitality = 52 + } + + public enum Runes_1 : byte + { + ApocalypsePowerOnAttack = 11, + ApocalypsePowerOnAttackNegated = 12, + ReflectionPowerOnAttack = 21, + ReflectionPowerOnAttackNegated = 22, + WolfPowerOnAttack = 31, + WolfPowerOnAttackNegated = 32, + EnemyPushOnAttack = 41, + EnemyPushOnDefend = 42, + ExplosionPowerOnMeleeAttack = 51, + ExplosionPowerOnMeleeAttackNegated = 52 + } + + public enum Runes_2 : byte + { + AgilityPowerOnAttack = 11, + AgilityPowerOnAttackNegated = 12, + LightningPowerOnAttack = 21, + LightningPowerOnAttackNegated = 22, + CursePowerOnAttack = 31, + CursePowerOnAttackNegated = 32, + BearPowerOnAttack = 41, + BearPowerOnAttackNegated = 42, + FrostPowerOnAttack = 51, + FrostPowerOnAttackNegated = 52 + } + + public enum SecondSPCard : byte + { + PlantBomb = 11, + SetBombWhenAttack = 12, // Same as 22! + PlantSelfDestructionBomb = 21, + PlantBombWhenAttack = 22, // Same as 12! + ReduceEnemySkill = 31, + ReduceEnemySkillNegated = 32, + HitAttacker = 41, + HitAttackerNegated = 42 + } + + public enum SESpecialist : byte + { + EnterNumberOfBuffsAndDamage = 11, + EnterNumberOfBuffs = 12, + MovingAura = 31, + DontNeedToEnter = 32, + LowerHPStrongerEffect = 41, + DoNotNeedToEnter = 42 + } + + public enum SniperAttack : byte + { + ChanceCausing = 11, + ChanceRemoving = 12, + AmbushRangeIncreased = 21, + AmbushRangeIncreasedNegated = 22, + ProduceChance = 31, + ProduceChanceNegated = 32, + KillerHPReducing = 41, + KillerHPIncreasing = 42, + ReceiveCriticalFromSniper = 51, + ReceiveCriticalFromSniperNegated = 52 + } + + public enum SP2MA_1 : byte + { + DodgeAndMakeChance = 11, + DodgeAndMakeChanceNegated = 12, + EnhancementWhenEnlightened = 21, + FullMoonSkillUse = 31, + LotusFlowerSkillUse = 41, + SignUseNextAttackIncrease = 51, + SignUseNextDamageTakenDecrease = 52 + } + + public enum SP2MA_2 : byte + { + AchieveWhenAttacked = 11, + AchieveWhenAttackedNegated = 12, + AdditionalAttackChance = 21, + AdditionalAttackChanceNegated = 22, + SignUseMarkedEnemyAttackIncrease = 31, + SignUseMarkedEnemyAttackIncreaseNegated = 32, + CreateFullMoonBoundWhenEnemyBoundByMoonlight = 41, + CreateFullMoonBoundWhenEnemyBoundByMoonlightNegated = 42 + } + + public enum SPCardUpgrade : byte + { + LowerSPScroll = 11, + LowerSPScrollNegated = 12, + HigherSPScroll = 21, + HigherSPScrollNegated = 22 + } + + public enum SpecialActions : byte + { + PushBack = 11, + PushBackNegated = 12, + FocusEnemies = 21, + FocusEnemiesNegated = 22, + Charge = 31, + ChargeNegated = 32, + RunAway = 41, + RunAwayNegated = 42, + Hide = 51, + SeeHiddenThings = 52 + } + + // 1-20 + public enum SpecialAttack : byte + { + NoAttack = 11, + NoAttackNegated = 12, + MeleeDisabled = 21, + MeleeDisabledNegated = 22, + RangedDisabled = 31, + RangedDisabledNegated = 32, + MagicDisabled = 41, + MagicDisabledNegated = 42, + FailIfMiss = 51, + FailIfMissNegated = 52 + } + + public enum SpecialBehaviour : byte + { + TeleportRandom = 11, + TeleportRandomNegated = 12, + JumpToEveryObject = 21, + JumpToEveryObjectNegated = 22, + InflictOnTeam = 31, + InflictOnEnemies = 32, + TransformInto = 41, + TransformIntoNegated = 42 + } + + public enum SpecialCritical : byte + { + AlwaysInflict = 11, + AlwaysInflictNegated = 12, + NeverInflict = 21, + NeverInflictNegated = 22, + AlwaysReceives = 31, + AlwaysReceivesNegated = 32, + NeverReceives = 41, + NeverReceivesNegated = 42, + InflictingChancePercent = 51, + ReceivingChancePercent = 52 + } + + public enum SpecialDamageAndExplosions : byte + { + ChanceExplosion = 11, + ChanceExplosionNegated = 12, + ExplosionCauses = 21, + ExplosionCausesNegated = 22, + SurroundingDamage = 31, + SurroundingDamageNegated = 32 + } + + public enum SpecialDefence : byte + { + AllDefenceNullified = 11, + AllDefenceNullifiedNegated = 12, + MeleeDefenceNullified = 21, + MeleeDefenceNullifiedNegated = 22, + RangedDefenceNullified = 31, + RangedDefenceNullifiedNegated = 32, + MagicDefenceNullified = 41, + MagicDefenceNullifiedNegated = 42, + NoDefence = 51, + NoDefenceNegated = 52 + } + + public enum SpecialEffects : byte + { + DecreaseKillerHP = 11, + IncreaseKillerHP = 12, + ToPrefferedAttack = 21, + ToNonPrefferedAttack = 22, + Gibberish = 31, + GibberishNegated = 32, + AbleToFightPVP = 41, + AbleToFightPVPNegated = 42, + ShadowAppears = 51, + ShadowAppearsNegated = 52 + } + + public enum SpecialEffects2 : byte + { + FocusEnemy = 11, + RemoveEnemyAttention = 12, + TeleportInRadius = 21, + TeleportInRadiusNegated = 22, + MainWeaponCausingChance = 31, + MainWeaponCausingChanceNegated = 32, + SecondaryWeaponCausingChance = 41, + SecondaryWeaponCausingChanceNegated = 42, + BefriendMonsters = 51, + BefriendMonstersNegated = 52 + } + + public enum SpecialisationBuffResistance : byte + { + IncreaseDamageAgainst = 11, + ReduceDamageAgainst = 12, + IncreaseCriticalAgainst = 21, + ReduceCriticalAgainst = 22, + ResistanceToEffect = 31, + ResistanceToEffectNegated = 32, + IncreaseDamageInPVP = 41, + DecreaseDamageInPVP = 42, + RemoveGoodEffects = 51, + RemoveBadEffects = 52 + } + + public enum StealBuff : byte + { + IgnoreDefenceChance = 11, + IgnoreDefenceChanceNegated = 12, + ReduceCriticalReceivedChance = 21, + ReduceCriticalReceivedChanceNegated = 22, + ChanceSummonOnyxDragon = 31, + ChanceSummonOnyxDragonNegated = 32, + StealGoodEffect = 41, + StealGoodEffectNegated = 42 + } + + public enum SummonAndRecoverHP : byte + { + ChanceSummon = 11, + ChanceSummonNegated = 12, + RestoreHP = 21, + ReduceHP = 22 + } + + public enum Summons : byte + { + SummonUponDeath = 11, + SummonUponDeathChance = 12, + Summons = 21, + SummonningChance = 22, + SummonTrainingDummy = 31, + SummonTrainingDummyChance = 32, + SummonTimedMonsters = 41, + SummonTimedMonstersChance = 42, + SummonGhostMP = 51, + SummonGhostMPChance = 52 + } + + public enum SummonSkill : byte + { + Summon = 31, + SummonTimed = 32 + } + + public enum Target : byte + { + AllHitRateIncreased = 11, + AllHitRateDecreased = 12, + MeleeHitRateIncreased = 21, + MeleeHitRateDecreased = 22, + RangedHitRateIncreased = 31, + RangedHitRateDecreased = 32, + MagicalConcentrationIncreased = 41, + MagicalConcentrationDecreased = 42 + } + + public enum TauntSkill : byte + { + ReflectsMaximumDamageFrom = 11, + ReflectsMaximumDamageFromNegated = 12, + DamageInflictedIncreased = 21, + DamageInflictedDecreased = 22, + EffectOnKill = 31, + EffectOnKillNegated = 32, + TauntWhenKnockdown = 41, + TauntWhenNormal = 42, + ReflectBadEffect = 51, + ReflectBadEffectNegated = 52 + } + + public enum TeamArenaBuff : byte + { + DamageTakenIncreased = 11, + DamageTakenDecreased = 12, + AttackPowerIncreased = 21, + AttackPowerDecreased = 22 + } + + public enum TimeCircleSkills : byte + { + GatherEnergy = 11, + GatherEnergyNegated = 12, + DisableHPConsumption = 21, + DisableHPRecovery = 22, + DisableMPConsumption = 31, + DisableMPRecovery = 32, + CancelAllBuff = 41, + CancelAllBuffNegated = 42, + ItemCannotBeUsed = 51, + ItemCannotBeUsedNegated = 52 + } + + public enum VulcanoElementBuff : byte + { + SkillsIncreased = 11, + SkillsDecreased = 12, + ReducesEnemyAttack = 21, + ReducesEnemyAttackNegated = 22, + PullBackBuffIncreasing = 31, + PullBackBuffIncreasingNegated = 32, + CriticalDefence = 41, + CriticalDefenceNegated = 42 + } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/ArenaTeamType.cs b/srcs/WingsAPI.Packets/Enums/ArenaTeamType.cs new file mode 100644 index 0000000..08954c7 --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/ArenaTeamType.cs @@ -0,0 +1,12 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.Enums +{ + public enum ArenaTeamType : byte + { + ZENAS = 1, + ERENIA = 2 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/BCardScalingType.cs b/srcs/WingsAPI.Packets/Enums/BCardScalingType.cs new file mode 100644 index 0000000..9c3d386 --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/BCardScalingType.cs @@ -0,0 +1,9 @@ +namespace WingsEmu.Game._enum +{ + public enum BCardScalingType + { + NORMAL_VALUE = 0, + LEVEL_MULTIPLIED = 1, + LEVEL_DIVIDED = 2 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/BankActionType.cs b/srcs/WingsAPI.Packets/Enums/BankActionType.cs new file mode 100644 index 0000000..27222e5 --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/BankActionType.cs @@ -0,0 +1,12 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.Enums +{ + public enum BankActionType : byte + { + Deposit = 1, + Withdraw = 2 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/Battle/AttackType.cs b/srcs/WingsAPI.Packets/Enums/Battle/AttackType.cs new file mode 100644 index 0000000..d35c3c4 --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/Battle/AttackType.cs @@ -0,0 +1,16 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.Enums.Battle +{ + public enum AttackType : byte + { + Melee = 0, + Ranged = 1, + Magical = 2, + Other = 3, + Charge = 4, + Dash = 5 // all skills that changes player position + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/Battle/CancelType.cs b/srcs/WingsAPI.Packets/Enums/Battle/CancelType.cs new file mode 100644 index 0000000..5c6ae4b --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/Battle/CancelType.cs @@ -0,0 +1,12 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.Enums.Battle +{ + public enum CancelType : byte + { + NotInCombatMode = 0, + InCombatMode = 2 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/Battle/TargetHitType.cs b/srcs/WingsAPI.Packets/Enums/Battle/TargetHitType.cs new file mode 100644 index 0000000..9b0130d --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/Battle/TargetHitType.cs @@ -0,0 +1,16 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.Enums.Battle +{ + public enum TargetHitType : byte + { + TargetOnly = 0, + EnemiesInAffectedAoE = 1, + AlliesInAffectedAoE = 2, + SpecialArea = 3, + PlayerAndHisMates = 4, + SpecialZoneHit = 5 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/Bazaar/BazaarListedItemType.cs b/srcs/WingsAPI.Packets/Enums/Bazaar/BazaarListedItemType.cs new file mode 100644 index 0000000..0317094 --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/Bazaar/BazaarListedItemType.cs @@ -0,0 +1,14 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsAPI.Packets.Enums.Bazaar +{ + public enum BazaarListedItemType : byte + { + All = 0, + ForSale = 1, + Sold = 2, + DeadlineExpired = 3 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/Bazaar/Filter/BazaarCategoryFilterType.cs b/srcs/WingsAPI.Packets/Enums/Bazaar/Filter/BazaarCategoryFilterType.cs new file mode 100644 index 0000000..87cd715 --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/Bazaar/Filter/BazaarCategoryFilterType.cs @@ -0,0 +1,23 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsAPI.Packets.Enums.Bazaar.Filter +{ + public enum BazaarCategoryFilterType : byte + { + All = 0, + Weapon = 1, + Armour = 2, + Equipment = 3, + Accessories = 4, + Specialist = 5, + Pet = 6, + Partner = 7, + Shell = 8, + MainItem = 9, + ConsumerItem = 10, + Miscellaneous = 11, + StoreMount = 12 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/Bazaar/Filter/BazaarLevelFilterType.cs b/srcs/WingsAPI.Packets/Enums/Bazaar/Filter/BazaarLevelFilterType.cs new file mode 100644 index 0000000..ee927f9 --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/Bazaar/Filter/BazaarLevelFilterType.cs @@ -0,0 +1,24 @@ +namespace WingsAPI.Packets.Enums.Bazaar.Filter +{ + public enum BazaarLevelFilterType : byte + { + All = 0, + OneToTen = 1, + ElevenToTwenty = 2, + TwentyOneToThirty = 3, + ThirtyOneToForty = 4, + FortyOneToFifty = 5, + FiftyOneToSixty = 6, + SixtyOneToSeventy = 7, + SeventyOneToEighty = 8, + EightyOneToNinty = 9, + NintyOneToNintyNine = 10, + ChampionGear = 11, + ChampionLevelOneToTen = 12, + ChampionLevelElevenToTwenty = 13, + ChampionLevelTwentyOneToThirty = 14, + ChampionLevelThirtyOneToForty = 15, + ChampionLevelFortyOneToFifty = 16, + ChampionLevelFiftyOneToSixty = 17 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/Bazaar/Filter/BazaarPerfectionFilterType.cs b/srcs/WingsAPI.Packets/Enums/Bazaar/Filter/BazaarPerfectionFilterType.cs new file mode 100644 index 0000000..34c88bd --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/Bazaar/Filter/BazaarPerfectionFilterType.cs @@ -0,0 +1,17 @@ +namespace WingsAPI.Packets.Enums.Bazaar.Filter +{ + public enum BazaarPerfectionFilterType + { + All = 0, + OneToTen = 1, + ElevenToTwenty = 2, + TwentyOneToThirty = 3, + ThirtyOneToForty = 4, + FortyOneToFifty = 5, + FiftyOneToSixty = 6, + SixtyOneToSeventy = 7, + SeventyOneToEighty = 8, + EightyOneToNinty = 9, + NintyOneToHundred = 10 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/Bazaar/Filter/BazaarRarityFilterType.cs b/srcs/WingsAPI.Packets/Enums/Bazaar/Filter/BazaarRarityFilterType.cs new file mode 100644 index 0000000..e7873cd --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/Bazaar/Filter/BazaarRarityFilterType.cs @@ -0,0 +1,16 @@ +namespace WingsAPI.Packets.Enums.Bazaar.Filter +{ + public enum BazaarRarityFilterType : byte + { + All = 0, + Normal = 1, + Useful = 2, + Good = 3, + HighQuality = 4, + Excellent = 5, + Ancient = 6, + Mysterious = 7, + Legendary = 8, + Phenomenal = 9 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/Bazaar/Filter/BazaarSortFilterType.cs b/srcs/WingsAPI.Packets/Enums/Bazaar/Filter/BazaarSortFilterType.cs new file mode 100644 index 0000000..64808c7 --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/Bazaar/Filter/BazaarSortFilterType.cs @@ -0,0 +1,10 @@ +namespace WingsAPI.Packets.Enums.Bazaar.Filter +{ + public enum BazaarSortFilterType : byte + { + PriceAscending = 0, + PriceDescending = 1, + AmountAscending = 2, + AmountDescending = 3 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/Bazaar/Filter/BazaarUpgradeFilterType.cs b/srcs/WingsAPI.Packets/Enums/Bazaar/Filter/BazaarUpgradeFilterType.cs new file mode 100644 index 0000000..4453bdc --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/Bazaar/Filter/BazaarUpgradeFilterType.cs @@ -0,0 +1,23 @@ +namespace WingsAPI.Packets.Enums.Bazaar.Filter +{ + public enum BazaarUpgradeFilterType : byte + { + All = 0, + Zero = 1, + One = 2, + Two = 3, + Three = 4, + Four = 5, + Five = 6, + Six = 7, + Seven = 8, + Eight = 9, + Nine = 10, + Ten = 11, + Eleven = 12, + Twelve = 13, + Thirteen = 14, + Fourteen = 15, + Fifteen = 16 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/Bazaar/SubFilter/BazaarCategoryAccessoriesSubFilterType.cs b/srcs/WingsAPI.Packets/Enums/Bazaar/SubFilter/BazaarCategoryAccessoriesSubFilterType.cs new file mode 100644 index 0000000..68fe3a4 --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/Bazaar/SubFilter/BazaarCategoryAccessoriesSubFilterType.cs @@ -0,0 +1,12 @@ +namespace WingsAPI.Packets.Enums.Bazaar.SubFilter +{ + public enum BazaarCategoryAccessoriesSubFilterType : byte + { + All = 0, + Necklace = 1, + Ring = 2, + Bracelet = 3, + Fairy = 4, + Amulet = 5 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/Bazaar/SubFilter/BazaarCategoryConsumerItemSubFilterType.cs b/srcs/WingsAPI.Packets/Enums/Bazaar/SubFilter/BazaarCategoryConsumerItemSubFilterType.cs new file mode 100644 index 0000000..a15d91a --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/Bazaar/SubFilter/BazaarCategoryConsumerItemSubFilterType.cs @@ -0,0 +1,13 @@ +namespace WingsAPI.Packets.Enums.Bazaar.SubFilter +{ + public enum BazaarCategoryConsumerItemSubFilterType : byte + { + All = 0, + Food = 1, + Snack = 2, + MagicItem = 3, + Ingredients = 4, + PartnerItem = 5, + SaleItem = 6 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/Bazaar/SubFilter/BazaarCategoryEquipmentSubFilterType.cs b/srcs/WingsAPI.Packets/Enums/Bazaar/SubFilter/BazaarCategoryEquipmentSubFilterType.cs new file mode 100644 index 0000000..df8e257 --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/Bazaar/SubFilter/BazaarCategoryEquipmentSubFilterType.cs @@ -0,0 +1,19 @@ +namespace WingsAPI.Packets.Enums.Bazaar.SubFilter +{ + public enum BazaarCategoryEquipmentSubFilterType : byte + { + All = 0, + Hat = 1, + Accessory = 2, + Gloves = 3, + Shoes = 4, + Costume = 5, + CostumeHat = 6, + CostumeWeapon = 7, + CostumeWings = 8, + CostumeFemale = 9, + CostumeHatFemale = 10, + CostumeMale = 11, + CostumeHatMale = 12 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/Bazaar/SubFilter/BazaarCategoryMainItemSubFilterType.cs b/srcs/WingsAPI.Packets/Enums/Bazaar/SubFilter/BazaarCategoryMainItemSubFilterType.cs new file mode 100644 index 0000000..82abbfc --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/Bazaar/SubFilter/BazaarCategoryMainItemSubFilterType.cs @@ -0,0 +1,14 @@ +namespace WingsAPI.Packets.Enums.Bazaar.SubFilter +{ + public enum BazaarCategoryMainItemSubFilterType : byte + { + All = 0, + GeneralItems = 1, + Material = 2, + ProductionItem = 3, + SpecialItems = 4, + HealingPotion = 5, + Event = 6, + Title = 7 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/Bazaar/SubFilter/BazaarCategoryPartnerSubFilterType.cs b/srcs/WingsAPI.Packets/Enums/Bazaar/SubFilter/BazaarCategoryPartnerSubFilterType.cs new file mode 100644 index 0000000..c276043 --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/Bazaar/SubFilter/BazaarCategoryPartnerSubFilterType.cs @@ -0,0 +1,13 @@ +namespace WingsAPI.Packets.Enums.Bazaar.SubFilter +{ + public enum BazaarCategoryPartnerSubFilterType : byte + { + All = 0, + EmptyPartnerBead = 1, + PartnerBead = 2, + EmptyCardHolder = 3, + CloseAttack = 4, + RemoteAttack = 5, + Magic = 6 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/Bazaar/SubFilter/BazaarCategoryPetSubFilterType.cs b/srcs/WingsAPI.Packets/Enums/Bazaar/SubFilter/BazaarCategoryPetSubFilterType.cs new file mode 100644 index 0000000..f518553 --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/Bazaar/SubFilter/BazaarCategoryPetSubFilterType.cs @@ -0,0 +1,9 @@ +namespace WingsAPI.Packets.Enums.Bazaar.SubFilter +{ + public enum BazaarCategoryPetSubFilterType : byte + { + All = 0, + EmptyPetBead = 1, + PetBead = 2 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/Bazaar/SubFilter/BazaarCategoryShellSubFilterType.cs b/srcs/WingsAPI.Packets/Enums/Bazaar/SubFilter/BazaarCategoryShellSubFilterType.cs new file mode 100644 index 0000000..acc5e4b --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/Bazaar/SubFilter/BazaarCategoryShellSubFilterType.cs @@ -0,0 +1,9 @@ +namespace WingsAPI.Packets.Enums.Bazaar.SubFilter +{ + public enum BazaarCategoryShellSubFilterType : byte + { + All = 0, + Weapon = 1, + Clothing = 2 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/Bazaar/SubFilter/BazaarCategorySpecialistSubFilterType.cs b/srcs/WingsAPI.Packets/Enums/Bazaar/SubFilter/BazaarCategorySpecialistSubFilterType.cs new file mode 100644 index 0000000..9575b25 --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/Bazaar/SubFilter/BazaarCategorySpecialistSubFilterType.cs @@ -0,0 +1,41 @@ +namespace WingsAPI.Packets.Enums.Bazaar.SubFilter +{ + public enum BazaarCategorySpecialistSubFilterType : byte + { + All = 0, + EmptyCardHolder = 1, + Crusader = 2, + Berserker = 3, + Warrior = 4, + Ninja = 5, + WildKeeper = 6, + Assassin = 7, + Destroyer = 8, + Ranger = 9, + HolyMage = 10, + DarkGunner = 11, + RedMagician = 12, + BlueMagician = 13, + Jajamaru = 14, + ChickenCostume = 15, + Pyjama = 16, + Pirate = 17, + Gladiator = 18, + FireCannoneer = 19, + Volcano = 20, + BattleMonk = 21, + Scout = 22, + TideLord = 23, + DeathReaper = 24, + DemonHunter = 25, + Seer = 26, + Renegade = 27, + AvengingAngel = 28, + Archmage = 29, + DraconicFist = 30, + MysticArts = 32, + WeddingCostume = 33, + MasterWolf = 34, + DemonWarrior = 35 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/Bazaar/SubFilter/BazaarCategoryStoreMountSubFilterType.cs b/srcs/WingsAPI.Packets/Enums/Bazaar/SubFilter/BazaarCategoryStoreMountSubFilterType.cs new file mode 100644 index 0000000..3aadc5a --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/Bazaar/SubFilter/BazaarCategoryStoreMountSubFilterType.cs @@ -0,0 +1,9 @@ +namespace WingsAPI.Packets.Enums.Bazaar.SubFilter +{ + public enum BazaarCategoryStoreMountSubFilterType : byte + { + All = 0, + EmptyMountBead = 1, + MountBead = 2 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/Bazaar/SubFilter/BazaarCategoryWeaponArmourSubFilterType.cs b/srcs/WingsAPI.Packets/Enums/Bazaar/SubFilter/BazaarCategoryWeaponArmourSubFilterType.cs new file mode 100644 index 0000000..77c0d56 --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/Bazaar/SubFilter/BazaarCategoryWeaponArmourSubFilterType.cs @@ -0,0 +1,12 @@ +namespace WingsAPI.Packets.Enums.Bazaar.SubFilter +{ + public enum BazaarCategoryWeaponArmourSubFilterType : byte + { + All = 0, + Swordsman = 1, + Archer = 2, + Magician = 3, + Adventurer = 4, + MartialArtist = 5 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/BrawlerMorphType.cs b/srcs/WingsAPI.Packets/Enums/BrawlerMorphType.cs new file mode 100644 index 0000000..5331112 --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/BrawlerMorphType.cs @@ -0,0 +1,12 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.Enums +{ + public enum BrawlerMorphType : byte + { + Normal = 29, + Dragon = 30 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/BsInfoType.cs b/srcs/WingsAPI.Packets/Enums/BsInfoType.cs new file mode 100644 index 0000000..b9a8600 --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/BsInfoType.cs @@ -0,0 +1,8 @@ +namespace WingsEmu.Packets.Enums +{ + public enum BsInfoType : byte + { + OpenWindow = 0, + CloseWindow = 2 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/BuffCategory.cs b/srcs/WingsAPI.Packets/Enums/BuffCategory.cs new file mode 100644 index 0000000..b0b94c8 --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/BuffCategory.cs @@ -0,0 +1,10 @@ +namespace WingsEmu.Packets.Enums +{ + public enum BuffCategory : byte + { + GeneralEffect = 0, + MagicEffect = 1, + PoisonType = 2, + DiseaseSeries = 3 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/BuyShopType.cs b/srcs/WingsAPI.Packets/Enums/BuyShopType.cs new file mode 100644 index 0000000..6875d0d --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/BuyShopType.cs @@ -0,0 +1,12 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.Enums +{ + public enum BuyShopType : byte + { + CharacterShop = 1, + ItemShop = 2 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/Character/CharacterOption.cs b/srcs/WingsAPI.Packets/Enums/Character/CharacterOption.cs new file mode 100644 index 0000000..86d2729 --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/Character/CharacterOption.cs @@ -0,0 +1,27 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.Enums.Character +{ + public enum CharacterOption + { + ExchangeBlocked = 1, + FriendRequestBlocked = 2, + FamilyRequestBlocked = 3, + WhisperBlocked = 4, + GroupRequestBlocked = 5, + PetAutoRelive = 6, + PartnerAutoRelive = 7, + GroupSharing = 8, + MouseAimLock = 9, + HeroChatBlocked = 10, + QuickGetUp = 11, + EmoticonsBlocked = 12, + HpBlocked = 13, + BuffBlocked = 14, + MinilandInviteBlocked = 15, + HideHat = 16, + UiBlocked = 17 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/Character/CharacterState.cs b/srcs/WingsAPI.Packets/Enums/Character/CharacterState.cs new file mode 100644 index 0000000..f1c2562 --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/Character/CharacterState.cs @@ -0,0 +1,13 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.Enums.Character +{ + public enum CharacterState : byte + { + Unknown = 0, + Active = 1, + Inactive = 2 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/Character/ClassType.cs b/srcs/WingsAPI.Packets/Enums/Character/ClassType.cs new file mode 100644 index 0000000..912093c --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/Character/ClassType.cs @@ -0,0 +1,16 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.Enums.Character +{ + public enum ClassType : byte + { + Adventurer = 0, + Swordman = 1, + Archer = 2, + Magician = 3, + Wrestler = 4, + Unknown = 5 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/Character/GenderType.cs b/srcs/WingsAPI.Packets/Enums/Character/GenderType.cs new file mode 100644 index 0000000..ffe30cf --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/Character/GenderType.cs @@ -0,0 +1,13 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.Enums.Character +{ + public enum GenderType : byte + { + Male = 0, + Female = 1, + Unisex = 2 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/Chat/ChatMessageColorType.cs b/srcs/WingsAPI.Packets/Enums/Chat/ChatMessageColorType.cs new file mode 100644 index 0000000..7cdf5ba --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/Chat/ChatMessageColorType.cs @@ -0,0 +1,31 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.Enums.Chat +{ + public enum ChatMessageColorType : byte + { + White = 0, + PlayerSay = 1, + Mate = 2, + LightGreen = 3, + DarkGrey = 4, + OrangeWhisper = 5, + Blue = 6, + LightYellow = 7, + RedW = 8, + Yellow = 10, + Red = 11, + Green = 12, + LightPurple = 13, + BlackPurple = 17, + Buff = 20, + + IntenseGreen = 54, + IntenseBlue = 55, + IntenseRed = 56, + IntenseYellow = 57, + IntenseCyan = 59 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/Chat/ChatType.cs b/srcs/WingsAPI.Packets/Enums/Chat/ChatType.cs new file mode 100644 index 0000000..65b0300 --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/Chat/ChatType.cs @@ -0,0 +1,16 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.Enums.Chat +{ + public enum ChatType : byte + { + General = 0, + Whisper = 1, + Party = 2, + Family = 3, + Friend = 4, + Speaker = 5 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/Chat/MsgMessageType.cs b/srcs/WingsAPI.Packets/Enums/Chat/MsgMessageType.cs new file mode 100644 index 0000000..f55f7ab --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/Chat/MsgMessageType.cs @@ -0,0 +1,12 @@ +namespace WingsEmu.Packets.Enums.Chat +{ + public enum MsgMessageType : byte + { + Middle = 0, + BottomCard = 1, + MiddleYellow = 2, + SmallMiddle = 3, + BottomRed = 5, + MiddleAndBottomCard = 6 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/Chat/SpeakType.cs b/srcs/WingsAPI.Packets/Enums/Chat/SpeakType.cs new file mode 100644 index 0000000..f480cc7 --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/Chat/SpeakType.cs @@ -0,0 +1,12 @@ +namespace WingsEmu.Packets.Enums.Chat +{ + public enum SpeakType : byte + { + Normal = 0, + Family = 1, + Group = 3, + Player = 5, + TimeSpace = 7, + GameMaster = 15 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/Chat/SpeakerType.cs b/srcs/WingsAPI.Packets/Enums/Chat/SpeakerType.cs new file mode 100644 index 0000000..2418b7a --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/Chat/SpeakerType.cs @@ -0,0 +1,8 @@ +namespace WingsEmu.Packets.Enums.Chat +{ + public enum SpeakerType : byte + { + Normal_Speaker = 0, + Items_Speaker = 1 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/ClockType.cs b/srcs/WingsAPI.Packets/Enums/ClockType.cs new file mode 100644 index 0000000..0823843 --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/ClockType.cs @@ -0,0 +1,9 @@ +namespace WingsEmu.Packets.Enums +{ + public enum ClockType : byte + { + TimeSpaceClock = 1, + RedMiddle = 3, + RemoveClock = 10 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/EntityType.cs b/srcs/WingsAPI.Packets/Enums/EntityType.cs new file mode 100644 index 0000000..1ed8b95 --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/EntityType.cs @@ -0,0 +1,14 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.Enums +{ + public enum EntityType : byte + { + Player = 0, + Mate = 1, + Monster = 2, + NPC = 3 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/EquipmentType.cs b/srcs/WingsAPI.Packets/Enums/EquipmentType.cs new file mode 100644 index 0000000..442e987 --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/EquipmentType.cs @@ -0,0 +1,27 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.Enums +{ + public enum EquipmentType : byte + { + MainWeapon = 0, + Armor = 1, + Hat = 2, + Gloves = 3, + Boots = 4, + SecondaryWeapon = 5, + Necklace = 6, + Ring = 7, + Bracelet = 8, + Mask = 9, + Fairy = 10, + Amulet = 11, + Sp = 12, + CostumeSuit = 13, + CostumeHat = 14, + WeaponSkin = 15, + Wings = 16 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/ExcCloseType.cs b/srcs/WingsAPI.Packets/Enums/ExcCloseType.cs new file mode 100644 index 0000000..1fe3f37 --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/ExcCloseType.cs @@ -0,0 +1,8 @@ +namespace WingsEmu.Packets.Enums +{ + public enum ExcCloseType : byte + { + Failed = 0, + Successful = 1 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/FactionType.cs b/srcs/WingsAPI.Packets/Enums/FactionType.cs new file mode 100644 index 0000000..0197c6e --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/FactionType.cs @@ -0,0 +1,13 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.Enums +{ + public enum FactionType : byte + { + Neutral = 0, + Angel = 1, + Demon = 2 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/Families/FamilyActionType.cs b/srcs/WingsAPI.Packets/Enums/Families/FamilyActionType.cs new file mode 100644 index 0000000..cbd92b2 --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/Families/FamilyActionType.cs @@ -0,0 +1,11 @@ +namespace WingsAPI.Packets.Enums.Families +{ + public enum FamilyActionType : byte + { + SendInvite = 0, + Notice = 1, + FamilyShout = 2, + FamilyWarehouseHistory = 3, + FamilyWarehouse = 4 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/Families/FamilyAuthority.cs b/srcs/WingsAPI.Packets/Enums/Families/FamilyAuthority.cs new file mode 100644 index 0000000..22b6969 --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/Families/FamilyAuthority.cs @@ -0,0 +1,14 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.Enums.Families +{ + public enum FamilyAuthority : byte + { + Head = 0, + Deputy = 1, + Keeper = 2, + Member = 3 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/Families/FamilyJoinType.cs b/srcs/WingsAPI.Packets/Enums/Families/FamilyJoinType.cs new file mode 100644 index 0000000..fb1986f --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/Families/FamilyJoinType.cs @@ -0,0 +1,9 @@ +namespace WingsAPI.Packets.Enums.Families +{ + public enum FamilyJoinType : byte + { + PreAccepted, + Rejected, + Accepted + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/Families/FamilyLogType.cs b/srcs/WingsAPI.Packets/Enums/Families/FamilyLogType.cs new file mode 100644 index 0000000..4bc31d9 --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/Families/FamilyLogType.cs @@ -0,0 +1,25 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.Enums.Families +{ + public enum FamilyLogType : byte + { + DailyMessage = 1, + RaidWon = 2, + RainbowBattle = 3, + FamilyXP = 4, + FamilyLevelUp = 5, + LevelUp = 6, + ItemUpgraded = 7, + RightChanged = 8, + AuthorityChanged = 9, + MemberLeave = 10, + MemberJoin = 11, + FamilyMission = 12, + FamilyAchievement = 13, + HeroLevelUp = 14, + MemberUsedItem = 15 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/Families/FamilySkillsType.cs b/srcs/WingsAPI.Packets/Enums/Families/FamilySkillsType.cs new file mode 100644 index 0000000..a6adb06 --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/Families/FamilySkillsType.cs @@ -0,0 +1,9 @@ +namespace WingsEmu.Packets.Enums.Families +{ + public enum FamilySkillsType : byte + { + CanBePurchased = 0, + AlreadyPurchased = 1, + CannotBePurchased = 2 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/Families/FamilyTitle.cs b/srcs/WingsAPI.Packets/Enums/Families/FamilyTitle.cs new file mode 100644 index 0000000..4b3e20e --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/Families/FamilyTitle.cs @@ -0,0 +1,34 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.Enums.Families +{ + public enum FamilyTitle : byte + { + Nothing = 0, + OldUncle = 1, + OldAunt = 2, + Father = 3, + Mother = 4, + Uncle = 5, + Aunt = 6, + Brother = 7, + Sister = 8, + Spouse = 9, + Brother2 = 10, + Sister2 = 11, + OldSon = 12, + OldDaugter = 13, + MiddleSon = 14, + MiddleDaughter = 15, + YoungSon = 16, + YoungDaugter = 17, + OldLittleSon = 18, + OldLittleDaughter = 19, + LittleSon = 20, + LittleDaughter = 21, + MiddleLittleSon = 22, + MiddleLittleDaugter = 23 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/Families/FamilyWarehouseAuthorityType.cs b/srcs/WingsAPI.Packets/Enums/Families/FamilyWarehouseAuthorityType.cs new file mode 100644 index 0000000..ee2ab75 --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/Families/FamilyWarehouseAuthorityType.cs @@ -0,0 +1,13 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsAPI.Packets.Enums.Families +{ + public enum FamilyWarehouseAuthorityType : byte + { + None = 0, + Put = 1, + PutAndWithdraw = 2 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/FixedUpMode.cs b/srcs/WingsAPI.Packets/Enums/FixedUpMode.cs new file mode 100644 index 0000000..2263aba --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/FixedUpMode.cs @@ -0,0 +1,12 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.Enums +{ + public enum FixedUpMode + { + None = 0, + HasAmulet = 1 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/GameType.cs b/srcs/WingsAPI.Packets/Enums/GameType.cs new file mode 100644 index 0000000..9129b07 --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/GameType.cs @@ -0,0 +1,12 @@ +namespace WingsEmu.Packets.Enums +{ + public enum GameType : byte + { + SheepFarm = 1, + ArenaOfTalents = 2, + DodgeMeteor = 4, + GoodVersusEvil = 5, + ArenaOfMasters = 6, + RainbowBattle = 7 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/GroupRequestType.cs b/srcs/WingsAPI.Packets/Enums/GroupRequestType.cs new file mode 100644 index 0000000..b4fa41d --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/GroupRequestType.cs @@ -0,0 +1,17 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.Enums +{ + public enum GroupRequestType : byte + { + Requested = 0, + Invited = 1, + Accepted = 3, + Declined = 4, + Sharing = 5, + AcceptedShare = 6, + DeclinedShare = 7 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/GroupSharingType.cs b/srcs/WingsAPI.Packets/Enums/GroupSharingType.cs new file mode 100644 index 0000000..411da4b --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/GroupSharingType.cs @@ -0,0 +1,12 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.Enums +{ + public enum GroupSharingType : byte + { + ByOrder = 0, + Everyone = 1 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/GuriType.cs b/srcs/WingsAPI.Packets/Enums/GuriType.cs new file mode 100644 index 0000000..8000934 --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/GuriType.cs @@ -0,0 +1,31 @@ +namespace WingsEmu.Packets.Enums +{ + public enum GuriType + { + OpeningBox = 0, + ButtonSwitch = 1, + InExploration = 2, + Transforming = 3, + Mining = 4, + Unfreezing = 5, + Cooking = 6, + UsingItem = 7, + Identifying = 8, + OpeningTreasureChest = 9, + SendBack = 10, + Summon = 11, + CancelThornBushInvocation = 12, + CancelTransformation = 13, + AfterSumming, + ShellEffect = 17, + PetBasket = 201, + PartnerBackpack = 202, + SpPointInitializer = 203, + ShellIdentification = 204, + Perfume = 205, + Title = 306, + IceBreaker = 501, + IceBreakerFrozen = 502, + EventInWaiting = 506 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/HairColorType.cs b/srcs/WingsAPI.Packets/Enums/HairColorType.cs new file mode 100644 index 0000000..245a84e --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/HairColorType.cs @@ -0,0 +1,138 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.Enums +{ + public enum HairColorType : byte + { + DarkPurple = 0, + Yellow = 1, + Blue = 2, + Purple = 3, + Orange = 4, + Brown = 5, + Green = 6, + DarkGrey = 7, + LightBlue = 8, + PinkRed = 9, + LightYellow = 10, + LightPink = 11, + LightGreen = 12, + LightGrey = 13, + SkyBlue = 14, + Black = 15, + DarkOrange = 16, + DarkOrangeVariant2 = 17, + DarkOrangeVariant3 = 18, + DarkOrangeVariant4 = 19, + DarkOrangeVariant5 = 20, + DarkOrangeVariant6 = 21, + LightOrange = 22, + LightLightOrange = 23, + LightLightLightOrange = 24, + LightLightLightLightOrange = 25, + SuperLightOrange = 26, + DarkYellow = 27, + LightLightYellow = 28, + KakiYellow = 29, + SuperLightYellow = 30, + SuperLightYellow2 = 31, + SuperLightYellow3 = 32, + LittleDarkYellow = 33, + YellowVariant = 34, + YellowVariant1 = 35, + YellowVariant2 = 36, + YellowVariant3 = 37, + YellowVariant4 = 38, + YellowVariant5 = 39, + YellowVariant6 = 40, + YellowVariant7 = 41, + YellowVariant8 = 42, + YellowVariant9 = 43, + GreenVariant = 44, + GreenVariant1 = 45, + DarkGreenVariant = 46, + GreenMoreDarkVariant = 47, + GreenVariant2 = 48, + GreenVariant3 = 49, + GreenVariant4 = 50, + GreenVariant5 = 51, + GreenVariant6 = 52, + GreenVariant7 = 53, + GreenVariant8 = 54, + GreenVariant9 = 55, + GreenVariant10 = 56, + GreenVariant11 = 57, + GreenVariant12 = 58, + GreenVariant13 = 59, + GreenVariant14 = 60, + GreenVariant15 = 61, + GreenVariant16 = 62, + GreenVariant17 = 63, + GreenVariant18 = 64, + GreenVariant19 = 65, + GreenVariant20 = 66, + LightBlueVariant1 = 67, + LightBlueVariant2 = 68, + LightBlueVariant3 = 69, + LightBlueVariant4 = 70, + LightBlueVariant5 = 71, + LightBlueVariant6 = 72, + LightBlueVariant7 = 73, + LightBlueVariant8 = 74, + LightBlueVariant9 = 75, + LightBlueVariant10 = 76, + LightBlueVariant11 = 77, + LightBlueVariant12 = 78, + LightBlueVariant13 = 79, + DarkBlack = 80, + LightBlueVariant14 = 81, + LightBlueVariant15 = 82, + LightBlueVariant16 = 83, + LightBlueVariant17 = 84, + BlueVariant = 85, + BlueVariantDark = 86, + BlueVariantDarkDark = 87, + BlueVariantDarkDark2 = 88, + FlashBlue = 89, + FlashBlueDark = 90, + FlashBlueDark2 = 91, + FlashBlueDark3 = 92, + FlashBlueDark4 = 93, + FlashBlueDark5 = 94, + FlashBlueDark6 = 95, + FlashBlueDark7 = 96, + FlashBlueDark8 = 97, + FlashBlueDark9 = 98, + White = 99, + FlashBlueDark10 = 100, + FlashBlue1 = 101, + FlashBlue2 = 102, + FlashBlue3 = 103, + FlashBlue4 = 104, + FlashBlue5 = 105, + FlashPurple = 106, + FlashLightPurple = 107, + FlashLightPurple2 = 108, + FlashLightPurple3 = 109, + FlashLightPurple4 = 110, + FlashLightPurple5 = 111, + LightPurple = 112, + PurpleVariant1 = 113, + PurpleVariant2 = 114, + PurpleVariant3 = 115, + PurpleVariant4 = 116, + PurpleVariant5 = 117, + PurpleVariant6 = 118, + PurpleVariant7 = 119, + PurpleVariant8 = 120, + PurpleVariant9 = 121, + PurpleVariant10 = 122, + PurpleVariant11 = 123, + PurpleVariant12 = 124, + PurpleVariant13 = 125, + PurpleVariant14 = 126, + PurpleVariant15 = 127 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/HairStyleType.cs b/srcs/WingsAPI.Packets/Enums/HairStyleType.cs new file mode 100644 index 0000000..091a1eb --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/HairStyleType.cs @@ -0,0 +1,21 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.Enums +{ + public enum HairStyleType : byte + { + A = 0, + B = 1, + Gel = 2, + Wax = 3, + NoHair = 4, + FrenchBraid = 10, // Female + ChoppyBangs = 11, // Female + FemaleSpecialHair = 12, + FauxHawk = 13, // Male + MaleSpecialHair = 14, + JellyRolls = 15 // Male + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/InRespawnType.cs b/srcs/WingsAPI.Packets/Enums/InRespawnType.cs new file mode 100644 index 0000000..fd961e0 --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/InRespawnType.cs @@ -0,0 +1,13 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.Enums +{ + public enum InRespawnType : byte + { + TeleportationEffect = 0, + NoEffect = 1, + FallingDown = 2 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/InventoryType.cs b/srcs/WingsAPI.Packets/Enums/InventoryType.cs new file mode 100644 index 0000000..42e987c --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/InventoryType.cs @@ -0,0 +1,20 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.Enums +{ + public enum InventoryType : byte + { + Equipment = 0, + Main = 1, + Etc = 2, + Miniland = 3, + + Specialist = 6, + Costume = 7, + + + EquippedItems = 200 // not present in the game, for DB purpose + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/InvitationType.cs b/srcs/WingsAPI.Packets/Enums/InvitationType.cs new file mode 100644 index 0000000..43ea784 --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/InvitationType.cs @@ -0,0 +1,14 @@ +namespace WingsEmu.Packets.Enums +{ + public enum InvitationType : sbyte + { + Blocked = -1, + Friend = 0, + Raid = 1, + HiddenSpouse = 2, + Spouse = 5, + Group = 6, + Exchange = 7, + GroupPointShare = 8 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/ItModeType.cs b/srcs/WingsAPI.Packets/Enums/ItModeType.cs new file mode 100644 index 0000000..39dbf53 --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/ItModeType.cs @@ -0,0 +1,9 @@ +namespace WingsAPI.Packets.Enums +{ + public enum ItModeType + { + ShipToAct4 = 1, + ToAct4 = 2, + ToPortAlveus = 3 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/ItemType.cs b/srcs/WingsAPI.Packets/Enums/ItemType.cs new file mode 100644 index 0000000..0c75eec --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/ItemType.cs @@ -0,0 +1,39 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.Enums +{ + public enum ItemType : byte + { + Weapon = 0, + Armor = 1, + Fashion = 2, + Jewelry = 3, + Specialist = 4, + Box = 5, + Shell = 6, + Main = 10, + Upgrade = 11, + Production = 12, + Map = 13, + Special = 14, + Potion = 15, + Event = 16, + Title = 17, + Quest1 = 18, + Sell = 20, + Food = 21, + Snack = 22, + Magical = 24, + Material = 25, + PetPartnerItem = 26, + Ammo = 27, + Quest2 = 28, + House = 30, + Terrace = 31, + Minigame = 32, + Garden = 33, + MinilandTheme = 34 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/JewelOptionType.cs b/srcs/WingsAPI.Packets/Enums/JewelOptionType.cs new file mode 100644 index 0000000..0caa98a --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/JewelOptionType.cs @@ -0,0 +1,16 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.Enums +{ + public enum JewelOptionType : byte + { + MaximumAugmentationHP = 0, + MaximumAugmentationMP = 1, + MaximumRegenerationHP = 2, + MaximumRegenerationMP = 3, + MinimiseUsedMP = 4, + MinimiseCriticalHit = 5 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/LevelType.cs b/srcs/WingsAPI.Packets/Enums/LevelType.cs new file mode 100644 index 0000000..ad8219c --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/LevelType.cs @@ -0,0 +1,16 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.Enums +{ + public enum LevelType : byte + { + Level, + JobLevel, + SpJobLevel, + Heroic, + Fairy, + LevelMate + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/LoginFailType.cs b/srcs/WingsAPI.Packets/Enums/LoginFailType.cs new file mode 100644 index 0000000..9a4351e --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/LoginFailType.cs @@ -0,0 +1,19 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.Enums +{ + public enum LoginFailType : byte + { + OldClient = 1, + UnhandledError = 2, + Maintenance = 3, + AlreadyConnected = 4, + AccountOrPasswordWrong = 5, + CantConnect = 6, + Banned = 7, + WrongCountry = 8, + WrongCaps = 9 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/MShopPacketType.cs b/srcs/WingsAPI.Packets/Enums/MShopPacketType.cs new file mode 100644 index 0000000..26cd5ec --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/MShopPacketType.cs @@ -0,0 +1,13 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.Enums +{ + public enum MShopPacketType : byte + { + OpenShop = 0, + CloseShop = 1, + OpenDialog = 2 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/Mails/MailType.cs b/srcs/WingsAPI.Packets/Enums/Mails/MailType.cs new file mode 100644 index 0000000..da51133 --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/Mails/MailType.cs @@ -0,0 +1,8 @@ +namespace WingsEmu.Packets.Enums.Mails +{ + public enum MailType + { + Received = 1, + Sent = 2 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/Mails/ParcelActionType.cs b/srcs/WingsAPI.Packets/Enums/Mails/ParcelActionType.cs new file mode 100644 index 0000000..f64527e --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/Mails/ParcelActionType.cs @@ -0,0 +1,14 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.Enums.Mails +{ + public enum ParcelActionType + { + Add = 1, + Delete = 2, + NotEnoughPlace = 5, + Removed = 7 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/MateLevelUpType.cs b/srcs/WingsAPI.Packets/Enums/MateLevelUpType.cs new file mode 100644 index 0000000..baf8068 --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/MateLevelUpType.cs @@ -0,0 +1,8 @@ +namespace WingsAPI.Packets.Enums +{ + public enum MateLevelUpType + { + Normal, + ItemUsed + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/MateType.cs b/srcs/WingsAPI.Packets/Enums/MateType.cs new file mode 100644 index 0000000..6c0601f --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/MateType.cs @@ -0,0 +1,12 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.Enums +{ + public enum MateType : byte + { + Partner = 0, + Pet = 1 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/MedalType.cs b/srcs/WingsAPI.Packets/Enums/MedalType.cs new file mode 100644 index 0000000..21c014a --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/MedalType.cs @@ -0,0 +1,13 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.Enums +{ + public enum MedalType : byte + { + None = 0, + Silver = 1, + Gold = 2 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/MinilandState.cs b/srcs/WingsAPI.Packets/Enums/MinilandState.cs new file mode 100644 index 0000000..a20851e --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/MinilandState.cs @@ -0,0 +1,13 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.Enums +{ + public enum MinilandState : byte + { + OPEN = 0, + PRIVATE = 1, + LOCK = 2 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/ModalType.cs b/srcs/WingsAPI.Packets/Enums/ModalType.cs new file mode 100644 index 0000000..2a43c0b --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/ModalType.cs @@ -0,0 +1,8 @@ +namespace WingsAPI.Packets.Enums +{ + public enum ModalType + { + Cancel = 0, + Confirm = 1 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/MoveType.cs b/srcs/WingsAPI.Packets/Enums/MoveType.cs new file mode 100644 index 0000000..0200c86 --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/MoveType.cs @@ -0,0 +1,18 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.Enums +{ + public enum MoveType : byte + { + Right, + Left, + Up, + Down, + DiagUpRight, + DiagUpLeft, + DiagDownRight, + DiagDownLeft + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/NpcRunType.cs b/srcs/WingsAPI.Packets/Enums/NpcRunType.cs new file mode 100644 index 0000000..3b8f24c --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/NpcRunType.cs @@ -0,0 +1,201 @@ +namespace WingsEmu.Packets.Enums +{ + public enum NpcRunType : short + { + TIMES_UP = 0, + CHANGE_CLASS = 1, + UPGRADE_ITEM_NPC = 2, + GET_PARTNER = 3, + MATE = 4, + TIME_SPACE_TIMER = 5, + TIME_SPACE_END_DIALOG = 6, + JEWELRY_CELLON = 10, + OPEN_WINDOW = 12, + OPEN_CRAFTING = 14, + SET_REVIVAL_SPAWN = 15, + WARP_TELEPORT = 16, + ASK_ENTER_ARENA = 17, + CIRCLE_TIME_SKILL = 18, + TIME_SPACE_UNKNOWN = 19, + FAMILY_DIALOGUE = 23, + WARP_TELEPORT_ACT5 = 26, + GRASSLIN_EVENT = 31, + NOS_11_YEAR_MEDAL_PRODUCTION = 36, + NOS_PRODUCTION = 37, + YEAR_11_PRODUCTION_ = 38, + RAID_LIST = 39, + EGG_FAIRY_PRODUCTION = 40, + GRASSLIN_SEAL_PRODUCTION = 41, + WEDDING_GAZEBO = 45, + RED_MAGIC_FAIRY_PRODUCTION = 46, + OPEN_NOSBAZAAR = 60, + GRENIGAS_SEAL_PRODUCTION = 61, + TELEPORT_GRENIGAS_SQUARE = 62, + ICY_FLOWERS_MISSION = 65, + HEAT_POTION_MISSION = 66, + JOHN_MISSION = 67, + AKAMUR_MISSION = 68, + ICE_FLOWERS_10_PRODUCTION = 69, + MAGIC_CAMEL_PRODUCTION = 70, + MAGIC_CAMEL_BOX_PRODUCTION = 71, + TIMESPACE_ON_FINISH_DIALOG = 144, + LAND_OF_CHAOS_ENTER = 150, + USE_BANK = 321, + GET_BANK_BOOK = 322, + AMORA_MISSION = 327, + SHOW_PLAYER_SHOP = 900, + ENTER_TO_TS_ID = 1000, + DAILY_MISSION_TAROT = 1500, + + /* HALLOWEEN */ + HALLOWEEN_PRODUCTION_BOX = 47, + HALLOWEEN_PRODUCTION_COSTUME_10_YELLOW_CANDY = 72, + HALLOWEEN_PRODUCTION_COSTUME_10_BLACK_CANDY = 73, + HALLOWEEN_PRODUCTION_SEAL_30_YELLOW_CANDY = 74, + HALLOWEEN_PRODUCTION_SEAL_30_BLACK_CANDY = 75, + HALLOWEEN_PRODUCTION_SEAL_BAG_OF_SWEATS = 76, + HALLOWEEN_MISSION_MARY = 77, + HALLOWEEN_MISSION_EVA = 78, + HALLOWEEN_MISSION_MALCOLM = 79, + HALLOWEEN_MISSION_TEOMAN = 80, + HALLOWEEN_MISSION_ERIC = 81, + HALLOWEEN_CATACOMBS_ENTRANCE = 316, + HALLOWEEN_PRODUCTION_LAURENA_WAND = 317, + + /* WINTER */ + WINTER_PRODUCTION_BOX = 34, + WINTER_PRODUCTION_SNOWMAN_SEAL = 35, + WINTER_MISSION_SANTA = 82, + WINTER_PRODUCTION_SPECIAL_BAG = 83, + WINTER_PRODUCTION_SEALED_CHRIST_VESSEL_30_CREAM_CAKE = 84, + WINTER_PRODUCTION_COSTUME_30_CHOCOLATE_CAKE = 85, + WINTER_PRODUCTION_SEAL_30_CREAM_CAKE = 86, + WINTER_PRODUCTION_SEAL_30_CHOCOLATE_CAKE = 87, + WINTER_PRODUCTION_5_STOCKINGS_CHRIST_BOX = 88, + WINTER_MISSION_SLUGG = 89, + WINTER_MISSION_EVA = 90, + WINTER_MISSION_MALCOLM = 91, + WINTER_MISSION_TEOMAN = 92, + WINTER_MISSION_SORAYA = 93, + WINTER_MISSION_VIKINGS = 128, + WINTER_PRODUCTION_10_HAPPY = 129, + WINTER_PRODUCTION_10_HAPPY_YEAR = 130, + WINTER_MISSION_FIRST_SON_PARK_1 = 178, + WINTER_MISSION_FIRST_SON_PARK_2 = 179, + WINTER_MISSION_SECOND_SON_PARK_1 = 180, + WINTER_MISSION_SECOND_SON_PARK_2 = 181, + WINTER_MISSION_THIRD_SON_PARK_1 = 182, + WINTER_MISSION_THIRD_SON_PARK_2 = 183, + WINTER_MISSION_WANDERER_1 = 184, + WINTER_MISSION_WANDERER_2 = 185, + WINTER_MISSION_WANDERER_3 = 186, + WINTER_MISSION_MARU_1 = 187, + WINTER_MISSION_MARU_2 = 188, + WINTER_PRODUCTION_3_BRASS_COINS_BOX = 190, + WINTER_PRODUCTION_BETTER_SLEIGH = 325, + WINTER_MISSION_REINDEER = 326, + WINTER_PRODUCTION_HAPPY = 6013, + WINTER_PRODUCTION_HAPPY_YEAR = 6014, + + /* EASTER */ + EASTER_MISSION_MIMI = 94, + EASTER_PRODUCTION_5_GOLD_EGGS_BOX = 95, + EASTER_PRODUCTION_30_CHOCOLATE_QFC_SEAL = 96, + EASTER_MISSION_SLUGG = 97, + EASTER_MISSION_CALVIN = 98, + EASTER_MISSION_EVA = 99, + EASTER_MISSION_MALCOLM = 100, + EASTER_MISSION_HUNGBU = 191, + EASTER_MISSION_HUNGBU_SWALLOW = 192, + EASTER_PRODUCTION_10_CLEAN_EGGS_CAKE = 328, + EASTER_PRODUCTION_10_CLEAN_EGGS_BOX = 329, + EASTER_PRODUCTION_10_ROTTER_EGGS_CLEAN = 330, + EASTER_MISSION_SLUGG_STORY = 331, + + /* SP5 & SP6 MISSIONS/PRODUCTION */ + DRACO_AMULET_MISSION = 110, + GLACERUS_AMULET_MISSION = 131, + DRACO_SEAL_PRODUCTION = 111, + ENTER_TO_WATTER_GROTO = 132, + GLACERUS_SEAL_PRODUCTION = 133, + GIVE_SP6_TS_MISSION = 134, + DRACO_CLAW_SP5_PRODUCTION = 145, + DRACO_CLAW_SP5_PERF_STON_PRODUCTION = 146, + GLACERUS_MANE_SP6_PRODUCTION = 147, + GLACERUS_MANE_SP6_PERF_STONE_PRODUCTION = 148, + + /* ARENA OF TALENTS/MASTERS */ + ARENA_OF_TALENTS_REGISTRATION = 135, + ARENA_OF_MASTERS_REGISTRATION = 136, + ARENA_OF_MASTERS_SPECTATOR_CHOICE = 137, + ARENA_OF_TALENTS_SPECTATOR_RANDOM = 138, + ARENA_OF_MASTERS_SPECTATOR_RANDOM = 139, + ARENA_OF_MASTERS_RANKING = 140, + + /* SP8 MISSIONS/PRODUCTION */ + SP8_MISSION_LAURENA = 193, + SP8_PRODUCTION_SOUL_SLIVER_3_POWDER = 194, + SP8_PRODUCTION_5_SEED_DAMNATION_SEAL = 195, + + /* SUMMER */ + SUMMER_MISSION_RAPHAEL_STORY = 197, + SUMMER_PRODUCTION_BUNNY_LIVER_WITH_BOX = 198, + SUMMER_PRODUCTION_BUNNY_LIVER = 199, + SUMMER_MISSION_RAPHAEL = 201, + SUMMER_MISSION_JACK = 1507, + SUMMER_EVENT_PIRATE_SP = 1508, + SUMMER_PRODUCTION_10_MARINER_10_MAP_SEAL = 1509, + SUMMER_PRODUCTION_PIRATE_SP_LUCK = 1511, + + /* ACT 6 */ + ACT61_MISSION_FIRST = 300, + ACT61_TELEPORT_CYLLOAN = 301, + ACT62_MISSION_FIRST = 302, + ACT62_MISSION_HEALING_CHOICE_SAVE = 303, + ACT62_MISSION_HEALING_CHOICE_NO_SAVE = 304, + ACT62_ENTER_TO_SHIP = 305, + ACT62_ENTER_TO_SHIP_2 = 306, + ACT62_BACK_TO_CYLLOAN = 307, + + /* VALHALLA */ + VALHALLA_MISSION_BARNI = 308, + VALHALLA_MISSION_RAGNAR_10KK_CHOICE = 309, + VALHALLA_MISSION_RAGNAR_FREE_CHOICE = 310, + VALHALLA_MISSION_RAGNAR = 311, + VALHALLA_MISSION_FRIGG = 312, + VALHALLA_ENTER_TO_TART_HAP = 313, + VALHALLA_BACK_TO_PORT = 314, + VALHALLA_MISSION_JENNIFER = 315, + + /* MARTIAL ARTIST */ + MA_MISSION_SP2 = 324, + MA_MISSION_SP3 = 340, + + /* FAMILY */ + FAMILY_WAREHOUSE_OPEN = 1600, + FAMILY_WAREHOUSE_HISTORY = 1601, + FAMILY_UPGRADE_WAREHOUSE_21 = 1602, + FAMILY_UPGRADE_WAREHOUSE_49 = 1603, + FAMILY_UPGRADE_MEMBERS_SIZE_70 = 1604, + FAMILY_UPGRADE_MEMBERS_SIZE_100 = 1605, + + /* QUESTS */ + QUEST_ADDITIONAL_ACT5 = 200, + QUEST_ADDITIONAL = 2000, + QUEST_END_SP = 2001, + QUEST_TELEPORT_TO_SP_MAP = 2002, + QUEST_RECEIVE = 3000, + QUEST_RECEIVE_MAIN = 3001, + QUEST_RECEIVE_TS_POINTS = 3002, + QUEST_RECEIVE_ADDITIONAL_BLUE = 3006, // (\!/) + + /* ACT 4/5 */ + ACT4_ENTER_SHIP = 5001, + ACT4_LEAVE = 5002, + ACT4_LEAVE_SHIP = 5004, + ACT4_ENTER = 5005, + ACT5_ENTER_SHIP = 5011, + ACT5_LEAVE = 5012, + ACT5_LEAVE_SHIP = 5014 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/PdtiType.cs b/srcs/WingsAPI.Packets/Enums/PdtiType.cs new file mode 100644 index 0000000..fe06634 --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/PdtiType.cs @@ -0,0 +1,20 @@ +namespace WingsEmu.Packets.Enums +{ + public enum PdtiType : byte + { + SkillIsTrained = 1, + ItemIsStrengthened = 2, + ItemIsSucceedsInBeeting = 3, + ItIsCraftedAsNormalQualityAccessory = 6, + ItIsCraftedAsHigherQualityAccessory = 7, + ItIsCraftedAsHighestQualityAccessory = 8, + ConvertedIntoPartnerEquip = 9, + ResistancesAreFused = 10, + ItemHasBeenProduced = 11, + ItemIsChanged = 12, + ItemIsObtained = 13, + ItemIsCollected = 14, + Acquisition = 15, // ? + ItemIdentificationSuccessful = 16 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/PenaltyType.cs b/srcs/WingsAPI.Packets/Enums/PenaltyType.cs new file mode 100644 index 0000000..46fb43e --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/PenaltyType.cs @@ -0,0 +1,12 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.Enums +{ + public enum PenaltyType : byte + { + Muted = 0, + Prison = 1 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/PortalType.cs b/srcs/WingsAPI.Packets/Enums/PortalType.cs new file mode 100644 index 0000000..9e5141f --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/PortalType.cs @@ -0,0 +1,25 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.Enums +{ + public enum PortalType : sbyte + { + MapPortal = -1, + TSNormal = 0, // same over >127 - sbyte + Closed = 1, + Open = 2, + Miniland = 3, + TSEnd = 4, + TSEndClosed = 5, + Exit = 6, + ExitClosed = 7, + Raid = 8, + Effect = 9, // same as 13 - 19 and 20 - 126 + AngelRaid = 10, + DemonRaid = 11, + TimeSpace = 12, + ShopTeleport = 20 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/QnamlType.cs b/srcs/WingsAPI.Packets/Enums/QnamlType.cs new file mode 100644 index 0000000..414e111 --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/QnamlType.cs @@ -0,0 +1,16 @@ +namespace WingsEmu.Packets.Enums +{ + public enum QnamlType : byte + { + InstantCombat = 1, + Icebreaker = 2, + FamilyMemberCalled = 3, + SheepRaid = 4, + TournamentEvent = 5, + MeteorInvRaid = 6, + BattleRoyale = 7, + RainbowBattle = 8, + BushiKingRaid = 9, + Raid = 100 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/QuestRewardType.cs b/srcs/WingsAPI.Packets/Enums/QuestRewardType.cs new file mode 100644 index 0000000..6e20540 --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/QuestRewardType.cs @@ -0,0 +1,23 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.Enums +{ + public enum QuestRewardType : byte + { + Gold = 1, + SecondGold = 2, + Exp = 3, + SecondExp = 4, + JobExp = 5, + RandomReward = 7, + AllRewards = 8, + Reput = 9, + ThirdGold = 10, + ThirdExp = 11, + SecondJobExp = 12, + Unknow = 13, //never used but it is in the dat file, + ItemsDependingOnClass = 14 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/QuestType.cs b/srcs/WingsAPI.Packets/Enums/QuestType.cs new file mode 100644 index 0000000..b09668c --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/QuestType.cs @@ -0,0 +1,37 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.Enums +{ + public enum QuestType : byte + { + KILL_MONSTER_BY_VNUM = 1, + DROP_HARDCODED = 2, + DROP_CHANCE = 3, + DELIVER_ITEM_TO_NPC = 4, + CAPTURE_WITHOUT_KEEPING = 5, + CAPTURE_AND_KEEP = 6, + COMPLETE_TIMESPACE = 7, + CRAFT_WITHOUT_KEEPING = 8, + DIE_X_TIMES = 9, + EARN_REPUTATION = 10, + COMPLETE_TIMESPACE_WITH_ATLEAST_X_POINTS = 11, + DIALOG = 12, + DROP_IN_TIMESPACE = 13, + GIVE_ITEM_TO_NPC = 14, + DIALOG_WHILE_WEARING = 15, + DIALOG_WHILE_HAVING_ITEM = 16, + DROP_CHANCE_2 = 17, + GIVE_NPC_GOLD = 18, + GO_TO_MAP = 19, + COLLECT = 20, + USE_ITEM_ON_TARGET = 21, + DIALOG_2 = 22, // "Get more information" + NOTHING = 23, // This quest will be finished automatically + GIVE_ITEM_TO_NPC_2 = 24, // "Inspect" + WIN_RAID_AND_TALK_TO_NPC = 25, + KILL_X_MOBS_SOUND_FLOWER = 26, + KILL_PLAYER_IN_REGION = 27 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/QueueWindow.cs b/srcs/WingsAPI.Packets/Enums/QueueWindow.cs new file mode 100644 index 0000000..cd7aae3 --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/QueueWindow.cs @@ -0,0 +1,15 @@ +namespace WingsEmu.Packets.Enums +{ + public enum QueueWindow : byte + { + WaitForEntry = 0, + SearchOpposingTeam = 1, + OpposingTeamFound = 2, + NoOpposingsTeamFound = 3, + RejectRegistrationRequest = 4, + SearchRegisteredTeam = 5, + FoundRegisteredTeam = 6, + NoRegisteredTeamFound = 7, + SearchRegisteredTeamImmediately = 8 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/RaidType.cs b/srcs/WingsAPI.Packets/Enums/RaidType.cs new file mode 100644 index 0000000..29f6a28 --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/RaidType.cs @@ -0,0 +1,46 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsAPI.Packets.Enums +{ + public enum RaidType : byte + { + Cuby = 0, + Ginseng = 1, + Castra = 2, + GiantBlackSpider = 3, + Slade = 4, + ChickenKing = 5, + Namaju = 6, + Grasslin = 7, + Snowman = 8, + RobberGang = 9, + + JackOLantern = 10, + ChickenQueen = 11, + Pirate = 12, + Kertos = 13, + Valakus = 14, + Grenigas = 15, + LordDraco = 16, + Glacerus = 17, + Foxy = 18, + Maru = 19, + + Laurena = 20, + HongbiCheongbi = 21, + LolaLopears = 22, + Zenas = 23, + Erenia = 24, + Fernon = 25, + Fafnir = 26, + Yertirand = 27, + MadProffesor = 28, + MadMarchHare = 29, + + Kirollas = 30, + Carno = 31, + Belial = 32 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/Rainbow/RainbowBattleFlagTeamType.cs b/srcs/WingsAPI.Packets/Enums/Rainbow/RainbowBattleFlagTeamType.cs new file mode 100644 index 0000000..54b2501 --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/Rainbow/RainbowBattleFlagTeamType.cs @@ -0,0 +1,9 @@ +namespace WingsAPI.Packets.Enums.Rainbow +{ + public enum RainbowBattleFlagTeamType + { + None = 0, + Red = 1, + Blue = 2 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/Rainbow/RainbowBattleFlagType.cs b/srcs/WingsAPI.Packets/Enums/Rainbow/RainbowBattleFlagType.cs new file mode 100644 index 0000000..d17dc2e --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/Rainbow/RainbowBattleFlagType.cs @@ -0,0 +1,9 @@ +namespace WingsAPI.Packets.Enums.Rainbow +{ + public enum RainbowBattleFlagType + { + Big = 5, + Medium = 2, + Small = 1 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/Rainbow/RainbowBattleTeamType.cs b/srcs/WingsAPI.Packets/Enums/Rainbow/RainbowBattleTeamType.cs new file mode 100644 index 0000000..699c587 --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/Rainbow/RainbowBattleTeamType.cs @@ -0,0 +1,8 @@ +namespace WingsAPI.Packets.Enums.Rainbow +{ + public enum RainbowBattleTeamType + { + Red, + Blue + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/Rainbow/RainbowTimeType.cs b/srcs/WingsAPI.Packets/Enums/Rainbow/RainbowTimeType.cs new file mode 100644 index 0000000..e1f18b2 --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/Rainbow/RainbowTimeType.cs @@ -0,0 +1,9 @@ +namespace WingsAPI.Packets.Enums.Rainbow +{ + public enum RainbowTimeType + { + End = 0, + Start = 1, + Enter = 2 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/RarifyMode.cs b/srcs/WingsAPI.Packets/Enums/RarifyMode.cs new file mode 100644 index 0000000..a6af047 --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/RarifyMode.cs @@ -0,0 +1,14 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.Enums +{ + public enum RarifyMode + { + Normal, + Free, + Drop, + Increase + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/RarifyProtection.cs b/srcs/WingsAPI.Packets/Enums/RarifyProtection.cs new file mode 100644 index 0000000..4dde1ab --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/RarifyProtection.cs @@ -0,0 +1,16 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.Enums +{ + public enum RarifyProtection + { + None, + ProtectionAmulet, + BlessingAmulet, + HeroicAmulet, + RandomHeroicAmulet, + Scroll + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/ReceiverType.cs b/srcs/WingsAPI.Packets/Enums/ReceiverType.cs new file mode 100644 index 0000000..b85e36d --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/ReceiverType.cs @@ -0,0 +1,17 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.Enums +{ + public enum ReceiverType : byte + { + Unknown = 0, + All = 1, + AllExceptMe = 2, + AllInRange = 3, + AllNoEmoBlocked = 5, + AllExceptGroup = 8, + AllExceptMeAct4 = 9 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/RefinerType.cs b/srcs/WingsAPI.Packets/Enums/RefinerType.cs new file mode 100644 index 0000000..e0d0b8d --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/RefinerType.cs @@ -0,0 +1,13 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.Enums +{ + public enum RefinerType : byte + { + SOUL_GEM = 1, + CRYSTAL = 2, + CELLON = 3 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/Relations/CharacterRelationType.cs b/srcs/WingsAPI.Packets/Enums/Relations/CharacterRelationType.cs new file mode 100644 index 0000000..5f0b610 --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/Relations/CharacterRelationType.cs @@ -0,0 +1,14 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.Enums.Relations +{ + public enum CharacterRelationType : short + { + Blocked = -1, + Friend = 0, + HiddenSpouse = 2, + Spouse = 5 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/RequestExchangeType.cs b/srcs/WingsAPI.Packets/Enums/RequestExchangeType.cs new file mode 100644 index 0000000..0790eac --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/RequestExchangeType.cs @@ -0,0 +1,16 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.Enums +{ + public enum RequestExchangeType : byte + { + Unknown = 0, + Requested = 1, + AcceptExchangeInvitation = 2, + Confirmed = 3, + Cancelled = 4, + Declined = 5 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/RespawnType.cs b/srcs/WingsAPI.Packets/Enums/RespawnType.cs new file mode 100644 index 0000000..ab7a507 --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/RespawnType.cs @@ -0,0 +1,14 @@ +namespace WingsEmu.Game._enum +{ + public enum RespawnType + { + NOSVILLE_SPAWN = 0, + KREM_SPAWN = 1, + ALVEUS_SPAWN = 2, + ACT4_DEMON_SPAWN = 3, + ACT4_ANGEL_SPAWN = 4, + MORTAZ_DESERT_PORT = 6, + AKAMUR_CAMP = 7, + DESERT_EAGLE_CITY = 8 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/ScriptedInstanceType.cs b/srcs/WingsAPI.Packets/Enums/ScriptedInstanceType.cs new file mode 100644 index 0000000..1aee86e --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/ScriptedInstanceType.cs @@ -0,0 +1,14 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.Enums +{ + public enum ScriptedInstanceType : byte + { + TimeSpace = 0, + Raid = 1, + RaidAct4 = 2, + RaidAct6 = 3 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/ServerState.cs b/srcs/WingsAPI.Packets/Enums/ServerState.cs new file mode 100644 index 0000000..0e1afc9 --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/ServerState.cs @@ -0,0 +1,12 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.Enums +{ + public enum ServerState : byte + { + Online, + Maintenance + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/SheepScoreType.cs b/srcs/WingsAPI.Packets/Enums/SheepScoreType.cs new file mode 100644 index 0000000..ffb0cb3 --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/SheepScoreType.cs @@ -0,0 +1,13 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.Enums +{ + public enum SheepScoreType : byte + { + // Just to make it clearer + Player = 1, + Monster = 2 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/Shells/ShellEffectCategory.cs b/srcs/WingsAPI.Packets/Enums/Shells/ShellEffectCategory.cs new file mode 100644 index 0000000..b7be8f9 --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/Shells/ShellEffectCategory.cs @@ -0,0 +1,33 @@ +namespace WingsAPI.Packets.Enums.Shells +{ + public enum ShellEffectCategory : byte + { + // Weapon + CNormalWeapon = 1, + BNormalWeapon = 2, + ANormalWeapon = 3, + SNormalWeapon = 4, + CBonusWeapon = 5, + BBonusWeapon = 6, + ABonusWeapon = 7, + SBonusWeapon = 8, + CPVPWeapon = 9, + BPVPWeapon = 10, + APVPWeapon = 11, + SPVPWeapon = 12, + + // Armor + CNormalArmor = 13, + BNormalArmor = 14, + ANormalArmor = 15, + SNormalArmor = 16, + CBonusArmor = 17, + BBonusArmor = 18, + ABonusArmor = 19, + SBonusArmor = 20, + CPVPArmor = 21, + BPVPArmor = 22, + APVPArmor = 23, + SPVPArmor = 24 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/Shells/ShellEffectType.cs b/srcs/WingsAPI.Packets/Enums/Shells/ShellEffectType.cs new file mode 100644 index 0000000..6be977f --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/Shells/ShellEffectType.cs @@ -0,0 +1,135 @@ +namespace WingsAPI.Packets.Enums.Shells +{ + public enum ShellEffectType : byte + { + /* WEAPON OPTIONS */ + + // INCREASE DAMAGES + DamageImproved = 1, + PercentageTotalDamage = 2, + + // DEBUFFS + MinorBleeding = 3, + Bleeding = 4, + HeavyBleeding = 5, + Blackout = 6, + Freeze = 7, + DeadlyBlackout = 8, + + // PVE OPTIONS + DamageIncreasedPlant = 9, + DamageIncreasedAnimal = 10, + DamageIncreasedEnemy = 11, + DamageIncreasedUnDead = 12, + DamageIncreasedSmallMonster = 13, + DamageIncreasedBigMonster = 14, + + // CHARACTER BONUSES + CriticalChance = 15, //Except Staff + CriticalDamage = 16, //Except Staff + AntiMagicDisorder = 17, //Only Staff + IncreasedFireProperties = 18, + IncreasedWaterProperties = 19, + IncreasedLightProperties = 20, + IncreasedDarkProperties = 21, + IncreasedElementalProperties = 22, + ReducedMPConsume = 23, + HPRecoveryForKilling = 24, + MPRecoveryForKilling = 25, + + // SP BONUSES + SLDamage = 26, + SLDefence = 27, + SLElement = 28, + SLHP = 29, + SLGlobal = 30, + + // PVE RATES INCREASE + GainMoreGold = 31, + GainMoreXP = 32, + GainMoreCXP = 33, + + // PVP OPTIONS + PercentageDamageInPVP = 34, + ReducesPercentageEnemyDefenceInPVP = 35, + ReducesEnemyFireResistanceInPVP = 36, + ReducesEnemyWaterResistanceInPVP = 37, + ReducesEnemyLightResistanceInPVP = 38, + ReducesEnemyDarkResistanceInPVP = 39, + ReducesEnemyAllResistancesInPVP = 40, + NeverMissInPVP = 41, + PVPDamageAt15Percent = 42, + ReducesEnemyMPInPVP = 43, + + // R8 CHAMPION OPTIONS + InspireFireResistanceWithPercentage = 44, + InspireWaterResistanceWithPercentage = 45, + InspireLightResistanceWithPercentage = 46, + InspireDarkResistanceWithPercentage = 47, + GainSPForKilling = 48, + IncreasedPrecision = 49, + IncreasedFocus = 50, + + /* ARMOR OPTIONS */ + + // DEFENSE INCREASE + CloseDefence = 51, + DistanceDefence = 52, + MagicDefence = 53, + PercentageTotalDefence = 54, + + // ANTI-DEBUFFS + ReducedMinorBleeding = 55, + ReducedBleedingAndMinorBleeding = 56, + ReducedAllBleedingType = 57, + ReducedStun = 58, + ReducedAllStun = 59, + ReducedParalysis = 60, + ReducedFreeze = 61, + ReducedBlind = 62, + ReducedSlow = 63, + ReducedArmorDeBuff = 64, + ReducedShock = 65, + ReducedPoisonParalysis = 66, + ReducedAllNegativeEffect = 67, + + // CHARACTER BONUSES + RecovryHPOnRest = 68, + RevoryHP = 69, + RecoveryMPOnRest = 70, + RecoveryMP = 71, + RecoveryHPInDefence = 72, + ReducedCritChanceRecive = 73, + + // RESISTANCE INCREASE + IncreasedFireResistance = 74, + IncreasedWaterResistance = 75, + IncreasedLightResistance = 76, + IncreasedDarkResistance = 77, + IncreasedAllResistance = 78, + + // VARIOUS PVE BONUSES + ReducedPrideLoss = 79, + ReducedProductionPointConsumed = 80, + IncreasedProductionPossibility = 81, + IncreasedRecoveryItemSpeed = 82, + + // PVP BONUSES + PercentageAllPVPDefence = 83, + CloseDefenceDodgeInPVP = 84, + DistanceDefenceDodgeInPVP = 85, + IgnoreMagicDamage = 86, + DodgeAllDamage = 87, + ProtectMPInPVP = 88, + + // R8 CHAMPION OPTIONS + FireDamageImmuneInPVP = 89, + WaterDamageImmuneInPVP = 90, + LightDamageImmuneInPVP = 91, + DarkDamageImmuneInPVP = 92, + AbsorbDamagePercentageA = 93, + AbsorbDamagePercentageB = 94, + AbsorbDamagePercentageC = 95, + IncreaseEvasiveness = 96 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/Shells/ShellType.cs b/srcs/WingsAPI.Packets/Enums/Shells/ShellType.cs new file mode 100644 index 0000000..1f70bc2 --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/Shells/ShellType.cs @@ -0,0 +1,16 @@ +namespace WingsAPI.Packets.Enums.Shells +{ + public enum ShellType : byte + { + HalfShellWeapon = 0, + FullShellWeapon = 1, + SpecialShellWeapon = 2, + PvpShellWeapon = 3, + PerfectShellWeapon = 4, + HalfShellArmor = 50, + FullShellArmor = 51, + SpecialShellArmor = 52, + PvpShellArmor = 53, + PerfectShellArmor = 54 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/ShopEndType.cs b/srcs/WingsAPI.Packets/Enums/ShopEndType.cs new file mode 100644 index 0000000..c8ae78d --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/ShopEndType.cs @@ -0,0 +1,9 @@ +namespace WingsEmu.Packets.Enums +{ + public enum ShopEndType : byte + { + Player = 0, + Npc = 1, + SpecialistHolder = 2 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/SmemoType.cs b/srcs/WingsAPI.Packets/Enums/SmemoType.cs new file mode 100644 index 0000000..2b5e985 --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/SmemoType.cs @@ -0,0 +1,16 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.Enums +{ + public enum SmemoType : byte + { + OK = 0, + Balance = 1, + Error = 3, + BankBalance = 4, + BankError = 5, + BankInfo = 6 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/SpUpgradeResult.cs b/srcs/WingsAPI.Packets/Enums/SpUpgradeResult.cs new file mode 100644 index 0000000..eed152d --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/SpUpgradeResult.cs @@ -0,0 +1,9 @@ +namespace WingsAPI.Packets.Enums +{ + public enum SpUpgradeResult + { + Succeed, + Fail, + Break + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/SpecialMapIdType.cs b/srcs/WingsAPI.Packets/Enums/SpecialMapIdType.cs new file mode 100644 index 0000000..4445512 --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/SpecialMapIdType.cs @@ -0,0 +1,13 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.Enums +{ + public enum SpecialMapIdType : short + { + BattleRoyal = 247, + MasterTalentArena = 2014, + TalentArena = 2015 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/SuPacketHitMode.cs b/srcs/WingsAPI.Packets/Enums/SuPacketHitMode.cs new file mode 100644 index 0000000..92c6864 --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/SuPacketHitMode.cs @@ -0,0 +1,16 @@ +namespace WingsEmu.Packets.ServerPackets.Battle +{ + public enum SuPacketHitMode : sbyte + { + NoDamageFail = -2, + NoDamageSuccess = -1, + SuccessAttack = 0, + Miss = 1, + OutOfRange = 2, + CriticalAttack = 3, + MissAoe = 4, + AttackedInAoe = 5, + AttackedInAoeCrit = 6, + ReflectionAoeMiss = 7 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/TalentArenaOptionType.cs b/srcs/WingsAPI.Packets/Enums/TalentArenaOptionType.cs new file mode 100644 index 0000000..852bdf7 --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/TalentArenaOptionType.cs @@ -0,0 +1,14 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.Enums +{ + public enum TalentArenaOptionType : byte + { + Watch = 0, + Nothing = 1, + Call = 2, + WatchAndCall = 3 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/TeleporterType.cs b/srcs/WingsAPI.Packets/Enums/TeleporterType.cs new file mode 100644 index 0000000..b9c8414 --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/TeleporterType.cs @@ -0,0 +1,12 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.Enums +{ + public enum TeleporterType : byte + { + TELEPORTER = 0, + TELEPORT_ON_MAP = 1 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/Titles/TitEqPacketType.cs b/srcs/WingsAPI.Packets/Enums/Titles/TitEqPacketType.cs new file mode 100644 index 0000000..bbee311 --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/Titles/TitEqPacketType.cs @@ -0,0 +1,8 @@ +namespace WingsEmu.Packets.Enums.Titles +{ + public enum TitEqPacketType + { + EquipAsVisible = 1, + EquipAsEffect = 2 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/Titles/TitleStatus.cs b/srcs/WingsAPI.Packets/Enums/Titles/TitleStatus.cs new file mode 100644 index 0000000..bc82f54 --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/Titles/TitleStatus.cs @@ -0,0 +1,14 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.Enums.Titles +{ + public enum TitleStatus + { + Available = 1, + EquippedVisible = 3, + EquippedEffect = 5, + EquippedEffectAndVisible = 7 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/UpgradeMode.cs b/srcs/WingsAPI.Packets/Enums/UpgradeMode.cs new file mode 100644 index 0000000..680b025 --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/UpgradeMode.cs @@ -0,0 +1,13 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.Enums +{ + public enum UpgradeMode + { + Normal, + Reduced, + Free + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/UpgradeProtection.cs b/srcs/WingsAPI.Packets/Enums/UpgradeProtection.cs new file mode 100644 index 0000000..576b521 --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/UpgradeProtection.cs @@ -0,0 +1,12 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.Enums +{ + public enum UpgradeProtection + { + None, + Protected + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/UpgradeResult.cs b/srcs/WingsAPI.Packets/Enums/UpgradeResult.cs new file mode 100644 index 0000000..af68702 --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/UpgradeResult.cs @@ -0,0 +1,9 @@ +namespace WingsAPI.Packets.Enums +{ + public enum UpgradeResult + { + Fixed, + Fail, + Succeed + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/VisualNameType.cs b/srcs/WingsAPI.Packets/Enums/VisualNameType.cs new file mode 100644 index 0000000..b2a347e --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/VisualNameType.cs @@ -0,0 +1,8 @@ +namespace WingsEmu.Packets.Enums +{ + public enum VisualNameType + { + Player = 0, + GameMaster = 2 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/VisualType.cs b/srcs/WingsAPI.Packets/Enums/VisualType.cs new file mode 100644 index 0000000..91eb81c --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/VisualType.cs @@ -0,0 +1,14 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.Enums +{ + public enum VisualType : byte + { + Player = 1, + Npc = 2, + Monster = 3, + Object = 9 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/Enums/WindowType.cs b/srcs/WingsAPI.Packets/Enums/WindowType.cs new file mode 100644 index 0000000..c28d63d --- /dev/null +++ b/srcs/WingsAPI.Packets/Enums/WindowType.cs @@ -0,0 +1,35 @@ +namespace WingsEmu.Packets.Enums +{ + public enum WindowType : byte + { + PARTNER_EXCHANGE = 0, + UPGRADE_ITEM_NPC = 1, + MERGE_JEWELRY = 3, + UPGRADE_ITEM_RARITY = 7, + CONNECT_BOOTS_GLOVES = 8, + UPGRADE_SP = 9, + UPGRADE_ITEM_PLUS_EQ_SCROLL = 20, + UPGRADE_ITEM_RARITY_EQ_SCROLL = 21, + DECONSTRUCTION_ITEM_TO_MATERIAL = 23, // not supported on official server, but it's in the client + CELLON_UPGRADING = 24, // not supported on official server, but it's in the client + UPGRADE_SP_BLUE_SCROLL = 25, + UPGRADE_SP_RED_SCROLL = 26, + CRAFTING_RANDOM_ITEMS_RARITY = 27, + CRAFTING_ITEMS = 28, + RELIC_RESEARCH = 29, + UNKNOWN_EXCHANGE = 30, // not used + NOSBAZAAR = 32, + CHICKEN_FREE_SCROLL = 35, + FAMILY_UPGRADE = 37, + PAJAMA_FREE_SCROLL = 38, + SP_PERFECTION = 41, + PIRATE_FREE_SCROLL = 42, + UPGRADE_ITEM_PLUS_GOLD_EQ_SCROLL = 43, + GOLDEN_SP_CARD_HOLDER = 44, + FAIRY_UPGRADE_ZENAS = 50, + FAIRY_UPGRADE_ERENIA = 51, + FAIRY_UPGRADE_FERNON = 52, + COSTUME_MERGE = 53, + CLOUSE_UI = 99 // sending c_close 0 packet + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/IClientPacket.cs b/srcs/WingsAPI.Packets/IClientPacket.cs new file mode 100644 index 0000000..5355238 --- /dev/null +++ b/srcs/WingsAPI.Packets/IClientPacket.cs @@ -0,0 +1,12 @@ +namespace WingsEmu.Packets +{ + /// + /// Packets that are sent by client + /// + public interface IClientPacket : IPacket + { + string OriginalContent { get; set; } + + string OriginalHeader { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/IPacket.cs b/srcs/WingsAPI.Packets/IPacket.cs new file mode 100644 index 0000000..684970d --- /dev/null +++ b/srcs/WingsAPI.Packets/IPacket.cs @@ -0,0 +1,6 @@ +namespace WingsEmu.Packets +{ + public interface IPacket + { + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/IPacketDeserializer.cs b/srcs/WingsAPI.Packets/IPacketDeserializer.cs new file mode 100644 index 0000000..1f9f098 --- /dev/null +++ b/srcs/WingsAPI.Packets/IPacketDeserializer.cs @@ -0,0 +1,9 @@ +using System; + +namespace WingsEmu.Packets +{ + public interface IPacketDeserializer + { + (IClientPacket, Type) Deserialize(string serializedData, bool includeKeepAlive); + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/IPacketSerializer.cs b/srcs/WingsAPI.Packets/IPacketSerializer.cs new file mode 100644 index 0000000..ef0078f --- /dev/null +++ b/srcs/WingsAPI.Packets/IPacketSerializer.cs @@ -0,0 +1,7 @@ +namespace WingsEmu.Packets +{ + public interface IPacketSerializer + { + string Serialize(T packet) where T : IPacket; + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/IServerPacket.cs b/srcs/WingsAPI.Packets/IServerPacket.cs new file mode 100644 index 0000000..a9c812b --- /dev/null +++ b/srcs/WingsAPI.Packets/IServerPacket.cs @@ -0,0 +1,9 @@ +namespace WingsEmu.Packets +{ + /// + /// Packets that are sent by server + /// + public interface IServerPacket : IPacket + { + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/PacketAliasAttribute.cs b/srcs/WingsAPI.Packets/PacketAliasAttribute.cs new file mode 100644 index 0000000..aae51e2 --- /dev/null +++ b/srcs/WingsAPI.Packets/PacketAliasAttribute.cs @@ -0,0 +1,24 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; + +namespace WingsEmu.Packets +{ + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] + public class PacketAliasAttribute : Attribute + { + #region Instantiation + + public PacketAliasAttribute(string alias) => Alias = alias; + + #endregion + + #region Properties + + public string Alias { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/PacketDeserializer.cs b/srcs/WingsAPI.Packets/PacketDeserializer.cs new file mode 100644 index 0000000..ae4ffe9 --- /dev/null +++ b/srcs/WingsAPI.Packets/PacketDeserializer.cs @@ -0,0 +1,397 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text.RegularExpressions; + +namespace WingsEmu.Packets +{ + public class PacketDeserializer : IPacketDeserializer + { + private static readonly IEnumerable EnumerableOfAcceptedTypes = new[] + { + typeof(int), + typeof(double), + typeof(long), + typeof(short), + typeof(int?), + typeof(double?), + typeof(long?), + typeof(short?) + }; + + private readonly Dictionary _headersToType = new(); + private readonly Dictionary> _packetSerializationInformations = new(); + + public PacketDeserializer(IEnumerable registeredPackets) + { + // Iterate thru all PacketDefinition implementations + foreach (ClientPacketRegistered registeredPacket in registeredPackets) + { + try + { + GenerateSerializationInformations(registeredPacket.PacketType); + } + catch (Exception e) + { + Console.WriteLine($"Error registering : {registeredPacket} : {e.Message}"); + } + + // add to serialization informations + } + } + + /// + /// Deserializes a string into a PacketDefinition + /// + /// The content to deseralize + /// The type of the packet to deserialize to + /// + /// Include the keep alive identity or exclude it + /// + /// The deserialized packet. + public (IClientPacket, Type) Deserialize(string packetContent, bool includesKeepAliveIdentity = false) + { + try + { + string packetHeader = packetContent.Split(' ')[includesKeepAliveIdentity ? 1 : 0]; + Type packetType = GetPacketTypeByHeader(packetHeader.StartsWith("#") ? packetHeader.Substring(1) : packetHeader); + + if (packetType == null) + { + var unresolvedPacket = new UnresolvedPacket + { + OriginalHeader = packetHeader, + OriginalContent = packetContent + }; + return (unresolvedPacket, typeof(UnresolvedPacket)); + } + + Dictionary serializationInformation = GetSerializationInformation(packetType); + var deserializedPacket = (ClientPacket)Activator.CreateInstance(packetType); // reflection is bad, improve? + SetDeserializationInformation(deserializedPacket, packetContent, packetHeader); + deserializedPacket = Deserialize(packetContent, deserializedPacket, serializationInformation, includesKeepAliveIdentity); + return (deserializedPacket, packetType); + } + catch (Exception e) + { + Console.WriteLine($"The serialized packet has the wrong format. Packet: {packetContent}"); + Console.WriteLine(e); + return (null, null); + } + } + + private Type GetPacketTypeByHeader(string packetHeader) + { + if (_headersToType.TryGetValue(packetHeader, out Type packetType)) + { + return packetType; + } + + return null; + } + + + private ClientPacket Deserialize(string packetContent, ClientPacket deserializedPacket, Dictionary serializationInformation, bool includesKeepAliveIdentity) + { + MatchCollection matches = Regex.Matches(packetContent, @"([^\040]+[\.][^\040]+[\040]?)+((?=\040)|$)|([^\040]+)((?=\040)|$)"); + + if (matches.Count <= 0) + { + return deserializedPacket; + } + + foreach (KeyValuePair packetBasePropertyInfo in serializationInformation) + { + int currentIndex = packetBasePropertyInfo.Key.Index + (includesKeepAliveIdentity ? 2 : 1); // adding 2 because we need to skip incrementing number and packet header + + if (currentIndex < matches.Count) + { + if (packetBasePropertyInfo.Key.SerializeToEnd) + { + // get the value to the end and stop deserialization + string valueToEnd = packetContent.Substring(matches[currentIndex].Index, packetContent.Length - matches[currentIndex].Index); + packetBasePropertyInfo.Value.SetValue(deserializedPacket, + DeserializeValue(packetBasePropertyInfo.Value.PropertyType, valueToEnd, packetBasePropertyInfo.Key, matches, includesKeepAliveIdentity)); + break; + } + + + string currentValue = matches[currentIndex].Value; + + if (packetBasePropertyInfo.Value.PropertyType == typeof(string) && string.IsNullOrEmpty(currentValue)) + { + throw new NullReferenceException(); + } + + // set the value & convert currentValue + packetBasePropertyInfo.Value.SetValue(deserializedPacket, + DeserializeValue(packetBasePropertyInfo.Value.PropertyType, currentValue, packetBasePropertyInfo.Key, matches, includesKeepAliveIdentity)); + } + else + { + break; + } + } + + return deserializedPacket; + } + + /// + /// Converts for instance -1.12.1.8.-1.-1.-1.-1.-1 to eg. List + /// String to convert + /// + /// Type + /// of the property to convert + /// + /// The string as converted List + private IList DeserializeSimpleList(string currentValues, Type genericListType) + { + var subpackets = (IList)Convert.ChangeType(Activator.CreateInstance(genericListType), genericListType); + foreach (string currentValue in currentValues.Split('.')) + { + object value = DeserializeValue(genericListType.GenericTypeArguments[0], currentValue, null, null); + subpackets.Add(value); + } + + return subpackets; + } + + private object DeserializeSubpacket(string currentSubValues, Type packetBasePropertyType, Dictionary subpacketSerializationInfo, + bool isReturnPacket = false) + { + string[] subpacketValues = currentSubValues.Split(isReturnPacket ? '^' : '.'); + object newSubpacket = Activator.CreateInstance(packetBasePropertyType); + + foreach (KeyValuePair subpacketPropertyInfo in subpacketSerializationInfo) + { + int currentSubIndex = isReturnPacket ? subpacketPropertyInfo.Key.Index + 1 : subpacketPropertyInfo.Key.Index; // return packets do include header + string currentSubValue = subpacketValues[currentSubIndex]; + + subpacketPropertyInfo.Value.SetValue(newSubpacket, DeserializeValue(subpacketPropertyInfo.Value.PropertyType, currentSubValue, subpacketPropertyInfo.Key, null)); + } + + return newSubpacket; + } + + /// + /// Converts a Sublist of Packets, For instance 0.4903.5.0.0 2.340.0.0.0 + /// 3.720.0.0.0 5.4912.6.0.0 9.227.0.0.0 10.803.0.0.0 to + /// + /// The value as String + /// Type of the Property to convert to + /// + /// + /// + /// + /// + private IList DeserializeSubpackets(string currentValue, Type packetBasePropertyType, bool shouldRemoveSeparator, MatchCollection packetMatchCollections, int? currentIndex, + bool includesKeepAliveIdentity) + { + // split into single values + var splitSubPacket = currentValue.Split(' ').ToList(); + + // generate new list + var subPackets = (IList)Convert.ChangeType(Activator.CreateInstance(packetBasePropertyType), packetBasePropertyType); + + Type subPacketType = packetBasePropertyType.GetGenericArguments()[0]; + Dictionary subpacketSerializationInfo = GetSerializationInformation(subPacketType); + + // handle subpackets with separator + if (shouldRemoveSeparator) + { + if (!currentIndex.HasValue || packetMatchCollections == null) + { + return subPackets; + } + + var splittedSubpacketParts = packetMatchCollections.Select(m => m.Value).ToList(); + splitSubPacket = new List(); + + string generatedPseudoDelimitedString = string.Empty; + int subPacketTypePropertiesCount = subpacketSerializationInfo.Count; + + // check if the amount of properties can be serialized properly + if (((splittedSubpacketParts.Count + (includesKeepAliveIdentity ? 1 : 0)) + % subPacketTypePropertiesCount) == 0) // amount of properties per subpacket does match the given value amount in % + { + for (int i = currentIndex.Value + 1 + (includesKeepAliveIdentity ? 1 : 0); i < splittedSubpacketParts.Count; i++) + { + int j; + for (j = i; j < i + subPacketTypePropertiesCount; j++) + { + // add delimited value + generatedPseudoDelimitedString += splittedSubpacketParts[j] + "."; + } + + i = j - 1; + + //remove last added separator + generatedPseudoDelimitedString = generatedPseudoDelimitedString.Substring(0, generatedPseudoDelimitedString.Length - 1); + + // add delimited values to list of values to serialize + splitSubPacket.Add(generatedPseudoDelimitedString); + generatedPseudoDelimitedString = string.Empty; + } + } + else + { + throw new Exception("The amount of splitted subpacket values without delimiter do not match the % property amount of the serialized type."); + } + } + + foreach (string subpacket in splitSubPacket) + { + subPackets.Add(DeserializeSubpacket(subpacket, subPacketType, subpacketSerializationInfo)); + } + + return subPackets; + } + + private object DeserializeValue(Type packetPropertyType, string currentValue, PacketIndexAttribute packetIndexAttribute, MatchCollection packetMatches, + bool includesKeepAliveIdentity = false) + { + // check for empty value and cast it to null + if (currentValue == "-1" || currentValue == "-") + { + currentValue = null; + } + + // enum should be casted to number + if (packetPropertyType.BaseType != null && packetPropertyType.BaseType == typeof(Enum)) + { + object convertedValue = null; + try + { + if (currentValue != null && packetPropertyType.IsEnumDefined(Enum.Parse(packetPropertyType, currentValue))) + { + convertedValue = Enum.Parse(packetPropertyType, currentValue); + } + } + catch (Exception) + { + //Log.Warn($"Could not convert value {currentValue} to type {packetPropertyType.Name}"); + } + + return convertedValue; + } + + if (packetPropertyType == typeof(bool)) // handle boolean values + { + return currentValue != "0"; + } + + if (packetPropertyType.BaseType != null && packetPropertyType.BaseType == typeof(ClientPacket)) // subpacket + { + Dictionary subpacketSerializationInfo = GetSerializationInformation(packetPropertyType); + return DeserializeSubpacket(currentValue, packetPropertyType, subpacketSerializationInfo, packetIndexAttribute?.IsReturnPacket ?? false); + } + + if (packetPropertyType.IsGenericType && packetPropertyType.GetGenericTypeDefinition().IsAssignableFrom(typeof(List<>)) // subpacket list + && packetPropertyType.GenericTypeArguments[0].BaseType == typeof(ClientPacket)) + { + return DeserializeSubpackets(currentValue, packetPropertyType, packetIndexAttribute?.RemoveSeparator ?? false, packetMatches, packetIndexAttribute?.Index, includesKeepAliveIdentity); + } + + if (packetPropertyType.IsGenericType && packetPropertyType.GetGenericTypeDefinition().IsAssignableFrom(typeof(List<>))) // simple list + { + return DeserializeSimpleList(currentValue, packetPropertyType); + } + + if (Nullable.GetUnderlyingType(packetPropertyType) != null && string.IsNullOrEmpty(currentValue)) // empty nullable value + { + return null; + } + + if (Nullable.GetUnderlyingType(packetPropertyType) == null) + { + return Convert.ChangeType(currentValue, packetPropertyType); // cast to specified type + } + + if (packetPropertyType.GenericTypeArguments[0]?.BaseType == typeof(Enum)) + { + return Enum.Parse(packetPropertyType.GenericTypeArguments[0], currentValue); + } + + if (!EnumerableOfAcceptedTypes.Contains(packetPropertyType)) + { + return Convert.ChangeType(currentValue, packetPropertyType.GenericTypeArguments[0]); + } + + switch (packetPropertyType) + { + case Type _ when packetPropertyType == typeof(long): + case Type _ when packetPropertyType == typeof(int): + case Type _ when packetPropertyType == typeof(double): + case Type _ when packetPropertyType == typeof(short): + if (int.TryParse(currentValue, out int b) && b < 0) + { + currentValue = "0"; + } + + break; + + case Type _ when packetPropertyType == typeof(long?): + case Type _ when packetPropertyType == typeof(int?): + case Type _ when packetPropertyType == typeof(double?): + case Type _ when packetPropertyType == typeof(short?): + if (currentValue == null) + { + currentValue = "0"; + } + + if (int.TryParse(currentValue, out int c) && c < 0) + { + currentValue = "0"; + } + + break; + } + + return Convert.ChangeType(currentValue, packetPropertyType.GenericTypeArguments[0]); + } + + private Dictionary GenerateSerializationInformations(Type serializationType) + { + string header = serializationType.GetCustomAttribute()?.Identification; + + if (string.IsNullOrEmpty(header)) + { + throw new Exception($"Packet header cannot be empty. PacketType: {serializationType.Name}"); + } + + var packetsForPacketDefinition = new Dictionary(); + + foreach (PropertyInfo packetBasePropertyInfo in serializationType.GetProperties().Where(x => x.GetCustomAttributes(false).OfType().Any())) + { + PacketIndexAttribute indexAttribute = packetBasePropertyInfo.GetCustomAttributes(false).OfType().FirstOrDefault(); + + if (indexAttribute != null) + { + packetsForPacketDefinition.Add(indexAttribute, packetBasePropertyInfo); + } + } + + _headersToType.Add(header, serializationType); + _packetSerializationInformations.Add(serializationType, packetsForPacketDefinition); + return _packetSerializationInformations[serializationType]; + } + + private Dictionary GetSerializationInformation(Type serializationType) => + _packetSerializationInformations.TryGetValue(serializationType, out Dictionary infos) + ? infos + : GenerateSerializationInformations(serializationType); + + private static void SetDeserializationInformation(ClientPacket packet, string packetContent, string packetHeader) + { + packet.OriginalContent = packetContent; + packet.OriginalHeader = packetHeader; + packet.IsReturnPacket = packetHeader.StartsWith("#"); + } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/PacketExtensions.cs b/srcs/WingsAPI.Packets/PacketExtensions.cs new file mode 100644 index 0000000..e0ac5b4 --- /dev/null +++ b/srcs/WingsAPI.Packets/PacketExtensions.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using WingsEmu.Packets; + +namespace WingsAPI.Packets +{ + public static class PacketExtensions + { + public static void AddClientPacketsInAssembly(this IServiceCollection services) + { + IEnumerable types = typeof(T).Assembly.GetTypes().Where(s => !s.IsInterface && !s.IsAbstract && typeof(ClientPacket).IsAssignableFrom(s) && s != typeof(UnresolvedPacket)).ToArray(); + + foreach (Type type in types) + { + services.AddSingleton(new ClientPacketRegistered + { + PacketType = type + }); + } + } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/PacketHeaderAttribute.cs b/srcs/WingsAPI.Packets/PacketHeaderAttribute.cs new file mode 100644 index 0000000..a9b7959 --- /dev/null +++ b/srcs/WingsAPI.Packets/PacketHeaderAttribute.cs @@ -0,0 +1,24 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; + +namespace WingsEmu.Packets +{ + public class PacketHeaderAttribute : Attribute + { + public PacketHeaderAttribute(string identification) => Identification = identification; + + + /// + /// Unique identification of the Packet + /// + public string Identification { get; set; } + + /// + /// Pass the packet to handler method even if the serialization has failed. + /// + public bool PassNonParseablePacket { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/PacketIndexAttribute.cs b/srcs/WingsAPI.Packets/PacketIndexAttribute.cs new file mode 100644 index 0000000..8905558 --- /dev/null +++ b/srcs/WingsAPI.Packets/PacketIndexAttribute.cs @@ -0,0 +1,63 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; + +namespace WingsEmu.Packets +{ + public class PacketIndexAttribute : Attribute + { + #region Instantiation + + /// Specify the Index of the packet to parse this property to. + /// + /// The zero based index starting from header (exclusive). + /// + /// + /// Adds an # to the Header and replaces Spaces with ^ if set to + /// true. + /// + /// + /// Defines if everything from this index should + /// be serialized into the underlying property + /// + /// + /// Removes + /// the separator (.) for List packets. + /// + public PacketIndexAttribute(int index, bool isReturnPacket = false, bool serializeToEnd = false, bool removeSeparator = false) + { + Index = index; + IsReturnPacket = isReturnPacket; + SerializeToEnd = serializeToEnd; + RemoveSeparator = removeSeparator; + } + + #endregion + + #region Properties + + /// + /// The zero based index starting from the header (exclusive). + /// + public int Index { get; set; } + + /// + /// Adds an # to the Header and replaces Spaces with ^ + /// + public bool IsReturnPacket { get; set; } + + /// Removes the separator (.) for List packets. + public bool RemoveSeparator { get; set; } + + /// + /// Defines if everything from this index should be serialized into the underlying property. + /// + public bool SerializeToEnd { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/PacketSerializationInformation.cs b/srcs/WingsAPI.Packets/PacketSerializationInformation.cs new file mode 100644 index 0000000..f5a2e12 --- /dev/null +++ b/srcs/WingsAPI.Packets/PacketSerializationInformation.cs @@ -0,0 +1,16 @@ +using System.Reflection; + +namespace WingsEmu.Packets +{ + internal class PacketSerializationInformation + { + public PacketSerializationInformation(string header, (PacketIndexAttribute, PropertyInfo)[] propertyInfos) + { + Header = header; + Properties = propertyInfos; + } + + public string Header { get; } + public (PacketIndexAttribute, PropertyInfo)[] Properties { get; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/PacketSerializer.cs b/srcs/WingsAPI.Packets/PacketSerializer.cs new file mode 100644 index 0000000..de8a893 --- /dev/null +++ b/srcs/WingsAPI.Packets/PacketSerializer.cs @@ -0,0 +1,246 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Packets +{ + public sealed class PacketSerializer : IPacketSerializer + { + private static readonly bool _isInitialized; + private static readonly Dictionary _packetSerialization = new(); + + static PacketSerializer() + { + IEnumerable packetTypes = typeof(GameStartPacket).Assembly.GetTypes().Where(s => !s.IsInterface && !s.IsAbstract && typeof(ServerPacket).IsAssignableFrom(s)); + + if (_isInitialized) + { + return; + } + + foreach (Type packetType in packetTypes) + { + if (!_packetSerialization.ContainsKey(packetType)) + { + _packetSerialization.Add(packetType, GenerateSerializationInformations(packetType)); + } + } + + _isInitialized = true; + } + + public string Serialize(T packet) where T : IPacket + { + try + { + // load pregenerated serialization information + PacketSerializationInformation serializationInformation = GetSerializationInformation(typeof(T)); + + var deserializedPacket = new StringBuilder(serializationInformation.Header); // set header + + int lastIndex = 0; + foreach ((PacketIndexAttribute index, PropertyInfo property) in serializationInformation.Properties) + { + // check if we need to add a non mapped values (pseudovalues) + if (index.Index > lastIndex + 1) + { + int amountOfEmptyValuesToAdd = index.Index - (lastIndex + 1); + + for (int i = 0; i < amountOfEmptyValuesToAdd; i++) + { + deserializedPacket.Append(" 0"); + } + } + + // add value for current configuration + deserializedPacket.Append(SerializeValue(property.PropertyType, property.GetValue(packet), index)); + + // check if the value should be serialized to end + if (index.SerializeToEnd) + { + // we reached the end + break; + } + + // set new index + lastIndex = index.Index; + } + + return deserializedPacket.ToString(); + } + catch (Exception e) + { + // Log.Warn("Wrong Packet Format!", e); + return string.Empty; + } + } + + private static PacketSerializationInformation GenerateSerializationInformations(Type serializationType) + { + string header = serializationType.GetCustomAttribute()?.Identification; + + if (string.IsNullOrEmpty(header)) + { + throw new Exception($"Packet header cannot be empty. PacketType: {serializationType.Name}"); + } + + var packetsForPacketDefinition = new Dictionary(); + + foreach (PropertyInfo packetBasePropertyInfo in serializationType.GetProperties().Where(x => x.GetCustomAttributes(false).OfType().Any())) + { + PacketIndexAttribute indexAttribute = packetBasePropertyInfo.GetCustomAttributes(false).OfType().FirstOrDefault(); + + if (indexAttribute != null) + { + packetsForPacketDefinition.Add(indexAttribute, packetBasePropertyInfo); + } + } + + // order by index + IOrderedEnumerable> keyValuePairs = packetsForPacketDefinition.OrderBy(p => p.Key.Index); + return new PacketSerializationInformation(header, keyValuePairs.Select(s => (s.Key, s.Value)).ToArray()); + } + + private PacketSerializationInformation GetSerializationInformation(Type serializationType) + { + if (!_packetSerialization.TryGetValue(serializationType, out PacketSerializationInformation infos)) + { + infos = GenerateSerializationInformations(serializationType); + _packetSerialization[serializationType] = infos; + } + + return infos; + } + + private void GenerateSerializationInformations() + where TPacketDefinition : ServerPacket + { + // Iterate thru all PacketDefinition implementations + foreach (Type packetBaseType in typeof(TPacketDefinition).Assembly.GetTypes().Where(p => !p.IsInterface && !p.IsAbstract && typeof(TPacketDefinition).BaseType.IsAssignableFrom(p))) + { + // add to serialization informations + GenerateSerializationInformations(packetBaseType); + } + } + + private string SerializeValue(Type propertyType, object value, PacketIndexAttribute packetIndexAttribute = null) + { + if (propertyType == null) + { + return string.Empty; + } + + // check for nullable without value or string + if (propertyType == typeof(string) && string.IsNullOrEmpty(Convert.ToString(value))) + { + return " -"; + } + + if (Nullable.GetUnderlyingType(propertyType) != null && string.IsNullOrEmpty(Convert.ToString(value))) + { + return " -1"; + } + + // enum should be casted to number + if (propertyType.BaseType != null && propertyType.BaseType == typeof(Enum)) + { + return $" {Convert.ToInt16(value)}"; + } + + if (propertyType == typeof(bool)) + { + // bool is 0 or 1 not True or False + return Convert.ToBoolean(value) ? " 1" : " 0"; + } + + if (propertyType.BaseType != null && propertyType.BaseType == typeof(ServerPacket)) + { + PacketSerializationInformation subpacketSerializationInfo = GetSerializationInformation(propertyType); + return SerializeSubpacket(value, subpacketSerializationInfo, packetIndexAttribute?.IsReturnPacket ?? false, packetIndexAttribute?.RemoveSeparator ?? false); + } + + if (propertyType.IsGenericType && propertyType.GetGenericTypeDefinition().IsAssignableFrom(typeof(List<>)) + && propertyType.GenericTypeArguments[0].BaseType == typeof(ServerPacket)) + { + return SerializeSubpackets((IList)value, propertyType, packetIndexAttribute?.RemoveSeparator ?? false); + } + + if (propertyType.IsGenericType && propertyType.GetGenericTypeDefinition().IsAssignableFrom(typeof(List<>))) //simple list + { + return SerializeSimpleList((IList)value, propertyType); + } + + return $" {value}"; + } + + private string SerializeSubpackets(ICollection listValues, Type packetBasePropertyType, bool shouldRemoveSeparator) + { + string serializedSubPacket = string.Empty; + PacketSerializationInformation tmp = GetSerializationInformation(packetBasePropertyType.GetGenericArguments()[0]); + + if (listValues.Count > 0) + { + serializedSubPacket = listValues.Cast().Aggregate(serializedSubPacket, + (current, listValue) => current + SerializeSubpacket(listValue, tmp, false, shouldRemoveSeparator)); + } + + return serializedSubPacket; + } + + /// + /// Converts for instance List to -1.12.1.8.-1.-1.-1.-1.-1 + /// + /// + /// Values in List of simple type. + /// + /// + /// The + /// simple type. + /// + /// + private string SerializeSimpleList(IList listValues, Type propertyType) + { + string resultListPacket = string.Empty; + int listValueCount = listValues.Count; + if (listValueCount <= 0) + { + return resultListPacket; + } + + resultListPacket += SerializeValue(propertyType.GenericTypeArguments[0], listValues[0]); + + for (int i = 1; i < listValueCount; i++) + { + resultListPacket += $".{SerializeValue(propertyType.GenericTypeArguments[0], listValues[i]).Replace(" ", "")}"; + } + + return resultListPacket; + } + + private string SerializeSubpacket(object value, PacketSerializationInformation subpacketSerializationInfo, bool isReturnPacket, + bool shouldRemoveSeparator) + { + string serializedSubpacket = isReturnPacket ? $" #{subpacketSerializationInfo.Header}^" : " "; + + // iterate thru configure subpacket properties + foreach ((PacketIndexAttribute index, PropertyInfo property) subpacketPropertyInfo in subpacketSerializationInfo.Properties) + { + // first element + if (subpacketPropertyInfo.index.Index != 0) + { + serializedSubpacket += isReturnPacket ? "^" : + shouldRemoveSeparator ? " " : "."; + } + + serializedSubpacket += SerializeValue(subpacketPropertyInfo.property.PropertyType, subpacketPropertyInfo.property.GetValue(value)).Replace(" ", ""); + } + + return serializedSubpacket; + } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ServerPacket.cs b/srcs/WingsAPI.Packets/ServerPacket.cs new file mode 100644 index 0000000..b890bc5 --- /dev/null +++ b/srcs/WingsAPI.Packets/ServerPacket.cs @@ -0,0 +1,10 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets +{ + public abstract class ServerPacket : IServerPacket + { + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ServerPackets/EffectServerPacket.cs b/srcs/WingsAPI.Packets/ServerPackets/EffectServerPacket.cs new file mode 100644 index 0000000..fe2c205 --- /dev/null +++ b/srcs/WingsAPI.Packets/ServerPackets/EffectServerPacket.cs @@ -0,0 +1,23 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets.ServerPackets +{ + [PacketHeader("eff")] + public class EffectServerPacket : ServerPacket + { + #region Properties + + [PacketIndex(0)] + public byte EffectType { get; set; } + + [PacketIndex(1)] + public long CharacterId { get; set; } + + [PacketIndex(2)] + public int Id { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ServerPackets/Titles/TitInfoPacket.cs b/srcs/WingsAPI.Packets/ServerPackets/Titles/TitInfoPacket.cs new file mode 100644 index 0000000..6cb35cf --- /dev/null +++ b/srcs/WingsAPI.Packets/ServerPackets/Titles/TitInfoPacket.cs @@ -0,0 +1,20 @@ +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Packets.ServerPackets.Titles +{ + [PacketHeader("titinfo")] + public class TitInfoPacket : ServerPacket, IServerPacket + { + [PacketIndex(0)] + public VisualType VisualType { get; set; } + + [PacketIndex(1)] + public long VisualId { get; set; } + + [PacketIndex(2)] + public int VisibleTitleVnum { get; set; } + + [PacketIndex(3)] + public int EffectTitleVnum { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ServerPackets/Titles/TitlePacket.cs b/srcs/WingsAPI.Packets/ServerPackets/Titles/TitlePacket.cs new file mode 100644 index 0000000..34752bc --- /dev/null +++ b/srcs/WingsAPI.Packets/ServerPackets/Titles/TitlePacket.cs @@ -0,0 +1,15 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; + +namespace WingsEmu.Packets.ServerPackets.Titles +{ + [PacketHeader("title")] + public class TitlePacket : ServerPacket + { + [PacketIndex(0)] + public List Titles { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/ServerPackets/Titles/TitleSubPacket.cs b/srcs/WingsAPI.Packets/ServerPackets/Titles/TitleSubPacket.cs new file mode 100644 index 0000000..3369ce9 --- /dev/null +++ b/srcs/WingsAPI.Packets/ServerPackets/Titles/TitleSubPacket.cs @@ -0,0 +1,14 @@ +using WingsEmu.Packets.Enums.Titles; + +namespace WingsEmu.Packets.ServerPackets.Titles +{ + [PacketHeader("title_subPacket")] + public class TitleSubPacket : ServerPacket + { + [PacketIndex(0)] + public int ItemVnum { get; set; } + + [PacketIndex(1)] + public TitleStatus State { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/UnresolvedPacket.cs b/srcs/WingsAPI.Packets/UnresolvedPacket.cs new file mode 100644 index 0000000..c6ef2f2 --- /dev/null +++ b/srcs/WingsAPI.Packets/UnresolvedPacket.cs @@ -0,0 +1,10 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsEmu.Packets +{ + public class UnresolvedPacket : ClientPacket + { + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Packets/WingsAPI.Packets.csproj b/srcs/WingsAPI.Packets/WingsAPI.Packets.csproj new file mode 100644 index 0000000..ce6114d --- /dev/null +++ b/srcs/WingsAPI.Packets/WingsAPI.Packets.csproj @@ -0,0 +1,13 @@ + + + + net5.0 + WingsAPI.Packets + + + + + + + + diff --git a/srcs/WingsAPI.Plugins/Exceptions/CriticalPluginException.cs b/srcs/WingsAPI.Plugins/Exceptions/CriticalPluginException.cs new file mode 100644 index 0000000..d9b7fa5 --- /dev/null +++ b/srcs/WingsAPI.Plugins/Exceptions/CriticalPluginException.cs @@ -0,0 +1,16 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsAPI.Plugins.Exceptions +{ + /// + /// This exception should be thrown only if you need to stop the software + /// + public class CriticalPluginException : PluginException + { + public CriticalPluginException(IPlugin plugin, string message = "Critical Plugin Exception") : base($"[{plugin.Name}] {message}") => Plugin = plugin; + + public IPlugin Plugin { get; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Plugins/Exceptions/PluginException.cs b/srcs/WingsAPI.Plugins/Exceptions/PluginException.cs new file mode 100644 index 0000000..218ebe9 --- /dev/null +++ b/srcs/WingsAPI.Plugins/Exceptions/PluginException.cs @@ -0,0 +1,15 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; + +namespace WingsAPI.Plugins.Exceptions +{ + public class PluginException : Exception + { + public PluginException(string message) : base(message) + { + } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Plugins/Extensions/FeatureToggleExtensions.cs b/srcs/WingsAPI.Plugins/Extensions/FeatureToggleExtensions.cs new file mode 100644 index 0000000..2843bba --- /dev/null +++ b/srcs/WingsAPI.Plugins/Extensions/FeatureToggleExtensions.cs @@ -0,0 +1,91 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Reflection; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using PhoenixLib.Extensions; + +namespace WingsAPI.Plugins.Extensions +{ + public static class AssemblyExtensions + { + public static void AddTypesImplementingInterfaceInAssembly(this IServiceCollection services, Assembly assembly) + { + Type[] types = assembly.GetTypesImplementingInterface(); + foreach (Type handlerType in types) + { + services.AddTransient(handlerType); + } + } + } + + public static class FeatureToggleExtensions + { + public static void TryAddSingletonFeatureToggleEnabledByDefault(this IServiceCollection services, string envVarName) + where TInterface : class + where TImplementation : class, TInterface + { + services.TryAddSingletonFeatureToggle(envVarName, true); + } + + public static void TryAddSingletonFeatureToggleDisabledByDefault(this IServiceCollection services, string envVarName) + where TInterface : class + where TImplementation : class, TInterface + { + services.TryAddSingletonFeatureToggle(envVarName, false); + } + + public static void TryAddSingletonFeatureToggle(this IServiceCollection services, string envVarName, bool defaultActivationState) + where TInterface : class + where TImplementation : class, TInterface + + { + if (!bool.TryParse(Environment.GetEnvironmentVariable(envVarName) ?? defaultActivationState.ToString(), out bool isActivated)) + { + return; + } + + if (!isActivated) + { + return; + } + + services.TryAddSingleton(); + } + + public static void TryAddTransientFeatureToggleEnabledByDefault(this IServiceCollection services, string envVarName) + where TInterface : class + where TImplementation : class, TInterface + { + services.TryAddTransientFeatureToggle(envVarName, true); + } + + public static void TryAddTransientFeatureToggleDisabledByDefault(this IServiceCollection services, string envVarName) + where TInterface : class + where TImplementation : class, TInterface + { + services.TryAddTransientFeatureToggle(envVarName, false); + } + + public static void TryAddTransientFeatureToggle(this IServiceCollection services, string envVarName, bool defaultActivationState) + where TInterface : class + where TImplementation : class, TInterface + + { + if (!bool.TryParse(Environment.GetEnvironmentVariable(envVarName) ?? defaultActivationState.ToString(), out bool isActivated)) + { + return; + } + + if (!isActivated) + { + return; + } + + services.TryAddTransient(); + } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Plugins/GameServerLoader.cs b/srcs/WingsAPI.Plugins/GameServerLoader.cs new file mode 100644 index 0000000..c203bbd --- /dev/null +++ b/srcs/WingsAPI.Plugins/GameServerLoader.cs @@ -0,0 +1,9 @@ +using WingsAPI.Communication.ServerApi.Protocol; + +namespace WingsAPI.Plugins +{ + public class GameServerLoader + { + public GameChannelType Type { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Plugins/IDependencyInjectorPlugin.cs b/srcs/WingsAPI.Plugins/IDependencyInjectorPlugin.cs new file mode 100644 index 0000000..7cf2782 --- /dev/null +++ b/srcs/WingsAPI.Plugins/IDependencyInjectorPlugin.cs @@ -0,0 +1,20 @@ +// WingsEmu +// +// Developed by NosWings Team + +using Microsoft.Extensions.DependencyInjection; + +namespace WingsAPI.Plugins +{ + /// + /// Plugins that injects dependencies + /// + public interface IDependencyInjectorPlugin : IPlugin + { + /// + /// Loads the plugin with the given container builder to register dependencies + /// + /// + void AddDependencies(IServiceCollection services); + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Plugins/IGamePlugin.cs b/srcs/WingsAPI.Plugins/IGamePlugin.cs new file mode 100644 index 0000000..46bc8a5 --- /dev/null +++ b/srcs/WingsAPI.Plugins/IGamePlugin.cs @@ -0,0 +1,14 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsAPI.Plugins +{ + public interface IGamePlugin : IPlugin + { + /// + /// Called when this plugin is loaded but before it has been enabled + /// + void OnLoad(); + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Plugins/IGameServerPlugin.cs b/srcs/WingsAPI.Plugins/IGameServerPlugin.cs new file mode 100644 index 0000000..95f3a60 --- /dev/null +++ b/srcs/WingsAPI.Plugins/IGameServerPlugin.cs @@ -0,0 +1,9 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace WingsAPI.Plugins +{ + public interface IGameServerPlugin : IPlugin + { + void AddDependencies(IServiceCollection services, GameServerLoader gameServer); + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Plugins/IPlugin.cs b/srcs/WingsAPI.Plugins/IPlugin.cs new file mode 100644 index 0000000..be01edd --- /dev/null +++ b/srcs/WingsAPI.Plugins/IPlugin.cs @@ -0,0 +1,14 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsAPI.Plugins +{ + public interface IPlugin + { + /// + /// Name of the plugin + /// + string Name { get; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Plugins/IPluginManager.cs b/srcs/WingsAPI.Plugins/IPluginManager.cs new file mode 100644 index 0000000..b42ed04 --- /dev/null +++ b/srcs/WingsAPI.Plugins/IPluginManager.cs @@ -0,0 +1,14 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.IO; + +namespace WingsAPI.Plugins +{ + public interface IPluginManager + { + IPlugin[] LoadPlugin(FileInfo file); + IPlugin[] LoadPlugins(DirectoryInfo directory); + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Plugins/IPluginPathConfigurationProvider.cs b/srcs/WingsAPI.Plugins/IPluginPathConfigurationProvider.cs new file mode 100644 index 0000000..e3a3e20 --- /dev/null +++ b/srcs/WingsAPI.Plugins/IPluginPathConfigurationProvider.cs @@ -0,0 +1,11 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace WingsAPI.Plugins +{ + public interface IPluginPathConfigurationProvider + { + string PluginsPath { get; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Plugins/PluginPathConfigurationProvider.cs b/srcs/WingsAPI.Plugins/PluginPathConfigurationProvider.cs new file mode 100644 index 0000000..93c92fa --- /dev/null +++ b/srcs/WingsAPI.Plugins/PluginPathConfigurationProvider.cs @@ -0,0 +1,9 @@ +namespace WingsAPI.Plugins +{ + public class PluginPathConfigurationProvider : IPluginPathConfigurationProvider + { + public PluginPathConfigurationProvider(string pluginsPath) => PluginsPath = pluginsPath; + + public string PluginsPath { get; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Plugins/WingsAPI.Plugins.csproj b/srcs/WingsAPI.Plugins/WingsAPI.Plugins.csproj new file mode 100644 index 0000000..408696f --- /dev/null +++ b/srcs/WingsAPI.Plugins/WingsAPI.Plugins.csproj @@ -0,0 +1,17 @@ + + + + net5.0 + + + + + + + + + + + + + diff --git a/srcs/WingsAPI.Scripting.LUA/Converter/Converter.cs b/srcs/WingsAPI.Scripting.LUA/Converter/Converter.cs new file mode 100644 index 0000000..bd6e2c5 --- /dev/null +++ b/srcs/WingsAPI.Scripting.LUA/Converter/Converter.cs @@ -0,0 +1,54 @@ +using System; +using MoonSharp.Interpreter; + +namespace WingsAPI.Scripting.LUA.Converter +{ + /// + /// Converter used to convert CSharp object to Lua object & Lua object to CSharp object + /// + public interface IConverter + { + /// + /// Type of lua data + /// + DataType DataType { get; } + + /// + /// Type of the object created by this converted + /// + Type ObjectType { get; } + + /// + /// Create lua object from csharp object + /// + /// Script used to create this object + /// Value to convert + /// Converted value + DynValue ToLuaObject(Script script, object value); + + /// + /// Create csharp object from lua object + /// + /// Lua object to convert + /// Converted value + object ToCSharpObject(DynValue value); + } + + public abstract class Converter : IConverter + { + /// + /// Empty object used to get correct properties name using nameof(Object.MyProperty) + /// + protected static readonly T Object = Activator.CreateInstance(); + + public virtual DataType DataType { get; } = DataType.Table; + public Type ObjectType { get; } = typeof(T); + + public DynValue ToLuaObject(Script script, object value) => ToLuaObject(script, (T)value); + + object IConverter.ToCSharpObject(DynValue value) => ToCSharpObject(value); + + protected abstract DynValue ToLuaObject(Script script, T value); + protected abstract T ToCSharpObject(DynValue value); + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting.LUA/Converter/GuidConverter.cs b/srcs/WingsAPI.Scripting.LUA/Converter/GuidConverter.cs new file mode 100644 index 0000000..7a95f8d --- /dev/null +++ b/srcs/WingsAPI.Scripting.LUA/Converter/GuidConverter.cs @@ -0,0 +1,14 @@ +using System; +using MoonSharp.Interpreter; + +namespace WingsAPI.Scripting.LUA.Converter +{ + public class GuidConverter : Converter + { + public override DataType DataType { get; } = DataType.String; + + protected override DynValue ToLuaObject(Script script, Guid value) => DynValue.NewString(value.ToString()); + + protected override Guid ToCSharpObject(DynValue value) => Guid.Parse(value.ToObject()); + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting.LUA/Converter/SEventConverter.cs b/srcs/WingsAPI.Scripting.LUA/Converter/SEventConverter.cs new file mode 100644 index 0000000..bf3d1ae --- /dev/null +++ b/srcs/WingsAPI.Scripting.LUA/Converter/SEventConverter.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using MoonSharp.Interpreter; +using WingsAPI.Scripting.Attribute; +using WingsAPI.Scripting.Event; +using WingsAPI.Scripting.LUA.Extension; + +namespace WingsAPI.Scripting.LUA.Converter +{ + public class SEventConverter : Converter + { + private static readonly Dictionary TypeByEventName = new(); + + static SEventConverter() + { + foreach (Type type in typeof(SEvent).Assembly.GetTypes()) + { + if (!typeof(SEvent).IsAssignableFrom(type)) + { + continue; + } + + if (type.IsAbstract || type.IsInterface) + { + continue; + } + + ScriptEventAttribute attribute = type.GetCustomAttribute(); + if (attribute == null) + { + throw new InvalidOperationException($"Missing ScriptEvent attribute on {type.Name}"); + } + + TypeByEventName[attribute.Name] = type; + } + } + + protected override DynValue ToLuaObject(Script script, SEvent value) => throw new NotImplementedException(); + + protected override SEvent ToCSharpObject(DynValue value) + { + Table table = value.Table; + + string eventName = table.GetValue("Name"); + DynValue parameters = table.Get("Parameters"); + + Type eventType = TypeByEventName.GetValueOrDefault(eventName); + if (eventType == null) + { + throw new NotImplementedException($"Event {eventName} is not yet implemented"); + } + + return (SEvent)parameters.ToObject(eventType); + } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting.LUA/Converter/SMapObjectConverter.cs b/srcs/WingsAPI.Scripting.LUA/Converter/SMapObjectConverter.cs new file mode 100644 index 0000000..cf90529 --- /dev/null +++ b/srcs/WingsAPI.Scripting.LUA/Converter/SMapObjectConverter.cs @@ -0,0 +1,29 @@ +using System; +using MoonSharp.Interpreter; +using WingsAPI.Scripting.Enum; +using WingsAPI.Scripting.LUA.Extension; +using WingsAPI.Scripting.Object.Common.Map; + +namespace WingsAPI.Scripting.LUA.Converter.Object.Common.Map +{ + public class SMapObjectConverter : Converter + { + protected override DynValue ToLuaObject(Script script, SMapObject value) => throw new NotImplementedException(); + + protected override SMapObject ToCSharpObject(DynValue value) + { + Table table = value.Table; + + MapObjectType type = table.GetValue("ObjectType"); + switch (type) + { + case MapObjectType.Button: + return table.GetValue("Parameters"); + case MapObjectType.Item: + return table.GetValue("Parameters"); + } + + throw new InvalidOperationException(); + } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting.LUA/Extension/AssemblyExtensions.cs b/srcs/WingsAPI.Scripting.LUA/Extension/AssemblyExtensions.cs new file mode 100644 index 0000000..d114d10 --- /dev/null +++ b/srcs/WingsAPI.Scripting.LUA/Extension/AssemblyExtensions.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace WingsAPI.Scripting.LUA.Extension +{ + public static class AssemblyExtensions + { + public static IEnumerable GetTypesWithAttribute(this Assembly assembly) where T : System.Attribute + { + return assembly.GetTypes().Where(x => x.GetCustomAttribute() != null); + } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting.LUA/Extension/DynValueExtension.cs b/srcs/WingsAPI.Scripting.LUA/Extension/DynValueExtension.cs new file mode 100644 index 0000000..7e286c6 --- /dev/null +++ b/srcs/WingsAPI.Scripting.LUA/Extension/DynValueExtension.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using MoonSharp.Interpreter; + +namespace WingsAPI.Scripting.LUA.Extension +{ + public static class DynValueExtension + { + public static T GetValue(this Table table, string key) => table.Get(key).ToObject(); + + public static IEnumerable GetValues(this Table table, string key) => table.Get(key).ToObject>(); + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting.LUA/LuaScriptFactory.cs b/srcs/WingsAPI.Scripting.LUA/LuaScriptFactory.cs new file mode 100644 index 0000000..e4f0ed5 --- /dev/null +++ b/srcs/WingsAPI.Scripting.LUA/LuaScriptFactory.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using MoonSharp.Interpreter; +using MoonSharp.Interpreter.Loaders; +using PhoenixLib.Logging; +using WingsAPI.Scripting.Attribute; +using WingsAPI.Scripting.LUA.Converter; +using WingsAPI.Scripting.LUA.Converter.Object.Common.Map; +using WingsAPI.Scripting.LUA.Extension; + +namespace WingsAPI.Scripting.LUA +{ + public class LuaScriptFactory : IScriptFactory + { + /// + /// Specific converter + /// + private static readonly IConverter[] SpecialConverters = + { + new SEventConverter(), + new GuidConverter(), + new SMapObjectConverter() + }; + + private readonly ScriptFactoryConfiguration _configuration; + + public LuaScriptFactory(ScriptFactoryConfiguration configuration) + { + _configuration = configuration; + + RegisterAllScriptingObjectsInAssembly(typeof(IScriptFactory).Assembly); + + foreach (IConverter converter in SpecialConverters) + { + RegisterConverter(converter); + } + } + + public void RegisterAllScriptingObjectsInAssembly(Assembly assembly) + { + IEnumerable scriptObjects = assembly.GetTypesWithAttribute(); + foreach (Type scriptObject in scriptObjects) + { + RegisterType(scriptObject); + } + } + + public void RegisterType() + { + RegisterType(typeof(T)); + } + + public T LoadScript(string name) + { + if (!File.Exists(name)) + { + throw new IOException($"Couldn't find file {name}"); + } + + var script = new Script + { + Options = + { + DebugPrint = Log.Debug, + ScriptLoader = new FileSystemScriptLoader + { + ModulePaths = new[] + { + $"{_configuration.LibDirectory}/?.lua", + $"{_configuration.LibDirectory}/enum/?.lua" + } + } + } + }; + + DynValue scriptReturn = script.DoFile(name); + return scriptReturn.ToObject(); + } + + public void RegisterConverter(IConverter converter) + { + Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(converter.DataType, converter.ObjectType, converter.ToCSharpObject); + Script.GlobalOptions.CustomConverters.SetClrToScriptCustomConversion(converter.ObjectType, converter.ToLuaObject); + } + + private void RegisterType(Type scriptObject) + { + Log.Debug($"[LUA_SCRIPT_FACTORY] {scriptObject.Name} registered"); + + UserData.RegisterType(scriptObject); + + Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.Table, scriptObject, value => ObjectFactory.ToCSharpObject(scriptObject, value)); + Script.GlobalOptions.CustomConverters.SetClrToScriptCustomConversion(scriptObject, ObjectFactory.ToLuaObject); + } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting.LUA/ObjectFactory.cs b/srcs/WingsAPI.Scripting.LUA/ObjectFactory.cs new file mode 100644 index 0000000..1cf28eb --- /dev/null +++ b/srcs/WingsAPI.Scripting.LUA/ObjectFactory.cs @@ -0,0 +1,43 @@ +using System; +using System.Reflection; +using MoonSharp.Interpreter; +using MoonSharp.Interpreter.Serialization.Json; +using PhoenixLib.Logging; + +namespace WingsAPI.Scripting.LUA +{ + public static class ObjectFactory + { + public static object ToCSharpObject(Type type, DynValue value) + { + object obj = Activator.CreateInstance(type); + foreach (PropertyInfo property in type.GetProperties()) + { + try + { + DynValue tableValue = value.Table.Get(property.Name); + object clrValue = tableValue.ToObject(property.PropertyType); + property.SetValue(obj, clrValue); + } + catch (Exception e) + { + Log.Error($"[LUA_MAPPING] type: {type.Name} | {property.Name} could not be mapped: {value.Table.Get(property.Name).Table.TableToJson()}", e); + throw; + } + } + + return obj; + } + + public static DynValue ToLuaObject(Script script, object value) + { + var table = new Table(script); + foreach (PropertyInfo property in value.GetType().GetProperties()) + { + table[property.Name] = DynValue.FromObject(script, property.GetValue(value)); + } + + return DynValue.NewTable(table); + } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting.LUA/ScriptFactoryConfiguration.cs b/srcs/WingsAPI.Scripting.LUA/ScriptFactoryConfiguration.cs new file mode 100644 index 0000000..a0627df --- /dev/null +++ b/srcs/WingsAPI.Scripting.LUA/ScriptFactoryConfiguration.cs @@ -0,0 +1,14 @@ +using System.IO; + +namespace WingsAPI.Scripting.LUA +{ + public class ScriptFactoryConfiguration + { + public string LibDirectory { get; set; } + public string RootDirectory { get; set; } + public string RaidsDirectory => Path.Combine(RootDirectory, "raids"); + public string DungeonsDirectory => Path.Combine(RootDirectory, "dungeons"); + public string TimeSpacesDirectory => Path.Combine(RootDirectory, "timespaces"); + public string MissionsSystemDirectory => Path.Combine(RootDirectory, "mission-system"); + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting.LUA/WingsAPI.Scripting.LUA.csproj b/srcs/WingsAPI.Scripting.LUA/WingsAPI.Scripting.LUA.csproj new file mode 100644 index 0000000..87c778f --- /dev/null +++ b/srcs/WingsAPI.Scripting.LUA/WingsAPI.Scripting.LUA.csproj @@ -0,0 +1,17 @@ + + + + net5.0 + + + + + + + + + + + + + diff --git a/srcs/WingsAPI.Scripting/Attribute/ScriptEventAttribute.cs b/srcs/WingsAPI.Scripting/Attribute/ScriptEventAttribute.cs new file mode 100644 index 0000000..4ecc243 --- /dev/null +++ b/srcs/WingsAPI.Scripting/Attribute/ScriptEventAttribute.cs @@ -0,0 +1,18 @@ +using System; + +namespace WingsAPI.Scripting.Attribute +{ + [AttributeUsage(AttributeTargets.Class)] + public class ScriptEventAttribute : System.Attribute + { + public ScriptEventAttribute(string name, bool isRemovedOnTrigger) + { + IsRemovedOnTrigger = isRemovedOnTrigger; + Name = name; + } + + public string Name { get; } + + public bool IsRemovedOnTrigger { get; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Attribute/ScriptObjectAttribute.cs b/srcs/WingsAPI.Scripting/Attribute/ScriptObjectAttribute.cs new file mode 100644 index 0000000..47a1a9b --- /dev/null +++ b/srcs/WingsAPI.Scripting/Attribute/ScriptObjectAttribute.cs @@ -0,0 +1,9 @@ +using System; + +namespace WingsAPI.Scripting.Attribute +{ + [AttributeUsage(AttributeTargets.Class)] + public class ScriptObjectAttribute : System.Attribute + { + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Converter/SRemovePortalEventConverter.cs b/srcs/WingsAPI.Scripting/Converter/SRemovePortalEventConverter.cs new file mode 100644 index 0000000..a97892f --- /dev/null +++ b/srcs/WingsAPI.Scripting/Converter/SRemovePortalEventConverter.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using PhoenixLib.Events; +using WingsAPI.Scripting.Event.Common; +using WingsEmu.Game; +using WingsEmu.Game.Maps.Event; + +namespace WingsAPI.Scripting.Converter +{ + public class SRemovePortalEventConverter : ScriptedEventConverter + { + private readonly Dictionary _portals; + + public SRemovePortalEventConverter(Dictionary portals) => _portals = portals; + + protected override IAsyncEvent Convert(SRemovePortalEvent e) => + new PortalRemoveEvent + { + Portal = _portals[e.Portal.Id] + }; + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Converter/SThrowRaidDropsEventConverter.cs b/srcs/WingsAPI.Scripting/Converter/SThrowRaidDropsEventConverter.cs new file mode 100644 index 0000000..9f8e95d --- /dev/null +++ b/srcs/WingsAPI.Scripting/Converter/SThrowRaidDropsEventConverter.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using PhoenixLib.Events; +using WingsAPI.Scripting.Event.Raid; +using WingsEmu.Core; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Raids.Events; + +namespace WingsAPI.Scripting.Converter +{ + public class SThrowRaidDropsEventConverter : ScriptedEventConverter + { + private readonly Dictionary monsters; + + public SThrowRaidDropsEventConverter(Dictionary monsters) => this.monsters = monsters; + + protected override IAsyncEvent Convert(SThrowRaidDropsEvent e) + { + return new RaidMonsterThrowEvent(monsters[e.BossId], e.Drops.Select(x => new Drop + { + ItemVNum = x.ItemVnum, + Amount = x.Amount + }).ToList(), e.DropsStackCount, new Range + { + Minimum = e.GoldRange.Minimum, + Maximum = e.GoldRange.Maximum + }, e.GoldStackCount); + } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Converter/ScriptedEventConverter.cs b/srcs/WingsAPI.Scripting/Converter/ScriptedEventConverter.cs new file mode 100644 index 0000000..80d6d81 --- /dev/null +++ b/srcs/WingsAPI.Scripting/Converter/ScriptedEventConverter.cs @@ -0,0 +1,19 @@ +using System; +using PhoenixLib.Events; +using WingsAPI.Scripting.Event; + +namespace WingsAPI.Scripting.Converter +{ + public interface IScriptedEventConverter + { + Type EventType { get; } + IAsyncEvent Convert(SEvent e); + } + + public abstract class ScriptedEventConverter : IScriptedEventConverter where T : SEvent + { + public Type EventType { get; } = typeof(T); + public IAsyncEvent Convert(SEvent e) => Convert((T)e); + protected abstract IAsyncEvent Convert(T e); + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Enum/Dungeon/SDungeonType.cs b/srcs/WingsAPI.Scripting/Enum/Dungeon/SDungeonType.cs new file mode 100644 index 0000000..ff76e50 --- /dev/null +++ b/srcs/WingsAPI.Scripting/Enum/Dungeon/SDungeonType.cs @@ -0,0 +1,10 @@ +namespace WingsAPI.Scripting.Enum.Dungeon +{ + public enum SDungeonType : byte + { + Morcos = 1, + Hatus = 2, + Calvinas = 3, + Berios = 4 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Enum/MapObjectType.cs b/srcs/WingsAPI.Scripting/Enum/MapObjectType.cs new file mode 100644 index 0000000..b68f9e6 --- /dev/null +++ b/srcs/WingsAPI.Scripting/Enum/MapObjectType.cs @@ -0,0 +1,8 @@ +namespace WingsAPI.Scripting.Enum +{ + public enum MapObjectType + { + Button, + Item + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Enum/Raid/SRaidFinishType.cs b/srcs/WingsAPI.Scripting/Enum/Raid/SRaidFinishType.cs new file mode 100644 index 0000000..a6656c5 --- /dev/null +++ b/srcs/WingsAPI.Scripting/Enum/Raid/SRaidFinishType.cs @@ -0,0 +1,7 @@ +namespace WingsAPI.Scripting.Enum.Raid +{ + public enum SRaidFinishType + { + MissionCompleted = 0 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Enum/Raid/SRaidType.cs b/srcs/WingsAPI.Scripting/Enum/Raid/SRaidType.cs new file mode 100644 index 0000000..aa74bf1 --- /dev/null +++ b/srcs/WingsAPI.Scripting/Enum/Raid/SRaidType.cs @@ -0,0 +1,46 @@ +namespace WingsAPI.Scripting.Enum.Raid +{ + /// + /// Type of raid + /// + public enum SRaidType : byte + { + Cuby = 0, + + Ginseng = 1, + Castra = 2, + GiantBlackSpider = 3, + Slade = 4, + ChickenKing = 5, + Namaju = 6, + Grasslin = 7, + Snowman = 8, + RobberGang = 9, + + JackOLantern = 10, + ChickenQueen = 11, + Pirate = 12, + Kertos = 13, + Valakus = 14, + Grenigas = 15, + LordDraco = 16, + Glacerus = 17, + Foxy = 18, + Maru = 19, + + Laurena = 20, + HongbiCheongbi = 21, + LolaLopears = 22, + Zenas = 23, + Erenia = 24, + Fernon = 25, + Fafnir = 26, + Yertirand = 27, + MadProffesor = 28, + MadMarchHare = 29, + + Kirollas = 30, + Carno = 31, + Belial = 32 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Enum/SMapFlags.cs b/srcs/WingsAPI.Scripting/Enum/SMapFlags.cs new file mode 100644 index 0000000..e0349de --- /dev/null +++ b/srcs/WingsAPI.Scripting/Enum/SMapFlags.cs @@ -0,0 +1,42 @@ +namespace WingsAPI.Scripting.Enum +{ + public enum SMapFlags + { + /* ACTS */ + ACT_1, + ACT_2, + ACT_3, + ACT_4, + ACT_5_1, + ACT_5_2, + ACT_6_1, + ACT_6_2, + ACT_7, + + /* FACTION */ + ANGEL_SIDE = 30, + DEMON_SIDE, + + NOSVILLE = 40, + PORT_ALVEUS, + + /* TYPES */ + IS_BASE_MAP = 50, + IS_MINILAND_MAP, + + /* FLAGS */ + HAS_PVP_ENABLED = 100, + HAS_PVP_FACTION_ENABLED, + HAS_PVP_FAMILY_ENABLED, + HAS_USER_SHOPS_DISABLED, + HAS_DROP_DIRECTLY_IN_INVENTORY_ENABLED, + HAS_CHAMPION_EXPERIENCE_ENABLED, + HAS_SEALED_VESSELS_DISABLED, + HAS_RAID_TEAM_SUMMON_STONE_ENABLED, + HAS_SIGNPOSTS_ENABLED, + + /* DEBUFFS */ + HAS_IMMUNITY_ON_MAP_CHANGE_ENABLED = 200, + HAS_BURNING_SWORD_ENABLED + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Enum/SMapType.cs b/srcs/WingsAPI.Scripting/Enum/SMapType.cs new file mode 100644 index 0000000..ee0bcd3 --- /dev/null +++ b/srcs/WingsAPI.Scripting/Enum/SMapType.cs @@ -0,0 +1,8 @@ +namespace WingsAPI.Scripting.Enum +{ + public enum SMapType + { + MapId, + MapVNum + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Enum/SObjectiveType.cs b/srcs/WingsAPI.Scripting/Enum/SObjectiveType.cs new file mode 100644 index 0000000..179c4b0 --- /dev/null +++ b/srcs/WingsAPI.Scripting/Enum/SObjectiveType.cs @@ -0,0 +1,11 @@ +namespace WingsAPI.Scripting.Enum +{ + /// + /// Type of objective + /// + public enum SObjectiveType + { + Monster, + Button + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Enum/SPortalType.cs b/srcs/WingsAPI.Scripting/Enum/SPortalType.cs new file mode 100644 index 0000000..4aaf453 --- /dev/null +++ b/srcs/WingsAPI.Scripting/Enum/SPortalType.cs @@ -0,0 +1,9 @@ +namespace WingsAPI.Scripting.Enum +{ + public enum SPortalType + { + TsNormal = 0, + Locked = 1, + Open = 2 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Enum/TimeSpace/SPortalMinimapOrientation.cs b/srcs/WingsAPI.Scripting/Enum/TimeSpace/SPortalMinimapOrientation.cs new file mode 100644 index 0000000..36958bb --- /dev/null +++ b/srcs/WingsAPI.Scripting/Enum/TimeSpace/SPortalMinimapOrientation.cs @@ -0,0 +1,10 @@ +namespace WingsAPI.Scripting.Enum.TimeSpace +{ + public enum SPortalMinimapOrientation + { + North = 0, + East = 1, + South = 2, + West = 3 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Enum/TimeSpace/STimeSpaceFinishType.cs b/srcs/WingsAPI.Scripting/Enum/TimeSpace/STimeSpaceFinishType.cs new file mode 100644 index 0000000..d93fd7d --- /dev/null +++ b/srcs/WingsAPI.Scripting/Enum/TimeSpace/STimeSpaceFinishType.cs @@ -0,0 +1,12 @@ +namespace WingsAPI.Scripting.Enum.TimeSpace +{ + public enum STimeSpaceFinishType + { + TIME_IS_UP = 1, + NPC_DIED = 2, + OUT_OF_LIVES = 3, + TEAM_MEMBER_OUT_OF_LIVES = 4, + SUCCESS = 5, + SUCCESS_HIGH_SCORE = 6 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Enum/TimeSpace/STimeSpaceTaskType.cs b/srcs/WingsAPI.Scripting/Enum/TimeSpace/STimeSpaceTaskType.cs new file mode 100644 index 0000000..9515258 --- /dev/null +++ b/srcs/WingsAPI.Scripting/Enum/TimeSpace/STimeSpaceTaskType.cs @@ -0,0 +1,9 @@ +namespace WingsAPI.Scripting.Enum.TimeSpace +{ + public enum STimeSpaceTaskType + { + None = 0, + KillAllMonsters = 1, + Survive = 2 + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Event/Common/SMonsterSummonEvent.cs b/srcs/WingsAPI.Scripting/Event/Common/SMonsterSummonEvent.cs new file mode 100644 index 0000000..28670ca --- /dev/null +++ b/srcs/WingsAPI.Scripting/Event/Common/SMonsterSummonEvent.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using WingsAPI.Scripting.Attribute; +using WingsAPI.Scripting.Object.Common; + +namespace WingsAPI.Scripting.Event.Common +{ + /// + /// Object represention of MonsterSummonEvent + /// + [ScriptEvent("MonsterSummon", true)] + public class SMonsterSummonEvent : SEvent + { + /// + /// Monster who will be summoned + /// + public IEnumerable Monsters { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Event/Common/SOpenPortalEvent.cs b/srcs/WingsAPI.Scripting/Event/Common/SOpenPortalEvent.cs new file mode 100644 index 0000000..a4b8cde --- /dev/null +++ b/srcs/WingsAPI.Scripting/Event/Common/SOpenPortalEvent.cs @@ -0,0 +1,11 @@ +using WingsAPI.Scripting.Attribute; +using WingsAPI.Scripting.Object.Common.Map; + +namespace WingsAPI.Scripting.Event.Common +{ + [ScriptEvent("OpenPortal", true)] + public class SOpenPortalEvent : SEvent + { + public SPortal Portal { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Event/Common/SRemovePortalEvent.cs b/srcs/WingsAPI.Scripting/Event/Common/SRemovePortalEvent.cs new file mode 100644 index 0000000..1c58dc1 --- /dev/null +++ b/srcs/WingsAPI.Scripting/Event/Common/SRemovePortalEvent.cs @@ -0,0 +1,12 @@ +using WingsAPI.Scripting.Attribute; +using WingsAPI.Scripting.Object.Common.Map; + +namespace WingsAPI.Scripting.Event.Common +{ + [ScriptEvent(Name, true)] + public class SRemovePortalEvent : SEvent + { + public const string Name = "RemovePortal"; + public SPortal Portal { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Event/Common/STeleportMembersEvent.cs b/srcs/WingsAPI.Scripting/Event/Common/STeleportMembersEvent.cs new file mode 100644 index 0000000..06d2fdf --- /dev/null +++ b/srcs/WingsAPI.Scripting/Event/Common/STeleportMembersEvent.cs @@ -0,0 +1,15 @@ +using System; +using WingsAPI.Scripting.Attribute; +using WingsAPI.Scripting.Object.Common; + +namespace WingsAPI.Scripting.Event.Common +{ + [ScriptEvent("Teleport", false)] + public class STeleportMembersEvent : SEvent + { + public Guid MapInstanceId { get; set; } + public SPosition SourcePosition { get; set; } + public SPosition DestinationPosition { get; set; } + public byte Range { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Event/Dungeon/SAct4DungeonRewardEvent.cs b/srcs/WingsAPI.Scripting/Event/Dungeon/SAct4DungeonRewardEvent.cs new file mode 100644 index 0000000..e13d397 --- /dev/null +++ b/srcs/WingsAPI.Scripting/Event/Dungeon/SAct4DungeonRewardEvent.cs @@ -0,0 +1,9 @@ +using WingsAPI.Scripting.Attribute; + +namespace WingsAPI.Scripting.Event.Dungeon +{ + [ScriptEvent("DungeonRewardEvent", true)] + public class SAct4DungeonRewardEvent : SEvent + { + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Event/Raid/SFinishRaidEvent.cs b/srcs/WingsAPI.Scripting/Event/Raid/SFinishRaidEvent.cs new file mode 100644 index 0000000..70d960a --- /dev/null +++ b/srcs/WingsAPI.Scripting/Event/Raid/SFinishRaidEvent.cs @@ -0,0 +1,11 @@ +using WingsAPI.Scripting.Attribute; +using WingsAPI.Scripting.Enum.Raid; + +namespace WingsAPI.Scripting.Event.Raid +{ + [ScriptEvent("FinishRaid", true)] + public class SFinishRaidEvent : SEvent + { + public SRaidFinishType FinishType { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Event/Raid/SRaidIncreaseObjectiveEvent.cs b/srcs/WingsAPI.Scripting/Event/Raid/SRaidIncreaseObjectiveEvent.cs new file mode 100644 index 0000000..dc7195d --- /dev/null +++ b/srcs/WingsAPI.Scripting/Event/Raid/SRaidIncreaseObjectiveEvent.cs @@ -0,0 +1,17 @@ +using WingsAPI.Scripting.Attribute; +using WingsAPI.Scripting.Enum; + +namespace WingsAPI.Scripting.Event.Raid +{ + /// + /// Object representation of IncreaseObjectiveEvent + /// + [ScriptEvent("RaidIncreaseObjective", true)] + public class SRaidIncreaseObjectiveEvent : SEvent + { + /// + /// Type of objective increased + /// + public SObjectiveType ObjectiveType { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Event/Raid/SThrowRaidDropsEvent.cs b/srcs/WingsAPI.Scripting/Event/Raid/SThrowRaidDropsEvent.cs new file mode 100644 index 0000000..9c3fab3 --- /dev/null +++ b/srcs/WingsAPI.Scripting/Event/Raid/SThrowRaidDropsEvent.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using WingsAPI.Scripting.Attribute; +using WingsAPI.Scripting.Object.Common; + +namespace WingsAPI.Scripting.Event.Raid +{ + [ScriptEvent("ThrowRaidDrops", true)] + public class SThrowRaidDropsEvent : SEvent + { + public Guid BossId { get; set; } + public IEnumerable Drops { get; set; } + public byte DropsStackCount { get; set; } + public SRange GoldRange { get; set; } + public byte GoldStackCount { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Event/SEvent.cs b/srcs/WingsAPI.Scripting/Event/SEvent.cs new file mode 100644 index 0000000..bf9eb26 --- /dev/null +++ b/srcs/WingsAPI.Scripting/Event/SEvent.cs @@ -0,0 +1,12 @@ +using WingsAPI.Scripting.Attribute; + +namespace WingsAPI.Scripting.Event +{ + /// + /// Object used to represent an Event in a script + /// + [ScriptObject] + public abstract class SEvent + { + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Event/TimeSpace/SAddTimeEvent.cs b/srcs/WingsAPI.Scripting/Event/TimeSpace/SAddTimeEvent.cs new file mode 100644 index 0000000..b89c2f3 --- /dev/null +++ b/srcs/WingsAPI.Scripting/Event/TimeSpace/SAddTimeEvent.cs @@ -0,0 +1,10 @@ +using WingsAPI.Scripting.Attribute; + +namespace WingsAPI.Scripting.Event.TimeSpace +{ + [ScriptEvent("AddTime", true)] + public class SAddTimeEvent : SEvent + { + public int Time { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Event/TimeSpace/SCheckForTasksCompletedEvent.cs b/srcs/WingsAPI.Scripting/Event/TimeSpace/SCheckForTasksCompletedEvent.cs new file mode 100644 index 0000000..e7a0a83 --- /dev/null +++ b/srcs/WingsAPI.Scripting/Event/TimeSpace/SCheckForTasksCompletedEvent.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using WingsAPI.Scripting.Attribute; + +namespace WingsAPI.Scripting.Event.TimeSpace +{ + [ScriptEvent("CheckForTasksCompleted", false)] + public class SCheckForTasksCompletedEvent : SEvent + { + public IEnumerable Maps { get; set; } + public IEnumerable Events { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Event/TimeSpace/SClosePortal.cs b/srcs/WingsAPI.Scripting/Event/TimeSpace/SClosePortal.cs new file mode 100644 index 0000000..0242a09 --- /dev/null +++ b/srcs/WingsAPI.Scripting/Event/TimeSpace/SClosePortal.cs @@ -0,0 +1,11 @@ +using WingsAPI.Scripting.Attribute; +using WingsAPI.Scripting.Object.Common.Map; + +namespace WingsAPI.Scripting.Event.TimeSpace +{ + [ScriptEvent("ClosePortal", true)] + public class SClosePortal : SEvent + { + public SPortal Portal { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Event/TimeSpace/SDespawnAllMobsInRoomEvent.cs b/srcs/WingsAPI.Scripting/Event/TimeSpace/SDespawnAllMobsInRoomEvent.cs new file mode 100644 index 0000000..3022542 --- /dev/null +++ b/srcs/WingsAPI.Scripting/Event/TimeSpace/SDespawnAllMobsInRoomEvent.cs @@ -0,0 +1,11 @@ +using System; +using WingsAPI.Scripting.Attribute; + +namespace WingsAPI.Scripting.Event.TimeSpace +{ + [ScriptEvent("DespawnAllMobsInRoom", true)] + public class SDespawnAllMobsInRoomEvent : SEvent + { + public Guid Map { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Event/TimeSpace/SRemoveItemsEvent.cs b/srcs/WingsAPI.Scripting/Event/TimeSpace/SRemoveItemsEvent.cs new file mode 100644 index 0000000..f86535e --- /dev/null +++ b/srcs/WingsAPI.Scripting/Event/TimeSpace/SRemoveItemsEvent.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using WingsAPI.Scripting.Attribute; + +namespace WingsAPI.Scripting.Event.TimeSpace +{ + [ScriptEvent("RemoveItems", true)] + public class SRemoveItemsEvent : SEvent + { + public IEnumerable Items { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Event/TimeSpace/STimeSpaceFinishEvent.cs b/srcs/WingsAPI.Scripting/Event/TimeSpace/STimeSpaceFinishEvent.cs new file mode 100644 index 0000000..5d6fa5b --- /dev/null +++ b/srcs/WingsAPI.Scripting/Event/TimeSpace/STimeSpaceFinishEvent.cs @@ -0,0 +1,11 @@ +using WingsAPI.Scripting.Attribute; +using WingsAPI.Scripting.Enum.TimeSpace; + +namespace WingsAPI.Scripting.Event.TimeSpace +{ + [ScriptEvent("FinishTimeSpace", true)] + public class STimeSpaceFinishEvent : SEvent + { + public STimeSpaceFinishType TimeSpaceFinishType { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Event/TimeSpace/STogglePortalEvent.cs b/srcs/WingsAPI.Scripting/Event/TimeSpace/STogglePortalEvent.cs new file mode 100644 index 0000000..6e186f5 --- /dev/null +++ b/srcs/WingsAPI.Scripting/Event/TimeSpace/STogglePortalEvent.cs @@ -0,0 +1,11 @@ +using WingsAPI.Scripting.Attribute; +using WingsAPI.Scripting.Object.Common.Map; + +namespace WingsAPI.Scripting.Event.TimeSpace +{ + [ScriptEvent("TogglePortal", false)] + public class STogglePortalEvent : SEvent + { + public SPortal Portal { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Event/TimeSpace/STryStartTaskEvent.cs b/srcs/WingsAPI.Scripting/Event/TimeSpace/STryStartTaskEvent.cs new file mode 100644 index 0000000..44b22dc --- /dev/null +++ b/srcs/WingsAPI.Scripting/Event/TimeSpace/STryStartTaskEvent.cs @@ -0,0 +1,11 @@ +using System; +using WingsAPI.Scripting.Attribute; + +namespace WingsAPI.Scripting.Event.TimeSpace +{ + [ScriptEvent("TryStartTaskForMap", true)] + public class STryStartTaskEvent : SEvent + { + public Guid MapId { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Event/TimeSpace/ScriptSetTimeEvent.cs b/srcs/WingsAPI.Scripting/Event/TimeSpace/ScriptSetTimeEvent.cs new file mode 100644 index 0000000..1e4dedb --- /dev/null +++ b/srcs/WingsAPI.Scripting/Event/TimeSpace/ScriptSetTimeEvent.cs @@ -0,0 +1,10 @@ +using WingsAPI.Scripting.Attribute; + +namespace WingsAPI.Scripting.Event.TimeSpace +{ + [ScriptEvent("SetTime", true)] + public class ScriptSetTimeEvent : SEvent + { + public int Time { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/IScriptFactory.cs b/srcs/WingsAPI.Scripting/IScriptFactory.cs new file mode 100644 index 0000000..68a337b --- /dev/null +++ b/srcs/WingsAPI.Scripting/IScriptFactory.cs @@ -0,0 +1,21 @@ +using System.Reflection; + +namespace WingsAPI.Scripting +{ + /// + /// Script factory used to instantiate script of defined type + /// + /// Type of the script created by this factory + public interface IScriptFactory + { + void RegisterAllScriptingObjectsInAssembly(Assembly assembly); + void RegisterType(); + + /// + /// Create a new script from file + /// + /// Path to the file + /// Script created + T LoadScript(string path); + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/InvalidScriptException.cs b/srcs/WingsAPI.Scripting/InvalidScriptException.cs new file mode 100644 index 0000000..21bab12 --- /dev/null +++ b/srcs/WingsAPI.Scripting/InvalidScriptException.cs @@ -0,0 +1,11 @@ +using System; + +namespace WingsAPI.Scripting +{ + public class InvalidScriptException : Exception + { + public InvalidScriptException(string message) : base(message) + { + } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Object/Common/Map/SButton.cs b/srcs/WingsAPI.Scripting/Object/Common/Map/SButton.cs new file mode 100644 index 0000000..de526cb --- /dev/null +++ b/srcs/WingsAPI.Scripting/Object/Common/Map/SButton.cs @@ -0,0 +1,29 @@ +using WingsAPI.Scripting.Attribute; + +namespace WingsAPI.Scripting.Object.Common.Map +{ + /// + /// Object used to represent a button in a script + /// + [ScriptObject] + public class SButton : SMapObject + { + /// + /// Vnum of button when it's activated + /// + public short ActivatedVnum { get; set; } + + /// + /// Vnum of button when it's deactivated + /// + public short DeactivatedVnum { get; set; } + + public bool IsObjective { get; set; } + + public bool IsRandomPosition { get; set; } + + public bool OnlyOnce { get; set; } + + public int? CustomDanceDuration { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Object/Common/Map/SItem.cs b/srcs/WingsAPI.Scripting/Object/Common/Map/SItem.cs new file mode 100644 index 0000000..505ba70 --- /dev/null +++ b/srcs/WingsAPI.Scripting/Object/Common/Map/SItem.cs @@ -0,0 +1,14 @@ +using WingsAPI.Scripting.Attribute; + +namespace WingsAPI.Scripting.Object.Common.Map +{ + [ScriptObject] + public class SItem : SMapObject + { + public short Vnum { get; set; } + public bool IsObjective { get; set; } + public bool IsRandomPosition { get; set; } + public bool IsRandomUniquePosition { get; set; } + public int? DanceDuration { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Object/Common/Map/SMapObject.cs b/srcs/WingsAPI.Scripting/Object/Common/Map/SMapObject.cs new file mode 100644 index 0000000..e456ee7 --- /dev/null +++ b/srcs/WingsAPI.Scripting/Object/Common/Map/SMapObject.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using WingsAPI.Scripting.Attribute; +using WingsAPI.Scripting.Event; + +namespace WingsAPI.Scripting.Object.Common.Map +{ + [ScriptObject] + public class SMapObject + { + /// + /// Randomly generated id + /// + public Guid Id { get; set; } + + /// + /// Position of this object in the map + /// + public SPosition Position { get; set; } + + public IDictionary> Events { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Object/Common/Map/SMonsterWave.cs b/srcs/WingsAPI.Scripting/Object/Common/Map/SMonsterWave.cs new file mode 100644 index 0000000..e9ccd3b --- /dev/null +++ b/srcs/WingsAPI.Scripting/Object/Common/Map/SMonsterWave.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using WingsAPI.Scripting.Attribute; + +namespace WingsAPI.Scripting.Object.Common.Map +{ + [ScriptObject] + public class SMonsterWave + { + public short TimeInSeconds { get; set; } + + public IEnumerable Monsters { get; set; } + + public bool Loop { get; set; } + + public short? LoopTick { get; set; } + + public bool IsScaledWithPlayerAmount { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Object/Common/Map/SPortal.cs b/srcs/WingsAPI.Scripting/Object/Common/Map/SPortal.cs new file mode 100644 index 0000000..4c98d70 --- /dev/null +++ b/srcs/WingsAPI.Scripting/Object/Common/Map/SPortal.cs @@ -0,0 +1,47 @@ +using System; +using WingsAPI.Scripting.Attribute; +using WingsAPI.Scripting.Enum; +using WingsAPI.Scripting.Enum.TimeSpace; + +namespace WingsAPI.Scripting.Object.Common.Map +{ + /// + /// Object used to represent a portal in a script + /// + [ScriptObject] + public class SPortal + { + /// + /// Randomly generated id + /// + public Guid Id { get; set; } + + /// + /// Id of the source map + /// + public Guid SourceId { get; set; } + + /// + /// Id of the destination map + /// + public Guid DestinationId { get; set; } + + /// + /// Position of the portal in the source map + /// + public SPosition SourcePosition { get; set; } + + /// + /// Position where you're teleported in destination map + /// + public SPosition DestinationPosition { get; set; } + + public SPortalType Type { get; set; } + + public int? CreationDelay { get; set; } + + public bool IsReturn { get; set; } + + public SPortalMinimapOrientation PortalMiniMapOrientation { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Object/Common/SDrop.cs b/srcs/WingsAPI.Scripting/Object/Common/SDrop.cs new file mode 100644 index 0000000..2a49644 --- /dev/null +++ b/srcs/WingsAPI.Scripting/Object/Common/SDrop.cs @@ -0,0 +1,19 @@ +using WingsAPI.Scripting.Attribute; + +namespace WingsAPI.Scripting.Object.Common +{ + [ScriptObject] + public class SDrop + { + public short ItemVnum { get; set; } + public int Amount { get; set; } + } + + [ScriptObject] + public class SDropChance + { + public int Chance { get; set; } + public short ItemVnum { get; set; } + public int Amount { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Object/Common/SLocation.cs b/srcs/WingsAPI.Scripting/Object/Common/SLocation.cs new file mode 100644 index 0000000..d28b83a --- /dev/null +++ b/srcs/WingsAPI.Scripting/Object/Common/SLocation.cs @@ -0,0 +1,12 @@ +using System; +using WingsAPI.Scripting.Attribute; + +namespace WingsAPI.Scripting.Object.Common +{ + [ScriptObject] + public class SLocation + { + public Guid MapId { get; set; } + public SPosition Position { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Object/Common/SMap.cs b/srcs/WingsAPI.Scripting/Object/Common/SMap.cs new file mode 100644 index 0000000..a961dbe --- /dev/null +++ b/srcs/WingsAPI.Scripting/Object/Common/SMap.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using WingsAPI.Scripting.Attribute; +using WingsAPI.Scripting.Enum; +using WingsAPI.Scripting.Event; +using WingsAPI.Scripting.Object.Common.Map; +using WingsAPI.Scripting.Object.Timespace; + +namespace WingsAPI.Scripting.Object.Common +{ + /// + /// Object used to represent a map in a script + /// + [ScriptObject] + public sealed class SMap + { + /// + /// Randomly generated id + /// + public Guid Id { get; set; } + + public SMapType MapType { get; set; } + + /// + /// Vnum or Id of the map + /// + public int MapIdVnum { get; set; } + + public int NameId { get; set; } + + public int MusicId { get; set; } + + /// + /// Used for TimeSpace minimap + /// + public byte MapIndexX { get; set; } + + /// + /// Used for TimeSpace minimap + /// + public byte MapIndexY { get; set; } + + /// + /// Contains all the flags needed to define a map via VNum + /// + public IEnumerable Flags { get; set; } + + /// + /// Contains all monsters who need to be spawned in this map + /// + public IEnumerable Monsters { get; set; } + + public IEnumerable Npcs { get; set; } + + /// + /// Contains all buttons who need to be added to this map + /// + public IEnumerable Objects { get; set; } + + /// + /// Contains all portals who need to be added to this map + /// + public IEnumerable Portals { get; set; } + + public IDictionary> Events { get; set; } + + /// + /// Spawn x monsters every y seconds + /// + public IEnumerable MonsterWaves { get; set; } + + public STimeSpaceTask TimeSpaceTask { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Object/Common/SMapNpc.cs b/srcs/WingsAPI.Scripting/Object/Common/SMapNpc.cs new file mode 100644 index 0000000..1e03b4a --- /dev/null +++ b/srcs/WingsAPI.Scripting/Object/Common/SMapNpc.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using WingsAPI.Scripting.Attribute; +using WingsAPI.Scripting.Event; + +namespace WingsAPI.Scripting.Object.Common +{ + [ScriptObject] + public class SMapNpc + { + public Guid Id { get; set; } + + public short Vnum { get; set; } + + public SPosition Position { get; set; } + + public bool CanMove { get; set; } + + public bool IsProtectedNpc { get; set; } + + public bool FollowPlayer { get; set; } + + public byte Direction { get; set; } + + public float? HpMultiplier { get; set; } + + public float? MpMultiplier { get; set; } + + public byte? CustomLevel { get; set; } + + public IDictionary> Events { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Object/Common/SMonster.cs b/srcs/WingsAPI.Scripting/Object/Common/SMonster.cs new file mode 100644 index 0000000..ee6163f --- /dev/null +++ b/srcs/WingsAPI.Scripting/Object/Common/SMonster.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using WingsAPI.Scripting.Attribute; +using WingsAPI.Scripting.Event; +using WingsAPI.Scripting.Object.Raid; + +namespace WingsAPI.Scripting.Object.Common +{ + /// + /// Object used to represent a monster in a script + /// + [ScriptObject] + public class SMonster + { + /// + /// Randomly generated id + /// + public Guid Id { get; set; } + + /// + /// Vnum of monster + /// + public short Vnum { get; set; } + + /// + /// Position where this monster will be spawned + /// + public SPosition Position { get; set; } + + public bool IsRandomPosition { get; set; } + + /// + /// Define if monster is a boss or not + /// + public bool IsBoss { get; set; } + + /// + /// Define if monster can move or not + /// + public bool CanMove { get; set; } + + /// + /// Define if monster is a target + /// + public bool IsTarget { get; set; } + + /// + /// Define if monster should walk to boss position + /// + public SPosition GoToBossPosition { get; set; } + + public IEnumerable Drop { get; set; } + + public IDictionary> Events { get; set; } + + public bool SpawnAfterTask { get; set; } + + public int SpawnAfterMobs { get; set; } + + public byte Direction { get; set; } + + public byte? CustomLevel { get; set; } + + public float? HpMultiplier { get; set; } + + public float? MpMultiplier { get; set; } + + public string AtAroundMobId { get; set; } + + public byte? AtAroundMobRange { get; set; } + + public IEnumerable Waypoints { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Object/Common/SPosition.cs b/srcs/WingsAPI.Scripting/Object/Common/SPosition.cs new file mode 100644 index 0000000..b11fbe6 --- /dev/null +++ b/srcs/WingsAPI.Scripting/Object/Common/SPosition.cs @@ -0,0 +1,21 @@ +using WingsAPI.Scripting.Attribute; + +namespace WingsAPI.Scripting.Object.Common +{ + /// + /// Object used to represent a position in a script + /// + [ScriptObject] + public class SPosition + { + /// + /// Position on X axis + /// + public short X { get; set; } + + /// + /// Position on Y axis + /// + public short Y { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Object/Common/SRange.cs b/srcs/WingsAPI.Scripting/Object/Common/SRange.cs new file mode 100644 index 0000000..a5a05e0 --- /dev/null +++ b/srcs/WingsAPI.Scripting/Object/Common/SRange.cs @@ -0,0 +1,11 @@ +using WingsAPI.Scripting.Attribute; + +namespace WingsAPI.Scripting.Object.Common +{ + [ScriptObject] + public class SRange + { + public int Minimum { get; set; } + public int Maximum { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Object/Dungeon/SDungeon.cs b/srcs/WingsAPI.Scripting/Object/Dungeon/SDungeon.cs new file mode 100644 index 0000000..d8f7cb3 --- /dev/null +++ b/srcs/WingsAPI.Scripting/Object/Dungeon/SDungeon.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using WingsAPI.Scripting.Attribute; +using WingsAPI.Scripting.Enum.Dungeon; +using WingsAPI.Scripting.Object.Common; +using WingsAPI.Scripting.Object.Raid; + +namespace WingsAPI.Scripting.Object.Dungeon +{ + [ScriptObject] + public class SDungeon + { + /// + /// The type of Dungeon + /// + public SDungeonType DungeonType { get; set; } + + /// + /// Maps in this raid + /// + public IEnumerable Maps { get; set; } + + /// + /// Spawn point of this raid + /// + public SLocation Spawn { get; set; } + + /// + /// Raid Rewards + /// + public SRaidReward Reward { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Object/Raid/SRaid.cs b/srcs/WingsAPI.Scripting/Object/Raid/SRaid.cs new file mode 100644 index 0000000..c286100 --- /dev/null +++ b/srcs/WingsAPI.Scripting/Object/Raid/SRaid.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using WingsAPI.Scripting.Attribute; +using WingsAPI.Scripting.Enum.Raid; +using WingsAPI.Scripting.Object.Common; + +namespace WingsAPI.Scripting.Object.Raid +{ + /// + /// Object used to represent a raid in a script + /// + [ScriptObject] + public class SRaid + { + /// + /// Randomly generated id + /// + public Guid Id { get; set; } + + /// + /// Type of raid + /// + public SRaidType RaidType { get; set; } + + /// + /// Requirements of the raid + /// + public SRaidRequirement Requirement { get; set; } + + /// + /// Maps in this raid + /// + public IEnumerable Maps { get; set; } + + /// + /// Spawn point of this raid + /// + public SLocation Spawn { get; set; } + + /// + /// Raid Rewards + /// + public SRaidReward Reward { get; set; } + + public int DurationInSeconds { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Object/Raid/SRaidBox.cs b/srcs/WingsAPI.Scripting/Object/Raid/SRaidBox.cs new file mode 100644 index 0000000..5f6c3dd --- /dev/null +++ b/srcs/WingsAPI.Scripting/Object/Raid/SRaidBox.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using WingsAPI.Scripting.Attribute; + +namespace WingsAPI.Scripting.Object.Raid +{ + [ScriptObject] + public class SRaidBox + { + public int RewardBox { get; set; } + + public IEnumerable RaidBoxRarity { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Object/Raid/SRaidBoxRarity.cs b/srcs/WingsAPI.Scripting/Object/Raid/SRaidBoxRarity.cs new file mode 100644 index 0000000..be3477b --- /dev/null +++ b/srcs/WingsAPI.Scripting/Object/Raid/SRaidBoxRarity.cs @@ -0,0 +1,11 @@ +using WingsAPI.Scripting.Attribute; + +namespace WingsAPI.Scripting.Object.Raid +{ + [ScriptObject] + public class SRaidBoxRarity + { + public byte Rarity { get; set; } + public int Chance { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Object/Raid/SRaidRequirement.cs b/srcs/WingsAPI.Scripting/Object/Raid/SRaidRequirement.cs new file mode 100644 index 0000000..7f0556f --- /dev/null +++ b/srcs/WingsAPI.Scripting/Object/Raid/SRaidRequirement.cs @@ -0,0 +1,15 @@ +using WingsAPI.Scripting.Attribute; + +namespace WingsAPI.Scripting.Object.Raid +{ + [ScriptObject] + public class SRaidRequirement + { + public byte MinimumLevel { get; set; } + public byte MaximumLevel { get; set; } + public byte MinimumHeroLevel { get; set; } + public byte MaximumHeroLevel { get; set; } + public byte MinimumParticipant { get; set; } + public byte MaximumParticipant { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Object/Raid/SRaidReward.cs b/srcs/WingsAPI.Scripting/Object/Raid/SRaidReward.cs new file mode 100644 index 0000000..1361fcb --- /dev/null +++ b/srcs/WingsAPI.Scripting/Object/Raid/SRaidReward.cs @@ -0,0 +1,12 @@ +using WingsAPI.Scripting.Attribute; + +namespace WingsAPI.Scripting.Object.Raid +{ + [ScriptObject] + public class SRaidReward + { + public SRaidBox RaidBox { get; set; } + public bool DefaultReputation { get; set; } + public int? FixedReputation { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Object/Raid/SWaypoint.cs b/srcs/WingsAPI.Scripting/Object/Raid/SWaypoint.cs new file mode 100644 index 0000000..1cfb422 --- /dev/null +++ b/srcs/WingsAPI.Scripting/Object/Raid/SWaypoint.cs @@ -0,0 +1,12 @@ +using WingsAPI.Scripting.Attribute; + +namespace WingsAPI.Scripting.Object.Raid +{ + [ScriptObject] + public class SWaypoint + { + public short X { get; set; } + public short Y { get; set; } + public int WaitTime { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Object/Timespace/STimeSpaceObject.cs b/srcs/WingsAPI.Scripting/Object/Timespace/STimeSpaceObject.cs new file mode 100644 index 0000000..4ae4cee --- /dev/null +++ b/srcs/WingsAPI.Scripting/Object/Timespace/STimeSpaceObject.cs @@ -0,0 +1,53 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Generic; +using WingsAPI.Scripting.Attribute; +using WingsAPI.Scripting.Object.Common; + +namespace WingsAPI.Scripting.Object.Timespace +{ + [ScriptObject] + public class ScriptTimeSpace + { + /// + /// Randomly generated id + /// + public Guid Id { get; set; } + + /// + /// + public int TimeSpaceId { get; set; } + + public STimeSpaceObjective Objectives { get; set; } + + /// + /// Maps of the timespace + /// + public IEnumerable Maps { get; set; } + + /// + /// Spawn point of this timespace + /// + public SLocation Spawn { get; set; } + + /// + /// Duration of the timespace + /// + public int DurationInSeconds { get; set; } + + public byte Lives { get; set; } + + public int BonusPointItemDropChance { get; set; } + + public int? PreFinishDialog { get; set; } + + public bool PreFinishDialogIsObjective { get; set; } + + public short? ObtainablePartnerVnum { get; set; } + + public bool InfiniteDuration { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Object/Timespace/STimeSpaceObjective.cs b/srcs/WingsAPI.Scripting/Object/Timespace/STimeSpaceObjective.cs new file mode 100644 index 0000000..c4b089b --- /dev/null +++ b/srcs/WingsAPI.Scripting/Object/Timespace/STimeSpaceObjective.cs @@ -0,0 +1,23 @@ +using WingsAPI.Scripting.Attribute; + +namespace WingsAPI.Scripting.Object.Timespace +{ + [ScriptObject] + public class STimeSpaceObjective + { + public bool KillAllMonsters { get; set; } + public bool GoToExit { get; set; } + public bool ProtectNPC { get; set; } + + public short? KillMonsterVnum { get; set; } + public short? KillMonsterAmount { get; set; } + + public short? CollectItemVnum { get; set; } + public short? CollectItemAmount { get; set; } + + public byte? Conversation { get; set; } + + public short? InteractObjectsVnum { get; set; } + public short? InteractObjectsAmount { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Object/Timespace/STimeSpaceRequirementObject.cs b/srcs/WingsAPI.Scripting/Object/Timespace/STimeSpaceRequirementObject.cs new file mode 100644 index 0000000..a55337f --- /dev/null +++ b/srcs/WingsAPI.Scripting/Object/Timespace/STimeSpaceRequirementObject.cs @@ -0,0 +1,16 @@ +using WingsAPI.Scripting.Attribute; + +namespace WingsAPI.Scripting.Object.Timespace +{ + [ScriptObject] + public class STimeSpaceRequirementObject + { + public int MinimumLevel { get; set; } + public int MaximumLevel { get; set; } + public int MinimumHeroLevel { get; set; } + public int MaximumHeroLevel { get; set; } + public int MinimumParticipant { get; set; } + public int MaximumParticipant { get; set; } + public short SeedOfPowerCost { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Object/Timespace/STimeSpaceRewardsObject.cs b/srcs/WingsAPI.Scripting/Object/Timespace/STimeSpaceRewardsObject.cs new file mode 100644 index 0000000..8db49ab --- /dev/null +++ b/srcs/WingsAPI.Scripting/Object/Timespace/STimeSpaceRewardsObject.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using WingsAPI.Scripting.Attribute; + +namespace WingsAPI.Scripting.Object.Timespace +{ + [ScriptObject] + public class STimeSpaceRewardsObject + { + public int? FixedReputation { get; set; } + public int? ReputationLevelMultiplier { get; set; } + public bool DefaultReputation { get; set; } + + public IEnumerable DrawRewards { get; set; } + public IEnumerable SpecialRewards { get; set; } + public IEnumerable BonusRewards { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Object/Timespace/STimeSpaceTask.cs b/srcs/WingsAPI.Scripting/Object/Timespace/STimeSpaceTask.cs new file mode 100644 index 0000000..aa6ba8f --- /dev/null +++ b/srcs/WingsAPI.Scripting/Object/Timespace/STimeSpaceTask.cs @@ -0,0 +1,22 @@ +using WingsAPI.Scripting.Attribute; +using WingsAPI.Scripting.Enum.TimeSpace; + +namespace WingsAPI.Scripting.Object.Timespace +{ + [ScriptObject] + public class STimeSpaceTask + { + public STimeSpaceTaskType TimeSpaceTaskType { get; set; } + public string GameDialogKey { get; set; } + public short? DurationInSeconds { get; set; } + + public int? StartDialog { get; set; } + public bool DialogStartTask { get; set; } + public int? EndDialog { get; set; } + public string StartDialogShout { get; set; } + public string EndDialogShout { get; set; } + + public bool StartDialogIsObjective { get; set; } + public bool EndDialogIsObjective { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Object/Timespace/STimespaceItemReward.cs b/srcs/WingsAPI.Scripting/Object/Timespace/STimespaceItemReward.cs new file mode 100644 index 0000000..b37d66d --- /dev/null +++ b/srcs/WingsAPI.Scripting/Object/Timespace/STimespaceItemReward.cs @@ -0,0 +1,11 @@ +using WingsAPI.Scripting.Attribute; + +namespace WingsAPI.Scripting.Object.Timespace +{ + [ScriptObject] + public class STimespaceItemReward + { + public int ItemVnum { get; set; } + public int Quantity { get; set; } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Object/Timespace/TimespaceConstEventKeys.cs b/srcs/WingsAPI.Scripting/Object/Timespace/TimespaceConstEventKeys.cs new file mode 100644 index 0000000..118f898 --- /dev/null +++ b/srcs/WingsAPI.Scripting/Object/Timespace/TimespaceConstEventKeys.cs @@ -0,0 +1,15 @@ +namespace WingsAPI.Scripting.Object.Timespace +{ + public static class TimespaceConstEventKeys + { + public const string OnTaskFinish = "OnTaskFinish"; + public const string OnTaskFail = "OnTaskFail"; + public const string OnMapJoin = "OnMapJoin"; + public const string OnAllTargetMobsDead = "OnAllTargetMobsDead"; + public const string ButtonSwitched = "Switched"; + public const string ButtonTriggered = "Triggered"; + public const string ObjectivesCompleted = "ObjectivesCompleted"; + public const string FinishTimeSpace = "FinishTimeSpace"; + public const string PickedUp = "PickedUp"; + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/ScriptManager/IDungeonScriptManager.cs b/srcs/WingsAPI.Scripting/ScriptManager/IDungeonScriptManager.cs new file mode 100644 index 0000000..6518111 --- /dev/null +++ b/srcs/WingsAPI.Scripting/ScriptManager/IDungeonScriptManager.cs @@ -0,0 +1,11 @@ +using WingsAPI.Scripting.Enum.Dungeon; +using WingsAPI.Scripting.Object.Dungeon; + +namespace WingsAPI.Scripting.ScriptManager +{ + public interface IDungeonScriptManager + { + SDungeon GetScriptedDungeon(SDungeonType raidType); + void Load(); + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/ScriptManager/IRaidScriptManager.cs b/srcs/WingsAPI.Scripting/ScriptManager/IRaidScriptManager.cs new file mode 100644 index 0000000..136d9e9 --- /dev/null +++ b/srcs/WingsAPI.Scripting/ScriptManager/IRaidScriptManager.cs @@ -0,0 +1,11 @@ +using WingsAPI.Scripting.Enum.Raid; +using WingsAPI.Scripting.Object.Raid; + +namespace WingsAPI.Scripting.ScriptManager +{ + public interface IRaidScriptManager + { + SRaid GetScriptedRaid(SRaidType raidType); + void Load(); + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/ScriptManager/ITimeSpaceScriptManager.cs b/srcs/WingsAPI.Scripting/ScriptManager/ITimeSpaceScriptManager.cs new file mode 100644 index 0000000..376daa7 --- /dev/null +++ b/srcs/WingsAPI.Scripting/ScriptManager/ITimeSpaceScriptManager.cs @@ -0,0 +1,10 @@ +using WingsAPI.Scripting.Object.Timespace; + +namespace WingsAPI.Scripting.ScriptManager +{ + public interface ITimeSpaceScriptManager + { + void Load(); + ScriptTimeSpace GetScriptedTimeSpace(long id); + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Validator/Common/Map/SButtonValidator.cs b/srcs/WingsAPI.Scripting/Validator/Common/Map/SButtonValidator.cs new file mode 100644 index 0000000..c52944f --- /dev/null +++ b/srcs/WingsAPI.Scripting/Validator/Common/Map/SButtonValidator.cs @@ -0,0 +1,18 @@ +using FluentValidation; +using WingsAPI.Scripting.Object.Common.Map; +using WingsEmu.Game.Managers.StaticData; + +namespace WingsAPI.Scripting.Validator.Common.Map +{ + public class SButtonValidator : AbstractValidator + { + public SButtonValidator(IItemsManager manager) + { + RuleFor(x => x.Id).NotEmpty(); + RuleFor(x => x.Position).NotNull(); + RuleFor(x => x.ActivatedVnum).NotEmpty().SetValidator(new ItemVnumValidator(manager)); + RuleFor(x => x.DeactivatedVnum).NotEmpty().SetValidator(new ItemVnumValidator(manager)); + RuleFor(x => x.Events).NotNull(); + } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Validator/Common/Map/SMapObjectValidator.cs b/srcs/WingsAPI.Scripting/Validator/Common/Map/SMapObjectValidator.cs new file mode 100644 index 0000000..497f5e5 --- /dev/null +++ b/srcs/WingsAPI.Scripting/Validator/Common/Map/SMapObjectValidator.cs @@ -0,0 +1,13 @@ +using FluentValidation; +using WingsAPI.Scripting.Object.Common.Map; +using WingsEmu.Game.Managers.StaticData; + +namespace WingsAPI.Scripting.Validator.Common.Map +{ + public class SMapObjectValidator : AbstractValidator + { + public SMapObjectValidator(IItemsManager itemsManager) + { + } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Validator/Common/Map/SPortalValidator.cs b/srcs/WingsAPI.Scripting/Validator/Common/Map/SPortalValidator.cs new file mode 100644 index 0000000..271425c --- /dev/null +++ b/srcs/WingsAPI.Scripting/Validator/Common/Map/SPortalValidator.cs @@ -0,0 +1,9 @@ +using FluentValidation; +using WingsAPI.Scripting.Object.Common.Map; + +namespace WingsAPI.Scripting.Validator.Common.Map +{ + public class SPortalValidator : AbstractValidator + { + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Validator/Common/SEventsValidator.cs b/srcs/WingsAPI.Scripting/Validator/Common/SEventsValidator.cs new file mode 100644 index 0000000..e3e78c7 --- /dev/null +++ b/srcs/WingsAPI.Scripting/Validator/Common/SEventsValidator.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using FluentValidation; +using WingsAPI.Scripting.Event; + +namespace WingsAPI.Scripting.Validator.Common +{ + public class SEventsValidator : AbstractValidator>> + { + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Validator/Common/SLocationValidator.cs b/srcs/WingsAPI.Scripting/Validator/Common/SLocationValidator.cs new file mode 100644 index 0000000..1f89911 --- /dev/null +++ b/srcs/WingsAPI.Scripting/Validator/Common/SLocationValidator.cs @@ -0,0 +1,9 @@ +using FluentValidation; +using WingsAPI.Scripting.Object.Common; + +namespace WingsAPI.Scripting.Validator.Common +{ + public class SLocationValidator : AbstractValidator + { + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Validator/Common/SMapValidator.cs b/srcs/WingsAPI.Scripting/Validator/Common/SMapValidator.cs new file mode 100644 index 0000000..f2484a3 --- /dev/null +++ b/srcs/WingsAPI.Scripting/Validator/Common/SMapValidator.cs @@ -0,0 +1,21 @@ +using FluentValidation; +using WingsAPI.Scripting.Object.Common; +using WingsAPI.Scripting.Validator.Common.Map; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Maps; + +namespace WingsAPI.Scripting.Validator.Common +{ + public class SMapValidator : AbstractValidator + { + public SMapValidator(IMapManager mapManager, INpcMonsterManager npcManager, IItemsManager itemsManager) + { + RuleFor(x => x.Id).NotEmpty(); + RuleFor(x => x.MapIdVnum).SetValidator(new MapVnumValidator(mapManager)); + + RuleForEach(x => x.Monsters).SetValidator(new SMonsterValidator(npcManager)); + RuleForEach(x => x.Objects).SetValidator(new SMapObjectValidator(itemsManager)); + RuleForEach(x => x.Portals).SetValidator(new SPortalValidator()); + } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Validator/Common/SMonsterValidator.cs b/srcs/WingsAPI.Scripting/Validator/Common/SMonsterValidator.cs new file mode 100644 index 0000000..cd9d485 --- /dev/null +++ b/srcs/WingsAPI.Scripting/Validator/Common/SMonsterValidator.cs @@ -0,0 +1,17 @@ +using FluentValidation; +using WingsAPI.Scripting.Object.Common; +using WingsEmu.Game.Managers.StaticData; + +namespace WingsAPI.Scripting.Validator.Common +{ + public class SMonsterValidator : AbstractValidator + { + public SMonsterValidator(INpcMonsterManager manager) + { + RuleFor(x => x.Id).NotEmpty(); + RuleFor(x => x.Position).NotNull(); + RuleFor(x => x.Vnum).SetValidator(new MonsterVnumValidator(manager)); + RuleFor(x => x.Events).NotNull(); + } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Validator/ItemVnumValidator.cs b/srcs/WingsAPI.Scripting/Validator/ItemVnumValidator.cs new file mode 100644 index 0000000..0d00d9f --- /dev/null +++ b/srcs/WingsAPI.Scripting/Validator/ItemVnumValidator.cs @@ -0,0 +1,13 @@ +using FluentValidation; +using WingsEmu.Game.Managers.StaticData; + +namespace WingsAPI.Scripting.Validator +{ + public class ItemVnumValidator : AbstractValidator + { + public ItemVnumValidator(IItemsManager manager) + { + RuleFor(x => x).Must(x => manager.GetItem(x) != null); + } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Validator/MapVnumValidator.cs b/srcs/WingsAPI.Scripting/Validator/MapVnumValidator.cs new file mode 100644 index 0000000..9c8549a --- /dev/null +++ b/srcs/WingsAPI.Scripting/Validator/MapVnumValidator.cs @@ -0,0 +1,13 @@ +using FluentValidation; +using WingsEmu.Game.Maps; + +namespace WingsAPI.Scripting.Validator +{ + public class MapVnumValidator : AbstractValidator + { + public MapVnumValidator(IMapManager manager) + { + RuleFor(x => x).Must(x => manager.GetMapByMapId(x) != null); + } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/Validator/MonsterVnumValidator.cs b/srcs/WingsAPI.Scripting/Validator/MonsterVnumValidator.cs new file mode 100644 index 0000000..128fe99 --- /dev/null +++ b/srcs/WingsAPI.Scripting/Validator/MonsterVnumValidator.cs @@ -0,0 +1,13 @@ +using FluentValidation; +using WingsEmu.Game.Managers.StaticData; + +namespace WingsAPI.Scripting.Validator +{ + public class MonsterVnumValidator : AbstractValidator + { + public MonsterVnumValidator(INpcMonsterManager manager) + { + RuleFor(x => x).Must(x => manager.GetNpc(x) != null); + } + } +} \ No newline at end of file diff --git a/srcs/WingsAPI.Scripting/WingsAPI.Scripting.csproj b/srcs/WingsAPI.Scripting/WingsAPI.Scripting.csproj new file mode 100644 index 0000000..abd028e --- /dev/null +++ b/srcs/WingsAPI.Scripting/WingsAPI.Scripting.csproj @@ -0,0 +1,16 @@ + + + + net5.0 + + + + + + + + + + + + diff --git a/srcs/WingsEmu.Communication.gRPC/Extensions/ServiceProviderExtensions.cs b/srcs/WingsEmu.Communication.gRPC/Extensions/ServiceProviderExtensions.cs new file mode 100644 index 0000000..625594a --- /dev/null +++ b/srcs/WingsEmu.Communication.gRPC/Extensions/ServiceProviderExtensions.cs @@ -0,0 +1,133 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using Grpc.Net.Client; +using Microsoft.Extensions.DependencyInjection; +using ProtoBuf.Grpc.Client; +using WingsAPI.Communication.Bazaar; +using WingsAPI.Communication.DbServer.AccountService; +using WingsAPI.Communication.DbServer.CharacterService; +using WingsAPI.Communication.DbServer.TimeSpaceService; +using WingsAPI.Communication.DbServer.WarehouseService; +using WingsAPI.Communication.Families; +using WingsAPI.Communication.Families.Warehouse; +using WingsAPI.Communication.Mail; +using WingsAPI.Communication.Player; +using WingsAPI.Communication.Relation; +using WingsAPI.Communication.ServerApi; +using WingsAPI.Communication.Services; +using WingsAPI.Communication.Sessions; +using WingsAPI.Communication.Translations; + +namespace WingsEmu.Communication.gRPC.Extensions +{ + public static class ServiceEnvironmentConsts + { + public const string MASTER_IP = "MASTER_IP"; + public const string MASTER_PORT = "MASTER_PORT"; + + public const string BAZAAR_SERVER_IP = "BAZAAR_SERVER_IP"; + public const string BAZAAR_SERVER_PORT = "BAZAAR_SERVER_PORT"; + + public const string RELATION_SERVER_IP = "RELATION_SERVER_IP"; + public const string RELATION_SERVER_PORT = "RELATION_SERVER_PORT"; + + public const string FAMILY_SERVER_IP = "FAMILY_SERVER_IP"; + public const string FAMILY_SERVER_PORT = "FAMILY_SERVER_PORT"; + + public const string MAIL_SERVER_IP = "MAIL_SERVER_IP"; + public const string MAIL_SERVER_PORT = "MAIL_SERVER_PORT"; + + public const string DB_SERVER_IP = "DB_SERVER_IP"; + public const string DB_SERVER_PORT = "DB_SERVER_PORT"; + + public const string TRANSLATION_SERVER_IP = "TRANSLATIONS_SERVER_IP"; + public const string TRANSLATION_SERVER_PORT = "TRANSLATIONS_SERVER_PORT"; + } + + public static class ServiceProviderExtensions + { + private const string DEFAULT_IP = "localhost"; + private const string DEFAULT_PORT = "20500"; + + private static void AddGrpcService(this IServiceCollection services, string ipEnvironmentVariable, string portEnvironmentVariable, string portDefault = null) where T : class + { + services.AddScoped(s => + { + string ip = Environment.GetEnvironmentVariable(ipEnvironmentVariable) ?? DEFAULT_IP; + int port = Convert.ToInt32(Environment.GetEnvironmentVariable(portEnvironmentVariable) ?? portDefault ?? DEFAULT_PORT); + var options = new GrpcChannelOptions + { + MaxReceiveMessageSize = null, + MaxSendMessageSize = null + }; + T generatedService = GrpcChannel.ForAddress($"http://{ip}:{port}", options).CreateGrpcService(); + return generatedService; + }); + } + + public static void AddServerApiServiceClient(this IServiceCollection services) + { + services.AddGrpcService(ServiceEnvironmentConsts.MASTER_IP, ServiceEnvironmentConsts.MASTER_PORT); + } + + public static void AddGrpcSessionServiceClient(this IServiceCollection services) + { + services.AddGrpcService(ServiceEnvironmentConsts.MASTER_IP, ServiceEnvironmentConsts.MASTER_PORT); + } + + public static void AddGrpcClusterStatusServiceClient(this IServiceCollection services) + { + services.AddGrpcService(ServiceEnvironmentConsts.MASTER_IP, ServiceEnvironmentConsts.MASTER_PORT); + } + + public static void AddClusterCharacterServiceClient(this IServiceCollection services) + { + services.AddGrpcService(ServiceEnvironmentConsts.MASTER_IP, ServiceEnvironmentConsts.MASTER_PORT); + } + + public static void AddTranslationsGrpcClient(this IServiceCollection services) + { + const string defaultPort = "19999"; + services.AddGrpcService(ServiceEnvironmentConsts.TRANSLATION_SERVER_IP, ServiceEnvironmentConsts.TRANSLATION_SERVER_PORT, defaultPort); + } + + public static void AddGrpcRelationServiceClient(this IServiceCollection services) + { + const string defaultPort = "21111"; + services.AddGrpcService(ServiceEnvironmentConsts.RELATION_SERVER_IP, ServiceEnvironmentConsts.RELATION_SERVER_PORT, defaultPort); + } + + public static void AddGrpcBazaarServiceClient(this IServiceCollection services) + { + const string defaultPort = "25555"; + services.AddGrpcService(ServiceEnvironmentConsts.BAZAAR_SERVER_IP, ServiceEnvironmentConsts.BAZAAR_SERVER_PORT, defaultPort); + } + + public static void AddGrpcFamilyServiceClient(this IServiceCollection services) + { + const string defaultPort = "26666"; + services.AddGrpcService(ServiceEnvironmentConsts.FAMILY_SERVER_IP, ServiceEnvironmentConsts.FAMILY_SERVER_PORT, defaultPort); + services.AddGrpcService(ServiceEnvironmentConsts.FAMILY_SERVER_IP, ServiceEnvironmentConsts.FAMILY_SERVER_PORT, defaultPort); + services.AddGrpcService(ServiceEnvironmentConsts.FAMILY_SERVER_IP, ServiceEnvironmentConsts.FAMILY_SERVER_PORT, defaultPort); + } + + public static void AddGrpcMailServiceClient(this IServiceCollection services) + { + const string defaultPort = "27777"; + services.AddGrpcService(ServiceEnvironmentConsts.MAIL_SERVER_IP, ServiceEnvironmentConsts.MAIL_SERVER_PORT, defaultPort); + services.AddGrpcService(ServiceEnvironmentConsts.MAIL_SERVER_IP, ServiceEnvironmentConsts.MAIL_SERVER_PORT, defaultPort); + } + + public static void AddGrpcDbServerServiceClient(this IServiceCollection services) + { + const string defaultPort = "29999"; + services.AddGrpcService(ServiceEnvironmentConsts.DB_SERVER_IP, ServiceEnvironmentConsts.DB_SERVER_PORT, defaultPort); + services.AddGrpcService(ServiceEnvironmentConsts.DB_SERVER_IP, ServiceEnvironmentConsts.DB_SERVER_PORT, defaultPort); + services.AddGrpcService(ServiceEnvironmentConsts.DB_SERVER_IP, ServiceEnvironmentConsts.DB_SERVER_PORT, defaultPort); + services.AddGrpcService(ServiceEnvironmentConsts.DB_SERVER_IP, ServiceEnvironmentConsts.DB_SERVER_PORT, defaultPort); + } + } +} \ No newline at end of file diff --git a/srcs/WingsEmu.Communication.gRPC/WingsEmu.Communication.gRPC.csproj b/srcs/WingsEmu.Communication.gRPC/WingsEmu.Communication.gRPC.csproj new file mode 100644 index 0000000..5e7a5a9 --- /dev/null +++ b/srcs/WingsEmu.Communication.gRPC/WingsEmu.Communication.gRPC.csproj @@ -0,0 +1,34 @@ + + + + net5.0 + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + diff --git a/srcs/WingsEmu.Health/Extensions/DependencyInjectionExtensions.cs b/srcs/WingsEmu.Health/Extensions/DependencyInjectionExtensions.cs new file mode 100644 index 0000000..71cb4b5 --- /dev/null +++ b/srcs/WingsEmu.Health/Extensions/DependencyInjectionExtensions.cs @@ -0,0 +1,23 @@ +using Microsoft.Extensions.DependencyInjection; +using PhoenixLib.Logging; +using PhoenixLib.ServiceBus.Extensions; + +namespace WingsEmu.Health.Extensions +{ + public static class DependencyInjectionExtensions + { + public static void AddMaintenanceMode(this IServiceCollection services) + { + services.AddSingleton(); + if (!EnvironmentExtensions.IsFeatureActivated("SERVICE_HEALTHCHECK_ACTIVATED")) + { + return; + } + + services.AddHostedService(); + services.AddMessageSubscriber(); + services.AddMessageSubscriber(); + services.AddMessagePublisher(); + } + } +} \ No newline at end of file diff --git a/srcs/WingsEmu.Health/HealthCheckHostedService.cs b/srcs/WingsEmu.Health/HealthCheckHostedService.cs new file mode 100644 index 0000000..71c53a2 --- /dev/null +++ b/srcs/WingsEmu.Health/HealthCheckHostedService.cs @@ -0,0 +1,38 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using PhoenixLib.ServiceBus; + +namespace WingsEmu.Health +{ + public class HealthCheckHostedService : BackgroundService + { + private static readonly TimeSpan Interval = TimeSpan.FromSeconds(Convert.ToInt32(Environment.GetEnvironmentVariable("WINGSEMU_HEALTHCHECK_INTERVAL_SECONDS") ?? "3")); + private readonly IMaintenanceManager _maintenanceManager; + + private readonly IMessagePublisher _publisher; + + public HealthCheckHostedService(IMessagePublisher publisher, IMaintenanceManager maintenanceManager) + { + _publisher = publisher; + _maintenanceManager = maintenanceManager; + } + + public string ServiceName => _maintenanceManager.ServiceName; + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + await _publisher.PublishAsync(new ServiceStatusUpdateMessage + { + ServiceName = ServiceName, + StatusType = _maintenanceManager.IsMaintenanceActive ? ServiceStatusType.UNDER_MAINTENANCE : ServiceStatusType.ONLINE, + LastUpdate = DateTime.UtcNow + }); + await Task.Delay(Interval, stoppingToken); + } + } + } +} \ No newline at end of file diff --git a/srcs/WingsEmu.Health/IMaintenanceManager.cs b/srcs/WingsEmu.Health/IMaintenanceManager.cs new file mode 100644 index 0000000..d972560 --- /dev/null +++ b/srcs/WingsEmu.Health/IMaintenanceManager.cs @@ -0,0 +1,10 @@ +namespace WingsEmu.Health +{ + public interface IMaintenanceManager + { + string ServiceName { get; } + bool IsMaintenanceActive { get; } + void ActivateMaintenance(); + void DeactivateMaintenance(); + } +} \ No newline at end of file diff --git a/srcs/WingsEmu.Health/MaintenanceActivateMessageConsumer.cs b/srcs/WingsEmu.Health/MaintenanceActivateMessageConsumer.cs new file mode 100644 index 0000000..89e4fc7 --- /dev/null +++ b/srcs/WingsEmu.Health/MaintenanceActivateMessageConsumer.cs @@ -0,0 +1,29 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; + +namespace WingsEmu.Health +{ + public class MaintenanceActivateMessageConsumer : IMessageConsumer + { + private readonly IMaintenanceManager _maintenanceManager; + + public MaintenanceActivateMessageConsumer(IMaintenanceManager maintenanceManager) => _maintenanceManager = maintenanceManager; + + public async Task HandleAsync(ServiceMaintenanceActivateMessage notification, CancellationToken token) + { + if (notification.IsGlobal) + { + _maintenanceManager.ActivateMaintenance(); + return; + } + + if (notification.TargetServiceName != _maintenanceManager.ServiceName) + { + return; + } + + _maintenanceManager.ActivateMaintenance(); + } + } +} \ No newline at end of file diff --git a/srcs/WingsEmu.Health/MaintenanceDeactivateMessageConsumer.cs b/srcs/WingsEmu.Health/MaintenanceDeactivateMessageConsumer.cs new file mode 100644 index 0000000..72c0ba1 --- /dev/null +++ b/srcs/WingsEmu.Health/MaintenanceDeactivateMessageConsumer.cs @@ -0,0 +1,29 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; + +namespace WingsEmu.Health +{ + public class MaintenanceDeactivateMessageConsumer : IMessageConsumer + { + private readonly IMaintenanceManager _maintenanceManager; + + public MaintenanceDeactivateMessageConsumer(IMaintenanceManager maintenanceManager) => _maintenanceManager = maintenanceManager; + + public async Task HandleAsync(ServiceMaintenanceDeactivateMessage notification, CancellationToken token) + { + if (notification.IsGlobal) + { + _maintenanceManager.DeactivateMaintenance(); + return; + } + + if (notification.TargetServiceName != _maintenanceManager.ServiceName) + { + return; + } + + _maintenanceManager.DeactivateMaintenance(); + } + } +} \ No newline at end of file diff --git a/srcs/WingsEmu.Health/MaintenanceManager.cs b/srcs/WingsEmu.Health/MaintenanceManager.cs new file mode 100644 index 0000000..27fdae0 --- /dev/null +++ b/srcs/WingsEmu.Health/MaintenanceManager.cs @@ -0,0 +1,21 @@ +using System; + +namespace WingsEmu.Health +{ + public class MaintenanceManager : IMaintenanceManager + { + private static readonly string _serviceName = Environment.GetEnvironmentVariable("WINGSEMU_HEALTHCHECK_SERVICENAME") ?? "wingsemu-service-" + Guid.NewGuid(); + public string ServiceName => _serviceName; + public bool IsMaintenanceActive { get; private set; } + + public void ActivateMaintenance() + { + IsMaintenanceActive = true; + } + + public void DeactivateMaintenance() + { + IsMaintenanceActive = false; + } + } +} \ No newline at end of file diff --git a/srcs/WingsEmu.Health/ServiceMaintenanceActivateMessage.cs b/srcs/WingsEmu.Health/ServiceMaintenanceActivateMessage.cs new file mode 100644 index 0000000..8f3f750 --- /dev/null +++ b/srcs/WingsEmu.Health/ServiceMaintenanceActivateMessage.cs @@ -0,0 +1,15 @@ +using PhoenixLib.ServiceBus; +using PhoenixLib.ServiceBus.Routing; + +namespace WingsEmu.Health +{ + /// + /// This message acts as a request + /// + [MessageType("service.status.maintenance.activate")] + public class ServiceMaintenanceActivateMessage : IMessage + { + public string TargetServiceName { get; init; } + public bool IsGlobal { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsEmu.Health/ServiceMaintenanceDeactivateMessage.cs b/srcs/WingsEmu.Health/ServiceMaintenanceDeactivateMessage.cs new file mode 100644 index 0000000..f405104 --- /dev/null +++ b/srcs/WingsEmu.Health/ServiceMaintenanceDeactivateMessage.cs @@ -0,0 +1,12 @@ +using PhoenixLib.ServiceBus; +using PhoenixLib.ServiceBus.Routing; + +namespace WingsEmu.Health +{ + [MessageType("service.status.maintenance.deactivate")] + public class ServiceMaintenanceDeactivateMessage : IMessage + { + public string TargetServiceName { get; init; } + public bool IsGlobal { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsEmu.Health/ServiceStatusType.cs b/srcs/WingsEmu.Health/ServiceStatusType.cs new file mode 100644 index 0000000..8a1b425 --- /dev/null +++ b/srcs/WingsEmu.Health/ServiceStatusType.cs @@ -0,0 +1,9 @@ +namespace WingsEmu.Health +{ + public enum ServiceStatusType + { + OFFLINE, + ONLINE, + UNDER_MAINTENANCE + } +} \ No newline at end of file diff --git a/srcs/WingsEmu.Health/ServiceStatusUpdateMessage.cs b/srcs/WingsEmu.Health/ServiceStatusUpdateMessage.cs new file mode 100644 index 0000000..8eacc21 --- /dev/null +++ b/srcs/WingsEmu.Health/ServiceStatusUpdateMessage.cs @@ -0,0 +1,18 @@ +using System; +using PhoenixLib.ServiceBus; +using PhoenixLib.ServiceBus.Routing; + +namespace WingsEmu.Health +{ + /// + /// This message just acts as data holder not as a command like or + /// + /// + [MessageType("service.status.update")] + public class ServiceStatusUpdateMessage : IMessage + { + public string ServiceName { get; init; } + public ServiceStatusType StatusType { get; init; } + public DateTime LastUpdate { get; init; } + } +} \ No newline at end of file diff --git a/srcs/WingsEmu.Health/WingsEmu.Health.csproj b/srcs/WingsEmu.Health/WingsEmu.Health.csproj new file mode 100644 index 0000000..9c7ab3e --- /dev/null +++ b/srcs/WingsEmu.Health/WingsEmu.Health.csproj @@ -0,0 +1,18 @@ + + + + net5.0 + + + + + + + + + + + + + + diff --git a/srcs/_plugins/Plugin.Act4/Act4DungeonManager.cs b/srcs/_plugins/Plugin.Act4/Act4DungeonManager.cs new file mode 100644 index 0000000..9528058 --- /dev/null +++ b/srcs/_plugins/Plugin.Act4/Act4DungeonManager.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using WingsEmu.Game; +using WingsEmu.Game.Act4; +using WingsEmu.Game.Act4.Configuration; +using WingsEmu.Game.Entities; +using WingsEmu.Packets.Enums; + +namespace Plugin.Act4; + +public class Act4DungeonManager : IAct4DungeonManager +{ + private readonly Act4DungeonsConfiguration _act4DungeonsConfiguration; + + private readonly List _dungeons = new(); + private readonly Dictionary _dungeonsByFamilyId = new(); + + private IReadOnlyList _guardians; + private IPortalEntity _portal; + + public Act4DungeonManager(Act4DungeonsConfiguration act4DungeonsConfiguration) => _act4DungeonsConfiguration = act4DungeonsConfiguration; + + public bool DungeonsActive { get; private set; } + public DungeonType DungeonType { get; private set; } + public FactionType AllowedFaction { get; private set; } + public DateTime DungeonEnd { get; private set; } + public DateTime DungeonStart { get; private set; } + public IReadOnlyList Dungeons => _dungeons; + public TimeSpan DungeonEndSpan => _act4DungeonsConfiguration.DungeonDuration; + + public void EnableDungeons(DungeonType dungeonType, FactionType allowedFaction) + { + if (DungeonsActive) + { + return; + } + + DungeonsActive = true; + DungeonType = dungeonType; + AllowedFaction = allowedFaction; + DateTime currentTime = DateTime.UtcNow; + DungeonEnd = currentTime.Add(DungeonEndSpan); + DungeonStart = currentTime; + } + + public void SetGuardiansAndPortal(IReadOnlyList guardians, IPortalEntity portal) + { + _guardians = guardians; + _portal = portal; + } + + public (IReadOnlyList, IPortalEntity) GetAndCleanGuardians() + { + IReadOnlyList list = _guardians; + IPortalEntity portal = _portal; + _guardians = null; + _portal = null; + return (list, portal); + } + + public void DisableDungeons() + { + DungeonsActive = false; + } + + public void RegisterDungeon(DungeonInstance dungeonInstance) + { + if (!DungeonsActive || dungeonInstance.DungeonType != DungeonType || _dungeonsByFamilyId.ContainsKey(dungeonInstance.FamilyId)) + { + return; + } + + _dungeons.Add(dungeonInstance); + _dungeonsByFamilyId.Add(dungeonInstance.FamilyId, dungeonInstance); + } + + public void UnregisterDungeon(DungeonInstance dungeonInstance) + { + _dungeons.Remove(dungeonInstance); + _dungeonsByFamilyId.Remove(dungeonInstance.FamilyId); + } + + public DungeonInstance GetDungeon(long familyId) => _dungeonsByFamilyId.TryGetValue(familyId, out DungeonInstance dungeon) ? dungeon : null; +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Act4/Act4Manager.cs b/srcs/_plugins/Plugin.Act4/Act4Manager.cs new file mode 100644 index 0000000..5a53ba9 --- /dev/null +++ b/srcs/_plugins/Plugin.Act4/Act4Manager.cs @@ -0,0 +1,154 @@ +using System; +using System.Collections.Concurrent; +using WingsAPI.Packets.Enums.Act4; +using WingsEmu.Game.Act4; +using WingsEmu.Game.Act4.Configuration; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Managers; +using WingsEmu.Packets.Enums; + +namespace Plugin.Act4; + +public class Act4Manager : IAct4Manager +{ + private readonly Act4Configuration _act4Configuration; + private readonly IAct4DungeonManager _act4DungeonManager; + private readonly ConcurrentQueue<(FactionType factionType, int amount)> _pointsToAdd = new(); + private int _angelPoints; + private int _demonPoints; + private IMonsterEntity _mukraju; + + private DateTime _mukrajuDeleteTime; + private FactionType _mukrajuFaction = FactionType.Neutral; + + public Act4Manager(Act4Configuration act4Configuration, IAct4DungeonManager act4DungeonManager) + { + _act4Configuration = act4Configuration; + _act4DungeonManager = act4DungeonManager; + } + + public void AddFactionPoints(FactionType factionType, int amount) + { + if (FactionPointsLocked) + { + return; + } + + _pointsToAdd.Enqueue((factionType, amount)); + } + + public void ResetFactionPoints(FactionType factionType) + { + if (factionType == FactionType.Angel) + { + _angelPoints = default; + } + else + { + _demonPoints = default; + } + } + + public bool FactionPointsLocked => _mukraju != null || _act4DungeonManager.DungeonsActive; + + public void RegisterMukraju(DateTime current, IMonsterEntity mukraju, FactionType factionType) + { + _mukrajuDeleteTime = current + _act4Configuration.MukrajuEndSpan; + _mukraju = mukraju; + _mukrajuFaction = factionType; + } + + public (DateTime deleteTime, IMonsterEntity mukraju, FactionType mukrajuFactionType) GetMukraju() => (_mukrajuDeleteTime, _mukraju, _mukrajuFaction); + + public IMonsterEntity UnregisterMukraju() + { + IMonsterEntity monsterEntity = _mukraju; + _mukraju = null; + _mukrajuFaction = FactionType.Neutral; + return monsterEntity; + } + + public FactionType MukrajuFaction() => _mukrajuFaction; + + public FactionType GetTriumphantFaction() + { + if (_pointsToAdd.IsEmpty) + { + return FactionType.Neutral; + } + + while (_pointsToAdd.TryDequeue(out (FactionType factionType, int amount) pointsToAdd)) + { + if (pointsToAdd.factionType == FactionType.Angel) + { + _angelPoints += pointsToAdd.amount; + if (_act4Configuration.MaximumFactionPoints > _angelPoints) + { + continue; + } + + _pointsToAdd.Clear(); + _angelPoints = default; + return FactionType.Angel; + } + + _demonPoints += pointsToAdd.amount; + if (_act4Configuration.MaximumFactionPoints > _demonPoints) + { + continue; + } + + _pointsToAdd.Clear(); + _demonPoints = default; + return FactionType.Demon; + } + + return FactionType.Neutral; + } + + public Act4Status GetStatus() + { + float maxPoints = _act4Configuration.MaximumFactionPoints; + + DateTime todayResetDate = DateTime.Today + _act4Configuration.ResetDate; + DateTime currentDate = DateTime.UtcNow; + TimeSpan timeSpan; + + if (todayResetDate <= currentDate) + { + timeSpan = todayResetDate.AddDays(1) - currentDate; + } + else + { + timeSpan = todayResetDate - currentDate; + } + + FactionType relevantFaction = FactionType.Neutral; + Act4FactionStateType factionStateType = Act4FactionStateType.Nothing; + TimeSpan currentTimeBeforeX = TimeSpan.Zero; + TimeSpan timeBeforeX = TimeSpan.Zero; + + (DateTime deleteTime, IMonsterEntity mukraju, FactionType mukrajuFactionType) = GetMukraju(); + if (mukraju != null) + { + relevantFaction = mukrajuFactionType; + factionStateType = Act4FactionStateType.Mukraju; + currentTimeBeforeX = deleteTime - currentDate; + timeBeforeX = _act4Configuration.MukrajuEndSpan; + } + + DungeonType dungeonType = DungeonType.Morcos; + + if (_act4DungeonManager.DungeonsActive) + { + relevantFaction = _act4DungeonManager.AllowedFaction; + factionStateType = Act4FactionStateType.RaidDungeon; + currentTimeBeforeX = _act4DungeonManager.DungeonEnd - currentDate; + timeBeforeX = _act4DungeonManager.DungeonEndSpan; + dungeonType = _act4DungeonManager.DungeonType; + } + + return new Act4Status(Convert.ToByte(_angelPoints / maxPoints * 100), Convert.ToByte(_demonPoints / maxPoints * 100), timeSpan, + relevantFaction, factionStateType, currentTimeBeforeX, timeBeforeX, dungeonType); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Act4/Act4Plugin.cs b/srcs/_plugins/Plugin.Act4/Act4Plugin.cs new file mode 100644 index 0000000..2747564 --- /dev/null +++ b/srcs/_plugins/Plugin.Act4/Act4Plugin.cs @@ -0,0 +1,20 @@ +using Plugin.Act4.Commands; +using WingsAPI.Plugins; +using WingsEmu.Commands.Interfaces; + +namespace Plugin.Act4; + +public class Act4Plugin : IGamePlugin +{ + private readonly ICommandContainer _commands; + + public Act4Plugin(ICommandContainer commands) => _commands = commands; + + + public string Name => nameof(Act4Plugin); + + public void OnLoad() + { + _commands.AddModule(); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Act4/Act4PluginCore.cs b/srcs/_plugins/Plugin.Act4/Act4PluginCore.cs new file mode 100644 index 0000000..a772fdc --- /dev/null +++ b/srcs/_plugins/Plugin.Act4/Act4PluginCore.cs @@ -0,0 +1,66 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using PhoenixLib.Configuration; +using PhoenixLib.Events; +using PhoenixLib.Logging; +using Plugin.Act4.RecurrentJob; +using Plugin.Act4.Scripting.Validator; +using WingsAPI.Communication.ServerApi.Protocol; +using WingsAPI.Plugins; +using WingsAPI.Scripting; +using WingsAPI.Scripting.LUA; +using WingsAPI.Scripting.ScriptManager; +using WingsEmu.Game.Act4; +using WingsEmu.Game.Act4.Configuration; +using WingsEmu.Game.Managers; + +namespace Plugin.Act4; + +public class Act4PluginCore : IGameServerPlugin +{ + public string Name => nameof(Act4PluginCore); + + public void AddDependencies(IServiceCollection services, GameServerLoader gameServer) + { + // TODO: Plz, when we have warmup we should move those "AddSingleton" down, so it only gets loaded when necessary + // Dungeon Script Cache + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddFileConfiguration(); + services.AddSingleton(); + // TODO: Remove building twice the ServiceProvider when we have another method to obtain server's type + if (gameServer.Type != GameChannelType.ACT_4) + { + Log.Debug("Not loading Act4 plugin because this is not an Act4 channel."); + return; + } + + services.AddEventHandlersInAssembly(); + + services.AddFileConfiguration(); + + services.AddSingleton(); + services.AddHostedService(); + + services.AddSingleton(); + services.AddHostedService(); + + services.TryAddSingleton(x => + { + IConfigurationPathProvider config = x.GetRequiredService(); + return new ScriptFactoryConfiguration + { + RootDirectory = config.GetConfigurationPath("scripts"), + LibDirectory = config.GetConfigurationPath("scripts/lib") + }; + }); + + + // script factory + services.TryAddSingleton(); + + // factory + services.AddSingleton(); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Act4/Commands/Act4CommandsModule.cs b/srcs/_plugins/Plugin.Act4/Commands/Act4CommandsModule.cs new file mode 100644 index 0000000..aa76dec --- /dev/null +++ b/srcs/_plugins/Plugin.Act4/Commands/Act4CommandsModule.cs @@ -0,0 +1,27 @@ +using System.Threading.Tasks; +using Qmmands; +using WingsEmu.Commands.Checks; +using WingsEmu.Commands.Entities; +using WingsEmu.DTOs.Account; +using WingsEmu.Game.Act4.Event; +using WingsEmu.Packets.Enums; + +namespace Plugin.Act4.Commands; + +[Name("Act4_Commands")] +[Group("act4", "glacernon")] +[Description("Module related to Act4 management commands.")] +[RequireAuthority(AuthorityType.GameAdmin)] +public partial class Act4CommandsModule : SaltyModuleBase +{ + [Command("addFactionPoints", "addPoints", "afp")] + public async Task AddFactionPoints(int points, FactionType factionType = FactionType.Neutral) + { + if (factionType == FactionType.Neutral) + { + await Context.Player.EmitEventAsync(new Act4FactionPointsIncreaseEvent(points)); + } + + await Context.Player.EmitEventAsync(new Act4FactionPointsIncreaseEvent(factionType, points)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Act4/Commands/Act4DungeonCommandsModule.cs b/srcs/_plugins/Plugin.Act4/Commands/Act4DungeonCommandsModule.cs new file mode 100644 index 0000000..2b10982 --- /dev/null +++ b/srcs/_plugins/Plugin.Act4/Commands/Act4DungeonCommandsModule.cs @@ -0,0 +1,106 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using PhoenixLib.Events; +using PhoenixLib.Logging; +using Qmmands; +using WingsAPI.Scripting.ScriptManager; +using WingsEmu.Commands.Entities; +using WingsEmu.Game.Act4; +using WingsEmu.Game.Act4.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Maps.Event; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace Plugin.Act4.Commands; + +public partial class Act4CommandsModule +{ + [Group("dungeon", "dungeons", "raid", "raids")] + [Description("Module related to Act4 Dungeons management commands.")] + public class Act4DungeonCommandsModule : SaltyModuleBase + { + private readonly IAct4DungeonManager _act4DungeonManager; + private readonly IAsyncEventPipeline _asyncEventPipeline; + private readonly IDungeonScriptManager _dungeonScriptManager; + + public Act4DungeonCommandsModule(IAsyncEventPipeline asyncEventPipeline, IDungeonScriptManager dungeonScriptManager, IAct4DungeonManager act4DungeonManager) + { + _asyncEventPipeline = asyncEventPipeline; + _dungeonScriptManager = dungeonScriptManager; + _act4DungeonManager = act4DungeonManager; + } + + [Command("reload")] + public async Task ReloadDungeons() + { + try + { + _dungeonScriptManager.Load(); + Context.Player.SendSuccessChatMessage("Dungeons reloaded, check your console output!"); + } + catch (Exception e) + { + Log.Error("[ACT4_DUNGEON_SYSTEM] Reload failed: ", e); + Context.Player.SendErrorChatMessage("Dungeons reload failed!"); + } + } + + [Command("start")] + public async Task StartDungeon(DungeonType dungeonType, FactionType factionType = FactionType.Neutral) + { + await _asyncEventPipeline.ProcessEventAsync( + new Act4DungeonSystemStartEvent(factionType == FactionType.Neutral ? Context.Player.PlayerEntity.Faction : factionType, dungeonType)); + } + + [Command("stop")] + public async Task StopDungeon() + { + await _asyncEventPipeline.ProcessEventAsync(new Act4DungeonSystemStopEvent()); + } + + [Command("open")] + public async Task OpenPortal() + { + IClientSession session = Context.Player; + if (!session.PlayerEntity.IsInFamily()) + { + return new SaltyCommandResult(false, "Not in family."); + } + + DungeonInstance dungeon = _act4DungeonManager.GetDungeon(session.PlayerEntity.Family.Id); + if (dungeon == null) + { + return new SaltyCommandResult(false, "The dungeon doesn't exist for this family."); + } + + if (!dungeon.DungeonSubInstances.TryGetValue(session.CurrentMapInstance.Id, out DungeonSubInstance instance)) + { + return new SaltyCommandResult(false, "You have to be inside the dungeon."); + } + + PortalGenerator portalGenerator = instance.PortalGenerators.FirstOrDefault(); + if (portalGenerator == null || instance.LastPortalGeneration == null) + { + return new SaltyCommandResult(false, "Portal doesn't exist or it's already opened."); + } + + instance.PortalGenerators.Remove(portalGenerator); + + await _asyncEventPipeline.ProcessEventAsync(new SpawnPortalEvent(instance.MapInstance, portalGenerator.Portal)); + + await _asyncEventPipeline.ProcessEventAsync(new Act4DungeonBroadcastPacketEvent + { + DungeonInstance = dungeon + }); + + await _asyncEventPipeline.ProcessEventAsync(new Act4DungeonBroadcastBossOpenEvent + { + DungeonInstance = dungeon + }); + + return new SaltyCommandResult(true); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Act4/Const/DungeonConstEventKeys.cs b/srcs/_plugins/Plugin.Act4/Const/DungeonConstEventKeys.cs new file mode 100644 index 0000000..27c1e1b --- /dev/null +++ b/srcs/_plugins/Plugin.Act4/Const/DungeonConstEventKeys.cs @@ -0,0 +1,7 @@ +namespace Plugin.Act4.Const; + +public class DungeonConstEventKeys +{ + public const string RaidSubInstanceAfterSlowMo = "AfterSlowMo"; + public const string RaidSubInstanceCleanUp = "CleanUp"; +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Act4/DungeonFactory.cs b/srcs/_plugins/Plugin.Act4/DungeonFactory.cs new file mode 100644 index 0000000..8e6cb12 --- /dev/null +++ b/srcs/_plugins/Plugin.Act4/DungeonFactory.cs @@ -0,0 +1,299 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Microsoft.Extensions.DependencyInjection; +using PhoenixLib.Events; +using PhoenixLib.Logging; +using Plugin.Act4.Event; +using Plugin.Act4.Extension; +using WingsAPI.Game.Extensions.PacketGeneration; +using WingsAPI.Scripting.Attribute; +using WingsAPI.Scripting.Converter; +using WingsAPI.Scripting.Enum; +using WingsAPI.Scripting.Event; +using WingsAPI.Scripting.Event.Common; +using WingsAPI.Scripting.Object.Common; +using WingsAPI.Scripting.Object.Common.Map; +using WingsAPI.Scripting.Object.Dungeon; +using WingsAPI.Scripting.Object.Raid; +using WingsAPI.Scripting.ScriptManager; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game; +using WingsEmu.Game.Act4; +using WingsEmu.Game.Act4.Entities; +using WingsEmu.Game.Act4.Event; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Entities.Event; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Portals; +using WingsEmu.Game.Raids; +using WingsEmu.Packets.Enums; + +namespace Plugin.Act4; + +public class DungeonFactory : IDungeonFactory +{ + private static readonly IEnumerable Converters; + private readonly IAsyncEventPipeline _eventPipeline; + + private readonly HatusDragonHead _hatusHead = new() + { + BluePositionX = 23, + RedPositionX = 36, + GreenPositionX = 49 + }; + + private readonly IMapManager _mapManager; + private readonly IMonsterEntityFactory _monsterEntityFactory; + private readonly IPortalFactory _portalFactory; + private readonly IDungeonScriptManager _raidScriptManager; + + static DungeonFactory() + { + Converters = typeof(IScriptedEventConverter).Assembly.GetTypes() + .Concat(typeof(DungeonFactory).Assembly.GetTypes()) + .Where(x => typeof(IScriptedEventConverter).IsAssignableFrom(x)) + .Where(x => !x.IsAbstract && !x.IsInterface); + } + + public DungeonFactory(IDungeonScriptManager raidScriptManager, IMapManager mapManager, IAsyncEventPipeline eventPipeline, IMonsterEntityFactory monsterEntityFactory, IPortalFactory portalFactory) + { + _raidScriptManager = raidScriptManager; + _mapManager = mapManager; + _eventPipeline = eventPipeline; + _monsterEntityFactory = monsterEntityFactory; + _portalFactory = portalFactory; + } + + public DungeonInstance CreateDungeon(long familyId, DungeonType dungeonType) + { + SDungeon scriptedDungeon = _raidScriptManager.GetScriptedDungeon(dungeonType.ToSDungeonType()); + if (scriptedDungeon == null) + { + Log.Error($"Unable to create dungeon of DungeonType: '{dungeonType.ToString()}', the script couldn't be found inside DungeonScriptManager", + new Act4DungeonSystemException($"Unable to create act4 dungeon - DungeonType: {dungeonType.ToString()}, FamilyId: {familyId}")); + return default; + } + + var raidBoxRarities = new List(); + + foreach (SRaidBoxRarity boxRarity in scriptedDungeon.Reward.RaidBox.RaidBoxRarity) + { + var newRarity = new RaidBoxRarity(boxRarity.Rarity, boxRarity.Chance); + raidBoxRarities.Add(newRarity); + } + + var raidBox = new RaidBox(scriptedDungeon.Reward.RaidBox.RewardBox, raidBoxRarities); + var raidReward = new RaidReward(raidBox, scriptedDungeon.Reward.DefaultReputation, scriptedDungeon.Reward.FixedReputation); + + //Preparing game object locators for the conversion of ScriptEvents to GameEvents + Dictionary dungeonSubInstancesByScriptId = new(); + Dictionary portalsByScriptId = new(); + Dictionary monstersByScriptId = new(); + Dictionary buttonsByScriptId = new(); + + Dictionary> events)>> eventsToConvertAndAdd = new(); + + HatusHeads hatusHeads = dungeonType == DungeonType.Hatus ? new HatusHeads(_hatusHead) : null; + + //We prepare first portals to allow the creation of portals + //(portals connect two maps and pre-categorizing the maps by their ScriptIds lets us obtain their GameIds, allowing us to generate gamePortals as we require of GameIds to do so) + foreach (SMap scriptedMap in scriptedDungeon.Maps) + { + IMapInstance map = scriptedMap.MapType == SMapType.MapId + ? _mapManager.GenerateMapInstanceByMapId(scriptedMap.MapIdVnum, MapInstanceType.Act4Dungeon) + : _mapManager.GenerateMapInstanceByMapVNum(new ServerMapDto + { + Flags = scriptedMap.Flags.Select(mapFlag => (MapFlags)(int)mapFlag).ToList(), + Id = scriptedMap.MapIdVnum, + MapVnum = scriptedMap.MapIdVnum, + NameId = scriptedMap.NameId, + MusicId = scriptedMap.MusicId + }, MapInstanceType.Act4Dungeon); + + dungeonSubInstancesByScriptId[scriptedMap.Id] = new DungeonSubInstance(map, _eventPipeline, hatusHeads); + eventsToConvertAndAdd[scriptedMap.Id] = new List<(IEventTriggerContainer container, IDictionary> events)> + { + (dungeonSubInstancesByScriptId[scriptedMap.Id], scriptedMap.Events) + }; + } + + foreach (SMap scriptedMap in scriptedDungeon.Maps) + { + DungeonSubInstance dungeonSubInstance = dungeonSubInstancesByScriptId[scriptedMap.Id]; + var dungeonWaves = new List(); + var dungeonLoopWaves = new List(); + + // Add Monster Waves + foreach (SMonsterWave monsterWave in scriptedMap.MonsterWaves) + { + var summons = new List(); + + foreach (SMonster mobWave in monsterWave.Monsters) + { + summons.Add(new ToSummon + { + VNum = mobWave.Vnum, + IsHostile = true, + IsMoving = true, + SpawnCell = mobWave.IsRandomPosition ? null : new Position(mobWave.Position.X, mobWave.Position.Y), + SummonType = SummonType.MONSTER_WAVE + }); + } + + if (monsterWave.Loop && monsterWave.LoopTick.HasValue) + { + dungeonLoopWaves.Add(new DungeonLoopWave + { + Delay = TimeSpan.FromSeconds(monsterWave.TimeInSeconds), + Monsters = summons, + TickDelay = TimeSpan.FromSeconds(monsterWave.LoopTick.Value), + IsScaledWithPlayerAmount = monsterWave.IsScaledWithPlayerAmount + }); + } + else + { + dungeonWaves.Add(new DungeonLinearWave + { + Monsters = summons, + Delay = TimeSpan.FromSeconds(monsterWave.TimeInSeconds) + }); + } + } + + dungeonSubInstance.SetLinearWaves(dungeonWaves); + dungeonSubInstance.SetLoopWaves(dungeonLoopWaves); + + var portalGenerators = new List(); + + foreach (SPortal scriptedPortal in scriptedMap.Portals) + { + bool isReturnPortal = scriptedPortal.IsReturn; + + var sourcePos = new Position(scriptedPortal.SourcePosition.X, scriptedPortal.SourcePosition.Y); + IMapInstance sourceMap = dungeonSubInstancesByScriptId[scriptedMap.Id].MapInstance; + var destPos = new Position(scriptedPortal.DestinationPosition.X, scriptedPortal.DestinationPosition.Y); + IMapInstance destinationMap = isReturnPortal ? null : dungeonSubInstancesByScriptId[scriptedPortal.DestinationId].MapInstance; + IPortalEntity portal = _portalFactory.CreatePortal((PortalType)scriptedPortal.Type, sourceMap, sourcePos, destinationMap, destPos); + + portalsByScriptId[scriptedPortal.Id] = portal; + + if (scriptedPortal.CreationDelay.HasValue) + { + portalGenerators.Add(new PortalGenerator + { + Portal = portal, + Delay = TimeSpan.FromSeconds(scriptedPortal.CreationDelay.Value) + }); + + continue; + } + + dungeonSubInstance.MapInstance.AddPortalToMap(portal); + } + + dungeonSubInstance.SetPortalGenerators(portalGenerators); + + // Add monsters + foreach (SMonster scriptedMonster in scriptedMap.Monsters) + { + IMonsterEntity monster = _monsterEntityFactory.CreateMonster(scriptedMonster.Vnum, dungeonSubInstance.MapInstance, + new MonsterEntityBuilder + { + IsRespawningOnDeath = false, + IsBoss = scriptedMonster.IsBoss, + IsTarget = scriptedMonster.IsTarget, + IsWalkingAround = scriptedMonster.CanMove, + IsHostile = true + }); + + monster.EmitEventAsync(new MapJoinMonsterEntityEvent(monster, scriptedMonster.Position.X, scriptedMonster.Position.Y)); + + monstersByScriptId[scriptedMonster.Id] = monster; + eventsToConvertAndAdd[scriptedMap.Id].Add((monster, scriptedMonster.Events)); + //Adding this to obtain the subInstance via the monsters' Script Id + dungeonSubInstancesByScriptId[scriptedMonster.Id] = dungeonSubInstance; + + dungeonSubInstance.AddDungeonMonster(monster); + } + + // Add map objects + foreach (SMapObject scriptedObject in scriptedMap.Objects) + { + if (scriptedObject is not SButton scriptedButton) + { + continue; + } + + var button = new ButtonMapItem(scriptedButton.Position.X, scriptedButton.Position.Y, scriptedButton.DeactivatedVnum, scriptedButton.ActivatedVnum, false, + dungeonSubInstance.MapInstance, _eventPipeline, scriptedButton.IsObjective); + + eventsToConvertAndAdd[scriptedMap.Id].Add((button, scriptedButton.Events)); + + dungeonSubInstance.AddDungeonButton(button); + } + } + + //Yeah, we need an ugly wrapper because the instance is not constructed until the end, and our events require that instance for the future. + DungeonInstanceWrapper dungeonInstanceWrapper = new(); + + IServiceCollection serviceLocator = new ServiceCollection(); + + serviceLocator.AddSingleton(dungeonSubInstancesByScriptId); + serviceLocator.AddSingleton(portalsByScriptId); + serviceLocator.AddSingleton(monstersByScriptId); + serviceLocator.AddSingleton(buttonsByScriptId); + serviceLocator.AddSingleton(dungeonInstanceWrapper); + + foreach (Type converter in Converters) + { + serviceLocator.AddTransient(typeof(IScriptedEventConverter), converter); + } + + foreach ((Guid scriptedMapId, List<(IEventTriggerContainer container, IDictionary> events)> eventsForContainers) in eventsToConvertAndAdd) + { + serviceLocator.AddSingleton(dungeonSubInstancesByScriptId[scriptedMapId]); + + var converters = serviceLocator.BuildServiceProvider() + .GetServices() + .ToDictionary(x => x.EventType, x => x); + + foreach ((IEventTriggerContainer container, IDictionary> containerEvents) in eventsForContainers) + { + foreach ((string key, IEnumerable events) in containerEvents) + { + foreach (SEvent scriptedEvent in events) + { + IAsyncEvent e = converters.GetValueOrDefault(scriptedEvent.GetType())?.Convert(scriptedEvent); + if (e == null) + { + throw new InvalidOperationException($"Failed to convert {scriptedEvent.GetType().Name} to async event"); + } + + ScriptEventAttribute attribute = scriptedEvent.GetType().GetCustomAttribute(); + if (attribute == null) + { + throw new InvalidOperationException($"Failed to find attribute for: {scriptedEvent.GetType().Name}"); + } + + //quick win + if (attribute.Name == SRemovePortalEvent.Name) + { + container.AddEvent(key, new Act4DungeonBroadcastBossClosedEvent + { + DungeonInstanceWrapper = dungeonInstanceWrapper + }, attribute.IsRemovedOnTrigger); + } + + container.AddEvent(key, e, attribute.IsRemovedOnTrigger); + } + } + } + } + + return dungeonInstanceWrapper.DungeonInstance = new DungeonInstance(familyId, dungeonType, dungeonSubInstancesByScriptId.Values, dungeonSubInstancesByScriptId[scriptedDungeon.Spawn.MapId], + new Position(scriptedDungeon.Spawn.Position.X, scriptedDungeon.Spawn.Position.Y), raidReward); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Act4/DungeonScriptManager.cs b/srcs/_plugins/Plugin.Act4/DungeonScriptManager.cs new file mode 100644 index 0000000..3feb211 --- /dev/null +++ b/srcs/_plugins/Plugin.Act4/DungeonScriptManager.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using FluentValidation.Results; +using MoonSharp.Interpreter; +using PhoenixLib.Logging; +using Plugin.Act4.Scripting.Validator; +using WingsAPI.Scripting; +using WingsAPI.Scripting.Enum.Dungeon; +using WingsAPI.Scripting.LUA; +using WingsAPI.Scripting.Object.Dungeon; +using WingsAPI.Scripting.ScriptManager; + +namespace Plugin.Act4; + +public class DungeonScriptManager : IDungeonScriptManager +{ + private readonly Dictionary _cache = new(); + private readonly IScriptFactory _scriptFactory; + private readonly ScriptFactoryConfiguration _scriptFactoryConfiguration; + private readonly SDungeonValidator _validator; + + public DungeonScriptManager(IScriptFactory scriptFactory, ScriptFactoryConfiguration scriptFactoryConfiguration, SDungeonValidator validator) + { + _scriptFactory = scriptFactory; + _scriptFactoryConfiguration = scriptFactoryConfiguration; + _validator = validator; + } + + public SDungeon GetScriptedDungeon(SDungeonType raidType) => _cache.GetValueOrDefault(raidType); + + public void Load() + { + IEnumerable files = Directory.GetFiles(_scriptFactoryConfiguration.DungeonsDirectory, "*.lua"); + foreach (string file in files) + { + try + { + SDungeon dungeon = _scriptFactory.LoadScript(file); + if (dungeon == null) + { + Log.Warn($"Failed to load raid script {file}"); + continue; + } + + ValidationResult result = _validator.Validate(dungeon); + if (!result.IsValid) + { + throw new InvalidScriptException(result.Errors.First().ErrorMessage); + } + + Log.Warn($"[DUNGEON_SCRIPT_MANAGER] Loaded {Path.GetFileName(file)} for raid: {dungeon.DungeonType}"); + _cache[dungeon.DungeonType] = dungeon; + } + catch (InvalidScriptException e) + { + Log.Error($"[DUNGEON_SCRIPT_MANAGER][SCRIPT_ERROR] InvalidScript: {file}", e); + } + catch (ScriptRuntimeException e) + { + Log.Error($"[DUNGEON_SCRIPT_MANAGER][SCRIPT ERROR] {file}, {e.DecoratedMessage}", e); + } + catch (Exception e) + { + Log.Error($"[DUNGEON_SCRIPT_MANAGER][SCRIPT_ERROR] {file}", e); + } + } + + Log.Info($"Loaded {_cache.Count} dungeons from scripts"); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Act4/Event/Act4DungeonBossMapCleanUpEventHandler.cs b/srcs/_plugins/Plugin.Act4/Event/Act4DungeonBossMapCleanUpEventHandler.cs new file mode 100644 index 0000000..182a9f6 --- /dev/null +++ b/srcs/_plugins/Plugin.Act4/Event/Act4DungeonBossMapCleanUpEventHandler.cs @@ -0,0 +1,20 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Act4.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; + +namespace Plugin.Act4.Event; + +public class Act4DungeonBossMapCleanUpEventHandler : IAsyncEventProcessor +{ + public async Task HandleAsync(Act4DungeonBossMapCleanUpEvent e, CancellationToken cancellation) + { + foreach (IClientSession session in e.BossMap.MapInstance.Sessions.ToList()) + { + session.ChangeMap(e.DungeonInstance.SpawnInstance.MapInstance, e.DungeonInstance.SpawnPoint.X, e.DungeonInstance.SpawnPoint.Y); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Act4/Event/Act4DungeonBroadcastBossClosedEventHandler.cs b/srcs/_plugins/Plugin.Act4/Event/Act4DungeonBroadcastBossClosedEventHandler.cs new file mode 100644 index 0000000..49ac2c2 --- /dev/null +++ b/srcs/_plugins/Plugin.Act4/Event/Act4DungeonBroadcastBossClosedEventHandler.cs @@ -0,0 +1,31 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Act4; +using WingsEmu.Game.Act4.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.Act4.Event; + +public class Act4DungeonBroadcastBossClosedEventHandler : IAsyncEventProcessor +{ + private readonly IGameLanguageService _languageService; + + public Act4DungeonBroadcastBossClosedEventHandler(IGameLanguageService languageService) => _languageService = languageService; + + public async Task HandleAsync(Act4DungeonBroadcastBossClosedEvent e, CancellationToken cancellation) + { + DungeonInstance dungeonInstance = e.DungeonInstanceWrapper.DungeonInstance; + + foreach (DungeonSubInstance dungeonSubInstance in dungeonInstance.DungeonSubInstances.Values) + { + foreach (IClientSession session in dungeonSubInstance.MapInstance.Sessions) + { + session.SendMsg(_languageService.GetLanguage(GameDialogKey.ACT4_DUNGEON_SHOUTMESSAGE_BOSS_CLOSED, session.UserLanguage), MsgMessageType.Middle); + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Act4/Event/Act4DungeonBroadcastBossOpenEventHandler.cs b/srcs/_plugins/Plugin.Act4/Event/Act4DungeonBroadcastBossOpenEventHandler.cs new file mode 100644 index 0000000..4c19680 --- /dev/null +++ b/srcs/_plugins/Plugin.Act4/Event/Act4DungeonBroadcastBossOpenEventHandler.cs @@ -0,0 +1,34 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Act4; +using WingsEmu.Game.Act4.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.Act4.Event; + +public class Act4DungeonBroadcastBossOpenEventHandler : IAsyncEventProcessor +{ + private readonly IGameLanguageService _languageService; + + public Act4DungeonBroadcastBossOpenEventHandler(IGameLanguageService languageService) => _languageService = languageService; + + public async Task HandleAsync(Act4DungeonBroadcastBossOpenEvent e, CancellationToken cancellation) + { + foreach (DungeonSubInstance subInstance in e.DungeonInstance.DungeonSubInstances.Values) + { + if (subInstance.MapInstance.Sessions.Count < 1) + { + continue; + } + + foreach (IClientSession session in subInstance.MapInstance.Sessions) + { + session.SendMsg(_languageService.GetLanguage(GameDialogKey.ACT4_DUNGEON_SHOUTMESSAGE_BOSS_OPEN, session.UserLanguage), MsgMessageType.Middle); + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Act4/Event/Act4DungeonBroadcastPacketEventHandler.cs b/srcs/_plugins/Plugin.Act4/Event/Act4DungeonBroadcastPacketEventHandler.cs new file mode 100644 index 0000000..ae3e26a --- /dev/null +++ b/srcs/_plugins/Plugin.Act4/Event/Act4DungeonBroadcastPacketEventHandler.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Act4; +using WingsEmu.Game.Act4.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; + +namespace Plugin.Act4.Event; + +public class Act4DungeonBroadcastPacketEventHandler : IAsyncEventProcessor +{ + private readonly IAct4DungeonManager _act4DungeonManager; + + public Act4DungeonBroadcastPacketEventHandler(IAct4DungeonManager act4DungeonManager) => _act4DungeonManager = act4DungeonManager; + + public async Task HandleAsync(Act4DungeonBroadcastPacketEvent e, CancellationToken cancellation) + { + IReadOnlyDictionary subInstances = e.DungeonInstance.DungeonSubInstances; + if (subInstances.Count < 1) + { + return; + } + + DateTime currentTime = DateTime.UtcNow; + + foreach (DungeonSubInstance subInstance in subInstances.Values) + { + string packet = null; + + foreach (IClientSession session in subInstance.MapInstance.Sessions) + { + packet ??= session.GenerateDungeonPacket(e.DungeonInstance, subInstance, _act4DungeonManager, currentTime); + session.SendPacket(packet); + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Act4/Event/Act4DungeonEnterEventHandler.cs b/srcs/_plugins/Plugin.Act4/Event/Act4DungeonEnterEventHandler.cs new file mode 100644 index 0000000..468f30b --- /dev/null +++ b/srcs/_plugins/Plugin.Act4/Event/Act4DungeonEnterEventHandler.cs @@ -0,0 +1,92 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using PhoenixLib.Logging; +using Plugin.Act4.Extension; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Act4; +using WingsEmu.Game.Act4.Configuration; +using WingsEmu.Game.Act4.Event; +using WingsEmu.Game.Extensions; + +namespace Plugin.Act4.Event; + +public class Act4DungeonSystemException : Exception +{ + public Act4DungeonSystemException(string message) : base(message) + { + } +} + +public class Act4DungeonEnterEventHandler : IAsyncEventProcessor +{ + private readonly Act4DungeonsConfiguration _act4DungeonsConfiguration; + private readonly IDungeonFactory _dungeonFactory; + private readonly IAct4DungeonManager _dungeonManager; + private readonly IGameLanguageService _languageService; + + public Act4DungeonEnterEventHandler(IAct4DungeonManager dungeonManager, IGameLanguageService languageService, Act4DungeonsConfiguration act4DungeonsConfiguration, IDungeonFactory dungeonFactory) + { + _dungeonManager = dungeonManager; + _languageService = languageService; + _act4DungeonsConfiguration = act4DungeonsConfiguration; + _dungeonFactory = dungeonFactory; + } + + public async Task HandleAsync(Act4DungeonEnterEvent e, CancellationToken cancellation) + { + if (!_dungeonManager.DungeonsActive) + { + return; + } + + if (e.Sender.PlayerEntity.Faction != _dungeonManager.AllowedFaction) + { + e.Sender.SendInformationChatMessage(_languageService.GetLanguage(GameDialogKey.PORTAL_CHATMESSAGE_BLOCKED, e.Sender.UserLanguage)); + return; + } + + if (!e.Sender.PlayerEntity.IsInFamily()) + { + e.Sender.SendInformationChatMessage(_languageService.GetLanguage(GameDialogKey.ACT4_DUNGEON_CHATMESSAGE_FAMILY_NEEDED, e.Sender.UserLanguage)); + return; + } + + long familyId = e.Sender.PlayerEntity.Family.Id; + + int reputationCost = e.Sender.GetDungeonReputationRequirement(_act4DungeonsConfiguration.DungeonEntryCostMultiplier); + if (!e.Confirmed) + { + e.Sender.SendQnaPacket("preq 1", _languageService.GetLanguageFormat(GameDialogKey.ACT4_DUNGEON_QNAMESSAGE_ENTRY_COST, e.Sender.UserLanguage, reputationCost)); + return; + } + + if (!e.Sender.PlayerEntity.RemoveReputation(reputationCost)) + { + e.Sender.SendInformationChatMessage(_languageService.GetLanguage(GameDialogKey.INFORMATION_CHATMESSAGE_NOT_ENOUGH_REPUT, e.Sender.UserLanguage)); + return; + } + + DungeonInstance dungeon = _dungeonManager.GetDungeon(familyId); + + if (dungeon == null) + { + dungeon = _dungeonFactory.CreateDungeon(familyId, _dungeonManager.DungeonType); + + if (dungeon != null) + { + _dungeonManager.RegisterDungeon(dungeon); + } + } + + if (dungeon == null) + { + Log.Error($"[ACT4_DUNGEON_ENTER] Wasn't able to generate a new Dungeon for the Family with FamilyId: '{familyId.ToString()}'", + new Act4DungeonSystemException($"FamilyId: {familyId} Dungeon Faction: {_dungeonManager.AllowedFaction} Dungeon Type: {_dungeonManager.DungeonType}")); + return; + } + + e.Sender.ChangeMap(dungeon.SpawnInstance.MapInstance, dungeon.SpawnPoint.X, dungeon.SpawnPoint.Y); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Act4/Event/Act4DungeonMonsterThrowEventHandler.cs b/srcs/_plugins/Plugin.Act4/Event/Act4DungeonMonsterThrowEventHandler.cs new file mode 100644 index 0000000..f2fd107 --- /dev/null +++ b/srcs/_plugins/Plugin.Act4/Event/Act4DungeonMonsterThrowEventHandler.cs @@ -0,0 +1,68 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game; +using WingsEmu.Game._enum; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Items; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Raids; +using WingsEmu.Game.Raids.Events; + +namespace Plugin.Act4.Event; + +public class Act4DungeonMonsterThrowEventHandler : IAsyncEventProcessor +{ + private readonly IGameItemInstanceFactory _gameItemInstanceFactory; + private readonly IRandomGenerator _randomGenerator; + + public Act4DungeonMonsterThrowEventHandler(IRandomGenerator randomGenerator, IGameItemInstanceFactory gameItemInstanceFactory) + { + _randomGenerator = randomGenerator; + _gameItemInstanceFactory = gameItemInstanceFactory; + } + + public async Task HandleAsync(RaidMonsterThrowEvent e, CancellationToken cancellation) + { + const byte minimumDistance = 0; + const byte maximumDistance = 10; + + int length = e.Drops.Count; + + for (int i = 0; i < e.ItemDropsAmount; i++) + { + int number = _randomGenerator.RandomNumber(0, length); + Drop drop = e.Drops[number]; + ThrowEvent(e.MonsterEntity, drop.ItemVNum, drop.Amount, minimumDistance, maximumDistance); + } + + for (int i = 0; i < e.GoldDropsAmount; i++) + { + int gold = _randomGenerator.RandomNumber(e.GoldDropRange.Minimum, e.GoldDropRange.Maximum + 1); + ThrowEvent(e.MonsterEntity, (int)ItemVnums.GOLD, gold, minimumDistance, maximumDistance); + } + } + + private void ThrowEvent(IBattleEntity battleEntity, int itemVNum, int quantity, byte minimumDistance, byte maximumDistance) + { + GameItemInstance newItem = _gameItemInstanceFactory.CreateItem(itemVNum, quantity); + + short rndX = -1; + short rndY = -1; + int count = 0; + while ((rndX == -1 || rndY == -1 || battleEntity.MapInstance.IsBlockedZone(rndX, rndY)) && count < 100) + { + rndX = (short)(battleEntity.PositionX + _randomGenerator.RandomNumber(minimumDistance, maximumDistance + 1) * (_randomGenerator.RandomNumber(0, 2) * 2 - 1)); + rndY = (short)(battleEntity.PositionY + _randomGenerator.RandomNumber(minimumDistance, maximumDistance + 1) * (_randomGenerator.RandomNumber(0, 2) * 2 - 1)); + count++; + } + + var position = new Position(rndX, rndY); + + var item = new MonsterMapItem(position.X, position.Y, newItem, battleEntity.MapInstance); + + battleEntity.MapInstance.AddDrop(item); + battleEntity.BroadcastThrow(item); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Act4/Event/Act4DungeonRewardEventHandler.cs b/srcs/_plugins/Plugin.Act4/Event/Act4DungeonRewardEventHandler.cs new file mode 100644 index 0000000..8356960 --- /dev/null +++ b/srcs/_plugins/Plugin.Act4/Event/Act4DungeonRewardEventHandler.cs @@ -0,0 +1,124 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using PhoenixLib.Logging; +using Plugin.Act4.Const; +using Plugin.Act4.Extension; +using WingsAPI.Game.Extensions.Families; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.Game; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Act4; +using WingsEmu.Game.Act4.Configuration; +using WingsEmu.Game.Act4.Entities; +using WingsEmu.Game.Act4.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Families.Enum; +using WingsEmu.Game.Items; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Raids; +using WingsEmu.Packets.Enums.Chat; +using WingsEmu.Packets.Enums.Families; + +namespace Plugin.Act4.Event; + +public class Act4DungeonRewardEventHandler : IAsyncEventProcessor +{ + private readonly Act4DungeonsConfiguration _act4DungeonsConfiguration; + private readonly IAsyncEventPipeline _asyncEventPipeline; + private readonly IGameItemInstanceFactory _gameItemInstance; + private readonly IGameLanguageService _languageService; + private readonly IRandomGenerator _randomGenerator; + + public Act4DungeonRewardEventHandler(IAsyncEventPipeline asyncEventPipeline, IGameItemInstanceFactory gameItemInstance, IRandomGenerator randomGenerator, + Act4DungeonsConfiguration act4DungeonsConfiguration, IGameLanguageService languageService) + { + _asyncEventPipeline = asyncEventPipeline; + _gameItemInstance = gameItemInstance; + _randomGenerator = randomGenerator; + _act4DungeonsConfiguration = act4DungeonsConfiguration; + _languageService = languageService; + } + + public async Task HandleAsync(Act4DungeonRewardEvent e, CancellationToken cancellation) + { + DungeonInstance dungeonInstance = e.DungeonInstanceWrapper.DungeonInstance; + DungeonSubInstance bossMap = dungeonInstance.DungeonSubInstances.Values.FirstOrDefault(x => 0 < x.Bosses.Count); + if (bossMap == null) + { + Log.Warn($"[ACT4_DUNGEON_SYSTEM] Can't give the Dungeon's Reward due to the impossibility of finding the bossMap. DungeonType: '{dungeonInstance.DungeonType.ToString()}'"); + await _asyncEventPipeline.ProcessEventAsync(new Act4DungeonStopEvent + { + DungeonInstance = dungeonInstance + }, cancellation); + return; + } + + RaidReward raidReward = dungeonInstance.DungeonReward; + + dungeonInstance.FinishSlowMoDate = DateTime.UtcNow + _act4DungeonsConfiguration.DungeonSlowMoDelay; + + dungeonInstance.CleanUpBossMapDate = dungeonInstance.FinishSlowMoDate + _act4DungeonsConfiguration.DungeonBossMapClosureAfterReward; + bossMap.AddEvent(DungeonConstEventKeys.RaidSubInstanceCleanUp, new Act4DungeonBossMapCleanUpEvent + { + DungeonInstance = dungeonInstance, + BossMap = bossMap + }); + + bossMap.LoopWaves.Clear(); + bossMap.LinearWaves.Clear(); + + if (dungeonInstance.DungeonType == DungeonType.Hatus) + { + bossMap.HatusHeads.HeadsState = HatusDragonHeadState.HIDE_HEAD; + bossMap.MapInstance.Broadcast(Act4DungeonExtension.HatusHeadStatePacket(7, bossMap.HatusHeads)); + } + + bool createRaidFinishLog = true; + + var members = bossMap.MapInstance.Sessions.ToList(); + + var randomBag = new RandomBag(_randomGenerator); + foreach (RaidBoxRarity toAdd in raidReward.RaidBox.RaidBoxRarities) + { + randomBag.AddEntry(toAdd, toAdd.Chance); + } + + foreach (IClientSession member in members) + { + if (member == null) + { + continue; + } + + if (createRaidFinishLog) + { + createRaidFinishLog = false; + await member.FamilyAddLogAsync(FamilyLogType.RaidWon, ((short)dungeonInstance.DungeonType).ToString()); + await member.FamilyAddExperience(10000 / bossMap.MapInstance.Sessions.Count, FamXpObtainedFromType.Raid); + } + + byte boxRarity = randomBag.GetRandom().Rarity; + + GameItemInstance rewardBox = _gameItemInstance.CreateItem(raidReward.RaidBox.RewardBox, 1, 0, (sbyte)boxRarity); + await member.AddNewItemToInventory(rewardBox, true, ChatMessageColorType.Yellow, true); + + member.SendMsg(_languageService.GetLanguage(GameDialogKey.ACT4_DUNGEON_SHOUTMESSAGE_BOSS_COMPLETED, member.UserLanguage), MsgMessageType.Middle); + member.SendEmptyRaidBoss(); + } + + await _asyncEventPipeline.ProcessEventAsync(new Act4DungeonWonEvent + { + DungeonInstance = dungeonInstance, + DungeonLeader = members[0], + Members = members + }, cancellation); + + await _asyncEventPipeline.ProcessEventAsync(new Act4DungeonBroadcastPacketEvent + { + DungeonInstance = dungeonInstance + }, cancellation); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Act4/Event/Act4DungeonStopEventHandler.cs b/srcs/_plugins/Plugin.Act4/Event/Act4DungeonStopEventHandler.cs new file mode 100644 index 0000000..42edaf2 --- /dev/null +++ b/srcs/_plugins/Plugin.Act4/Event/Act4DungeonStopEventHandler.cs @@ -0,0 +1,49 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Act4; +using WingsEmu.Game.Act4.Configuration; +using WingsEmu.Game.Act4.Event; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Revival; + +namespace Plugin.Act4.Event; + +public class Act4DungeonStopEventHandler : IAsyncEventProcessor +{ + private readonly IAct4DungeonManager _act4DungeonManager; + private readonly Act4DungeonsConfiguration _act4DungeonsConfiguration; + private readonly IMapManager _mapManager; + + public Act4DungeonStopEventHandler(IAct4DungeonManager act4DungeonManager, IMapManager mapManager, Act4DungeonsConfiguration act4DungeonsConfiguration) + { + _act4DungeonManager = act4DungeonManager; + _mapManager = mapManager; + _act4DungeonsConfiguration = act4DungeonsConfiguration; + } + + public async Task HandleAsync(Act4DungeonStopEvent e, CancellationToken cancellation) + { + _act4DungeonManager.UnregisterDungeon(e.DungeonInstance); + + foreach (DungeonSubInstance subInstance in e.DungeonInstance.DungeonSubInstances.Values) + { + foreach (IClientSession session in subInstance.MapInstance.Sessions.ToList()) + { + if (!session.PlayerEntity.IsAlive()) + { + await session.EmitEventAsync(new RevivalReviveEvent()); + } + + session.ChangeMap(_act4DungeonsConfiguration.DungeonReturnPortalMapId, _act4DungeonsConfiguration.DungeonReturnPortalMapX, _act4DungeonsConfiguration.DungeonReturnPortalMapY); + } + + _mapManager.RemoveMapInstance(subInstance.MapInstance.Id); + subInstance.MapInstance.Destroy(); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Act4/Event/Act4DungeonSystemStartEventHandler.cs b/srcs/_plugins/Plugin.Act4/Event/Act4DungeonSystemStartEventHandler.cs new file mode 100644 index 0000000..554ca0e --- /dev/null +++ b/srcs/_plugins/Plugin.Act4/Event/Act4DungeonSystemStartEventHandler.cs @@ -0,0 +1,117 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using PhoenixLib.Logging; +using WingsEmu.Game; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Act4; +using WingsEmu.Game.Act4.Configuration; +using WingsEmu.Game.Act4.Event; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Entities.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Maps.Event; +using WingsEmu.Game.Networking.Broadcasting; +using WingsEmu.Game.Portals; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.Act4.Event; + +public class Act4DungeonSystemStartEventHandler : IAsyncEventProcessor +{ + private static readonly List PossibleAct4DungeonTypes = new(); + private readonly IAct4DungeonManager _act4DungeonManager; + private readonly Act4DungeonsConfiguration _act4DungeonsConfiguration; + private readonly IAsyncEventPipeline _asyncEventPipeline; + private readonly IGameLanguageService _gameLanguage; + private readonly IMapManager _mapManager; + private readonly IMonsterEntityFactory _monsterEntityFactory; + private readonly INpcMonsterManager _npcMonsterManager; + private readonly IPortalFactory _portalFactory; + private readonly IRandomGenerator _randomGenerator; + private readonly ISessionManager _sessionManager; + + static Act4DungeonSystemStartEventHandler() + { + PossibleAct4DungeonTypes.Add(DungeonType.Berios); + PossibleAct4DungeonTypes.Add(DungeonType.Calvinas); + PossibleAct4DungeonTypes.Add(DungeonType.Hatus); + PossibleAct4DungeonTypes.Add(DungeonType.Morcos); + } + + public Act4DungeonSystemStartEventHandler(IAct4DungeonManager act4DungeonManager, IRandomGenerator randomGenerator, IMapManager mapManager, IAsyncEventPipeline asyncEventPipeline, + Act4DungeonsConfiguration act4DungeonsConfiguration, IMonsterEntityFactory monsterEntityFactory, INpcMonsterManager npcMonsterManager, ISessionManager sessionManager, + IGameLanguageService gameLanguage, IPortalFactory portalFactory) + { + _act4DungeonManager = act4DungeonManager; + _randomGenerator = randomGenerator; + _mapManager = mapManager; + _asyncEventPipeline = asyncEventPipeline; + _act4DungeonsConfiguration = act4DungeonsConfiguration; + _monsterEntityFactory = monsterEntityFactory; + _npcMonsterManager = npcMonsterManager; + _sessionManager = sessionManager; + _gameLanguage = gameLanguage; + _portalFactory = portalFactory; + } + + public async Task HandleAsync(Act4DungeonSystemStartEvent e, CancellationToken cancellation) + { + if (_act4DungeonManager.DungeonsActive) + { + Log.Debug("[ACT4_DUNGEON_SYSTEM] Dungeons are already started, denying new start."); + return; + } + + IMapInstance portalMap = _mapManager.GetBaseMapInstanceByMapId(_act4DungeonsConfiguration.DungeonPortalMapId); + if (portalMap == null) + { + Log.Warn("[ACT4_DUNGEON_SYSTEM] Can't start system. The mapId defined for the dungeon portal in the configuration couldn't be obtained. " + + $"Defined MapId: '{_act4DungeonsConfiguration.DungeonPortalMapId.ToString()}'"); + return; + } + + (FactionType factionType, DungeonType? dungeonType) = e; + DungeonType randomDungeonType = dungeonType ?? PossibleAct4DungeonTypes[_randomGenerator.RandomNumber(0, PossibleAct4DungeonTypes.Count)]; + _act4DungeonManager.EnableDungeons(randomDungeonType, factionType); + + var portalPos = new Position(_act4DungeonsConfiguration.DungeonPortalMapX, _act4DungeonsConfiguration.DungeonPortalMapY); + IPortalEntity portal = _portalFactory.CreatePortal(factionType == FactionType.Angel ? PortalType.AngelRaid : PortalType.DemonRaid, portalMap, portalPos, -1, portalPos); + + await _asyncEventPipeline.ProcessEventAsync(new SpawnPortalEvent(portalMap, portal), cancellation); + + List guardians = factionType == FactionType.Angel ? _act4DungeonsConfiguration.GuardiansForAngels : _act4DungeonsConfiguration.GuardiansForDemons; + var spawnedGuardians = new List(); + foreach (GuardianSpawn guardian in guardians) + { + IMonsterData npcMonster = _npcMonsterManager.GetNpc(guardian.MonsterVnum); + if (npcMonster == null) + { + Log.Warn($"[ACT4_DUNGEON_SYSTEM] Couldn't spawn guardian with VNum: '{guardian.MonsterVnum.ToString()}'"); + continue; + } + + IMonsterEntity monster = _monsterEntityFactory.CreateMonster(npcMonster, portalMap, new MonsterEntityBuilder + { + Direction = guardian.Direction, + IsHostile = true + }); + + await monster.EmitEventAsync(new MapJoinMonsterEntityEvent(monster, guardian.MapX, guardian.MapY)); + + spawnedGuardians.Add(monster); + } + + _act4DungeonManager.SetGuardiansAndPortal(spawnedGuardians, portal); + _sessionManager.Broadcast(x => { return x.GenerateMsgPacket(_gameLanguage.GetLanguage(GameDialogKey.ACT4_DUNGEON_SHOUTMESSAGE_STARTED, x.UserLanguage), MsgMessageType.Middle); }, + new FactionBroadcast(factionType)); + + await _asyncEventPipeline.ProcessEventAsync(new Act4SystemFcBroadcastEvent(), cancellation); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Act4/Event/Act4DungeonSystemStopEventHandler.cs b/srcs/_plugins/Plugin.Act4/Event/Act4DungeonSystemStopEventHandler.cs new file mode 100644 index 0000000..bd658a6 --- /dev/null +++ b/srcs/_plugins/Plugin.Act4/Event/Act4DungeonSystemStopEventHandler.cs @@ -0,0 +1,63 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.PacketGeneration; +using WingsEmu.Game; +using WingsEmu.Game.Act4; +using WingsEmu.Game.Act4.Event; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Entities.Event; +using WingsEmu.Game.Maps; + +namespace Plugin.Act4.Event; + +public class Act4DungeonSystemStopEventHandler : IAsyncEventProcessor +{ + private readonly IAct4DungeonManager _act4DungeonManager; + private readonly IAsyncEventPipeline _asyncEventPipeline; + private readonly IMapManager _mapManager; + + public Act4DungeonSystemStopEventHandler(IAct4DungeonManager act4DungeonManager, IAsyncEventPipeline asyncEventPipeline, IMapManager mapManager) + { + _act4DungeonManager = act4DungeonManager; + _asyncEventPipeline = asyncEventPipeline; + _mapManager = mapManager; + } + + public async Task HandleAsync(Act4DungeonSystemStopEvent e, CancellationToken cancellation) + { + if (!_act4DungeonManager.DungeonsActive) + { + return; + } + + (IReadOnlyList guardiansToRemove, IPortalEntity portal) = _act4DungeonManager.GetAndCleanGuardians(); + + portal.MapInstance.DeletePortal(portal); + + foreach (IMonsterEntity guardian in guardiansToRemove) + { + await guardian.EmitEventAsync(new MapLeaveMonsterEntityEvent(guardian)); + } + + _act4DungeonManager.DisableDungeons(); + + await _asyncEventPipeline.ProcessEventAsync(new Act4SystemFcBroadcastEvent(), cancellation); + + IReadOnlyList dungeons = _act4DungeonManager.Dungeons; + if (dungeons.Count < 1) + { + return; + } + + foreach (DungeonInstance dungeon in dungeons.ToList()) + { + await _asyncEventPipeline.ProcessEventAsync(new Act4DungeonStopEvent + { + DungeonInstance = dungeon + }, cancellation); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Act4/Event/Act4FactionPointsGenerationEventHandler.cs b/srcs/_plugins/Plugin.Act4/Event/Act4FactionPointsGenerationEventHandler.cs new file mode 100644 index 0000000..d9e346d --- /dev/null +++ b/srcs/_plugins/Plugin.Act4/Event/Act4FactionPointsGenerationEventHandler.cs @@ -0,0 +1,45 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game; +using WingsEmu.Game.Act4.Configuration; +using WingsEmu.Game.Act4.Event; +using WingsEmu.Game.Managers; +using WingsEmu.Packets.Enums; + +namespace Plugin.Act4.Event; + +public class Act4FactionPointsGenerationEventHandler : IAsyncEventProcessor +{ + private readonly Act4Configuration _act4Configuration; + private readonly IAct4Manager _act4Manager; + private readonly IRandomGenerator _randomGenerator; + private readonly ISessionManager _sessionManager; + + public Act4FactionPointsGenerationEventHandler(IAct4Manager act4Manager, ISessionManager sessionManager, Act4Configuration act4Configuration, IRandomGenerator randomGenerator) + { + _act4Manager = act4Manager; + _sessionManager = sessionManager; + _act4Configuration = act4Configuration; + _randomGenerator = randomGenerator; + } + + public async Task HandleAsync(Act4FactionPointsGenerationEvent e, CancellationToken cancellation) + { + if (_act4Manager.FactionPointsLocked) + { + return; + } + + int currentSessions = _sessionManager.SessionsCount; + PointGeneration configuration = _act4Configuration.PointGeneration.FirstOrDefault( + x => (x.PlayerAmount.Minimum == null || x.PlayerAmount.Minimum <= currentSessions) && (x.PlayerAmount.Maximum == null || currentSessions <= x.PlayerAmount.Maximum)); + if (configuration == null) + { + return; + } + + _act4Manager.AddFactionPoints(_randomGenerator.RandomNumber(0, 2) == 0 ? FactionType.Angel : FactionType.Demon, configuration.PointsAmount); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Act4/Event/Act4FactionPointsIncreaseEventHandler.cs b/srcs/_plugins/Plugin.Act4/Event/Act4FactionPointsIncreaseEventHandler.cs new file mode 100644 index 0000000..39c3059 --- /dev/null +++ b/srcs/_plugins/Plugin.Act4/Event/Act4FactionPointsIncreaseEventHandler.cs @@ -0,0 +1,24 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Act4.Event; +using WingsEmu.Game.Managers; + +namespace Plugin.Act4.Event; + +public class Act4FactionPointsIncreaseEventHandler : IAsyncEventProcessor +{ + private readonly IAct4Manager _act4Manager; + + public Act4FactionPointsIncreaseEventHandler(IAct4Manager act4Manager) => _act4Manager = act4Manager; + + public async Task HandleAsync(Act4FactionPointsIncreaseEvent e, CancellationToken cancellation) + { + if (_act4Manager.FactionPointsLocked) + { + return; + } + + _act4Manager.AddFactionPoints(e.FactionType, e.PointsToAdd); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Act4/Event/Act4JoinMapEndEventHandler.cs b/srcs/_plugins/Plugin.Act4/Event/Act4JoinMapEndEventHandler.cs new file mode 100644 index 0000000..3a71e2b --- /dev/null +++ b/srcs/_plugins/Plugin.Act4/Event/Act4JoinMapEndEventHandler.cs @@ -0,0 +1,75 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Act4; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Maps.Event; +using WingsEmu.Game.Networking; + +namespace Plugin.Act4.Event; + +public class Act4JoinMapEndEventHandler : IAsyncEventProcessor +{ + private readonly IAct4DungeonManager _act4DungeonManager; + private readonly IAct4Manager _act4Manager; + + public Act4JoinMapEndEventHandler(IAct4Manager act4Manager, IAct4DungeonManager act4DungeonManager) + { + _act4Manager = act4Manager; + _act4DungeonManager = act4DungeonManager; + } + + public async Task HandleAsync(JoinMapEndEvent e, CancellationToken cancellation) + { + e.Sender.SendFcPacket(_act4Manager.GetStatus()); + e.Sender.SendGuriFactionOverridePacket(); + + if (e.JoinedMapInstance.MapInstanceType != MapInstanceType.Act4Dungeon || e.Sender.PlayerEntity.Family == null) + { + return; + } + + DungeonInstance dungeon = _act4DungeonManager.GetDungeon(e.Sender.PlayerEntity.Family.Id); + + if (dungeon == null || !dungeon.DungeonSubInstances.TryGetValue(e.JoinedMapInstance.Id, out DungeonSubInstance dungeonSubInstance)) + { + return; + } + + bool firstPlayerToJoinInBoss = (dungeonSubInstance.LastDungeonWaveLoop == null || dungeonSubInstance.LastDungeonWaveLinear == null || dungeonSubInstance.LastPortalGeneration == null) && + dungeonSubInstance.Bosses.Count > 0; + + DateTime currentTime = DateTime.UtcNow; + e.Sender.SendDungeonPacket(dungeon, dungeonSubInstance, _act4DungeonManager, currentTime); + dungeonSubInstance.LastDungeonWaveLoop ??= currentTime; + dungeonSubInstance.LastDungeonWaveLinear ??= currentTime; + dungeonSubInstance.LastPortalGeneration ??= currentTime; + + if (!firstPlayerToJoinInBoss) + { + return; + } + + dungeon.StartInBoosRoom = currentTime; + foreach (DungeonSubInstance subInstance in dungeon.DungeonSubInstances.Values) + { + foreach (DungeonLoopWave wave in subInstance.LoopWaves) + { + wave.FirstSpawnWave = currentTime + wave.Delay; + } + + if (subInstance == dungeonSubInstance) + { + continue; + } + + foreach (IClientSession session in subInstance.MapInstance.Sessions) + { + session.ChangeMap(dungeonSubInstance.MapInstance, e.Sender.PlayerEntity.PositionX, e.Sender.PlayerEntity.PositionY); + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Act4/Event/Act4KillBonusEventHandler.cs b/srcs/_plugins/Plugin.Act4/Event/Act4KillBonusEventHandler.cs new file mode 100644 index 0000000..b447a3f --- /dev/null +++ b/srcs/_plugins/Plugin.Act4/Event/Act4KillBonusEventHandler.cs @@ -0,0 +1,62 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game.Act4.Configuration; +using WingsEmu.Game.Act4.Event; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Managers; + +namespace Plugin.Act4.Event; + +public sealed class Act4KillBonusEventHandler : IAsyncEventProcessor +{ + private readonly Act4Configuration _act4Configuration; + private readonly IAct4Manager _act4Manager; + + public Act4KillBonusEventHandler(IAct4Manager act4Manager, Act4Configuration act4Configuration) + { + _act4Manager = act4Manager; + _act4Configuration = act4Configuration; + } + + public async Task HandleAsync(KillBonusEvent e, CancellationToken cancellation) + { + IMonsterEntity monsterEntityToAttack = e.MonsterEntity; + + if (monsterEntityToAttack == null) + { + return; + } + + if (monsterEntityToAttack.IsStillAlive) + { + return; + } + + if (!monsterEntityToAttack.MapInstance.HasMapFlag(MapFlags.ACT_4)) + { + return; + } + + if (monsterEntityToAttack.MapInstance.HasMapFlag(MapFlags.HAS_PVE_REPUTATION_ENABLED)) + { + await e.Sender.EmitEventAsync(new GenerateReputationEvent + { + Amount = monsterEntityToAttack.Level / 2, + SendMessage = true + }); + } + + if (_act4Manager.FactionPointsLocked) + { + return; + } + + if (_act4Configuration.PveFactionPoints) + { + await e.Sender.EmitEventAsync(new Act4FactionPointsIncreaseEvent(_act4Configuration.FactionPointsPerPveKill)); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Act4/Event/Act4MukrajuDeathEventHandler.cs b/srcs/_plugins/Plugin.Act4/Event/Act4MukrajuDeathEventHandler.cs new file mode 100644 index 0000000..a096b14 --- /dev/null +++ b/srcs/_plugins/Plugin.Act4/Event/Act4MukrajuDeathEventHandler.cs @@ -0,0 +1,43 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Act4.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.Act4.Event; + +public class Act4MukrajuDeathEventHandler : IAsyncEventProcessor +{ + private readonly IAct4Manager _act4Manager; + private readonly IAsyncEventPipeline _asyncEventPipeline; + private readonly IGameLanguageService _languageService; + private readonly ISessionManager _sessionManager; + + public Act4MukrajuDeathEventHandler(IAct4Manager act4Manager, ISessionManager sessionManager, IGameLanguageService languageService, IAsyncEventPipeline asyncEventPipeline) + { + _act4Manager = act4Manager; + _sessionManager = sessionManager; + _languageService = languageService; + _asyncEventPipeline = asyncEventPipeline; + } + + public async Task HandleAsync(Act4MukrajuDeathEvent e, CancellationToken cancellation) + { + FactionType mukrajuFaction = _act4Manager.MukrajuFaction(); + _act4Manager.UnregisterMukraju(); + + _sessionManager.Broadcast(x => + { + string factionKey = _languageService.GetLanguage(mukrajuFaction == FactionType.Angel ? GameDialogKey.ACT4_SHOUTMESSAGE_CAMP_ANGELS : GameDialogKey.ACT4_SHOUTMESSAGE_CAMP_DEMONS, + x.UserLanguage); + + return x.GenerateMsgPacket(_languageService.GetLanguageFormat(GameDialogKey.ACT4_SHOUTMESSAGE_MUKRAJU_DEATH, x.UserLanguage, factionKey), MsgMessageType.Middle); + }); + + await _asyncEventPipeline.ProcessEventAsync(new Act4DungeonSystemStartEvent(mukrajuFaction), cancellation); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Act4/Event/Act4MukrajuDespawnEventHandler.cs b/srcs/_plugins/Plugin.Act4/Event/Act4MukrajuDespawnEventHandler.cs new file mode 100644 index 0000000..c987500 --- /dev/null +++ b/srcs/_plugins/Plugin.Act4/Event/Act4MukrajuDespawnEventHandler.cs @@ -0,0 +1,40 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Act4.Event; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.Act4.Event; + +public class Act4MukrajuDespawnEventHandler : IAsyncEventProcessor +{ + private readonly IAct4Manager _act4Manager; + private readonly IGameLanguageService _languageService; + private readonly ISessionManager _sessionManager; + + public Act4MukrajuDespawnEventHandler(IAct4Manager act4Manager, ISessionManager sessionManager, IGameLanguageService languageService) + { + _act4Manager = act4Manager; + _sessionManager = sessionManager; + _languageService = languageService; + } + + public async Task HandleAsync(Act4MukrajuDespawnEvent e, CancellationToken cancellation) + { + FactionType mukrajuFaction = _act4Manager.MukrajuFaction(); + IMonsterEntity mukraju = _act4Manager.UnregisterMukraju(); + mukraju.MapInstance.DespawnMonster(mukraju); + + _sessionManager.Broadcast(x => + { + string factionKey = _languageService.GetLanguage(mukrajuFaction == FactionType.Angel ? GameDialogKey.ACT4_SHOUTMESSAGE_CAMP_ANGELS : GameDialogKey.ACT4_SHOUTMESSAGE_CAMP_DEMONS, + x.UserLanguage); + return x.GenerateMsgPacket(_languageService.GetLanguageFormat(GameDialogKey.ACT4_SHOUTMESSAGE_MUKRAJU_DESPAWNED, x.UserLanguage, factionKey), MsgMessageType.Middle); + }); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Act4/Event/Act4MukrajuSpawnEventHandler.cs b/srcs/_plugins/Plugin.Act4/Event/Act4MukrajuSpawnEventHandler.cs new file mode 100644 index 0000000..49308d4 --- /dev/null +++ b/srcs/_plugins/Plugin.Act4/Event/Act4MukrajuSpawnEventHandler.cs @@ -0,0 +1,88 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using PhoenixLib.Logging; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Act4.Configuration; +using WingsEmu.Game.Act4.Event; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Entities.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Npcs; +using WingsEmu.Game.Triggers; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.Act4.Event; + +public class Act4MukrajuSpawnEventHandler : IAsyncEventProcessor +{ + private readonly Act4Configuration _act4Configuration; + private readonly IAct4Manager _act4Manager; + private readonly IAsyncEventPipeline _asyncEventPipeline; + private readonly IGameLanguageService _languageService; + private readonly IMapManager _mapManager; + private readonly IMonsterEntityFactory _monsterEntityFactory; + private readonly INpcMonsterManager _npcMonsterManager; + private readonly ISessionManager _sessionManager; + + public Act4MukrajuSpawnEventHandler(Act4Configuration act4Configuration, IMapManager mapManager, IAsyncEventPipeline asyncEventPipeline, IAct4Manager act4Manager, ISessionManager sessionManager, + IGameLanguageService languageService, IMonsterEntityFactory monsterEntityFactory, INpcMonsterManager npcMonsterManager) + { + _act4Configuration = act4Configuration; + _mapManager = mapManager; + _asyncEventPipeline = asyncEventPipeline; + _act4Manager = act4Manager; + _sessionManager = sessionManager; + _languageService = languageService; + _monsterEntityFactory = monsterEntityFactory; + _npcMonsterManager = npcMonsterManager; + } + + public async Task HandleAsync(Act4MukrajuSpawnEvent e, CancellationToken cancellation) + { + bool isAngel = e.FactionType == FactionType.Angel; + MukrajuSpawn mukrajuSpawn = isAngel ? _act4Configuration.AngelMukrajuSpawn : _act4Configuration.DemonMukrajuSpawn; + + int mapId = mukrajuSpawn.MapId; + int monsterVNum = mukrajuSpawn.MonsterVnum; + short mapX = mukrajuSpawn.MapX; + short mapY = mukrajuSpawn.MapY; + + IMapInstance mapToSpawn = _mapManager.GetBaseMapInstanceByMapId(mapId); + if (mapToSpawn == null) + { + Log.Warn($"Mukraju couldn't be spawned be spawned because the specified map hasn't been instantiated. MapId: {mapId.ToString()}"); + return; + } + + IMonsterData data = new MonsterData(_npcMonsterManager.GetNpc(monsterVNum)) + { + SuggestedFaction = isAngel ? FactionType.Demon : FactionType.Angel + }; + + IMonsterEntity mukraju = _monsterEntityFactory.CreateMonster(monsterVNum, data, mapToSpawn, new MonsterEntityBuilder + { + PositionX = mapX, + PositionY = mapY, + IsHostile = true, + IsWalkingAround = true, + IsRespawningOnDeath = false + }); + + mukraju.AddEvent(BattleTriggers.OnDeath, new Act4MukrajuDeathEvent(e.FactionType), true); + await mukraju.EmitEventAsync(new MapJoinMonsterEntityEvent(mukraju, mapX, mapY)); + + _act4Manager.RegisterMukraju(DateTime.UtcNow, mukraju, e.FactionType); + + _sessionManager.Broadcast(x => + { + string faction = _languageService.GetLanguage(isAngel ? GameDialogKey.ACT4_SHOUTMESSAGE_CAMP_ANGELS : GameDialogKey.ACT4_SHOUTMESSAGE_CAMP_DEMONS, x.UserLanguage); + return x.GenerateMsgPacket(_languageService.GetLanguageFormat(GameDialogKey.ACT4_SHOUTMESSAGE_MUKRAJU_SPAWNED, x.UserLanguage, faction), MsgMessageType.Middle); + }); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Act4/Event/Act4PutFlagEventHandler.cs b/srcs/_plugins/Plugin.Act4/Event/Act4PutFlagEventHandler.cs new file mode 100644 index 0000000..f02353f --- /dev/null +++ b/srcs/_plugins/Plugin.Act4/Event/Act4PutFlagEventHandler.cs @@ -0,0 +1,122 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Communication.ServerApi.Protocol; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Act4; +using WingsEmu.Game.Act4.Event; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Entities.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Helpers; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.Act4.Event; + +public class Act4PutFlagEventHandler : IAsyncEventProcessor +{ + private readonly IAct4FlagManager _act4FlagManager; + private readonly SerializableGameServer _gameServer; + private readonly IMonsterEntityFactory _monsterEntityFactory; + private readonly ISessionManager _sessionManager; + + public Act4PutFlagEventHandler(IAct4FlagManager act4FlagManager, IMonsterEntityFactory monsterEntityFactory, ISessionManager sessionManager, SerializableGameServer gameServer) + { + _act4FlagManager = act4FlagManager; + _monsterEntityFactory = monsterEntityFactory; + _sessionManager = sessionManager; + _gameServer = gameServer; + } + + public async Task HandleAsync(Act4PutFlagEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + GameItemInstance item = e.InventoryItem.ItemInstance; + if (!session.CurrentMapInstance.HasMapFlag(MapFlags.ACT_4)) + { + return; + } + + if (_gameServer.ChannelType != GameChannelType.ACT_4) + { + return; + } + + if (!session.CurrentMapInstance.HasMapFlag(MapFlags.IS_BASE_MAP)) + { + return; + } + + switch (item.ItemVNum) + { + case (short)ItemVnums.ANGEL_BASE_FLAG when session.PlayerEntity.Faction != FactionType.Angel: + case (short)ItemVnums.DEMON_BASE_FLAG when session.PlayerEntity.Faction != FactionType.Demon: + return; + } + + if (session.CurrentMapInstance.MapVnum is (short)MapIds.ACT4_ANGEL_CITADEL or (short)MapIds.ACT4_DEMON_CITADEL) + { + return; + } + + if (session.PlayerEntity.Level < 70) + { + session.SendMsg(session.GetLanguage(GameDialogKey.INFORMATION_CHATMESSAGE_NOT_REQUIERED_LEVEL), MsgMessageType.Middle); + return; + } + + if (session.PlayerEntity.Reput < 100000) + { + session.SendMsg(session.GetLanguage(GameDialogKey.INFORMATION_CHATMESSAGE_NOT_ENOUGH_REPUT), MsgMessageType.Middle); + return; + } + + FactionType faction = session.PlayerEntity.Faction; + + MapLocation map = faction == FactionType.Angel ? _act4FlagManager.AngelFlag : _act4FlagManager.DemonFlag; + if (map != null) + { + session.SendMsg(session.GetLanguage(GameDialogKey.ACT4_SHOUTMESSAGE_FLAG_ALREADY_IN), MsgMessageType.Middle); + return; + } + + MapLocation placement = new() + { + MapInstanceId = session.CurrentMapInstance.Id, + X = session.PlayerEntity.PositionX, + Y = session.PlayerEntity.PositionY + }; + + switch (faction) + { + case FactionType.Angel: + _act4FlagManager.SetAngelFlag(placement); + break; + case FactionType.Demon: + _act4FlagManager.SetDemonFlag(placement); + break; + default: + return; + } + + IMonsterEntity monster = _monsterEntityFactory.CreateMonster(e.InventoryItem.ItemInstance.GameItem.Data[2], session.CurrentMapInstance, new MonsterEntityBuilder + { + IsHostile = false, + IsWalkingAround = false, + FactionType = faction + }); + + GameDialogKey message = faction == FactionType.Angel ? GameDialogKey.ACT4_SHOUTMESSAGE_FLAG_PLACED_ANGEL : GameDialogKey.ACT4_SHOUTMESSAGE_FLAG_PLACED_DEMON; + + _sessionManager.Broadcast(x => x.GenerateMsgPacket(x.GetLanguage(message), MsgMessageType.Middle)); + await monster.EmitEventAsync(new MapJoinMonsterEntityEvent(monster, placement.X, placement.Y)); + await session.RemoveItemFromInventory(item: e.InventoryItem); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Act4/Event/Act4SystemFcBroadcastEventHandler.cs b/srcs/_plugins/Plugin.Act4/Event/Act4SystemFcBroadcastEventHandler.cs new file mode 100644 index 0000000..0498989 --- /dev/null +++ b/srcs/_plugins/Plugin.Act4/Event/Act4SystemFcBroadcastEventHandler.cs @@ -0,0 +1,32 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Act4.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking.Broadcasting; +using WingsEmu.Packets.Enums; + +namespace Plugin.Act4.Event; + +public class Act4SystemFcBroadcastEventHandler : IAsyncEventProcessor +{ + private readonly IAct4Manager _act4Manager; + private readonly ISessionManager _sessionManager; + + public Act4SystemFcBroadcastEventHandler(IAct4Manager act4Manager, ISessionManager sessionManager) + { + _act4Manager = act4Manager; + _sessionManager = sessionManager; + } + + public async Task HandleAsync(Act4SystemFcBroadcastEvent e, CancellationToken cancellation) + { + Act4Status status = _act4Manager.GetStatus(); + string angelPacket = UiPacketExtension.GenerateFcPacket(FactionType.Angel, status); + string demonPacket = UiPacketExtension.GenerateFcPacket(FactionType.Demon, status); + + _sessionManager.Broadcast(angelPacket, new FactionBroadcast(FactionType.Angel)); + _sessionManager.Broadcast(demonPacket, new FactionBroadcast(FactionType.Demon)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Act4/Event/PortalTriggerAct4EventHandler.cs b/srcs/_plugins/Plugin.Act4/Event/PortalTriggerAct4EventHandler.cs new file mode 100644 index 0000000..161f044 --- /dev/null +++ b/srcs/_plugins/Plugin.Act4/Event/PortalTriggerAct4EventHandler.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Act4.Configuration; +using WingsEmu.Game.Act4.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Maps.Event; +using WingsEmu.Packets.Enums; + +namespace Plugin.Act4.Event; + +public class PortalTriggerAct4EventHandler : IAsyncEventProcessor +{ + private readonly Act4Configuration _act4Configuration; + private readonly IGameLanguageService _languageService; + private readonly IMapManager _mapManager; + + public PortalTriggerAct4EventHandler(Act4Configuration act4Configuration, IGameLanguageService languageService, IMapManager mapManager) + { + _act4Configuration = act4Configuration; + _languageService = languageService; + _mapManager = mapManager; + } + + public async Task HandleAsync(PortalTriggerEvent e, CancellationToken cancellation) + { + if (!e.Sender.CurrentMapInstance.HasMapFlag(MapFlags.ACT_4)) + { + return; + } + + if (e.Sender.CurrentMapInstance.MapInstanceType == MapInstanceType.TimeSpaceInstance) + { + return; + } + + List bannedMapIds = e.Sender.PlayerEntity.Faction == FactionType.Angel ? _act4Configuration.BannedMapIdsToAngels : _act4Configuration.BannedMapIdsToDemons; + if (e.Portal.DestinationMapInstance != null && bannedMapIds.Contains(e.Portal.DestinationMapInstance.MapId)) + { + e.Sender.SendInformationChatMessage(_languageService.GetLanguage(GameDialogKey.PORTAL_CHATMESSAGE_BLOCKED, e.Sender.UserLanguage)); + return; + } + + e.Sender.PlayerEntity.LastPortal = DateTime.UtcNow; + + if (e.Portal.Type == PortalType.AngelRaid || e.Portal.Type == PortalType.DemonRaid) + { + await e.Sender.EmitEventAsync(new Act4DungeonEnterEvent + { + Confirmed = e.Confirmed + }); + return; + } + + if (e.Portal.DestinationMapInstance == null) + { + return; + } + + //TP Logic + if (e.Portal.DestinationX == -1 && e.Portal.DestinationY == -1) + { + await _mapManager.TeleportOnRandomPlaceInMapAsync(e.Sender, e.Portal.DestinationMapInstance); + return; + } + + if (e.Portal.DestinationMapInstance.Id == e.Sender.PlayerEntity.MapInstanceId && e.Portal.DestinationX.HasValue && e.Portal.DestinationY.HasValue) + { + e.Sender.PlayerEntity.TeleportOnMap(e.Portal.DestinationX.Value, e.Portal.DestinationY.Value, true); + return; + } + + e.Sender.ChangeMap(e.Portal.DestinationMapInstance, e.Portal.DestinationX, e.Portal.DestinationY); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Act4/Event/PortalTriggerDungeonEventHandler.cs b/srcs/_plugins/Plugin.Act4/Event/PortalTriggerDungeonEventHandler.cs new file mode 100644 index 0000000..56695bd --- /dev/null +++ b/srcs/_plugins/Plugin.Act4/Event/PortalTriggerDungeonEventHandler.cs @@ -0,0 +1,82 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Act4.Configuration; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Maps.Event; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace Plugin.Act4.Event; + +public class PortalTriggerDungeonEventHandler : IAsyncEventProcessor +{ + private readonly Act4DungeonsConfiguration _act4DungeonsConfiguration; + private readonly IMapManager _mapManager; + + public PortalTriggerDungeonEventHandler(IMapManager mapManager, Act4DungeonsConfiguration act4DungeonsConfiguration) + { + _mapManager = mapManager; + _act4DungeonsConfiguration = act4DungeonsConfiguration; + } + + public async Task HandleAsync(PortalTriggerEvent e, CancellationToken cancellation) + { + if (e.Sender.CurrentMapInstance.MapInstanceType != MapInstanceType.Act4Dungeon) + { + return; + } + + switch (e.Portal.Type) + { + case PortalType.TSNormal: + if (!e.Confirmed && e.Portal.DestinationMapInstance == null) // Tundra portal + { + e.Sender.SendQnaPacket("preq 1", e.Sender.GetLanguage(GameDialogKey.ACT4_ASK_DIALOG_DUNGEON_EXIT)); + return; + } + + break; + case PortalType.Open: + break; + default: + return; + } + + e.Sender.PlayerEntity.LastPortal = DateTime.UtcNow; + + if (e.Portal.DestinationMapInstance == null) + { + e.Sender.ChangeMap(_act4DungeonsConfiguration.DungeonReturnPortalMapId, _act4DungeonsConfiguration.DungeonReturnPortalMapX, _act4DungeonsConfiguration.DungeonReturnPortalMapY); + return; + } + + await ProcessTeleport(e.Sender, e.Portal); + } + + private async Task ProcessTeleport(IClientSession session, IPortalEntity portal) + { + if (portal.DestinationMapInstance == null) + { + return; + } + + if (portal.DestinationX == -1 && portal.DestinationY == -1) + { + await _mapManager.TeleportOnRandomPlaceInMapAsync(session, portal.DestinationMapInstance); + return; + } + + if (portal.DestinationMapInstance.Id == session.PlayerEntity.MapInstanceId && portal.DestinationX.HasValue && portal.DestinationY.HasValue) + { + session.PlayerEntity.TeleportOnMap(portal.DestinationX.Value, portal.DestinationY.Value, true); + return; + } + + session.ChangeMap(portal.DestinationMapInstance, portal.DestinationX, portal.DestinationY); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Act4/Event/RevivalAskEventDungeonHandler.cs b/srcs/_plugins/Plugin.Act4/Event/RevivalAskEventDungeonHandler.cs new file mode 100644 index 0000000..89098f6 --- /dev/null +++ b/srcs/_plugins/Plugin.Act4/Event/RevivalAskEventDungeonHandler.cs @@ -0,0 +1,42 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using Plugin.Act4.Extension; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Act4.Configuration; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Revival; + +namespace Plugin.Act4.Event; + +public class RevivalAskEventDungeonHandler : IAsyncEventProcessor +{ + private readonly Act4DungeonsConfiguration _act4DungeonsConfiguration; + private readonly IGameLanguageService _languageService; + + public RevivalAskEventDungeonHandler(IGameLanguageService languageService, Act4DungeonsConfiguration act4DungeonsConfiguration) + { + _languageService = languageService; + _act4DungeonsConfiguration = act4DungeonsConfiguration; + } + + public async Task HandleAsync(RevivalAskEvent e, CancellationToken cancellation) + { + if (e.Sender.PlayerEntity.IsAlive() || e.AskRevivalType != AskRevivalType.DungeonRevival) + { + return; + } + + int reputationCost = e.Sender.GetDungeonReputationRequirement(_act4DungeonsConfiguration.DungeonEntryCostMultiplier); + if (e.Sender.PlayerEntity.Reput < reputationCost) + { + e.Sender.PlayerEntity.UpdateRevival(DateTime.MinValue, RevivalType.DontPayRevival, ForcedType.Forced); + return; + } + + e.Sender.SendDialog(CharacterPacketExtension.GenerateRevivalPacket(RevivalType.TryPayRevival), CharacterPacketExtension.GenerateRevivalPacket(RevivalType.DontPayRevival), + _languageService.GetLanguageFormat(GameDialogKey.ACT4_DUNGEON_REVIVAL_DIALOG, e.Sender.UserLanguage, reputationCost.ToString())); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Act4/Event/RevivalEventAct4Handler.cs b/srcs/_plugins/Plugin.Act4/Event/RevivalEventAct4Handler.cs new file mode 100644 index 0000000..3653a1d --- /dev/null +++ b/srcs/_plugins/Plugin.Act4/Event/RevivalEventAct4Handler.cs @@ -0,0 +1,152 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.Groups; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage.Configuration; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Extensions.Mates; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Revival; + +namespace Plugin.Act4.Event; + +public class RevivalEventAct4Handler : IAsyncEventProcessor +{ + private readonly IBuffFactory _buffFactory; + private readonly IItemsManager _itemsManager; + private readonly IGameLanguageService _languageService; + private readonly PlayerRevivalConfiguration _revivalConfiguration; + private readonly ISpPartnerConfiguration _spPartner; + + public RevivalEventAct4Handler(IGameLanguageService languageService, GameRevivalConfiguration revivalConfiguration, IBuffFactory buffFactory, IItemsManager itemsManager, + ISpPartnerConfiguration spPartner) + { + _languageService = languageService; + _buffFactory = buffFactory; + _itemsManager = itemsManager; + _spPartner = spPartner; + _revivalConfiguration = revivalConfiguration.PlayerRevivalConfiguration; + } + + public async Task HandleAsync(RevivalReviveEvent e, CancellationToken cancellation) + { + IClientSession sender = e.Sender; + IPlayerEntity character = e.Sender.PlayerEntity; + + if (!sender.CurrentMapInstance.HasMapFlag(MapFlags.ACT_4)) + { + return; + } + + if (sender.CurrentMapInstance.MapInstanceType == MapInstanceType.Act4Dungeon) + { + return; + } + + if (e.Forced == ForcedType.Act4SealRevival) + { + if (character.IsAlive()) + { + return; + } + + await character.Restore(restoreMates: false); + + foreach (IMateEntity mate in character.MateComponent.TeamMembers()) + { + character.MapInstance.RemoveMate(mate); + character.MapInstance.Broadcast(mate.GenerateOut()); + } + + character.IsSeal = true; + character.Morph = 1564; + sender.BroadcastCMode(); + sender.BroadcastRevive(); + sender.UpdateVisibility(); + sender.SendBuffsPacket(); + sender.PlayerEntity.UpdateRevival(DateTime.UtcNow + _revivalConfiguration.Act4RevivalDelay, RevivalType.DontPayRevival, ForcedType.Forced); + return; + } + + await character.Restore(restoreMates: false); + character.IsSeal = false; + character.DisableRevival(); + + bool hasPaidPenalization = false; + if (e.RevivalType == RevivalType.TryPayRevival) + { + hasPaidPenalization = await TryPayPenalization(character, _revivalConfiguration.PlayerRevivalPenalization); + } + else if (e.Forced == ForcedType.HolyRevival) + { + hasPaidPenalization = true; + } + + if (hasPaidPenalization) + { + await sender.EmitEventAsync(new GetDefaultMorphEvent()); + sender.RefreshStat(); + sender.BroadcastTeleportPacket(); + sender.BroadcastInTeamMembers(_languageService, _spPartner); + sender.RefreshParty(_spPartner); + } + else + { + character.Hp = character.MaxHp; + character.Mp = character.MaxMp; + await sender.Respawn(); + await sender.EmitEventAsync(new GetDefaultMorphEvent()); + } + + sender.BroadcastRevive(); + sender.UpdateVisibility(); + await sender.CheckPartnerBuff(); + sender.SendBuffsPacket(); + + if (!hasPaidPenalization && e.RevivalType != RevivalType.DontPayRevival && character.Level > _revivalConfiguration.PlayerRevivalPenalization.MaxLevelWithoutRevivalPenalization) + { + await character.AddBuffAsync(_buffFactory.CreateBuff(_revivalConfiguration.PlayerRevivalPenalization.BaseMapRevivalPenalizationDebuff, character)); + } + } + + private async Task TryPayPenalization(IPlayerEntity character, PlayerRevivalPenalization playerRevivalPenalization) + { + if (character.Level <= playerRevivalPenalization.MaxLevelWithoutRevivalPenalization) + { + await character.Restore(restoreMates: false); + return true; + } + + int item = playerRevivalPenalization.BaseMapRevivalPenalizationSaver; + short amount = (short)playerRevivalPenalization.BaseMapRevivalPenalizationSaverAmount; + string itemName = _languageService.GetItemName(_itemsManager.GetItem(item), character.Session); + + if (!character.HasItem(item, amount)) + { + string tmp = _languageService.GetLanguageFormat(GameDialogKey.INVENTORY_SHOUTMESSAGE_NOT_ENOUGH_ITEMS, character.Session.UserLanguage, amount, itemName); + character.Session.SendErrorChatMessage(tmp); + return false; + } + + await character.Session.RemoveItemFromInventory(item, amount); + + string chatMessage = _languageService.GetLanguageFormat(GameDialogKey.INFORMATION_CHATMESSAGE_REQUIRED_ITEM_EXPENDED, character.Session.UserLanguage, itemName, amount); + character.Session.SendErrorChatMessage(chatMessage); + + character.Hp = character.MaxHp / 2; + character.Mp = character.MaxMp / 2; + return true; + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Act4/Event/RevivalEventDungeonHandler.cs b/srcs/_plugins/Plugin.Act4/Event/RevivalEventDungeonHandler.cs new file mode 100644 index 0000000..678f6f6 --- /dev/null +++ b/srcs/_plugins/Plugin.Act4/Event/RevivalEventDungeonHandler.cs @@ -0,0 +1,90 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using Plugin.Act4.Extension; +using WingsAPI.Game.Extensions.Groups; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage.Configuration; +using WingsEmu.Game.Act4; +using WingsEmu.Game.Act4.Configuration; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Revival; + +namespace Plugin.Act4.Event; + +public class RevivalEventDungeonHandler : IAsyncEventProcessor +{ + private readonly IAct4DungeonManager _act4DungeonManager; + private readonly Act4DungeonsConfiguration _act4DungeonsConfiguration; + private readonly IGameLanguageService _gameLanguage; + private readonly ISpPartnerConfiguration _spPartnerConfiguration; + + public RevivalEventDungeonHandler(IAct4DungeonManager act4DungeonManager, Act4DungeonsConfiguration act4DungeonsConfiguration, IGameLanguageService gameLanguage, + ISpPartnerConfiguration spPartnerConfiguration) + { + _act4DungeonManager = act4DungeonManager; + _act4DungeonsConfiguration = act4DungeonsConfiguration; + _gameLanguage = gameLanguage; + _spPartnerConfiguration = spPartnerConfiguration; + } + + public async Task HandleAsync(RevivalReviveEvent e, CancellationToken cancellation) + { + if (e.Sender.PlayerEntity.IsAlive()) + { + return; + } + + if (e.Sender.CurrentMapInstance.MapInstanceType != MapInstanceType.Act4Dungeon) + { + return; + } + + if (!e.Sender.PlayerEntity.IsInFamily()) + { + return; + } + + DungeonInstance dungeon = _act4DungeonManager.GetDungeon(e.Sender.PlayerEntity.Family.Id); + if (dungeon == null) + { + return; + } + + e.Sender.PlayerEntity.DisableRevival(); + + if (dungeon.DungeonSubInstances.TryGetValue(e.Sender.CurrentMapInstance.Id, out DungeonSubInstance subInstance)) + { + if (subInstance != null && subInstance.Bosses.Count > 0) + { + dungeon.PlayerDeathInBossRoom = true; + } + } + + if (e.RevivalType == RevivalType.DontPayRevival && e.Forced != ForcedType.HolyRevival) + { + e.Sender.UpdateVisibility(); + e.Sender.PlayerEntity.Hp = 1; + e.Sender.PlayerEntity.Mp = 1; + await e.Sender.PlayerEntity.Restore(restoreHealth: false, restoreMana: false, restoreMates: false); + e.Sender.ChangeMap(dungeon.SpawnInstance.MapInstance, dungeon.SpawnPoint.X, dungeon.SpawnPoint.Y); + e.Sender.SendBuffsPacket(); + return; + } + + if (e.Forced != ForcedType.HolyRevival) + { + e.Sender.PlayerEntity.RemoveReputation(e.Sender.GetDungeonReputationRequirement(_act4DungeonsConfiguration.DungeonEntryCostMultiplier)); + } + + e.Sender.UpdateVisibility(); + await e.Sender.PlayerEntity.Restore(restoreMates: false); + e.Sender.BroadcastRevive(); + e.Sender.BroadcastInTeamMembers(_gameLanguage, _spPartnerConfiguration); + e.Sender.RefreshParty(_spPartnerConfiguration); + await e.Sender.CheckPartnerBuff(); + e.Sender.SendBuffsPacket(); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Act4/Event/RevivalStartProcedureEventAct4Handler.cs b/srcs/_plugins/Plugin.Act4/Event/RevivalStartProcedureEventAct4Handler.cs new file mode 100644 index 0000000..b7b4092 --- /dev/null +++ b/srcs/_plugins/Plugin.Act4/Event/RevivalStartProcedureEventAct4Handler.cs @@ -0,0 +1,292 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.DAL.Redis.Locks; +using PhoenixLib.Events; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Act4.Configuration; +using WingsEmu.Game.Act4.Event; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Entities.Extensions; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Networking.Broadcasting; +using WingsEmu.Game.Revival; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.Act4.Event; + +public class RevivalStartProcedureEventAct4Handler : IAsyncEventProcessor +{ + private readonly Act4Configuration _act4Configuration; + private readonly IAsyncEventPipeline _asyncEventPipeline; + private readonly IGameLanguageService _languageService; + private readonly IExpirableLockService _lockService; + private readonly GameMinMaxConfiguration _minMaxConfiguration; + private readonly IRankingManager _rankingManager; + private readonly IReputationConfiguration _reputationConfiguration; + private readonly PlayerRevivalConfiguration _revivalConfiguration; + private readonly ISessionManager _sessionManager; + + public RevivalStartProcedureEventAct4Handler(GameRevivalConfiguration gameRevivalConfiguration, Act4Configuration act4Configuration, IAsyncEventPipeline asyncEventPipeline, + GameMinMaxConfiguration minMaxConfiguration, IGameLanguageService languageService, ISessionManager sessionManager, IReputationConfiguration reputationConfiguration, + IRankingManager rankingManager, IExpirableLockService lockService) + { + _act4Configuration = act4Configuration; + _asyncEventPipeline = asyncEventPipeline; + _minMaxConfiguration = minMaxConfiguration; + _languageService = languageService; + _sessionManager = sessionManager; + _reputationConfiguration = reputationConfiguration; + _rankingManager = rankingManager; + _lockService = lockService; + _revivalConfiguration = gameRevivalConfiguration.PlayerRevivalConfiguration; + } + + public async Task HandleAsync(RevivalStartProcedureEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + IBattleEntity killer = e.Killer; + if (session.PlayerEntity.IsAlive()) + { + return; + } + + if (!session.CurrentMapInstance.HasMapFlag(MapFlags.ACT_4)) + { + return; + } + + if (session.CurrentMapInstance.MapInstanceType == MapInstanceType.Act4Dungeon) + { + return; + } + + if (session.PlayerEntity.IsOnVehicle) + { + await session.EmitEventAsync(new RemoveVehicleEvent()); + } + + await session.PlayerEntity.RemoveBuffsOnDeathAsync(); + session.RefreshStat(); + + if (killer?.Faction != session.PlayerEntity.Faction) + { + IPlayerEntity playerEntity = killer switch + { + IPlayerEntity player => player, + IMateEntity mateEntity => mateEntity.Owner, + IMonsterEntity monsterEntity => monsterEntity.SummonerType is VisualType.Player && monsterEntity.SummonerId.HasValue + ? monsterEntity.MapInstance.GetCharacterById(monsterEntity.SummonerId.Value) + : null, + _ => null + }; + + if (playerEntity != null) + { + if (!session.PlayerEntity.IsGettingLosingReputation) + { + if (session.PlayerEntity.DeathsOnAct4 < 10) + { + session.PlayerEntity.DeathsOnAct4++; + } + else + { + session.PlayerEntity.IsGettingLosingReputation = true; + await _lockService.TryAddTemporaryLockAsync($"game:locks:character:{session.PlayerEntity.Id}:act-4-less-rep", DateTime.UtcNow.Date.AddDays(1)); + session.SendChatMessage(session.GetLanguage(GameDialogKey.ACT4_CHATMESSAGE_LESS_REPUTATION), ChatMessageColorType.Red); + } + } + else + { + session.SendChatMessage(session.GetLanguage(GameDialogKey.ACT4_CHATMESSAGE_LESS_REPUTATION), ChatMessageColorType.Red); + } + + await HandleReputation(session, playerEntity); + await playerEntity.Session.EmitEventAsync(new Act4KillEvent { TargetId = session.PlayerEntity.Id }); + playerEntity.Act4Kill++; + session.PlayerEntity.Act4Dead++; + + playerEntity.Session.SendChatMessage(playerEntity.Session.GetLanguageFormat(GameDialogKey.ACT4_CHATMESSAGE_KILL_INFO, playerEntity.Act4Kill, playerEntity.Act4Dead), + ChatMessageColorType.Yellow); + session.SendChatMessage(session.GetLanguageFormat(GameDialogKey.ACT4_CHATMESSAGE_KILL_INFO, session.PlayerEntity.Act4Kill, session.PlayerEntity.Act4Dead), ChatMessageColorType.Yellow); + + switch (session.PlayerEntity.Faction) + { + case FactionType.Angel: + + // For every demon, send "killer killed an angel" + _sessionManager.Broadcast(x => + { + string factionKey = _languageService.GetLanguage(GameDialogKey.ACT4_CHATMESSAGE_PVP_ANGELS, x.UserLanguage); + + return session.PlayerEntity.GenerateSayPacket(_languageService.GetLanguageFormat(GameDialogKey.ACT4_CHATMESSAGE_PVP_KILL, x.UserLanguage, + factionKey, playerEntity.Name), ChatMessageColorType.Green); + }, new FactionBroadcast(FactionType.Demon)); + + // For every angel, send "nick died from a demon" + _sessionManager.Broadcast(x => + { + string factionKey = _languageService.GetLanguage(GameDialogKey.ACT4_CHATMESSAGE_PVP_DEMONS, x.UserLanguage); + + return session.PlayerEntity.GenerateSayPacket(_languageService.GetLanguageFormat(GameDialogKey.ACT4_CHATMESSAGE_PVP_DEATH, x.UserLanguage, + factionKey, session.PlayerEntity.Name), ChatMessageColorType.Red); + }, new FactionBroadcast(FactionType.Angel)); + + break; + case FactionType.Demon: + + _sessionManager.Broadcast(x => + { + string factionKey = _languageService.GetLanguage(GameDialogKey.ACT4_CHATMESSAGE_PVP_ANGELS, x.UserLanguage); + + return session.PlayerEntity.GenerateSayPacket(_languageService.GetLanguageFormat(GameDialogKey.ACT4_CHATMESSAGE_PVP_DEATH, x.UserLanguage, + factionKey, session.PlayerEntity.Name), ChatMessageColorType.Red); + }, new FactionBroadcast(FactionType.Demon)); + + _sessionManager.Broadcast(x => + { + string factionKey = _languageService.GetLanguage(GameDialogKey.ACT4_CHATMESSAGE_PVP_DEMONS, x.UserLanguage); + + return session.PlayerEntity.GenerateSayPacket(_languageService.GetLanguageFormat(GameDialogKey.ACT4_CHATMESSAGE_PVP_KILL, x.UserLanguage, + factionKey, playerEntity.Name), ChatMessageColorType.Green); + }, new FactionBroadcast(FactionType.Angel)); + + break; + } + } + } + + DateTime actualTime = DateTime.UtcNow; + + if (killer is null or IMonsterEntity { SummonerType: not VisualType.Player }) + { + await Penalty(e); + session.PlayerEntity.UpdateRevival(actualTime + _revivalConfiguration.ForcedRevivalDelay, RevivalType.DontPayRevival, ForcedType.Forced); + session.PlayerEntity.UpdateAskRevival(actualTime + _revivalConfiguration.RevivalDialogDelay, AskRevivalType.BasicRevival); + return; + } + + session.PlayerEntity.UpdateRevival(actualTime + _revivalConfiguration.Act4SealRevivalDelay, RevivalType.DontPayRevival, ForcedType.Act4SealRevival); + } + + private async Task HandleReputation(IClientSession session, IPlayerEntity killer) + { + if (_act4Configuration.PvpFactionPoints) + { + await _asyncEventPipeline.ProcessEventAsync(new Act4FactionPointsIncreaseEvent(killer.Faction, _act4Configuration.FactionPointsPerPvpKill)); + } + + int killerReputDegree = (int)killer.GetReputationIcon(_reputationConfiguration, _rankingManager.TopReputation); + int victimReputDegree = (int)session.PlayerEntity.GetReputationIcon(_reputationConfiguration, _rankingManager.TopReputation); + int formulaResult = victimReputDegree * session.PlayerEntity.Level * 10 / killerReputDegree + killerReputDegree * 50 / 3; + int finalReputation = 9 < killerReputDegree - victimReputDegree ? Convert.ToInt32(formulaResult * 0.1) : formulaResult; + + if (session.PlayerEntity.IsGettingLosingReputation || killer.IsGettingLosingReputation) + { + finalReputation = (int)(finalReputation * 0.05); + } + + finalReputation = (int)(finalReputation * (1 + + killer.BCardComponent.GetAllBCardsInformation(BCardType.ReputHeroLevel, (byte)AdditionalTypes.ReputHeroLevel.ReputIncreased, killer.Level).firstData * 0.01)); + + if (killer.IsInGroup()) + { + foreach (IPlayerEntity member in killer.GetGroup().Members) + { + if (member == null) + { + continue; + } + + if (killer.MapInstance.Id != member.MapInstance?.Id) + { + continue; + } + + if (member.Id != killer.Id) + { + int reputationForMember = (int)(finalReputation * 0.1); + + if (finalReputation <= 0) + { + continue; + } + + await member.Session.EmitEventAsync(new GenerateReputationEvent + { + Amount = reputationForMember, + SendMessage = true + }); + continue; + } + + if (finalReputation <= 0) + { + continue; + } + + await member.Session.EmitEventAsync(new GenerateReputationEvent + { + Amount = finalReputation, + SendMessage = true + }); + } + } + else + { + await killer.Session.EmitEventAsync(new GenerateReputationEvent + { + Amount = finalReputation, + SendMessage = true + }); + } + + int toRemove = killerReputDegree * killer.Level * 10 / victimReputDegree + victimReputDegree * 50 / 35; + + if (session.PlayerEntity.IsGettingLosingReputation || killer.IsGettingLosingReputation) + { + toRemove = (int)(toRemove * 0.05); + } + + int decrease = session.PlayerEntity.BCardComponent + .GetAllBCardsInformation(BCardType.ChangingPlace, (byte)AdditionalTypes.ChangingPlace.DecreaseReputationLostAfterDeath, session.PlayerEntity.Level).firstData; + toRemove = (int)(toRemove * (1 - decrease * 0.01)); + + if (toRemove <= 0) + { + return; + } + + await session.EmitEventAsync(new GenerateReputationEvent + { + Amount = -toRemove, + SendMessage = true + }); + } + + private async Task Penalty(RevivalStartProcedureEvent e) + { + PlayerRevivalPenalization playerRevivalPenalization = _revivalConfiguration.PlayerRevivalPenalization; + if (e.Sender.PlayerEntity.Level <= playerRevivalPenalization.MaxLevelWithoutRevivalPenalization) + { + return; + } + + int amount = e.Sender.PlayerEntity.Level < playerRevivalPenalization.MaxLevelWithDignityPenalizationIncrement + ? e.Sender.PlayerEntity.Level * playerRevivalPenalization.DignityPenalizationIncrementMultiplier + : playerRevivalPenalization.MaxLevelWithDignityPenalizationIncrement * playerRevivalPenalization.DignityPenalizationIncrementMultiplier; + + await e.Sender.PlayerEntity.RemoveDignity(amount, _minMaxConfiguration, _languageService, _reputationConfiguration, _rankingManager.TopReputation); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Act4/Event/RevivalStartProcedureEventDungeonHandler.cs b/srcs/_plugins/Plugin.Act4/Event/RevivalStartProcedureEventDungeonHandler.cs new file mode 100644 index 0000000..64537f0 --- /dev/null +++ b/srcs/_plugins/Plugin.Act4/Event/RevivalStartProcedureEventDungeonHandler.cs @@ -0,0 +1,35 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Act4.Configuration; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Revival; + +namespace Plugin.Act4.Event; + +public class RevivalStartProcedureEventDungeonHandler : IAsyncEventProcessor +{ + private readonly Act4DungeonsConfiguration _act4DungeonsConfiguration; + private readonly PlayerRevivalConfiguration _revivalConfiguration; + + public RevivalStartProcedureEventDungeonHandler(Act4DungeonsConfiguration act4DungeonsConfiguration, GameRevivalConfiguration gameRevivalConfiguration) + { + _act4DungeonsConfiguration = act4DungeonsConfiguration; + _revivalConfiguration = gameRevivalConfiguration.PlayerRevivalConfiguration; + } + + public async Task HandleAsync(RevivalStartProcedureEvent e, CancellationToken cancellation) + { + if (e.Sender.PlayerEntity.IsAlive() || e.Sender.CurrentMapInstance.MapInstanceType != MapInstanceType.Act4Dungeon) + { + return; + } + + DateTime currentTime = DateTime.UtcNow; + e.Sender.PlayerEntity.UpdateRevival(currentTime + _act4DungeonsConfiguration.DungeonDeathRevivalDelay, RevivalType.DontPayRevival, ForcedType.Reconnect); + e.Sender.PlayerEntity.UpdateAskRevival(currentTime + _revivalConfiguration.RevivalDialogDelay, AskRevivalType.DungeonRevival); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Act4/Extension/Act4DungeonExtension.cs b/srcs/_plugins/Plugin.Act4/Extension/Act4DungeonExtension.cs new file mode 100644 index 0000000..6747059 --- /dev/null +++ b/srcs/_plugins/Plugin.Act4/Extension/Act4DungeonExtension.cs @@ -0,0 +1,12 @@ +using WingsEmu.Game.Act4.Entities; +using WingsEmu.Game.Networking; + +namespace Plugin.Act4.Extension; + +public static class Act4DungeonExtension +{ + public static string HatusHeadStatePacket(short whichHeadsBitFlag, HatusHeads heads) => + $"bc 1 {(byte)heads.HeadsState} {whichHeadsBitFlag} {heads.DragonHeads.BluePositionX} {heads.DragonHeads.RedPositionX} {heads.DragonHeads.GreenPositionX}"; + + public static int GetDungeonReputationRequirement(this IClientSession session, int multiplier) => session.PlayerEntity.Level * multiplier; +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Act4/Extension/ScriptExtension.cs b/srcs/_plugins/Plugin.Act4/Extension/ScriptExtension.cs new file mode 100644 index 0000000..095dfc6 --- /dev/null +++ b/srcs/_plugins/Plugin.Act4/Extension/ScriptExtension.cs @@ -0,0 +1,9 @@ +using WingsAPI.Scripting.Enum.Dungeon; +using WingsEmu.Game.Act4; + +namespace Plugin.Act4.Extension; + +public static class ScriptExtensions +{ + public static SDungeonType ToSDungeonType(this DungeonType raidType) => (SDungeonType)(byte)raidType; +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Act4/Plugin.Act4.csproj b/srcs/_plugins/Plugin.Act4/Plugin.Act4.csproj new file mode 100644 index 0000000..f8831c6 --- /dev/null +++ b/srcs/_plugins/Plugin.Act4/Plugin.Act4.csproj @@ -0,0 +1,24 @@ + + + + net5.0 + latest + + + + + + + + + + + + + + + + + + + diff --git a/srcs/_plugins/Plugin.Act4/RecurrentJob/Act4DungeonSystem.cs b/srcs/_plugins/Plugin.Act4/RecurrentJob/Act4DungeonSystem.cs new file mode 100644 index 0000000..bcd1a9c --- /dev/null +++ b/srcs/_plugins/Plugin.Act4/RecurrentJob/Act4DungeonSystem.cs @@ -0,0 +1,530 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using PhoenixLib.Events; +using PhoenixLib.Logging; +using Plugin.Act4.Const; +using Plugin.Act4.Extension; +using WingsEmu.Game.Act4; +using WingsEmu.Game.Act4.Entities; +using WingsEmu.Game.Act4.Event; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Entities.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Extensions.Mates; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Maps.Event; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Mates.Events; +using WingsEmu.Game.Monster.Event; + +namespace Plugin.Act4.RecurrentJob; + +public class Act4DungeonSystem : BackgroundService +{ + private static readonly TimeSpan Interval = TimeSpan.FromSeconds(1); + private readonly IAct4DungeonManager _act4DungeonManager; + private readonly IAsyncEventPipeline _asyncEventPipeline; + private readonly IBuffFactory _buffFactory; + private readonly IDungeonManager _dungeonManager; + private readonly GameRevivalConfiguration _gameRevivalConfiguration; + + public Act4DungeonSystem(IAct4DungeonManager act4dungeonManager, IAsyncEventPipeline asyncEventPipeline, + IDungeonManager dungeonManager, GameRevivalConfiguration gameRevivalConfiguration, IBuffFactory buffFactory) + { + _act4DungeonManager = act4dungeonManager; + _asyncEventPipeline = asyncEventPipeline; + _dungeonManager = dungeonManager; + _gameRevivalConfiguration = gameRevivalConfiguration; + _buffFactory = buffFactory; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + Log.Info("[ACT4_DUNGEON_SYSTEM] Started!"); + while (!stoppingToken.IsCancellationRequested) + { + try + { + await Process(stoppingToken); + } + catch (Exception e) + { + Log.Error("[ACT4_DUNGEON_SYSTEM]", e); + } + + await Task.Delay(Interval, stoppingToken); + } + } + + private async Task Process(CancellationToken stoppingToken) + { + if (!_act4DungeonManager.DungeonsActive) + { + return; + } + + DateTime currentTime = DateTime.UtcNow; + + foreach (DungeonInstance dungeon in _act4DungeonManager.Dungeons.ToArray()) + { + try + { + await ProcessAct4DungeonInstanceAfterSlowMo(dungeon, currentTime); + await ProcessAct4DungeonInstanceCleanUp(dungeon, currentTime); + + foreach (DungeonSubInstance subInstance in dungeon.DungeonSubInstances.Values) + { + try + { + await ProcessAct4HatusInstance(subInstance, currentTime); + await ProcessAct4DungeonCalvinas(subInstance, currentTime, dungeon.DungeonType); + await ProcessAct4DungeonSubInstanceLoopWave(subInstance, currentTime, stoppingToken); + await ProcessAct4DungeonSubInstanceWave(subInstance, currentTime, stoppingToken); + await ProcessAct4DungeonSubInstancePortalGeneration(dungeon, subInstance, currentTime, stoppingToken); + } + catch (Exception e) + { + Log.Error($"[DUNGEON_SUBINSTANCE_PROCESS] familyId: {dungeon.FamilyId}", e); + } + } + } + catch (Exception e) + { + Log.Error($"[DUNGEON_PROCESS] familyId: {dungeon.FamilyId}", e); + } + } + + if (_act4DungeonManager.DungeonEnd < currentTime) + { + await _asyncEventPipeline.ProcessEventAsync(new Act4DungeonSystemStopEvent(), stoppingToken); + } + } + + private async Task ProcessAct4DungeonCalvinas(DungeonSubInstance dungeon, DateTime currentTime, DungeonType dungeonType) + { + if (dungeonType != DungeonType.Calvinas) + { + return; + } + + CalvinasState calvinasState = _dungeonManager.GetCalvinasDragons(dungeon.MapInstance.Id); + if (calvinasState == null) + { + return; + } + + if (calvinasState.CastTime > currentTime) + { + return; + } + + _dungeonManager.RemoveCalvinasDragons(dungeon.MapInstance.Id); + + if (calvinasState.CalvinasDragonsList == null) + { + return; + } + + IEnumerable entities = dungeon.MapInstance.GetNonMonsterBattleEntities(); + + IMonsterEntity boss = dungeon.Bosses.FirstOrDefault(); + foreach (IBattleEntity entity in entities) + { + // checks if the player/mate is inside the (dragon) rectangle + foreach (CalvinasDragon dragon in calvinasState.CalvinasDragonsList) + { + bool isInX = false; + bool isInY = false; + if (dragon.Axis == CoordType.X) + { + isInY = entity.IsInLineY(dragon.At, dragon.Size); + + if (dragon.Start < dragon.End) + { + if (dragon.Start <= entity.PositionX && entity.PositionX <= dragon.End) + { + isInX = true; + } + } + else + { + if (dragon.End <= entity.PositionX && entity.PositionX <= dragon.Start) + { + isInX = true; + } + } + } + else + { + isInX = entity.IsInLineX(dragon.At, dragon.Size); + + if (dragon.Start < dragon.End) + { + if (dragon.Start <= entity.PositionY && entity.PositionY <= dragon.End) + { + isInY = true; + } + } + else + { + if (dragon.End <= entity.PositionY && entity.PositionY <= dragon.Start) + { + isInY = true; + } + } + } + + if (!isInX || !isInY) + { + continue; + } + + if (!entity.IsAlive()) + { + continue; + } + + if (await boss.ShouldSaveDefender(entity, entity.MaxHp, _gameRevivalConfiguration, _buffFactory)) + { + continue; + } + + entity.Hp = 0; + await entity.EmitEventAsync(new GenerateEntityDeathEvent + { + Entity = entity + }); + boss.BroadcastCleanSuPacket(entity, entity.MaxHp); + break; + } + } + } + + private async Task ProcessAct4HatusInstance(DungeonSubInstance dungeon, DateTime currentTime) + { + try + { + if (dungeon?.HatusHeads?.DragonHeads == null) + { + return; + } + + if (dungeon.Bosses.Count == 0) + { + return; + } + + HatusState hatusState = _dungeonManager.GetHatusState(dungeon.MapInstance.Id); + if (hatusState == null) + { + return; + } + + HatusHeads heads = dungeon.HatusHeads; + + if (heads.HeadsState == HatusDragonHeadState.HIDE_HEAD) + { + return; + } + + short headsChange = 0; + + HatusDragonHeadState toChange = heads.HeadsState; + short[] xToHit = new short[3]; + + switch (heads.HeadsState) + { + case HatusDragonHeadState.SHOW: + case HatusDragonHeadState.IDLE: + + if (hatusState.BlueAttack) + { + headsChange += 1; + heads.DragonHeads.BluePositionX = hatusState.BlueX; + heads.DragonHeads.BlueIsActive = true; + toChange = HatusDragonHeadState.ATTACK_CAST; + } + + if (hatusState.RedAttack) + { + headsChange += 2; + heads.DragonHeads.RedPositionX = hatusState.RedX; + heads.DragonHeads.RedIsActive = true; + toChange = HatusDragonHeadState.ATTACK_CAST; + } + + if (hatusState.GreenAttack) + { + headsChange += 4; + heads.DragonHeads.GreenPositionX = hatusState.GreenX; + heads.DragonHeads.GreenIsActive = true; + toChange = HatusDragonHeadState.ATTACK_CAST; + } + + heads.CastTime = currentTime + hatusState.CastTime; + + break; + case HatusDragonHeadState.ATTACK_CAST: + + if (heads.CastTime < currentTime) + { + toChange = HatusDragonHeadState.ATTACK_USE; + + if (heads.DragonHeads.BlueIsActive) + { + headsChange += 1; + } + + if (heads.DragonHeads.RedIsActive) + { + headsChange += 2; + } + + if (heads.DragonHeads.GreenIsActive) + { + headsChange += 4; + } + + heads.CastTime = currentTime + hatusState.CastTime; + } + + break; + case HatusDragonHeadState.ATTACK_USE: + + if (heads.CastTime > currentTime) + { + break; + } + + if (heads.DragonHeads.BlueIsActive) + { + headsChange += 1; + heads.DragonHeads.BlueIsActive = false; + xToHit[0] = heads.DragonHeads.BluePositionX; + } + + if (heads.DragonHeads.RedIsActive) + { + headsChange += 2; + heads.DragonHeads.RedIsActive = false; + xToHit[1] = heads.DragonHeads.RedPositionX; + } + + if (heads.DragonHeads.GreenIsActive) + { + headsChange += 4; + heads.DragonHeads.GreenIsActive = false; + xToHit[2] = heads.DragonHeads.GreenPositionX; + } + + break; + } + + if (headsChange == 0) + { + if (heads.CastTime > currentTime) + { + return; + } + + _dungeonManager.RemoveHatusState(dungeon.MapInstance.Id); + heads.HeadsState = HatusDragonHeadState.IDLE; + return; + } + + dungeon.MapInstance.Broadcast(Act4DungeonExtension.HatusHeadStatePacket(headsChange, heads)); + heads.HeadsState = toChange; + + if (heads.HeadsState == HatusDragonHeadState.ATTACK_USE) + { + if (heads.CastTime > currentTime) + { + return; + } + + List entitesToAttack = new(); + IMonsterEntity boss = dungeon.Bosses.FirstOrDefault(); + + foreach (short x in xToHit) + { + entitesToAttack.AddRange(dungeon.MapInstance.GetNonMonsterBattleEntities().Where(e => e.IsInLineX(x, (short)heads.DragonAttackWidth))); + } + + foreach (IBattleEntity entity in entitesToAttack) + { + if (!entity.IsAlive()) + { + continue; + } + + int damage = (int)(entity.MaxHp * hatusState.DealtDamage); + if (await boss.ShouldSaveDefender(entity, damage, _gameRevivalConfiguration, _buffFactory)) + { + continue; + } + + if (entity.Hp - damage <= 0) + { + entity.Hp = 0; + await entity.EmitEventAsync(new GenerateEntityDeathEvent + { + Entity = entity + }); + + boss.BroadcastCleanSuPacket(entity, damage); + continue; + } + + entity.Hp -= damage; + + switch (entity) + { + case IPlayerEntity character: + character.LastDefence = DateTime.UtcNow; + character.Session.RefreshStat(); + + if (character.IsSitting) + { + await character.Session.RestAsync(force: true); + } + + break; + case IMateEntity mate: + mate.LastDefence = DateTime.UtcNow; + mate.Owner.Session.SendMateLife(mate); + + if (mate.IsSitting) + { + await mate.Owner.Session.EmitEventAsync(new MateRestEvent + { + MateEntity = mate, + Force = true + }); + } + + break; + } + + boss.BroadcastCleanSuPacket(entity, damage); + } + + heads.HeadsState = HatusDragonHeadState.IDLE; + _dungeonManager.RemoveHatusState(dungeon.MapInstance.Id); + } + } + catch (Exception e) + { + Log.Error("[PROCESS_HATUS]", e); + } + } + + private async Task ProcessAct4DungeonSubInstanceLoopWave(DungeonSubInstance dungeonSubInstance, DateTime currentTime, CancellationToken stoppingToken) + { + try + { + if (dungeonSubInstance.LastDungeonWaveLoop == null || dungeonSubInstance.LoopWaves.Count < 1) + { + return; + } + + foreach (DungeonLoopWave wave in dungeonSubInstance.LoopWaves) + { + if (wave.FirstSpawnWave > currentTime) + { + continue; + } + + if (wave.LastMonsterSpawn > currentTime) + { + continue; + } + + wave.LastMonsterSpawn = currentTime + wave.TickDelay; + short? scaledWithPlayersAmount = wave.IsScaledWithPlayerAmount ? (short?)dungeonSubInstance.MapInstance.Sessions.Count : null; + await _asyncEventPipeline.ProcessEventAsync(new MonsterSummonEvent(dungeonSubInstance.MapInstance, wave.Monsters, scaledWithPlayerAmount: scaledWithPlayersAmount), stoppingToken); + } + } + catch (Exception e) + { + Log.Error("[PROCESS_DUNGEON_LOOP]", e); + } + } + + private async Task ProcessAct4DungeonSubInstanceWave(DungeonSubInstance dungeonSubInstance, DateTime currentTime, CancellationToken stoppingToken) + { + try + { + DungeonLinearWave wave = dungeonSubInstance.LinearWaves.FirstOrDefault(); + if (wave == null || dungeonSubInstance.LastDungeonWaveLinear == null || currentTime < dungeonSubInstance.LastDungeonWaveLinear + wave.Delay) + { + return; + } + + dungeonSubInstance.LinearWaves.Remove(wave); + await _asyncEventPipeline.ProcessEventAsync(new MonsterSummonEvent(dungeonSubInstance.MapInstance, wave.Monsters), stoppingToken); + } + catch (Exception e) + { + Log.Error("[PROCESS_DUNGEON_WAVE]", e); + } + } + + private async Task ProcessAct4DungeonSubInstancePortalGeneration(DungeonInstance dungeonInstance, DungeonSubInstance dungeonSubInstance, DateTime currentTime, CancellationToken stoppingToken) + { + PortalGenerator portalGenerator = dungeonSubInstance.PortalGenerators.FirstOrDefault(); + if (portalGenerator == null || dungeonSubInstance.LastPortalGeneration == null || currentTime < _act4DungeonManager.DungeonStart + portalGenerator.Delay) + { + return; + } + + dungeonSubInstance.PortalGenerators.Remove(portalGenerator); + + await _asyncEventPipeline.ProcessEventAsync(new SpawnPortalEvent(dungeonSubInstance.MapInstance, portalGenerator.Portal), stoppingToken); + //quick win + await _asyncEventPipeline.ProcessEventAsync(new Act4DungeonBroadcastPacketEvent + { + DungeonInstance = dungeonInstance + }, stoppingToken); + await _asyncEventPipeline.ProcessEventAsync(new Act4DungeonBroadcastBossOpenEvent + { + DungeonInstance = dungeonInstance + }, stoppingToken); + } + + private static async Task ProcessAct4DungeonInstanceAfterSlowMo(DungeonInstance dungeonInstance, DateTime currentTime) + { + if (dungeonInstance.FinishSlowMoDate == null || currentTime < dungeonInstance.FinishSlowMoDate) + { + return; + } + + dungeonInstance.FinishSlowMoDate = DateTime.MaxValue; + foreach (DungeonSubInstance subInstance in dungeonInstance.DungeonSubInstances.Values) + { + await subInstance.TriggerEvents(DungeonConstEventKeys.RaidSubInstanceAfterSlowMo); + } + } + + private static async Task ProcessAct4DungeonInstanceCleanUp(DungeonInstance dungeonInstance, DateTime currentTime) + { + if (dungeonInstance.CleanUpBossMapDate == null || currentTime < dungeonInstance.CleanUpBossMapDate) + { + return; + } + + dungeonInstance.CleanUpBossMapDate = null; + foreach (DungeonSubInstance subInstance in dungeonInstance.DungeonSubInstances.Values) + { + await subInstance.TriggerEvents(DungeonConstEventKeys.RaidSubInstanceCleanUp); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Act4/RecurrentJob/Act4System.cs b/srcs/_plugins/Plugin.Act4/RecurrentJob/Act4System.cs new file mode 100644 index 0000000..13a7a4c --- /dev/null +++ b/srcs/_plugins/Plugin.Act4/RecurrentJob/Act4System.cs @@ -0,0 +1,66 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using PhoenixLib.Events; +using PhoenixLib.Logging; +using WingsEmu.Game.Act4.Event; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Managers; +using WingsEmu.Packets.Enums; + +namespace Plugin.Act4.RecurrentJob; + +public class Act4System : BackgroundService +{ + private static readonly TimeSpan Interval = TimeSpan.FromMinutes(1); + + private readonly IAct4Manager _act4Manager; + private readonly IAsyncEventPipeline _asyncEventPipeline; + private readonly ISessionManager _sessionManager; + + public Act4System(IAct4Manager act4Manager, ISessionManager sessionManager, IAsyncEventPipeline asyncEventPipeline) + { + _act4Manager = act4Manager; + _sessionManager = sessionManager; + _asyncEventPipeline = asyncEventPipeline; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + Log.Info("[ACT4_SYSTEM] Started!"); + while (!stoppingToken.IsCancellationRequested) + { + await ProcessAct4(stoppingToken); + await ProcessAct4Mukraju(stoppingToken); + await _asyncEventPipeline.ProcessEventAsync(new Act4SystemFcBroadcastEvent(), stoppingToken); + await Task.Delay(Interval, stoppingToken); + } + } + + private async Task ProcessAct4(CancellationToken stoppingToken) + { + if (!_act4Manager.FactionPointsLocked) + { + await _asyncEventPipeline.ProcessEventAsync(new Act4FactionPointsGenerationEvent(), stoppingToken); + } + } + + private async Task ProcessAct4Mukraju(CancellationToken stoppingToken) + { + FactionType faction = _act4Manager.GetTriumphantFaction(); + if (faction == FactionType.Neutral) + { + (DateTime deleteTime, IMonsterEntity mukraju, FactionType _) = _act4Manager.GetMukraju(); + if (DateTime.UtcNow < deleteTime || mukraju == null) + { + return; + } + + await _asyncEventPipeline.ProcessEventAsync(new Act4MukrajuDespawnEvent(), stoppingToken); + return; + } + + await _asyncEventPipeline.ProcessEventAsync(new Act4MukrajuSpawnEvent(faction), stoppingToken); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Act4/Scripting/Converter/SAct4DungeonRewardEventConverter.cs b/srcs/_plugins/Plugin.Act4/Scripting/Converter/SAct4DungeonRewardEventConverter.cs new file mode 100644 index 0000000..24f5f4d --- /dev/null +++ b/srcs/_plugins/Plugin.Act4/Scripting/Converter/SAct4DungeonRewardEventConverter.cs @@ -0,0 +1,20 @@ +using PhoenixLib.Events; +using WingsAPI.Scripting.Converter; +using WingsAPI.Scripting.Event.Dungeon; +using WingsEmu.Game.Act4; +using WingsEmu.Game.Act4.Event; + +namespace Plugin.Act4.Scripting.Converter; + +public class SAct4DungeonRewardEventConverter : ScriptedEventConverter +{ + private readonly DungeonInstanceWrapper _dungeonInstanceWrapper; + + public SAct4DungeonRewardEventConverter(DungeonInstanceWrapper dungeonInstanceWrapper) => _dungeonInstanceWrapper = dungeonInstanceWrapper; + + protected override IAsyncEvent Convert(SAct4DungeonRewardEvent e) => + new Act4DungeonRewardEvent + { + DungeonInstanceWrapper = _dungeonInstanceWrapper + }; +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Act4/Scripting/Validator/SDungeonValidator.cs b/srcs/_plugins/Plugin.Act4/Scripting/Validator/SDungeonValidator.cs new file mode 100644 index 0000000..935dab2 --- /dev/null +++ b/srcs/_plugins/Plugin.Act4/Scripting/Validator/SDungeonValidator.cs @@ -0,0 +1,19 @@ +using FluentValidation; +using WingsAPI.Scripting.Object.Dungeon; +using WingsAPI.Scripting.Validator.Common; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Maps; + +namespace Plugin.Act4.Scripting.Validator; + +public class SDungeonValidator : AbstractValidator +{ + public SDungeonValidator(IMapManager mapManager, INpcMonsterManager npcManager, IItemsManager itemsManager) + { + RuleFor(x => x.Maps).NotEmpty(); + RuleForEach(x => x.Maps).SetValidator(new SMapValidator(mapManager, npcManager, itemsManager)); + + RuleFor(x => x.Spawn).SetValidator(new SLocationValidator()); + RuleFor(x => x.DungeonType).IsInEnum(); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.CoreImpl/Configs/HardcodedDialogsByNpcVnum.cs b/srcs/_plugins/Plugin.CoreImpl/Configs/HardcodedDialogsByNpcVnum.cs new file mode 100644 index 0000000..f5e3455 --- /dev/null +++ b/srcs/_plugins/Plugin.CoreImpl/Configs/HardcodedDialogsByNpcVnum.cs @@ -0,0 +1,8 @@ +namespace Plugin.CoreImpl.Configs +{ + public class HardcodedDialogsByNpcVnum + { + public short NpcVnum { get; init; } + public short DialogId { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.CoreImpl/Configs/HardcodedDialogsByNpcVnumFileConfig.cs b/srcs/_plugins/Plugin.CoreImpl/Configs/HardcodedDialogsByNpcVnumFileConfig.cs new file mode 100644 index 0000000..57e42bc --- /dev/null +++ b/srcs/_plugins/Plugin.CoreImpl/Configs/HardcodedDialogsByNpcVnumFileConfig.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; + +namespace Plugin.CoreImpl.Configs +{ + public class HardcodedDialogsByNpcVnumFileConfig : List + { + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.CoreImpl/CoreImplDependencyPlugin.cs b/srcs/_plugins/Plugin.CoreImpl/CoreImplDependencyPlugin.cs new file mode 100644 index 0000000..79f4ef3 --- /dev/null +++ b/srcs/_plugins/Plugin.CoreImpl/CoreImplDependencyPlugin.cs @@ -0,0 +1,50 @@ +using Microsoft.Extensions.DependencyInjection; +using PhoenixLib.Configuration; +using Plugin.CoreImpl.Configs; +using Plugin.CoreImpl.Entities; +using Plugin.CoreImpl.Maps; +using Plugin.CoreImpl.Skills; +using WingsAPI.Plugins; +using WingsEmu.Game._enum; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Portals; +using WingsEmu.Game.Skills; + +namespace Plugin.CoreImpl +{ + public class CoreImplDependencyPlugin : IGameServerPlugin + { + public string Name => nameof(CoreImplDependencyPlugin); + + + public void AddDependencies(IServiceCollection services, GameServerLoader gameServer) + { + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddTransient(); + services.AddSingleton(); + services.AddSingleton(); + + services.AddFileConfiguration(new HardcodedDialogsByNpcVnumFileConfig + { + new() { NpcVnum = (int)MonsterVnum.BIG_MINILAND_SIGN, DialogId = (int)DialogVnums.MINILAND_SIGN }, + new() { NpcVnum = (int)MonsterVnum.SMALL_MINILAND_SIGN, DialogId = (int)DialogVnums.MINILAND_SIGN }, + new() { NpcVnum = (int)MonsterVnum.HALLOWEEN_MINILAND_SIGN, DialogId = (int)DialogVnums.MINILAND_SIGN }, + new() { NpcVnum = (int)MonsterVnum.EASTER_MINILAND_SIGN, DialogId = (int)DialogVnums.MINILAND_SIGN }, + new() { NpcVnum = (int)MonsterVnum.PIRATE_MINILAND_SIGN, DialogId = (int)DialogVnums.MINILAND_SIGN }, + new() { NpcVnum = (int)MonsterVnum.CHRISTMAS_MINILAND_SIGN, DialogId = (int)DialogVnums.MINILAND_SIGN }, + new() { NpcVnum = (int)MonsterVnum.BIG_FLAG, DialogId = (int)DialogVnums.NPC_REQ }, + new() { NpcVnum = (int)MonsterVnum.MEDIUM_FLAG, DialogId = (int)DialogVnums.NPC_REQ }, + new() { NpcVnum = (int)MonsterVnum.SMALL_FLAG, DialogId = (int)DialogVnums.NPC_REQ }, + + new() { NpcVnum = (int)MonsterVnum.SMALL_CAMPFIRE, DialogId = (int)DialogVnums.SMALL_CAMPFIRE }, + + new() { NpcVnum = (int)MonsterVnum.ICE_MACHINE, DialogId = (int)DialogVnums.ICE_MACHINE }, + + new() { NpcVnum = (int)MonsterVnum.GIANT_CAMPFIRE, DialogId = (int)DialogVnums.BIG_CAMPFIRE } + }); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.CoreImpl/Entities/MonsterEntity.cs b/srcs/_plugins/Plugin.CoreImpl/Entities/MonsterEntity.cs new file mode 100644 index 0000000..1862baa --- /dev/null +++ b/srcs/_plugins/Plugin.CoreImpl/Entities/MonsterEntity.cs @@ -0,0 +1,652 @@ +// WingsEmu +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Data.Drops; +using WingsEmu.Core.Generics; +using WingsEmu.DTOs.BCards; +using WingsEmu.DTOs.Skills; +using WingsEmu.Game; +using WingsEmu.Game._enum; +using WingsEmu.Game.Algorithm; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Battle.Managers; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Monster; +using WingsEmu.Game.Raids; +using WingsEmu.Game.Skills; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Battle; + +namespace Plugin.CoreImpl.Entities +{ + public class MonsterEntity : IMonsterEntity + { + private readonly IAsyncEventPipeline _asyncEventPipeline; + private readonly IBattleEntityAlgorithmService _battleEntityAlgorithmService; + private readonly ICastingComponent _castingComponent; + private readonly IEndBuffDamageComponent _endBuffDamageComponent; + private readonly IEventTriggerContainer _eventContainer; + + private double _enhancedHp = 1; + + public MonsterEntity(int id, IEventTriggerContainer eventContainer, IBCardComponent bCardComponent, IAsyncEventPipeline asyncEventPipeline, IMonsterData monsterData, IMapInstance mapInstance, + MonsterEntityBuilder builder, IReadOnlyList npcMonsterSkills, IBattleEntityAlgorithmService battleEntityAlgorithmService) + { + UniqueId = builder?.GeneratedGuid ?? Guid.NewGuid(); + BCardComponent = bCardComponent; + _castingComponent = new CastingComponent(); + Damagers = new ThreadSafeHashSet(); + _endBuffDamageComponent = new EndBuffDamageComponent(); + ChargeComponent = new ChargeComponent(); + _eventContainer = eventContainer; + BuffComponent = new BuffComponent(); + _asyncEventPipeline = asyncEventPipeline; + _battleEntityAlgorithmService = battleEntityAlgorithmService; + + Id = id; + AmountRequired = monsterData.AmountRequired; + ArmorLevel = monsterData.ArmorLevel; + AttackType = monsterData.AttackType; + AttackUpgrade = monsterData.AttackUpgrade; + BasicCastTime = monsterData.BasicCastTime; + BasicCooldown = monsterData.BasicCooldown; + BasicRange = monsterData.BasicRange; + BCards = monsterData.BCards; + AttackEffect = monsterData.AttackEffect; + BasicHitChance = monsterData.BasicHitChance; + CanBeCollected = monsterData.CanBeCollected; + CanBeDebuffed = monsterData.CanBeDebuffed; + CanBeCaught = monsterData.CanBeCaught; + CanBePushed = monsterData.CanBePushed; + CanRegenMp = monsterData.CanRegenMp; + CellSize = monsterData.CellSize; + CleanDamageMin = monsterData.CleanDamageMin; + CleanDamageMax = monsterData.CleanDamageMax; + CleanHitRate = monsterData.CleanHitRate; + CleanMeleeDefence = monsterData.CleanMeleeDefence; + CleanRangeDefence = monsterData.CleanRangeDefence; + CleanMagicDefence = monsterData.CleanMagicDefence; + CleanDodge = monsterData.CleanDodge; + CleanHp = monsterData.CleanHp; + CleanMp = monsterData.CleanMp; + BaseCloseDefence = monsterData.BaseCloseDefence; + BaseConcentrate = monsterData.BaseConcentrate; + BaseCriticalChance = monsterData.BaseCriticalChance; + BaseCriticalRate = monsterData.BaseCriticalRate; + DamagedOnlyLastJajamaruSkill = monsterData.DamagedOnlyLastJajamaruSkill; + BaseDamageMinimum = monsterData.BaseDamageMaximum; + BaseDamageMaximum = monsterData.BaseDamageMinimum; + BaseDarkResistance = monsterData.BaseDarkResistance; + DeathEffect = monsterData.DeathEffect; + BaseMaxHp = monsterData.BaseMaxHp; + BaseMaxMp = monsterData.BaseMaxMp; + DefenceDodge = monsterData.DefenceDodge; + DefenceUpgrade = monsterData.DefenceUpgrade; + Drops = monsterData.Drops; + DisappearAfterHitting = monsterData.DisappearAfterHitting; + DisappearAfterSeconds = monsterData.DisappearAfterSeconds; + DisappearAfterSecondsMana = monsterData.DisappearAfterSecondsMana; + DistanceDefence = monsterData.DistanceDefence; + DistanceDefenceDodge = monsterData.DistanceDefenceDodge; + BaseElement = monsterData.BaseElement; + BaseElementRate = monsterData.BaseElementRate; + BaseFireResistance = monsterData.BaseFireResistance; + GiveDamagePercentage = monsterData.GiveDamagePercentage; + GroupAttack = monsterData.GroupAttack; + HasMode = monsterData.HasMode; + RawHostility = monsterData.RawHostility; + IconId = monsterData.IconId; + IsPercent = monsterData.IsPercent; + JobXp = monsterData.JobXp; + BaseLevel = monsterData.BaseLevel; + BaseLightResistance = monsterData.BaseLightResistance; + MagicDefence = monsterData.MagicDefence; + MagicMpFactor = monsterData.MagicMpFactor; + MeleeHpFactor = monsterData.MeleeHpFactor; + MinimumAttackRange = monsterData.MinimumAttackRange; + MonsterVNum = monsterData.MonsterVNum; + Name = monsterData.Name; + NoticeRange = monsterData.NoticeRange; + OnDefenseOnlyOnce = monsterData.OnDefenseOnlyOnce; + PermanentEffect = monsterData.PermanentEffect; + MonsterRaceType = monsterData.MonsterRaceType; + MonsterRaceSubType = monsterData.MonsterRaceSubType; + RangeDodgeFactor = monsterData.RangeDodgeFactor; + BaseRespawnTime = monsterData.BaseRespawnTime; + SpawnMobOrColor = monsterData.SpawnMobOrColor; + BaseSpeed = monsterData.BaseSpeed; + SpriteSize = monsterData.SpriteSize; + TakeDamages = monsterData.TakeDamages; + VNumRequired = monsterData.VNumRequired; + BaseWaterResistance = monsterData.BaseWaterResistance; + WeaponLevel = monsterData.WeaponLevel; + WinfoValue = monsterData.WinfoValue; + Xp = monsterData.Xp; + MaxTries = monsterData.MaxTries; + CollectionCooldown = monsterData.CollectionCooldown; + CollectionDanceTime = monsterData.CollectionDanceTime; + TeleportRemoveFromInventory = monsterData.TeleportRemoveFromInventory; + BasicDashSpeed = monsterData.BasicDashSpeed; + ModeIsHpTriggered = monsterData.ModeIsHpTriggered; + ModeLimiterType = monsterData.ModeLimiterType; + ModeRangeTreshold = monsterData.ModeRangeTreshold; + ModeCModeVnum = monsterData.ModeCModeVnum; + ModeHpTresholdOrItemVnum = monsterData.ModeHpTresholdOrItemVnum; + MidgardDamage = monsterData.MidgardDamage; + HasDash = monsterData.HasDash; + DropToInventory = monsterData.DropToInventory; + ModeBCards = monsterData.ModeBCards; + BaseXp = monsterData.BaseXp; + BaseJobXp = monsterData.BaseJobXp; + + LastEffect = Death = SpawnDate = DateTime.UtcNow; + LastSkill = DateTime.MinValue; + IsHostile = RawHostility != (int)HostilityType.NOT_HOSTILE; + + IsStillAlive = true; + Speed = this.GetSpeed(BaseSpeed); + Hp = BaseMaxHp; + Mp = BaseMaxMp; + Level = BaseLevel; + CanSeeInvisible = monsterData.CanSeeInvisible; + Faction = RawHostility switch + { + (int)HostilityType.ATTACK_ANGELS_ONLY => FactionType.Demon, + (int)HostilityType.ATTACK_DEVILS_ONLY => FactionType.Angel, + _ => monsterData.SuggestedFaction ?? FactionType.Neutral + }; + + if (builder != null) + { + PositionX = FirstX = builder.PositionX; + PositionY = FirstY = builder.PositionY; + Direction = builder.Direction; + CanWalk = builder.IsWalkingAround & monsterData.CanWalk; + BaseShouldRespawn = builder.IsRespawningOnDeath; + IsMateTrainer = builder.IsMateTrainer; + IsBonus = builder.IsBonus; + IsBoss = builder.IsBoss; + IsTarget = builder.IsTarget; + VesselMonster = builder.IsVesselMonster; + SummonerId = builder.SummonerId; + SummonerType = builder.SummonerType; + IsHostile = builder.IsHostile; + SummonType = builder.SummonType; + GoToBossPosition = builder.GoToBossPosition; + IsInstantBattle = builder.IsInstantBattle; + RaidDrop = builder.RaidDrop; + + if (builder.Level.HasValue) + { + Level = builder.Level.Value; + UpdateMonstersBaseStatistics(); + Hp = MaxHp; + Mp = MaxMp; + } + + if (builder.HpMultiplier.HasValue) + { + BaseMaxHp = (int)(BaseMaxHp * builder.HpMultiplier.Value); + Hp = BaseMaxHp; + } + + if (builder.MpMultiplier.HasValue) + { + BaseMaxMp = (int)(BaseMaxMp * builder.MpMultiplier.Value); + Mp = BaseMaxMp; + } + + Faction = builder.FactionType ?? (SuggestedFaction ?? FactionType.Neutral); + } + + Drops = monsterData.Drops; + MonsterSkills = npcMonsterSkills; + + NotBasicSkills = MonsterSkills.Where(x => !x.IsBasicAttack).ToArray(); + if (HasDash) + { + DashSkill = MonsterSkills.Count != 0 ? MonsterSkills[0] : null; + } + + SkillsWithoutDashSkill = MonsterSkills.Where(x => DashSkill != null && DashSkill.Skill.Id != x.Skill.Id).ToArray(); + + foreach (INpcMonsterSkill skill in MonsterSkills) + { + Skills.Add(skill); + + if (skill.IsBasicAttack) + { + ReplacedBasicSkill = skill; + } + } + + this.InitializeBCards(); + + BasicSkill = new SkillInfo + { + Vnum = default, + AttackType = AttackType, + Range = BasicRange, + CastTime = BasicCastTime, + Cooldown = BasicCooldown, + HitEffect = AttackEffect, + Element = BaseElement, + HitChance = builder?.SetHitChance ?? BasicHitChance, + HitAnimation = 11, + TargetType = TargetType.Target, + TargetAffectedEntities = TargetAffectedEntities.Enemies, + SkillType = SkillType.MonsterSkill + }; + + MapInstance = mapInstance; + } + + public bool IsEnhanced { get; set; } + + public DateTime LastEnhancedEffect { get; set; } + + public bool BaseShouldRespawn { get; } + public bool TeleportBackOnNoticeRangeExceed { get; } + + public void RefreshStats() + { + Speed = this.GetSpeed(BaseSpeed); + } + + public ConcurrentDictionary PlayersDamage { get; } = new(); + + public DateTime LastMpRegen { get; set; } + + public Position? GoToBossPosition { get; set; } + public bool IsInstantBattle { get; set; } + + public IEnumerable RaidDrop { get; } + public DateTime LastBonusEffectTime { get; set; } + public DateTime AttentionTime { get; set; } + public ConcurrentDictionary Waypoints { get; set; } + public DateTime LastWayPoint { get; set; } + public byte CurrentWayPoint { get; set; } + public byte ReturnTimeOut { get; set; } + public (VisualType, long) LastAttackedEntity { get; set; } + public bool IsRunningAway { get; set; } + public IReadOnlyList NotBasicSkills { get; } + public IReadOnlyList SkillsWithoutDashSkill { get; } + public INpcMonsterSkill ReplacedBasicSkill { get; } + public INpcMonsterSkill DashSkill { get; } + + public IBCardComponent BCardComponent { get; } + public IChargeComponent ChargeComponent { get; } + public ThreadSafeHashSet AggroedEntities { get; } = new(); + public IBuffComponent BuffComponent { get; } + public bool IsMateTrainer { get; } + + public int MaxHp + { + get => this.GetMaxHp((int)(BaseMaxHp * _enhancedHp)); + set => throw new NotImplementedException(); + } + + public int MaxMp + { + get => this.GetMaxMp(BaseMaxMp); + set => throw new NotImplementedException(); + } + + public int Hp { get; set; } + public int Mp { get; set; } + + public ThreadSafeHashSet Damagers { get; } + public ThreadSafeHashSet Targets { get; } = new(); + public ThreadSafeHashSet<(VisualType, long)> TargetsByVisualTypeAndId { get; } = new(); + public DateTime LastTargetsRefresh { get; set; } + + public DateTime Death { get; set; } + + public bool IsStillAlive { get; set; } + + public byte Level { get; set; } + + public byte Element + { + get => BaseElement; + set => throw new NotImplementedException(); + } + + public int ElementRate + { + get => BaseElementRate; + set => throw new NotImplementedException(); + } + + public int FireResistance + { + get => BaseFireResistance; + set => throw new NotImplementedException(); + } + + public int WaterResistance + { + get => BaseWaterResistance; + set => throw new NotImplementedException(); + } + + public int LightResistance + { + get => BaseLightResistance; + set => throw new NotImplementedException(); + } + + public int DarkResistance + { + get => BaseDarkResistance; + set => throw new NotImplementedException(); + } + + public int DamagesMinimum { get; set; } + public int DamagesMaximum { get; set; } + + public Position Position + { + get => new(PositionX, PositionY); + set + { + PositionX = value.X; + PositionY = value.Y; + } + } + + public short PositionX { get; private set; } + + public short PositionY { get; private set; } + + public FactionType Faction { get; } + + public byte Speed { get; set; } + + public byte Size { get; set; } = 10; + + public bool IsBonus { get; set; } + + public bool IsBoss { get; } + + public string Name { get; } + public byte NoticeRange { get; } + public bool OnDefenseOnlyOnce { get; } + public short PermanentEffect { get; } + + public SummonType? SummonType { get; } + public bool IsHostile { get; set; } + + public bool IsTarget { get; set; } + + public DateTime LastEffect { get; set; } + + public DateTime LastSkill { get; set; } + + public IMapInstance MapInstance { get; set; } + + public List Skills { get; set; } = new(); + + public IBattleEntity Target { get; set; } + public DateTime NextTick { get; set; } + public DateTime NextAttackReady { get; set; } + public bool ModeIsActive { get; set; } + public short Morph { get; set; } + public long ModeDeathsSinceRespawn { get; set; } + + public short FirstX { get; set; } + + public short FirstY { get; set; } + + public bool VesselMonster { get; set; } + + public short AmountRequired { get; } + public byte ArmorLevel { get; } + public AttackType AttackType { get; } + public byte AttackUpgrade { get; } + public byte BasicCastTime { get; } + public short BasicCooldown { get; } + public byte BasicRange { get; } + public short AttackEffect { get; } + public short BasicHitChance { get; } + public bool CanBeCollected { get; } + public bool CanBeDebuffed { get; } + public bool CanBeCaught { get; } + public bool CanBePushed { get; } + public bool CanRegenMp { get; } + public bool CanWalk { get; set; } + public int CellSize { get; } + public int CleanDamageMin { get; } + public int CleanDamageMax { get; } + public int CleanHitRate { get; } + public int CleanMeleeDefence { get; } + public int CleanRangeDefence { get; } + public int CleanMagicDefence { get; } + public int CleanDodge { get; } + public int CleanHp { get; } + public int CleanMp { get; } + public short BaseCloseDefence { get; private set; } + + public short BaseConcentrate { get; private set; } + + public short BaseCriticalChance { get; } + public short BaseCriticalRate { get; } + public bool DamagedOnlyLastJajamaruSkill { get; } + public int BaseDamageMaximum { get; private set; } + + public int BaseDamageMinimum { get; private set; } + + public short BaseDarkResistance { get; } + public short DeathEffect { get; } + public int BaseMaxHp { get; private set; } + + public int BaseMaxMp { get; private set; } + + public short DefenceDodge { get; private set; } + + public byte DefenceUpgrade { get; } + public bool DisappearAfterHitting { get; } + public bool DisappearAfterSeconds { get; } + public bool DisappearAfterSecondsMana { get; } + public short DistanceDefence { get; private set; } + + public short DistanceDefenceDodge { get; private set; } + + public byte BaseElement { get; } + public short BaseElementRate { get; } + public short BaseFireResistance { get; } + public FactionType? SuggestedFaction => Faction; + public int GiveDamagePercentage { get; } + public int GroupAttack { get; } + public bool HasMode { get; } + public int RawHostility { get; } + public int IconId { get; } + public bool IsPercent { get; } + public int JobXp { get; private set; } + + public byte BaseLevel { get; } + public short BaseLightResistance { get; } + public short MagicDefence { get; private set; } + + public short MagicMpFactor { get; } + public short MeleeHpFactor { get; } + public sbyte MinimumAttackRange { get; } + + public long? SummonerId { get; set; } + public VisualType? SummonerType { get; set; } + + public IBattleEntity Killer { get; set; } + + public Guid UniqueId { get; } + + public void GenerateDeath(IBattleEntity killer = null) + { + IsStillAlive = false; + Hp = 0; + Mp = 0; + Death = DateTime.UtcNow; + this.RemoveAllBuffsAsync(true); + Target = null; + } + + public VisualType Type => VisualType.Monster; + public int Id { get; set; } + public byte Direction { get; set; } = 2; + + + bool INpcMonsterEntity.ShouldRespawn => BaseShouldRespawn; + public bool ReturningToFirstPosition { get; set; } + public bool ShouldFindNewTarget { get; set; } + public bool FindNewPositionAroundTarget { get; set; } + public bool IsApproachingTarget { get; set; } + public bool OnFirstDamageReceive { get; set; } + + public DateTime SpawnDate { get; set; } + public DateTime LastSpecialHpDecrease { get; set; } + + public bool IsMoving => CanWalk; + + public int MonsterVNum { get; } + + public void AddEvent(string key, IAsyncEvent notification, bool removedOnTrigger = false) => _eventContainer.AddEvent(key, notification, removedOnTrigger); + + public async Task TriggerEvents(string key) => await _eventContainer.TriggerEvents(key); + + public async Task EmitEventAsync(T eventArgs) where T : IBattleEntityEvent + { + if (eventArgs.Entity != this) + { + throw new ArgumentException("An event should be emitted only from the event sender"); + } + + await _asyncEventPipeline.ProcessEventAsync(eventArgs); + } + + public void EmitEvent(T eventArgs) where T : IBattleEntityEvent + { + EmitEventAsync(eventArgs).ConfigureAwait(false).GetAwaiter().GetResult(); + } + + public MonsterRaceType MonsterRaceType { get; set; } + public byte MonsterRaceSubType { get; } + public short RangeDodgeFactor { get; } + public TimeSpan BaseRespawnTime { get; } + public int SpawnMobOrColor { get; } + public byte BaseSpeed { get; } + public int SpriteSize { get; } + public int TakeDamages { get; } + public short VNumRequired { get; } + public short BaseWaterResistance { get; } + public byte WeaponLevel { get; } + public byte WinfoValue { get; } + public int Xp { get; private set; } + + public byte MaxTries { get; } + public short CollectionCooldown { get; } + public byte CollectionDanceTime { get; } + public bool TeleportRemoveFromInventory { get; } + public short BasicDashSpeed { get; } + public bool ModeIsHpTriggered { get; } + public byte ModeLimiterType { get; } + public short ModeRangeTreshold { get; } + public short ModeCModeVnum { get; } + public short ModeHpTresholdOrItemVnum { get; } + public short MidgardDamage { get; } + public bool HasDash { get; } + public bool DropToInventory { get; } + public int BaseXp { get; } + public int BaseJobXp { get; } + public IReadOnlyList Drops { get; } + public bool CanSeeInvisible { get; } + public IReadOnlyList BCards { get; } + public IReadOnlyList ModeBCards { get; } + + public IReadOnlyList MonsterSkills { get; } + + public SkillInfo BasicSkill { get; } + + public SkillCast SkillCast => _castingComponent.SkillCast; + + public bool IsCastingSkill => _castingComponent.IsCastingSkill; + + public void SetCastingSkill(SkillInfo skill, DateTime time) + { + _castingComponent.SetCastingSkill(skill, time); + } + + public void RemoveCastingSkill() + { + _castingComponent.RemoveCastingSkill(); + } + + public IReadOnlyDictionary EndBuffDamages => _endBuffDamageComponent.EndBuffDamages; + + public void AddEndBuff(short buffVnum, int damage) + { + _endBuffDamageComponent.AddEndBuff(buffVnum, damage); + } + + public int DecreaseDamageEndBuff(short buffVnum, int damage) => _endBuffDamageComponent.DecreaseDamageEndBuff(buffVnum, damage); + + public void RemoveEndBuffDamage(short buffVnum) + { + _endBuffDamageComponent.RemoveEndBuffDamage(buffVnum); + } + + public void ChangeHpMultiplier(double hp = 1) + { + _enhancedHp = hp; + } + + private void UpdateMonstersBaseStatistics() + { + BaseMaxHp = _battleEntityAlgorithmService.GetBasicHp((short)MonsterRaceType, Level, MeleeHpFactor, CleanHp); + BaseMaxMp = _battleEntityAlgorithmService.GetBasicMp((short)MonsterRaceType, Level, MagicMpFactor, CleanMp); + + BaseDamageMinimum = _battleEntityAlgorithmService.GetAttack(true, (short)MonsterRaceType, AttackType, WeaponLevel, WinfoValue, Level, GetModifier(), CleanDamageMin); + BaseDamageMaximum = _battleEntityAlgorithmService.GetAttack(false, (short)MonsterRaceType, AttackType, WeaponLevel, WinfoValue, Level, GetModifier(), CleanDamageMax); + BaseConcentrate = (short)_battleEntityAlgorithmService.GetHitrate((short)MonsterRaceType, AttackType, WeaponLevel, Level, GetModifier(), CleanHitRate); + BaseCloseDefence = (short)_battleEntityAlgorithmService.GetDefense((short)MonsterRaceType, AttackType.Melee, ArmorLevel, Level, GetModifier(), CleanMeleeDefence); + DistanceDefence = (short)_battleEntityAlgorithmService.GetDefense((short)MonsterRaceType, AttackType.Ranged, ArmorLevel, Level, GetModifier(), CleanRangeDefence); + MagicDefence = (short)_battleEntityAlgorithmService.GetDefense((short)MonsterRaceType, AttackType.Magical, ArmorLevel, Level, GetModifier(), CleanMagicDefence); + DefenceDodge = (short)_battleEntityAlgorithmService.GetDodge((short)MonsterRaceType, ArmorLevel, Level, GetModifier(), CleanDodge); + DistanceDefenceDodge = (short)_battleEntityAlgorithmService.GetDodge((short)MonsterRaceType, ArmorLevel, Level, GetModifier(), CleanDodge); + + Xp = Level < 20 ? 60 * Level + BaseXp : 70 * Level + BaseXp; + JobXp = Level > 60 ? 105 + BaseJobXp : 120 + BaseJobXp; + + if (Xp < 0) + { + Xp = 0; + } + + if (JobXp < 0) + { + JobXp = 0; + } + } + + private int GetModifier() + { + return AttackType switch + { + AttackType.Melee => MeleeHpFactor, + AttackType.Ranged => RangeDodgeFactor, + AttackType.Magical => MagicMpFactor, + _ => 0 + }; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.CoreImpl/Entities/MonsterEntityFactory.cs b/srcs/_plugins/Plugin.CoreImpl/Entities/MonsterEntityFactory.cs new file mode 100644 index 0000000..908951f --- /dev/null +++ b/srcs/_plugins/Plugin.CoreImpl/Entities/MonsterEntityFactory.cs @@ -0,0 +1,100 @@ +using System.Linq; +using PhoenixLib.Events; +using WingsEmu.DTOs.Maps; +using WingsEmu.DTOs.NpcMonster; +using WingsEmu.Game; +using WingsEmu.Game._enum; +using WingsEmu.Game.Algorithm; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Skills; +using WingsEmu.Game.Triggers; + +namespace Plugin.CoreImpl.Entities +{ + public class MonsterEntityFactory : IMonsterEntityFactory + { + private readonly IAsyncEventPipeline _asyncEventPipeline; + private readonly IBattleEntityAlgorithmService _battleEntityAlgorithmService; + private readonly INpcMonsterManager _npcMonsterManager; + private readonly IRandomGenerator _randomGenerator; + private readonly IEntitySkillFactory _skillFactory; + + public MonsterEntityFactory(INpcMonsterManager npcMonsterManager, IAsyncEventPipeline asyncEventPipeline, IRandomGenerator randomGenerator, IEntitySkillFactory skillFactory, + IBattleEntityAlgorithmService battleEntityAlgorithmService) + { + _npcMonsterManager = npcMonsterManager; + _asyncEventPipeline = asyncEventPipeline; + _randomGenerator = randomGenerator; + _skillFactory = skillFactory; + _battleEntityAlgorithmService = battleEntityAlgorithmService; + } + + public IMonsterEntity CreateMapMonster(MapMonsterDTO mapMonsterDto, IMapInstance mapInstance) + { + IMonsterData monsterData = _npcMonsterManager.GetNpc(mapMonsterDto.MonsterVNum); + if (monsterData == null) + { + return null; + } + + var skills = monsterData.MonsterSkills.Select(skill => _skillFactory.CreateNpcMonsterSkill(skill.Skill.Id, skill.Rate, skill.IsBasicAttack, skill.IsIgnoringHitChance)).ToList(); + // this is pure puke, needs to rework NpcMonster properly... + if (monsterData is NpcMonsterDto npcMonsterDto) + { + skills.AddRange(npcMonsterDto.Skills.Select(s => _skillFactory.CreateNpcMonsterSkill(s.SkillVNum, s.Rate, s.IsBasicAttack, s.IsIgnoringHitChance))); + } + + // fetch skill + + var monsterBuilder = new MonsterEntityBuilder + { + IsWalkingAround = mapMonsterDto.IsMoving, + IsRespawningOnDeath = true, + IsHostile = (HostilityType)monsterData.RawHostility != HostilityType.NOT_HOSTILE, + IsMateTrainer = mapMonsterDto.MonsterVNum == (int)MonsterVnum.DOLL_XP_FIREBALL, + PositionX = mapMonsterDto.MapX, + PositionY = mapMonsterDto.MapY, + Direction = mapMonsterDto.Direction + }; + + return new MonsterEntity(mapInstance.GenerateEntityId(), new EventTriggerContainer(_asyncEventPipeline), new BCardComponent(_randomGenerator), + _asyncEventPipeline, monsterData, mapInstance, monsterBuilder, skills, _battleEntityAlgorithmService); + } + + public IMonsterEntity CreateMonster(int monsterVNum, IMapInstance mapInstance, MonsterEntityBuilder monsterAdditionalData = null) + { + IMonsterData monsterData = _npcMonsterManager.GetNpc(monsterVNum); + return monsterData == null ? null : CreateMonster(monsterData, mapInstance, monsterAdditionalData); + } + + public IMonsterEntity CreateMonster(int? id, int monsterVNum, IMapInstance mapInstance, MonsterEntityBuilder monsterAdditionalData = null) + { + IMonsterData monsterData = _npcMonsterManager.GetNpc(monsterVNum); + return monsterData == null ? null : CreateMonster(id, monsterData, mapInstance, monsterAdditionalData); + } + + public IMonsterEntity CreateMonster(IMonsterData monsterData, IMapInstance mapInstance, MonsterEntityBuilder monsterAdditionalData = null) => + CreateMonster(mapInstance.GenerateEntityId(), monsterData, mapInstance, monsterAdditionalData); + + public IMonsterEntity CreateMonster(int? id, IMonsterData monsterData, IMapInstance mapInstance, MonsterEntityBuilder monsterAdditionalData = null) + { + var skills = monsterData.MonsterSkills.Select(skill => _skillFactory.CreateNpcMonsterSkill(skill.Skill.Id, skill.Rate, skill.IsBasicAttack, skill.IsIgnoringHitChance)).ToList(); + + // this is pure puke, needs to rework NpcMonster properly... + if (monsterData is NpcMonsterDto npcMonsterDto) + { + skills.AddRange(npcMonsterDto.Skills.Select(s => _skillFactory.CreateNpcMonsterSkill(s.SkillVNum, s.Rate, s.IsBasicAttack, s.IsIgnoringHitChance))); + } + + // get skills + // get drops + // get bcards + return new MonsterEntity(id ?? mapInstance.GenerateEntityId(), new EventTriggerContainer(_asyncEventPipeline), + new BCardComponent(_randomGenerator), _asyncEventPipeline, monsterData, mapInstance, monsterAdditionalData, skills, _battleEntityAlgorithmService); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.CoreImpl/Entities/NpcEntity.cs b/srcs/_plugins/Plugin.CoreImpl/Entities/NpcEntity.cs new file mode 100644 index 0000000..5316ca9 --- /dev/null +++ b/srcs/_plugins/Plugin.CoreImpl/Entities/NpcEntity.cs @@ -0,0 +1,582 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Data.Drops; +using WingsEmu.Core.Generics; +using WingsEmu.DTOs.BCards; +using WingsEmu.DTOs.Maps; +using WingsEmu.DTOs.NpcMonster; +using WingsEmu.DTOs.Skills; +using WingsEmu.Game; +using WingsEmu.Game._enum; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Battle.Managers; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Maps; +using WingsEmu.Game.RainbowBattle; +using WingsEmu.Game.Shops; +using WingsEmu.Game.Skills; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Battle; + +namespace Plugin.CoreImpl.Entities +{ + public class NpcEntity : INpcEntity + { + private readonly IAsyncEventPipeline _asyncEventPipeline; + private readonly ICastingComponent _castingComponent; + private readonly IEndBuffDamageComponent _endBuffDamageComponent; + private readonly IEventTriggerContainer _eventContainer; + + public NpcEntity(IEventTriggerContainer eventContainer, IBCardComponent bCardComponent, IAsyncEventPipeline asyncEventPipeline, IMonsterData monsterData, IMapInstance mapInstance, + ISkillsManager skillsManager, + MapNpcDTO npcDto = null, ShopNpc shopNpc = null, int? id = null, INpcAdditionalData npcAdditionalData = null, short? optionalDialog = null) + { + UniqueId = Guid.NewGuid(); + BCardComponent = bCardComponent; + _castingComponent = new CastingComponent(); + ChargeComponent = new ChargeComponent(); + Damagers = new ThreadSafeHashSet(); + _endBuffDamageComponent = new EndBuffDamageComponent(); + _eventContainer = eventContainer; + BuffComponent = new BuffComponent(); + _asyncEventPipeline = asyncEventPipeline; + + ShouldRespawn = true; + + IsHostile = RawHostility != (int)HostilityType.NOT_HOSTILE; + if (npcDto != null) + { + Id = npcDto.Id; + Dialog = npcDto.Dialog; + QuestDialog = npcDto.QuestDialog; + Direction = npcDto.Direction; + Effect = npcDto.Effect; + EffectDelay = TimeSpan.FromMilliseconds(npcDto.EffectDelay); + IsDisabled = npcDto.IsDisabled; + IsMoving = npcDto.IsMoving; + IsSitting = npcDto.IsSitting; + PositionX = npcDto.MapX; + PositionY = npcDto.MapY; + MapId = npcDto.MapId; + CanAttack = npcDto.CanAttack; + HasGodMode = npcDto.HasGodMode; + CustomName = npcDto.CustomName; + IsHostile = npcDto.CanAttack; + } + + Dialog = optionalDialog ?? Dialog; + AmountRequired = monsterData.AmountRequired; + ArmorLevel = monsterData.ArmorLevel; + AttackType = monsterData.AttackType; + AttackUpgrade = monsterData.AttackUpgrade; + BasicCastTime = monsterData.BasicCastTime; + BasicCooldown = monsterData.BasicCooldown; + BasicRange = monsterData.BasicRange; + BCards = monsterData.BCards; + AttackEffect = monsterData.AttackEffect; + BasicHitChance = monsterData.BasicHitChance; + CanBeCollected = monsterData.CanBeCollected; + CanBeDebuffed = monsterData.CanBeDebuffed; + CanBeCaught = monsterData.CanBeCaught; + CanBePushed = monsterData.CanBePushed; + CanRegenMp = monsterData.CanRegenMp; + CanWalk = monsterData.CanWalk; + CellSize = monsterData.CellSize; + CleanDamageMin = monsterData.CleanDamageMin; + CleanDamageMax = monsterData.CleanDamageMax; + CleanHitRate = monsterData.CleanHitRate; + CleanMeleeDefence = monsterData.CleanMeleeDefence; + CleanRangeDefence = monsterData.CleanRangeDefence; + CleanMagicDefence = monsterData.CleanMagicDefence; + CleanDodge = monsterData.CleanDodge; + CleanHp = monsterData.CleanHp; + CleanMp = monsterData.CleanMp; + BaseCloseDefence = monsterData.BaseCloseDefence; + BaseConcentrate = monsterData.BaseConcentrate; + BaseCriticalChance = monsterData.BaseCriticalChance; + BaseCriticalRate = monsterData.BaseCriticalRate; + DamagedOnlyLastJajamaruSkill = monsterData.DamagedOnlyLastJajamaruSkill; + BaseDamageMaximum = monsterData.BaseDamageMaximum; + BaseDamageMinimum = monsterData.BaseDamageMinimum; + BaseDarkResistance = monsterData.BaseDarkResistance; + DeathEffect = monsterData.DeathEffect; + BaseMaxHp = monsterData.BaseMaxHp; + BaseMaxMp = monsterData.BaseMaxMp; + DefenceDodge = monsterData.DefenceDodge; + DefenceUpgrade = monsterData.DefenceUpgrade; + DisappearAfterHitting = monsterData.DisappearAfterHitting; + DisappearAfterSeconds = monsterData.DisappearAfterSeconds; + DisappearAfterSecondsMana = monsterData.DisappearAfterSecondsMana; + DistanceDefence = monsterData.DistanceDefence; + DistanceDefenceDodge = monsterData.DistanceDefenceDodge; + Drops = monsterData.Drops; + BaseElement = monsterData.BaseElement; + BaseElementRate = monsterData.BaseElementRate; + BaseFireResistance = monsterData.BaseFireResistance; + GiveDamagePercentage = monsterData.GiveDamagePercentage; + GroupAttack = monsterData.GroupAttack; + HasMode = monsterData.HasMode; + RawHostility = monsterData.RawHostility; + IconId = monsterData.IconId; + IsPercent = monsterData.IsPercent; + JobXp = monsterData.JobXp; + BaseLevel = monsterData.BaseLevel; + BaseLightResistance = monsterData.BaseLightResistance; + MagicDefence = monsterData.MagicDefence; + MagicMpFactor = monsterData.MagicMpFactor; + MeleeHpFactor = monsterData.MeleeHpFactor; + MinimumAttackRange = monsterData.MinimumAttackRange; + MonsterVNum = monsterData.MonsterVNum; + Name = monsterData.Name; + NoticeRange = monsterData.NoticeRange; + OnDefenseOnlyOnce = monsterData.OnDefenseOnlyOnce; + PermanentEffect = monsterData.PermanentEffect; + MonsterRaceType = monsterData.MonsterRaceType; + MonsterRaceSubType = monsterData.MonsterRaceSubType; + RangeDodgeFactor = monsterData.RangeDodgeFactor; + BaseRespawnTime = monsterData.BaseRespawnTime; + SpawnMobOrColor = monsterData.SpawnMobOrColor; + BaseSpeed = monsterData.BaseSpeed; + SpriteSize = monsterData.SpriteSize; + TakeDamages = monsterData.TakeDamages; + VNumRequired = monsterData.VNumRequired; + BaseWaterResistance = monsterData.BaseWaterResistance; + WeaponLevel = monsterData.WeaponLevel; + WinfoValue = monsterData.WinfoValue; + Xp = monsterData.Xp; + MaxTries = monsterData.MaxTries; + CollectionCooldown = monsterData.CollectionCooldown; + CollectionDanceTime = monsterData.CollectionDanceTime; + TeleportRemoveFromInventory = monsterData.TeleportRemoveFromInventory; + BasicDashSpeed = monsterData.BasicDashSpeed; + ModeIsHpTriggered = monsterData.ModeIsHpTriggered; + ModeLimiterType = monsterData.ModeLimiterType; + ModeRangeTreshold = monsterData.ModeRangeTreshold; + ModeCModeVnum = monsterData.ModeCModeVnum; + ModeHpTresholdOrItemVnum = monsterData.ModeHpTresholdOrItemVnum; + MidgardDamage = monsterData.MidgardDamage; + HasDash = monsterData.HasDash; + DropToInventory = monsterData.DropToInventory; + ModeBCards = monsterData.ModeBCards; + BaseXp = monsterData.BaseXp; + BaseJobXp = monsterData.BaseJobXp; + Level = BaseLevel; + + if (npcAdditionalData != null) + { + IsTimeSpaceMate = npcAdditionalData.IsTimeSpaceMate; + IsProtected = npcAdditionalData.IsProtected; + MinilandOwner = npcAdditionalData.MinilandOwner; + ShouldRespawn = npcAdditionalData.NpcShouldRespawn; + CanMove = npcAdditionalData.CanMove; + IsMoving = npcAdditionalData.CanMove; + CanAttack = npcAdditionalData.CanAttack; + Direction = npcAdditionalData.NpcDirection; + IsHostile = npcAdditionalData.IsHostile; + Faction = npcAdditionalData.FactionType; + TimeSpaceInfo = npcAdditionalData.TimeSpaceInfo; + TimeSpaceOwnerId = npcAdditionalData.TimeSpaceOwnerId; + RainbowFlag = npcAdditionalData.RainbowFlag; + + if (npcAdditionalData.HpMultiplier.HasValue) + { + BaseMaxHp = (int)(BaseMaxHp * npcAdditionalData.HpMultiplier.Value); + Hp = BaseMaxHp; + } + + if (npcAdditionalData.MpMultiplier.HasValue) + { + BaseMaxMp = (int)(BaseMaxMp * npcAdditionalData.MpMultiplier.Value); + Mp = BaseMaxMp; + } + + if (npcAdditionalData.CustomLevel.HasValue) + { + Level = npcAdditionalData.CustomLevel.Value; + } + } + + LastEffect = Death = SpawnDate = DateTime.UtcNow; + FirstX = PositionX; + FirstY = PositionY; + IsStillAlive = true; + Speed = BaseSpeed; + Hp = BaseMaxHp; + Mp = BaseMaxMp; + Killer = null; + Target = null; + MonsterSkills = monsterData.MonsterSkills; + this.InitializeBCards(); + ShopNpc = shopNpc; + CurrentCollection = MaxTries; + CanSeeInvisible = monsterData.CanSeeInvisible; + + // pure puke, needs proper entity rework :( + if (monsterData is NpcMonsterDto npcMonsterDto) + { + var skills = new List(); + + foreach (NpcMonsterSkillDTO skill in npcMonsterDto.Skills) + { + var monsterSkill = new NpcMonsterSkill(skillsManager.GetSkill(skill.SkillVNum), skill.Rate, skill.IsBasicAttack, skill.IsIgnoringHitChance); + skills.Add(monsterSkill); + Skills.Add(monsterSkill); + + if (monsterSkill.IsBasicAttack) + { + ReplacedBasicSkill = monsterSkill; + } + } + + NotBasicSkills = skills.Where(x => !x.IsBasicAttack).ToArray(); + if (HasDash) + { + DashSkill = skills.Count != 0 ? skills[0] : null; + } + + SkillsWithoutDashSkill = skills.Where(x => DashSkill != null && DashSkill.Skill.Id != x.Skill.Id).ToArray(); + } + + BasicSkill = new SkillInfo + { + Vnum = default, + AttackType = AttackType, + Range = BasicRange, + CastTime = BasicCastTime, + Cooldown = BasicCooldown, + HitEffect = AttackEffect, + Element = BaseElement, + HitChance = BasicHitChance, + HitAnimation = 11, + TargetType = TargetType.Target, + TargetAffectedEntities = TargetAffectedEntities.Enemies, + SkillType = SkillType.MonsterSkill + }; + + if (id != null) + { + Id = id.Value; + } + + if (Id == default) + { + Id = mapInstance.GenerateEntityId(); + } + + MapInstance = mapInstance; + } + + public IBCardComponent BCardComponent { get; } + public IChargeComponent ChargeComponent { get; } + public IBuffComponent BuffComponent { get; } + + public byte Size { get; set; } = 10; + public DateTime SpawnDate { get; set; } + + public VisualType Type => VisualType.Npc; + + public void AddEvent(string key, IAsyncEvent notification, bool removedOnTrigger = false) => _eventContainer.AddEvent(key, notification, removedOnTrigger); + + public async Task TriggerEvents(string key) => await _eventContainer.TriggerEvents(key); + + public async Task EmitEventAsync(T eventArgs) where T : IBattleEntityEvent + { + if (eventArgs.Entity != this) + { + throw new ArgumentException("An event should be emitted only from the event sender"); + } + + await _asyncEventPipeline.ProcessEventAsync(eventArgs); + } + + public void EmitEvent(T eventArgs) where T : IBattleEntityEvent + { + EmitEventAsync(eventArgs).ConfigureAwait(false).GetAwaiter().GetResult(); + } + + public SkillCast SkillCast => _castingComponent.SkillCast; + public bool IsCastingSkill => _castingComponent.IsCastingSkill; + + public void SetCastingSkill(SkillInfo skill, DateTime time) + { + _castingComponent.SetCastingSkill(skill, time); + } + + public void RemoveCastingSkill() + { + _castingComponent.RemoveCastingSkill(); + } + + public IReadOnlyDictionary EndBuffDamages => _endBuffDamageComponent.EndBuffDamages; + + public void AddEndBuff(short buffVnum, int damage) + { + _endBuffDamageComponent.AddEndBuff(buffVnum, damage); + } + + public int DecreaseDamageEndBuff(short buffVnum, int damage) => _endBuffDamageComponent.DecreaseDamageEndBuff(buffVnum, damage); + + public void RemoveEndBuffDamage(short buffVnum) + { + _endBuffDamageComponent.RemoveEndBuffDamage(buffVnum); + } + + #region Properties + + public short AmountRequired { get; } + public byte ArmorLevel { get; } + public AttackType AttackType { get; } + public byte AttackUpgrade { get; } + public byte BasicCastTime { get; } + public short BasicCooldown { get; } + public byte BasicRange { get; } + public short AttackEffect { get; } + public short BasicHitChance { get; } + public bool CanBeCollected { get; } + public bool CanBeDebuffed { get; } + public bool CanBeCaught { get; } + public bool CanBePushed { get; } + public bool CanRegenMp { get; } + public bool CanWalk { get; } + public int CellSize { get; } + public int CleanDamageMin { get; } + public int CleanDamageMax { get; } + public int CleanHitRate { get; } + public int CleanMeleeDefence { get; } + public int CleanRangeDefence { get; } + public int CleanMagicDefence { get; } + public int CleanDodge { get; } + public int CleanHp { get; } + public int CleanMp { get; } + public short BaseCloseDefence { get; } + public short BaseConcentrate { get; } + public short BaseCriticalChance { get; } + public short BaseCriticalRate { get; } + public bool DamagedOnlyLastJajamaruSkill { get; } + public int BaseDamageMaximum { get; } + public int BaseDamageMinimum { get; } + public short BaseDarkResistance { get; } + public short DeathEffect { get; } + public int BaseMaxHp { get; } + public int BaseMaxMp { get; } + public short DefenceDodge { get; } + public byte DefenceUpgrade { get; } + public bool DisappearAfterHitting { get; } + public bool DisappearAfterSeconds { get; } + public bool DisappearAfterSecondsMana { get; } + public short DistanceDefence { get; } + public short DistanceDefenceDodge { get; } + public byte BaseElement { get; } + public short BaseElementRate { get; } + public short BaseFireResistance { get; } + public FactionType? SuggestedFaction => Faction; + public int GiveDamagePercentage { get; } + public int GroupAttack { get; } + public bool HasMode { get; } + public int RawHostility { get; } + public int IconId { get; } + public bool IsPercent { get; } + public int JobXp { get; } + public byte BaseLevel { get; } + public short BaseLightResistance { get; } + public short MagicDefence { get; } + public short MagicMpFactor { get; } + public short MeleeHpFactor { get; } + public sbyte MinimumAttackRange { get; } + public int MonsterVNum { get; } + public string Name { get; } + public byte NoticeRange { get; } + public bool OnDefenseOnlyOnce { get; } + public short PermanentEffect { get; } + public MonsterRaceType MonsterRaceType { get; } + public byte MonsterRaceSubType { get; } + public short RangeDodgeFactor { get; } + public TimeSpan BaseRespawnTime { get; } + public int SpawnMobOrColor { get; } + public byte BaseSpeed { get; } + public int SpriteSize { get; } + public int TakeDamages { get; } + public short VNumRequired { get; } + public short BaseWaterResistance { get; } + public byte WeaponLevel { get; } + public byte WinfoValue { get; } + public int Xp { get; } + public byte MaxTries { get; } + public short CollectionCooldown { get; } + public byte CollectionDanceTime { get; } + public bool TeleportRemoveFromInventory { get; } + public short BasicDashSpeed { get; } + public bool ModeIsHpTriggered { get; } + public byte ModeLimiterType { get; } + public short ModeRangeTreshold { get; } + public short ModeCModeVnum { get; } + public short ModeHpTresholdOrItemVnum { get; } + public short MidgardDamage { get; } + public bool HasDash { get; } + public bool DropToInventory { get; } + public bool TeleportBackOnNoticeRangeExceed { get; } + public int BaseXp { get; } + public int BaseJobXp { get; } + + public FactionType Faction { get; } = FactionType.Neutral; + + public byte Speed { get; set; } + public bool ShouldRespawn { get; set; } + public short FirstX { get; set; } + public short FirstY { get; set; } + public Guid UniqueId { get; } + public bool IsHostile { get; } + public float? HpMultiplier { get; } + public float? MpMultiplier { get; } + public byte? CustomLevel { get; } + public FactionType FactionType { get; } + public long? TimeSpaceOwnerId { get; } + public TimeSpaceFileConfiguration TimeSpaceInfo { get; } + public bool IsTimeSpaceMate { get; set; } + public bool IsProtected { get; set; } + public IPlayerEntity MinilandOwner { get; } + public bool NpcShouldRespawn { get; } + public bool CanMove { get; } + public ThreadSafeHashSet Targets { get; } = new(); + public ThreadSafeHashSet<(VisualType, long)> TargetsByVisualTypeAndId { get; } = new(); + public DateTime LastTargetsRefresh { get; set; } + public DateTime Death { get; set; } + public DateTime LastEffect { get; set; } + public DateTime LastSkill { get; set; } + public DateTime LastSpecialHpDecrease { get; set; } + + public IMapInstance MapInstance { get; private set; } + + public IBattleEntity Killer { get; set; } + public IBattleEntity Target { get; set; } + public DateTime NextTick { get; set; } + public DateTime NextAttackReady { get; set; } + public bool ModeIsActive { get; set; } + public short Morph { get; set; } + public long ModeDeathsSinceRespawn { get; set; } + public (VisualType, long) LastAttackedEntity { get; set; } + public bool IsRunningAway { get; set; } + public byte ReturnTimeOut { get; set; } + public IReadOnlyList NotBasicSkills { get; } + public IReadOnlyList SkillsWithoutDashSkill { get; } + public INpcMonsterSkill ReplacedBasicSkill { get; } + public INpcMonsterSkill DashSkill { get; } + + public byte Level { get; set; } + public int Hp { get; set; } + + public int MaxHp + { + get => this.GetMaxHp(BaseMaxHp); + set => throw new NotImplementedException(); + } + + public int Mp { get; set; } + + public int MaxMp + { + get => this.GetMaxMp(BaseMaxMp); + set => throw new NotImplementedException(); + } + + public SkillInfo BasicSkill { get; } + public ThreadSafeHashSet Damagers { get; } + + public IReadOnlyList Drops { get; } + + public bool CanSeeInvisible { get; } + + public IReadOnlyList BCards { get; } + public IReadOnlyList ModeBCards { get; } + + private IReadOnlyList _monsterSkills; + + public IReadOnlyList MonsterSkills + { + get => _monsterSkills; + private set + { + _monsterSkills = value; + Skills = value.Cast().ToList(); + } + } + + public List Skills { get; private set; } + + public byte Element { get; set; } + public int ElementRate { get; set; } + public int FireResistance { get; set; } + public int WaterResistance { get; set; } + public int LightResistance { get; set; } + public int DarkResistance { get; set; } + public int DamagesMinimum { get; set; } + public int DamagesMaximum { get; set; } + + public Position Position + { + get => new(PositionX, PositionY); + set + { + Position pos = value; + + PositionX = pos.X; + PositionY = pos.Y; + } + } + + public short PositionX { get; private set; } + public short PositionY { get; private set; } + public bool IsStillAlive { get; set; } + public bool ReturningToFirstPosition { get; set; } + public bool ShouldFindNewTarget { get; set; } + public bool FindNewPositionAroundTarget { get; set; } + public bool IsApproachingTarget { get; set; } + public bool OnFirstDamageReceive { get; set; } + public bool HasGodMode { get; } + public byte CurrentCollection { get; set; } + public DateTime LastCollection { get; set; } = DateTime.UtcNow; + public string CustomName { get; } + public long? CharacterPartnerId { get; set; } + public DateTime LastBasicAttack { get; set; } + public DateTime LastTimeSpaceHeal { get; set; } + public RainBowFlag RainbowFlag { get; set; } + + public void ChangeMapInstance(IMapInstance mapInstance) + { + MapInstance = mapInstance; + } + + public bool CanAttack { get; set; } + public byte NpcDirection { get; } + + public ThreadSafeHashSet AggroedEntities { get; } = new(); + + public int Id { get; } + public short Dialog { get; } + public int? QuestDialog { get; } + public short Effect { get; } + public TimeSpan EffectDelay { get; } + public bool IsDisabled { get; } + public bool IsMoving { get; } + public bool IsSitting { get; } + public int MapId { get; } + public int NpcVNum => MonsterVNum; + public ShopNpc ShopNpc { get; set; } + public byte Direction { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.CoreImpl/Entities/NpcEntityFactory.cs b/srcs/_plugins/Plugin.CoreImpl/Entities/NpcEntityFactory.cs new file mode 100644 index 0000000..a84aeb8 --- /dev/null +++ b/srcs/_plugins/Plugin.CoreImpl/Entities/NpcEntityFactory.cs @@ -0,0 +1,82 @@ +using System.Linq; +using PhoenixLib.Events; +using Plugin.CoreImpl.Configs; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Managers.ServerData; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Shops; +using WingsEmu.Game.Triggers; + +namespace Plugin.CoreImpl.Entities +{ + public class NpcEntityFactory : INpcEntityFactory + { + private readonly IAsyncEventPipeline _asyncEventPipeline; + private readonly HardcodedDialogsByNpcVnumFileConfig _config; + private readonly IMapNpcManager _mapNpcManager; + private readonly INpcMonsterManager _npcMonsterManager; + private readonly IRandomGenerator _randomGenerator; + private readonly IShopManager _shopManager; + private readonly ISkillsManager _skillsManager; + + public NpcEntityFactory(IAsyncEventPipeline asyncEventPipeline, IRandomGenerator randomGenerator, INpcMonsterManager npcMonsterManager, IMapNpcManager mapNpcManager, IShopManager shopManager, + HardcodedDialogsByNpcVnumFileConfig config, ISkillsManager skillsManager) + { + _asyncEventPipeline = asyncEventPipeline; + _randomGenerator = randomGenerator; + _npcMonsterManager = npcMonsterManager; + _mapNpcManager = mapNpcManager; + _shopManager = shopManager; + _config = config; + _skillsManager = skillsManager; + } + + public INpcEntity CreateMapNpc(int monsterVNum, IMapInstance mapInstance, int? id = null, INpcAdditionalData npcAdditionalData = null) + { + MapNpcDTO npcDto = _mapNpcManager.GetMapNpcsPerVNum(monsterVNum).FirstOrDefault(); + return npcDto == null ? null : CreateMapNpc(npcDto, mapInstance, id, npcAdditionalData); + } + + public INpcEntity CreateMapNpc(IMonsterData monsterDto, IMapInstance mapInstance, int? id = null, INpcAdditionalData npcAdditionalData = null) + { + MapNpcDTO npcDto = _mapNpcManager.GetMapNpcsPerVNum(monsterDto.MonsterVNum).FirstOrDefault(); + return npcDto == null ? null : CreateMapNpc(monsterDto, npcDto, mapInstance, id, npcAdditionalData); + } + + public INpcEntity CreateMapNpc(MapNpcDTO npcDto, IMapInstance mapInstance, int? id = null, INpcAdditionalData npcAdditionalData = null) + { + IMonsterData monsterData = _npcMonsterManager.GetNpc(npcDto.NpcVNum); + return monsterData == null ? null : CreateMapNpc(monsterData, npcDto, mapInstance, id, npcAdditionalData); + } + + public INpcEntity CreateMapNpc(IMonsterData monsterData, MapNpcDTO npcDto, IMapInstance mapInstance, int? id = null, INpcAdditionalData npcAdditionalData = null) + { + ShopNpc shop = _shopManager.GetShopByNpcId(npcDto.Id); + return new NpcEntity(new EventTriggerContainer(_asyncEventPipeline), new BCardComponent(_randomGenerator), _asyncEventPipeline, monsterData, mapInstance, _skillsManager, npcDto, shop, id, + npcAdditionalData); + } + + public INpcEntity CreateNpc(int monsterVNum, IMapInstance mapInstance, int? id = null, INpcAdditionalData npcAdditionalData = null) + { + IMonsterData monsterData = _npcMonsterManager.GetNpc(monsterVNum); + if (monsterData == null) + { + // should throw; + return null; + } + + return CreateNpc(monsterData, mapInstance, id, npcAdditionalData); + } + + public INpcEntity CreateNpc(IMonsterData monsterDto, IMapInstance mapInstance, int? id = null, INpcAdditionalData npcAdditionalData = null) + { + HardcodedDialogsByNpcVnum tmp = _config.FirstOrDefault(s => s.NpcVnum == monsterDto.MonsterVNum); + return new NpcEntity(new EventTriggerContainer(_asyncEventPipeline), new BCardComponent(_randomGenerator), _asyncEventPipeline, monsterDto, mapInstance, _skillsManager, null, null, id, + npcAdditionalData, tmp?.DialogId); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.CoreImpl/Entities/PortalEntity.cs b/srcs/_plugins/Plugin.CoreImpl/Entities/PortalEntity.cs new file mode 100644 index 0000000..72186db --- /dev/null +++ b/srcs/_plugins/Plugin.CoreImpl/Entities/PortalEntity.cs @@ -0,0 +1,99 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Maps; +using WingsEmu.Game.TimeSpaces.Enums; +using WingsEmu.Game.Triggers; +using WingsEmu.Packets.Enums; + +namespace Plugin.CoreImpl.Entities +{ + public class PortalEntity : IPortalEntity + { + private readonly Guid? _destinationMapId; + private readonly int? _destinationMapMapId; + private readonly IEventTriggerContainer _eventContainer; + private IMapInstance? _destinationMap; + + + public PortalEntity(IAsyncEventPipeline eventPipeline, PortalType portalType, IMapInstance mapInstance, Position position, short? raidType = null, short? mapNameId = null, + Position? destPos = null, + Guid? destinationMapId = null, IMapInstance? destinationMap = null, int? destinationMapMapId = null, + PortalMinimapOrientation minimapOrientation = PortalMinimapOrientation.NORTH) + { + Type = portalType; + MapInstance = mapInstance; + PositionX = position.X; + PositionY = position.Y; + MinimapOrientation = minimapOrientation; + RaidType = raidType; + MapNameId = mapNameId; + + if (destPos.HasValue) + { + DestinationX = destPos.Value.X; + DestinationY = destPos.Value.Y; + } + + _destinationMap = destinationMap; + _destinationMapId = destinationMapId; + _destinationMapMapId = destinationMapMapId; + _eventContainer = new EventTriggerContainer(eventPipeline); + } + + private static IMapManager _mapManager => StaticMapManager.Instance; + + public int Id { get; set; } + public PortalType Type { get; set; } + public bool IsDisabled { get; set; } + public PortalMinimapOrientation MinimapOrientation { get; set; } + public IMapInstance MapInstance { get; } + public short PositionX { get; set; } + public short PositionY { get; set; } + + public short? RaidType { get; set; } + public short? MapNameId { get; set; } + public short? DestinationX { get; set; } + public short? DestinationY { get; set; } + + public IMapInstance? DestinationMapInstance + { + get + { + if (_destinationMap != null) + { + return _destinationMap; + } + + if (_destinationMapId.HasValue) + { + return _destinationMap = _mapManager.GetMapInstance(_destinationMapId.Value); + } + + if (_destinationMapMapId.HasValue) + { + return _destinationMap = _mapManager.GetBaseMapInstanceByMapId(_destinationMapMapId.Value); + } + + return null; + } + } + + + public void AddEvent(string key, IAsyncEvent notification, bool removedOnTrigger = false) + { + _eventContainer.AddEvent(key, notification, removedOnTrigger); + } + + public async Task TriggerEvents(string key) + { + await _eventContainer.TriggerEvents(key); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.CoreImpl/Entities/PortalFactory.cs b/srcs/_plugins/Plugin.CoreImpl/Entities/PortalFactory.cs new file mode 100644 index 0000000..b1e0d5c --- /dev/null +++ b/srcs/_plugins/Plugin.CoreImpl/Entities/PortalFactory.cs @@ -0,0 +1,35 @@ +using System; +using PhoenixLib.Events; +using WingsEmu.Game; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Portals; +using WingsEmu.Game.TimeSpaces.Enums; +using WingsEmu.Packets.Enums; + +namespace Plugin.CoreImpl.Entities +{ + public class PortalFactory : IPortalFactory + { + private readonly IAsyncEventPipeline _eventPipeline; + + public PortalFactory(IAsyncEventPipeline eventPipeline) => _eventPipeline = eventPipeline; + + public IPortalEntity CreatePortal(PortalType portalType, IMapInstance source, Position sourcePos) => new PortalEntity(_eventPipeline, portalType, source, sourcePos); + + public IPortalEntity CreatePortal(PortalType portalType, IMapInstance source, Position sourcePos, Guid destMapInstanceId, Position destPos) => + new PortalEntity(_eventPipeline, portalType, source, sourcePos, destPos: destPos, destinationMapId: destMapInstanceId); + + public IPortalEntity CreatePortal(PortalType portalType, IMapInstance source, Position sourcePos, int mapDestId, Position destPos) => + new PortalEntity(_eventPipeline, portalType, source, sourcePos, destinationMapMapId: mapDestId, destPos: destPos); + + public IPortalEntity CreatePortal(PortalType portalType, IMapInstance source, Position sourcePos, int mapDestId, Position destPos, short? raidType, short? mapNameId) => + new PortalEntity(_eventPipeline, portalType, source, sourcePos, raidType, mapNameId, destinationMapMapId: mapDestId, destPos: destPos); + + public IPortalEntity CreatePortal(PortalType portalType, IMapInstance source, Position sourcePos, IMapInstance destination, Position destPos) => + CreatePortal(portalType, source, sourcePos, destination, destPos, PortalMinimapOrientation.NORTH); + + public IPortalEntity CreatePortal(PortalType portalType, IMapInstance source, Position sourcePos, IMapInstance destination, Position destPos, PortalMinimapOrientation minimapOrientation) => + new PortalEntity(_eventPipeline, portalType, source, sourcePos, destPos: destPos, destinationMap: destination, minimapOrientation: minimapOrientation); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.CoreImpl/Entities/TimeSpacePortalEntity.cs b/srcs/_plugins/Plugin.CoreImpl/Entities/TimeSpacePortalEntity.cs new file mode 100644 index 0000000..1f8bfe2 --- /dev/null +++ b/srcs/_plugins/Plugin.CoreImpl/Entities/TimeSpacePortalEntity.cs @@ -0,0 +1,69 @@ +using System.Collections.Generic; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Portals; + +namespace Plugin.CoreImpl.Entities +{ + public class TimeSpacePortalEntity : ITimeSpacePortalEntity + { + public TimeSpacePortalEntity(TimeSpaceFileConfiguration timeSpaceFileConfiguration, Position position, long? groupId) + { + TimeSpaceId = timeSpaceFileConfiguration.TsId; + Position = position; + IsHero = timeSpaceFileConfiguration.IsHero; + IsSpecial = timeSpaceFileConfiguration.IsSpecial; + IsHidden = timeSpaceFileConfiguration.IsHidden; + MinLevel = timeSpaceFileConfiguration.MinLevel; + MaxLevel = timeSpaceFileConfiguration.MaxLevel; + SeedsOfPowerRequired = timeSpaceFileConfiguration.SeedsOfPowerRequired; + Name = timeSpaceFileConfiguration.Name; + Description = timeSpaceFileConfiguration.Description; + + DrawRewards = new List<(short, short)>(); + SpecialRewards = new List<(short, short)>(); + BonusRewards = new List<(short, short)>(); + + if (timeSpaceFileConfiguration.Rewards?.Draw != null) + { + foreach (TimeSpaceItemConfiguration draw in timeSpaceFileConfiguration.Rewards.Draw) + { + DrawRewards.Add((draw.ItemVnum, draw.Amount)); + } + } + + if (timeSpaceFileConfiguration.Rewards?.Special != null) + { + foreach (TimeSpaceItemConfiguration special in timeSpaceFileConfiguration.Rewards.Special) + { + SpecialRewards.Add((special.ItemVnum, special.Amount)); + } + } + + if (timeSpaceFileConfiguration.Rewards?.Bonus != null) + { + foreach (TimeSpaceItemConfiguration bonus in timeSpaceFileConfiguration.Rewards.Bonus) + { + BonusRewards.Add((bonus.ItemVnum, bonus.Amount)); + } + } + + GroupId = groupId; + } + + public long TimeSpaceId { get; } + public Position Position { get; } + public bool IsHero { get; } + public bool IsSpecial { get; } + public bool IsHidden { get; } + public byte MinLevel { get; } + public byte MaxLevel { get; } + public byte SeedsOfPowerRequired { get; } + public string Name { get; } + public string Description { get; } + public List<(short, short)> DrawRewards { get; } + public List<(short, short)> SpecialRewards { get; } + public List<(short, short)> BonusRewards { get; } + public long? GroupId { get; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.CoreImpl/Entities/TimeSpacePortalFactory.cs b/srcs/_plugins/Plugin.CoreImpl/Entities/TimeSpacePortalFactory.cs new file mode 100644 index 0000000..8bf6166 --- /dev/null +++ b/srcs/_plugins/Plugin.CoreImpl/Entities/TimeSpacePortalFactory.cs @@ -0,0 +1,15 @@ +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Portals; + +namespace Plugin.CoreImpl.Entities +{ + public class TimeSpacePortalFactory : ITimeSpacePortalFactory + { + public ITimeSpacePortalEntity CreateTimeSpacePortal(TimeSpaceFileConfiguration timeSpaceFileConfiguration, Position position) => + CreateTimeSpacePortal(timeSpaceFileConfiguration, position, null); + + public ITimeSpacePortalEntity CreateTimeSpacePortal(TimeSpaceFileConfiguration timeSpaceFileConfiguration, Position position, long? groupId) => + new TimeSpacePortalEntity(timeSpaceFileConfiguration, position, groupId); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.CoreImpl/Maps/GenericEntityIdManager.cs b/srcs/_plugins/Plugin.CoreImpl/Maps/GenericEntityIdManager.cs new file mode 100644 index 0000000..c591e09 --- /dev/null +++ b/srcs/_plugins/Plugin.CoreImpl/Maps/GenericEntityIdManager.cs @@ -0,0 +1,26 @@ +using System.Threading; +using WingsEmu.Game.Maps; + +namespace Plugin.CoreImpl.Maps +{ + public class GenericEntityIdManager : IEntityIdManager + { + private readonly ReaderWriterLockSlim _lock = new(); + + private int _baseOffset = 25000; + + public int GenerateEntityId() + { + _lock.EnterWriteLock(); + try + { + _baseOffset++; + return _baseOffset; + } + finally + { + _lock.ExitWriteLock(); + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.CoreImpl/Maps/MapInstance.cs b/srcs/_plugins/Plugin.CoreImpl/Maps/MapInstance.cs new file mode 100644 index 0000000..597fe2c --- /dev/null +++ b/srcs/_plugins/Plugin.CoreImpl/Maps/MapInstance.cs @@ -0,0 +1,737 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Generic; +using System.Linq; +using PhoenixLib.Events; +using PhoenixLib.Logging; +using Plugin.CoreImpl.Maps.Systems; +using Plugin.CoreImpl.Pathfinding; +using WingsEmu.DTOs.Maps; +using WingsEmu.DTOs.ServerDatas; +using WingsEmu.Game; +using WingsEmu.Game._ECS; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage.Configuration; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Entities.Extensions; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Miniland; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Portals; +using WingsEmu.Game.Raids; +using WingsEmu.Game.RainbowBattle; +using WingsEmu.Game.Skills; +using WingsEmu.Packets.Enums; + +namespace Plugin.CoreImpl.Maps +{ + public class MapInstance : SessionsContainer, IMapInstance + { + private static readonly TimeSpan _timeToIdleState = TimeSpan.FromSeconds(Convert.ToInt32(Environment.GetEnvironmentVariable("WINGSEMU_TICK_IDLE_DELAY") ?? "10")); + + private readonly BattleSystem _battleSystem; + private readonly CharacterSystem _characterSystem; + private readonly DropSystem _dropSystem; + private readonly IEntityIdManager _entityIdManager; + + private readonly HashSet _flags = new(); + private readonly IGameItemInstanceFactory _gameItemInstanceFactory; + private readonly MateSystem _mateSystem; + private readonly MonsterSystem _monsterSystem; + private readonly NpcSystem _npcSystem; + private readonly IPortalFactory _portalFactory; + + private readonly IRandomGenerator _randomGenerator; + private readonly IMapSystem[] _systems; + + private readonly ITickManager _tickManager; + private List _cells; + + private DateTime? _idleRequest; + private bool _isTickRefreshRequested; + + public MapInstance(Map map, MapInstanceType type, ITickManager tickManager, GameMinMaxConfiguration minMaxConfiguration, + ISpyOutManager spyOutManager, ISkillsManager skillsManager, IGameLanguageService languageService, IMeditationManager meditationManager, IAsyncEventPipeline asyncEventPipeline, + IRandomGenerator randomGenerator, IBCardEffectHandlerContainer bCardEffectHandlerContainer, IBuffFactory buffFactory, + IPortalFactory portalFactory, IGameItemInstanceFactory gameItemInstanceFactory, ISpPartnerConfiguration spPartnerConfiguration, IMonsterTalkingConfig monsterTalkingConfig) + { + Id = Guid.NewGuid(); + + Grid = map.Grid; + Width = map.Width; + Height = map.Height; + MapId = map.MapId; + MapVnum = map.MapVnum; + MapNameId = map.MapNameId; + Music = map.Music; + + foreach (MapFlags flag in map.Flags) + { + _flags.Add(flag); + } + + IsPvp = HasMapFlag(MapFlags.HAS_PVP_ENABLED) || HasMapFlag(MapFlags.HAS_PVP_FACTION_ENABLED) || HasMapFlag(MapFlags.HAS_PVP_FAMILY_ENABLED); + ShopAllowed = !HasMapFlag(MapFlags.HAS_USER_SHOPS_DISABLED) && !HasMapFlag(MapFlags.ACT_4) && !IsPvp && type == MapInstanceType.BaseMapInstance; + + MapInstanceType = type; + + IPathFinder pathfinder = new PathFinder(Grid, Width, Height); + + _tickManager = tickManager; + _randomGenerator = randomGenerator; + _portalFactory = portalFactory; + _gameItemInstanceFactory = gameItemInstanceFactory; + _npcSystem = new NpcSystem(bCardEffectHandlerContainer, this, asyncEventPipeline, randomGenerator, buffFactory, languageService, pathfinder, monsterTalkingConfig); + _monsterSystem = new MonsterSystem(randomGenerator, bCardEffectHandlerContainer, asyncEventPipeline, this, buffFactory, languageService, pathfinder, monsterTalkingConfig); + _characterSystem = new CharacterSystem(bCardEffectHandlerContainer, buffFactory, meditationManager, asyncEventPipeline, this, spyOutManager, randomGenerator, skillsManager, + minMaxConfiguration, languageService); + _mateSystem = new MateSystem(bCardEffectHandlerContainer, languageService, minMaxConfiguration, this, randomGenerator, buffFactory, languageService, spPartnerConfiguration); + _battleSystem = new BattleSystem(asyncEventPipeline, this, bCardEffectHandlerContainer, languageService, buffFactory, randomGenerator); + _dropSystem = new DropSystem(this, randomGenerator); + _entityIdManager = new GenericEntityIdManager(); + + _systems = new IMapSystem[] + { + _monsterSystem, + _characterSystem, + _npcSystem, + _mateSystem, + _dropSystem, + _battleSystem + }; + + SetIdle(); + } + + + public MapInstanceState State { get; private set; } + + public bool IsDance { get; set; } + + public short? MapMusic { get; set; } + + public bool IsPvp { get; set; } + + public byte MapIndexX { get; set; } + + public byte MapIndexY { get; set; } + + public Guid Id { get; } + public MapInstanceType MapInstanceType { get; } + + public IReadOnlyList Grid { get; } + public int Width { get; } + public int Height { get; } + public int MapId { get; } + public int Music { get; } + public int MapVnum { get; } + public int MapNameId { get; } + + public bool ShopAllowed { get; } + + public bool AIDisabled { get; set; } + + public List TimeSpacePortals { get; } = new(); + public List MapDesignObjects { get; set; } = new(); + public List Portals { get; } = new(); + + public override void RegisterSession(IClientSession session) + { + base.RegisterSession(session); + AddCharacter(session.PlayerEntity); + foreach (IMateEntity activeMate in session.PlayerEntity.MateComponent.TeamMembers()) + { + AddMate(activeMate); + } + + ActivateMap(); + } + + public override void UnregisterSession(IClientSession session) + { + base.UnregisterSession(session); + RemoveCharacter(session.PlayerEntity); + foreach (IMateEntity activeMate in session.PlayerEntity.MateComponent.TeamMembers()) + { + RemoveMate(activeMate); + } + + if (State == MapInstanceState.Running && Sessions.Count == 0) + { + RequestIdleState(); + } + } + + public IReadOnlyList GetEntitiesOnMapPackets(bool onlyItemsAndPortals = false) + { + var packetCache = new List(); + + foreach (IPortalEntity portal in Portals) + { + packetCache.Add(portal.GenerateGp()); + } + + if (!onlyItemsAndPortals) + { + bool firstBossSent = false; + foreach (IMonsterEntity monster in GetAliveMonsters()) + { + if (!monster.IsStillAlive) + { + continue; + } + + packetCache.Add(monster.GenerateIn()); + if (monster.IsBoss && monster.IsTarget) + { + packetCache.Add(monster.GenerateRaidBossPacket(firstBossSent)); + firstBossSent = true; + } + + TryAddScal(packetCache, monster); + packetCache.AddRange(monster.GenerateConstBuffEffects()); + } + } + + foreach (MapItem mapItem in Drops) + { + packetCache.Add(mapItem.GenerateIn()); + } + + if (!onlyItemsAndPortals) + { + foreach (INpcEntity npc in GetAliveNpcs()) + { + if (!npc.IsStillAlive) + { + continue; + } + + packetCache.Add(npc.GenerateIn()); + if (npc.ShopNpc != null) + { + packetCache.Add($"shop 2 {npc.Id} 1 {(byte)npc.ShopNpc.MenuType} {npc.ShopNpc.ShopType} {npc.ShopNpc.Name}"); + } + + TryAddScal(packetCache, npc); + packetCache.AddRange(npc.GenerateConstBuffEffects()); + } + + foreach (INpcEntity npc in GetPassiveNpcs()) + { + if (!npc.IsStillAlive) + { + continue; + } + + packetCache.Add(npc.GenerateIn()); + if (npc.ShopNpc != null) + { + packetCache.Add($"shop 2 {npc.Id} 1 {(byte)npc.ShopNpc.MenuType} {npc.ShopNpc.ShopType} {npc.ShopNpc.Name}"); + } + + if (npc.TimeSpaceInfo != null) + { + packetCache.Add(npc.GenerateEffectGround(EffectType.BlueTimeSpace, npc.PositionX, npc.PositionY, false)); + } + + if (npc.RainbowFlag != null) + { + packetCache.Add(npc.GenerateFlagPacket()); + } + + TryAddScal(packetCache, npc); + packetCache.AddRange(npc.GenerateConstBuffEffects()); + } + } + + return packetCache; + } + + public IBattleEntity GetBattleEntity(VisualType type, long id) + { + switch (type) + { + case VisualType.Monster: + return GetMonsterById(id); + case VisualType.Player: + return GetCharacterById(id); + case VisualType.Npc: + if (id >= 1_000_000) + { + return GetMateById(id); + } + + return GetNpcById(id); + default: + return null; + } + } + + public bool HasMapFlag(MapFlags flags) => _flags.Contains(flags); + + public void LoadPortals(IEnumerable tmp = null) + { + if (tmp == null) + { + return; + } + + foreach (PortalDTO portalInfo in tmp) + { + var sourcePos = new Position(portalInfo.SourceX, portalInfo.SourceY); + var destPos = new Position(portalInfo.DestinationX, portalInfo.DestinationY); + IPortalEntity portal = _portalFactory.CreatePortal((PortalType)portalInfo.Type, this, sourcePos, portalInfo.DestinationMapId, destPos, portalInfo.RaidType, portalInfo.MapNameId); + Portals.Add(portal); + } + } + + public void Initialize(DateTime date) + { + State = MapInstanceState.Running; + ProcessTick(date); + State = MapInstanceState.Idle; + } + + public void Destroy() + { + SetIdle(); + + foreach (IClientSession session in Sessions.ToList()) + { + session.ChangeToLastBaseMap(); + } + + foreach (IMapSystem t in _systems) + { + t.Clear(); + } + } + + public Position GetRandomPosition() + { + if (_cells != null) + { + return _cells[_randomGenerator.RandomNumber(_cells.Count - 1)]; + } + + _cells = new List(); + for (short y = 0; y < Height; y++) + { + for (short x = 0; x < Width; x++) + { + if (this.CanWalkAround(x, y)) + { + _cells.Add(new Position(x, y)); + } + } + } + + return _cells[_randomGenerator.RandomNumber(_cells.Count - 1)]; + } + + public MapItem PutItem(ushort amount, ref GameItemInstance inv, IClientSession session) + { + var possibilities = new List(); + + for (short x = -1; x < 2; x++) + { + for (short y = -1; y < 2; y++) + { + possibilities.Add(new Position(x, y)); + } + } + + short mapX = 0; + short mapY = 0; + bool niceSpot = false; + foreach (Position possibility in possibilities.OrderBy(s => _randomGenerator.RandomNumber())) + { + mapX = (short)(session.PlayerEntity.PositionX + possibility.X); + mapY = (short)(session.PlayerEntity.PositionY + possibility.Y); + if (this.IsBlockedZone(mapX, mapY)) + { + continue; + } + + niceSpot = true; + break; + } + + if (!niceSpot) + { + return null; + } + + if (amount <= 0 || amount > inv.Amount) + { + return null; + } + + GameItemInstance newItemInstance = _gameItemInstanceFactory.DuplicateItem(inv); + newItemInstance.Amount = amount; + MapItem droppedItem = new CharacterMapItem(mapX, mapY, newItemInstance, this); + AddDrop(droppedItem); + return droppedItem; + } + + public void DespawnMonster(IMonsterEntity monsterEntity) + { + monsterEntity.GenerateDeath(); + Broadcast(monsterEntity.GenerateOut()); + } + + public IReadOnlyList GetCharactersInRange(Position pos, short distance, Func predicate) => _characterSystem.GetCharactersInRange(pos, distance, predicate); + + public IReadOnlyList GetCharactersInRange(Position position, short distance) => GetCharactersInRange(position, distance, null); + + public IReadOnlyList GetClosestCharactersInRange(Position pos, short distance) => _characterSystem.GetClosestCharactersInRange(pos, distance); + + public IReadOnlyList GetAliveCharactersInRange(Position pos, short distance, Func predicate) => + _characterSystem.GetAliveCharactersInRange(pos, distance, predicate); + + public IReadOnlyList GetAliveCharactersInRange(Position position, short distance) => GetAliveCharactersInRange(position, distance, null); + + public IReadOnlyList GetNonMonsterBattleEntities(Func predicate) + { + var battleEntitiesInRange = new List(); + + battleEntitiesInRange.AddRange(GetAliveCharacters(predicate)); + battleEntitiesInRange.AddRange(GetAliveMates(predicate)); + battleEntitiesInRange.AddRange(GetAliveNpcs(predicate)); + + return battleEntitiesInRange; + } + + public IReadOnlyList GetBattleEntities(Func predicate) + { + var battleEntitiesInRange = new List(); + + battleEntitiesInRange.AddRange(GetAliveCharacters(predicate)); + battleEntitiesInRange.AddRange(GetAliveMates(predicate)); + battleEntitiesInRange.AddRange(GetAliveNpcs(predicate)); + battleEntitiesInRange.AddRange(GetAliveMonsters(predicate)); + + return battleEntitiesInRange; + } + + public IReadOnlyList GetBattleEntitiesInRange(Position pos, short distance) + { + var battleEntitiesInRange = new List(); + + battleEntitiesInRange.AddRange(GetAliveCharactersInRange(pos, distance)); + battleEntitiesInRange.AddRange(GetAliveMatesInRange(pos, distance)); + battleEntitiesInRange.AddRange(GetAliveNpcsInRange(pos, distance)); + battleEntitiesInRange.AddRange(GetAliveMonstersInRange(pos, distance)); + + return battleEntitiesInRange; + } + + public IReadOnlyList GetNonMonsterBattleEntitiesInRange(Position pos, short distance) => GetNonMonsterBattleEntitiesInRange(pos, distance, null); + + public IReadOnlyList GetNonMonsterBattleEntitiesInRange(Position pos, short distance, Func predicate) + { + var battleEntitiesInRange = new List(); + + battleEntitiesInRange.AddRange(GetAliveCharactersInRange(pos, distance, predicate)); + battleEntitiesInRange.AddRange(GetAliveMatesInRange(pos, distance, predicate)); + battleEntitiesInRange.AddRange(GetAliveNpcsInRange(pos, distance, predicate)); + + return battleEntitiesInRange; + } + + + public IReadOnlyList GetNonMonsterBattleEntities() + { + var battleEntitiesInRange = new List(); + + battleEntitiesInRange.AddRange(GetAliveCharacters()); + battleEntitiesInRange.AddRange(GetAliveMates()); + battleEntitiesInRange.AddRange(GetAliveNpcs()); + + return battleEntitiesInRange; + } + + + public IReadOnlyList GetDeadMonsters() => _monsterSystem.GetDeadMonsters(); + + public IReadOnlyList GetAliveMonsters(Func predicate) => _monsterSystem.GetAliveMonsters(predicate); + public IReadOnlyList GetAliveMonstersInRange(Position pos, short distance) => _monsterSystem.GetAliveMonstersInRange(pos, distance); + public IReadOnlyList GetClosestMonstersInRange(Position pos, short distance) => _monsterSystem.GetClosestMonstersInRange(pos, distance); + + public IReadOnlyList GetClosestBattleEntitiesInRange(Position pos, short distance) + { + var battleEntitiesInRange = new List(); + + battleEntitiesInRange.AddRange(GetClosestCharactersInRange(pos, distance)); + battleEntitiesInRange.AddRange(GetClosestMatesInRange(pos, distance)); + battleEntitiesInRange.AddRange(GetClosestNpcsInRange(pos, distance)); + battleEntitiesInRange.AddRange(GetClosestMonstersInRange(pos, distance)); + + return battleEntitiesInRange; + } + + public string Name => $"{nameof(MapInstance)}: {MapInstanceType.ToString()} - {MapId.ToString()}"; + + public void ProcessTick(DateTime date) + { + try + { + if (State == MapInstanceState.Idle) + { + return; + } + + foreach (IMapSystem t in _systems) + { + t.ProcessTick(date, _isTickRefreshRequested); + //_systemTicks.WithLabels(t.Name, Name).Observe(processingTime.ElapsedMilliseconds); + } + + FlushPackets(); + + if (_isTickRefreshRequested) + { + _isTickRefreshRequested = false; + } + + // idle state + if (Sessions.Count > 0) + { + //Log.Warn($"[TICK_MAP_INSTANCE] {Map.MapId} SessionCount > 0"); + return; + } + + _idleRequest ??= date; + if (_idleRequest.HasValue && _idleRequest.Value + _timeToIdleState < date) + { + SetIdle(); + } + } + catch (Exception e) + { + Log.Error($"TICK - MapId: {MapId.ToString()}", e); + } + } + + public IMonsterEntity GetMonsterById(long id) => _monsterSystem.GetMonsterById(id); + public IMonsterEntity GetMonsterByUniqueId(Guid id) => _monsterSystem.GetMonsterByUniqueId(id); + + public IReadOnlyList GetAliveMonsters() => _monsterSystem.GetAliveMonsters(); + + public void AddMonster(IMonsterEntity entity) + { + _monsterSystem.AddMonster(entity); + } + + public void RemoveMonster(IMonsterEntity entity) + { + _monsterSystem.RemoveMonster(entity); + } + + public void AddEntityToTargets(IMonsterEntity monsterEntity, IBattleEntity target) + { + _monsterSystem.AddEntityToTargets(monsterEntity, target); + } + + public void MonsterRefreshTarget(IMonsterEntity target, IBattleEntity caster, in DateTime time, bool isByAttacking = false) + { + _monsterSystem.MonsterRefreshTarget(target, caster, time, isByAttacking); + } + + public void ForgetAll(IMonsterEntity monsterEntity, in DateTime time, bool clearDamagers = true) + { + _monsterSystem.ForgetAll(monsterEntity, time, clearDamagers); + } + + public void RemoveTarget(IMonsterEntity monsterEntity, IBattleEntity entity, bool checkIfPlayer = false) + { + _monsterSystem.RemoveTarget(monsterEntity, entity, checkIfPlayer); + } + + public void ActivateMode(IMonsterEntity monsterEntity) + { + _monsterSystem.ActivateMode(monsterEntity); + } + + public void DeactivateMode(IMonsterEntity monsterEntity) + { + _monsterSystem.DeactivateMode(monsterEntity); + } + + public void IncreaseMonsterDeathsOnMap() + { + _monsterSystem.IncreaseMonsterDeathsOnMap(); + } + + public long MonsterDeathsOnMap() => _monsterSystem.MonsterDeathsOnMap(); + public byte CurrentVessels() => _monsterSystem.CurrentVessels(); + public bool IsSummonLimitReached(int? summonerId, SummonType? summonSummonType) => _monsterSystem.IsSummonLimitReached(summonerId, summonSummonType); + + public IReadOnlyList GetAliveNpcs() => _npcSystem.GetAliveNpcs(); + public IReadOnlyList GetAliveNpcs(Func predicate) => _npcSystem.GetAliveNpcs(predicate); + + public IReadOnlyList GetPassiveNpcs() => _npcSystem.GetPassiveNpcs(); + + public IReadOnlyList GetAliveNpcsInRange(Position pos, short distance) => _npcSystem.GetAliveNpcsInRange(pos, distance); + public IReadOnlyList GetClosestNpcsInRange(Position pos, short distance) => _npcSystem.GetClosestNpcsInRange(pos, distance); + + public void NpcRefreshTarget(INpcEntity npcEntity, IBattleEntity target) + { + _npcSystem.NpcRefreshTarget(npcEntity, target); + } + + public IReadOnlyList GetAliveNpcsInRange(Position pos, short distance, Func predicate) => _npcSystem.GetAliveNpcsInRange(pos, distance, predicate); + + + public INpcEntity GetNpcById(long id) => _npcSystem.GetNpcById(id); + + public void AddNpc(INpcEntity entity) + { + _npcSystem.AddNpc(entity); + } + + public void RemoveNpc(INpcEntity entity) + { + _npcSystem.RemoveNpc(entity); + } + + public IPlayerEntity GetCharacterById(long id) => _characterSystem.GetCharacterById(id); + + public IReadOnlyList GetCharacters() => _characterSystem.GetCharacters(); + + public IReadOnlyList GetCharacters(Func predicate) => _characterSystem.GetCharacters(predicate); + + public IReadOnlyList GetAliveCharacters() => _characterSystem.GetAliveCharacters(); + + public IReadOnlyList GetAliveCharacters(Func predicate) => _characterSystem.GetAliveCharacters(predicate); + + public void AddCharacter(IPlayerEntity character) + { + _characterSystem.AddCharacter(character); + } + + public void RemoveCharacter(IPlayerEntity entity) + { + _characterSystem.RemoveCharacter(entity); + } + + public IReadOnlyList GetAliveMates() => _mateSystem.GetAliveMates(); + public IReadOnlyList GetAliveMates(Func predicate) => _mateSystem.GetAliveMates(predicate); + public IReadOnlyList GetAliveMatesInRange(Position position, short range) => _mateSystem.GetAliveMatesInRange(position, range); + public IReadOnlyList GetClosestMatesInRange(Position position, short range) => _mateSystem.GetClosestMatesInRange(position, range); + + public IReadOnlyList GetAliveMatesInRange(Position position, short range, Func predicate) => _mateSystem.GetAliveMatesInRange(position, range, predicate); + public IMateEntity GetMateById(long mateId) => _mateSystem.GetMateById(mateId); + + public void AddMate(IMateEntity entity) + { + _mateSystem.AddMate(entity); + } + + public void RemoveMate(IMateEntity entity) + { + _mateSystem.RemoveMate(entity); + } + + public IReadOnlyList Drops => _dropSystem.Drops; + + public void AddDrop(MapItem item) + { + _dropSystem.AddDrop(item); + } + + public bool RemoveDrop(long dropId) => _dropSystem.RemoveDrop(dropId); + + public bool HasDrop(long dropId) => _dropSystem.HasDrop(dropId); + + public MapItem GetDrop(long dropId) => _dropSystem.GetDrop(dropId); + + public void AddCastHitRequest(HitProcessable hitProcessable) + { + _battleSystem.AddCastHitRequest(hitProcessable); + } + + public void AddCastBuffRequest(BuffProcessable buffProcessable) + { + _battleSystem.AddCastBuffRequest(buffProcessable); + } + + public void AddHitRequest(HitRequest hitRequest) + { + _battleSystem.AddHitRequest(hitRequest); + } + + public void AddBuffRequest(BuffRequest buffRequest) + { + _battleSystem.AddBuffRequest(buffRequest); + } + + public int GenerateEntityId() => _entityIdManager.GenerateEntityId(); + + private static void TryAddScal(ICollection list, IBattleEntity battleEntity) + { + if (!battleEntity.ShouldSendScal()) + { + return; + } + + list.Add(battleEntity.GenerateScal()); + } + + public void SetInactive() + { + SetIdle(); + } + + private void RequestIdleState() + { + _idleRequest = DateTime.UtcNow; + // Log.Warn($"[TICK_IDLE] Requested by: {Map.MapId.ToString()}"); + } + + private void ActivateMap() + { + if (State == MapInstanceState.Running) + { + _idleRequest = null; + return; + } + + // Log.Warn($"[TICK_ACTIVATE] Activation of map: {Map.MapId.ToString()}"); + State = MapInstanceState.Running; + _isTickRefreshRequested = true; + _tickManager.AddProcessable(this); + } + + private void SetIdle() + { + if (State == MapInstanceState.Idle) + { + return; + } + + // Log.Warn($"[TICK_IDLE] {Map.MapId.ToString()} is now in idle mode"); + State = MapInstanceState.Idle; + _idleRequest = null; + _tickManager.RemoveProcessable(this); + foreach (IMapSystem t in _systems) + { + t.PutIdleState(); + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.CoreImpl/Maps/MapInstanceFactory.cs b/srcs/_plugins/Plugin.CoreImpl/Maps/MapInstanceFactory.cs new file mode 100644 index 0000000..fd8d298 --- /dev/null +++ b/srcs/_plugins/Plugin.CoreImpl/Maps/MapInstanceFactory.cs @@ -0,0 +1,60 @@ +using PhoenixLib.Events; +using WingsEmu.Game; +using WingsEmu.Game._ECS; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage.Configuration; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Portals; +using WingsEmu.Game.Skills; + +namespace Plugin.CoreImpl.Maps +{ + public class MapInstanceFactory : IMapInstanceFactory + { + private readonly IAsyncEventPipeline _asyncEventPipeline; + private readonly IBCardEffectHandlerContainer _bCardEffectHandlerContainer; + private readonly IBuffFactory _buffFactory; + private readonly IGameItemInstanceFactory _gameItemInstanceFactory; + private readonly GameMinMaxConfiguration _gameMinMaxConfiguration; + private readonly IGameLanguageService _languageService; + private readonly IMeditationManager _meditationManager; + private readonly IMonsterTalkingConfig _monsterTalkingConfig; + private readonly IPortalFactory _portalFactory; + private readonly IRandomGenerator _randomGenerator; + private readonly ISkillsManager _skillsManager; + private readonly ISpPartnerConfiguration _spPartnerConfiguration; + private readonly ISpyOutManager _spyOutManager; + private readonly ITickManager _tickManager; + + public MapInstanceFactory(ITickManager tickManager, GameMinMaxConfiguration gameMinMaxConfiguration, + ISpyOutManager spyOutManager, ISkillsManager skillsManager, IGameLanguageService languageService, + IMeditationManager meditationManager, IAsyncEventPipeline asyncEventPipeline, IRandomGenerator randomGenerator, IBCardEffectHandlerContainer bCardEffectHandlerContainer, + IBuffFactory buffFactory, IPortalFactory portalFactory, IGameItemInstanceFactory gameItemInstanceFactory, ISpPartnerConfiguration spPartnerConfiguration, + IMonsterTalkingConfig monsterTalkingConfig) + { + _tickManager = tickManager; + _gameMinMaxConfiguration = gameMinMaxConfiguration; + _spyOutManager = spyOutManager; + _skillsManager = skillsManager; + _languageService = languageService; + _meditationManager = meditationManager; + _asyncEventPipeline = asyncEventPipeline; + _randomGenerator = randomGenerator; + _bCardEffectHandlerContainer = bCardEffectHandlerContainer; + _buffFactory = buffFactory; + _portalFactory = portalFactory; + _gameItemInstanceFactory = gameItemInstanceFactory; + _spPartnerConfiguration = spPartnerConfiguration; + _monsterTalkingConfig = monsterTalkingConfig; + } + + public IMapInstance CreateMap(Map map, MapInstanceType mapInstanceType) => + new MapInstance(map, mapInstanceType, _tickManager, _gameMinMaxConfiguration, _spyOutManager, _skillsManager, _languageService, _meditationManager, + _asyncEventPipeline, _randomGenerator, _bCardEffectHandlerContainer, _buffFactory, _portalFactory, _gameItemInstanceFactory, _spPartnerConfiguration, _monsterTalkingConfig); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.CoreImpl/Maps/Systems/BCardTickSystem.cs b/srcs/_plugins/Plugin.CoreImpl/Maps/Systems/BCardTickSystem.cs new file mode 100644 index 0000000..8f175bc --- /dev/null +++ b/srcs/_plugins/Plugin.CoreImpl/Maps/Systems/BCardTickSystem.cs @@ -0,0 +1,218 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Generic; +using System.Linq; +using WingsEmu.DTOs.BCards; +using WingsEmu.Game; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Buffs.Events; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Extensions.Mates; +using WingsEmu.Game.Mates; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.CoreImpl.Maps.Systems +{ + public sealed class BCardTickSystem + { + private readonly IBCardEffectHandlerContainer _bCardHandlers; + private readonly IBuffFactory _buffFactory; + private readonly IGameLanguageService _gameLanguage; + + private readonly Dictionary<(Guid bCardId, long entityId), DateTime> _lastBCardTick = new(); + private readonly IRandomGenerator _randomGenerator; + private readonly Queue _toAdd; + private readonly Queue _toRemove; + + public BCardTickSystem(IBCardEffectHandlerContainer bCardHandlers, IRandomGenerator randomGenerator, IBuffFactory buffFactory, IGameLanguageService gameLanguage) + { + _bCardHandlers = bCardHandlers; + _randomGenerator = randomGenerator; + _buffFactory = buffFactory; + _gameLanguage = gameLanguage; + _toRemove = new Queue(); + _toAdd = new Queue(); + } + + public void ProcessUpdate(IBattleEntity battleEntity, in DateTime time) + { + ProcessRecurrentBCards(battleEntity, time); + } + + public void Clear() + { + _lastBCardTick.Clear(); + } + + private void ProcessRecurrentBCards(IBattleEntity entity, in DateTime time) + { + if (!entity.BuffComponent.HasAnyBuff()) + { + return; + } + + IReadOnlyList buffs = entity.BuffComponent.GetAllBuffs(); + foreach (Buff buff in buffs) + { + if (ProcessExpiringBuff(buff, entity, time)) + { + continue; + } + + foreach (BCardDTO bCard in buff.BCards) + { + if (!bCard.TickPeriod.HasValue) + { + continue; + } + + (Guid, int) key = (bCard.Id, entity.Id); + + if (!_lastBCardTick.TryGetValue(key, out DateTime dateTime)) + { + _lastBCardTick[key] = time; + } + + if (dateTime.AddSeconds(bCard.TickPeriod.Value) >= time) + { + continue; + } + + _lastBCardTick[key] = time; + + if (bCard.ProcChance != 0 && bCard.ProcChance < _randomGenerator.RandomNumber()) + { + continue; + } + + _bCardHandlers.Execute(entity, buff.Caster, bCard); + } + } + + while (_toRemove.TryDequeue(out Buff buff)) + { + entity.EmitEvent(new BuffRemoveEvent + { + Buffs = new[] { buff }, + Entity = entity, + RemovePermanentBuff = buff.IsSavingOnDisconnect() + }); + } + + while (_toAdd.TryDequeue(out Buff buff)) + { + entity.EmitEvent(new BuffAddEvent(entity, new[] { buff })); + } + } + + private void CheckEverySecBCard(Buff buff, BCardDTO bCard, IBattleEntity entity, in DateTime time) + { + byte firstData = (byte)bCard.FirstDataValue(buff.CasterLevel); + bool isFirst = false; + + (Guid, int) key = (bCard.Id, entity.Id); + + if (!_lastBCardTick.TryGetValue(key, out DateTime dateTime)) + { + _lastBCardTick[key] = time; + isFirst = true; + } + + if (dateTime.AddSeconds(firstData) >= time) + { + return; + } + + _lastBCardTick[key] = time; + + switch (entity) + { + case IPlayerEntity playerEntity: + playerEntity.Session.RefreshStatChar(); + playerEntity.Session.RefreshStat(); + playerEntity.Session.SendCondPacket(); + break; + case IMonsterEntity monsterEntity: + monsterEntity.RefreshStats(); + break; + case IMateEntity mateEntity: + mateEntity.Owner?.Session.SendPetInfo(mateEntity, _gameLanguage); + mateEntity.Owner?.Session.SendCondMate(mateEntity); + break; + } + } + + private bool ProcessExpiringBuff(Buff buff, IBattleEntity battleEntity, in DateTime time) + { + if (buff.SecondBCardsDelay != 0 && !buff.SecondBCardsExecuted) + { + bool execute = buff.Start.AddMilliseconds(buff.SecondBCardsDelay * 100) <= time; + if (execute) + { + var bCards = buff.BCards.Where(x => x.IsSecondBCardExecution != null && x.IsSecondBCardExecution.Value).ToList(); + battleEntity.BCardComponent.AddBuffBCards(buff, bCards); + buff.SecondBCardsExecuted = true; + + foreach (BCardDTO bCard in bCards) + { + _bCardHandlers.Execute(battleEntity, buff.Caster, bCard); + } + + battleEntity.ShadowAppears(false, buff); + switch (battleEntity) + { + case IPlayerEntity character: + { + character.Session.RefreshStat(); + character.Session.RefreshStatChar(); + character.Session.SendCondPacket(); + string buffName = _gameLanguage.GetLanguage(GameDataType.Card, buff.Name, character.Session.UserLanguage); + character.Session.SendChatMessage(character.Session.GetLanguageFormat(GameDialogKey.BUFF_CHATMESSAGE_SIDE_EFFECTS, buffName), ChatMessageColorType.Buff); + break; + } + case IMonsterEntity monsterEntity: + monsterEntity.RefreshStats(); + break; + case IMateEntity mateEntity: + mateEntity.Owner?.Session.SendPetInfo(mateEntity, _gameLanguage); + mateEntity.Owner?.Session.SendCondMate(mateEntity); + break; + } + } + } + + if (buff.Start.AddMilliseconds(buff.Duration.TotalMilliseconds) > time) + { + return false; + } + + if (buff.Duration.TotalMilliseconds < 0 || buff.CardId == (short)BuffVnums.CHARGE) + { + return false; + } + + foreach (BCardDTO bCard in buff.BCards) + { + _lastBCardTick.Remove((bCard.Id, battleEntity.Id)); + } + + if (buff.TimeoutBuff != 0 && _randomGenerator.RandomNumber() < buff.TimeoutBuffChance) + { + _toRemove.Enqueue(buff); + Buff timeoutBuff = _buffFactory.CreateBuff(buff.TimeoutBuff, buff.Caster); + _toAdd.Enqueue(timeoutBuff); + return true; + } + + _toRemove.Enqueue(buff); + return true; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.CoreImpl/Maps/Systems/BattleSystem.cs b/srcs/_plugins/Plugin.CoreImpl/Maps/Systems/BattleSystem.cs new file mode 100644 index 0000000..ebf3478 --- /dev/null +++ b/srcs/_plugins/Plugin.CoreImpl/Maps/Systems/BattleSystem.cs @@ -0,0 +1,737 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using PhoenixLib.Events; +using WingsEmu.DTOs.BCards; +using WingsEmu.DTOs.Skills; +using WingsEmu.Game; +using WingsEmu.Game._ECS; +using WingsEmu.Game._ECS.Systems; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Monster.Event; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Battle; +using WingsEmu.Packets.Enums.Chat; +using WingsEmu.Packets.ServerPackets.Battle; + +namespace Plugin.CoreImpl.Maps.Systems +{ + public class BattleSystem : IMapSystem, IBattleSystem + { + private readonly IAsyncEventPipeline _asyncEventPipeline; + private readonly IBCardEffectHandlerContainer _bCardHandlerContainer; + private readonly IBuffFactory _buffFactory; + private readonly IGameLanguageService _gameLanguage; + private readonly IMapInstance _mapInstance; + private readonly IRandomGenerator _randomGenerator; + private readonly ConcurrentQueue _buffProcessables = new(); + private readonly ConcurrentQueue _buffRequests = new(); + private readonly ConcurrentQueue _hitProcessables = new(); + private readonly ConcurrentQueue _hitRequests = new(); + + public BattleSystem(IAsyncEventPipeline asyncEventPipeline, IMapInstance mapInstance, IBCardEffectHandlerContainer bCardHandlerContainer, IGameLanguageService gameLanguage, + IBuffFactory buffFactory, IRandomGenerator randomGenerator) + { + _asyncEventPipeline = asyncEventPipeline; + _mapInstance = mapInstance; + _bCardHandlerContainer = bCardHandlerContainer; + _gameLanguage = gameLanguage; + _buffFactory = buffFactory; + _randomGenerator = randomGenerator; + } + + public void AddCastHitRequest(HitProcessable hitProcessable) + { + _hitProcessables.Enqueue(hitProcessable); + } + + public void AddCastBuffRequest(BuffProcessable buffProcessable) + { + _buffProcessables.Enqueue(buffProcessable); + } + + public void AddHitRequest(HitRequest hitRequest) + { + _hitRequests.Enqueue(hitRequest); + } + + public void AddBuffRequest(BuffRequest buffRequest) + { + _buffRequests.Enqueue(buffRequest); + } + + public void ProcessTick(DateTime date, bool isTickRefresh = false) + { + ProcessAttackSkillCast(date); + ProcessBuffSkillCast(date); + ProcessAttack(); + ProcessBuff(); + } + + public string Name => nameof(BattleSystem); + + public void PutIdleState() + { + Clear(); + } + + public void Clear() + { + _hitProcessables.Clear(); + _hitRequests.Clear(); + _buffProcessables.Clear(); + _buffRequests.Clear(); + } + + private bool IsInterrupted(in IBattleEntity entity) + { + if (entity is not IPlayerEntity character) + { + return false; + } + + if (!character.SkillComponent.IsSkillInterrupted) + { + return false; + } + + int focus = character.HitRate; + focus += character.BCardComponent.GetAllBCardsInformation(BCardType.Target, (byte)AdditionalTypes.Target.MagicalConcentrationIncreased, character.Level).firstData; + focus -= character.BCardComponent.GetAllBCardsInformation(BCardType.Target, (byte)AdditionalTypes.Target.MagicalConcentrationDecreased, character.Level).firstData; + double chance = 100 / Math.PI * Math.Atan(0.015 * (focus - 130) + 2) + 58; + if (_randomGenerator.RandomNumber() < chance) + { + return false; + } + + character.SkillComponent.IsSkillInterrupted = false; + character.SkillComponent.CanBeInterrupted = false; + character.CancelCastingSkill(); + character.Session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.SKILL_CHATMESSAGE_WAS_INTERRUPTED, character.Session.UserLanguage), ChatMessageColorType.Red); + return true; + } + + private void ProcessAttackSkillCast(in DateTime date) + { + var requestsInPending = new List(); + while (_hitProcessables.TryDequeue(out HitProcessable hitProcessable)) + { + DateTime time = hitProcessable.SkillCast.SkillEndCastTime; + IBattleEntity caster = hitProcessable.Caster; + + if (time > date) + { + requestsInPending.Add(hitProcessable); + continue; + } + + if (IsInterrupted(caster)) + { + continue; + } + + IBattleEntity target = hitProcessable.Target; + SkillInfo skillInfo = hitProcessable.SkillCast.Skill; + Position position = hitProcessable.Position; + + _asyncEventPipeline.ProcessEventAsync(new ProcessHitEvent(caster, target, skillInfo, position)); + } + + foreach (HitProcessable request in requestsInPending) + { + AddCastHitRequest(request); + } + } + + private void ProcessBuffSkillCast(in DateTime date) + { + var requestsInPending = new List(); + while (_buffProcessables.TryDequeue(out BuffProcessable buffProcessable)) + { + DateTime time = buffProcessable.SkillCast.SkillEndCastTime; + IBattleEntity caster = buffProcessable.Caster; + + if (time > date) + { + requestsInPending.Add(buffProcessable); + continue; + } + + if (IsInterrupted(caster)) + { + continue; + } + + IBattleEntity target = buffProcessable.Target; + SkillCast skillCast = buffProcessable.SkillCast; + Position position = buffProcessable.Position; + + _asyncEventPipeline.ProcessEventAsync(new ProcessBuffEvent(caster, target, skillCast, position)); + } + + foreach (BuffProcessable request in requestsInPending) + { + AddCastBuffRequest(request); + } + } + + private void ProcessBuff() + { + while (_buffRequests.TryDequeue(out BuffRequest request)) + { + SkillCast skillCast = request.SkillCast; + SkillInfo skill = skillCast.Skill; + IBattleEntity caster = request.Caster; + + caster.SetSkillCooldown(skill); + request.Caster.RemoveCastingSkill(); + + if (!caster.IsAlive()) + { + CancelHitRequest(caster, skill, request.Position, false, true); + return; + } + + switch (skill.AttackType) + { + case AttackType.Dash: + caster.ChangePosition(request.Position); + break; + case AttackType.Charge: + { + caster.BroadcastSuPacket(caster, skill, 0, SuPacketHitMode.NoDamageFail); + if (caster is not IPlayerEntity character) + { + continue; + } + + character.BCardComponent.ClearChargeBCard(); + bool addCharge = false; + foreach (BCardDTO bCard in skill.BCards.Where(x => x.Type == (short)BCardType.AttackPower)) + { + addCharge = true; + character.BCardComponent.AddChargeBCard(bCard); + } + + if (!addCharge && !character.HasBuff(BuffVnums.CHARGE)) + { + continue; + } + + Buff charge = _buffFactory.CreateBuff((short)BuffVnums.CHARGE, character); + character.AddBuffAsync(charge).ConfigureAwait(false).GetAwaiter().GetResult(); + continue; + } + } + + switch (skill.TargetAffectedEntities) + { + case TargetAffectedEntities.DebuffForEnemies: + DebuffEnemies(request); + break; + case TargetAffectedEntities.BuffForAllies: + BuffAllies(request); + break; + } + + if (caster is not IMonsterEntity monster) + { + continue; + } + + if (!monster.DisappearAfterHitting) + { + continue; + } + + _asyncEventPipeline.ProcessEventAsync(new MonsterDeathEvent(monster)).ConfigureAwait(false).GetAwaiter().GetResult(); + } + } + + private void BuffAllies(BuffRequest request) + { + SkillInfo skill = request.SkillCast.Skill; + IBattleEntity caster = request.Caster; + IEnumerable entities = request.Targets; + Position position = request.Position; + + BCardDTO[] afterAttackAllAllies = skill.BCardsType.TryGetValue(SkillCastType.AFTER_ATTACK_ALL_ALLIES, out HashSet allAllies) ? allAllies.ToArray() : Array.Empty(); + BCardDTO[] afterAttackAllTargets = skill.BCardsType.TryGetValue(SkillCastType.AFTER_ATTACK_ALL_TARGETS, out HashSet allTargets) ? allTargets.ToArray() : Array.Empty(); + + foreach (BCardDTO bCard in afterAttackAllAllies) + { + _bCardHandlerContainer.Execute(caster, caster, bCard, skill, position); + } + + if (skill.TargetType == TargetType.NonTarget) + { + caster.BroadcastSuPacket(caster, skill, 0, SuPacketHitMode.NoDamageSuccess); + switch (skill.HitType) + { + case TargetHitType.AlliesInAffectedAoE: + foreach (IBattleEntity entity in entities) + { + if (entity.IsInvisibleGm()) + { + continue; + } + + if (entity.Id != caster.Id) + { + caster.BroadcastSuPacket(entity, skill, 0, SuPacketHitMode.AttackedInAoe); + foreach (BCardDTO bCard in afterAttackAllAllies) + { + _bCardHandlerContainer.Execute(entity, caster, bCard, skill, position); + } + } + + foreach (BCardDTO bCard in afterAttackAllTargets) + { + _bCardHandlerContainer.Execute(entity, caster, bCard, skill, position); + } + } + + break; + case TargetHitType.TargetOnly: + foreach (BCardDTO bCard in afterAttackAllTargets) + { + _bCardHandlerContainer.Execute(caster, caster, bCard, skill, position); + } + + break; + } + } + else + { + switch (skill.HitType) + { + case TargetHitType.AlliesInAffectedAoE: + caster.BroadcastSuPacket(caster, skill, 0, SuPacketHitMode.NoDamageSuccess); + foreach (IBattleEntity entity in entities) + { + if (entity.IsInvisibleGm()) + { + continue; + } + + if (entity.Id != caster.Id) + { + caster.BroadcastSuPacket(entity, skill, 0, SuPacketHitMode.AttackedInAoe); + foreach (BCardDTO bCard in afterAttackAllAllies) + { + _bCardHandlerContainer.Execute(entity, caster, bCard, skill, position); + } + } + + foreach (BCardDTO bCard in afterAttackAllTargets) + { + _bCardHandlerContainer.Execute(entity, caster, bCard, skill, position); + } + } + + break; + case TargetHitType.PlayerAndHisMates: + if (caster.IsInvisibleGm()) + { + return; + } + + switch (caster) + { + case IPlayerEntity character: + { + caster.BroadcastSuPacket(caster, skill, 0, SuPacketHitMode.NoDamageSuccess); + foreach (IBattleEntity entityMate in entities) + { + caster.BroadcastSuPacket(entityMate, skill, 0, SuPacketHitMode.AttackedInAoe); + foreach (BCardDTO bCard in afterAttackAllAllies) + { + _bCardHandlerContainer.Execute(entityMate, caster, bCard, skill, position); + } + + foreach (BCardDTO bCard in afterAttackAllTargets) + { + _bCardHandlerContainer.Execute(entityMate, caster, bCard, skill, position); + } + } + + caster.BroadcastSuPacket(character, skill, 0, SuPacketHitMode.AttackedInAoe); + foreach (BCardDTO bCard in afterAttackAllTargets) + { + _bCardHandlerContainer.Execute(character, caster, bCard, skill, position); + } + + break; + } + case IMateEntity mate: + { + IPlayerEntity owner = mate.Owner; + IBattleEntity secondMate = owner.MateComponent.GetMate(m => mate.Position.IsInAoeZone(m.Position, skill.AoERange) && m.MateType != mate.MateType && m.IsTeamMember); + + mate.BroadcastSuPacket(mate, skill, 0, SuPacketHitMode.AttackedInAoe); + if (secondMate != null) + { + foreach (BCardDTO bCard in afterAttackAllAllies) + { + _bCardHandlerContainer.Execute(secondMate, caster, bCard, skill, position); + } + + foreach (BCardDTO bCard in afterAttackAllTargets) + { + _bCardHandlerContainer.Execute(secondMate, caster, bCard, skill, position); + } + } + + foreach (BCardDTO bCard in afterAttackAllAllies) + { + _bCardHandlerContainer.Execute(owner, caster, bCard, skill, position); + } + + foreach (BCardDTO bCard in afterAttackAllTargets) + { + _bCardHandlerContainer.Execute(owner, caster, bCard, skill, position); + } + + foreach (BCardDTO bCard in afterAttackAllTargets) + { + _bCardHandlerContainer.Execute(mate, caster, bCard, skill, position); + } + + break; + } + } + + break; + case TargetHitType.TargetOnly: + IBattleEntity target = entities.FirstOrDefault(); + if (target == null) + { + return; + } + + if (!target.IsAlive()) + { + CancelHitRequest(caster, skill, request.Position); + return; + } + + if (target.IsInvisibleGm()) + { + return; + } + + caster.BroadcastSuPacket(target, skill, 0, SuPacketHitMode.NoDamageSuccess); + + if (caster.Id != target.Id) + { + foreach (BCardDTO bCard in afterAttackAllAllies) + { + _bCardHandlerContainer.Execute(target, caster, bCard, skill, position); + } + } + + foreach (BCardDTO bCard in afterAttackAllTargets) + { + _bCardHandlerContainer.Execute(target, caster, bCard, skill, position); + } + + break; + } + } + } + + private void DebuffEnemies(BuffRequest request) + { + SkillInfo skill = request.SkillCast.Skill; + Position position = request.Position; + IEnumerable entities = request.Targets.ToList(); + IBattleEntity target = request.Target; + IBattleEntity caster = request.Caster; + + BCardDTO[] afterAttackAllAllies = skill.BCardsType.TryGetValue(SkillCastType.AFTER_ATTACK_ALL_ALLIES, out HashSet allAllies) ? allAllies.ToArray() : Array.Empty(); + BCardDTO[] afterAttackAllTargets = skill.BCardsType.TryGetValue(SkillCastType.AFTER_ATTACK_ALL_TARGETS, out HashSet allTargets) ? allTargets.ToArray() : Array.Empty(); + + if (target == null) + { + if (skill.TargetType != TargetType.NonTarget && skill.HitType != TargetHitType.TargetOnly) + { + CancelHitRequest(caster, skill, request.Position, false, true); + return; + } + + target = caster; + } + + if (!target.IsAlive()) + { + CancelHitRequest(caster, skill, request.Position); + return; + } + + if (!skill.BCards.Any(x => x.Type == (short)BCardType.Capture && x.SubType == (byte)AdditionalTypes.Capture.CaptureAnimal)) + { + caster.BroadcastSuPacket(target, skill, 0, SuPacketHitMode.NoDamageFail); + } + + if (skill.Vnum == (short)SkillsVnums.TAUNT) + { + caster.MapInstance.Broadcast(caster.GenerateEffectPacket(EffectType.Taunt)); + caster.MapInstance.Broadcast(target.GenerateEffectPacket(EffectType.Taunt)); + (caster as IPlayerEntity)?.Session.SendSound(SoundType.TAUNT_SKILL); + (target as IPlayerEntity)?.Session.SendSound(SoundType.TAUNT_SKILL); + } + + foreach (BCardDTO bCard in afterAttackAllAllies) + { + _bCardHandlerContainer.Execute(caster, caster, bCard, skill, position); + } + + if (skill.TargetType == TargetType.NonTarget) + { + switch (skill.HitType) + { + case TargetHitType.EnemiesInAffectedAoE: + foreach (IBattleEntity entity in entities) + { + if (entity.IsInvisibleGm()) + { + continue; + } + + caster.BroadcastSuPacket(entity, skill, 0, SuPacketHitMode.AttackedInAoe); + foreach (BCardDTO bCard in afterAttackAllAllies) + { + _bCardHandlerContainer.Execute(entity, caster, bCard, skill, position); + } + + foreach (BCardDTO bCard in afterAttackAllTargets) + { + _bCardHandlerContainer.Execute(entity, caster, bCard, skill, position); + } + } + + break; + case TargetHitType.TargetOnly: + if (target.IsInvisibleGm()) + { + return; + } + + caster.BroadcastSuPacket(target, skill, 0, SuPacketHitMode.NoDamageSuccess); + + foreach (BCardDTO bCard in afterAttackAllTargets) + { + _bCardHandlerContainer.Execute(target, caster, bCard, skill, position); + } + + break; + } + } + else + { + switch (skill.HitType) + { + case TargetHitType.EnemiesInAffectedAoE: + foreach (IBattleEntity entity in entities) + { + if (entity.IsInvisibleGm()) + { + continue; + } + + if (entity.Id != target.Id) + { + caster.BroadcastSuPacket(entity, skill, 0, SuPacketHitMode.AttackedInAoe); + } + else + { + caster.BroadcastSuPacket(target, skill, 0, SuPacketHitMode.NoDamageSuccess); + } + + foreach (BCardDTO bCard in afterAttackAllTargets) + { + _bCardHandlerContainer.Execute(entity, caster, bCard, skill, position); + } + } + + break; + case TargetHitType.TargetOnly: + if (target.IsInvisibleGm()) + { + return; + } + + if (!skill.BCards.Any(x => x.Type == (short)BCardType.Capture && x.SubType == (byte)AdditionalTypes.Capture.CaptureAnimal)) + { + caster.BroadcastSuPacket(target, skill, 0, SuPacketHitMode.NoDamageSuccess); + } + + foreach (BCardDTO bCard in afterAttackAllTargets) + { + _bCardHandlerContainer.Execute(target, caster, bCard, skill, position); + } + + break; + } + } + } + + private void ProcessAttack() + { + while (_hitRequests.TryDequeue(out HitRequest request)) + { + IBattleEntity caster = request.EHitInformation.Caster; + IBattleEntity mainTarget = request.MainTarget; + SkillInfo skill = request.EHitInformation.Skill; + if (caster == null) + { + continue; + } + + if (request?.EHitInformation == null) + { + continue; + } + + if (IsBombSkill(caster, skill, _bCardHandlerContainer)) + { + continue; + } + + if (skill.AttackType == AttackType.Dash && request.EHitInformation.IsFirst) + { + caster.ChangePosition(request.EHitInformation.Position); + } + + if (!request.Targets.Any()) + { + CancelHitRequest(caster, skill, request.EHitInformation.Position, true, true); + continue; + } + + caster.SetSkillCooldown(skill); + caster.RemoveCastingSkill(); + + bool cancelByMiss = false; + + int increaseDamageByCharge = 0; + if (caster.ChargeComponent.GetCharge() != 0) + { + increaseDamageByCharge += caster.ChargeComponent.GetCharge(); + caster.ChargeComponent.ResetCharge(); + } + + foreach ((IBattleEntity target, DamageAlgorithmResult result) in request.Targets) + { + if (request.EHitInformation.Caster.MapInstance?.Id != target.MapInstance?.Id) + { + continue; + } + + if (cancelByMiss) + { + break; + } + + if (target.IsSameEntity(mainTarget) && skill.TargetType != TargetType.NonTarget) + { + if (!mainTarget.IsAlive()) + { + CancelHitRequest(caster, skill, request.EHitInformation.Position); + break; + } + + if (result.HitType == HitType.Miss && skill.HitType == TargetHitType.SpecialArea && request.Targets.Count() > 1) + { + cancelByMiss = true; + } + } + + result.Damages += increaseDamageByCharge; + + _asyncEventPipeline.ProcessEventAsync(new ApplyHitEvent(target, result, request.EHitInformation)).ConfigureAwait(false).GetAwaiter().GetResult(); + } + + if (!caster.IsMonster()) + { + continue; + } + + var monster = (IMonsterEntity)caster; + if (!monster.DisappearAfterHitting) + { + continue; + } + + _asyncEventPipeline.ProcessEventAsync(new MonsterDeathEvent(monster)).ConfigureAwait(false).GetAwaiter().GetResult(); + } + } + + private bool IsBombSkill(IBattleEntity caster, SkillInfo skill, IBCardEffectHandlerContainer bCardEffectHandlerContainer) + { + if (caster is not IPlayerEntity character) + { + return false; + } + + if (skill.Vnum != (short)SkillsVnums.BOMB) + { + return false; + } + + if (character.SkillComponent.BombEntityId.HasValue) + { + return false; + } + + character.BroadcastSuPacket(caster, skill, 0, SuPacketHitMode.NoDamageSuccess); + character.SetSkillCooldown(skill); + character.RemoveCastingSkill(); + foreach (BCardDTO x in skill.BCards) + { + bCardEffectHandlerContainer.Execute(character, character, x, skill); + } + + return true; + } + + private void CancelHitRequest(IBattleEntity caster, SkillInfo skill, Position position, bool setCooldown = false, bool isNoTargets = false) + { + if (skill.TargetType == TargetType.NonTarget) + { + caster.BroadcastNonTargetSkill(position, skill); + } + else + { + caster.BroadcastSuPacket(caster, skill, 0, isNoTargets ? SuPacketHitMode.NoDamageFail : SuPacketHitMode.OutOfRange, isFirst: true); + } + + if (setCooldown) + { + caster.SetSkillCooldown(skill); + caster.CancelCastingSkill(); + } + + switch (caster) + { + case IMonsterEntity monster: + if (monster.DisappearAfterHitting) + { + _asyncEventPipeline.ProcessEventAsync(new MonsterDeathEvent(monster)).ConfigureAwait(false).GetAwaiter().GetResult(); + } + + break; + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.CoreImpl/Maps/Systems/CharacterSystem.cs b/srcs/_plugins/Plugin.CoreImpl/Maps/Systems/CharacterSystem.cs new file mode 100644 index 0000000..4da5b73 --- /dev/null +++ b/srcs/_plugins/Plugin.CoreImpl/Maps/Systems/CharacterSystem.cs @@ -0,0 +1,908 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using PhoenixLib.Events; +using WingsAPI.Data.Account; +using WingsAPI.Game.Extensions.Quicklist; +using WingsAPI.Packets.Enums.Rainbow; +using WingsEmu.Core.Extensions; +using WingsEmu.Game; +using WingsEmu.Game._ECS; +using WingsEmu.Game._ECS.Systems; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Groups.Events; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Networking.Broadcasting; +using WingsEmu.Game.Raids; +using WingsEmu.Game.Revival; +using WingsEmu.Game.Skills; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Character; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.CoreImpl.Maps.Systems +{ + public class CharacterSystem : ICharacterSystem, IMapSystem + { + private static readonly TimeSpan ProcessSpInterval = TimeSpan.FromSeconds(5); + private static readonly TimeSpan RefreshRate = TimeSpan.FromMilliseconds(500); + private readonly IAsyncEventPipeline _asyncEventPipeline; + private readonly BCardTickSystem _bCardTickSystem; + private readonly IBuffFactory _buffFactory; + private readonly List _characters = new(); + private readonly ConcurrentDictionary _charactersById = new(); + private readonly IGameLanguageService _gameLanguage; + + private readonly ReaderWriterLockSlim _lock = new(LockRecursionPolicy.SupportsRecursion); + private readonly IMapInstance _mapInstance; + private readonly IMeditationManager _meditationManager; + private readonly SkillCooldownSystem _skillCooldownSystem; + private readonly ISkillsManager _skillsManager; + private readonly SnackFoodSystem _snackFoodSystem; + private readonly ISpyOutManager _spyOutManager; + private readonly ConcurrentQueue _toAddPlayers = new(); + private readonly ConcurrentQueue _toRemovePlayers = new(); + + private DateTime _lastProcess; + + public CharacterSystem(IBCardEffectHandlerContainer bcardHandlers, IBuffFactory buffFactory, IMeditationManager meditationManager, IAsyncEventPipeline asyncEventPipeline, + IMapInstance mapInstance, ISpyOutManager spyOutManager, IRandomGenerator randomGenerator, ISkillsManager skillsManager, + GameMinMaxConfiguration gameMinMaxConfiguration, IGameLanguageService gameLanguage) + { + _buffFactory = buffFactory; + _meditationManager = meditationManager; + _asyncEventPipeline = asyncEventPipeline; + _mapInstance = mapInstance; + _spyOutManager = spyOutManager; + _skillsManager = skillsManager; + _gameLanguage = gameLanguage; + _snackFoodSystem = new SnackFoodSystem(gameMinMaxConfiguration); + _bCardTickSystem = new BCardTickSystem(bcardHandlers, randomGenerator, _buffFactory, _gameLanguage); + _lastProcess = DateTime.MinValue; + _skillCooldownSystem = new SkillCooldownSystem(); + } + + public IPlayerEntity GetCharacterById(long id) => _charactersById.GetOrDefault(id); + + public IReadOnlyList GetCharacters() + { + _lock.EnterReadLock(); + try + { + return _characters.ToArray(); + } + finally + { + _lock.ExitReadLock(); + } + } + + public IReadOnlyList GetCharacters(Func predicate) + { + _lock.EnterReadLock(); + try + { + return _characters.FindAll(s => s != null && (predicate == null || predicate(s))); + } + finally + { + _lock.ExitReadLock(); + } + } + + + public IReadOnlyList GetCharactersInRange(Position position, short range, Func predicate) + { + _lock.EnterReadLock(); + try + { + return _characters.FindAll(s => s != null && position.IsInAoeZone(s.Position, range) && (predicate == null || predicate(s))); + } + finally + { + _lock.ExitReadLock(); + } + } + + public IReadOnlyList GetCharactersInRange(Position pos, short distance) => GetCharactersInRange(pos, distance, null); + + public IReadOnlyList GetClosestCharactersInRange(Position pos, short distance) + { + _lock.EnterReadLock(); + try + { + List toReturn = _characters.FindAll(s => s != null && s.IsAlive() && pos.IsInAoeZone(s.Position, distance)); + toReturn.Sort((prev, next) => prev.Position.GetDistance(pos) - next.Position.GetDistance(pos)); + + return toReturn; + } + finally + { + _lock.ExitReadLock(); + } + } + + public IReadOnlyList GetAliveCharacters() => GetCharacters(null); + + public IReadOnlyList GetAliveCharacters(Func predicate) + { + _lock.EnterReadLock(); + try + { + return _characters.FindAll(s => s != null && s.IsAlive() && (predicate == null || predicate(s))); + } + finally + { + _lock.ExitReadLock(); + } + } + + + public IReadOnlyList GetAliveCharactersInRange(Position position, short range, Func predicate) + { + _lock.EnterReadLock(); + try + { + return _characters.FindAll(s => s != null && s.IsAlive() && position.IsInAoeZone(s.Position, range) && (predicate == null || predicate(s))); + } + finally + { + _lock.ExitReadLock(); + } + } + + public IReadOnlyList GetAliveCharactersInRange(Position pos, short distance) => GetCharactersInRange(pos, distance, null); + + public void AddCharacter(IPlayerEntity character) + { + _toAddPlayers.Enqueue(character); + } + + public void RemoveCharacter(IPlayerEntity entity) + { + _toRemovePlayers.Enqueue(entity); + } + + public string Name => nameof(CharacterSystem); + + public void ProcessTick(DateTime date, bool isTickRefresh = false) + { + if (_lastProcess + RefreshRate > date) + { + return; + } + + _lastProcess = date; + Update(date); + } + + public void PutIdleState() + { + _bCardTickSystem.Clear(); + } + + public void Clear() + { + _lock.EnterWriteLock(); + try + { + _charactersById.Clear(); + _characters.Clear(); + _toAddPlayers.Clear(); + _toRemovePlayers.Clear(); + } + finally + { + _lock.ExitWriteLock(); + } + } + + private void Update(DateTime date) + { + _lock.EnterWriteLock(); + try + { + while (_toRemovePlayers.TryDequeue(out IPlayerEntity player)) + { + RemovePrivateCharacter(player); + } + + while (_toAddPlayers.TryDequeue(out IPlayerEntity player)) + { + AddPrivateCharacter(player); + } + } + finally + { + _lock.ExitWriteLock(); + } + + foreach (IPlayerEntity character in _characters) + { + Update(date, character); + } + } + + private void ProcessDayAndNight(in DateTime date, in IPlayerEntity character, in IReadOnlyList packets) + { + if (character.LastDayNight.AddSeconds(50) >= date) + { + return; + } + + character.LastDayNight = date; + character.Session.SendPackets(packets); + } + + private void Update(in DateTime date, in IPlayerEntity character) + { + ProcessRevivalEvents(date, character); + RemoveManaOnDeath(date, character); + HealthHeal(date, character); + ProcessSpecialist(date, character); + ProcessArchmageTeleport(date, character); + _bCardTickSystem.ProcessUpdate(character, date); + _skillCooldownSystem.Update(character, date); + _snackFoodSystem.ProcessUpdate(character, date); + ProcessSkillReset(date, character); + ProcessSpSkillReset(date, character); + ProcessWeedingBuffCheck(character, date); + ProcessMinigameEffect(date, character); + ProcessRandomTeleport(date, character); + ProcessMuteMessage(date, character); + ProcessArenaImmunity(date, character); + ProcessBlockedZone(character); + + RefreshRaidStat(date, character); + ProcessCharacterEffects(date, character); + ProcessBubble(date, character); + ProcessItemsToRemove(date, character); + ProcessExpiredStaticBonus(date, character); + + ProcessMeditation(date, character); + ProcessComboSkills(date, character); + ProcessSpyOut(date, character); + + if (!character.UseSp || character.Specialist == null) + { + return; + } + + ProcessSpPointsRemoving(date, character); + } + + private void ProcessBlockedZone(in IPlayerEntity character) + { + if (character.Session.IsGameMaster()) + { + return; + } + + if (character.MapInstance == null) + { + return; + } + + if (!character.MapInstance.IsBlockedZone(character.Position.X, character.Position.Y)) + { + return; + } + + Position getRandomPosition = character.MapInstance.GetRandomPosition(); + character.ChangePosition(getRandomPosition); + character.Session.SendCondPacket(); + character.Session.BroadcastTeleportPacket(); + } + + private void ProcessArenaImmunity(in DateTime date, in IPlayerEntity character) + { + if (character.MapInstance?.MapInstanceType != MapInstanceType.ArenaInstance) + { + return; + } + + if (!character.ArenaImmunity.HasValue) + { + return; + } + + if (character.ArenaImmunity.Value.AddSeconds(5) > date) + { + return; + } + + character.ArenaImmunity = null; + character.Session.SendChatMessage(character.Session.GetLanguage(GameDialogKey.ARENA_CHATMESSAGE_PVP_ACTIVE), ChatMessageColorType.Yellow); + } + + private void ProcessRandomTeleport(in DateTime date, in IPlayerEntity character) + { + if (!character.RandomMapTeleport.HasValue) + { + return; + } + + if (character.RandomMapTeleport.Value.AddSeconds(1.5) > date) + { + return; + } + + Position randomPosition = character.MapInstance.GetRandomPosition(); + character.TeleportOnMap(randomPosition.X, randomPosition.Y); + character.RandomMapTeleport = null; + character.BroadcastEffectGround(EffectType.VehicleTeleportation, character.PositionX, character.PositionY, false); + } + + private void ProcessMuteMessage(in DateTime date, in IPlayerEntity character) + { + if (!character.MuteRemainingTime.HasValue) + { + return; + } + + if (character.GameStartDate.AddSeconds(1) > date) + { + return; + } + + character.MuteRemainingTime -= date - character.LastMuteTick; + character.LastMuteTick = date; + if (character.MuteRemainingTime.Value.TotalMilliseconds <= 0) + { + character.MuteRemainingTime = null; + character.LastChatMuteMessage = null; + + AccountPenaltyDto penalty = character.Session.Account.Logs.FirstOrDefault(x => x.RemainingTime.HasValue && x.PenaltyType == PenaltyType.Muted); + if (penalty == null) + { + return; + } + + penalty.RemainingTime = null; + + return; + } + + character.LastChatMuteMessage ??= DateTime.MinValue; + if (character.LastChatMuteMessage.Value.AddMinutes(1) > date) + { + return; + } + + character.LastChatMuteMessage = date; + string timeLeft = character.MuteRemainingTime.Value.ToString(@"hh\:mm\:ss"); + character.Session.SendChatMessage(character.Session.GetLanguageFormat(GameDialogKey.MUTE_CHATMESSAGE_TIME_LEFT, timeLeft), ChatMessageColorType.Green); + } + + private void ProcessExpiredStaticBonus(in DateTime date, in IPlayerEntity character) + { + if (character.Bonus == null) + { + return; + } + + if (!character.Bonus.Any()) + { + return; + } + + if (character.BonusesToRemove.AddMinutes(1) > date) + { + return; + } + + character.BonusesToRemove = date; + character.Session.EmitEvent(new CharacterBonusExpiredEvent()); + } + + private void ProcessWeedingBuffCheck(in IPlayerEntity character, in DateTime date) + { + if (!character.IsInGroup()) + { + return; + } + + if (character.CheckWeedingBuff == null) + { + return; + } + + if (character.CheckWeedingBuff.Value.AddSeconds(2) > date) + { + return; + } + + character.CheckWeedingBuff = null; + character.Session.EmitEvent(new GroupWeedingEvent()); + } + + private void ProcessSpSkillReset(in DateTime date, in IPlayerEntity character) + { + if (!character.SkillComponent.ResetSpSkillCooldowns.HasValue) + { + return; + } + + if (character.SkillComponent.ResetSpSkillCooldowns.Value.AddMilliseconds(500) > date) + { + return; + } + + if (!character.UseSp || character.Specialist == null) + { + return; + } + + character.Session.LearnSpSkill(_skillsManager, _gameLanguage); + character.SkillComponent.ResetSpSkillCooldowns = null; + } + + private void ProcessSkillReset(in DateTime date, in IPlayerEntity character) + { + if (!character.SkillComponent.ResetSkillCooldowns.HasValue) + { + return; + } + + if (character.SkillComponent.ResetSkillCooldowns.Value.AddMilliseconds(500) > date) + { + return; + } + + character.Session.LearnAdventurerSkill(_skillsManager, _gameLanguage); + character.SkillComponent.ResetSkillCooldowns = null; + } + + private void ProcessArchmageTeleport(in DateTime date, in IPlayerEntity character) + { + if (character.SkillComponent.SendTeleportPacket == null) + { + return; + } + + if (character.SkillComponent.SendTeleportPacket.Value.AddMilliseconds(500) > date) + { + return; + } + + SkillInfo fakeTeleport = character.GetFakeTeleportSkill(); + character.SkillComponent.SendTeleportPacket = null; + character.Session.SendSkillCooldownResetAfter(fakeTeleport.CastId, (short)character.ApplyCooldownReduction(fakeTeleport)); + } + + private void ProcessSpecialist(in DateTime date, in IPlayerEntity character) + { + if (!character.SpCooldownEnd.HasValue) + { + return; + } + + if (character.SpCooldownEnd.Value > date) + { + return; + } + + character.SpCooldownEnd = null; + character.Session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.SPECIALIST_CHATMESSAGE_TRANSFORM_DISAPPEAR, character.Session.UserLanguage), ChatMessageColorType.Yellow); + character.Session.ResetSpCooldownUi(); + } + + private void ProcessItemsToRemove(in DateTime date, in IPlayerEntity character) + { + if (character.ItemsToRemove.AddSeconds(10) > date) + { + return; + } + + character.ItemsToRemove = date; + character.Session.EmitEvent(new InventoryExpiredItemsEvent()); + } + + private void ProcessSpyOut(DateTime time, IPlayerEntity character) + { + if (!_spyOutManager.ContainsSpyOut(character.Id)) + { + return; + } + + if (character.SpyOutStart.AddMinutes(2) > time) + { + return; + } + + character.Session.SendObArPacket(); + _spyOutManager.RemoveSpyOutSkill(character.Id); + } + + private static void ProcessRevivalEvents(in DateTime date, IPlayerEntity character) + { + if (character.RevivalDateTimeForExecution <= date) + { + character.DisableRevival(); + character.Session.EmitEvent(new RevivalReviveEvent(character.RevivalType, character.ForcedType)); + return; + } + + if (character.IsAlive()) + { + return; + } + + if (character.AskRevivalDateTimeForExecution > date) + { + return; + } + + character.DisableAskRevival(); + character.Session.EmitEvent(new RevivalAskEvent(character.AskRevivalType)); + } + + private void ProcessBubble(in DateTime date, IPlayerEntity character) + { + if (!character.IsUsingBubble()) + { + return; + } + + if (character.Bubble.AddMinutes(30) > date) + { + return; + } + + character.RemoveBubble(); + } + + private void RemoveManaOnDeath(in DateTime date, IPlayerEntity character) + { + if (character.LastHealth.AddSeconds(1) > date) + { + return; + } + + if (character.Hp != 0) + { + return; + } + + character.Mp = 0; + character.Session.RefreshStat(); + character.LastHealth = date; + } + + private void HealthHeal(in DateTime date, IPlayerEntity character) + { + if (!character.IsAlive()) + { + return; + } + + if (character.LastHealth.AddSeconds(2) > date && (!character.IsSitting || character.LastHealth.AddSeconds(1.5) > date)) + { + return; + } + + character.LastHealth = date; + if (character.LastDefence.AddSeconds(4) > date || character.LastSkillUse.AddSeconds(2) > date) + { + return; + } + + character.Hp += character.Hp + character.HealthHpLoad() < character.MaxHp ? character.HealthHpLoad() : character.MaxHp - character.Hp; + character.Mp += character.Mp + character.HealthMpLoad() < character.MaxMp ? character.HealthMpLoad() : character.MaxMp - character.Mp; + character.Session.RefreshStat(); + } + + private void ProcessMinigameEffect(in DateTime date, IPlayerEntity character) + { + if (character.LastEffectMinigame.AddSeconds(3) > date) + { + return; + } + + if (character.CurrentMinigame == 0) + { + return; + } + + character.Session.BroadcastEffectInRange(character.CurrentMinigame); + character.LastEffectMinigame = date; + } + + private void RefreshRaidStat(in DateTime date, IPlayerEntity character) + { + if (!character.IsInRaidParty) + { + return; + } + + if (character.Session.CurrentMapInstance?.MapInstanceType != MapInstanceType.RaidInstance) + { + return; + } + + character.Session.SendRaidPacket(RaidPacketType.REFRESH_MEMBERS_HP_MP); + } + + private void ProcessCharacterEffects(in DateTime date, IPlayerEntity character) + { + if (character.IsInvisible()) + { + return; + } + + if (character.RainbowBattleComponent.IsInRainbowBattle) + { + if ((date - character.LastRainbowEffects).TotalSeconds >= 1) + { + character.LastRainbowEffects = date; + RainbowBattleTeamType team = character.RainbowBattleComponent.Team; + EffectType effectType = team switch + { + RainbowBattleTeamType.Red => EffectType.RedTeam, + RainbowBattleTeamType.Blue => EffectType.BlueTeam + }; + + character.Session.BroadcastEffect(effectType); + + if (character.RainbowBattleComponent.IsFrozen) + { + character.Session.BroadcastEffect(EffectType.Frozen); + } + } + } + + if (character.LastEffect.AddSeconds(5) > date) + { + return; + } + + character.LastEffect = date; + + if (character.HasBuff(BuffVnums.WEDDING)) + { + character.Session.BroadcastEffect(EffectType.MediumHearth); + } + + if (character.IsInRaidParty) + { + if (character.IsRaidLeader(character.Id)) + { + character.Session.BroadcastEffect(EffectType.OtherRaidLeader, new ExceptRaidBroadcast(character.Raid.Id)); + character.Session.BroadcastEffect(EffectType.OwnRaidLeader, new InRaidBroadcast(character.Raid)); + } + else + { + if (character.MapInstance.MapInstanceType == MapInstanceType.RaidInstance) + { + return; + } + + character.Session.BroadcastEffect(EffectType.OtherRaidMember, new ExceptRaidBroadcast(character.Raid.Id)); + character.Session.BroadcastEffect(EffectType.OwnRaidMember, new InRaidBroadcast(character.Raid)); + } + } + + if (character.Specialist is { ItemVNum: (short)ItemVnums.JAJAMARU_SP } && character.UseSp) + { + if (character.MateComponent.GetTeamMember(x => x.MateType == MateType.Partner && x.MonsterVNum == (short)MonsterVnum.SAKURA) != null) + { + character.BroadcastEffectInRange(EffectType.Heart); + } + } + + GameItemInstance amulet = character.Amulet; + if (amulet == null) + { + return; + } + + if (character.Invisible || character.CheatComponent.IsInvisible) + { + return; + } + + if (amulet.GameItem.EffectValue == -1) + { + return; + } + + if (amulet.ItemVNum is (int)ItemVnums.DRACO_AMULET or (int)ItemVnums.GLACERUS_AMULET) + { + character.Session.BroadcastEffectInRange(amulet.GameItem.EffectValue + (character.Class == ClassType.Adventurer ? 0 : (byte)character.Class - 1)); + } + else + { + character.Session.BroadcastEffectInRange(amulet.GameItem.EffectValue); + } + } + + private void ProcessMeditation(in DateTime date, IPlayerEntity character) + { + if (!_meditationManager.HasMeditation(character)) + { + return; + } + + // Get all the meditations from the character + List<(short, DateTime)> meditations = _meditationManager.GetAllMeditations(character); + foreach ((short meditationId, DateTime meditationStart) in meditations.ToList()) + { + // If that meditation is not ready to start, go next + if (meditationStart >= date) + { + continue; + } + + character.Session.BroadcastEffectInRange(meditationId == (short)BuffVnums.SPIRIT_OF_STRENGTH + ? EffectType.MeditationFinalStage + : EffectType.MeditationFirstStage); + + // Removes one buff or another depending of the current meditation state + Buff firstBuff, secondBuff; + switch (meditationId) + { + case (short)BuffVnums.SPIRIT_OF_ENLIGHTENMENT: + firstBuff = character.BuffComponent.GetBuff((short)BuffVnums.SPIRIT_OF_TEMPERANCE); + secondBuff = character.BuffComponent.GetBuff((short)BuffVnums.SPIRIT_OF_STRENGTH); + character.Session.SendSound(SoundType.MEDITATION_FIRST); + break; + case (short)BuffVnums.SPIRIT_OF_TEMPERANCE: + firstBuff = character.BuffComponent.GetBuff((short)BuffVnums.SPIRIT_OF_ENLIGHTENMENT); + secondBuff = character.BuffComponent.GetBuff((short)BuffVnums.SPIRIT_OF_STRENGTH); + character.Session.SendSound(SoundType.MEDITATION_SECOND); + break; + case (short)BuffVnums.SPIRIT_OF_STRENGTH: + firstBuff = character.BuffComponent.GetBuff((short)BuffVnums.SPIRIT_OF_ENLIGHTENMENT); + secondBuff = character.BuffComponent.GetBuff((short)BuffVnums.SPIRIT_OF_TEMPERANCE); + character.Session.SendSound(SoundType.MEDITATION_THIRD); + break; + default: + firstBuff = null; + secondBuff = null; + break; + } + + character.RemoveBuffAsync(false, firstBuff).ConfigureAwait(false).GetAwaiter().GetResult(); + character.RemoveBuffAsync(false, secondBuff).ConfigureAwait(false).GetAwaiter().GetResult(); + + Buff actualBuff = _buffFactory.CreateBuff(meditationId, character); + character.AddBuffAsync(actualBuff).GetAwaiter().GetResult(); + _meditationManager.RemoveMeditation(character, meditationId); + } + } + + private void ProcessComboSkills(in DateTime date, IPlayerEntity character) + { + ComboSkillState comboSkillState = character.GetComboState(); + if (comboSkillState == null) + { + return; + } + + if (character.AngelElement.HasValue) + { + return; + } + + if (!character.LastSkillCombo.HasValue) + { + return; + } + + if (character.LastSkillCombo.Value.AddSeconds(5) > date) + { + return; + } + + character.LastSkillCombo = null; + character.Session.SendMsCPacket(0); + character.Session.RefreshQuicklist(); + character.CleanComboState(); + } + + private void ProcessSpPointsRemoving(in DateTime date, IPlayerEntity character) + { + if (character.HasBuff(BuffVnums.RAINBOW_ENERGY)) + { + return; + } + + if (!character.IsAlive()) + { + return; + } + + if (character.Skills.All(s => s.LastUse <= DateTime.UtcNow)) + { + return; + } + + if (character.LastSkillUse.AddSeconds(15) <= date) + { + if (!character.IsRemovingSpecialistPoints) + { + return; + } + + character.IsRemovingSpecialistPoints = false; + character.Session.SendScpPacket(0); + character.Session.RefreshSpPoint(); + character.InitialScpPacketSent = false; + return; + } + + if (character.LastSpRemovingProcess.AddSeconds(1) <= date) + { + character.LastSpRemovingProcess = date; + character.IsRemovingSpecialistPoints = true; + RemoveSpecialistPoints(character); + } + + if (character.LastSpPacketSent + ProcessSpInterval > date) + { + return; + } + + character.Session.RefreshSpPoint(); + character.LastSpPacketSent = date; + } + + private void RemoveSpecialistPoints(IPlayerEntity character) + { + if (!character.InitialScpPacketSent) + { + character.Session.SendScpPacket(1); + character.InitialScpPacketSent = true; + } + + byte spPoints = character.Specialist.GameItem.SpPointsUsage; + + if (character.SpPointsBasic == 0 && character.SpPointsBonus == 0) + { + if (character.IsOnVehicle) + { + character.Session.EmitEvent(new RemoveVehicleEvent(true)); + } + + character.Session.EmitEvent(new SpUntransformEvent()); + character.Session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.SPECIALIST_SHOUTMESSAGE_NO_POINTS, character.Session.UserLanguage), MsgMessageType.Middle); + return; + } + + character.SpPointsBasic = character.SpPointsBasic - spPoints < 0 ? 0 : character.SpPointsBasic - spPoints; + } + + private void AddPrivateCharacter(IPlayerEntity character) + { + if (_characters.Contains(character)) + { + return; + } + + _charactersById.TryAdd(character.Id, character); + _characters.Add(character); + } + + private void RemovePrivateCharacter(IPlayerEntity character) + { + _charactersById.TryRemove(character.Id, out _); + _characters.Remove(character); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.CoreImpl/Maps/Systems/DropSystem.cs b/srcs/_plugins/Plugin.CoreImpl/Maps/Systems/DropSystem.cs new file mode 100644 index 0000000..f979150 --- /dev/null +++ b/srcs/_plugins/Plugin.CoreImpl/Maps/Systems/DropSystem.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using WingsEmu.Core.Extensions; +using WingsEmu.Game; +using WingsEmu.Game._ECS; +using WingsEmu.Game._ECS.Systems; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Maps; + +namespace Plugin.CoreImpl.Maps.Systems +{ + public sealed class DropSystem : IMapSystem, IDropSystem + { + private const int MAX_DROPS = 200; + private const int MESSAGE_PERCENTAGE_CHANCE = 10; + private readonly ConcurrentDictionary _drops; + private readonly IMapInstance _mapInstance; + private readonly IRandomGenerator _randomGenerator; + + public DropSystem(IMapInstance mapInstance, IRandomGenerator randomGenerator) + { + _mapInstance = mapInstance; + _randomGenerator = randomGenerator; + _drops = new ConcurrentDictionary(); + } + + + public IReadOnlyList Drops => _drops.Values.ToArray(); + + public void AddDrop(MapItem item) + { + if (!_drops.TryAdd(item.TransportId, item)) + { + return; + } + + if (_drops.Count <= MAX_DROPS) + { + return; + } + + MapItem first = _drops.Values.OrderBy(x => x.CreatedDate).FirstOrDefault(); + if (first == null) + { + return; + } + + first.BroadcastOut(); + RemoveDrop(first.TransportId); + } + + public bool RemoveDrop(long dropId) => _drops.TryRemove(dropId, out MapItem item); + public bool HasDrop(long dropId) => _drops.ContainsKey(dropId); + public MapItem GetDrop(long dropId) => _drops.GetOrDefault(dropId); + + public string Name => nameof(DropSystem); + + public void ProcessTick(DateTime date, bool isTickRefresh = false) + { + if (_mapInstance.MapInstanceType == MapInstanceType.Miniland) + { + return; + } + + foreach (KeyValuePair drop in _drops) + { + Update(date, drop.Value); + } + } + + public void PutIdleState() + { + } + + public void Clear() + { + _drops.Clear(); + } + + private void Update(DateTime date, MapItem mapItem) + { + if (mapItem.CreatedDate == null) + { + return; + } + + DateTime createdDate = mapItem.CreatedDate.Value; + if (createdDate.AddSeconds(150) > date) + { + return; + } + + if (createdDate.AddSeconds(180) > date) + { + if (mapItem.ShowMessageEasterEgg.AddSeconds(3) > date) + { + return; + } + + mapItem.ShowMessageEasterEgg = DateTime.UtcNow; + int randomNumber = _randomGenerator.RandomNumber(10000); + int chance = _randomGenerator.RandomNumber(MESSAGE_PERCENTAGE_CHANCE); + + if (chance > randomNumber) + { + mapItem.BroadcastSayDrop(); + } + + return; + } + + mapItem.BroadcastOut(); + RemoveDrop(mapItem.TransportId); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.CoreImpl/Maps/Systems/MateSystem.cs b/srcs/_plugins/Plugin.CoreImpl/Maps/Systems/MateSystem.cs new file mode 100644 index 0000000..c417b81 --- /dev/null +++ b/srcs/_plugins/Plugin.CoreImpl/Maps/Systems/MateSystem.cs @@ -0,0 +1,425 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; +using WingsAPI.Game.Extensions.Groups; +using WingsEmu.Core.Extensions; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game; +using WingsEmu.Game._ECS; +using WingsEmu.Game._ECS.Systems; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage.Configuration; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Extensions.Mates; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Mates.Events; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.CoreImpl.Maps.Systems +{ + public sealed class MateSystem : IMapSystem, IMateSystem + { + private const short LOYALTY_RECOVERED_PER_TICK = 100; + private const short LOW_LOYALTY_EFFECT_IN_COMBAT = 100; + private const short LOW_LOYALTY_EFFECT_FOR_CONTROL = 0; + private const short LOW_LOYALTY_EFFECT_COST = 1; + private const short LOW_LOYALTY_EFFECT_SECONDS_INTERVAL = 10; + private const short LEVEL_WITHOUT_LOYALTY_PENALIZATION = 20; + private static readonly TimeSpan RefreshRate = TimeSpan.FromMilliseconds(500); + private readonly BCardTickSystem _bCardTickSystem; + private readonly IBuffFactory _buffFactory; + private readonly IGameLanguageService _gameLanguage; + + private readonly IGameLanguageService _languageService; + + private readonly ReaderWriterLockSlim _lock = new(LockRecursionPolicy.SupportsRecursion); + private readonly IMapInstance _mapInstance; + private readonly List _mates = new(); + private readonly ConcurrentDictionary _matesById = new(); + private readonly GameMinMaxConfiguration _minMaxConfiguration; + private readonly IRandomGenerator _randomGenerator; + private readonly ISpPartnerConfiguration _spPartnerConfiguration; + private readonly ConcurrentQueue _toAddMates = new(); + private readonly ConcurrentQueue _toRemoveMates = new(); + private DateTime _lastProcess = DateTime.MinValue; + + public MateSystem(IBCardEffectHandlerContainer bCardHandlers, IGameLanguageService languageService, + GameMinMaxConfiguration minMaxConfiguration, IMapInstance mapInstance, IRandomGenerator randomGenerator, + IBuffFactory buffFactory, IGameLanguageService gameLanguage, ISpPartnerConfiguration spPartnerConfiguration) + { + _languageService = languageService; + _minMaxConfiguration = minMaxConfiguration; + _mapInstance = mapInstance; + _randomGenerator = randomGenerator; + _buffFactory = buffFactory; + _gameLanguage = gameLanguage; + _spPartnerConfiguration = spPartnerConfiguration; + _bCardTickSystem = new BCardTickSystem(bCardHandlers, _randomGenerator, _buffFactory, _gameLanguage); + } + + public void PutIdleState() + { + _bCardTickSystem.Clear(); + } + + public void Clear() + { + _lock.EnterWriteLock(); + try + { + _mates.Clear(); + _toAddMates.Clear(); + _toRemoveMates.Clear(); + _matesById.Clear(); + } + finally + { + _lock.ExitWriteLock(); + } + } + + public string Name => nameof(MateSystem); + + public void ProcessTick(DateTime date, bool isTickRefresh = false) + { + if (_lastProcess + RefreshRate > date) + { + return; + } + + _lastProcess = date; + + _lock.EnterWriteLock(); + try + { + while (_toRemoveMates.TryDequeue(out IMateEntity toRemove)) + { + _matesById.TryRemove(toRemove.Id, out _); + _mates.Remove(toRemove); + } + + while (_toAddMates.TryDequeue(out IMateEntity toAdd)) + { + if (_matesById.ContainsKey(toAdd.Id)) + { + continue; + } + + _matesById[toAdd.Id] = toAdd; + _mates.Add(toAdd); + } + } + finally + { + _lock.ExitWriteLock(); + } + + foreach (IMateEntity mate in _mates) + { + Update(date, mate); + } + } + + public IReadOnlyList GetAliveMates() => GetAliveMates(null); + + public IReadOnlyList GetAliveMates(Func predicate) + { + _lock.EnterReadLock(); + try + { + return _mates.FindAll(s => s != null && s.IsAlive() && (predicate == null || predicate(s))); + } + finally + { + _lock.ExitReadLock(); + } + } + + public IReadOnlyList GetAliveMatesInRange(Position position, short range) => GetAliveMatesInRange(position, range, null); + + public IReadOnlyList GetClosestMatesInRange(Position position, short range) + { + _lock.EnterReadLock(); + try + { + List toReturn = _mates.FindAll(s => s != null && s.IsAlive() && position.IsInAoeZone(s.Position, range)); + toReturn.Sort((prev, next) => prev.Position.GetDistance(position) - next.Position.GetDistance(position)); + + return toReturn; + } + finally + { + _lock.ExitReadLock(); + } + } + + + public IReadOnlyList GetAliveMatesInRange(Position position, short range, Func predicate) + { + _lock.EnterReadLock(); + try + { + return _mates.FindAll(s => s != null && s.IsAlive() && position.IsInAoeZone(s.Position, (short)(range + s.CellSize)) && (predicate == null || predicate(s))); + } + finally + { + _lock.ExitReadLock(); + } + } + + public IMateEntity GetMateById(long mateId) => _matesById.GetOrDefault(mateId); + + public void AddMate(IMateEntity entity) + { + _toAddMates.Enqueue(entity); + } + + public void RemoveMate(IMateEntity entity) + { + _toRemoveMates.Enqueue(entity); + } + + private void Update(DateTime date, IMateEntity entity) + { + ProcessSpawnByGuardian(date, entity); + if (!entity.IsAlive()) + { + ProcessRevivalEvents(date, entity); + return; + } + + if (entity.Owner?.Session == null) + { + return; + } + + TryToHeal(date, entity); + TryRecoveringLoyalty(date, entity); + BroadcastMateEffect(date, entity); + BroadcastMateUpgradeEffect(date, entity); + ProcessSpecialistCooldown(date, entity); + _bCardTickSystem.ProcessUpdate(entity, date); + + TryShowLowLoyaltyEffect(date, entity); + } + + private void BroadcastMateUpgradeEffect(in DateTime date, in IMateEntity entity) + { + if (!entity.IsAlive()) + { + return; + } + + if (entity.Defence < 10 && entity.Attack < 10) + { + return; + } + + if (entity.LastPetUpgradeEffect > date) + { + return; + } + + entity.LastPetUpgradeEffect = date.AddSeconds(10); + + if (entity.Defence == 10 && entity.Attack == 10) + { + entity.BroadcastEffectInRange(EffectType.MateAttackDefenceUpgrade); + return; + } + + if (entity.Attack == 10) + { + entity.BroadcastEffectInRange(EffectType.MateAttackUpgrade); + return; + } + + if (entity.Defence != 10) + { + return; + } + + entity.BroadcastEffectInRange(EffectType.MateDefenceUpgrade); + } + + private void ProcessSpawnByGuardian(in DateTime date, IMateEntity entity) + { + if (!entity.SpawnMateByGuardian.HasValue) + { + return; + } + + if (entity.SpawnMateByGuardian.Value.AddSeconds(1.5) > date) + { + return; + } + + IClientSession session = entity.Owner.Session; + + if (session.CurrentMapInstance == null) + { + return; + } + + session.CurrentMapInstance.Broadcast(x => entity.GenerateOut()); + session.CurrentMapInstance.Broadcast(x => entity.GenerateOut()); + + entity.SpawnMateByGuardian = null; + entity.ChangePosition(entity.Owner.Position); + entity.Hp = entity.MaxHp; + entity.Mp = entity.MaxMp; + session.PlayerEntity.MapInstance.Broadcast(x => + { + bool isAnonymous = session.CurrentMapInstance.HasMapFlag(MapFlags.ACT_4) && x.PlayerEntity.Faction != session.PlayerEntity.Faction; + string inPacket = entity.GenerateIn(_gameLanguage, x.UserLanguage, _spPartnerConfiguration, isAnonymous); + return inPacket; + }); + + session.SendCondMate(entity); + session.RefreshParty(_spPartnerConfiguration); + session.SendPetInfo(entity, _gameLanguage); + session.SendMateLife(entity); + } + + private void BroadcastMateEffect(in DateTime date, IMateEntity entity) + { + if (!entity.CanPickUp) + { + return; + } + + if (entity.LastEffect.AddSeconds(5) > date) + { + return; + } + + if (entity.MapInstance == null) + { + return; + } + + entity.LastEffect = date; + entity.BroadcastEffectInRange(EffectType.PetPickupEnabled); + } + + private void ProcessSpecialistCooldown(in DateTime date, IMateEntity entity) + { + if (entity.MateType == MateType.Pet) + { + return; + } + + if (!entity.SpCooldownEnd.HasValue) + { + return; + } + + if (entity.SpCooldownEnd.Value > date) + { + return; + } + + entity.SpCooldownEnd = null; + + if (entity.Owner?.Session == null) + { + return; + } + + IClientSession session = entity.Owner.Session; + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.PARTNER_CHATMESSAGE_TRANSFORM_DISAPPEAR, session.UserLanguage), ChatMessageColorType.Yellow); + session.SendMateSpCooldown(entity, 0); + } + + private static void ProcessRevivalEvents(in DateTime date, IMateEntity entity) + { + if (entity.Owner != null && entity.MapInstance.Id == entity.Owner.Miniland.Id && entity.Owner.MapInstance.MapInstanceType == MapInstanceType.Miniland) + { + entity.DisableRevival(); + entity.Owner.Session.EmitEvent(new MateReviveEvent(entity, true)); + entity.Hp = 1; + entity.Mp = 1; + return; + } + + if (entity.RevivalDateTimeForExecution > date) + { + return; + } + + entity.DisableRevival(); + entity.Owner.Session.EmitEvent(new MateReviveEvent(entity, false)); + } + + private static void TryToHeal(in DateTime date, IMateEntity entity) + { + if (entity.LastHealth.AddSeconds(entity.IsSitting ? 1.5 : 2) > date || entity.IsInCombat(date)) + { + return; + } + + entity.LastHealth = date; + + entity.Hp += entity.Hp + entity.HealthHpLoad() < entity.MaxHp ? entity.HealthHpLoad() : entity.MaxHp - entity.Hp; + entity.Mp += entity.Mp + entity.HealthMpLoad() < entity.MaxMp ? entity.HealthMpLoad() : entity.MaxMp - entity.Mp; + + if (entity.Owner.GameStartDate.AddSeconds(5) <= DateTime.UtcNow) + { + if (entity.Hp > entity.MaxHp) + { + entity.Hp = entity.MaxHp; + } + + if (entity.Mp > entity.MaxMp) + { + entity.Mp = entity.MaxMp; + } + } + + entity.Owner.Session.SendMateLife(entity); + } + + private void TryShowLowLoyaltyEffect(in DateTime date, IMateEntity entity) + { + if (entity.LastLowLoyaltyEffect > date || entity.Loyalty > LOW_LOYALTY_EFFECT_IN_COMBAT) + { + return; + } + + if (!entity.IsInCombat(date) && entity.Loyalty > LOW_LOYALTY_EFFECT_FOR_CONTROL && entity.Owner.Session.PlayerEntity.Level <= LEVEL_WITHOUT_LOYALTY_PENALIZATION) + { + return; + } + + entity.Owner.Session.SendMateEffect(entity, EffectType.PetLoveBroke); + entity.RemoveLoyalty(LOW_LOYALTY_EFFECT_COST, _minMaxConfiguration, _languageService); + entity.LastLowLoyaltyEffect = date.AddSeconds(LOW_LOYALTY_EFFECT_SECONDS_INTERVAL); + } + + private void TryRecoveringLoyalty(in DateTime date, IMateEntity entity) + { + if (entity.MapInstance?.Id != entity.Owner.Miniland?.Id) + { + return; + } + + if (entity.LastLoyaltyRecover.AddSeconds(5) > DateTime.UtcNow) + { + return; + } + + entity.LastLoyaltyRecover = date; + entity.AddLoyalty(LOYALTY_RECOVERED_PER_TICK, _minMaxConfiguration, _languageService); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.CoreImpl/Maps/Systems/MonsterQuestSystemException.cs b/srcs/_plugins/Plugin.CoreImpl/Maps/Systems/MonsterQuestSystemException.cs new file mode 100644 index 0000000..9104483 --- /dev/null +++ b/srcs/_plugins/Plugin.CoreImpl/Maps/Systems/MonsterQuestSystemException.cs @@ -0,0 +1,11 @@ +using System; + +namespace Plugin.CoreImpl.Maps.Systems +{ + public class MonsterQuestSystemException : Exception + { + public MonsterQuestSystemException(string message) : base(message) + { + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.CoreImpl/Maps/Systems/MonsterSystem.cs b/srcs/_plugins/Plugin.CoreImpl/Maps/Systems/MonsterSystem.cs new file mode 100644 index 0000000..3013f87 --- /dev/null +++ b/srcs/_plugins/Plugin.CoreImpl/Maps/Systems/MonsterSystem.cs @@ -0,0 +1,2189 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading; +using PhoenixLib.Events; +using PhoenixLib.Logging; +using Plugin.CoreImpl.Pathfinding; +using WingsAPI.Game.Extensions.Quests; +using WingsEmu.Core.Extensions; +using WingsEmu.DTOs.BCards; +using WingsEmu.DTOs.Maps; +using WingsEmu.DTOs.Skills; +using WingsEmu.Game; +using WingsEmu.Game._ECS; +using WingsEmu.Game._ECS.Systems; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Entities.Extensions; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Helpers; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Monster; +using WingsEmu.Game.Monster.Event; +using WingsEmu.Game.Networking.Broadcasting; +using WingsEmu.Game.Skills; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Battle; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.CoreImpl.Maps.Systems +{ + public class MonsterSystem : IMapSystem, IMonsterSystem + { + private const int TICK_DELAY_MILLISECONDS = 50; + private const int AGGRO_LIMIT_PER_ENTITY = 5; + private const int RETURN_TIME_OUT = 5; + + private const int SUMMONED_MONSTERS_LIMIT_PER_ENTITY = 40; + private const int SUMMONED_MONSTERS_LIMIT_MONSTER_WAVES = 120; + + private static readonly TimeSpan _refreshRate = TimeSpan.FromMilliseconds(TICK_DELAY_MILLISECONDS); + + private readonly Dictionary> _attackerByRace = new(); + private readonly IBCardEffectHandlerContainer _bcardHandlers; + + private readonly BCardTickSystem _bCardTickSystem; + private readonly IAsyncEventPipeline _eventPipeline; + + private readonly ReaderWriterLockSlim _lock = new(LockRecursionPolicy.SupportsRecursion); + private readonly IMapInstance _mapInstance; + private readonly List _monsters = new(); + private readonly ConcurrentDictionary _monstersById = new(); + private readonly ConcurrentDictionary _monstersByUniqueId = new(); + + private readonly IMonsterTalkingConfig _monsterTalkingConfig; + private readonly IPathFinder _pathFinder; + private readonly IRandomGenerator _randomGenerator; + private readonly ConcurrentDictionary _summonedByMonsterId = new(); + private readonly ConcurrentQueue _toAddMonsters = new(); + private readonly ConcurrentQueue _toRemoveMonsters = new(); + private byte _currentVessels; + private DateTime _lastProcess = DateTime.MinValue; + private int _summonMonsterWaveCounter; + private long _totalMonstersDeaths; + + public MonsterSystem(IRandomGenerator randomGenerator, IBCardEffectHandlerContainer bcardHandlers, IAsyncEventPipeline eventPipeline, IMapInstance mapInstance, IBuffFactory buffFactory, + IGameLanguageService gameLanguage, IPathFinder pathFinder, IMonsterTalkingConfig monsterTalkingConfig) + { + _randomGenerator = randomGenerator; + _bcardHandlers = bcardHandlers; + _eventPipeline = eventPipeline; + _mapInstance = mapInstance; + _bCardTickSystem = new BCardTickSystem(bcardHandlers, _randomGenerator, buffFactory, gameLanguage); + _pathFinder = pathFinder; + _monsterTalkingConfig = monsterTalkingConfig; + _totalMonstersDeaths = 0; + } + + + public void PutIdleState() + { + _bCardTickSystem.Clear(); + _attackerByRace.Clear(); + } + + public void Clear() + { + _lock.EnterWriteLock(); + try + { + _monsters.Clear(); + _monstersById.Clear(); + _attackerByRace.Clear(); + _toAddMonsters.Clear(); + _toRemoveMonsters.Clear(); + _monstersByUniqueId.Clear(); + } + finally + { + _lock.ExitWriteLock(); + } + } + + public string Name => nameof(MonsterSystem); + + public void ProcessTick(DateTime date, bool isTickRefresh = false) + { + if (_lastProcess + _refreshRate > date) + { + return; + } + + _lastProcess = date; + + _lock.EnterWriteLock(); + try + { + while (_toRemoveMonsters.TryDequeue(out IMonsterEntity toRemove)) + { + if (toRemove.Target != null) + { + RemoveTarget(toRemove, toRemove); + } + + _monstersById.TryRemove(toRemove.Id, out _); + _monstersByUniqueId.TryRemove(toRemove.UniqueId, out _); + _monsters.Remove(toRemove); + + if (toRemove.VesselMonster) + { + _currentVessels--; + } + + switch (toRemove.SummonType) + { + case SummonType.MONSTER_WAVE: + _summonMonsterWaveCounter--; + break; + } + + if (!toRemove.SummonerId.HasValue || toRemove.SummonerType is not VisualType.Monster) + { + continue; + } + + if (_summonedByMonsterId.TryGetValue(toRemove.SummonerId.Value, out int currentMonsters)) + { + _summonedByMonsterId.TryUpdate(toRemove.SummonerId.Value, currentMonsters - 1, currentMonsters); + } + } + + while (_toAddMonsters.TryDequeue(out IMonsterEntity entity)) + { + if (entity.VesselMonster) + { + _currentVessels++; + } + + _monstersById[entity.Id] = entity; + _monsters.Add(entity); + _monstersByUniqueId.TryAdd(entity.UniqueId, entity); + + switch (entity.SummonType) + { + case SummonType.MONSTER_WAVE: + _summonMonsterWaveCounter++; + break; + } + + if (!entity.SummonerId.HasValue || entity.SummonerType is not VisualType.Monster) + { + continue; + } + + if (!_summonedByMonsterId.TryGetValue(entity.SummonerId.Value, out int currentMonsters)) + { + currentMonsters = 1; + _summonedByMonsterId.TryAdd(entity.SummonerId.Value, currentMonsters); + } + else + { + _summonedByMonsterId.TryUpdate(entity.SummonerId.Value, currentMonsters + 1, currentMonsters); + } + } + } + finally + { + _lock.ExitWriteLock(); + } + + foreach (IMonsterEntity monster in _monsters) + { + Update(date, monster, isTickRefresh); + } + } + + public void ActivateMode(IMonsterEntity monsterEntity) + { + if (monsterEntity.MapInstance.MapInstanceType == MapInstanceType.EventGameInstance) + { + return; + } + + monsterEntity.ModeIsActive = true; + + if (monsterEntity.ModeCModeVnum != 0) + { + monsterEntity.Morph = monsterEntity.ModeCModeVnum; + monsterEntity.BroadcastMonsterMorph(); + } + + AddModeBCards(monsterEntity); + } + + public void DeactivateMode(IMonsterEntity monsterEntity) + { + if (monsterEntity.MapInstance.MapInstanceType == MapInstanceType.EventGameInstance) + { + return; + } + + if (!monsterEntity.HasMode) + { + return; + } + + monsterEntity.ModeIsActive = false; + + if (monsterEntity.Morph != 0) + { + monsterEntity.Morph = 0; + monsterEntity.BroadcastMonsterMorph(-1); + } + + RemoveModeBCards(monsterEntity); + } + + public void IncreaseMonsterDeathsOnMap() + { + _totalMonstersDeaths++; + } + + public long MonsterDeathsOnMap() => _totalMonstersDeaths; + public byte CurrentVessels() => _currentVessels; + + public bool IsSummonLimitReached(int? summonerId, SummonType? summonSummonType) + { + if (summonSummonType is SummonType.MONSTER_WAVE) + { + return _summonMonsterWaveCounter >= SUMMONED_MONSTERS_LIMIT_MONSTER_WAVES; + } + + if (summonerId is null) + { + return false; + } + + if (!_summonedByMonsterId.TryGetValue(summonerId.Value, out int summonedMonsters)) + { + return false; + } + + return summonedMonsters >= SUMMONED_MONSTERS_LIMIT_PER_ENTITY; + } + + public void RemoveTarget(IMonsterEntity monsterEntity, IBattleEntity entityToRemove, bool checkIfPlayer = false) + { + if (checkIfPlayer) + { + IPlayerEntity playerEntity = entityToRemove switch + { + IPlayerEntity player => player, + IMateEntity mateEntity => mateEntity?.Owner, + _ => null + }; + + if (playerEntity != null) + { + AggroIsRemoved(monsterEntity, playerEntity); + foreach (IBattleEntity mate in monsterEntity.Targets.Where(x => x is IMateEntity mateEntity && mateEntity.Owner?.Id == playerEntity.Id)) + { + AggroIsRemoved(monsterEntity, mate); + } + } + else + { + AggroIsRemoved(monsterEntity, entityToRemove); + } + } + else + { + AggroIsRemoved(monsterEntity, entityToRemove); + } + + if (monsterEntity.GroupAttack != (int)GroupAttackType.None) + { + RemoveTargetFromGroupAttackers(monsterEntity, entityToRemove); + } + + monsterEntity.Targets.Remove(entityToRemove); + + if (monsterEntity.Target != null && monsterEntity.Target.IsSameEntity(entityToRemove)) + { + monsterEntity.Target = null; + } + + TryApproachToClosestTarget(monsterEntity); + monsterEntity.IsApproachingTarget = false; + if (entityToRemove != null) + { + monsterEntity.PlayersDamage.TryRemove(entityToRemove.Id, out _); + } + + monsterEntity.ShouldFindNewTarget = monsterEntity.Targets.Count == 0; + } + + public void AddEntityToTargets(IMonsterEntity monsterEntity, IBattleEntity target) + { + if (target == null) + { + return; + } + + if (!monsterEntity.CanSeeInvisible && target.IsInvisible()) + { + return; + } + + if (!AddPlayerOrMateTarget(monsterEntity, target, false)) + { + return; + } + + monsterEntity.NextTick -= TimeSpan.FromMilliseconds(800); + monsterEntity.AttentionTime = DateTime.UtcNow + TimeSpan.FromSeconds(10); + if (monsterEntity.GroupAttack != (short)GroupAttackType.None) + { + AddAttackerToGroupAttackers(monsterEntity, target); + } + + IsAggroLimitReached(monsterEntity, target, true, true); + TryApproachToClosestTarget(monsterEntity); + } + + public void MonsterRefreshTarget(IMonsterEntity monsterEntity, IBattleEntity target, in DateTime time, bool isByAttacking = false) + { + monsterEntity.ReturnTimeOut = 0; + if (monsterEntity.Targets.Contains(target)) + { + if (!isByAttacking) + { + return; + } + + AddPlayerOrMateTarget(monsterEntity, target, true); + monsterEntity.AttentionTime = time + TimeSpan.FromSeconds(15); + + return; + } + + if (target.IsMonsterAggroDisabled()) + { + return; + } + + if (!monsterEntity.CanSeeInvisible && target.IsInvisible() && !isByAttacking) + { + return; + } + + monsterEntity.Waypoints = null; + monsterEntity.AttentionTime = time + TimeSpan.FromSeconds(10); + monsterEntity.NextTick -= TimeSpan.FromMilliseconds(800); + monsterEntity.GoToBossPosition = null; + + switch (target) + { + case IMateEntity: + case IPlayerEntity: + AggroLogic(monsterEntity, target, isByAttacking, time); + break; + default: + if (!monsterEntity.Targets.Contains(target)) + { + monsterEntity.Targets.Add(target); + } + + if (!monsterEntity.TargetsByVisualTypeAndId.Contains((target.Type, target.Id))) + { + monsterEntity.TargetsByVisualTypeAndId.Add((target.Type, target.Id)); + } + + TryApproachToClosestTarget(monsterEntity); + + if (!isByAttacking) + { + return; + } + + if (monsterEntity.Damagers.Contains(target)) + { + return; + } + + monsterEntity.Damagers.Add(target); + break; + } + } + + public void ForgetAll(IMonsterEntity monsterEntity, in DateTime time, bool clearDamagers = true) + { + if (monsterEntity.Target != null) + { + RemoveTarget(monsterEntity, monsterEntity.Target); + } + + monsterEntity.LastSkill = DateTime.MinValue; + + if (monsterEntity.GroupAttack != (int)GroupAttackType.None) + { + ForgetAllDamagersFromGroupAttackers(monsterEntity); + } + + monsterEntity.PlayersDamage.Clear(); + + if (clearDamagers) + { + monsterEntity.Damagers.Clear(); + } + + monsterEntity.Targets.Clear(); + monsterEntity.TargetsByVisualTypeAndId.Clear(); + } + + public IMonsterEntity GetMonsterById(long id) => _monstersById.GetOrDefault(id); + public IMonsterEntity GetMonsterByUniqueId(Guid id) => _monstersByUniqueId.GetOrDefault(id); + + public IReadOnlyList GetAliveMonsters() + { + _lock.EnterReadLock(); + try + { + return _monsters.FindAll(x => x != null && x.IsAlive() && x.IsStillAlive); + } + finally + { + _lock.ExitReadLock(); + } + } + + public IReadOnlyList GetDeadMonsters() + { + _lock.EnterReadLock(); + try + { + return _monsters.FindAll(x => !x.IsAlive() || !x.IsStillAlive); + } + finally + { + _lock.ExitReadLock(); + } + } + + public IReadOnlyList GetAliveMonsters(Func predicate) + { + _lock.EnterReadLock(); + try + { + return _monsters.FindAll(entity => entity is { IsStillAlive: true } && entity.IsAlive() && (predicate == null || predicate(entity))); + } + finally + { + _lock.ExitReadLock(); + } + } + + public IReadOnlyList GetAliveMonstersInRange(Position pos, short distance) + { + return GetAliveMonsters(s => pos.IsInAoeZone(s.Position, (byte)(distance + s.CellSize))); + } + + public IReadOnlyList GetClosestMonstersInRange(Position pos, short distance) + { + _lock.EnterReadLock(); + try + { + List toReturn = _monsters.FindAll(s => s != null && s.IsAlive() && s.IsStillAlive && pos.IsInAoeZone(s.Position, distance)); + toReturn.Sort((prev, next) => prev.Position.GetDistance(pos) - next.Position.GetDistance(pos)); + + return toReturn; + } + finally + { + _lock.ExitReadLock(); + } + } + + public void AddMonster(IMonsterEntity entity) + { + _toAddMonsters.Enqueue(entity); + } + + public void RemoveMonster(IMonsterEntity entity) + { + _toRemoveMonsters.Enqueue(entity); + } + + private void Update(in DateTime date, IMonsterEntity monsterEntity, bool isTickRefresh) + { + if (!monsterEntity.IsStillAlive) + { + ProcessRespawnLogic(monsterEntity, date); + return; + } + + if (monsterEntity.SpawnDate.AddMilliseconds(500) > date) + { + if (monsterEntity.SummonerType is not VisualType.Player) + { + return; + } + } + + if (!monsterEntity.IsAlive()) + { + return; + } + + ShowEffect(monsterEntity, date); + _bCardTickSystem.ProcessUpdate(monsterEntity, date); + ProcessRecurrentLifeDecrease(monsterEntity, date); + ProcessManaRegeneration(monsterEntity, date); + TryRemoveTargets(monsterEntity, date); + + if (_mapInstance.AIDisabled) + { + return; + } + + if (monsterEntity.IsCastingSkill) + { + IBattleEntity entity = monsterEntity.MapInstance.GetBattleEntity(monsterEntity.LastAttackedEntity.Item1, monsterEntity.LastAttackedEntity.Item2); + if (entity == null) + { + monsterEntity.CancelCastingSkill(); + } + + return; + } + + if (monsterEntity.IsMonsterAggroDisabled()) + { + return; + } + + if (monsterEntity.NextTick > date) + { + return; + } + + TryApproachToClosestTarget(monsterEntity); + RefreshTarget(monsterEntity, date); + TryRunAway(monsterEntity, date); + TryTalk(monsterEntity); + + if (monsterEntity.IsApproachingTarget) + { + monsterEntity.IsApproachingTarget = false; + ApproachTarget(monsterEntity, date); + monsterEntity.NextTick = (isTickRefresh ? date : monsterEntity.NextTick) + TimeSpan.FromMilliseconds(1000); + return; + } + + if (monsterEntity.FindNewPositionAroundTarget) + { + monsterEntity.FindNewPositionAroundTarget = false; + monsterEntity.NextTick = (isTickRefresh ? date : monsterEntity.NextTick) + TimeSpan.FromMilliseconds(200); + + if (monsterEntity.Target == null) + { + return; + } + + short randomX = (short)(monsterEntity.Target.Position.X + _randomGenerator.RandomNumber(-1, 2)); + short randomY = (short)(monsterEntity.Target.Position.Y + _randomGenerator.RandomNumber(-1, 2)); + + if (randomX == monsterEntity.Position.X && randomY == monsterEntity.Position.Y) + { + return; + } + + if (randomX == monsterEntity.Target.Position.X && randomY == monsterEntity.Target.PositionY) + { + monsterEntity.FindNewPositionAroundTarget = true; + return; + } + + if (!MovementPreChecks(monsterEntity)) + { + return; + } + + if (monsterEntity.MapInstance.IsBlockedZone(randomX, randomY)) + { + return; + } + + if (monsterEntity.IsMonsterAggroDisabled(randomX, randomY)) + { + return; + } + + ProcessMovement(monsterEntity, randomX, randomY); + return; + } + + if (monsterEntity.Target == null && (monsterEntity.MapInstance.MapInstanceType != MapInstanceType.RaidInstance + || monsterEntity.MapInstance.MapInstanceType == MapInstanceType.RaidInstance && (monsterEntity.GoToBossPosition != null || monsterEntity.Waypoints != null))) + { + int random = _randomGenerator.RandomNumber(); + bool move = random <= 60; + + if (move || monsterEntity.ReturningToFirstPosition || monsterEntity.GoToBossPosition != null || monsterEntity.Waypoints != null) + { + WalkAround(monsterEntity, date); + } + + monsterEntity.NextTick = (isTickRefresh ? date + TimeSpan.FromMilliseconds(_randomGenerator.RandomNumber(1000)) : monsterEntity.NextTick) + TimeSpan.FromMilliseconds(1000); + return; + } + + TryFight(date, monsterEntity, isTickRefresh); + CheckQuestMonster(monsterEntity, date); + CheckForModeRange(monsterEntity); + } + + private void TryTalk(IMonsterEntity monsterEntity) + { + if (!_monsterTalkingConfig.HasPossibleMessages(monsterEntity.MonsterVNum)) + { + return; + } + + if (_randomGenerator.RandomNumber() > 5) + { + return; + } + + IReadOnlyList messages = _monsterTalkingConfig.PossibleMessage(monsterEntity.MonsterVNum); + if (messages == null) + { + return; + } + + if (messages.Count < 1) + { + return; + } + + string message = messages[_randomGenerator.RandomNumber(messages.Count)]; + monsterEntity.MapInstance.Broadcast(x => monsterEntity.GenerateSayPacket(x.GetLanguage(message), ChatMessageColorType.PlayerSay), + new RangeBroadcast(monsterEntity.PositionX, monsterEntity.PositionY, 30)); + } + + private void TryRunAway(IMonsterEntity monsterEntity, in DateTime date) + { + if (!monsterEntity.IsRunningAway) + { + return; + } + + ApproachTarget(monsterEntity, date); + monsterEntity.NextTick += TimeSpan.FromMilliseconds(1000); + } + + private void TryRemoveTargets(IMonsterEntity monsterEntity, in DateTime date) + { + if (monsterEntity.Targets.Count == 0 && monsterEntity.TargetsByVisualTypeAndId.Count == 0) + { + return; + } + + if (monsterEntity.LastTargetsRefresh.AddSeconds(5) > date) + { + return; + } + + monsterEntity.LastTargetsRefresh = date; + HashSet toRemove = new(); + HashSet<(VisualType, long)> toRemoveByTypeAndId = new(); + + foreach ((VisualType visualType, long id) in monsterEntity.TargetsByVisualTypeAndId) + { + IBattleEntity entityOnMap = monsterEntity.MapInstance.GetBattleEntity(visualType, id); + if (entityOnMap != null && entityOnMap.IsAlive()) + { + continue; + } + + toRemoveByTypeAndId.Add((visualType, id)); + } + + foreach (IBattleEntity target in monsterEntity.Targets) + { + if (target == null) + { + continue; + } + + IBattleEntity entityOnMap = monsterEntity.MapInstance.GetBattleEntity(target.Type, target.Id); + if (entityOnMap != null && entityOnMap.IsAlive()) + { + continue; + } + + toRemove.Add(target); + } + + foreach ((VisualType, long) entity in toRemoveByTypeAndId) + { + monsterEntity.TargetsByVisualTypeAndId.Remove((entity.Item1, entity.Item2)); + } + + foreach (IBattleEntity entity in toRemove) + { + RemoveTarget(monsterEntity, entity); + } + } + + private void ProcessManaRegeneration(IMonsterEntity monsterEntity, in DateTime date) + { + if (!monsterEntity.CanRegenMp) + { + return; + } + + if (monsterEntity.Mp == monsterEntity.MaxMp) + { + return; + } + + if (monsterEntity.LastMpRegen.AddSeconds(3) > date) + { + return; + } + + monsterEntity.LastMpRegen = date; + int toAdd = (int)(monsterEntity.MaxMp * 0.01); + monsterEntity.Mp = monsterEntity.Mp + toAdd > monsterEntity.MaxMp ? monsterEntity.MaxMp : monsterEntity.Mp + toAdd; + } + + private void TryApproachToClosestTarget(IMonsterEntity monsterEntity) + { + if (monsterEntity.TargetsByVisualTypeAndId.Count == 0) + { + return; + } + + IBattleEntity newEntity = monsterEntity.MapInstance + .GetBattleEntities(x => monsterEntity.CanHit(x) && monsterEntity.IsEnemyWith(x) && monsterEntity.TargetsByVisualTypeAndId.Contains((x.Type, x.Id))) + .OrderBy(x => monsterEntity.Position.GetDistance(x.Position)).FirstOrDefault(); + + if (newEntity == null) + { + return; + } + + monsterEntity.Target = newEntity; + } + + private void CheckQuestMonster(IMonsterEntity monsterEntity, in DateTime date) + { + if (!monsterEntity.IsMonsterSpawningMonstersForQuest()) + { + return; + } + + if (date <= monsterEntity.NextAttackReady) + { + return; + } + + ForgetAll(monsterEntity, date); + } + + private void TryActivateMode(IMonsterEntity monsterEntity) + { + if (monsterEntity.MapInstance.MapInstanceType == MapInstanceType.EventGameInstance) + { + return; + } + + if (monsterEntity.ModeIsActive) + { + return; + } + + if (monsterEntity.ModeLimiterType == 1) + { + if (monsterEntity.Position.GetDistance(monsterEntity.Target.Position) > monsterEntity.ModeRangeTreshold) + { + return; + } + } + + if (monsterEntity.ModeIsHpTriggered && monsterEntity.GetHpPercentage() > monsterEntity.ModeHpTresholdOrItemVnum) + { + return; + } + + ActivateMode(monsterEntity); + } + + private void TryDeactivateMode(IMonsterEntity monsterEntity) + { + if (monsterEntity.MapInstance.MapInstanceType == MapInstanceType.EventGameInstance) + { + return; + } + + if (!monsterEntity.ModeIsActive) + { + return; + } + + if (monsterEntity.ModeLimiterType == 2) + { + if (_totalMonstersDeaths - monsterEntity.ModeDeathsSinceRespawn < monsterEntity.ModeRangeTreshold) + { + return; + } + } + + DeactivateMode(monsterEntity); + } + + private void CheckForModeRange(IMonsterEntity monsterEntity) + { + if (monsterEntity.MapInstance.MapInstanceType == MapInstanceType.EventGameInstance) + { + return; + } + + if (!monsterEntity.HasMode) + { + return; + } + + if (!monsterEntity.ModeIsHpTriggered) + { + return; + } + + if (monsterEntity.ModeLimiterType != 1) + { + return; + } + + if (monsterEntity.Target == null) + { + DeactivateMode(monsterEntity); + return; + } + + if (monsterEntity.Position.GetDistance(monsterEntity.Target.Position) <= monsterEntity.ModeRangeTreshold) + { + return; + } + + DeactivateMode(monsterEntity); + } + + private void AddModeBCards(IMonsterEntity monsterEntity) + { + IReadOnlyList modeBCards = monsterEntity.ModeBCards; + + var attackBCards = new List(); + var defenseBCards = new List(); + + foreach (BCardDTO bCard in modeBCards) + { + switch ((CastType)bCard.CastType) + { + case CastType.SELF: + monsterEntity.BCardComponent.AddBCard(bCard); + _bcardHandlers.Execute(monsterEntity, monsterEntity, bCard); + break; + case CastType.ATTACK: + attackBCards.Add(bCard); + break; + case CastType.DEFENSE: + defenseBCards.Add(bCard); + break; + } + } + + monsterEntity.BCardComponent.AddTriggerBCards(BCardTriggerType.ATTACK, attackBCards); + monsterEntity.BCardComponent.AddTriggerBCards(BCardTriggerType.DEFENSE, defenseBCards); + monsterEntity.RefreshStats(); + } + + private void RemoveModeBCards(IMonsterEntity monsterEntity) + { + IReadOnlyList modeBCards = monsterEntity.ModeBCards; + + foreach (BCardDTO bCard in modeBCards.Where(x => (CastType)x.CastType == CastType.SELF)) + { + monsterEntity.BCardComponent.RemoveBCard(bCard); + } + + monsterEntity.BCardComponent.RemoveAllTriggerBCards(BCardTriggerType.ATTACK); + monsterEntity.BCardComponent.RemoveAllTriggerBCards(BCardTriggerType.DEFENSE); + monsterEntity.RefreshStats(); + } + + + private void ProcessRecurrentLifeDecrease(IMonsterEntity monsterEntity, DateTime date) + { + if (monsterEntity.LastSpecialHpDecrease.AddSeconds(1) > date) + { + return; + } + + bool isSummonedByAnotherMonster = monsterEntity.SummonerId != 0 && monsterEntity.SummonerType == VisualType.Monster; + + monsterEntity.LastSpecialHpDecrease = date; + if (monsterEntity.DisappearAfterSeconds || isSummonedByAnotherMonster) + { + int hpToDecrease = monsterEntity.MaxHp / (monsterEntity.MaxHp / 5); + monsterEntity.Hp -= hpToDecrease; + if (monsterEntity.Hp > 0) + { + return; + } + + _eventPipeline.ProcessEventAsync(new MonsterDeathEvent(monsterEntity)); + return; + } + + if (!monsterEntity.DisappearAfterSecondsMana) + { + return; + } + + int toRemove = monsterEntity.MaxMp / (monsterEntity.MaxMp / 10); + monsterEntity.Mp -= toRemove; + if (monsterEntity.Mp > 0) + { + return; + } + + _eventPipeline.ProcessEventAsync(new MonsterDeathEvent(monsterEntity)); + } + + private void RefreshTarget(IMonsterEntity monsterEntity, in DateTime date) + { + if (monsterEntity.BCardComponent.HasBCard(BCardType.SpecialEffects, (byte)AdditionalTypes.SpecialEffects.ToNonPrefferedAttack)) + { + return; + } + + IBattleEntity target = monsterEntity.Target; + // if someone attack monster + if (target != null) + { + // if target is on diffrent map + // if target is dead + if (target.MapInstance?.Id != monsterEntity.MapInstance?.Id) + { + RemoveTarget(monsterEntity, target); + return; + } + + if (!target.IsAlive()) + { + RemoveTarget(monsterEntity, target); + return; + } + } + + // if monster is agressive, find target || looking for whether or not his companions were attacked + if (!monsterEntity.IsHostile && monsterEntity.GroupAttack == (int)GroupAttackType.None || target != null) + { + return; + } + + FindTarget(monsterEntity, date); + } + + private bool IsAggroLimitReached(IMonsterEntity monsterEntity, IBattleEntity entity, bool addValue = true, bool isByAttacking = false) + { + if (entity == null) + { + return false; + } + + if (monsterEntity.IsMateTrainer) + { + return false; + } + + if (monsterEntity.RawHostility is (int)HostilityType.ATTACK_ANGELS_ONLY or (int)HostilityType.ATTACK_DEVILS_ONLY) + { + return false; + } + + if (!monsterEntity.MapInstance.HasMapFlag(MapFlags.IS_BASE_MAP)) + { + return false; + } + + if (monsterEntity.IsMonsterSpawningMonstersForQuest()) + { + return false; + } + + if (!monsterEntity.CanWalk) + { + return false; + } + + if (entity.AggroedEntities.Count >= AGGRO_LIMIT_PER_ENTITY && !isByAttacking) + { + return true; + } + + if (!addValue) + { + return false; + } + + entity.AggroedEntities.Add(monsterEntity.UniqueId); + return false; + } + + private void AggroIsRemoved(IMonsterEntity monsterEntity, IBattleEntity target) + { + if (!monsterEntity.IsAlive()) + { + foreach (IBattleEntity targetInTargets in monsterEntity.Targets) + { + if (targetInTargets == null) + { + continue; + } + + targetInTargets.AggroedEntities?.Remove(monsterEntity.UniqueId); + monsterEntity.TargetsByVisualTypeAndId.Remove((targetInTargets.Type, targetInTargets.Id)); + } + + return; + } + + if (target == null) + { + return; + } + + target.AggroedEntities?.Remove(monsterEntity.UniqueId); + monsterEntity.TargetsByVisualTypeAndId.Remove((target.Type, target.Id)); + } + + private void FindTarget(IMonsterEntity monsterEntity, in DateTime time, bool ignoreHostile = false) + { + IBattleEntity target = null; + + if (monsterEntity.IsHostile || ignoreHostile) + { + target = HostileFinding(monsterEntity); + } + else if (monsterEntity.GroupAttack != (int)GroupAttackType.None) + { + target = DefensiveFinding(monsterEntity); + } + + if (target == null) + { + return; + } + + MonsterRefreshTarget(monsterEntity, target, time); + } + + private IBattleEntity HostileFinding(IMonsterEntity monsterEntity) + { + IEnumerable targets; + byte noticeRange = monsterEntity.NoticeRange; + + switch (monsterEntity.RawHostility) + { + case (int)HostilityType.ATTACK_MATES: + targets = monsterEntity.MapInstance.GetAliveMatesInRange(monsterEntity.Position, noticeRange); + break; + case (int)HostilityType.ATTACK_IN_RANGE: + targets = monsterEntity.MapInstance.GetBattleEntitiesInRange(monsterEntity.Position, noticeRange); + break; + case (int)HostilityType.ATTACK_ANGELS_ONLY: + targets = monsterEntity.MapInstance.GetNonMonsterBattleEntitiesInRange(monsterEntity.Position, noticeRange, x => x.Faction == FactionType.Angel); + break; + case (int)HostilityType.ATTACK_DEVILS_ONLY: + targets = monsterEntity.MapInstance.GetNonMonsterBattleEntitiesInRange(monsterEntity.Position, noticeRange, x => x.Faction == FactionType.Demon); + break; + case (int)HostilityType.ATTACK_NOT_WEARING_PHANTOM_AMULET: + targets = monsterEntity.MapInstance.GetCharactersInRange(monsterEntity.Position, noticeRange, x => !monsterEntity.FindPhantomAmulet(x)); + break; + case (int)HostilityType.NOT_HOSTILE: + targets = monsterEntity.MapInstance.GetBattleEntitiesInRange(monsterEntity.Position, noticeRange); + break; + default: + if (monsterEntity.IsMonsterSpawningMonstersForQuest() == false) + { + throw new ArgumentOutOfRangeException("HostilityType", + $"The HostilityType: '{monsterEntity.RawHostility.ToString()}' is not being handled in the actual switch inside MonsterAISystem.FindTarget"); + } + + int questId = monsterEntity.RawHostility - 20000; + targets = monsterEntity.MapInstance.GetCharactersInRange(monsterEntity.Position, 1, s => s.HasQuestWithId(questId) && !s.IsQuestCompleted(s.GetQuestById(questId))); + break; + } + + return BasicTargetChecks(monsterEntity, targets); + } + + private IBattleEntity BasicTargetChecks(IMonsterEntity monsterEntity, IEnumerable targets) + { + if (monsterEntity.SummonerId.HasValue && monsterEntity.SummonerType.HasValue) + { + IBattleEntity summoner = monsterEntity.MapInstance.GetBattleEntity(monsterEntity.SummonerType.Value, monsterEntity.SummonerId.Value); + if (monsterEntity.SummonerType.Value == VisualType.Player && summoner != null) + { + targets = monsterEntity.GetEnemiesInRange(monsterEntity, monsterEntity.NoticeRange); + } + else + { + targets = targets.Where(monsterEntity.IsEnemyWith); + } + } + else + { + targets = targets.Where(monsterEntity.IsEnemyWith); + } + + if (monsterEntity.SummonerId == null) + { + targets = targets.Where(e => + { + if (monsterEntity.IsMonsterSpawningMonstersForQuest() && !e.IsPlayer()) + { + return false; + } + + if (monsterEntity.CanHit(e) && e.IsAlive() && !IsAggroLimitReached(monsterEntity, e, false) && !e.IsMonsterAggroDisabled()) + { + return monsterEntity.CanSeeInvisible || !e.IsInvisible(); + } + + return false; + }); + } + + return targets.OrderBy(monsterEntity.GetDistance).FirstOrDefault(); + } + + private IBattleEntity DefensiveFinding(IMonsterEntity monsterEntity) + { + if (monsterEntity.Target != null) + { + return monsterEntity.Target; + } + + int monsterBy = monsterEntity.GroupAttack == (int)GroupAttackType.ByRace ? (int)monsterEntity.MonsterRaceType : monsterEntity.IconId; + + if (!_attackerByRace.TryGetValue(monsterBy, out Dictionary dictionary)) + { + return default; + } + + byte noticeRange = monsterEntity.NoticeRange; + IEnumerable targets = monsterEntity.MapInstance.GetCharactersInRange(monsterEntity.Position, noticeRange, + e => dictionary.ContainsKey(e.Id) && dictionary.GetOrDefault(e.Id) > 0); + + return BasicTargetChecks(monsterEntity, targets); + } + + private void AggroLogicGroupAttackType(IMonsterEntity monsterEntity, IBattleEntity entity, bool isByAttacking, in DateTime time) + { + if (IsAggroLimitReached(monsterEntity, entity, true, isByAttacking)) + { + return; + } + + AddAttackerToGroupAttackers(monsterEntity, entity); + if (!isByAttacking) + { + monsterEntity.LastSkill = time; + } + + AddPlayerOrMateTarget(monsterEntity, entity, isByAttacking); + TryApproachToClosestTarget(monsterEntity); + + if (isByAttacking) + { + return; + } + + if (entity is not IPlayerEntity playerEntity) + { + return; + } + + playerEntity.Session?.SendEffectEntity(monsterEntity, EffectType.TargetedByOthers); + } + + private bool AddPlayerOrMateTarget(IMonsterEntity monsterEntity, IBattleEntity entity, bool isByAttacking) + { + bool removeTickFromMonster = false; + + long? dollOwnerId = monsterEntity.SummonerId; + + switch (entity) + { + case IMateEntity mateEntity: + if (monsterEntity.IsMonsterSpawningMonstersForQuest()) + { + return true; + } + + IPlayerEntity owner = mateEntity.Owner; + if (!monsterEntity.IsMateTrainer) + { + if (!monsterEntity.Targets.Contains(owner)) + { + monsterEntity.Targets.Add(owner); + removeTickFromMonster = true; + } + + if (!monsterEntity.TargetsByVisualTypeAndId.Contains((owner.Type, owner.Id))) + { + monsterEntity.TargetsByVisualTypeAndId.Add((owner.Type, owner.Id)); + removeTickFromMonster = true; + } + + if (isByAttacking && !monsterEntity.Damagers.Contains(owner)) + { + monsterEntity.Damagers.Add(owner); + removeTickFromMonster = true; + } + } + else if (dollOwnerId.HasValue && dollOwnerId.Value != owner.Id) + { + return true; + } + + if (!monsterEntity.Targets.Contains(mateEntity)) + { + monsterEntity.Targets.Add(mateEntity); + removeTickFromMonster = true; + } + + if (!monsterEntity.TargetsByVisualTypeAndId.Contains((mateEntity.Type, mateEntity.Id))) + { + monsterEntity.TargetsByVisualTypeAndId.Add((mateEntity.Type, mateEntity.Id)); + removeTickFromMonster = true; + } + + if (isByAttacking && !monsterEntity.Damagers.Contains(mateEntity)) + { + monsterEntity.Damagers.Add(mateEntity); + removeTickFromMonster = true; + } + + IMateEntity secondMate = owner.MateComponent.GetTeamMember(x => x.Type != mateEntity.Type); + if (secondMate == null) + { + break; + } + + if (!monsterEntity.Targets.Contains(secondMate)) + { + monsterEntity.Targets.Add(secondMate); + removeTickFromMonster = true; + } + + if (!monsterEntity.TargetsByVisualTypeAndId.Contains((secondMate.Type, secondMate.Id))) + { + monsterEntity.TargetsByVisualTypeAndId.Add((secondMate.Type, secondMate.Id)); + removeTickFromMonster = true; + } + + if (isByAttacking && !monsterEntity.Damagers.Contains(secondMate)) + { + monsterEntity.Damagers.Add(secondMate); + removeTickFromMonster = true; + } + + break; + case IPlayerEntity playerEntity: + if (!monsterEntity.IsMateTrainer) + { + if (!monsterEntity.Targets.Contains(playerEntity)) + { + monsterEntity.Targets.Add(playerEntity); + removeTickFromMonster = true; + } + + if (!monsterEntity.TargetsByVisualTypeAndId.Contains((playerEntity.Type, playerEntity.Id))) + { + monsterEntity.TargetsByVisualTypeAndId.Add((playerEntity.Type, playerEntity.Id)); + removeTickFromMonster = true; + } + } + else if (dollOwnerId.HasValue && dollOwnerId.Value != playerEntity.Id) + { + return true; + } + + if (monsterEntity.IsMonsterSpawningMonstersForQuest()) + { + return true; + } + + if (isByAttacking && !monsterEntity.Damagers.Contains(playerEntity)) + { + monsterEntity.Damagers.Add(playerEntity); + removeTickFromMonster = true; + } + + foreach (IMateEntity mate in playerEntity.MateComponent.TeamMembers()) + { + if (!monsterEntity.Targets.Contains(mate)) + { + monsterEntity.Targets.Add(mate); + removeTickFromMonster = true; + } + + if (!monsterEntity.TargetsByVisualTypeAndId.Contains((mate.Type, mate.Id))) + { + monsterEntity.TargetsByVisualTypeAndId.Add((mate.Type, mate.Id)); + removeTickFromMonster = true; + } + + if (!isByAttacking) + { + continue; + } + + if (monsterEntity.Damagers.Contains(mate)) + { + continue; + } + + monsterEntity.Damagers.Add(mate); + removeTickFromMonster = true; + } + + break; + } + + return removeTickFromMonster; + } + + private void AddAttackerToGroupAttackers(IMonsterEntity monsterEntity, IBattleEntity target) + { + IBattleEntity entity = target switch + { + IMateEntity mate => mate.Owner, + _ => target + }; + + int monsterBy = monsterEntity.GroupAttack == (int)GroupAttackType.ByRace ? (int)monsterEntity.MonsterRaceType : monsterEntity.IconId; + if (_attackerByRace.TryGetValue(monsterBy, out Dictionary dictionary)) + { + if (!dictionary.TryGetValue(entity.Id, out long monsterRaceAggro)) + { + dictionary.Add(entity.Id, 1); + return; + } + + if (monsterRaceAggro >= AGGRO_LIMIT_PER_ENTITY) + { + return; + } + + monsterRaceAggro++; + dictionary[entity.Id] = monsterRaceAggro; + return; + } + + _attackerByRace.Add(monsterBy, new Dictionary + { + { entity.Id, 1 } + }); + } + + private void RemoveTargetFromGroupAttackers(IMonsterEntity monsterEntity, IBattleEntity target) + { + if (target?.Type != VisualType.Player) + { + return; + } + + int monsterBy = monsterEntity.GroupAttack == (int)GroupAttackType.ByRace ? (int)monsterEntity.MonsterRaceType : monsterEntity.IconId; + if (!_attackerByRace.TryGetValue(monsterBy, out Dictionary dictionary)) + { + return; + } + + if (!dictionary.TryGetValue(target.Id, out long amount)) + { + return; + } + + amount -= 1; + dictionary[target.Id] = amount; + + bool removeFromDic = amount <= 0; + + if (!removeFromDic) + { + return; + } + + _attackerByRace[monsterBy].Remove(target.Id); + } + + private void ForgetAllDamagersFromGroupAttackers(IMonsterEntity monsterEntity) + { + int monsterBy = monsterEntity.GroupAttack == (int)GroupAttackType.ByRace ? (int)monsterEntity.MonsterRaceType : monsterEntity.IconId; + if (!_attackerByRace.TryGetValue(monsterBy, out Dictionary dictionary)) + { + return; + } + + foreach (IBattleEntity target in monsterEntity.Targets) + { + if (!dictionary.TryGetValue(target.Id, out long amount)) + { + continue; + } + + amount -= 1; + dictionary[target.Id] = amount; + + bool removeFromDic = amount <= 0; + + if (!removeFromDic) + { + continue; + } + + _attackerByRace[monsterBy].Remove(target.Id); + } + } + + private void AggroLogic(IMonsterEntity monsterEntity, IBattleEntity target, bool isByAttacking, in DateTime time) + { + if (monsterEntity.GroupAttack != (int)GroupAttackType.None) + { + AggroLogicGroupAttackType(monsterEntity, target, isByAttacking, time); + return; + } + + if (IsAggroLimitReached(monsterEntity, target, true, isByAttacking)) + { + return; + } + + if (!isByAttacking) + { + monsterEntity.LastSkill = time; + } + + AddPlayerOrMateTarget(monsterEntity, target, isByAttacking); + TryApproachToClosestTarget(monsterEntity); + + if (monsterEntity.MonsterVNum == (short)MonsterVnum.ONYX_MONSTER) + { + return; + } + + switch (monsterEntity.MonsterRaceType) + { + case MonsterRaceType.Other: + case MonsterRaceType.Fixed: + return; + } + + if (!target.IsPlayer()) + { + return; + } + + if (monsterEntity.ReturningToFirstPosition) + { + return; + } + + if (isByAttacking) + { + return; + } + + if (monsterEntity.ShouldFindNewTarget) + { + return; + } + + ((IPlayerEntity)target).Session.SendEffectEntity(monsterEntity, EffectType.Targeted); + } + + private void TryFight(in DateTime date, IMonsterEntity monsterEntity, bool isTickRefresh) + { + if (monsterEntity == null) + { + return; + } + + if (monsterEntity.MonsterVNum == (short)MonsterVnum.ONYX_MONSTER) + { + return; + } + + if (!monsterEntity.IsAlive()) + { + return; + } + + if (!monsterEntity.IsStillAlive) + { + return; + } + + if (monsterEntity.Target == null) + { + return; + } + + if (monsterEntity.IsRunningAway) + { + return; + } + + if (!monsterEntity.Target.IsAlive()) + { + RemoveTarget(monsterEntity, monsterEntity.Target); + monsterEntity.NextTick = (isTickRefresh ? date : monsterEntity.NextTick) + TimeSpan.FromMilliseconds(200); + return; + } + + if (!monsterEntity.CanSeeInvisible) + { + // if target is player and he become invisible + if (monsterEntity.Target.IsInvisible()) + { + RemoveTarget(monsterEntity, monsterEntity.Target, true); + monsterEntity.NextTick = (isTickRefresh ? date : monsterEntity.NextTick) + TimeSpan.FromMilliseconds(200); + return; + } + } + + if (!monsterEntity.CanHit(monsterEntity.Target)) + { + RemoveTarget(monsterEntity, monsterEntity.Target); + monsterEntity.NextTick = (isTickRefresh ? date : monsterEntity.NextTick) + TimeSpan.FromMilliseconds(200); + return; + } + + if (monsterEntity.MapInstance.IsPvp) + { + if (!monsterEntity.Target.IsInPvpZone()) + { + RemoveTarget(monsterEntity, monsterEntity.Target); + monsterEntity.NextTick = (isTickRefresh ? date : monsterEntity.NextTick) + TimeSpan.FromMilliseconds(200); + return; + } + } + + bool isModeActive = monsterEntity.ModeIsActive; + + if (monsterEntity.ModeLimiterType == 2) + { + TryDeactivateMode(monsterEntity); + } + + IReadOnlyList monsterSkills = monsterEntity.NotBasicSkills; + + IBattleEntitySkill dashSkill = null; + if (monsterEntity.HasDash) + { + dashSkill = monsterEntity.DashSkill; + monsterSkills = monsterEntity.SkillsWithoutDashSkill; + } + + int count = monsterSkills.Count; + IBattleEntitySkill getRandomSkill = count != 0 ? monsterSkills[_randomGenerator.RandomNumber(0, count)] : null; + + bool randomSkill = _randomGenerator.RandomNumber() <= getRandomSkill?.Rate; + + // Find basic skill that replace ZSKILL + IBattleEntitySkill replacedBasic = monsterEntity.ReplacedBasicSkill; + IBattleEntitySkill skillToUse = randomSkill ? getRandomSkill : replacedBasic; + SkillInfo skillInfo = skillToUse?.Skill.GetInfo(battleEntity: monsterEntity) ?? monsterEntity.BasicSkill; + + bool randomSkillCantBeUsed = skillToUse != null && !monsterEntity.SkillCanBeUsed(skillToUse, date) && skillInfo.Vnum != 0 && skillToUse != replacedBasic; + + if (randomSkillCantBeUsed) + { + skillToUse = replacedBasic; + skillInfo = skillToUse?.Skill.GetInfo(battleEntity: monsterEntity) ?? monsterEntity.BasicSkill; + } + + short effectiveRange = skillInfo.Range == 0 || skillInfo.TargetType == TargetType.Self ? skillInfo.AoERange : skillInfo.Range; + + bool isInRange = monsterEntity.IsInRange(monsterEntity.Target.PositionX, monsterEntity.Target.PositionY, (byte)effectiveRange); + + bool shouldWalk = skillInfo.TargetAffectedEntities != + TargetAffectedEntities.BuffForAllies; //skillInfo.TargetType == TargetType.Target && skillInfo.TargetAffectedEntities != TargetAffectedEntities.BuffForAllies || + //skillInfo.TargetType == TargetType.Self && skillInfo.HitType is TargetHitType.AlliesInAffectedAoE or TargetHitType.EnemiesInAffectedAoE; + + if (!isInRange && shouldWalk) + { + if (monsterEntity.HasDash && dashSkill != null) + { + skillToUse = dashSkill; + skillInfo = dashSkill.Skill.GetInfo(); + + effectiveRange = skillInfo.Range == 0 || skillInfo.TargetType == TargetType.Self ? skillInfo.AoERange : skillInfo.Range; + if (!monsterEntity.SkillCanBeUsed(skillToUse, date) || !monsterEntity.CanPerformAttack() + || !monsterEntity.IsInRange(monsterEntity.Target.PositionX, monsterEntity.Target.PositionY, (byte)effectiveRange) + && skillInfo.TargetAffectedEntities != TargetAffectedEntities.BuffForAllies) + { + monsterEntity.IsApproachingTarget = true; + monsterEntity.NextTick = date; + return; + } + + if (_randomGenerator.RandomNumber() > skillToUse.Rate) + { + monsterEntity.IsApproachingTarget = true; + monsterEntity.NextTick = date; + return; + } + } + else + { + monsterEntity.IsApproachingTarget = true; + monsterEntity.NextTick = date; + return; + } + } + + monsterEntity.LastSkill = date; + monsterEntity.AttentionTime = date + TimeSpan.FromSeconds(15); + + if (monsterEntity.NextAttackReady > date) + { + return; + } + + if (getRandomSkill != null && getRandomSkill != monsterEntity.ReplacedBasicSkill && !randomSkill && monsterEntity.SkillCanBeUsed(getRandomSkill, date)) + { + monsterEntity.SetSkillCooldown(getRandomSkill.Skill.GetInfo()); + } + + int random = _randomGenerator.RandomNumber(); + if (monsterEntity.BasicHitChance == 0 || random >= monsterEntity.BasicHitChance * 20) + { + if (skillToUse is null or INpcMonsterSkill { IsIgnoringHitChance: false }) + { + monsterEntity.NextTick = (isTickRefresh ? date : monsterEntity.NextTick) + TimeSpan.FromMilliseconds(200); + return; + } + } + + // Invisible monsters who spawns monsters for quest + if (monsterEntity.IsMonsterSpawningMonstersForQuest()) + { + // check, if in notice range of monster there is a monster, who has been already spawned + if (GetAliveMonstersInRange(monsterEntity.Position, monsterEntity.NoticeRange).Any(x => x.MonsterVNum == monsterEntity.SpawnMobOrColor)) + { + return; + } + + int questVnum = monsterEntity.RawHostility - 20000; + if (monsterEntity.Target is IPlayerEntity character) + { + if (!character.HasQuestWithId(questVnum)) + { + ForgetAll(monsterEntity, date); + return; + } + } + else + { + string targetString = monsterEntity.Target == null ? "target is null" : $"{monsterEntity.Target.Type}:{monsterEntity.Target.Id}:{monsterEntity.Target.IsMate().ToString()}"; + Log.Error($"[MONSTER_QUEST_SYSTEM][TryFight] Target does not have quest! {targetString}", + new MonsterQuestSystemException( + $"[MONSTER_QUEST_SYSTEM][TryFight] Target was not a player: {_mapInstance.MapId.ToString()}, {monsterEntity.MonsterVNum}, {monsterEntity.PositionX}, {monsterEntity.PositionY}")); + ForgetAll(monsterEntity, date); + return; + } + } + + IMonsterEntity monster = GetAliveMonstersInRange(monsterEntity.Position, 0).FirstOrDefault(); + if (monster != null && monsterEntity != monster && _randomGenerator.RandomNumber() <= 60 && MovementPreChecks(monsterEntity)) + { + ProcessMovement(monsterEntity, monsterEntity.Target.Position.X, monsterEntity.Target.Position.Y); + monsterEntity.NextTick += TimeSpan.FromMilliseconds(1000); + return; + } + + IBattleEntity skillTarget = monsterEntity.Target; + + (int firstData, int secondData) cooldownToIncrease = (0, 0); + (int firstData, int secondData) cooldownToDecrease = (0, 0); + + if (monsterEntity.BCards.Count > 1) + { + cooldownToIncrease = monsterEntity.BCardComponent.GetAllBCardsInformation(BCardType.Mode, (byte)AdditionalTypes.Mode.AttackTimeIncreased, monsterEntity.Level); + cooldownToDecrease = monsterEntity.BCardComponent.GetAllBCardsInformation(BCardType.Mode, (byte)AdditionalTypes.Mode.AttackTimeDecreased, monsterEntity.Level); + } + + int basicCooldown = monsterEntity.BasicCooldown; + + if (cooldownToIncrease.firstData != 0) + { + basicCooldown += cooldownToIncrease.firstData; + } + + if (cooldownToDecrease.firstData != 0) + { + basicCooldown -= cooldownToDecrease.firstData; + } + + int tickToAdd = (2 + monsterEntity.BasicCastTime + 2 * basicCooldown) * 100; + tickToAdd = tickToAdd < 800 ? 800 : tickToAdd; + monsterEntity.NextAttackReady = date + TimeSpan.FromMilliseconds(tickToAdd); + + if (replacedBasic != null && skillInfo.Vnum == replacedBasic.Skill.Id && !monsterEntity.SkillCanBeUsed(replacedBasic, date)) + { + return; + } + + if (!monsterEntity.CanPerformAttack()) + { + return; + } + + if (!monsterEntity.Target.IsAlive()) + { + RemoveTarget(monsterEntity, monsterEntity.Target); + monsterEntity.NextTick = (isTickRefresh ? date : monsterEntity.NextTick) + TimeSpan.FromMilliseconds(200); + return; + } + + if (monsterEntity.ModeIsHpTriggered) + { + TryActivateMode(monsterEntity); + } + + if (isModeActive != monsterEntity.ModeIsActive) + { + return; + } + + if (skillInfo.Vnum != 0 && skillInfo.TargetType == TargetType.Self) + { + skillTarget = monsterEntity; + } + + Position positionAfterDash = default; + if (skillInfo.AttackType == AttackType.Dash && !monsterEntity.MapInstance.IsBlockedZone(monsterEntity.Target.Position.X, monsterEntity.Target.Position.Y) + && monsterEntity.Position.IsInRange(monsterEntity.Target.Position, skillInfo.Range + 2)) + { + positionAfterDash = monsterEntity.Target.Position; + } + + monsterEntity.RemoveEntityMp((short)skillInfo.ManaCost, skillToUse?.Skill); + + int castTick = (2 + monsterEntity.BasicCastTime) * 100; + DateTime castFinish = monsterEntity.GenerateSkillCastTime(skillInfo) + TimeSpan.FromMilliseconds(castTick < 200 ? 200 : castTick); + + monsterEntity.LastSkill = castFinish + TimeSpan.FromMilliseconds(monsterEntity.ApplyCooldownReduction(skillInfo) * 100); + monsterEntity.LastAttackedEntity = (skillTarget.Type, skillTarget.Id); + + short castTime = monsterEntity.GenerateSkillCastTimeNumber(skillInfo); + + monsterEntity.NextTick = castFinish + TimeSpan.FromMilliseconds(monsterEntity.BasicCastTime * 100 + castTime); + monsterEntity.EmitEvent(new BattleExecuteSkillEvent(monsterEntity, skillTarget, skillInfo, castFinish, positionAfterDash)); + } + + private void TryMoveToFirstPosition(IMonsterEntity monsterEntity, in DateTime date) + { + if (!monsterEntity.IsStillAlive) + { + return; + } + + if (!monsterEntity.IsAlive()) + { + return; + } + + if (!monsterEntity.CanWalk) + { + return; + } + + if (!MovementPreChecks(monsterEntity)) + { + return; + } + + if (monsterEntity.ReturningToFirstPosition) + { + return; + } + + if (monsterEntity.Target != null) + { + RemoveTarget(monsterEntity, monsterEntity.Target); + } + + monsterEntity.ReturningToFirstPosition = true; + ProcessMovement(monsterEntity, monsterEntity.FirstX, monsterEntity.FirstY); + } + + private void ApproachTarget(IMonsterEntity monsterEntity, in DateTime date) + { + if (monsterEntity.Target == null) + { + return; + } + + if (!monsterEntity.CanSeeInvisible) + { + // if target is player and he become invisible + if (monsterEntity.Target.IsInvisible()) + { + RemoveTarget(monsterEntity, monsterEntity.Target, true); + return; + } + } + + if (monsterEntity.Target.IsMonsterAggroDisabled()) + { + RemoveTarget(monsterEntity, monsterEntity.Target); + return; + } + + bool isInSeekRange = monsterEntity.Target.Position.IsInRange(monsterEntity.Position, monsterEntity.NoticeRange * 2); + bool isInBigSeekRange = monsterEntity.Target.Position.IsInRange(monsterEntity.Position, monsterEntity.NoticeRange * 3); + + if (!isInBigSeekRange && date < monsterEntity.AttentionTime - TimeSpan.FromSeconds(10)) + { + RemoveTarget(monsterEntity, monsterEntity.Target, true); + if (monsterEntity.Targets.Count != 0) + { + return; + } + + ForgetAll(monsterEntity, date, false); + return; + } + + if (!isInSeekRange && date < monsterEntity.AttentionTime - TimeSpan.FromSeconds(15)) + { + RemoveTarget(monsterEntity, monsterEntity.Target, true); + if (monsterEntity.Targets.Count != 0) + { + return; + } + + ForgetAll(monsterEntity, date, false); + return; + } + + if (date > monsterEntity.AttentionTime) + { + RemoveTarget(monsterEntity, monsterEntity.Target, true); + if (monsterEntity.Targets.Count != 0) + { + return; + } + + ForgetAll(monsterEntity, date, false); + return; + } + + if (!MovementPreChecks(monsterEntity)) + { + TryMoveToFirstPosition(monsterEntity, date); + return; + } + + short targetX = monsterEntity.Target.PositionX; + short targetY = monsterEntity.Target.PositionY; + + if (monsterEntity.IsRunningAway) + { + monsterEntity.AttentionTime = date + TimeSpan.FromSeconds(15); + + short newX; + short newY; + if (monsterEntity.PositionX == monsterEntity.Target.PositionX && monsterEntity.PositionY == monsterEntity.Target.PositionY) + { + newX = 0; + newY = 0; + } + else + { + newX = (short)(monsterEntity.PositionX + (monsterEntity.PositionX - targetX) * 50); + newY = (short)(monsterEntity.PositionY + (monsterEntity.PositionY - targetY) * 50); + } + + targetX = newX; + targetY = newY; + } + + ProcessMovement(monsterEntity, targetX, targetY); + } + + private void ShowEffect(IMonsterEntity monsterEntity, in DateTime date) + { + if (monsterEntity.IsBonus && (date - monsterEntity.LastBonusEffectTime).TotalSeconds >= 3) + { + monsterEntity.MapInstance.Broadcast(monsterEntity.GenerateEffectPacket((int)EffectType.TsBonus), new RangeBroadcast(monsterEntity.PositionX, monsterEntity.PositionY)); + monsterEntity.LastBonusEffectTime = date; + } + + if ((date - monsterEntity.LastEffect).TotalSeconds < 5) + { + return; + } + + if (monsterEntity.PermanentEffect != 0) + { + monsterEntity.BroadcastEffectInRange(monsterEntity.PermanentEffect); + } + + if (monsterEntity.IsTarget) + { + monsterEntity.MapInstance.Broadcast(monsterEntity.GenerateEffectPacket((int)EffectType.TsTarget)); + } + + monsterEntity.LastEffect = date; + } + + private void WalkAround(IMonsterEntity entity, in DateTime date) + { + if (!MovementPreChecks(entity)) + { + return; + } + + if (entity.ShouldFindNewTarget) + { + FindTarget(entity, date); + entity.ShouldFindNewTarget = false; + + if (entity.Target != null) + { + return; + } + + TryMoveToFirstPosition(entity, date); + ForgetAll(entity, date); + entity.NextTick += TimeSpan.FromMilliseconds(_randomGenerator.RandomNumber(1000)); + return; + } + + if (entity.GoToBossPosition.HasValue && entity.GoToBossPosition.Value != default) + { + short x = entity.GoToBossPosition.Value.X; + short y = entity.GoToBossPosition.Value.Y; + + if (entity.IsMonsterAggroDisabled(x, y)) + { + return; + } + + ProcessMovement(entity, x, y); + + if (entity.GoToBossPosition != null && entity.GoToBossPosition.Value == entity.Position) + { + entity.GoToBossPosition = null; + } + + return; + } + + if (entity.Waypoints != null) + { + if (entity.LastWayPoint > date) + { + return; + } + + Waypoint currentState = entity.Waypoints.TryGetValue(entity.CurrentWayPoint, out Waypoint waypoint) ? waypoint : null; + if (currentState == null) + { + entity.CurrentWayPoint = 0; + return; + } + + if (entity.PositionX == currentState.X && entity.PositionY == currentState.Y) + { + entity.CurrentWayPoint++; + entity.LastWayPoint = date.AddMilliseconds(currentState.WaitTime); + return; + } + + short stateMapX = currentState.X; + short stateMapY = currentState.Y; + + if (entity.MapInstance.IsBlockedZone(stateMapX, stateMapY)) + { + return; + } + + if (entity.IsMonsterAggroDisabled(stateMapX, stateMapY)) + { + return; + } + + ProcessMovement(entity, stateMapX, stateMapY); + return; + } + + short mapX = entity.FirstX; + short mapY = entity.FirstY; + + if (!entity.MapInstance.GetFreePosition(_randomGenerator, ref mapX, ref mapY, (byte)_randomGenerator.RandomNumber(0, 5), (byte)_randomGenerator.RandomNumber(0, 5))) + { + return; + } + + if (entity.MapInstance.IsBlockedZone(mapX, mapY)) + { + return; + } + + if (entity.IsMonsterAggroDisabled(mapX, mapY)) + { + return; + } + + ProcessMovement(entity, mapX, mapY); + } + + /// + /// Returns True if you can move and False if you can't + /// + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool MovementPreChecks(IMonsterEntity monsterEntity) + { + if (monsterEntity.BCardComponent.HasBCard(BCardType.Move, (byte)AdditionalTypes.Move.MovementImpossible)) + { + return false; + } + + return monsterEntity.IsMoving && monsterEntity.IsStillAlive && monsterEntity.CanWalk && monsterEntity.Speed > 0; + } + + private void ProcessRespawnLogic(IMonsterEntity monsterEntity, in DateTime date) + { + if (!monsterEntity.ShouldRespawn) + { + ForgetAll(monsterEntity, date); + monsterEntity.BroadcastDie(); + monsterEntity.MapInstance.RemoveMonster(monsterEntity); + return; + } + + if (date - monsterEntity.Death < monsterEntity.BaseRespawnTime) + { + return; + } + + if (monsterEntity.ModeIsHpTriggered) + { + monsterEntity.ModeIsActive = false; + } + else + { + ActivateMode(monsterEntity); + } + + ForgetAll(monsterEntity, date); + monsterEntity.Target = null; + monsterEntity.ModeDeathsSinceRespawn = _totalMonstersDeaths; + monsterEntity.SpawnDate = date; + monsterEntity.IsStillAlive = true; + monsterEntity.ReturningToFirstPosition = false; + monsterEntity.ShouldFindNewTarget = false; + monsterEntity.OnFirstDamageReceive = true; + monsterEntity.CancelCastingSkill(); + monsterEntity.ChargeComponent.ResetCharge(); + + monsterEntity.Hp = monsterEntity.MaxHp; + monsterEntity.Mp = monsterEntity.MaxMp; + monsterEntity.NextTick = date + TimeSpan.FromMilliseconds(_randomGenerator.RandomNumber(1000)); + monsterEntity.NextAttackReady = date; + monsterEntity.ChangePosition(new Position(monsterEntity.FirstX, monsterEntity.FirstY)); + monsterEntity.MapInstance.Broadcast(monsterEntity.GenerateIn(monsterEntity.MonsterRaceType != MonsterRaceType.Fixed)); + + _eventPipeline.ProcessEventAsync(new MonsterRespawnedEvent + { + Monster = monsterEntity + }).ConfigureAwait(false).GetAwaiter().GetResult(); + } + + private void ProcessMovement(IMonsterEntity entity, short mapX, short mapY) + { + switch (entity.ReturningToFirstPosition) + { + case true when entity.ReturnTimeOut > RETURN_TIME_OUT: + return; + case false: + entity.ReturnTimeOut = 0; + break; + } + + int speed = entity.Target != null || entity.ReturningToFirstPosition ? entity.Speed + entity.Speed / 2 : entity.Speed; + double speedIndexDefault = Math.Ceiling(speed * 0.4f); + float speedIndex = (float)(speedIndexDefault < 1 ? 1 : speedIndexDefault); + + Position position = _pathFinder.FindPath(entity.Position, new Position(mapX, mapY), speedIndex, + entity.MapInstance.Grid, entity.MapInstance.Width, entity.MapInstance.Height, entity.ReturningToFirstPosition); + + Position pos = position; + + if (pos.X < 0 || pos.Y < 0) + { + pos = entity.Position; + } + + if (pos == entity.Position && entity.ReturningToFirstPosition) + { + entity.ReturnTimeOut++; + } + + if (entity.Target != null && pos == entity.Target.Position) + { + IReadOnlyList getRandomCell = entity.Target.Position.GetNeighbors(entity.MapInstance.Grid, entity.MapInstance.Width, entity.MapInstance.Height); + if (getRandomCell.Count != 0) + { + pos = getRandomCell[_randomGenerator.RandomNumber(0, getRandomCell.Count)]; + } + + if (pos == entity.Target.Position || entity.MapInstance.IsBlockedZone(pos.X, pos.Y)) + { + pos = position; + } + } + + entity.ChangePosition(pos); + string packet = entity.GenerateMvPacket(speed); + entity.MapInstance.Broadcast(packet); + + if (!entity.ReturningToFirstPosition) + { + return; + } + + if (entity.Position.X != entity.FirstX || entity.Position.Y != entity.FirstY) + { + return; + } + + entity.ReturningToFirstPosition = false; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.CoreImpl/Maps/Systems/NpcSystem.cs b/srcs/_plugins/Plugin.CoreImpl/Maps/Systems/NpcSystem.cs new file mode 100644 index 0000000..9ab569d --- /dev/null +++ b/srcs/_plugins/Plugin.CoreImpl/Maps/Systems/NpcSystem.cs @@ -0,0 +1,1162 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading; +using PhoenixLib.Events; +using Plugin.CoreImpl.Pathfinding; +using WingsAPI.Packets.Enums.Rainbow; +using WingsEmu.Core.Extensions; +using WingsEmu.DTOs.Skills; +using WingsEmu.Game; +using WingsEmu.Game._ECS; +using WingsEmu.Game._ECS.Systems; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Entities.Extensions; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Extensions.Mates; +using WingsEmu.Game.Helpers; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Networking.Broadcasting; +using WingsEmu.Game.Npcs.Event; +using WingsEmu.Game.Skills; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Battle; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.CoreImpl.Maps.Systems +{ + public sealed class NpcSystem : IMapSystem, INpcSystem + { + private const int TICK_DELAY_MILLISECONDS = 100; + private const int RETURN_TIME_OUT = 5; + + private static readonly TimeSpan _refreshRate = TimeSpan.FromMilliseconds(TICK_DELAY_MILLISECONDS); + + private readonly BCardTickSystem _bCardTickSystem; + private readonly IBuffFactory _buffFactory; + private readonly IAsyncEventPipeline _eventPipeline; + private readonly IGameLanguageService _gameLanguage; + + private readonly ReaderWriterLockSlim _lock = new(LockRecursionPolicy.SupportsRecursion); + private readonly IMapInstance _mapInstance; + private readonly IMonsterTalkingConfig _monsterTalkingConfig; + private readonly List _npcs = new(); + private readonly ConcurrentDictionary _npcsById = new(); + private readonly IPathFinder _pathFinder; + private readonly IRandomGenerator _randomGenerator; + private readonly ConcurrentQueue _toAddNpcs = new(); + private readonly ConcurrentQueue _toRemoveNpcs = new(); + private DateTime _lastProcess = DateTime.MinValue; + + public NpcSystem(IBCardEffectHandlerContainer bcardHandlers, IMapInstance mapInstance, + IAsyncEventPipeline eventPipeline, IRandomGenerator randomGenerator, IBuffFactory buffFactory, IGameLanguageService gameLanguage, + IPathFinder pathFinder, IMonsterTalkingConfig monsterTalkingConfig) + { + _mapInstance = mapInstance; + _eventPipeline = eventPipeline; + _randomGenerator = randomGenerator; + _buffFactory = buffFactory; + _gameLanguage = gameLanguage; + _pathFinder = pathFinder; + _monsterTalkingConfig = monsterTalkingConfig; + _bCardTickSystem = new BCardTickSystem(bcardHandlers, _randomGenerator, _buffFactory, _gameLanguage); + } + + public void PutIdleState() + { + _bCardTickSystem.Clear(); + } + + public void Clear() + { + _lock.EnterWriteLock(); + try + { + _npcs.Clear(); + _toRemoveNpcs.Clear(); + _toAddNpcs.Clear(); + _npcsById.Clear(); + } + finally + { + _lock.ExitWriteLock(); + } + } + + public string Name => nameof(NpcSystem); + + public void ProcessTick(DateTime date, bool isTickRefresh = false) + { + if (_lastProcess + _refreshRate > date) + { + return; + } + + _lastProcess = date; + + _lock.EnterWriteLock(); + try + { + while (_toRemoveNpcs.TryDequeue(out INpcEntity toRemove)) + { + RemoveTarget(toRemove, date); + _npcsById.TryRemove(toRemove.Id, out _); + _npcs.Remove(toRemove); + } + + while (_toAddNpcs.TryDequeue(out INpcEntity toAdd)) + { + _npcsById[toAdd.Id] = toAdd; + _npcs.Add(toAdd); + } + } + finally + { + _lock.ExitWriteLock(); + } + + foreach (INpcEntity monster in _npcs) + { + Update(date, monster, isTickRefresh); + } + } + + public void NpcRefreshTarget(INpcEntity npcEntity, IBattleEntity target) + { + if (npcEntity.Damagers.Contains(target)) + { + return; + } + + if (!npcEntity.CanSeeInvisible && target.IsInvisible()) + { + return; + } + + npcEntity.NextTick -= TimeSpan.FromMilliseconds(800); + AggroLogic(npcEntity, target); + } + + public IReadOnlyList GetAliveNpcs() + { + _lock.EnterReadLock(); + try + { + return _npcs.FindAll(s => s != null && s.IsAlive() && s.IsStillAlive && s.CanAttack && s.MonsterRaceType != MonsterRaceType.Other && s.MonsterRaceType != MonsterRaceType.Fixed); + } + finally + { + _lock.ExitReadLock(); + } + } + + public IReadOnlyList GetAliveNpcs(Func predicate) + { + _lock.EnterReadLock(); + try + { + return _npcs.FindAll(s => s != null && s.IsAlive() && s.IsStillAlive && s.CanAttack + && s.MonsterRaceType != MonsterRaceType.Other && s.MonsterRaceType != MonsterRaceType.Fixed && (predicate == null || predicate(s))); + } + finally + { + _lock.ExitReadLock(); + } + } + + + public IReadOnlyList GetPassiveNpcs() + { + _lock.EnterReadLock(); + try + { + return _npcs.FindAll(s => s != null && (s.CanAttack == false || s.MonsterRaceType is MonsterRaceType.Other or MonsterRaceType.Fixed)); + } + finally + { + _lock.ExitReadLock(); + } + } + + public IReadOnlyList GetAliveNpcsInRange(Position pos, short distance, Func predicate) + { + _lock.EnterReadLock(); + try + { + return _npcs.FindAll(s => + s != null && s.IsAlive() && s.IsStillAlive && s.CanAttack + && s.MonsterRaceType != MonsterRaceType.Other + && s.MonsterRaceType != MonsterRaceType.Fixed + && pos.IsInAoeZone(s.Position, distance) + && (predicate == null || predicate(s)) + ); + } + finally + { + _lock.ExitReadLock(); + } + } + + public IReadOnlyList GetAliveNpcsInRange(Position pos, short distance) => GetAliveNpcsInRange(pos, distance, null); + + public IReadOnlyList GetClosestNpcsInRange(Position pos, short distance) + { + _lock.EnterReadLock(); + try + { + List toReturn = _npcs.FindAll(s => + s != null && s.IsAlive() && s.IsStillAlive && s.CanAttack + && s.MonsterRaceType != MonsterRaceType.Other + && s.MonsterRaceType != MonsterRaceType.Fixed + && pos.IsInAoeZone(s.Position, distance)); + toReturn.Sort((prev, next) => prev.Position.GetDistance(pos) - next.Position.GetDistance(pos)); + + return toReturn; + } + finally + { + _lock.ExitReadLock(); + } + } + + public INpcEntity GetNpcById(long id) => _npcsById.GetOrDefault(id); + + public void AddNpc(INpcEntity entity) + { + _toAddNpcs.Enqueue(entity); + } + + public void RemoveNpc(INpcEntity entity) + { + _toRemoveNpcs.Enqueue(entity); + } + + private void Update(in DateTime date, INpcEntity npcEntity, bool isTickRefresh) + { + if (!npcEntity.IsStillAlive) + { + ProcessRespawnLogic(npcEntity, date); + return; + } + + if (npcEntity.SpawnDate.AddMilliseconds(500) > date) + { + return; + } + + if (!npcEntity.IsAlive()) + { + return; + } + + ShowEffect(npcEntity, date); + _bCardTickSystem.ProcessUpdate(npcEntity, date); + ProcessRecurrentLifeDecrease(npcEntity, date); + ProcessCollection(npcEntity, date); + TryHealInTimeSpace(npcEntity, date); + + if (_mapInstance.AIDisabled) + { + return; + } + + if (npcEntity.CharacterPartnerId.HasValue) + { + return; + } + + if (npcEntity.IsCastingSkill) + { + IBattleEntity entity = npcEntity.MapInstance.GetBattleEntity(npcEntity.LastAttackedEntity.Item1, npcEntity.LastAttackedEntity.Item2); + if (entity == null) + { + npcEntity.CancelCastingSkill(); + } + + return; + } + + if (npcEntity.NextTick > date) + { + return; + } + + FindCharacterAsPartner(npcEntity); + RefreshTarget(npcEntity, date); + TryRunAway(npcEntity, date); + TryTalk(npcEntity); + + if (npcEntity.IsApproachingTarget) + { + npcEntity.IsApproachingTarget = false; + ApproachTarget(npcEntity, date); + npcEntity.NextTick = (isTickRefresh ? date : npcEntity.NextTick) + TimeSpan.FromMilliseconds(1000); + return; + } + + if (npcEntity.FindNewPositionAroundTarget) + { + npcEntity.FindNewPositionAroundTarget = false; + npcEntity.NextTick = (isTickRefresh ? date : npcEntity.NextTick) + TimeSpan.FromMilliseconds(200); + + if (npcEntity.Target == null) + { + return; + } + + short randomX = (short)(npcEntity.Target.Position.X + _randomGenerator.RandomNumber(-1, 2)); + short randomY = (short)(npcEntity.Target.Position.Y + _randomGenerator.RandomNumber(-1, 2)); + + if (randomX == npcEntity.Position.X && randomY == npcEntity.Position.Y) + { + return; + } + + if (randomX == npcEntity.Target.Position.X && randomY == npcEntity.Target.PositionY) + { + npcEntity.FindNewPositionAroundTarget = true; + return; + } + + if (!MovementPreChecks(npcEntity)) + { + return; + } + + if (npcEntity.MapInstance.IsBlockedZone(randomX, randomY)) + { + return; + } + + ProcessMovement(npcEntity, randomX, randomY); + return; + } + + if (npcEntity.Target == null) + { + int random = _randomGenerator.RandomNumber(); + bool move = random <= 60; + + if (move || npcEntity.ReturningToFirstPosition) + { + WalkAround(npcEntity, date); + } + + npcEntity.NextTick = (isTickRefresh ? date + TimeSpan.FromMilliseconds(_randomGenerator.RandomNumber(1000)) : npcEntity.NextTick) + TimeSpan.FromMilliseconds(1000); + return; + } + + TryFight(date, npcEntity, isTickRefresh); + } + + private void TryRunAway(INpcEntity npcEntity, in DateTime date) + { + if (!npcEntity.IsRunningAway) + { + return; + } + + ApproachTarget(npcEntity, date); + npcEntity.NextTick += TimeSpan.FromMilliseconds(1000); + } + + private void TryTalk(INpcEntity npcEntity) + { + if (!_monsterTalkingConfig.HasPossibleMessages(npcEntity.MonsterVNum)) + { + return; + } + + if (_randomGenerator.RandomNumber() > 5) + { + return; + } + + IReadOnlyList messages = _monsterTalkingConfig.PossibleMessage(npcEntity.MonsterVNum); + if (messages == null) + { + return; + } + + if (messages.Count < 1) + { + return; + } + + string message = messages[_randomGenerator.RandomNumber(messages.Count)]; + npcEntity.MapInstance.Broadcast(x => npcEntity.GenerateSayPacket(x.GetLanguage(message), ChatMessageColorType.PlayerSay), + new RangeBroadcast(npcEntity.PositionX, npcEntity.PositionY, 30)); + } + + private void TryHealInTimeSpace(INpcEntity npcEntity, in DateTime date) + { + if (npcEntity.MapInstance.MapInstanceType != MapInstanceType.TimeSpaceInstance) + { + return; + } + + if (!npcEntity.IsAlive()) + { + return; + } + + if (!npcEntity.IsProtected) + { + return; + } + + if (npcEntity.LastTimeSpaceHeal.AddSeconds(2) > date) + { + return; + } + + npcEntity.LastTimeSpaceHeal = date; + + int hpToHeal = (int)(npcEntity.MaxHp * 0.01); + int mpToHeal = (int)(npcEntity.MaxMp * 0.01); + + if (npcEntity.Hp + hpToHeal < npcEntity.MaxHp) + { + npcEntity.Hp += hpToHeal; + } + else + { + npcEntity.Hp = npcEntity.MaxHp; + } + + if (npcEntity.Mp + mpToHeal < npcEntity.MaxMp) + { + npcEntity.Mp += mpToHeal; + } + else + { + npcEntity.Mp = npcEntity.MaxMp; + } + + foreach (IClientSession session in npcEntity.MapInstance.Sessions) + { + session.SendPacket(npcEntity.GenerateStPacket()); + } + } + + private void FindCharacterAsPartner(INpcEntity npcEntity) + { + if (npcEntity.MapInstance.MapInstanceType != MapInstanceType.TimeSpaceInstance) + { + return; + } + + if (!npcEntity.IsTimeSpaceMate) + { + return; + } + + if (npcEntity.CharacterPartnerId.HasValue) + { + return; + } + + IPlayerEntity getClosestCharacter = npcEntity.MapInstance.GetClosestCharactersInRange(npcEntity.Position, npcEntity.NoticeRange).FirstOrDefault(); + if (getClosestCharacter == null) + { + return; + } + + if (getClosestCharacter.TimeSpaceComponent.Partners.Any(x => x.MonsterVNum == npcEntity.MonsterVNum)) + { + return; + } + + npcEntity.CharacterPartnerId = getClosestCharacter.Id; + getClosestCharacter.TimeSpaceComponent.Partners.Add(npcEntity); + getClosestCharacter.Session.SendNpcEffect(npcEntity, EffectType.PetPickUp); + getClosestCharacter.Session.SendMateControl(npcEntity); + getClosestCharacter.Session.SendCondMate(npcEntity); + } + + private void ProcessCollection(INpcEntity entity, in DateTime date) + { + if (entity.MonsterRaceType != MonsterRaceType.Fixed && entity.MonsterRaceSubType != 7) + { + return; + } + + if (entity.CurrentCollection >= entity.MaxTries) + { + return; + } + + if (entity.LastCollection > date) + { + return; + } + + entity.LastCollection = date.AddSeconds(entity.CollectionCooldown); + entity.CurrentCollection++; + } + + + private void ProcessRecurrentLifeDecrease(INpcEntity entity, DateTime date) + { + if (entity.LastSpecialHpDecrease.AddSeconds(1) > date) + { + return; + } + + entity.LastSpecialHpDecrease = date; + if (entity.DisappearAfterSeconds) + { + int hpToDecrease = entity.MaxHp / (entity.MaxHp / 5); + entity.Hp -= hpToDecrease; + if (entity.Hp > 0) + { + return; + } + + _eventPipeline.ProcessEventAsync(new MapNpcGenerateDeathEvent(entity, null)); + return; + } + + if (!entity.DisappearAfterSecondsMana) + { + return; + } + + int toRemove = entity.MaxMp / (entity.MaxMp / 10); + entity.Mp -= toRemove; + if (entity.Mp > 0) + { + return; + } + + _eventPipeline.ProcessEventAsync(new MapNpcGenerateDeathEvent(entity, null)); + } + + + private void RemoveTarget(INpcEntity npcEntity, in DateTime time) + { + if (npcEntity.Target == null) + { + return; + } + + npcEntity.Damagers.Remove(npcEntity.Target); + npcEntity.Target = null; + npcEntity.IsApproachingTarget = false; + npcEntity.ShouldFindNewTarget = true; + } + + private void RefreshTarget(INpcEntity npcEntity, in DateTime date) + { + // if monster is going back to his own position + if (npcEntity.ReturningToFirstPosition) + { + return; + } + + // if monster have a lot of damagers -> find nearest target + if (npcEntity.Damagers.Count > 0) + { + IBattleEntity oldTarget = npcEntity.Target; + IBattleEntity nearestTarget = npcEntity.Damagers.OrderBy(e => npcEntity.Position.GetDistance(e.Position)) + .FirstOrDefault(x => npcEntity.IsEnemyWith(x) && x.IsAlive()); + + if (oldTarget != null && nearestTarget != null && oldTarget.Id != nearestTarget.Id && oldTarget.Type != nearestTarget.Type) + { + RemoveTarget(npcEntity, date); + } + + npcEntity.Target = nearestTarget; + } + + // if someone attack monster + if (npcEntity.Target != null) + { + // if target is on diffrent map + // if target is dead + if (npcEntity.Target.MapInstance?.Id != npcEntity.MapInstance?.Id || !npcEntity.Target.IsAlive()) + { + RemoveTarget(npcEntity, date); + return; + } + } + + // if monster is agressive, find target || looking for whether or not his companions were attacked + if (npcEntity.IsHostile || npcEntity.GroupAttack != (int)GroupAttackType.None) + { + FindTarget(npcEntity, date); + return; + } + + // if nobody attack him + if (npcEntity.Target == null && npcEntity.Damagers.Count == 0) + { + return; + } + + if (npcEntity.Target != null) + { + return; + } + + FindTarget(npcEntity, date); + } + + private void FindTarget(INpcEntity npcEntity, in DateTime time) + { + IBattleEntity target = null; + + if (npcEntity.IsHostile) + { + target = HostileFinding(npcEntity); + } + + if (target == null) + { + return; + } + + NpcRefreshTarget(npcEntity, target); + } + + private IBattleEntity HostileFinding(INpcEntity npcEntity) + { + byte noticeRange = npcEntity.NoticeRange; + IEnumerable targets = npcEntity.MapInstance.GetAliveMonstersInRange(npcEntity.Position, noticeRange); + return BasicTargetChecks(npcEntity, targets); + } + + private IBattleEntity BasicTargetChecks(INpcEntity npcEntity, IEnumerable targets) + { + targets = targets.Where(e => + { + if (!npcEntity.CanHit(e) || !e.IsAlive() || npcEntity.IsAllyWith(e)) + { + return false; + } + + return npcEntity.CanSeeInvisible || !e.IsInvisible(); + }); + + return targets.OrderBy(npcEntity.GetDistance).FirstOrDefault(); + } + + private void AggroLogic(INpcEntity npcEntity, IBattleEntity target) + { + npcEntity.Target = target; + npcEntity.Damagers.Add(target); + } + + private void ForgetAll(INpcEntity npcEntity, in DateTime time) + { + if (npcEntity.Target != null) + { + RemoveTarget(npcEntity, time); + } + + npcEntity.LastSkill = DateTime.MinValue; + npcEntity.Damagers.Clear(); + } + + private void TryFight(in DateTime date, INpcEntity npcEntity, bool isTickRefresh) + { + if (npcEntity == null) + { + return; + } + + if (!npcEntity.IsAlive()) + { + return; + } + + if (!npcEntity.IsStillAlive) + { + return; + } + + if (npcEntity.Target == null) + { + return; + } + + if (npcEntity.IsRunningAway) + { + return; + } + + if (!npcEntity.Target.IsAlive()) + { + RemoveTarget(npcEntity, date); + npcEntity.NextTick = (isTickRefresh ? date : npcEntity.NextTick) + TimeSpan.FromMilliseconds(200); + return; + } + + if (!npcEntity.CanSeeInvisible) + { + // if target is player and he become invisible + if (npcEntity.Target.IsInvisible()) + { + RemoveTarget(npcEntity, date); + npcEntity.NextTick = (isTickRefresh ? date : npcEntity.NextTick) + TimeSpan.FromMilliseconds(200); + return; + } + } + + if (!npcEntity.CanHit(npcEntity.Target)) + { + RemoveTarget(npcEntity, date); + npcEntity.NextTick = (isTickRefresh ? date : npcEntity.NextTick) + TimeSpan.FromMilliseconds(200); + return; + } + + IReadOnlyList monsterSkills = npcEntity.NotBasicSkills; + + IBattleEntitySkill dashSkill = null; + if (npcEntity.HasDash) + { + dashSkill = npcEntity.DashSkill; + monsterSkills = npcEntity.SkillsWithoutDashSkill; + } + + int count = monsterSkills.Count; + IBattleEntitySkill getRandomSkill = count != 0 ? monsterSkills[_randomGenerator.RandomNumber(0, count)] : null; + + bool randomSkill = _randomGenerator.RandomNumber() <= getRandomSkill?.Rate; + + // Find basic skill that replace ZSKILL + IBattleEntitySkill replacedBasic = npcEntity.ReplacedBasicSkill; + IBattleEntitySkill skillToUse = randomSkill ? getRandomSkill : replacedBasic; + SkillInfo skillInfo = skillToUse?.Skill.GetInfo(battleEntity: npcEntity) ?? npcEntity.BasicSkill; + + bool randomSkillCantBeUsed = skillToUse != null && !npcEntity.SkillCanBeUsed(skillToUse, date) && skillInfo.Vnum != 0 && skillToUse != replacedBasic; + + if (randomSkillCantBeUsed) + { + skillToUse = replacedBasic; + skillInfo = skillToUse?.Skill.GetInfo(battleEntity: npcEntity) ?? npcEntity.BasicSkill; + } + + short effectiveRange = skillInfo.Range == 0 || skillInfo.TargetType == TargetType.Self ? skillInfo.AoERange : skillInfo.Range; + + bool isInRange = npcEntity.IsInRange(npcEntity.Target.PositionX, npcEntity.Target.PositionY, (byte)effectiveRange); + + bool shouldWalk = skillInfo.TargetAffectedEntities != + TargetAffectedEntities.BuffForAllies; //skillInfo.TargetType == TargetType.Target && skillInfo.TargetAffectedEntities != TargetAffectedEntities.BuffForAllies || + //skillInfo.TargetType == TargetType.Self && skillInfo.HitType is TargetHitType.AlliesInAffectedAoE or TargetHitType.EnemiesInAffectedAoE; + + if (!isInRange && shouldWalk) + { + if (npcEntity.HasDash && dashSkill != null) + { + skillToUse = dashSkill; + skillInfo = dashSkill.Skill.GetInfo(); + + effectiveRange = skillInfo.Range == 0 || skillInfo.TargetType == TargetType.Self ? skillInfo.AoERange : skillInfo.Range; + if (!npcEntity.SkillCanBeUsed(skillToUse, date) || !npcEntity.CanPerformAttack() + || !npcEntity.IsInRange(npcEntity.Target.PositionX, npcEntity.Target.PositionY, (byte)effectiveRange) + && skillInfo.TargetAffectedEntities != TargetAffectedEntities.BuffForAllies) + { + npcEntity.IsApproachingTarget = true; + npcEntity.NextTick = date; + return; + } + + if (_randomGenerator.RandomNumber() > skillToUse.Rate) + { + npcEntity.IsApproachingTarget = true; + npcEntity.NextTick = date; + return; + } + } + else + { + npcEntity.IsApproachingTarget = true; + npcEntity.NextTick = date; + return; + } + } + + npcEntity.LastSkill = date; + + if (npcEntity.NextAttackReady > date) + { + return; + } + + if (getRandomSkill != null && getRandomSkill != replacedBasic && !randomSkill && npcEntity.SkillCanBeUsed(getRandomSkill, date)) + { + npcEntity.SetSkillCooldown(getRandomSkill.Skill.GetInfo()); + } + + int random = _randomGenerator.RandomNumber(); + if (npcEntity.BasicHitChance == 0 || random >= npcEntity.BasicHitChance * 20) + { + if (skillToUse is null or INpcMonsterSkill { IsIgnoringHitChance: false }) + { + npcEntity.NextTick = (isTickRefresh ? date : npcEntity.NextTick) + TimeSpan.FromMilliseconds(200); + return; + } + } + + IBattleEntity skillTarget = npcEntity.Target; + + (int firstData, int secondData) cooldownToIncrease = (0, 0); + (int firstData, int secondData) cooldownToDecrease = (0, 0); + + if (npcEntity.BCards.Any()) + { + cooldownToIncrease = npcEntity.BCardComponent.GetAllBCardsInformation(BCardType.Mode, (byte)AdditionalTypes.Mode.AttackTimeIncreased, npcEntity.Level); + cooldownToDecrease = npcEntity.BCardComponent.GetAllBCardsInformation(BCardType.Mode, (byte)AdditionalTypes.Mode.AttackTimeDecreased, npcEntity.Level); + } + + int basicCooldown = npcEntity.BasicCooldown; + + if (cooldownToIncrease.firstData != 0) + { + basicCooldown += cooldownToIncrease.firstData; + } + + if (cooldownToDecrease.firstData != 0) + { + basicCooldown -= cooldownToDecrease.firstData; + } + + int tickToAdd = (2 + npcEntity.BasicCastTime + 2 * basicCooldown) * 100; + tickToAdd = tickToAdd < 800 ? 800 : tickToAdd; + npcEntity.NextAttackReady = date + TimeSpan.FromMilliseconds(tickToAdd); + + if (replacedBasic != null && skillInfo.Vnum == replacedBasic.Skill.Id && !npcEntity.SkillCanBeUsed(replacedBasic, date)) + { + return; + } + + if (!npcEntity.CanPerformAttack()) + { + return; + } + + if (!npcEntity.Target.IsAlive()) + { + RemoveTarget(npcEntity, date); + npcEntity.NextTick = (isTickRefresh ? date : npcEntity.NextTick) + TimeSpan.FromMilliseconds(200); + return; + } + + if (skillInfo.Vnum != 0 && skillInfo.TargetType == TargetType.Self) + { + skillTarget = npcEntity; + } + + Position positionAfterDash = default; + if (skillInfo.AttackType == AttackType.Dash && !npcEntity.MapInstance.IsBlockedZone(npcEntity.Target.Position.X, npcEntity.Target.Position.Y) + && npcEntity.Position.IsInRange(npcEntity.Target.Position, skillInfo.Range + 2)) + { + positionAfterDash = npcEntity.Target.Position; + } + + npcEntity.RemoveEntityMp((short)skillInfo.ManaCost, skillToUse?.Skill); + npcEntity.LastSkill = npcEntity.GenerateSkillCastTime(skillInfo) + TimeSpan.FromMilliseconds(npcEntity.ApplyCooldownReduction(skillInfo) * 100); + npcEntity.LastAttackedEntity = (skillTarget.Type, skillTarget.Id); + npcEntity.EmitEvent(new BattleExecuteSkillEvent(npcEntity, skillTarget, skillInfo, npcEntity.GenerateSkillCastTime(skillInfo), positionAfterDash)); + } + + private void TryMoveToFirstPosition(INpcEntity npcEntity, in DateTime date) + { + if (npcEntity.Target != null) + { + return; + } + + if (!npcEntity.IsStillAlive) + { + return; + } + + if (!npcEntity.IsAlive()) + { + return; + } + + if (!npcEntity.CanWalk) + { + return; + } + + if (!MovementPreChecks(npcEntity)) + { + return; + } + + if (npcEntity.ReturningToFirstPosition) + { + return; + } + + npcEntity.ReturningToFirstPosition = true; + ProcessMovement(npcEntity, npcEntity.FirstX, npcEntity.FirstY); + } + + private void ApproachTarget(INpcEntity npcEntity, DateTime date) + { + if (npcEntity.Target == null) + { + return; + } + + if (!MovementPreChecks(npcEntity)) + { + TryMoveToFirstPosition(npcEntity, date); + return; + } + + if (!npcEntity.CanSeeInvisible) + { + // if target is player and he become invisible + if (npcEntity.Target.IsInvisible()) + { + RemoveTarget(npcEntity, date); + return; + } + } + + if (npcEntity.LastSkill != DateTime.MinValue && npcEntity.LastSkill.AddSeconds(15) <= date) + { + RemoveTarget(npcEntity, date); + return; + } + + short targetX = npcEntity.Target.PositionX; + short targetY = npcEntity.Target.PositionY; + + if (npcEntity.IsRunningAway) + { + short newX; + short newY; + if (npcEntity.PositionX == npcEntity.Target.PositionX && npcEntity.PositionY == npcEntity.Target.PositionY) + { + newX = 0; + newY = 0; + } + else + { + newX = (short)(npcEntity.PositionX + (npcEntity.PositionX - targetX) * 50); + newY = (short)(npcEntity.PositionY + (npcEntity.PositionY - targetY) * 50); + } + + targetX = newX; + targetY = newY; + } + + ProcessMovement(npcEntity, targetX, targetY); + } + + private void ShowEffect(INpcEntity entity, DateTime date) + { + if (entity.LastEffect.AddSeconds(5) > date) + { + return; + } + + if (entity.IsTimeSpaceMate || entity.IsProtected) + { + entity.MapInstance.Broadcast(entity.GenerateEffectPacket(EffectType.TsMate), new RangeBroadcast(entity.PositionX, entity.PositionY)); + } + + if (entity.Effect > 0) + { + entity.MapInstance.Broadcast(entity.GenerateEffectPacket(entity.Effect), new RangeBroadcast(entity.PositionX, entity.PositionY)); + } + + if (entity.MapInstance.MapInstanceType == MapInstanceType.RainbowBattle && entity.RainbowFlag != null) + { + RainbowBattleFlagTeamType teamFlag = entity.RainbowFlag.FlagTeamType; + EffectType effectType = teamFlag switch + { + RainbowBattleFlagTeamType.None => EffectType.NoneFlag, + RainbowBattleFlagTeamType.Red => EffectType.RedFlag, + RainbowBattleFlagTeamType.Blue => EffectType.BlueFlag, + _ => EffectType.NoneFlag + }; + + entity.MapInstance.Broadcast(entity.GenerateEffectPacket(effectType)); + } + + entity.LastEffect = date; + } + + + private void WalkAround(INpcEntity entity, in DateTime date) + { + if (!MovementPreChecks(entity)) + { + return; + } + + if (entity.ShouldFindNewTarget) + { + FindTarget(entity, date); + entity.ShouldFindNewTarget = false; + + if (entity.Target != null) + { + return; + } + + TryMoveToFirstPosition(entity, date); + ForgetAll(entity, date); + return; + } + + short mapX = entity.FirstX; + short mapY = entity.FirstY; + + if (!entity.MapInstance.GetFreePosition(_randomGenerator, ref mapX, ref mapY, (byte)_randomGenerator.RandomNumber(0, 5), (byte)_randomGenerator.RandomNumber(0, 5))) + { + return; + } + + if (entity.MapInstance.IsBlockedZone(mapX, mapY)) + { + return; + } + + ProcessMovement(entity, mapX, mapY); + } + + /// + /// Returns True if you can move and False if you can't + /// + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool MovementPreChecks(INpcEntity npcEntity) + { + if (npcEntity.BCardComponent.HasBCard(BCardType.Move, (byte)AdditionalTypes.Move.MovementImpossible)) + { + return false; + } + + if (!npcEntity.IsStillAlive) + { + return false; + } + + if (npcEntity.Speed <= 0) + { + return false; + } + + if (npcEntity.IsProtected || npcEntity.IsTimeSpaceMate) + { + return true; + } + + if (npcEntity.CanMove) + { + return true; + } + + return npcEntity.IsMoving && npcEntity.CanWalk; + } + + private void ProcessRespawnLogic(INpcEntity npcEntity, in DateTime date) + { + if (!npcEntity.ShouldRespawn) + { + npcEntity.MapInstance.Broadcast(npcEntity.GenerateOut()); + npcEntity.MapInstance.RemoveNpc(npcEntity); + return; + } + + if (!(date - npcEntity.Death >= npcEntity.BaseRespawnTime)) + { + ForgetAll(npcEntity, date); + return; + } + + npcEntity.SpawnDate = date; + npcEntity.IsStillAlive = true; + npcEntity.ReturningToFirstPosition = false; + npcEntity.Hp = npcEntity.MaxHp; + npcEntity.Mp = npcEntity.MaxMp; + npcEntity.NextTick = date + TimeSpan.FromMilliseconds(_randomGenerator.RandomNumber(1000)); + npcEntity.NextAttackReady = date; + npcEntity.CancelCastingSkill(); + npcEntity.ChangePosition(new Position(npcEntity.FirstX, npcEntity.FirstY)); + npcEntity.CurrentCollection = npcEntity.MaxTries; + npcEntity.MapInstance.Broadcast(npcEntity.GenerateIn(npcEntity.MonsterRaceType != MonsterRaceType.Fixed)); + } + + private void ProcessMovement(INpcEntity entity, short mapX, short mapY) + { + switch (entity.ReturningToFirstPosition) + { + case true when entity.ReturnTimeOut > RETURN_TIME_OUT: + return; + case false: + entity.ReturnTimeOut = 0; + break; + } + + int speed = entity.Target != null || entity.ReturningToFirstPosition ? entity.Speed + entity.Speed / 2 : entity.Speed; + double speedIndexDefault = Math.Ceiling(speed * 0.4f); + float speedIndex = (float)(speedIndexDefault < 1 ? 1 : speedIndexDefault); + + Position position = _pathFinder.FindPath(entity.Position, new Position(mapX, mapY), speedIndex, + entity.MapInstance.Grid, entity.MapInstance.Width, entity.MapInstance.Height, entity.ReturningToFirstPosition); + + Position pos = position; + + if (pos.X < 0 || pos.Y < 0) + { + pos = entity.Position; + } + + if (pos == entity.Position && entity.ReturningToFirstPosition) + { + entity.ReturnTimeOut++; + } + + if (entity.Target != null && pos == entity.Target.Position) + { + IReadOnlyList getRandomCell = entity.Target.Position.GetNeighbors(entity.MapInstance.Grid, entity.MapInstance.Width, entity.MapInstance.Height); + if (getRandomCell.Count != 0) + { + pos = getRandomCell[_randomGenerator.RandomNumber(0, getRandomCell.Count)]; + } + + if (pos == entity.Target.Position || entity.MapInstance.IsBlockedZone(pos.X, pos.Y)) + { + pos = position; + } + } + + entity.ChangePosition(pos); + string packet = entity.GenerateMvPacket(speed); + entity.MapInstance.Broadcast(packet); + + if (!entity.ReturningToFirstPosition) + { + return; + } + + if (entity.Position.X != entity.FirstX || entity.Position.Y != entity.FirstY) + { + return; + } + + entity.ReturningToFirstPosition = false; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.CoreImpl/Maps/Systems/SkillCooldownSystem.cs b/srcs/_plugins/Plugin.CoreImpl/Maps/Systems/SkillCooldownSystem.cs new file mode 100644 index 0000000..ca8b699 --- /dev/null +++ b/srcs/_plugins/Plugin.CoreImpl/Maps/Systems/SkillCooldownSystem.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Extensions.Mates; +using WingsEmu.Game.Skills; +using WingsEmu.Packets.Enums; + +namespace Plugin.CoreImpl.Maps.Systems +{ + public sealed class SkillCooldownSystem + { + public void Update(IPlayerEntity character, in DateTime date) + { + UpdateCharacterCooldowns(character, date); + } + + private void UpdateCharacterCooldowns(IPlayerEntity character, in DateTime date) + { + if (character.SkillCooldowns.IsEmpty && character.MatesSkillCooldowns.IsEmpty) + { + return; + } + + var toProcess = new List<(DateTime time, short castId)>(); + var toProcessMates = new List<(DateTime time, short castId, MateType matetype)>(); + + while (character.SkillCooldowns.TryDequeue(out (DateTime time, short castId) cooldown)) + { + if (cooldown.time > date) + { + toProcess.Add((cooldown.time, cooldown.castId)); + continue; + } + + character.Session.SendSkillCooldownReset(cooldown.castId); + } + + while (character.MatesSkillCooldowns.TryDequeue(out (DateTime time, short castId, MateType mateType) cooldown)) + { + if (cooldown.time > date) + { + toProcessMates.Add((cooldown.time, cooldown.castId, cooldown.mateType)); + continue; + } + + switch (cooldown.mateType) + { + case MateType.Partner: + character.Session.SendPartnerSkillCooldown(cooldown.castId); + break; + case MateType.Pet: + character.Session.SendMateSkillCooldownReset(); + break; + } + } + + foreach ((DateTime time, short castId) in toProcess) + { + character.AddSkillCooldown(time, castId); + } + + foreach ((DateTime time, short castId, MateType mateType) in toProcessMates) + { + character.AddMateSkillCooldown(time, castId, mateType); + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.CoreImpl/Maps/Systems/SnackFoodSystem.cs b/srcs/_plugins/Plugin.CoreImpl/Maps/Systems/SnackFoodSystem.cs new file mode 100644 index 0000000..31754e7 --- /dev/null +++ b/srcs/_plugins/Plugin.CoreImpl/Maps/Systems/SnackFoodSystem.cs @@ -0,0 +1,381 @@ +using System; +using WingsAPI.Data.Families; +using WingsAPI.Packets.Enums.Shells; +using WingsEmu.Core.Extensions; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Mates.Events; +using WingsEmu.Game.SnackFood; +using WingsEmu.Game.SnackFood.Events; +using WingsEmu.Packets.Enums; + +namespace Plugin.CoreImpl.Maps.Systems +{ + public sealed class SnackFoodSystem + { + private const double _bufferFraction = 0.2; + + private static readonly TimeSpan _tickDelay = TimeSpan.FromSeconds(2); + private readonly GameMinMaxConfiguration _configuration; + + public SnackFoodSystem(GameMinMaxConfiguration configuration) => _configuration = configuration; + + public void ProcessUpdate(IPlayerEntity character, DateTime time) + { + ProcessFood(character, time); + ProcessSnack(character, time); + ProcessAdditionalFood(character, time); + ProcessAdditionalSnack(character, time); + } + + private void ProcessFood(IPlayerEntity character, in DateTime date) + { + FoodProgress progress = character.GetFoodProgress; + if (progress == null) + { + return; + } + + if (!character.IsSitting) + { + character.ClearFoodBuffer(); + return; + } + + if (progress.LastTick + _tickDelay * progress.IncreaseTick > date) + { + return; + } + + int hp = 0; + int mp = 0; + int sp = 0; + + if (progress.FoodHpBuffer > 0) + { + hp = (int)(progress.FoodHpBufferSize * _bufferFraction); + progress.FoodHpBuffer -= hp; + } + else + { + progress.FoodHpBufferSize = 0; + progress.FoodHpBuffer = 0; + } + + if (progress.FoodMpBuffer > 0) + { + mp = (int)(progress.FoodMpBufferSize * _bufferFraction); + progress.FoodMpBuffer -= mp; + } + else + { + progress.FoodMpBufferSize = 0; + progress.FoodMpBuffer = 0; + } + + if (progress.FoodSpBuffer > 0) + { + sp = (int)(progress.FoodSpBufferSize * _bufferFraction); + progress.FoodSpBuffer -= sp; + } + else + { + progress.FoodSpBufferSize = 0; + progress.FoodSpBuffer = 0; + } + + int toAdd = character.BCardComponent.GetAllBCardsInformation(BCardType.LeonaPassiveSkill, (byte)AdditionalTypes.LeonaPassiveSkill.IncreaseRecoveryItems, character.Level).firstData; + toAdd += character.GetMaxArmorShellValue(ShellEffectType.IncreasedRecoveryItemSpeed); + toAdd += character.Family?.UpgradeValues.GetOrDefault(FamilyUpgradeType.INCREASE_FOOD_SNACK_REGEN) ?? 0; + int toRemove = character.BCardComponent.GetAllBCardsInformation(BCardType.LeonaPassiveSkill, (byte)AdditionalTypes.LeonaPassiveSkill.DecreaseRecoveryItems, character.Level).firstData; + + double finalHeal = (100 + (toAdd - toRemove)) * 0.01; + + hp = (int)(hp * finalHeal); + mp = (int)(mp * finalHeal); + + if (hp <= 0) + { + hp = 0; + } + + if (mp <= 0) + { + mp = 0; + } + + int mateHeal = 0; + if (progress.FoodMateMaxHpBuffer > 0) + { + mateHeal = (int)(progress.FoodMateMaxHpBufferSize * _bufferFraction); + progress.FoodMateMaxHpBuffer -= mateHeal; + } + else + { + progress.FoodMateMaxHpBuffer = 0; + progress.FoodMateMaxHpBufferSize = 0; + } + + if (mateHeal != 0) + { + foreach (IMateEntity mate in character.MateComponent.TeamMembers()) + { + if (!mate.IsSitting) + { + continue; + } + + int toHeal = (int)(mate.MaxHp * (mateHeal * 0.01)); + character.Session.EmitEvent(new MateHealEvent + { + MateEntity = mate, + HpHeal = toHeal + }); + } + } + + if (hp != 0 || mp != 0) + { + character.EmitEvent(new BattleEntityHealEvent + { + HpHeal = hp, + MpHeal = mp, + HealMates = mateHeal == 0, + Entity = character + }); + } + + if (sp != 0) + { + bool addToBonus = character.SpPointsBasic + sp > _configuration.MaxSpBasePoints; + if (addToBonus) + { + int remove = _configuration.MaxSpBasePoints - character.SpPointsBasic; + character.SpPointsBasic = _configuration.MaxSpBasePoints; + + sp -= remove; + character.SpPointsBonus = character.SpPointsBonus + sp > _configuration.MaxSpAdditionalPoints ? _configuration.MaxSpAdditionalPoints : character.SpPointsBonus + sp; + } + else + { + character.SpPointsBasic += sp; + } + + character.Session.RefreshSpPoint(); + } + + progress.LastTick = date; + } + + private void ProcessAdditionalFood(IPlayerEntity character, in DateTime date) + { + AdditionalFoodProgress progress = character.GetAdditionalFoodProgress; + if (progress == null) + { + return; + } + + if (progress.LastTick + _tickDelay > date) + { + return; + } + + int hp = 0; + int mp = 0; + + if (progress.FoodAdditionalHpBuffer > 0) + { + hp = (int)(progress.FoodAdditionalHpBufferSize * _bufferFraction); + progress.FoodAdditionalHpBuffer -= hp; + } + else + { + progress.FoodAdditionalHpBufferSize = 0; + progress.FoodAdditionalHpBuffer = 0; + } + + if (progress.FoodAdditionalMpBuffer > 0) + { + mp = (int)(progress.FoodAdditionalMpBufferSize * _bufferFraction); + progress.FoodAdditionalMpBuffer -= mp; + } + else + { + progress.FoodAdditionalMpBufferSize = 0; + progress.FoodAdditionalMpBuffer = 0; + } + + if (hp != 0 || mp != 0) + { + character.Session.EmitEvent(new AddAdditionalHpMpEvent + { + Hp = hp, + Mp = mp, + MaxHpPercentage = progress.HpCap, + MaxMpPercentage = progress.MpCap + }); + } + + progress.LastTick = date; + } + + private void ProcessSnack(IPlayerEntity character, in DateTime date) + { + SnackProgress progress = character.GetSnackProgress; + if (progress == null) + { + return; + } + + if (progress.LastTick + _tickDelay > date) + { + return; + } + + int hp = 0; + int mp = 0; + int sp = 0; + + if (progress.SnackHpBuffer > 0) + { + hp = (int)(progress.SnackHpBufferSize * _bufferFraction); + progress.SnackHpBuffer -= hp; + } + else + { + progress.SnackHpBufferSize = 0; + progress.SnackHpBuffer = 0; + } + + if (progress.SnackMpBuffer > 0) + { + mp = (int)(progress.SnackMpBufferSize * _bufferFraction); + progress.SnackMpBuffer -= mp; + } + else + { + progress.SnackMpBufferSize = 0; + progress.SnackMpBuffer = 0; + } + + if (progress.SnackSpBuffer > 0) + { + sp = (int)(progress.SnackSpBufferSize * _bufferFraction); + progress.SnackSpBuffer -= sp; + } + else + { + progress.SnackSpBufferSize = 0; + progress.SnackSpBuffer = 0; + } + + int toAdd = character.BCardComponent.GetAllBCardsInformation(BCardType.LeonaPassiveSkill, (byte)AdditionalTypes.LeonaPassiveSkill.IncreaseRecoveryItems, character.Level).firstData; + toAdd += character.GetMaxArmorShellValue(ShellEffectType.IncreasedRecoveryItemSpeed); + toAdd += character.Family?.UpgradeValues.GetOrDefault(FamilyUpgradeType.INCREASE_FOOD_SNACK_REGEN) ?? 0; + int toRemove = character.BCardComponent.GetAllBCardsInformation(BCardType.LeonaPassiveSkill, (byte)AdditionalTypes.LeonaPassiveSkill.DecreaseRecoveryItems, character.Level).firstData; + + double finalHeal = (100 + (toAdd - toRemove)) * 0.01; + + hp = (int)(hp * finalHeal); + mp = (int)(mp * finalHeal); + + if (hp <= 0) + { + hp = 0; + } + + if (mp <= 0) + { + mp = 0; + } + + if (hp != 0 || mp != 0) + { + character.EmitEvent(new BattleEntityHealEvent + { + HpHeal = hp, + MpHeal = mp, + HealMates = true, + Entity = character + }); + } + + if (sp != 0) + { + bool addToBonus = character.SpPointsBasic + sp > _configuration.MaxSpBasePoints; + if (addToBonus) + { + int remove = _configuration.MaxSpBasePoints - character.SpPointsBasic; + character.SpPointsBasic = _configuration.MaxSpBasePoints; + + sp -= remove; + character.SpPointsBonus = character.SpPointsBonus + sp > _configuration.MaxSpAdditionalPoints ? _configuration.MaxSpAdditionalPoints : character.SpPointsBonus + sp; + } + else + { + character.SpPointsBasic += sp; + } + + character.Session.RefreshSpPoint(); + } + + progress.LastTick = date; + } + + private void ProcessAdditionalSnack(IPlayerEntity character, in DateTime date) + { + AdditionalSnackProgress progress = character.GetAdditionalSnackProgress; + if (progress == null) + { + return; + } + + if (progress.LastTick + _tickDelay > date) + { + return; + } + + int hp = 0; + int mp = 0; + + if (progress.SnackAdditionalHpBuffer > 0) + { + hp = (int)(progress.SnackAdditionalHpBufferSize * _bufferFraction); + progress.SnackAdditionalHpBuffer -= hp; + } + else + { + progress.SnackAdditionalHpBufferSize = 0; + progress.SnackAdditionalHpBuffer = 0; + } + + if (progress.SnackAdditionalMpBuffer > 0) + { + mp = (int)(progress.SnackAdditionalMpBufferSize * _bufferFraction); + progress.SnackAdditionalMpBuffer -= mp; + } + else + { + progress.SnackAdditionalMpBufferSize = 0; + progress.SnackAdditionalMpBuffer = 0; + } + + if (hp != 0 || mp != 0) + { + character.Session.EmitEvent(new AddAdditionalHpMpEvent + { + Hp = hp, + Mp = mp, + MaxHpPercentage = progress.HpCap, + MaxMpPercentage = progress.MpCap + }); + } + + progress.LastTick = date; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.CoreImpl/Pathfinding/ComparePfNodeMatrix.cs b/srcs/_plugins/Plugin.CoreImpl/Pathfinding/ComparePfNodeMatrix.cs new file mode 100644 index 0000000..058922a --- /dev/null +++ b/srcs/_plugins/Plugin.CoreImpl/Pathfinding/ComparePfNodeMatrix.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using WingsEmu.Game.Helpers.Damages; + +namespace Plugin.CoreImpl.Pathfinding +{ + internal class ComparePfNodeMatrix : IComparer + { + private readonly PathFinderNodeFast[,] _matrix; + + public ComparePfNodeMatrix(PathFinderNodeFast[,] matrix) => _matrix = matrix; + + public int Compare(Position a, Position b) + { + if (_matrix[a.X, a.Y].F_Gone_Plus_Heuristic > _matrix[b.X, b.Y].F_Gone_Plus_Heuristic) + { + return 1; + } + + if (_matrix[a.X, a.Y].F_Gone_Plus_Heuristic < _matrix[b.X, b.Y].F_Gone_Plus_Heuristic) + { + return -1; + } + + return 0; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.CoreImpl/Pathfinding/Heuristic.cs b/srcs/_plugins/Plugin.CoreImpl/Pathfinding/Heuristic.cs new file mode 100644 index 0000000..80ecc5a --- /dev/null +++ b/srcs/_plugins/Plugin.CoreImpl/Pathfinding/Heuristic.cs @@ -0,0 +1,49 @@ +using System; +using WingsEmu.Game.Helpers.Damages; + +namespace Plugin.CoreImpl.Pathfinding +{ + public class Heuristic + { + public static int DetermineH(HeuristicFormula heuristicFormula, Position end, int heuristicEstimate, int newLocationY, int newLocationX) + { + int h; + + switch (heuristicFormula) + { + case HeuristicFormula.Chebyshev: + h = heuristicEstimate * Math.Max(Math.Abs(newLocationX - end.X), Math.Abs(newLocationY - end.Y)); + break; + + case HeuristicFormula.DiagonalShortCut: + int hDiagonal = Math.Min(Math.Abs(newLocationX - end.X), Math.Abs(newLocationY - end.Y)); + int hStraight = Math.Abs(newLocationX - end.X) + Math.Abs(newLocationY - end.Y); + h = heuristicEstimate * 2 * hDiagonal + heuristicEstimate * (hStraight - 2 * hDiagonal); + break; + + case HeuristicFormula.Euclidean: + h = (int)(heuristicEstimate * Math.Sqrt(Math.Pow(newLocationX - end.X, 2) + Math.Pow(newLocationY - end.Y, 2))); + break; + + case HeuristicFormula.EuclideanNoSQR: + h = (int)(heuristicEstimate * (Math.Pow(newLocationX - end.X, 2) + Math.Pow(newLocationY - end.Y, 2))); + break; + + case HeuristicFormula.Custom1: + var dxy = new Position((short)Math.Abs(end.X - newLocationX), (short)Math.Abs(end.Y - newLocationY)); + int orthogonal = Math.Abs(dxy.X - dxy.Y); + int diagonal = Math.Abs((dxy.X + dxy.Y - orthogonal) / 2); + h = heuristicEstimate * (diagonal + orthogonal + dxy.X + dxy.Y); + break; + + // ReSharper disable once RedundantCaseLabel + case HeuristicFormula.Manhattan: + default: + h = heuristicEstimate * (Math.Abs(newLocationX - end.X) + Math.Abs(newLocationY - end.Y)); + break; + } + + return h; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.CoreImpl/Pathfinding/HeuristicFormula.cs b/srcs/_plugins/Plugin.CoreImpl/Pathfinding/HeuristicFormula.cs new file mode 100644 index 0000000..3fb7447 --- /dev/null +++ b/srcs/_plugins/Plugin.CoreImpl/Pathfinding/HeuristicFormula.cs @@ -0,0 +1,15 @@ +namespace Plugin.CoreImpl.Pathfinding +{ + public enum HeuristicFormula + { + // ReSharper disable InconsistentNaming + Manhattan = 1, + Chebyshev = 2, + DiagonalShortCut = 3, + Euclidean = 4, + EuclideanNoSQR = 5, + + Custom1 = 6 + // ReSharper restore InconsistentNaming + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.CoreImpl/Pathfinding/IPathFinder.cs b/srcs/_plugins/Plugin.CoreImpl/Pathfinding/IPathFinder.cs new file mode 100644 index 0000000..d4f62cf --- /dev/null +++ b/srcs/_plugins/Plugin.CoreImpl/Pathfinding/IPathFinder.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using WingsEmu.Game.Helpers.Damages; + +namespace Plugin.CoreImpl.Pathfinding +{ + public interface IPathFinder + { + Position FindPath(Position start, Position end, float speedIndex, IReadOnlyList grid, int width, int height, bool useBresenhamFirst = false); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.CoreImpl/Pathfinding/IPriorityQueue.cs b/srcs/_plugins/Plugin.CoreImpl/Pathfinding/IPriorityQueue.cs new file mode 100644 index 0000000..a335164 --- /dev/null +++ b/srcs/_plugins/Plugin.CoreImpl/Pathfinding/IPriorityQueue.cs @@ -0,0 +1,11 @@ +namespace Plugin.CoreImpl.Pathfinding +{ + public interface IPriorityQueue + { + int Count { get; } + int Enqueue(T item); + T Dequeue(); + + void Clear(); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.CoreImpl/Pathfinding/PathFinder.cs b/srcs/_plugins/Plugin.CoreImpl/Pathfinding/PathFinder.cs new file mode 100644 index 0000000..a0b109b --- /dev/null +++ b/srcs/_plugins/Plugin.CoreImpl/Pathfinding/PathFinder.cs @@ -0,0 +1,253 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using WingsEmu.Game.Helpers; +using WingsEmu.Game.Helpers.Damages; + +namespace Plugin.CoreImpl.Pathfinding +{ + public class PathFinder : IPathFinder + { + private readonly List _closed = new(); + private readonly sbyte[,] _direction; + private readonly IReadOnlyList _grid; + private readonly PathFinderOptions _options; + private byte _closeNodeValue = 2; + private int _horiz; + private PathFinderNodeFast[,] _mCalcGrid; + private IPriorityQueue _open; + private byte _openNodeValue = 1; + + public PathFinder(IReadOnlyList grid, int width, int height, PathFinderOptions pathFinderOptions = null) + { + _grid = grid ?? throw new Exception("Grid cannot be null"); + Width = (ushort)width; + Height = (ushort)height; + + + _options = pathFinderOptions ?? new PathFinderOptions(); + + _direction = _options.Diagonals + ? new sbyte[,] { { 0, -1 }, { 1, 0 }, { 0, 1 }, { -1, 0 }, { 1, -1 }, { 1, 1 }, { -1, 1 }, { -1, -1 } } + : new sbyte[,] { { 0, -1 }, { 1, 0 }, { 0, 1 }, { -1, 0 } }; + } + + private ushort Width { get; } + private ushort Height { get; } + + public Position FindPath(Position start, Position end, float speedIndex, IReadOnlyList grid, int width, int height, bool useBresenhamFirst) + { + try + { + Position position; + if (useBresenhamFirst) + { + position = PathfindingAlgorithm.Bresenham(start, end, 10, grid, width, height, true); + position = AStar(start, position, (byte)speedIndex); + if (position == start) + { + position = PathfindingAlgorithm.Bresenham(start, end, speedIndex, grid, width, height, false); + } + } + else + { + position = AStar(start, end, (byte)speedIndex); + } + + return position; + } + catch + { + return start; + } + } + + private List FindPath(Position start, Position end) + { + if (_mCalcGrid == null || _mCalcGrid.GetLength(0) != Width || _mCalcGrid.GetLength(1) != Height) + { + _mCalcGrid = new PathFinderNodeFast[Width, Height]; + _open = new PriorityQueueB(new ComparePfNodeMatrix(_mCalcGrid)); + } + + bool found = false; + int closedNodeCounter = 0; + _openNodeValue += 2; //increment for subsequent runs + _closeNodeValue += 2; + _open.Clear(); + _closed.Clear(); + + _mCalcGrid[start.X, start.Y].Gone = 0; + _mCalcGrid[start.X, start.Y].F_Gone_Plus_Heuristic = _options.HeuristicEstimate; + _mCalcGrid[start.X, start.Y].ParentX = start.X; + _mCalcGrid[start.X, start.Y].ParentY = start.Y; + _mCalcGrid[start.X, start.Y].Status = _openNodeValue; + + _open.Enqueue(start); + + while (_open.Count > 0) + { + Position location = _open.Dequeue(); + + //Is it in closed list? means this node was already processed + if (_mCalcGrid[location.X, location.Y].Status == _closeNodeValue) + { + continue; + } + + short locationX = location.X; + short locationY = location.Y; + + if (location == end) + { + _mCalcGrid[location.X, location.Y].Status = _closeNodeValue; + found = true; + break; + } + + if (closedNodeCounter > _options.SearchLimit) + { + return null; + } + + if (_options.PunishChangeDirection) + { + _horiz = locationX - _mCalcGrid[location.X, location.Y].ParentX; + } + + //Lets calculate each successors + for (int i = 0; i < _direction.GetLength(0); i++) + { + //unsign incase we went out of bounds + short newLocationX = (short)(locationX + _direction[i, 0]); + short newLocationY = (short)(locationY + _direction[i, 1]); + + if (newLocationX >= Width || newLocationY >= Height) + { + continue; + } + + // Unbreakeable? + if ((_grid[newLocationX + newLocationY * Width] & (int)PathfindingAlgorithm.MapCellFlags.IsWalkingDisabled) != 0) + { + continue; + } + + int newG; + if (_options.HeavyDiagonals && i > 3) + { + newG = _mCalcGrid[location.X, location.Y].Gone + (int)(_grid[newLocationX + newLocationY * Width] * 2.41); + } + else + { + newG = _mCalcGrid[location.X, location.Y].Gone + _grid[newLocationX + newLocationY * Width]; + } + + if (_options.PunishChangeDirection) + { + if ((newLocationX - locationX) != 0) + { + if (_horiz == 0) + { + newG += Math.Abs(newLocationX - end.X) + Math.Abs(newLocationY - end.Y); + } + } + + if ((newLocationY - locationY) != 0) + { + if (_horiz != 0) + { + newG += Math.Abs(newLocationX - end.X) + Math.Abs(newLocationY - end.Y); + } + } + } + + //Is it open or closed? + if (_mCalcGrid[newLocationX, newLocationY].Status == _openNodeValue || _mCalcGrid[newLocationX, newLocationY].Status == _closeNodeValue) + { + // The current node has less code than the previous? then skip this node + if (_mCalcGrid[newLocationX, newLocationY].Gone <= newG) + { + continue; + } + } + + _mCalcGrid[newLocationX, newLocationY].ParentX = locationX; + _mCalcGrid[newLocationX, newLocationY].ParentY = locationY; + _mCalcGrid[newLocationX, newLocationY].Gone = newG; + + int h = Heuristic.DetermineH(HeuristicFormula.Chebyshev, end, _options.HeuristicEstimate, newLocationY, newLocationX); + + if (_options.TieBreaker) + { + int dx1 = locationX - end.X; + int dy1 = locationY - end.Y; + int dx2 = start.X - end.X; + int dy2 = start.Y - end.Y; + int cross = Math.Abs(dx1 * dy2 - dx2 * dy1); + h = (int)(h + cross * 0.001); + } + + _mCalcGrid[newLocationX, newLocationY].F_Gone_Plus_Heuristic = newG + h; + + _open.Enqueue(new Position(newLocationX, newLocationY)); + + _mCalcGrid[newLocationX, newLocationY].Status = _openNodeValue; + } + + closedNodeCounter++; + _mCalcGrid[location.X, location.Y].Status = _closeNodeValue; + } + + return !found ? null : OrderClosedListAsPath(end); + } + + private Position AStar(Position start, Position end, byte speedIndex) + { + IReadOnlyList positions = FindPath(start, end); + if (positions == null || positions.Count < 1) + { + return start; + } + + PathFinderNode[] array = positions.Reverse().ToArray(); + int max = array.Length > speedIndex + 1 ? speedIndex : array.Length - 1; + + PathFinderNode pos = array[max]; + return new Position(pos.X, pos.Y); + } + + private List OrderClosedListAsPath(Position end) + { + _closed.Clear(); + + PathFinderNodeFast fNodeTmp = _mCalcGrid[end.X, end.Y]; + + var fNode = new PathFinderNode + { + ParentX = fNodeTmp.ParentX, + ParentY = fNodeTmp.ParentY, + X = end.X, + Y = end.Y + }; + + while (fNode.X != fNode.ParentX || fNode.Y != fNode.ParentY) + { + _closed.Add(fNode); + + short posX = fNode.ParentX; + short posY = fNode.ParentY; + + fNodeTmp = _mCalcGrid[posX, posY]; + fNode.ParentX = fNodeTmp.ParentX; + fNode.ParentY = fNodeTmp.ParentY; + fNode.X = posX; + fNode.Y = posY; + } + + _closed.Add(fNode); + + return _closed; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.CoreImpl/Pathfinding/PathFinderNode.cs b/srcs/_plugins/Plugin.CoreImpl/Pathfinding/PathFinderNode.cs new file mode 100644 index 0000000..3b8e43b --- /dev/null +++ b/srcs/_plugins/Plugin.CoreImpl/Pathfinding/PathFinderNode.cs @@ -0,0 +1,10 @@ +namespace Plugin.CoreImpl.Pathfinding +{ + public struct PathFinderNode + { + public short X; + public short Y; + public short ParentX; // Parent + public short ParentY; + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.CoreImpl/Pathfinding/PathFinderNodeFast.cs b/srcs/_plugins/Plugin.CoreImpl/Pathfinding/PathFinderNodeFast.cs new file mode 100644 index 0000000..bdb562c --- /dev/null +++ b/srcs/_plugins/Plugin.CoreImpl/Pathfinding/PathFinderNodeFast.cs @@ -0,0 +1,14 @@ +using System.Runtime.InteropServices; + +namespace Plugin.CoreImpl.Pathfinding +{ + [StructLayout(LayoutKind.Sequential, Pack = 1)] + internal struct PathFinderNodeFast + { + public int F_Gone_Plus_Heuristic; // f = gone + heuristic + public int Gone; + public short ParentX; // Parent + public short ParentY; + public byte Status; + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.CoreImpl/Pathfinding/PathFinderOptions.cs b/srcs/_plugins/Plugin.CoreImpl/Pathfinding/PathFinderOptions.cs new file mode 100644 index 0000000..8e293c8 --- /dev/null +++ b/srcs/_plugins/Plugin.CoreImpl/Pathfinding/PathFinderOptions.cs @@ -0,0 +1,27 @@ +namespace Plugin.CoreImpl.Pathfinding +{ + public class PathFinderOptions + { + public PathFinderOptions() + { + Formula = HeuristicFormula.Chebyshev; + HeuristicEstimate = 2; + SearchLimit = 100; + Diagonals = false; + } + + public HeuristicFormula Formula { get; set; } + + public bool Diagonals { get; set; } + + public bool HeavyDiagonals { get; set; } + + public int HeuristicEstimate { get; set; } + + public bool PunishChangeDirection { get; set; } + + public bool TieBreaker { get; set; } + + public int SearchLimit { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.CoreImpl/Pathfinding/PriorityQueueB.cs b/srcs/_plugins/Plugin.CoreImpl/Pathfinding/PriorityQueueB.cs new file mode 100644 index 0000000..eef2d7b --- /dev/null +++ b/srcs/_plugins/Plugin.CoreImpl/Pathfinding/PriorityQueueB.cs @@ -0,0 +1,160 @@ +using System.Collections.Generic; + +namespace Plugin.CoreImpl.Pathfinding +{ + public class PriorityQueueB : IPriorityQueue + { + private readonly IComparer _comparer; + private readonly List _innerList = new(); + + public PriorityQueueB() => _comparer = Comparer.Default; + + public PriorityQueueB(IComparer comparer) => _comparer = comparer; + + public T this[int index] + { + get => _innerList[index]; + set + { + _innerList[index] = value; + Update(index); + } + } + + public void Clear() + { + _innerList.Clear(); + } + + public int Count => _innerList.Count; + + public int Enqueue(T item) + { + int p = _innerList.Count; + _innerList.Add(item); // E[p] = O + + do + { + if (p == 0) + { + break; + } + + int p2 = (p - 1) / 2; + + if (OnCompare(p, p2) < 0) + { + SwitchElements(p, p2); + p = p2; + } + else + { + break; + } + } while (true); + + return p; + } + + public T Dequeue() + { + T result = _innerList[0]; + int p = 0; + + _innerList[0] = _innerList[^1]; + _innerList.RemoveAt(_innerList.Count - 1); + + do + { + int pn = p; + int p1 = 2 * p + 1; + int p2 = 2 * p + 2; + + if (_innerList.Count > p1 && OnCompare(p, p1) > 0) + { + p = p1; + } + + if (_innerList.Count > p2 && OnCompare(p, p2) > 0) + { + p = p2; + } + + if (p == pn) + { + break; + } + + SwitchElements(p, pn); + } while (true); + + return result; + } + + public T Peek() => _innerList.Count > 0 ? _innerList[0] : default; + + private void Update(int i) + { + int p = i; + int p2; + + do + { + if (p == 0) + { + break; + } + + p2 = (p - 1) / 2; + + if (OnCompare(p, p2) < 0) + { + SwitchElements(p, p2); + p = p2; + } + else + { + break; + } + } while (true); + + if (p < i) + { + return; + } + + do + { + int pn = p; + int p1 = 2 * p + 1; + p2 = 2 * p + 2; + + if (_innerList.Count > p1 && OnCompare(p, p1) > 0) + { + p = p1; + } + + if (_innerList.Count > p2 && OnCompare(p, p2) > 0) + { + p = p2; + } + + if (p == pn) + { + break; + } + + SwitchElements(p, pn); + } while (true); + } + + private void SwitchElements(int i, int j) + { + T h = _innerList[i]; + _innerList[i] = _innerList[j]; + _innerList[j] = h; + } + + private int OnCompare(int i, int j) => _comparer.Compare(_innerList[i], _innerList[j]); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.CoreImpl/Plugin.CoreImpl.csproj b/srcs/_plugins/Plugin.CoreImpl/Plugin.CoreImpl.csproj new file mode 100644 index 0000000..6d51a0c --- /dev/null +++ b/srcs/_plugins/Plugin.CoreImpl/Plugin.CoreImpl.csproj @@ -0,0 +1,17 @@ + + + + net5.0 + + + + + + + + + + + + + diff --git a/srcs/_plugins/Plugin.CoreImpl/Skills/SkillEntityFactory.cs b/srcs/_plugins/Plugin.CoreImpl/Skills/SkillEntityFactory.cs new file mode 100644 index 0000000..f34e548 --- /dev/null +++ b/srcs/_plugins/Plugin.CoreImpl/Skills/SkillEntityFactory.cs @@ -0,0 +1,23 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsEmu.DTOs.Skills; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Skills; + +namespace Plugin.CoreImpl.Skills +{ + public class SkillEntityFactory : IEntitySkillFactory + { + private readonly ISkillsManager _skillsManager; + + public SkillEntityFactory(ISkillsManager skillsManager) => _skillsManager = skillsManager; + + public INpcMonsterSkill CreateNpcMonsterSkill(int skillVnum, short rate, bool isBasicAttack, bool isIgnoringHitChance) + { + SkillDTO tmp = _skillsManager.GetSkill(skillVnum); + return tmp == null ? null : new NpcMonsterSkill(tmp, rate, isBasicAttack, isIgnoringHitChance); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.DB.EF/Auth/ClientVersion/AuthorizedClientVersionEntity.cs b/srcs/_plugins/Plugin.DB.EF/Auth/ClientVersion/AuthorizedClientVersionEntity.cs new file mode 100644 index 0000000..0f8b18b --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/Auth/ClientVersion/AuthorizedClientVersionEntity.cs @@ -0,0 +1,28 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using PhoenixLib.DAL.EFCore.PGSQL; +using Plugin.Database.DB; + +namespace Plugin.Database.Auth.ClientVersion +{ + [Table("authorized_client_versions", Schema = DatabaseSchemas.CONFIG_AUTH)] + public class AuthorizedClientVersionEntity : ILongEntity + { + [Required] + public string ClientVersion { get; set; } + + [Required] + public string ExecutableHash { get; set; } + + [Required] + public string DllHash { get; set; } + + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long Id { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.DB.EF/Auth/ClientVersion/EfAuthorizedClientVersionRepository.cs b/srcs/_plugins/Plugin.DB.EF/Auth/ClientVersion/EfAuthorizedClientVersionRepository.cs new file mode 100644 index 0000000..31b62d7 --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/Auth/ClientVersion/EfAuthorizedClientVersionRepository.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using PhoenixLib.DAL; +using Plugin.Database.Auth.HWID; +using Plugin.Database.DB; +using WingsAPI.Communication.Auth; + +namespace Plugin.Database.Auth.ClientVersion +{ + public class EfAuthorizedClientVersionRepository : IAuthorizedClientVersionRepository + { + private readonly IDbContextFactory _contextFactory; + private readonly ILogger _logger; + private readonly IMapper _mapper; + private readonly IGenericAsyncLongRepository _repository; + + public EfAuthorizedClientVersionRepository(IGenericAsyncLongRepository repository, IDbContextFactory contextFactory, + ILogger logger, IMapper mapper) + { + _repository = repository; + _contextFactory = contextFactory; + _logger = logger; + _mapper = mapper; + } + + + public async Task DeleteAsync(string clientVersion) + { + try + { + await using GameContext context = _contextFactory.CreateDbContext(); + AuthorizedClientVersionEntity entity = await context.AuthorizedClientVersions.FirstOrDefaultAsync(s => s.ClientVersion == clientVersion); + if (entity == null) + { + return null; + } + + context.AuthorizedClientVersions.Remove(entity); + await context.SaveChangesAsync(); + return _mapper.Map(entity); + } + catch (Exception e) + { + _logger.LogError("DeleteAsync", e); + throw; + } + } + + public async Task> GetAllAsync() => await _repository.GetAllAsync(); + + public async Task GetByIdAsync(long id) => await _repository.GetByIdAsync(id); + + public async Task> GetByIdsAsync(IEnumerable ids) => await _repository.GetByIdsAsync(ids); + + public async Task SaveAsync(AuthorizedClientVersionDto obj) => await _repository.SaveAsync(obj); + + public async Task> SaveAsync(IReadOnlyList objs) => await _repository.SaveAsync(objs); + + public async Task DeleteByIdAsync(long id) + { + await _repository.DeleteByIdAsync(id); + } + + public async Task DeleteByIdsAsync(IEnumerable ids) + { + await _repository.DeleteByIdsAsync(ids); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.DB.EF/Auth/ClientVersion/IAuthorizedClientVersionRepository.cs b/srcs/_plugins/Plugin.DB.EF/Auth/ClientVersion/IAuthorizedClientVersionRepository.cs new file mode 100644 index 0000000..1cc94cb --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/Auth/ClientVersion/IAuthorizedClientVersionRepository.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; +using PhoenixLib.DAL; +using WingsAPI.Communication.Auth; + +namespace Plugin.Database.Auth.ClientVersion +{ + public interface IAuthorizedClientVersionRepository : IGenericAsyncLongRepository + { + Task DeleteAsync(string clientVersion); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.DB.EF/Auth/HWID/BlacklistedHwidDao.cs b/srcs/_plugins/Plugin.DB.EF/Auth/HWID/BlacklistedHwidDao.cs new file mode 100644 index 0000000..fceabc1 --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/Auth/HWID/BlacklistedHwidDao.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Mapster; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using PhoenixLib.DAL; +using Plugin.Database.DB; +using WingsAPI.Communication.Auth; + +namespace Plugin.Database.Auth.HWID +{ + public class BlacklistedHwidDao : IBlacklistedHwidDao + { + private readonly IDbContextFactory _contextFactory; + private readonly ILogger _logger; + private readonly IMapper _mapper; + + public BlacklistedHwidDao(IDbContextFactory contextFactory, IMapper mapper, ILogger logger) + { + _contextFactory = contextFactory; + _mapper = mapper; + _logger = logger; + } + + public async Task SaveAsync(BlacklistedHwidDto dto) + { + try + { + await using DbContext context = _contextFactory.CreateDbContext(); + + BlacklistedHwidEntity entity = await context.FindAsync(dto.HardwareId); + + if (entity == null) + { + entity = dto.Adapt(); + await context.Set().AddAsync(entity); + } + else + { + dto.Adapt(entity); + context.Set().Update(entity); + } + + await context.SaveChangesAsync(); + } + catch (Exception e) + { + _logger.LogError("[BLACKLISTED_HWID_DAO][SaveAsync] ", e); + throw; + } + } + + public async Task DeleteAsync(string hwid) + { + try + { + await using GameContext context = _contextFactory.CreateDbContext(); + BlacklistedHwidEntity entity = await context.BlacklistedHwids.FindAsync(hwid); + context.BlacklistedHwids.Remove(entity); + await context.SaveChangesAsync(); + } + catch (Exception e) + { + _logger.LogError("[BLACKLISTED_HWID_DAO][DeleteAsync] ", e); + throw; + } + } + + public async Task GetByKeyAsync(string hwid) + { + try + { + await using GameContext context = _contextFactory.CreateDbContext(); + BlacklistedHwidEntity item = await context.BlacklistedHwids.FindAsync(hwid); + return item == null ? null : _mapper.Map(item); + } + catch (Exception e) + { + _logger.LogError("[BLACKLISTED_HWID_DAO][GetByKeyAsync] ", e); + throw; + } + } + + public async Task> GetAllAsync() + { + try + { + await using GameContext context = _contextFactory.CreateDbContext(); + List items = await context.BlacklistedHwids.ToListAsync(); + return _mapper.Map(items); + } + catch (Exception e) + { + _logger.LogError("[BLACKLISTED_HWID_DAO][GetAllAsync] ", e); + throw; + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.DB.EF/Auth/HWID/BlacklistedHwidEntity.cs b/srcs/_plugins/Plugin.DB.EF/Auth/HWID/BlacklistedHwidEntity.cs new file mode 100644 index 0000000..1477864 --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/Auth/HWID/BlacklistedHwidEntity.cs @@ -0,0 +1,20 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Plugin.Database.DB; + +namespace Plugin.Database.Auth.HWID +{ + [Table("blacklisted_hardware_ids", Schema = DatabaseSchemas.CONFIG_AUTH)] + public class BlacklistedHwidEntity + { + [Key] + [Required] + public string HardwareId { get; set; } + + [Required] + public string Comment { get; set; } + + [Required] + public string Judge { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.DB.EF/Auth/HWID/IBlacklistedHwidDao.cs b/srcs/_plugins/Plugin.DB.EF/Auth/HWID/IBlacklistedHwidDao.cs new file mode 100644 index 0000000..eefbab2 --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/Auth/HWID/IBlacklistedHwidDao.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using WingsAPI.Communication.Auth; + +namespace Plugin.Database.Auth.HWID +{ + public interface IBlacklistedHwidDao + { + Task SaveAsync(BlacklistedHwidDto dto); + Task DeleteAsync(string hwid); + Task GetByKeyAsync(string hwid); + Task> GetAllAsync(); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.DB.EF/Bazaar/BazaarItemDAO.cs b/srcs/_plugins/Plugin.DB.EF/Bazaar/BazaarItemDAO.cs new file mode 100644 index 0000000..e8efbff --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/Bazaar/BazaarItemDAO.cs @@ -0,0 +1,87 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using PhoenixLib.DAL; +using PhoenixLib.Logging; +using Plugin.Database.DB; +using WingsAPI.Data.Bazaar; +using WingsEmu.DTOs.Bazaar; + +namespace Plugin.Database.Bazaar +{ + public class BazaarItemDAO : IBazaarItemDAO + { + private readonly IDbContextFactory _contextFactory; + private readonly IMapper _mapper; + private readonly IGenericAsyncLongRepository _repository; + + public BazaarItemDAO(IDbContextFactory contextFactory, IGenericAsyncLongRepository repository, IMapper mapper) + { + _contextFactory = contextFactory; + _repository = repository; + _mapper = mapper; + } + + public async Task> GetAllAsync() => await _repository.GetAllAsync(); + + public async Task GetByIdAsync(long id) => await _repository.GetByIdAsync(id); + + public async Task> GetByIdsAsync(IEnumerable ids) => await _repository.GetByIdsAsync(ids); + + public async Task SaveAsync(BazaarItemDTO obj) => await _repository.SaveAsync(obj); + + public async Task> SaveAsync(IReadOnlyList objs) => await _repository.SaveAsync(objs); + + public async Task DeleteByIdAsync(long id) + { + await _repository.DeleteByIdAsync(id); + } + + public async Task DeleteByIdsAsync(IEnumerable ids) + { + await _repository.DeleteByIdsAsync(ids); + } + + public async Task> GetAllNonDeletedBazaarItems() + { + try + { + await using GameContext context = _contextFactory.CreateDbContext(); + var tmp = context.BazaarItem.Where(s => s.DeletedAt == null).ToList(); + return _mapper.Map(tmp); + } + catch (Exception e) + { + Log.Error("GetBazaarItemByItemInstanceId", e); + return null; + } + } + + public async Task> GetBazaarItemsByCharacterId(long characterId) + { + try + { + await using GameContext context = _contextFactory.CreateDbContext(); + var list = new List(); + IQueryable tmp = context.BazaarItem.Where(s => s.CharacterId == characterId); + foreach (DbBazaarItemEntity item in tmp) + { + list.Add(_mapper.Map(item)); + } + + return list.AsReadOnly(); + } + catch (Exception e) + { + Log.Error("GetBazaarItemsByCharacterId", e); + return null; + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.DB.EF/Bazaar/DbBazaarItemEntity.cs b/srcs/_plugins/Plugin.DB.EF/Bazaar/DbBazaarItemEntity.cs new file mode 100644 index 0000000..b91161f --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/Bazaar/DbBazaarItemEntity.cs @@ -0,0 +1,48 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using PhoenixLib.DAL.EFCore.PGSQL; +using Plugin.Database.DB; +using Plugin.Database.Entities; +using Plugin.Database.Entities.PlayersData; +using WingsEmu.DTOs.Items; + +namespace Plugin.Database.Bazaar +{ + [Table("items", Schema = DatabaseSchemas.BAZAAR)] + public class DbBazaarItemEntity : BaseAuditableEntity, ILongEntity + { + public long CharacterId { get; set; } + + + public int Amount { get; set; } + + public int SoldAmount { get; set; } + + public long PricePerItem { get; set; } + + public long SaleFee { get; set; } + + public bool IsPackage { get; set; } + + public bool UsedMedal { get; set; } + + public DateTime ExpiryDate { get; set; } + + public short DayExpiryAmount { get; set; } + + [Column(TypeName = "jsonb")] + public ItemInstanceDTO ItemInstance { get; set; } + + [ForeignKey(nameof(CharacterId))] + public virtual DbCharacter DbCharacter { get; set; } + + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long Id { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.DB.EF/DAOs/AccountBanDao.cs b/srcs/_plugins/Plugin.DB.EF/DAOs/AccountBanDao.cs new file mode 100644 index 0000000..1afa714 --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/DAOs/AccountBanDao.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using PhoenixLib.DAL; +using PhoenixLib.Logging; +using Plugin.Database.DB; +using Plugin.Database.Entities.Account; +using WingsAPI.Data.Account; + +namespace Plugin.Database.DAOs +{ + public class AccountBanDao : IAccountBanDao + { + private readonly IDbContextFactory _contextFactory; + private readonly IMapper _mapper; + private readonly IGenericAsyncLongRepository _repository; + + public AccountBanDao(IMapper mapper, IDbContextFactory contextFactory, IGenericAsyncLongRepository repository) + { + _mapper = mapper; + _contextFactory = contextFactory; + _repository = repository; + } + + public async Task FindAccountBan(long accountId) + { + try + { + await using GameContext context = _contextFactory.CreateDbContext(); + AccountBanEntity accountBanEntity = await context.AccountBans.FirstOrDefaultAsync(x => x.AccountId == accountId && (x.End == null || x.End > DateTime.UtcNow)); + return _mapper.Map(accountBanEntity); + } + catch (Exception e) + { + Log.Error($"FindAccountBan - AccountId: {accountId}", e); + return null; + } + } + + public async Task> GetAccountBans(long accountId) + { + try + { + await using GameContext context = _contextFactory.CreateDbContext(); + List accountBanEntity = await context.AccountBans.Where(x => x.AccountId == accountId).ToListAsync(); + return _mapper.Map(accountBanEntity); + } + catch (Exception e) + { + Log.Error($"GetAccountBans - AccountId: {accountId}", e); + return null; + } + } + + public async Task> GetAllAsync() => await _repository.GetAllAsync(); + + public async Task GetByIdAsync(long id) => await _repository.GetByIdAsync(id); + + public async Task> GetByIdsAsync(IEnumerable ids) => await _repository.GetByIdsAsync(ids); + + public async Task SaveAsync(AccountBanDto obj) => await _repository.SaveAsync(obj); + + public async Task> SaveAsync(IReadOnlyList objs) => await _repository.SaveAsync(objs); + + public async Task DeleteByIdAsync(long id) => await _repository.DeleteByIdAsync(id); + + public async Task DeleteByIdsAsync(IEnumerable ids) => await _repository.DeleteByIdsAsync(ids); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.DB.EF/DAOs/AccountDAO.cs b/srcs/_plugins/Plugin.DB.EF/DAOs/AccountDAO.cs new file mode 100644 index 0000000..ed48d17 --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/DAOs/AccountDAO.cs @@ -0,0 +1,97 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using PhoenixLib.DAL; +using PhoenixLib.Logging; +using Plugin.Database.DB; +using Plugin.Database.Entities.Account; +using WingsAPI.Data.Account; + +namespace Plugin.Database.DAOs +{ + public class AccountDAO : IAccountDAO + { + private readonly IDbContextFactory _contextFactory; + private readonly IMapper _mapper; + private readonly IGenericAsyncLongRepository _repository; + + public AccountDAO(IMapper mapper, IDbContextFactory contextFactory, IGenericAsyncLongRepository repository) + { + _mapper = mapper; + _contextFactory = contextFactory; + _repository = repository; + } + + public AccountDTO LoadByName(string name) + { + try + { + using GameContext context = _contextFactory.CreateDbContext(); + AccountEntity accountEntity = context.Account.FirstOrDefault(a => a.Name.Equals(name)); + return accountEntity == null ? null : _mapper.Map(accountEntity); + } + catch (Exception e) + { + Log.Error("LoadByName", e); + return null; + } + } + + public async Task GetByNameAsync(string name) + { + try + { + await using GameContext context = _contextFactory.CreateDbContext(); + AccountEntity accountEntity = await context.Account.FirstOrDefaultAsync(a => a.Name == name); + return accountEntity == null ? null : _mapper.Map(accountEntity); + } + catch (Exception e) + { + Log.Error("LoadByName", e); + return null; + } + } + + + public async Task> LoadByMasterAccountIdAsync(Guid masterAccountId) + { + try + { + await using GameContext context = _contextFactory.CreateDbContext(); + List tmp = await context.Account.Where(s => s.MasterAccountId == masterAccountId).ToListAsync(); + return _mapper.Map(tmp); + } + catch (Exception e) + { + Log.Error("WriteGeneralLog", e); + return new List(); + } + } + + public async Task> GetAllAsync() => await _repository.GetAllAsync(); + + public async Task GetByIdAsync(long id) => await _repository.GetByIdAsync(id); + + public async Task> GetByIdsAsync(IEnumerable ids) => await _repository.GetByIdsAsync(ids); + + public async Task SaveAsync(AccountDTO obj) => await _repository.SaveAsync(obj); + + public async Task> SaveAsync(IReadOnlyList objs) => await _repository.SaveAsync(objs); + + public async Task DeleteByIdAsync(long id) + { + await _repository.DeleteByIdAsync(id); + } + + public async Task DeleteByIdsAsync(IEnumerable ids) + { + await _repository.DeleteByIdsAsync(ids); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.DB.EF/DAOs/AccountPenaltyDao.cs b/srcs/_plugins/Plugin.DB.EF/DAOs/AccountPenaltyDao.cs new file mode 100644 index 0000000..145a5cc --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/DAOs/AccountPenaltyDao.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using PhoenixLib.DAL; +using PhoenixLib.Logging; +using Plugin.Database.DB; +using Plugin.Database.Entities.Account; +using WingsAPI.Data.Account; + +namespace Plugin.Database.DAOs +{ + public class AccountPenaltyDao : IAccountPenaltyDao + { + private readonly IDbContextFactory _contextFactory; + private readonly IMapper _mapper; + private readonly IGenericAsyncLongRepository _repository; + + public AccountPenaltyDao(IMapper mapper, IDbContextFactory contextFactory, IGenericAsyncLongRepository repository) + { + _mapper = mapper; + _contextFactory = contextFactory; + _repository = repository; + } + + public async Task> GetPenaltiesByAccountId(long accountId) + { + try + { + await using GameContext context = _contextFactory.CreateDbContext(); + List accountBanEntity = await context.AccountPenalties.Where(x => x.AccountId == accountId).ToListAsync(); + return _mapper.Map(accountBanEntity); + } + catch (Exception e) + { + Log.Error($"GetPenaltiesByAccountId - AccountId: {accountId}", e); + return new List(); + } + } + + public async Task> GetAllAsync() => await _repository.GetAllAsync(); + + public async Task GetByIdAsync(long id) => await _repository.GetByIdAsync(id); + + public async Task> GetByIdsAsync(IEnumerable ids) => await _repository.GetByIdsAsync(ids); + + public async Task SaveAsync(AccountPenaltyDto obj) => await _repository.SaveAsync(obj); + + public async Task> SaveAsync(IReadOnlyList objs) => await _repository.SaveAsync(objs); + + public async Task DeleteByIdAsync(long id) => await _repository.DeleteByIdAsync(id); + + public async Task DeleteByIdsAsync(IEnumerable ids) => await _repository.DeleteByIdsAsync(ids); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.DB.EF/DAOs/CharacterDAO.cs b/srcs/_plugins/Plugin.DB.EF/DAOs/CharacterDAO.cs new file mode 100644 index 0000000..5969818 --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/DAOs/CharacterDAO.cs @@ -0,0 +1,312 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using PhoenixLib.DAL; +using PhoenixLib.Logging; +using Plugin.Database.DB; +using Plugin.Database.Entities.PlayersData; +using Plugin.Database.Families; +using WingsAPI.Data.Character; +using WingsEmu.DTOs.Account; +using WingsEmu.DTOs.Enums; +using WingsEmu.Packets.Enums.Character; + +namespace Plugin.Database.DAOs +{ + public class CharacterDAO : ICharacterDAO + { + private readonly IDbContextFactory _contextFactory; + private readonly IMapper _mapper; + private readonly IGenericAsyncLongRepository _repository; + + public CharacterDAO(IGenericAsyncLongRepository repository, IDbContextFactory contextFactory, IMapper mapper) + { + _repository = repository; + _contextFactory = contextFactory; + _mapper = mapper; + } + + public async Task DeleteByPrimaryKey(long accountId, byte characterSlot) + { + try + { + await using GameContext context = _contextFactory.CreateDbContext(); + // actually a Character wont be deleted, it just will be disabled for future traces + DbCharacter dbCharacter = await context.Character.FirstOrDefaultAsync(c => c.AccountId == accountId && c.Slot == characterSlot); + + if (dbCharacter == null) + { + return DeleteResult.Deleted; + } + + context.Character.Remove(dbCharacter); + + DbFamilyMembership familyCharacter = await context.FamilyCharacter.FirstOrDefaultAsync(s => s.CharacterId == dbCharacter.Id); + if (familyCharacter != null) + { + context.FamilyCharacter.Remove(familyCharacter); + } + + await context.SaveChangesAsync(); + + return DeleteResult.Deleted; + } + catch (Exception e) + { + Log.Error("DeleteByPrimaryKey", e); + return DeleteResult.Error; + } + } + + /// + /// Returns first 30 occurences of highest Compliment + /// + /// + public async Task> GetTopCompliment(int top = 30) + { + try + { + await using GameContext context = _contextFactory.CreateDbContext(); + return _mapper.Map(await context.Character.Where(c => c.AccountEntity.Authority <= AuthorityType.Moderator).OrderByDescending(c => c.Compliment).Take(top).ToListAsync()); + } + catch (Exception e) + { + Log.Error("GetTopCompliment", e); + return new List(); + } + } + + /// + /// Returns first 30 occurences of highest Act4Points + /// + /// + public async Task> GetTopPoints(int top = 30) + { + try + { + await using GameContext context = _contextFactory.CreateDbContext(); + return _mapper.Map(await context.Character.Where(c => c.AccountEntity.Authority <= AuthorityType.Moderator).OrderByDescending(c => c.Act4Points).Take(top).ToListAsync()); + } + catch (Exception e) + { + Log.Error("GetTopPoints", e); + return new List(); + } + } + + /// + /// Returns first 43 occurences of highest Reputation + /// + /// + public async Task> GetTopReputation(int top = 43) + { + try + { + await using GameContext context = _contextFactory.CreateDbContext(); + return _mapper.Map(await context.Character.Where(c => c.AccountEntity.Authority <= AuthorityType.Moderator).OrderByDescending(c => c.Reput).Take(top).ToListAsync()); + } + catch (Exception e) + { + Log.Error("GetTopPoints", e); + return new List(); + } + } + + + public IEnumerable LoadByAccount(long accountId) + { + try + { + using GameContext context = _contextFactory.CreateDbContext(); + var characters = context.Character.Where(c => c.AccountId == accountId).OrderByDescending(c => c.Slot).ToList(); + return _mapper.Map(characters); + } + catch (Exception e) + { + Log.Error("LoadByAccount", e); + return null; + } + } + + public async Task> LoadByAccountAsync(long accountId) + { + try + { + await using GameContext context = _contextFactory.CreateDbContext(); + List characters = await context.Character.Where(c => c.AccountId == accountId).OrderByDescending(c => c.Slot).ToListAsync(); + return _mapper.Map(characters); + } + catch (Exception e) + { + Log.Error("LoadByAccount", e); + return null; + } + } + + public IEnumerable LoadAllCharactersByAccount(long accountId) + { + try + { + using GameContext context = _contextFactory.CreateDbContext(); + return _mapper.Map(context.Character.Where(c => c.AccountId.Equals(accountId)).OrderByDescending(c => c.Slot).ToList()); + } + catch (Exception e) + { + Log.Error($"LoadAllCharactersByAccount - AccountId: {accountId}", e); + return Enumerable.Empty(); + } + } + + public async Task> GetAllCharactersByMasterAccountIdAsync(Guid accountId) + { + try + { + await using GameContext context = _contextFactory.CreateDbContext(); + return _mapper.Map(await context.Character.Where(c => c.AccountEntity.MasterAccountId == accountId).ToListAsync()); + } + catch (Exception e) + { + Log.Error($"GetAllCharactersByMasterAccountIdAsync - AccountId: {accountId}", e); + return Enumerable.Empty(); + } + } + + public CharacterDTO GetById(long characterId) + { + try + { + using GameContext context = _contextFactory.CreateDbContext(); + return _mapper.Map(context.Character.FirstOrDefault(s => s.Id == characterId)); + } + catch (Exception e) + { + Log.Error($"LoadById - CharacterId: {characterId}", e); + return null; + } + } + + public async Task LoadByNameAsync(string name) + { + try + { + await using GameContext context = _contextFactory.CreateDbContext(); + DbCharacter character = await context.Character.FirstOrDefaultAsync(c => c.DeletedAt == null && EF.Functions.ILike(c.Name, name)); + if (character == null) + { + return null; + } + + return _mapper.Map(character); + } + catch (Exception e) + { + Log.Error($"LoadByNameAsync - Character name: {name}", e); + throw; + } + } + + public CharacterDTO LoadBySlot(long accountId, byte slot) + { + try + { + using GameContext context = _contextFactory.CreateDbContext(); + return _mapper.Map(context.Character.FirstOrDefault(c => c.AccountId == accountId && c.Slot == slot)); + } + catch (Exception e) + { + Log.Error($"LoadBySlot - AccountId: {accountId}, slot: {slot}", e); + return null; + } + } + + public async Task LoadBySlotAsync(long accountId, byte slot) + { + try + { + await using GameContext context = _contextFactory.CreateDbContext(); + return _mapper.Map(await context.Character.FirstOrDefaultAsync(c => c.AccountId == accountId && c.Slot == slot)); + } + catch (Exception e) + { + Log.Error($"LoadBySlot - AccountId: {accountId}, slot: {slot}", e); + return null; + } + } + + public async Task> GetTopByLevelAsync(int number) + { + try + { + await using GameContext context = _contextFactory.CreateDbContext(); + return _mapper.Map(await context.Character.Where(x => x.AccountEntity.Authority <= AuthorityType.Moderator).OrderByDescending(s => s.Level).Take(number).ToListAsync()); + } + catch (Exception e) + { + Log.Error("GetTopByLevel", e); + return null; + } + } + + public async Task> GetTopLevelByClassTypeAsync(ClassType classType, int number) + { + try + { + await using GameContext context = _contextFactory.CreateDbContext(); + return _mapper.Map(await context.Character.Where(c => c.AccountEntity.Authority <= AuthorityType.Moderator && c.Class == classType).OrderByDescending(s => s.Level).Take(number) + .ToListAsync()); + } + catch (Exception e) + { + Log.Error("GetTopLevelByClassType", e); + return new List(); + } + } + + public async Task> GetClassesCountAsync() + { + try + { + await using GameContext context = _contextFactory.CreateDbContext(); + return new Dictionary + { + { ClassType.Adventurer, await context.Character.CountAsync(x => x.Class == ClassType.Adventurer) }, + { ClassType.Archer, await context.Character.CountAsync(x => x.Class == ClassType.Archer) }, + { ClassType.Magician, await context.Character.CountAsync(x => x.Class == ClassType.Magician) }, + { ClassType.Swordman, await context.Character.CountAsync(x => x.Class == ClassType.Swordman) }, + { ClassType.Wrestler, await context.Character.CountAsync(x => x.Class == ClassType.Wrestler) } + }; + } + catch (Exception e) + { + Log.Error("GetClassesCount", e); + return new Dictionary(); + } + } + + public async Task> GetAllAsync() => await _repository.GetAllAsync(); + + public async Task GetByIdAsync(long id) => await _repository.GetByIdAsync(id); + + public async Task> GetByIdsAsync(IEnumerable ids) => await _repository.GetByIdsAsync(ids); + + public async Task SaveAsync(CharacterDTO obj) => await _repository.SaveAsync(obj); + + public async Task> SaveAsync(IReadOnlyList objs) => await _repository.SaveAsync(objs); + + public async Task DeleteByIdAsync(long id) + { + await _repository.DeleteByIdAsync(id); + } + + public async Task DeleteByIdsAsync(IEnumerable ids) + { + await _repository.DeleteByIdsAsync(ids); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.DB.EF/DAOs/CharacterRelationDAO.cs b/srcs/_plugins/Plugin.DB.EF/DAOs/CharacterRelationDAO.cs new file mode 100644 index 0000000..92089f3 --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/DAOs/CharacterRelationDAO.cs @@ -0,0 +1,121 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using PhoenixLib.DAL; +using PhoenixLib.Logging; +using Plugin.Database.DB; +using Plugin.Database.Entities.PlayersData; +using WingsEmu.DTOs.Relations; + +namespace Plugin.Database.DAOs +{ + public class CharacterRelationDAO : ICharacterRelationDAO + { + private readonly IDbContextFactory _contextFactory; + private readonly IMapper _mapper; + + public CharacterRelationDAO( + IMapper mapper, + IDbContextFactory contextFactory + ) + { + _mapper = mapper; + _contextFactory = contextFactory; + } + + public async Task GetRelationByCharacterIdAsync(long characterId, long targetId) + { + try + { + await using GameContext context = _contextFactory.CreateDbContext(); + CharacterRelationEntity entity = await context.Set().FindAsync(characterId, targetId); + return _mapper.Map(entity); + } + catch (Exception e) + { + Log.Error("GetRelationByCharacterIdAsync", e); + throw; + } + } + + public async Task SaveRelationsByCharacterIdAsync(long characterId, CharacterRelationDTO relations) + { + await using GameContext context = _contextFactory.CreateDbContext(); + + try + { + CharacterRelationEntity obj = _mapper.Map(relations); + CharacterRelationEntity entity = await context.Set().FindAsync(obj.CharacterId, obj.RelatedCharacterId); + + if (entity == null) + { + entity = obj; + entity = (await context.Set().AddAsync(entity)).Entity; + } + else + { + context.Entry(entity).CurrentValues.SetValues(obj); + } + + await context.SaveChangesAsync(); + } + catch (Exception e) + { + Log.Error("SaveRelationsByCharacterIdAsync", e); + throw; + } + } + + public async Task> LoadRelationsByCharacterIdAsync(long characterId) + { + await using GameContext context = _contextFactory.CreateDbContext(); + var relationsToReturn = new List(); + + try + { + List relations = await context.CharacterRelation.Where(x => x.CharacterId == characterId).ToListAsync(); + foreach (CharacterRelationEntity relation in relations) + { + relationsToReturn.Add(_mapper.Map(relation)); + } + + return relationsToReturn; + } + catch (Exception e) + { + Log.Error("LoadRelationsByCharacterIdAsync", e); + throw; + } + } + + public async Task RemoveRelationAsync(CharacterRelationDTO relation) + { + await using GameContext context = _contextFactory.CreateDbContext(); + + try + { + CharacterRelationEntity relationToRemove = await context.CharacterRelation.FirstOrDefaultAsync(x => x.CharacterId == relation.CharacterId + && x.RelatedCharacterId == relation.RelatedCharacterId && x.RelationType == relation.RelationType); + + if (relationToRemove == null) + { + return; + } + + context.Remove(relationToRemove); + await context.SaveChangesAsync(); + } + catch (Exception e) + { + Log.Error("RemoveRelationAsync", e); + throw; + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.DB.EF/DAOs/TimeSpaceRecordDao.cs b/srcs/_plugins/Plugin.DB.EF/DAOs/TimeSpaceRecordDao.cs new file mode 100644 index 0000000..1a3f48e --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/DAOs/TimeSpaceRecordDao.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using PhoenixLib.DAL; +using PhoenixLib.Logging; +using Plugin.Database.DB; +using Plugin.Database.Entities.ServerData; +using WingsAPI.Data.TimeSpace; + +namespace Plugin.Database.DAOs +{ + public class TimeSpaceRecordDao : ITimeSpaceRecordDao + { + private readonly IDbContextFactory _contextFactory; + private readonly IMapper _mapper; + + public TimeSpaceRecordDao(IMapper mapper, IDbContextFactory contextFactory) + { + _mapper = mapper; + _contextFactory = contextFactory; + } + + public async Task GetRecordById(long timeSpaceId) + { + try + { + await using GameContext context = _contextFactory.CreateDbContext(); + DbTimeSpaceRecord entity = await context.Set().FindAsync(timeSpaceId); + return _mapper.Map(entity); + } + catch (Exception e) + { + Log.Error("GetRecordById", e); + throw; + } + } + + public async Task SaveRecord(TimeSpaceRecordDto recordDto) + { + await using GameContext context = _contextFactory.CreateDbContext(); + try + { + DbTimeSpaceRecord obj = _mapper.Map(recordDto); + DbTimeSpaceRecord entity = await context.Set().FindAsync(obj.TimeSpaceId); + + if (entity == null) + { + entity = obj; + await context.Set().AddAsync(entity); + } + else + { + context.Update(obj); + } + + await context.SaveChangesAsync(); + } + catch (Exception e) + { + Log.Error("SaveRecord", e); + throw; + } + } + + public async Task> GetAllRecords() + { + await using GameContext context = _contextFactory.CreateDbContext(); + var list = new List(); + + try + { + List relations = await context.TimeSpaceRecords.ToListAsync(); + foreach (DbTimeSpaceRecord relation in relations) + { + list.Add(_mapper.Map(relation)); + } + + return list; + } + catch (Exception e) + { + Log.Error("GetAllRecords", e); + throw; + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.DB.EF/DB/Configs/AccountBansTypeConfiguration.cs b/srcs/_plugins/Plugin.DB.EF/DB/Configs/AccountBansTypeConfiguration.cs new file mode 100644 index 0000000..e6d0b5b --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/DB/Configs/AccountBansTypeConfiguration.cs @@ -0,0 +1,17 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Plugin.Database.Entities.Account; + +namespace Plugin.Database.DB.Configs +{ + public class AccountBansTypeConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder + .HasOne(e => e.AccountEntity) + .WithMany(e => e.AccountBans) + .HasForeignKey(s => s.AccountId); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.DB.EF/DB/Configs/AccountPenaltyTypeConfiguration.cs b/srcs/_plugins/Plugin.DB.EF/DB/Configs/AccountPenaltyTypeConfiguration.cs new file mode 100644 index 0000000..8f7da69 --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/DB/Configs/AccountPenaltyTypeConfiguration.cs @@ -0,0 +1,17 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Plugin.Database.Entities.Account; + +namespace Plugin.Database.DB.Configs +{ + public class AccountPenaltyTypeConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder + .HasOne(e => e.AccountEntity) + .WithMany(e => e.AccountPenalties) + .HasForeignKey(s => s.AccountId); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.DB.EF/DB/Configs/AccountTypeConfiguration.cs b/srcs/_plugins/Plugin.DB.EF/DB/Configs/AccountTypeConfiguration.cs new file mode 100644 index 0000000..390de64 --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/DB/Configs/AccountTypeConfiguration.cs @@ -0,0 +1,40 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Plugin.Database.Entities.Account; + +namespace Plugin.Database.DB.Configs +{ + public class AccountTypeConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder + .Property(e => e.Password) + .IsUnicode(false); + + builder + .HasMany(e => e.Character) + .WithOne(e => e.AccountEntity) + .HasForeignKey(s => s.AccountId) + .OnDelete(DeleteBehavior.Cascade); + + builder + .HasMany(e => e.AccountBans) + .WithOne(e => e.AccountEntity) + .HasForeignKey(e => e.AccountId) + .OnDelete(DeleteBehavior.Cascade); + + builder + .HasMany(e => e.AccountPenalties) + .WithOne(e => e.AccountEntity) + .HasForeignKey(e => e.AccountId) + .OnDelete(DeleteBehavior.Cascade); + + builder + .HasMany(e => e.WarehouseItems) + .WithOne(e => e.Account) + .HasForeignKey(e => e.AccountId) + .OnDelete(DeleteBehavior.Cascade); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.DB.EF/DB/Configs/BaseAuditableEntityTypeConfiguration.cs b/srcs/_plugins/Plugin.DB.EF/DB/Configs/BaseAuditableEntityTypeConfiguration.cs new file mode 100644 index 0000000..d8f5ae6 --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/DB/Configs/BaseAuditableEntityTypeConfiguration.cs @@ -0,0 +1,18 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Plugin.Database.Entities; + +namespace Plugin.Database.DB.Configs +{ + public abstract class BaseAuditableEntityTypeConfiguration : IEntityTypeConfiguration + where T : class, IAuditableEntity + { + public void Configure(EntityTypeBuilder builder) + { + builder.HasQueryFilter(s => s.DeletedAt == null); + ConfigureEntity(builder); + } + + protected abstract void ConfigureEntity(EntityTypeBuilder builder); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.DB.EF/DB/Configs/CharacterBazaarItemEntityTypeConfiguration.cs b/srcs/_plugins/Plugin.DB.EF/DB/Configs/CharacterBazaarItemEntityTypeConfiguration.cs new file mode 100644 index 0000000..e456b67 --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/DB/Configs/CharacterBazaarItemEntityTypeConfiguration.cs @@ -0,0 +1,17 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Plugin.Database.Bazaar; + +namespace Plugin.Database.DB.Configs +{ + public class CharacterBazaarItemEntityTypeConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder + .HasOne(s => s.DbCharacter) + .WithMany(s => s.BazaarItem) + .HasForeignKey(s => s.CharacterId); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.DB.EF/DB/Configs/CharacterEntityTypeConfiguration.cs b/srcs/_plugins/Plugin.DB.EF/DB/Configs/CharacterEntityTypeConfiguration.cs new file mode 100644 index 0000000..039c2aa --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/DB/Configs/CharacterEntityTypeConfiguration.cs @@ -0,0 +1,63 @@ +// WingsEmu +// +// Developed by NosWings Team + +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Plugin.Database.Entities.PlayersData; + +namespace Plugin.Database.DB.Configs +{ + public class CharacterEntityTypeConfiguration : BaseAuditableEntityTypeConfiguration + { + protected override void ConfigureEntity(EntityTypeBuilder builder) + { + builder + .Property(e => e.Name) + .IsUnicode(false); + + + builder + .HasMany(e => e.BazaarItem) + .WithOne(e => e.DbCharacter) + .HasForeignKey(e => e.CharacterId) + .OnDelete(DeleteBehavior.Cascade); + + builder + .HasMany(e => e.SourceRelations) + .WithOne(e => e.Source) + .HasForeignKey(e => e.CharacterId) + .OnDelete(DeleteBehavior.Cascade); + + builder + .HasMany(e => e.TargetRelations) + .WithOne(e => e.Target) + .HasForeignKey(e => e.RelatedCharacterId) + .OnDelete(DeleteBehavior.Cascade); + + builder + .HasMany(e => e.ReceivedMails) + .WithOne(e => e.Receiver) + .HasForeignKey(e => e.ReceiverId) + .OnDelete(DeleteBehavior.Cascade); + + builder + .HasMany(e => e.SentNotes) + .WithOne(e => e.Sender) + .HasForeignKey(e => e.SenderId) + .OnDelete(DeleteBehavior.Cascade); + + builder + .HasMany(e => e.ReceivedNotes) + .WithOne(e => e.Receiver) + .HasForeignKey(e => e.ReceiverId) + .OnDelete(DeleteBehavior.Cascade); + + builder + .HasMany(s => s.FamilyCharacter) + .WithOne(s => s.DbCharacter) + .HasForeignKey(s => s.CharacterId) + .OnDelete(DeleteBehavior.SetNull); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.DB.EF/DB/Configs/CharacterRelationEntityTypeConfiguration.cs b/srcs/_plugins/Plugin.DB.EF/DB/Configs/CharacterRelationEntityTypeConfiguration.cs new file mode 100644 index 0000000..a54cc12 --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/DB/Configs/CharacterRelationEntityTypeConfiguration.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Plugin.Database.Entities.PlayersData; + +namespace Plugin.Database.DB.Configs +{ + public class CharacterRelationEntityTypeConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(s => new { s.CharacterId, s.RelatedCharacterId }); + + builder.HasOne(s => s.Source) + .WithMany(s => s.SourceRelations) + .HasForeignKey(s => s.CharacterId); + + builder.HasOne(s => s.Target) + .WithMany(s => s.TargetRelations) + .HasForeignKey(s => s.RelatedCharacterId); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.DB.EF/DB/Configs/DbTimeSpaceRecordTypeConfiguration.cs b/srcs/_plugins/Plugin.DB.EF/DB/Configs/DbTimeSpaceRecordTypeConfiguration.cs new file mode 100644 index 0000000..ea99e01 --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/DB/Configs/DbTimeSpaceRecordTypeConfiguration.cs @@ -0,0 +1,13 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Plugin.Database.Entities.ServerData; + +namespace Plugin.Database.DB.Configs +{ + public class DbTimeSpaceRecordTypeConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.DB.EF/DB/DatabaseConfiguration.cs b/srcs/_plugins/Plugin.DB.EF/DB/DatabaseConfiguration.cs new file mode 100644 index 0000000..2bacf0d --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/DB/DatabaseConfiguration.cs @@ -0,0 +1,46 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; + +namespace Plugin.Database.DB +{ + public class DatabaseConfiguration + { + public DatabaseConfiguration() + { + Ip = Environment.GetEnvironmentVariable("DATABASE_IP") ?? "localhost"; + Username = Environment.GetEnvironmentVariable("DATABASE_USER") ?? "postgres"; + Password = Environment.GetEnvironmentVariable("DATABASE_PASSWORD") ?? "VaNOSilla2022"; + Database = Environment.GetEnvironmentVariable("DATABASE_NAME") ?? "game"; + WriteBufferSize = Convert.ToInt32(Environment.GetEnvironmentVariable("DATABASE_WRITE_BUFFER_SIZE") ?? "8192"); + ReadBufferSize = Convert.ToInt32(Environment.GetEnvironmentVariable("DATABASE_READ_BUFFER_SIZE") ?? "8192"); + IncludeErrorDetail = bool.Parse(Environment.GetEnvironmentVariable("DATABASE_ERROR_DETAIL") ?? "true"); + if (!ushort.TryParse(Environment.GetEnvironmentVariable("DATABASE_PORT") ?? "5432", out ushort port)) + { + port = 5432; + } + + Port = port; + } + + + public string Ip { get; } + public string Username { get; } + public string Password { get; } + public string Database { get; } + public ushort Port { get; } + public int WriteBufferSize { get; } + public int ReadBufferSize { get; } + public bool IncludeErrorDetail { get; } + + public override string ToString() => $"Host={Ip};Port={Port.ToString()}" + + $";Database={Database}" + + $";Username={Username}" + + $";Password={Password}" + + $";Read Buffer Size={ReadBufferSize.ToString()}" + + $";Write Buffer Size={WriteBufferSize.ToString()}" + + $";Include Error Detail={IncludeErrorDetail.ToString()}"; + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.DB.EF/DB/DatabaseSchemas.cs b/srcs/_plugins/Plugin.DB.EF/DB/DatabaseSchemas.cs new file mode 100644 index 0000000..16f926e --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/DB/DatabaseSchemas.cs @@ -0,0 +1,25 @@ +// WingsEmu +// +// Developed by NosWings Team + +namespace Plugin.Database.DB +{ + public class DatabaseSchemas + { + // auth + public const string CONFIG_AUTH = "_config_auth"; + + // core + public const string CLIENT_DATA_SCHEMA = "_datas_client"; + public const string LANGUAGES_DATA_SCHEMA = "_datas_langs"; + + // player + public const string ACCOUNTS = "accounts"; + public const string CHARACTERS = "characters"; + + // services + public const string FAMILIES = "families"; + public const string MAILS = "mails"; + public const string BAZAAR = "bazaar"; + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.DB.EF/DB/DesignTimeContextFactory.cs b/srcs/_plugins/Plugin.DB.EF/DB/DesignTimeContextFactory.cs new file mode 100644 index 0000000..80da71a --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/DB/DesignTimeContextFactory.cs @@ -0,0 +1,20 @@ +// WingsEmu +// +// Developed by NosWings Team + +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; + +namespace Plugin.Database.DB +{ + public class DesignTimeContextFactory : IDesignTimeDbContextFactory + { + public GameContext CreateDbContext(string[] args) + { + var optionsBuilder = new DbContextOptionsBuilder(); + + optionsBuilder.UseNpgsql(new DatabaseConfiguration().ToString()); + return new GameContext(optionsBuilder.Options); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.DB.EF/DB/GameContext.cs b/srcs/_plugins/Plugin.DB.EF/DB/GameContext.cs new file mode 100644 index 0000000..f5b1b8d --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/DB/GameContext.cs @@ -0,0 +1,144 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.ChangeTracking; +using Plugin.Database.Auth.ClientVersion; +using Plugin.Database.Auth.HWID; +using Plugin.Database.Bazaar; +using Plugin.Database.DB.Configs; +using Plugin.Database.Entities; +using Plugin.Database.Entities.Account; +using Plugin.Database.Entities.PlayersData; +using Plugin.Database.Entities.ServerData; +using Plugin.Database.Families; +using Plugin.Database.Mail; +using Plugin.Database.Warehouse; + +namespace Plugin.Database.DB +{ + public class GameContext : DbContext + { + public GameContext(DbContextOptions options) : base(options) + { + } + + public DbSet BlacklistedHwids { get; set; } + public DbSet AuthorizedClientVersions { get; set; } + + + public DbSet Account { get; set; } + public DbSet AccountWarehouseItems { get; set; } + public DbSet AccountBans { get; set; } + public DbSet AccountPenalties { get; set; } + + #region Bazaar + + public DbSet BazaarItem { get; set; } + + #endregion + + + public DbSet TimeSpaceRecords { get; set; } + + public override int SaveChanges() + { + UpdateSoftDeleteStatuses(); + return base.SaveChanges(); + } + + public override int SaveChanges(bool acceptAllChangesOnSuccess) + { + UpdateSoftDeleteStatuses(); + return base.SaveChanges(acceptAllChangesOnSuccess); + } + + public override async Task SaveChangesAsync(CancellationToken cancellationToken = default) + { + UpdateSoftDeleteStatuses(); + return await base.SaveChangesAsync(cancellationToken); + } + + public override async Task SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = new()) + { + UpdateSoftDeleteStatuses(); + return await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken); + } + + + private void UpdateSoftDeleteStatuses() + { + foreach (EntityEntry entry in ChangeTracker.Entries().Where(s => s.Entity is IAuditableEntity)) + { + var entity = (IAuditableEntity)entry.Entity; + switch (entry.State) + { + case EntityState.Modified: + entity.UpdatedAt = DateTime.UtcNow; + break; + case EntityState.Added: + entity.CreatedAt = DateTime.UtcNow; + break; + case EntityState.Deleted: + entry.State = EntityState.Modified; + entity.DeletedAt = DateTime.UtcNow; + break; + } + } + } + + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + // accounts + modelBuilder.ApplyConfiguration(new AccountTypeConfiguration()); + modelBuilder.ApplyConfiguration(new AccountPenaltyTypeConfiguration()); + modelBuilder.ApplyConfiguration(new AccountBansTypeConfiguration()); + modelBuilder.ApplyConfiguration(new AccountWarehouseItemEntityTypeConfiguration()); + + // player data + modelBuilder.ApplyConfiguration(new CharacterEntityTypeConfiguration()); + modelBuilder.ApplyConfiguration(new CharacterRelationEntityTypeConfiguration()); + modelBuilder.ApplyConfiguration(new CharacterBazaarItemEntityTypeConfiguration()); + modelBuilder.ApplyConfiguration(new DbTimeSpaceRecordTypeConfiguration()); + + + // families data + modelBuilder.ApplyConfiguration(new DbFamilyTypeConfiguration()); + modelBuilder.ApplyConfiguration(new DbFamilyCharacterTypeConfiguration()); + modelBuilder.ApplyConfiguration(new DbFamilyLogTypeConfiguration()); + modelBuilder.ApplyConfiguration(new FamilyWarehouseItemEntityTypeConfiguration()); + modelBuilder.ApplyConfiguration(new FamilyWarehouseLogEntityTypeConfiguration()); + + // mails + modelBuilder.ApplyConfiguration(new CharacterMailEntityTypeConfiguration()); + modelBuilder.ApplyConfiguration(new DbCharacterNoteEntityTypeConfiguration()); + } + + #region Character + + public DbSet Character { get; set; } + public DbSet CharacterRelation { get; set; } + public DbSet Mail { get; set; } + public DbSet Note { get; set; } + + #endregion + + #region Family + + public DbSet Family { get; set; } + public DbSet FamilyCharacter { get; set; } + public DbSet FamilyLog { get; set; } + public DbSet FamilyWarehouseItems { get; set; } + public DbSet FamilyWarehouseLogs { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.DB.EF/DatabasePlugin.cs b/srcs/_plugins/Plugin.DB.EF/DatabasePlugin.cs new file mode 100644 index 0000000..d38898e --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/DatabasePlugin.cs @@ -0,0 +1,118 @@ +// WingsEmu +// +// Developed by NosWings Team + +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Npgsql; +using PhoenixLib.DAL; +using Plugin.Database.Bazaar; +using Plugin.Database.DAOs; +using Plugin.Database.DB; +using Plugin.Database.Entities.Account; +using Plugin.Database.Entities.PlayersData; +using Plugin.Database.Extensions; +using Plugin.Database.Families; +using Plugin.Database.Mail; +using Plugin.Database.Mapping; +using Plugin.Database.Warehouse; +using WingsAPI.Data.Account; +using WingsAPI.Data.Bazaar; +using WingsAPI.Data.Character; +using WingsAPI.Data.Families; +using WingsAPI.Data.TimeSpace; +using WingsAPI.Data.Warehouse; +using WingsAPI.Plugins; +using WingsEmu.DTOs.Bazaar; +using WingsEmu.DTOs.Mails; +using WingsEmu.DTOs.Relations; + +namespace Plugin.Database +{ + public class DatabasePlugin : IDependencyInjectorPlugin + { + public string Name => nameof(DatabasePlugin); + + public void AddDependencies(IServiceCollection services) + { + NpgsqlConnection.GlobalTypeMapper.UseJsonNet(settings: new JsonSerializerSettings + { + Formatting = Formatting.None, + DefaultValueHandling = DefaultValueHandling.Ignore + }); + services.AddDbContextFactory((serviceProvider, options) => + { + DatabaseConfiguration conf = serviceProvider.GetRequiredService(); + options + .UseNpgsql(conf.ToString(), providerOptions => { providerOptions.EnableRetryOnFailure(); }) + .UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking) + .EnableServiceProviderCaching() + .ConfigureWarnings(s => s.Log( + (RelationalEventId.CommandExecuting, LogLevel.Debug), + (RelationalEventId.CommandExecuted, LogLevel.Debug) + )); + }); + services.AddSingleton(); + services.AddTransient(typeof(IMapper<,>), typeof(MapsterMapper<,>)); + + // accounts + services.AddAsyncLongRepository(); + services.AddTransient(); + + // accounts warehouse + services.AddTransient(); + + // accounts bans + services.AddAsyncLongRepository(); + services.TryAddTransient(); + + // accounts penalties + services.AddAsyncLongRepository(); + services.TryAddTransient(); + + // bazaar + services.AddAsyncLongRepository(); + services.TryAddTransient(); + + + // character + services.AddAsyncLongRepository(); + services.TryAddTransient(); + + // timespaces records + services.AddSingleton(); + + // relations + services.TryAddTransient(); + + // mails + services.AddAsyncLongRepository(); + services.TryAddTransient(); + + // notes + services.AddAsyncLongRepository(); + services.TryAddTransient(); + + + // families + services.AddAsyncLongRepository(); + services.TryAddTransient(); + + // families membership + services.AddAsyncLongRepository(); + services.TryAddTransient(); + + // families logs + services.AddAsyncLongRepository(); + services.TryAddTransient(); + + // family warehouses + services.TryAddTransient(); + services.TryAddTransient(); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.DB.EF/Entities/Account/AccountBanEntity.cs b/srcs/_plugins/Plugin.DB.EF/Entities/Account/AccountBanEntity.cs new file mode 100644 index 0000000..c1c79b1 --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/Entities/Account/AccountBanEntity.cs @@ -0,0 +1,32 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using PhoenixLib.DAL.EFCore.PGSQL; +using Plugin.Database.DB; + +namespace Plugin.Database.Entities.Account +{ + [Table("accounts_bans", Schema = DatabaseSchemas.ACCOUNTS)] + public class AccountBanEntity : ILongEntity + { + public long AccountId { get; set; } + + public string JudgeName { get; set; } + + public string TargetName { get; set; } + + public DateTime Start { get; set; } + + public DateTime? End { get; set; } + + public string Reason { get; set; } + + public string UnlockReason { get; set; } + + public virtual AccountEntity AccountEntity { get; set; } + + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long Id { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.DB.EF/Entities/Account/AccountEntity.cs b/srcs/_plugins/Plugin.DB.EF/Entities/Account/AccountEntity.cs new file mode 100644 index 0000000..07510b7 --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/Entities/Account/AccountEntity.cs @@ -0,0 +1,46 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using PhoenixLib.DAL.EFCore.PGSQL; +using Plugin.Database.DB; +using Plugin.Database.Entities.PlayersData; +using Plugin.Database.Warehouse; +using WingsEmu.DTOs.Account; + +namespace Plugin.Database.Entities.Account +{ + [Table("accounts", Schema = DatabaseSchemas.ACCOUNTS)] + public class AccountEntity : BaseAuditableEntity, ILongEntity + { + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public Guid MasterAccountId { get; set; } + + public AuthorityType Authority { get; set; } + + public AccountLanguage Language { get; set; } + + public long BankMoney { get; set; } + + public bool IsPrimaryAccount { get; set; } + + [MaxLength(255)] + public string Name { get; set; } + + [MaxLength(255)] + public string Password { get; set; } + + public virtual ICollection Character { get; set; } + public virtual ICollection AccountBans { get; set; } + public virtual ICollection AccountPenalties { get; set; } + public virtual IEnumerable WarehouseItems { get; set; } + + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long Id { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.DB.EF/Entities/Account/AccountPenaltyEntity.cs b/srcs/_plugins/Plugin.DB.EF/Entities/Account/AccountPenaltyEntity.cs new file mode 100644 index 0000000..c1bb894 --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/Entities/Account/AccountPenaltyEntity.cs @@ -0,0 +1,35 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using PhoenixLib.DAL.EFCore.PGSQL; +using Plugin.Database.DB; +using WingsEmu.Packets.Enums; + +namespace Plugin.Database.Entities.Account +{ + [Table("accounts_penalties", Schema = DatabaseSchemas.ACCOUNTS)] + public class AccountPenaltyEntity : ILongEntity + { + public long AccountId { get; set; } + + public string JudgeName { get; set; } + + public string TargetName { get; set; } + + public DateTime Start { get; set; } + + public int? RemainingTime { get; set; } + + public PenaltyType PenaltyType { get; set; } + + public string Reason { get; set; } + + public string UnlockReason { get; set; } + + public virtual AccountEntity AccountEntity { get; set; } + + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long Id { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.DB.EF/Entities/BaseAuditableEntity.cs b/srcs/_plugins/Plugin.DB.EF/Entities/BaseAuditableEntity.cs new file mode 100644 index 0000000..890ae05 --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/Entities/BaseAuditableEntity.cs @@ -0,0 +1,17 @@ +using System; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Plugin.Database.Entities +{ + public abstract class BaseAuditableEntity : IAuditableEntity + { + [Column(Order = 50)] + public DateTime? CreatedAt { get; set; } + + [Column(Order = 51)] + public DateTime? UpdatedAt { get; set; } + + [Column(Order = 52)] + public DateTime? DeletedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.DB.EF/Entities/IAuditableEntity.cs b/srcs/_plugins/Plugin.DB.EF/Entities/IAuditableEntity.cs new file mode 100644 index 0000000..71dbdaa --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/Entities/IAuditableEntity.cs @@ -0,0 +1,15 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; + +namespace Plugin.Database.Entities +{ + public interface IAuditableEntity + { + DateTime? DeletedAt { get; set; } + DateTime? UpdatedAt { get; set; } + DateTime? CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.DB.EF/Entities/PlayersData/CharacterRelationEntity.cs b/srcs/_plugins/Plugin.DB.EF/Entities/PlayersData/CharacterRelationEntity.cs new file mode 100644 index 0000000..2911221 --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/Entities/PlayersData/CharacterRelationEntity.cs @@ -0,0 +1,25 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.ComponentModel.DataAnnotations.Schema; +using Plugin.Database.DB; +using WingsEmu.Packets.Enums.Relations; + +namespace Plugin.Database.Entities.PlayersData +{ + [Table("characters_relations", Schema = DatabaseSchemas.CHARACTERS)] + public class CharacterRelationEntity + { + public long CharacterId { get; set; } + public long RelatedCharacterId { get; set; } + + public string RelatedName { get; set; } + + public CharacterRelationType RelationType { get; set; } + + public virtual DbCharacter Source { get; set; } + + public virtual DbCharacter Target { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.DB.EF/Entities/PlayersData/DbCharacter.cs b/srcs/_plugins/Plugin.DB.EF/Entities/PlayersData/DbCharacter.cs new file mode 100644 index 0000000..99f1fa0 --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/Entities/PlayersData/DbCharacter.cs @@ -0,0 +1,228 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using PhoenixLib.DAL.EFCore.PGSQL; +using Plugin.Database.Bazaar; +using Plugin.Database.DB; +using Plugin.Database.Entities.Account; +using Plugin.Database.Families; +using Plugin.Database.Mail; +using WingsAPI.Data.Character; +using WingsAPI.Data.Miniland; +using WingsAPI.Packets.Enums; +using WingsEmu.DTOs.Bonus; +using WingsEmu.DTOs.Buffs; +using WingsEmu.DTOs.Inventory; +using WingsEmu.DTOs.Mates; +using WingsEmu.DTOs.Quests; +using WingsEmu.DTOs.Quicklist; +using WingsEmu.DTOs.Respawns; +using WingsEmu.DTOs.Skills; +using WingsEmu.DTOs.Titles; +using WingsEmu.Game._enum; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Character; + +namespace Plugin.Database.Entities.PlayersData +{ + [Table("characters", Schema = DatabaseSchemas.CHARACTERS)] + public class DbCharacter : BaseAuditableEntity, ILongEntity + { + public long AccountId { get; set; } + + public int Act4Dead { get; set; } + + public int Act4Kill { get; set; } + public int Act4Points { get; set; } + + public int ArenaWinner { get; set; } + + [MaxLength(255)] + public string Biography { get; set; } + + public bool BuffBlocked { get; set; } + + public ClassType Class { get; set; } + public short Compliment { get; set; } + + public float Dignity { get; set; } + + public bool EmoticonsBlocked { get; set; } + + public bool ExchangeBlocked { get; set; } + + public FactionType Faction { get; set; } + + public bool FamilyRequestBlocked { get; set; } + + public bool FriendRequestBlocked { get; set; } + + public GenderType Gender { get; set; } + + public long Gold { get; set; } + + public bool GroupRequestBlocked { get; set; } + + public HairColorType HairColor { get; set; } + + public HairStyleType HairStyle { get; set; } + + public bool HeroChatBlocked { get; set; } + + public byte HeroLevel { get; set; } + + public long HeroXp { get; set; } + + public int Hp { get; set; } + + public bool HpBlocked { get; set; } + + public bool IsPetAutoRelive { get; set; } + + public bool IsPartnerAutoRelive { get; set; } + + public byte JobLevel { get; set; } + + public long JobLevelXp { get; set; } + + public byte Level { get; set; } + + public long LevelXp { get; set; } + + public int MapId { get; set; } + + public short MapX { get; set; } + + public short MapY { get; set; } + + public int MasterPoints { get; set; } + + public int MasterTicket { get; set; } + + public byte MaxPartnerCount { get; set; } + + public byte MaxPetCount { get; set; } + + public bool MinilandInviteBlocked { get; set; } + + [MaxLength(255)] + public string MinilandMessage { get; set; } + + public short MinilandPoint { get; set; } + + public MinilandState MinilandState { get; set; } + + public bool MouseAimLock { get; set; } + + public int Mp { get; set; } + + [MaxLength(25)] + public string Prefix { get; set; } + + [MaxLength(30)] + public string Name { get; set; } + + public bool QuickGetUp { get; set; } + + public bool HideHat { get; set; } + + public bool UiBlocked { get; set; } + public long RagePoint { get; set; } + + public long Reput { get; set; } + public byte Slot { get; set; } + + public int SpPointsBonus { get; set; } + + public int SpPointsBasic { get; set; } + + public int TalentLose { get; set; } + + public int TalentSurrender { get; set; } + + public int TalentWin { get; set; } + + public bool WhisperBlocked { get; set; } + + public Act5RespawnType Act5RespawnType { get; set; } + + [Column(TypeName = "jsonb")] + public List PartnerInventory { get; set; } = new(); + + [Column(TypeName = "jsonb")] + public List NosMates { get; set; } + + [Column(TypeName = "jsonb")] + public List PartnerWarehouse { get; set; } + + [Column(TypeName = "jsonb")] + public List Bonus { get; set; } + + [Column(TypeName = "jsonb")] + public List StaticBuffs { get; set; } + + [Column(TypeName = "jsonb")] + public List Quicklist { get; set; } + + [Column(TypeName = "jsonb")] + public List LearnedSkills { get; set; } + + [Column(TypeName = "jsonb")] + public List Titles { get; set; } + + [Column(TypeName = "jsonb")] + public List CompletedScripts { get; set; } + + [Column(TypeName = "jsonb")] + public List CompletedQuests { get; set; } + + [Column(TypeName = "jsonb")] + public List ActiveQuests { get; set; } + + [Column(TypeName = "jsonb")] + public List CompletedPeriodicQuests { get; set; } + + [Column(TypeName = "jsonb")] + public List MinilandObjects { get; set; } + + [Column(TypeName = "jsonb")] + public CharacterReturnDto ReturnPoint { get; set; } + + [Column(TypeName = "jsonb")] + public List Inventory { get; set; } + + [Column(TypeName = "jsonb")] + public List EquippedStuffs { get; set; } + + [Column(TypeName = "jsonb")] + public CharacterLifetimeStatsDto LifetimeStats { get; set; } + + public RespawnType RespawnType { get; set; } + + [Column(TypeName = "jsonb")] + public HashSet CompletedTimeSpaces { get; set; } + + [Column(TypeName = "jsonb")] + public CharacterRaidRestrictionDto RaidRestrictionDto { get; set; } + + [Column(TypeName = "jsonb")] + public RainbowBattleLeaverBusterDto RainbowBattleLeaverBusterDto { get; set; } + + public virtual ICollection BazaarItem { get; set; } + public virtual ICollection FamilyCharacter { get; set; } + public virtual ICollection SourceRelations { get; set; } + public virtual ICollection TargetRelations { get; set; } + public virtual ICollection SentNotes { get; set; } + public virtual ICollection ReceivedNotes { get; set; } + public virtual ICollection ReceivedMails { get; set; } + public virtual AccountEntity AccountEntity { get; set; } + + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long Id { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.DB.EF/Entities/ServerData/DbTimeSpaceRecord.cs b/srcs/_plugins/Plugin.DB.EF/Entities/ServerData/DbTimeSpaceRecord.cs new file mode 100644 index 0000000..a546f5b --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/Entities/ServerData/DbTimeSpaceRecord.cs @@ -0,0 +1,21 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Plugin.Database.DB; + +namespace Plugin.Database.Entities.ServerData +{ + [Table("time_space_records", Schema = DatabaseSchemas.CHARACTERS)] + public class DbTimeSpaceRecord + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public long TimeSpaceId { get; set; } + + public string CharacterName { get; set; } + + public long Record { get; set; } + + public DateTime Date { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.DB.EF/Extensions/DependencyInjectionExtensions.cs b/srcs/_plugins/Plugin.DB.EF/Extensions/DependencyInjectionExtensions.cs new file mode 100644 index 0000000..547e912 --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/Extensions/DependencyInjectionExtensions.cs @@ -0,0 +1,53 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using PhoenixLib.DAL; +using PhoenixLib.DAL.EFCore.PGSQL; +using PhoenixLib.DAL.EFCore.PGSQL.Extensions; +using Plugin.Database.Auth.ClientVersion; +using Plugin.Database.Auth.HWID; +using Plugin.Database.DB; +using WingsAPI.Communication.Auth; +using Z.EntityFramework.Extensions; + +namespace Plugin.Database.Extensions +{ + public static class DependencyInjectionExtensions + { + public static void UseZEfCoreExtensions(this IApplicationBuilder app) + { + EntityFrameworkManager.ContextFactory = context => app.ApplicationServices.GetRequiredService>().CreateDbContext(); + } + + internal static void AddAsyncLongRepository(this IServiceCollection services) + where TDto : class, ILongDto, new() + where TEntity : class, ILongEntity, new() + { + services.TryAddLongRepository(); + } + + internal static void AddAsyncIntRepository(this IServiceCollection services) + where TDto : class, IIntDto, new() + where TEntity : class, IIntEntity, new() + { + services.TryAddIntRepository(); + } + + internal static void AddAsyncUuidRepository(this IServiceCollection services) + where TDto : class, IUuidDto, new() + where TEntity : class, IUuidEntity, new() + { + services.TryAddUuidRepository(); + } + + public static void AddGameAuthDataAccess(this IServiceCollection services) + { + // client version + services.AddAsyncLongRepository(); + services.TryAddTransient(); + // hwid + services.TryAddTransient(); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.DB.EF/Extensions/GameContextFactoryExtensions.cs b/srcs/_plugins/Plugin.DB.EF/Extensions/GameContextFactoryExtensions.cs new file mode 100644 index 0000000..1e75855 --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/Extensions/GameContextFactoryExtensions.cs @@ -0,0 +1,32 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using PhoenixLib.Logging; +using Plugin.Database.DB; + +namespace Plugin.Database.Extensions +{ + public static class GameContextFactoryExtensions + { + public static async Task TryMigrateAsync(this IDbContextFactory contextFactory) + { + await using GameContext context = contextFactory.CreateDbContext(); + try + { + await context.Database.MigrateAsync(); + Log.Info("DATABASE_INITIALIZED"); + } + catch (Exception ex) + { + Log.Error("DATABASE_NOT_UPTODATE", ex); + return false; + } + + return true; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.DB.EF/Families/DbFamily.cs b/srcs/_plugins/Plugin.DB.EF/Families/DbFamily.cs new file mode 100644 index 0000000..2e0c379 --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/Families/DbFamily.cs @@ -0,0 +1,68 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using PhoenixLib.DAL.EFCore.PGSQL; +using Plugin.Database.DB; +using Plugin.Database.Entities; +using WingsAPI.Data.Families; +using WingsAPI.Packets.Enums.Families; +using WingsEmu.Packets.Enums.Character; + +namespace Plugin.Database.Families +{ + [Table("families", Schema = DatabaseSchemas.FAMILIES)] + public class DbFamily : BaseAuditableEntity, ILongEntity + { + [MinLength(3)] + [MaxLength(20)] + public string Name { get; set; } + + public byte Level { get; set; } + + public long Experience { get; set; } + + public byte Faction { get; set; } + + public GenderType HeadGender { get; set; } + + [MaxLength(50)] + public string Message { get; set; } + + public FamilyWarehouseAuthorityType AssistantWarehouseAuthorityType { get; set; } + + public FamilyWarehouseAuthorityType MemberWarehouseAuthorityType { get; set; } + + public bool AssistantCanGetHistory { get; set; } + + public bool AssistantCanInvite { get; set; } + + public bool AssistantCanNotice { get; set; } + + public bool AssistantCanShout { get; set; } + + public bool MemberCanGetHistory { get; set; } + + [Column(TypeName = "jsonb")] + public FamilyUpgradeDto Upgrades { get; set; } + + [Column(TypeName = "jsonb")] + public FamilyAchievementsDto Achievements { get; set; } + + [Column(TypeName = "jsonb")] + public FamilyMissionsDto Missions { get; set; } + + public virtual ICollection FamilyCharacters { get; set; } + + public virtual ICollection FamilyLogs { get; set; } + public virtual ICollection WarehouseItems { get; set; } + public virtual FamilyWarehouseLogEntity WarehouseLogs { get; set; } + + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long Id { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.DB.EF/Families/DbFamilyCharacterTypeConfiguration.cs b/srcs/_plugins/Plugin.DB.EF/Families/DbFamilyCharacterTypeConfiguration.cs new file mode 100644 index 0000000..7baab96 --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/Families/DbFamilyCharacterTypeConfiguration.cs @@ -0,0 +1,20 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Plugin.Database.DB.Configs; + +namespace Plugin.Database.Families +{ + public class DbFamilyCharacterTypeConfiguration : BaseAuditableEntityTypeConfiguration + { + protected override void ConfigureEntity(EntityTypeBuilder builder) + { + builder.HasKey(s => s.Id); + + builder + .HasOne(s => s.Family) + .WithMany(s => s.FamilyCharacters) + .HasForeignKey(s => s.FamilyId) + .OnDelete(DeleteBehavior.NoAction); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.DB.EF/Families/DbFamilyLogEntity.cs b/srcs/_plugins/Plugin.DB.EF/Families/DbFamilyLogEntity.cs new file mode 100644 index 0000000..2545cbf --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/Families/DbFamilyLogEntity.cs @@ -0,0 +1,43 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using PhoenixLib.DAL.EFCore.PGSQL; +using Plugin.Database.DB; +using Plugin.Database.Entities; +using WingsEmu.Packets.Enums.Families; + +namespace Plugin.Database.Families +{ + [Table("families_logs", Schema = DatabaseSchemas.FAMILIES)] + public class DbFamilyLog : BaseAuditableEntity, ILongEntity + { + public long FamilyId { get; set; } + + public FamilyLogType FamilyLogType { get; set; } + + public DateTime Timestamp { get; set; } + + [MaxLength(32)] + public string Actor { get; set; } + + [MaxLength(16)] + public string Argument1 { get; set; } + + [MaxLength(16)] + public string Argument2 { get; set; } + + [MaxLength(16)] + public string Argument3 { get; set; } + + [ForeignKey(nameof(FamilyId))] + public virtual DbFamily Family { get; set; } + + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long Id { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.DB.EF/Families/DbFamilyLogTypeConfiguration.cs b/srcs/_plugins/Plugin.DB.EF/Families/DbFamilyLogTypeConfiguration.cs new file mode 100644 index 0000000..0076416 --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/Families/DbFamilyLogTypeConfiguration.cs @@ -0,0 +1,20 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Plugin.Database.DB.Configs; + +namespace Plugin.Database.Families +{ + public class DbFamilyLogTypeConfiguration : BaseAuditableEntityTypeConfiguration + { + protected override void ConfigureEntity(EntityTypeBuilder builder) + { + builder.HasKey(s => s.Id); + + builder + .HasOne(s => s.Family) + .WithMany(s => s.FamilyLogs) + .HasForeignKey(s => s.FamilyId) + .OnDelete(DeleteBehavior.NoAction); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.DB.EF/Families/DbFamilyMembership.cs b/srcs/_plugins/Plugin.DB.EF/Families/DbFamilyMembership.cs new file mode 100644 index 0000000..c5a068b --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/Families/DbFamilyMembership.cs @@ -0,0 +1,45 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using PhoenixLib.DAL.EFCore.PGSQL; +using Plugin.Database.DB; +using Plugin.Database.Entities; +using Plugin.Database.Entities.PlayersData; +using WingsEmu.Packets.Enums.Families; + +namespace Plugin.Database.Families +{ + [Table("families_memberships", Schema = DatabaseSchemas.FAMILIES)] + public class DbFamilyMembership : BaseAuditableEntity, ILongEntity + { + public long? CharacterId { get; set; } + public long FamilyId { get; set; } + + public FamilyAuthority Authority { get; set; } + + [MaxLength(50)] + public string DailyMessage { get; set; } + + public long Experience { get; set; } + + public FamilyTitle Title { get; set; } + + public DateTime JoinDate { get; set; } + + public DateTime LastOnlineDate { get; set; } + + [ForeignKey(nameof(CharacterId))] + public virtual DbCharacter DbCharacter { get; set; } + + [ForeignKey(nameof(FamilyId))] + public virtual DbFamily Family { get; set; } + + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long Id { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.DB.EF/Families/DbFamilyTypeConfiguration.cs b/srcs/_plugins/Plugin.DB.EF/Families/DbFamilyTypeConfiguration.cs new file mode 100644 index 0000000..aad984d --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/Families/DbFamilyTypeConfiguration.cs @@ -0,0 +1,39 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Plugin.Database.DB.Configs; + +namespace Plugin.Database.Families +{ + public class DbFamilyTypeConfiguration : BaseAuditableEntityTypeConfiguration + { + protected override void ConfigureEntity(EntityTypeBuilder builder) + { + builder.HasKey(s => s.Id); + + builder + .Property(s => s.Name) + .HasMaxLength(30) + .IsRequired() + .IsUnicode(); + + + builder + .HasMany(s => s.FamilyCharacters) + .WithOne(s => s.Family) + .HasForeignKey(s => s.FamilyId) + .OnDelete(DeleteBehavior.Cascade); + + builder + .HasMany(s => s.FamilyLogs) + .WithOne(s => s.Family) + .HasForeignKey(s => s.FamilyId) + .OnDelete(DeleteBehavior.Cascade); + + builder + .HasMany(s => s.WarehouseItems) + .WithOne(s => s.Family) + .HasForeignKey(s => s.FamilyId) + .OnDelete(DeleteBehavior.Cascade); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.DB.EF/Families/FamilyDAO.cs b/srcs/_plugins/Plugin.DB.EF/Families/FamilyDAO.cs new file mode 100644 index 0000000..5f1038e --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/Families/FamilyDAO.cs @@ -0,0 +1,63 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using PhoenixLib.DAL; +using PhoenixLib.Logging; +using Plugin.Database.DB; +using WingsAPI.Data.Families; + +namespace Plugin.Database.Families +{ + public class FamilyDAO : IFamilyDAO + { + private readonly IDbContextFactory _contextFactory; + private readonly IMapper _mapper; + private readonly IGenericAsyncLongRepository _repository; + + public FamilyDAO(IMapper mapper, IDbContextFactory contextFactory, IGenericAsyncLongRepository repository) + { + _mapper = mapper; + _contextFactory = contextFactory; + _repository = repository; + } + + public async Task GetByNameAsync(string reqName) + { + try + { + await using GameContext context = _contextFactory.CreateDbContext(); + return _mapper.Map(await context.Family.FirstOrDefaultAsync(s => EF.Functions.ILike(s.Name, $"%{reqName}%"))); + } + catch (Exception e) + { + Log.Error("GetByNameAsync", e); + throw; + } + } + + public async Task> GetAllAsync() => await _repository.GetAllAsync(); + + public async Task GetByIdAsync(long id) => await _repository.GetByIdAsync(id); + + public async Task> GetByIdsAsync(IEnumerable ids) => await _repository.GetByIdsAsync(ids); + + public async Task SaveAsync(FamilyDTO obj) => await _repository.SaveAsync(obj); + + public async Task> SaveAsync(IReadOnlyList objs) => await _repository.SaveAsync(objs); + + public async Task DeleteByIdAsync(long id) + { + await _repository.DeleteByIdAsync(id); + } + + public async Task DeleteByIdsAsync(IEnumerable ids) + { + await _repository.DeleteByIdsAsync(ids); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.DB.EF/Families/FamilyLogDAO.cs b/srcs/_plugins/Plugin.DB.EF/Families/FamilyLogDAO.cs new file mode 100644 index 0000000..235a384 --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/Families/FamilyLogDAO.cs @@ -0,0 +1,66 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using PhoenixLib.DAL; +using PhoenixLib.Logging; +using Plugin.Database.DB; +using WingsAPI.Data.Families; + +namespace Plugin.Database.Families +{ + public class FamilyLogDAO : IFamilyLogDAO + { + private readonly IDbContextFactory _contextFactory; + private readonly IMapper _mapper; + private readonly IGenericAsyncLongRepository _repository; + + public FamilyLogDAO(IMapper mapper, IDbContextFactory contextFactory, IGenericAsyncLongRepository repository) + { + _mapper = mapper; + _contextFactory = contextFactory; + _repository = repository; + } + + + public async Task> GetLogsByFamilyIdAsync(long familyId) + { + try + { + await using GameContext context = _contextFactory.CreateDbContext(); + List tmp = await context.FamilyLog.Where(s => s.FamilyId.Equals(familyId)).OrderByDescending(s => s.Timestamp).Take(200).ToListAsync(); + return _mapper.Map(tmp); + } + catch (Exception e) + { + Log.Error("LoadById", e); + return null; + } + } + + public async Task> GetAllAsync() => await _repository.GetAllAsync(); + + public async Task GetByIdAsync(long id) => await _repository.GetByIdAsync(id); + + public async Task> GetByIdsAsync(IEnumerable ids) => await _repository.GetByIdsAsync(ids); + + public async Task SaveAsync(FamilyLogDto obj) => await _repository.SaveAsync(obj); + + public async Task> SaveAsync(IReadOnlyList objs) => await _repository.SaveAsync(objs); + + public async Task DeleteByIdAsync(long id) + { + await _repository.DeleteByIdAsync(id); + } + + public async Task DeleteByIdsAsync(IEnumerable ids) + { + await _repository.DeleteByIdsAsync(ids); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.DB.EF/Families/FamilyMembershipDao.cs b/srcs/_plugins/Plugin.DB.EF/Families/FamilyMembershipDao.cs new file mode 100644 index 0000000..8fbe002 --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/Families/FamilyMembershipDao.cs @@ -0,0 +1,81 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using PhoenixLib.DAL; +using PhoenixLib.Logging; +using Plugin.Database.DB; +using WingsAPI.Data.Families; + +namespace Plugin.Database.Families +{ + public class FamilyMembershipDao : IFamilyMembershipDao + { + private readonly IDbContextFactory _contextFactory; + private readonly IMapper _mapper; + private readonly IGenericAsyncLongRepository _repository; + + public FamilyMembershipDao(IMapper mapper, IDbContextFactory contextFactory, IGenericAsyncLongRepository repository) + { + _mapper = mapper; + _contextFactory = contextFactory; + _repository = repository; + } + + public async Task GetByCharacterIdAsync(long characterId) + { + try + { + await using GameContext context = _contextFactory.CreateDbContext(); + DbFamilyMembership dbFamilyMembership = await context.FamilyCharacter.FirstOrDefaultAsync(c => c.CharacterId == characterId); + + return _mapper.Map(dbFamilyMembership); + } + catch (Exception e) + { + Log.Error("GetByCharacterIdAsync", e); + return null; + } + } + + public async Task> GetByFamilyIdAsync(long familyId) + { + try + { + await using GameContext context = _contextFactory.CreateDbContext(); + List familyCharacter = await context.FamilyCharacter.Where(fc => fc.FamilyId.Equals(familyId)).ToListAsync(); + return _mapper.Map(familyCharacter); + } + catch (Exception e) + { + Log.Error("GetByFamilyIdAsync", e); + return null; + } + } + + public async Task> GetAllAsync() => await _repository.GetAllAsync(); + + public async Task GetByIdAsync(long id) => await _repository.GetByIdAsync(id); + + public async Task> GetByIdsAsync(IEnumerable ids) => await _repository.GetByIdsAsync(ids); + + public async Task SaveAsync(FamilyMembershipDto obj) => await _repository.SaveAsync(obj); + + public async Task> SaveAsync(IReadOnlyList objs) => await _repository.SaveAsync(objs); + + public async Task DeleteByIdAsync(long id) + { + await _repository.DeleteByIdAsync(id); + } + + public async Task DeleteByIdsAsync(IEnumerable ids) + { + await _repository.DeleteByIdsAsync(ids); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.DB.EF/Families/FamilyWarehouseItemDao.cs b/srcs/_plugins/Plugin.DB.EF/Families/FamilyWarehouseItemDao.cs new file mode 100644 index 0000000..de5022c --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/Families/FamilyWarehouseItemDao.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using PhoenixLib.DAL; +using PhoenixLib.Logging; +using Plugin.Database.DB; +using WingsAPI.Data.Families; + +namespace Plugin.Database.Families +{ + public class FamilyWarehouseItemDao : IFamilyWarehouseItemDao + { + private readonly IDbContextFactory _contextFactory; + private readonly IMapper _mapper; + + public FamilyWarehouseItemDao(IDbContextFactory contextFactory, IMapper mapper) + { + _contextFactory = contextFactory; + _mapper = mapper; + } + + public async Task SaveAsync(IReadOnlyList objs) + { + try + { + IEnumerable entities = _mapper.Map(objs); + await using GameContext context = _contextFactory.CreateDbContext(); + await context.FamilyWarehouseItems.BulkMergeAsync(entities); + return await context.SaveChangesAsync(); + } + catch (Exception e) + { + Log.Error("[FAMILY_WAREHOUSE_ITEM_DAO][SaveAsync] ", e); + throw; + } + } + + public async Task DeleteAsync(IEnumerable objs) + { + try + { + IEnumerable entities = _mapper.Map(objs); + await using GameContext context = _contextFactory.CreateDbContext(); + await context.FamilyWarehouseItems.BulkDeleteAsync(entities); + return await context.SaveChangesAsync(); + } + catch (Exception e) + { + Log.Error("[FAMILY_WAREHOUSE_ITEM_DAO][DeleteAsync] ", e); + throw; + } + } + + public async Task> GetByFamilyIdAsync(long familyId) + { + try + { + await using GameContext context = _contextFactory.CreateDbContext(); + IEnumerable items = await context.FamilyWarehouseItems.Where(s => s.FamilyId == familyId).ToListAsync(); + return _mapper.Map(items); + } + catch (Exception e) + { + Log.Error("[FAMILY_WAREHOUSE_ITEM_DAO][GetByFamilyIdAsync] ", e); + throw; + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.DB.EF/Families/FamilyWarehouseItemEntity.cs b/srcs/_plugins/Plugin.DB.EF/Families/FamilyWarehouseItemEntity.cs new file mode 100644 index 0000000..c197255 --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/Families/FamilyWarehouseItemEntity.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations.Schema; +using Plugin.Database.DB; +using Plugin.Database.Entities; +using WingsEmu.DTOs.Items; + +namespace Plugin.Database.Families +{ + [Table("families_warehouses", Schema = DatabaseSchemas.FAMILIES)] + public class FamilyWarehouseItemEntity : BaseAuditableEntity + { + public long FamilyId { get; set; } + + public short Slot { get; set; } + + [Column(TypeName = "jsonb")] + public ItemInstanceDTO ItemInstance { get; set; } + + [ForeignKey(nameof(FamilyId))] + public virtual DbFamily Family { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.DB.EF/Families/FamilyWarehouseItemEntityTypeConfiguration.cs b/srcs/_plugins/Plugin.DB.EF/Families/FamilyWarehouseItemEntityTypeConfiguration.cs new file mode 100644 index 0000000..db9fab5 --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/Families/FamilyWarehouseItemEntityTypeConfiguration.cs @@ -0,0 +1,17 @@ +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Plugin.Database.DB.Configs; + +namespace Plugin.Database.Families +{ + public class FamilyWarehouseItemEntityTypeConfiguration : BaseAuditableEntityTypeConfiguration + { + protected override void ConfigureEntity(EntityTypeBuilder builder) + { + builder.HasKey(s => new { s.FamilyId, s.Slot }); + + builder.HasOne(s => s.Family) + .WithMany(s => s.WarehouseItems) + .HasForeignKey(s => s.FamilyId); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.DB.EF/Families/FamilyWarehouseLogDao.cs b/srcs/_plugins/Plugin.DB.EF/Families/FamilyWarehouseLogDao.cs new file mode 100644 index 0000000..634ec86 --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/Families/FamilyWarehouseLogDao.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using PhoenixLib.Logging; +using Plugin.Database.DB; +using WingsAPI.Data.Families; + +namespace Plugin.Database.Families +{ + public class FamilyWarehouseLogDao : IFamilyWarehouseLogDao + { + private readonly IDbContextFactory _contextFactory; + + public FamilyWarehouseLogDao(IDbContextFactory contextFactory) => _contextFactory = contextFactory; + + public async Task SaveAsync(long familyId, IEnumerable objs) + { + try + { + await using GameContext context = _contextFactory.CreateDbContext(); + await context.FamilyWarehouseLogs.SingleMergeAsync(new FamilyWarehouseLogEntity + { + FamilyId = familyId, + LogEntries = objs.ToList() + }); + return await context.SaveChangesAsync(); + } + catch (Exception e) + { + Log.Error("[FAMILY_WAREHOUSE_LOG_DAO][SaveAsync] ", e); + throw; + } + } + + public async Task> GetByFamilyIdAsync(long familyId) + { + try + { + await using GameContext context = _contextFactory.CreateDbContext(); + FamilyWarehouseLogEntity logs = await context.FamilyWarehouseLogs.FindAsync(familyId); + return logs?.LogEntries; + } + catch (Exception e) + { + Log.Error("[FAMILY_WAREHOUSE_LOG_DAO][GetByFamilyIdAsync] ", e); + throw; + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.DB.EF/Families/FamilyWarehouseLogEntity.cs b/srcs/_plugins/Plugin.DB.EF/Families/FamilyWarehouseLogEntity.cs new file mode 100644 index 0000000..fa07c47 --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/Families/FamilyWarehouseLogEntity.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; +using Plugin.Database.DB; +using Plugin.Database.Entities; +using WingsAPI.Data.Families; + +namespace Plugin.Database.Families +{ + [Table("families_warehouses_logs", Schema = DatabaseSchemas.FAMILIES)] + public class FamilyWarehouseLogEntity : BaseAuditableEntity + { + public long FamilyId { get; set; } + + [Column(TypeName = "jsonb")] + public List LogEntries { get; set; } + + [ForeignKey(nameof(FamilyId))] + public virtual DbFamily Family { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.DB.EF/Families/FamilyWarehouseLogEntityTypeConfiguration.cs b/srcs/_plugins/Plugin.DB.EF/Families/FamilyWarehouseLogEntityTypeConfiguration.cs new file mode 100644 index 0000000..88721d2 --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/Families/FamilyWarehouseLogEntityTypeConfiguration.cs @@ -0,0 +1,16 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Plugin.Database.Families +{ + public class FamilyWarehouseLogEntityTypeConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(s => new { s.FamilyId }); + + builder.HasOne(s => s.Family) + .WithOne(s => s.WarehouseLogs); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.DB.EF/Mail/CharacterMailDao.cs b/srcs/_plugins/Plugin.DB.EF/Mail/CharacterMailDao.cs new file mode 100644 index 0000000..7e20299 --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/Mail/CharacterMailDao.cs @@ -0,0 +1,56 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using PhoenixLib.DAL; +using Plugin.Database.DB; +using WingsEmu.DTOs.Mails; + +namespace Plugin.Database.Mail +{ + public class CharacterMailDao : ICharacterMailDao + { + private readonly IDbContextFactory _contextFactory; + private readonly IMapper _mapper; + private readonly IGenericAsyncLongRepository _repository; + + public CharacterMailDao(IMapper mapper, IDbContextFactory contextFactory, IGenericAsyncLongRepository repository) + { + _mapper = mapper; + _contextFactory = contextFactory; + _repository = repository; + } + + + public async Task> GetByCharacterIdAsync(long characterId) + { + await using GameContext context = _contextFactory.CreateDbContext(); + List mails = await context.Mail.Where(s => s.ReceiverId == characterId).ToListAsync(); + return _mapper.Map(mails); + } + + public async Task> GetAllAsync() => await _repository.GetAllAsync(); + + public async Task GetByIdAsync(long id) => await _repository.GetByIdAsync(id); + + public async Task> GetByIdsAsync(IEnumerable ids) => await _repository.GetByIdsAsync(ids); + + public async Task SaveAsync(CharacterMailDto obj) => await _repository.SaveAsync(obj); + + public async Task> SaveAsync(IReadOnlyList objs) => await _repository.SaveAsync(objs); + + public async Task DeleteByIdAsync(long id) + { + await _repository.DeleteByIdAsync(id); + } + + public async Task DeleteByIdsAsync(IEnumerable ids) + { + await _repository.DeleteByIdsAsync(ids); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.DB.EF/Mail/CharacterMailEntityTypeConfiguration.cs b/srcs/_plugins/Plugin.DB.EF/Mail/CharacterMailEntityTypeConfiguration.cs new file mode 100644 index 0000000..66b10ec --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/Mail/CharacterMailEntityTypeConfiguration.cs @@ -0,0 +1,16 @@ +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Plugin.Database.DB.Configs; + +namespace Plugin.Database.Mail +{ + public class CharacterMailEntityTypeConfiguration : BaseAuditableEntityTypeConfiguration + { + protected override void ConfigureEntity(EntityTypeBuilder builder) + { + builder + .HasOne(s => s.Receiver) + .WithMany(s => s.ReceivedMails) + .HasForeignKey(s => s.ReceiverId); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.DB.EF/Mail/CharacterNoteDao.cs b/srcs/_plugins/Plugin.DB.EF/Mail/CharacterNoteDao.cs new file mode 100644 index 0000000..518f840 --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/Mail/CharacterNoteDao.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using PhoenixLib.DAL; +using Plugin.Database.DB; +using WingsEmu.DTOs.Mails; + +namespace Plugin.Database.Mail +{ + public class CharacterNoteDao : ICharacterNoteDao + { + private readonly IDbContextFactory _contextFactory; + private readonly IMapper _mapper; + private readonly IGenericAsyncLongRepository _repository; + + public CharacterNoteDao(IMapper mapper, IDbContextFactory contextFactory, IGenericAsyncLongRepository repository) + { + _mapper = mapper; + _contextFactory = contextFactory; + _repository = repository; + } + + + public async Task> GetByCharacterIdAsync(long characterId) + { + await using GameContext context = _contextFactory.CreateDbContext(); + List notes = await context.Note.Where(s => (s.ReceiverId == characterId && !s.IsSenderCopy || s.IsSenderCopy && s.SenderId == characterId) && s.DeletedAt == null) + .ToListAsync(); + return _mapper.Map(notes); + } + + public async Task> GetAllAsync() => await _repository.GetAllAsync(); + + public async Task GetByIdAsync(long id) => await _repository.GetByIdAsync(id); + + public async Task> GetByIdsAsync(IEnumerable ids) => await _repository.GetByIdsAsync(ids); + + public async Task SaveAsync(CharacterNoteDto obj) => await _repository.SaveAsync(obj); + + public async Task> SaveAsync(IReadOnlyList objs) => await _repository.SaveAsync(objs); + + public async Task DeleteByIdAsync(long id) + { + await _repository.DeleteByIdAsync(id); + } + + public async Task DeleteByIdsAsync(IEnumerable ids) + { + await _repository.DeleteByIdsAsync(ids); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.DB.EF/Mail/DbCharacterMail.cs b/srcs/_plugins/Plugin.DB.EF/Mail/DbCharacterMail.cs new file mode 100644 index 0000000..69288f9 --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/Mail/DbCharacterMail.cs @@ -0,0 +1,39 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using PhoenixLib.DAL.EFCore.PGSQL; +using Plugin.Database.DB; +using Plugin.Database.Entities; +using Plugin.Database.Entities.PlayersData; +using WingsEmu.DTOs.Items; +using WingsEmu.DTOs.Mails; + +namespace Plugin.Database.Mail +{ + [Table("characters_mails", Schema = DatabaseSchemas.MAILS)] + public class DbCharacterMail : BaseAuditableEntity, ILongEntity + { + public DateTime Date { get; set; } + + [MaxLength(255)] + public string SenderName { get; set; } + + public long ReceiverId { get; set; } + + public MailGiftType MailGiftType { get; set; } + + [Column(TypeName = "jsonb")] + public ItemInstanceDTO ItemInstance { get; set; } + + [ForeignKey(nameof(ReceiverId))] + public virtual DbCharacter Receiver { get; set; } + + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long Id { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.DB.EF/Mail/DbCharacterNote.cs b/srcs/_plugins/Plugin.DB.EF/Mail/DbCharacterNote.cs new file mode 100644 index 0000000..da02afa --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/Mail/DbCharacterNote.cs @@ -0,0 +1,57 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using PhoenixLib.DAL.EFCore.PGSQL; +using Plugin.Database.DB; +using Plugin.Database.Entities; +using Plugin.Database.Entities.PlayersData; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Character; + +namespace Plugin.Database.Mail +{ + [Table("characters_notes", Schema = DatabaseSchemas.MAILS)] + public class DbCharacterNote : BaseAuditableEntity, ILongEntity + { + public DateTime Date { get; set; } + + public long SenderId { get; set; } + + public long ReceiverId { get; set; } + + [MaxLength(255)] + public string Title { get; set; } + + [MaxLength(255)] + public string Message { get; set; } + + [MaxLength(255)] + public string EquipmentPackets { get; set; } + + public bool IsSenderCopy { get; set; } + + public bool IsOpened { get; set; } + + public GenderType SenderGender { get; set; } + + public ClassType SenderClass { get; set; } + + public HairColorType SenderHairColor { get; set; } + + public HairStyleType SenderHairStyle { get; set; } + + public string SenderName { get; set; } + + public string ReceiverName { get; set; } + + [ForeignKey(nameof(SenderId))] + public virtual DbCharacter Sender { get; set; } + + [ForeignKey(nameof(ReceiverId))] + public virtual DbCharacter Receiver { get; set; } + + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long Id { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.DB.EF/Mail/DbCharacterNoteEntityTypeConfiguration.cs b/srcs/_plugins/Plugin.DB.EF/Mail/DbCharacterNoteEntityTypeConfiguration.cs new file mode 100644 index 0000000..435d771 --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/Mail/DbCharacterNoteEntityTypeConfiguration.cs @@ -0,0 +1,21 @@ +// WingsEmu +// +// Developed by NosWings Team + +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Plugin.Database.DB.Configs; + +namespace Plugin.Database.Mail +{ + public class DbCharacterNoteEntityTypeConfiguration : BaseAuditableEntityTypeConfiguration + { + protected override void ConfigureEntity(EntityTypeBuilder builder) + { + builder.HasOne(s => s.Receiver) + .WithMany(s => s.ReceivedNotes) + .HasForeignKey(s => s.ReceiverId) + .OnDelete(DeleteBehavior.NoAction); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.DB.EF/Mapping/GameMappingRules.cs b/srcs/_plugins/Plugin.DB.EF/Mapping/GameMappingRules.cs new file mode 100644 index 0000000..3f9c34a --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/Mapping/GameMappingRules.cs @@ -0,0 +1,134 @@ +using Mapster; +using Plugin.Database.Bazaar; +using Plugin.Database.Entities.Account; +using Plugin.Database.Entities.PlayersData; +using Plugin.Database.Entities.ServerData; +using Plugin.Database.Families; +using Plugin.Database.Mail; +using Plugin.Database.Warehouse; +using WingsAPI.Data.Account; +using WingsAPI.Data.Bazaar; +using WingsAPI.Data.Character; +using WingsAPI.Data.Families; +using WingsAPI.Data.TimeSpace; +using WingsEmu.DTOs.Mails; +using WingsEmu.DTOs.Relations; +using WingsEmu.DTOs.Skills; +using WingsEmu.Game; +using WingsEmu.Game.Families; +using WingsEmu.Game.Skills; +using WingsEmu.Game.Warehouse; + +namespace Plugin.Database.Mapping +{ + public static class GameMappingRules + { + public static void InitializeMapping() + { + TypeAdapterConfig.GlobalSettings.AllowImplicitDestinationInheritance = true; + TypeAdapterConfig.GlobalSettings.Default.Settings.Unflattening = false; + + TypeAdapterConfig.NewConfig() + .ConstructUsing(s => new Account()); + TypeAdapterConfig.NewConfig() + .Include() + .Ignore( + s => s.Character, + s => s.AccountPenalties, + s => s.AccountBans + ); + + TypeAdapterConfig.NewConfig() + .Include() + .Ignore(s => s.Account); + + TypeAdapterConfig.NewConfig() + .ConstructUsing(s => + new WarehouseItem()); + + // character + TypeAdapterConfig.NewConfig(); + + TypeAdapterConfig.NewConfig() + .Ignore(s => s.AccountEntity, + s => s.SentNotes, + s => s.ReceivedNotes, + s => s.ReceivedMails, + s => s.FamilyCharacter, + s => s.BazaarItem, + s => s.SourceRelations, + s => s.TargetRelations); + + TypeAdapterConfig + .NewConfig() + .Compile(); + // DTO -> Entity + TypeAdapterConfig + .NewConfig() + .Compile(); + + + // Entity -> DTO + + + TypeAdapterConfig.NewConfig(); + TypeAdapterConfig.NewConfig(); + + // mails + TypeAdapterConfig.NewConfig(); + TypeAdapterConfig.NewConfig(); + + // notes + TypeAdapterConfig.NewConfig(); + TypeAdapterConfig.NewConfig(); + + + // family + TypeAdapterConfig.NewConfig(); + TypeAdapterConfig.NewConfig() + .Ignore( + s => s.FamilyCharacters, + s => s.FamilyLogs + ) + .Ignore( + s => s.FamilyCharacters, + s => s.FamilyLogs + ); + + + TypeAdapterConfig.NewConfig(); + TypeAdapterConfig.NewConfig() + .ConstructUsing(s => new FamilyMembership()); + TypeAdapterConfig.NewConfig() + .Ignore( + s => s.DbCharacter, + s => s.Family + ) + .Include() + .Ignore( + s => s.DbCharacter, + s => s.Family + ); + + TypeAdapterConfig.NewConfig(); + TypeAdapterConfig.NewConfig(); + + // penalty + TypeAdapterConfig.NewConfig(); + TypeAdapterConfig.NewConfig(); + + // bans + TypeAdapterConfig.NewConfig(); + TypeAdapterConfig.NewConfig(); + + + TypeAdapterConfig.NewConfig(); + TypeAdapterConfig.NewConfig(); + + TypeAdapterConfig.NewConfig(); + TypeAdapterConfig.NewConfig(); + + TypeAdapterConfig.GlobalSettings.Compile(); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.DB.EF/Mapping/MapsterMapper.cs b/srcs/_plugins/Plugin.DB.EF/Mapping/MapsterMapper.cs new file mode 100644 index 0000000..dff991f --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/Mapping/MapsterMapper.cs @@ -0,0 +1,39 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using Mapster; +using PhoenixLib.DAL; + +namespace Plugin.Database.Mapping +{ + public class MapsterMapper : IMapper + { + public void Map(TEntity input, TDto output) + { + input.Adapt(output); + } + + public TEntity Map(TDto input) => input.Adapt(); + + public List Map(List input) => input.Adapt, List>(); + + public IEnumerable Map(IEnumerable input) => input.Adapt, IEnumerable>(); + + public IReadOnlyList Map(IReadOnlyList input) => input.Adapt>(); + + public TDto Map(TEntity input) => input.Adapt(); + + public List Map(List input) => input.Adapt, List>(); + + public IEnumerable Map(IEnumerable input) => input.Adapt, IEnumerable>(); + + public IReadOnlyList Map(IReadOnlyList input) => input.Adapt>(); + + public void Map(TDto input, TEntity output) + { + input.Adapt(output); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.DB.EF/Mapping/NonGameMappingRules.cs b/srcs/_plugins/Plugin.DB.EF/Mapping/NonGameMappingRules.cs new file mode 100644 index 0000000..08083d7 --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/Mapping/NonGameMappingRules.cs @@ -0,0 +1,73 @@ +using Mapster; +using Plugin.Database.Bazaar; +using Plugin.Database.Entities.Account; +using Plugin.Database.Entities.PlayersData; +using Plugin.Database.Entities.ServerData; +using Plugin.Database.Families; +using Plugin.Database.Mail; +using WingsAPI.Data.Account; +using WingsAPI.Data.Bazaar; +using WingsAPI.Data.Character; +using WingsAPI.Data.Families; +using WingsAPI.Data.TimeSpace; +using WingsEmu.DTOs.Mails; +using WingsEmu.DTOs.Relations; + +namespace Plugin.Database.Mapping +{ + public static class NonGameMappingRules + { + public static void InitializeMapping() + { + TypeAdapterConfig.GlobalSettings.AllowImplicitDestinationInheritance = true; + + + TypeAdapterConfig.NewConfig(); + TypeAdapterConfig.NewConfig(); + + // character + TypeAdapterConfig.NewConfig(); + TypeAdapterConfig.NewConfig(); + + TypeAdapterConfig.NewConfig(); + // DTO -> Entity + TypeAdapterConfig.NewConfig(); + + + // Entity -> DTO + + + TypeAdapterConfig.NewConfig(); + TypeAdapterConfig.NewConfig(); + + TypeAdapterConfig.NewConfig(); + TypeAdapterConfig.NewConfig(); + + + // family + TypeAdapterConfig.NewConfig(); + TypeAdapterConfig.NewConfig(); + + TypeAdapterConfig.NewConfig(); + TypeAdapterConfig.NewConfig(); + + TypeAdapterConfig.NewConfig(); + TypeAdapterConfig.NewConfig(); + + // penalty + TypeAdapterConfig.NewConfig(); + TypeAdapterConfig.NewConfig(); + + // bans + TypeAdapterConfig.NewConfig(); + TypeAdapterConfig.NewConfig(); + + // time-space records + TypeAdapterConfig.NewConfig(); + TypeAdapterConfig.NewConfig(); + + // logs + TypeAdapterConfig.GlobalSettings.Compile(); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.DB.EF/Migrations/20211227011918_Init.Designer.cs b/srcs/_plugins/Plugin.DB.EF/Migrations/20211227011918_Init.Designer.cs new file mode 100644 index 0000000..aa7c24b --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/Migrations/20211227011918_Init.Designer.cs @@ -0,0 +1,1107 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Plugin.Database.DB; +using WingsAPI.Data.Character; +using WingsAPI.Data.Families; +using WingsAPI.Data.Miniland; +using WingsEmu.DTOs.Bonus; +using WingsEmu.DTOs.Buffs; +using WingsEmu.DTOs.Inventory; +using WingsEmu.DTOs.Items; +using WingsEmu.DTOs.Mates; +using WingsEmu.DTOs.Quests; +using WingsEmu.DTOs.Quicklist; +using WingsEmu.DTOs.Respawns; +using WingsEmu.DTOs.Skills; +using WingsEmu.DTOs.Titles; + +namespace Plugin.Database.Migrations +{ + [DbContext(typeof(GameContext))] + [Migration("20211227011918_Init")] + partial class Init + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Relational:MaxIdentifierLength", 63) + .HasAnnotation("ProductVersion", "5.0.7") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + modelBuilder.Entity("Plugin.Database.Auth.ClientVersion.AuthorizedClientVersionEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("ClientVersion") + .IsRequired() + .HasColumnType("text"); + + b.Property("DllHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExecutableHash") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("authorized_client_versions", "_config_auth"); + }); + + modelBuilder.Entity("Plugin.Database.Auth.HWID.BlacklistedHwidEntity", b => + { + b.Property("HardwareId") + .HasColumnType("text"); + + b.Property("Comment") + .IsRequired() + .HasColumnType("text"); + + b.Property("Judge") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("HardwareId"); + + b.ToTable("blacklisted_hardware_ids", "_config_auth"); + }); + + modelBuilder.Entity("Plugin.Database.Bazaar.DbBazaarItemEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Amount") + .HasColumnType("integer"); + + b.Property("CharacterId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("DayExpiryAmount") + .HasColumnType("smallint"); + + b.Property("DeletedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("ExpiryDate") + .HasColumnType("timestamp without time zone"); + + b.Property("IsPackage") + .HasColumnType("boolean"); + + b.Property("ItemInstance") + .HasColumnType("jsonb"); + + b.Property("PricePerItem") + .HasColumnType("bigint"); + + b.Property("SaleFee") + .HasColumnType("bigint"); + + b.Property("SoldAmount") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("UsedMedal") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("CharacterId"); + + b.ToTable("items", "bazaar"); + }); + + modelBuilder.Entity("Plugin.Database.Entities.Account.AccountBanEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("End") + .HasColumnType("timestamp without time zone"); + + b.Property("JudgeName") + .HasColumnType("text"); + + b.Property("Reason") + .HasColumnType("text"); + + b.Property("Start") + .HasColumnType("timestamp without time zone"); + + b.Property("TargetName") + .HasColumnType("text"); + + b.Property("UnlockReason") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("accounts_bans", "accounts"); + }); + + modelBuilder.Entity("Plugin.Database.Entities.Account.AccountEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Authority") + .HasColumnType("smallint"); + + b.Property("BankMoney") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("IsPrimaryAccount") + .HasColumnType("boolean"); + + b.Property("Language") + .HasColumnType("integer"); + + b.Property("MasterAccountId") + .HasColumnType("uuid"); + + b.Property("Name") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Password") + .HasMaxLength(255) + .IsUnicode(false) + .HasColumnType("character varying(255)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp without time zone"); + + b.HasKey("Id"); + + b.ToTable("accounts", "accounts"); + }); + + modelBuilder.Entity("Plugin.Database.Entities.Account.AccountPenaltyEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("JudgeName") + .HasColumnType("text"); + + b.Property("PenaltyType") + .HasColumnType("smallint"); + + b.Property("Reason") + .HasColumnType("text"); + + b.Property("RemainingTime") + .HasColumnType("integer"); + + b.Property("Start") + .HasColumnType("timestamp without time zone"); + + b.Property("TargetName") + .HasColumnType("text"); + + b.Property("UnlockReason") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("accounts_penalties", "accounts"); + }); + + modelBuilder.Entity("Plugin.Database.Entities.PlayersData.CharacterRelationEntity", b => + { + b.Property("CharacterId") + .HasColumnType("bigint"); + + b.Property("RelatedCharacterId") + .HasColumnType("bigint"); + + b.Property("RelatedName") + .HasColumnType("text"); + + b.Property("RelationType") + .HasColumnType("smallint"); + + b.HasKey("CharacterId", "RelatedCharacterId"); + + b.HasIndex("RelatedCharacterId"); + + b.ToTable("characters_relations", "characters"); + }); + + modelBuilder.Entity("Plugin.Database.Entities.PlayersData.DbCharacter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Act4Dead") + .HasColumnType("integer"); + + b.Property("Act4Kill") + .HasColumnType("integer"); + + b.Property("Act4Points") + .HasColumnType("integer"); + + b.Property("Act5RespawnType") + .HasColumnType("integer"); + + b.Property>("ActiveQuests") + .HasColumnType("jsonb"); + + b.Property("ArenaWinner") + .HasColumnType("integer"); + + b.Property("Biography") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property>("Bonus") + .HasColumnType("jsonb"); + + b.Property("BuffBlocked") + .HasColumnType("boolean"); + + b.Property("Class") + .HasColumnType("smallint"); + + b.Property>("CompletedPeriodicQuests") + .HasColumnType("jsonb"); + + b.Property>("CompletedQuests") + .HasColumnType("jsonb"); + + b.Property>("CompletedScripts") + .HasColumnType("jsonb"); + + b.Property>("CompletedTimeSpaces") + .HasColumnType("jsonb"); + + b.Property("Compliment") + .HasColumnType("smallint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("Dignity") + .HasColumnType("real"); + + b.Property("EmoticonsBlocked") + .HasColumnType("boolean"); + + b.Property>("EquippedStuffs") + .HasColumnType("jsonb"); + + 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("HideHat") + .HasColumnType("boolean"); + + b.Property("Hp") + .HasColumnType("integer"); + + b.Property("HpBlocked") + .HasColumnType("boolean"); + + b.Property>("Inventory") + .HasColumnType("jsonb"); + + b.Property("IsPartnerAutoRelive") + .HasColumnType("boolean"); + + b.Property("IsPetAutoRelive") + .HasColumnType("boolean"); + + b.Property("JobLevel") + .HasColumnType("smallint"); + + b.Property("JobLevelXp") + .HasColumnType("bigint"); + + b.Property>("LearnedSkills") + .HasColumnType("jsonb"); + + b.Property("Level") + .HasColumnType("smallint"); + + b.Property("LevelXp") + .HasColumnType("bigint"); + + b.Property("LifetimeStats") + .HasColumnType("jsonb"); + + b.Property("MapId") + .HasColumnType("integer"); + + b.Property("MapX") + .HasColumnType("smallint"); + + b.Property("MapY") + .HasColumnType("smallint"); + + b.Property("MasterPoints") + .HasColumnType("integer"); + + b.Property("MasterTicket") + .HasColumnType("integer"); + + b.Property("MaxPartnerCount") + .HasColumnType("smallint"); + + b.Property("MaxPetCount") + .HasColumnType("smallint"); + + b.Property("MinilandInviteBlocked") + .HasColumnType("boolean"); + + b.Property("MinilandMessage") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property>("MinilandObjects") + .HasColumnType("jsonb"); + + b.Property("MinilandPoint") + .HasColumnType("smallint"); + + b.Property("MinilandState") + .HasColumnType("smallint"); + + b.Property("MouseAimLock") + .HasColumnType("boolean"); + + b.Property("Mp") + .HasColumnType("integer"); + + b.Property("Name") + .HasMaxLength(30) + .IsUnicode(false) + .HasColumnType("character varying(30)"); + + b.Property>("NosMates") + .HasColumnType("jsonb"); + + b.Property>("PartnerInventory") + .HasColumnType("jsonb"); + + b.Property>("PartnerWarehouse") + .HasColumnType("jsonb"); + + b.Property("Prefix") + .HasMaxLength(25) + .HasColumnType("character varying(25)"); + + b.Property("QuickGetUp") + .HasColumnType("boolean"); + + b.Property>("Quicklist") + .HasColumnType("jsonb"); + + b.Property("RagePoint") + .HasColumnType("bigint"); + + b.Property("RaidRestrictionDto") + .HasColumnType("jsonb"); + + b.Property("RainbowBattleLeaverBusterDto") + .HasColumnType("jsonb"); + + b.Property("Reput") + .HasColumnType("bigint"); + + b.Property("RespawnType") + .HasColumnType("integer"); + + b.Property("ReturnPoint") + .HasColumnType("jsonb"); + + b.Property("Slot") + .HasColumnType("smallint"); + + b.Property("SpPointsBasic") + .HasColumnType("integer"); + + b.Property("SpPointsBonus") + .HasColumnType("integer"); + + b.Property>("StaticBuffs") + .HasColumnType("jsonb"); + + b.Property("TalentLose") + .HasColumnType("integer"); + + b.Property("TalentSurrender") + .HasColumnType("integer"); + + b.Property("TalentWin") + .HasColumnType("integer"); + + b.Property>("Titles") + .HasColumnType("jsonb"); + + b.Property("UiBlocked") + .HasColumnType("boolean"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("WhisperBlocked") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("characters", "characters"); + }); + + modelBuilder.Entity("Plugin.Database.Entities.ServerData.DbTimeSpaceRecord", b => + { + b.Property("TimeSpaceId") + .HasColumnType("bigint"); + + b.Property("CharacterName") + .HasColumnType("text"); + + b.Property("Date") + .HasColumnType("timestamp without time zone"); + + b.Property("Record") + .HasColumnType("bigint"); + + b.HasKey("TimeSpaceId"); + + b.ToTable("time_space_records", "characters"); + }); + + modelBuilder.Entity("Plugin.Database.Families.DbFamily", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Achievements") + .HasColumnType("jsonb"); + + b.Property("AssistantCanGetHistory") + .HasColumnType("boolean"); + + b.Property("AssistantCanInvite") + .HasColumnType("boolean"); + + b.Property("AssistantCanNotice") + .HasColumnType("boolean"); + + b.Property("AssistantCanShout") + .HasColumnType("boolean"); + + b.Property("AssistantWarehouseAuthorityType") + .HasColumnType("smallint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("Experience") + .HasColumnType("bigint"); + + b.Property("Faction") + .HasColumnType("smallint"); + + b.Property("HeadGender") + .HasColumnType("smallint"); + + b.Property("Level") + .HasColumnType("smallint"); + + b.Property("MemberCanGetHistory") + .HasColumnType("boolean"); + + b.Property("MemberWarehouseAuthorityType") + .HasColumnType("smallint"); + + b.Property("Message") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Missions") + .HasColumnType("jsonb"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(30) + .IsUnicode(true) + .HasColumnType("character varying(30)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("Upgrades") + .HasColumnType("jsonb"); + + b.HasKey("Id"); + + b.ToTable("families", "families"); + }); + + modelBuilder.Entity("Plugin.Database.Families.DbFamilyLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Actor") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Argument1") + .HasMaxLength(16) + .HasColumnType("character varying(16)"); + + b.Property("Argument2") + .HasMaxLength(16) + .HasColumnType("character varying(16)"); + + b.Property("Argument3") + .HasMaxLength(16) + .HasColumnType("character varying(16)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("FamilyId") + .HasColumnType("bigint"); + + b.Property("FamilyLogType") + .HasColumnType("smallint"); + + b.Property("Timestamp") + .HasColumnType("timestamp without time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp without time zone"); + + b.HasKey("Id"); + + b.HasIndex("FamilyId"); + + b.ToTable("families_logs", "families"); + }); + + modelBuilder.Entity("Plugin.Database.Families.DbFamilyMembership", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Authority") + .HasColumnType("smallint"); + + b.Property("CharacterId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("DailyMessage") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("Experience") + .HasColumnType("bigint"); + + b.Property("FamilyId") + .HasColumnType("bigint"); + + b.Property("JoinDate") + .HasColumnType("timestamp without time zone"); + + b.Property("LastOnlineDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Title") + .HasColumnType("smallint"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp without time zone"); + + b.HasKey("Id"); + + b.HasIndex("CharacterId"); + + b.HasIndex("FamilyId"); + + b.ToTable("families_memberships", "families"); + }); + + modelBuilder.Entity("Plugin.Database.Families.FamilyWarehouseItemEntity", b => + { + b.Property("FamilyId") + .HasColumnType("bigint"); + + b.Property("Slot") + .HasColumnType("smallint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("ItemInstance") + .HasColumnType("jsonb"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp without time zone"); + + b.HasKey("FamilyId", "Slot"); + + b.ToTable("families_warehouses", "families"); + }); + + modelBuilder.Entity("Plugin.Database.Families.FamilyWarehouseLogEntity", b => + { + b.Property("FamilyId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp without time zone"); + + b.Property>("LogEntries") + .HasColumnType("jsonb"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp without time zone"); + + b.HasKey("FamilyId"); + + b.ToTable("families_warehouses_logs", "families"); + }); + + modelBuilder.Entity("Plugin.Database.Mail.DbCharacterMail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("CreatedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("Date") + .HasColumnType("timestamp without time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("ItemInstance") + .HasColumnType("jsonb"); + + b.Property("MailGiftType") + .HasColumnType("integer"); + + b.Property("ReceiverId") + .HasColumnType("bigint"); + + b.Property("SenderName") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp without time zone"); + + b.HasKey("Id"); + + b.HasIndex("ReceiverId"); + + b.ToTable("characters_mails", "mails"); + }); + + modelBuilder.Entity("Plugin.Database.Mail.DbCharacterNote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("CreatedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("Date") + .HasColumnType("timestamp without time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("EquipmentPackets") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("IsOpened") + .HasColumnType("boolean"); + + b.Property("IsSenderCopy") + .HasColumnType("boolean"); + + b.Property("Message") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ReceiverId") + .HasColumnType("bigint"); + + b.Property("ReceiverName") + .HasColumnType("text"); + + b.Property("SenderClass") + .HasColumnType("smallint"); + + b.Property("SenderGender") + .HasColumnType("smallint"); + + b.Property("SenderHairColor") + .HasColumnType("smallint"); + + b.Property("SenderHairStyle") + .HasColumnType("smallint"); + + b.Property("SenderId") + .HasColumnType("bigint"); + + b.Property("SenderName") + .HasColumnType("text"); + + b.Property("Title") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp without time zone"); + + b.HasKey("Id"); + + b.HasIndex("ReceiverId"); + + b.HasIndex("SenderId"); + + b.ToTable("characters_notes", "mails"); + }); + + modelBuilder.Entity("Plugin.Database.Warehouse.AccountWarehouseItemEntity", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Slot") + .HasColumnType("smallint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("ItemInstance") + .HasColumnType("jsonb"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp without time zone"); + + b.HasKey("AccountId", "Slot"); + + b.ToTable("accounts_warehouse", "accounts"); + }); + + modelBuilder.Entity("Plugin.Database.Bazaar.DbBazaarItemEntity", b => + { + b.HasOne("Plugin.Database.Entities.PlayersData.DbCharacter", "DbCharacter") + .WithMany("BazaarItem") + .HasForeignKey("CharacterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DbCharacter"); + }); + + modelBuilder.Entity("Plugin.Database.Entities.Account.AccountBanEntity", b => + { + b.HasOne("Plugin.Database.Entities.Account.AccountEntity", "AccountEntity") + .WithMany("AccountBans") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AccountEntity"); + }); + + modelBuilder.Entity("Plugin.Database.Entities.Account.AccountPenaltyEntity", b => + { + b.HasOne("Plugin.Database.Entities.Account.AccountEntity", "AccountEntity") + .WithMany("AccountPenalties") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AccountEntity"); + }); + + modelBuilder.Entity("Plugin.Database.Entities.PlayersData.CharacterRelationEntity", b => + { + b.HasOne("Plugin.Database.Entities.PlayersData.DbCharacter", "Source") + .WithMany("SourceRelations") + .HasForeignKey("CharacterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Plugin.Database.Entities.PlayersData.DbCharacter", "Target") + .WithMany("TargetRelations") + .HasForeignKey("RelatedCharacterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Source"); + + b.Navigation("Target"); + }); + + modelBuilder.Entity("Plugin.Database.Entities.PlayersData.DbCharacter", b => + { + b.HasOne("Plugin.Database.Entities.Account.AccountEntity", "AccountEntity") + .WithMany("Character") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AccountEntity"); + }); + + modelBuilder.Entity("Plugin.Database.Families.DbFamilyLog", b => + { + b.HasOne("Plugin.Database.Families.DbFamily", "Family") + .WithMany("FamilyLogs") + .HasForeignKey("FamilyId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Family"); + }); + + modelBuilder.Entity("Plugin.Database.Families.DbFamilyMembership", b => + { + b.HasOne("Plugin.Database.Entities.PlayersData.DbCharacter", "DbCharacter") + .WithMany("FamilyCharacter") + .HasForeignKey("CharacterId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Plugin.Database.Families.DbFamily", "Family") + .WithMany("FamilyCharacters") + .HasForeignKey("FamilyId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("DbCharacter"); + + b.Navigation("Family"); + }); + + modelBuilder.Entity("Plugin.Database.Families.FamilyWarehouseItemEntity", b => + { + b.HasOne("Plugin.Database.Families.DbFamily", "Family") + .WithMany("WarehouseItems") + .HasForeignKey("FamilyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Family"); + }); + + modelBuilder.Entity("Plugin.Database.Families.FamilyWarehouseLogEntity", b => + { + b.HasOne("Plugin.Database.Families.DbFamily", "Family") + .WithOne("WarehouseLogs") + .HasForeignKey("Plugin.Database.Families.FamilyWarehouseLogEntity", "FamilyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Family"); + }); + + modelBuilder.Entity("Plugin.Database.Mail.DbCharacterMail", b => + { + b.HasOne("Plugin.Database.Entities.PlayersData.DbCharacter", "Receiver") + .WithMany("ReceivedMails") + .HasForeignKey("ReceiverId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Receiver"); + }); + + modelBuilder.Entity("Plugin.Database.Mail.DbCharacterNote", b => + { + b.HasOne("Plugin.Database.Entities.PlayersData.DbCharacter", "Receiver") + .WithMany("ReceivedNotes") + .HasForeignKey("ReceiverId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Plugin.Database.Entities.PlayersData.DbCharacter", "Sender") + .WithMany("SentNotes") + .HasForeignKey("SenderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Receiver"); + + b.Navigation("Sender"); + }); + + modelBuilder.Entity("Plugin.Database.Warehouse.AccountWarehouseItemEntity", b => + { + b.HasOne("Plugin.Database.Entities.Account.AccountEntity", "Account") + .WithMany("WarehouseItems") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("Plugin.Database.Entities.Account.AccountEntity", b => + { + b.Navigation("AccountBans"); + + b.Navigation("AccountPenalties"); + + b.Navigation("Character"); + + b.Navigation("WarehouseItems"); + }); + + modelBuilder.Entity("Plugin.Database.Entities.PlayersData.DbCharacter", b => + { + b.Navigation("BazaarItem"); + + b.Navigation("FamilyCharacter"); + + b.Navigation("ReceivedMails"); + + b.Navigation("ReceivedNotes"); + + b.Navigation("SentNotes"); + + b.Navigation("SourceRelations"); + + b.Navigation("TargetRelations"); + }); + + modelBuilder.Entity("Plugin.Database.Families.DbFamily", b => + { + b.Navigation("FamilyCharacters"); + + b.Navigation("FamilyLogs"); + + b.Navigation("WarehouseItems"); + + b.Navigation("WarehouseLogs"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/srcs/_plugins/Plugin.DB.EF/Migrations/20211227011918_Init.cs b/srcs/_plugins/Plugin.DB.EF/Migrations/20211227011918_Init.cs new file mode 100644 index 0000000..7ede195 --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/Migrations/20211227011918_Init.cs @@ -0,0 +1,709 @@ +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using WingsAPI.Data.Character; +using WingsAPI.Data.Families; +using WingsAPI.Data.Miniland; +using WingsEmu.DTOs.Bonus; +using WingsEmu.DTOs.Buffs; +using WingsEmu.DTOs.Inventory; +using WingsEmu.DTOs.Items; +using WingsEmu.DTOs.Mates; +using WingsEmu.DTOs.Quests; +using WingsEmu.DTOs.Quicklist; +using WingsEmu.DTOs.Respawns; +using WingsEmu.DTOs.Skills; +using WingsEmu.DTOs.Titles; + +namespace Plugin.Database.Migrations +{ + public partial class Init : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: "accounts"); + + migrationBuilder.EnsureSchema( + name: "_config_auth"); + + migrationBuilder.EnsureSchema( + name: "characters"); + + migrationBuilder.EnsureSchema( + name: "mails"); + + migrationBuilder.EnsureSchema( + name: "families"); + + migrationBuilder.EnsureSchema( + name: "bazaar"); + + migrationBuilder.CreateTable( + name: "accounts", + schema: "accounts", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + MasterAccountId = table.Column(type: "uuid", nullable: false), + Authority = table.Column(type: "smallint", nullable: false), + Language = table.Column(type: "integer", nullable: false), + BankMoney = table.Column(type: "bigint", nullable: false), + IsPrimaryAccount = table.Column(type: "boolean", nullable: false), + Name = table.Column(type: "character varying(255)", maxLength: 255, nullable: true), + Password = table.Column(type: "character varying(255)", unicode: false, maxLength: 255, nullable: true), + CreatedAt = table.Column(type: "timestamp without time zone", nullable: true), + UpdatedAt = table.Column(type: "timestamp without time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp without time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_accounts", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "authorized_client_versions", + schema: "_config_auth", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + ClientVersion = table.Column(type: "text", nullable: false), + ExecutableHash = table.Column(type: "text", nullable: false), + DllHash = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_authorized_client_versions", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "blacklisted_hardware_ids", + schema: "_config_auth", + columns: table => new + { + HardwareId = table.Column(type: "text", nullable: false), + Comment = table.Column(type: "text", nullable: false), + Judge = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_blacklisted_hardware_ids", x => x.HardwareId); + }); + + migrationBuilder.CreateTable( + name: "families", + schema: "families", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Name = table.Column(type: "character varying(30)", maxLength: 30, nullable: false), + Level = table.Column(type: "smallint", nullable: false), + Experience = table.Column(type: "bigint", nullable: false), + Faction = table.Column(type: "smallint", nullable: false), + HeadGender = table.Column(type: "smallint", nullable: false), + Message = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + AssistantWarehouseAuthorityType = table.Column(type: "smallint", nullable: false), + MemberWarehouseAuthorityType = table.Column(type: "smallint", nullable: false), + AssistantCanGetHistory = table.Column(type: "boolean", nullable: false), + AssistantCanInvite = table.Column(type: "boolean", nullable: false), + AssistantCanNotice = table.Column(type: "boolean", nullable: false), + AssistantCanShout = table.Column(type: "boolean", nullable: false), + MemberCanGetHistory = table.Column(type: "boolean", nullable: false), + Upgrades = table.Column(type: "jsonb", nullable: true), + Achievements = table.Column(type: "jsonb", nullable: true), + Missions = table.Column(type: "jsonb", nullable: true), + CreatedAt = table.Column(type: "timestamp without time zone", nullable: true), + UpdatedAt = table.Column(type: "timestamp without time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp without time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_families", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "time_space_records", + schema: "characters", + columns: table => new + { + TimeSpaceId = table.Column(type: "bigint", nullable: false), + CharacterName = table.Column(type: "text", nullable: true), + Record = table.Column(type: "bigint", nullable: false), + Date = table.Column(type: "timestamp without time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_time_space_records", x => x.TimeSpaceId); + }); + + migrationBuilder.CreateTable( + name: "accounts_bans", + schema: "accounts", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + AccountId = table.Column(type: "bigint", nullable: false), + JudgeName = table.Column(type: "text", nullable: true), + TargetName = table.Column(type: "text", nullable: true), + Start = table.Column(type: "timestamp without time zone", nullable: false), + End = table.Column(type: "timestamp without time zone", nullable: true), + Reason = table.Column(type: "text", nullable: true), + UnlockReason = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_accounts_bans", x => x.Id); + table.ForeignKey( + name: "FK_accounts_bans_accounts_AccountId", + column: x => x.AccountId, + principalSchema: "accounts", + principalTable: "accounts", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "accounts_penalties", + schema: "accounts", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + AccountId = table.Column(type: "bigint", nullable: false), + JudgeName = table.Column(type: "text", nullable: true), + TargetName = table.Column(type: "text", nullable: true), + Start = table.Column(type: "timestamp without time zone", nullable: false), + RemainingTime = table.Column(type: "integer", nullable: true), + PenaltyType = table.Column(type: "smallint", nullable: false), + Reason = table.Column(type: "text", nullable: true), + UnlockReason = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_accounts_penalties", x => x.Id); + table.ForeignKey( + name: "FK_accounts_penalties_accounts_AccountId", + column: x => x.AccountId, + principalSchema: "accounts", + principalTable: "accounts", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "accounts_warehouse", + schema: "accounts", + columns: table => new + { + AccountId = table.Column(type: "bigint", nullable: false), + Slot = table.Column(type: "smallint", nullable: false), + ItemInstance = table.Column(type: "jsonb", nullable: true), + CreatedAt = table.Column(type: "timestamp without time zone", nullable: true), + UpdatedAt = table.Column(type: "timestamp without time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp without time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_accounts_warehouse", x => new { x.AccountId, x.Slot }); + table.ForeignKey( + name: "FK_accounts_warehouse_accounts_AccountId", + column: x => x.AccountId, + principalSchema: "accounts", + principalTable: "accounts", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "characters", + schema: "characters", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + AccountId = table.Column(type: "bigint", nullable: false), + Act4Dead = table.Column(type: "integer", nullable: false), + Act4Kill = table.Column(type: "integer", nullable: false), + Act4Points = table.Column(type: "integer", nullable: false), + ArenaWinner = table.Column(type: "integer", nullable: false), + Biography = table.Column(type: "character varying(255)", maxLength: 255, nullable: true), + BuffBlocked = table.Column(type: "boolean", nullable: false), + Class = table.Column(type: "smallint", nullable: false), + Compliment = table.Column(type: "smallint", nullable: false), + Dignity = table.Column(type: "real", nullable: false), + EmoticonsBlocked = table.Column(type: "boolean", nullable: false), + ExchangeBlocked = table.Column(type: "boolean", nullable: false), + Faction = table.Column(type: "smallint", nullable: false), + FamilyRequestBlocked = table.Column(type: "boolean", nullable: false), + FriendRequestBlocked = table.Column(type: "boolean", nullable: false), + Gender = table.Column(type: "smallint", nullable: false), + Gold = table.Column(type: "bigint", nullable: false), + GroupRequestBlocked = table.Column(type: "boolean", nullable: false), + HairColor = table.Column(type: "smallint", nullable: false), + HairStyle = table.Column(type: "smallint", nullable: false), + HeroChatBlocked = table.Column(type: "boolean", nullable: false), + HeroLevel = table.Column(type: "smallint", nullable: false), + HeroXp = table.Column(type: "bigint", nullable: false), + Hp = table.Column(type: "integer", nullable: false), + HpBlocked = table.Column(type: "boolean", nullable: false), + IsPetAutoRelive = table.Column(type: "boolean", nullable: false), + IsPartnerAutoRelive = table.Column(type: "boolean", nullable: false), + JobLevel = table.Column(type: "smallint", nullable: false), + JobLevelXp = table.Column(type: "bigint", nullable: false), + Level = table.Column(type: "smallint", nullable: false), + LevelXp = table.Column(type: "bigint", nullable: false), + MapId = table.Column(type: "integer", nullable: false), + MapX = table.Column(type: "smallint", nullable: false), + MapY = table.Column(type: "smallint", nullable: false), + MasterPoints = table.Column(type: "integer", nullable: false), + MasterTicket = table.Column(type: "integer", nullable: false), + MaxPartnerCount = table.Column(type: "smallint", nullable: false), + MaxPetCount = table.Column(type: "smallint", nullable: false), + MinilandInviteBlocked = table.Column(type: "boolean", nullable: false), + MinilandMessage = table.Column(type: "character varying(255)", maxLength: 255, nullable: true), + MinilandPoint = table.Column(type: "smallint", nullable: false), + MinilandState = table.Column(type: "smallint", nullable: false), + MouseAimLock = table.Column(type: "boolean", nullable: false), + Mp = table.Column(type: "integer", nullable: false), + Prefix = table.Column(type: "character varying(25)", maxLength: 25, nullable: true), + Name = table.Column(type: "character varying(30)", unicode: false, maxLength: 30, nullable: true), + QuickGetUp = table.Column(type: "boolean", nullable: false), + HideHat = table.Column(type: "boolean", nullable: false), + UiBlocked = table.Column(type: "boolean", nullable: false), + RagePoint = table.Column(type: "bigint", nullable: false), + Reput = table.Column(type: "bigint", nullable: false), + Slot = table.Column(type: "smallint", nullable: false), + SpPointsBonus = table.Column(type: "integer", nullable: false), + SpPointsBasic = table.Column(type: "integer", nullable: false), + TalentLose = table.Column(type: "integer", nullable: false), + TalentSurrender = table.Column(type: "integer", nullable: false), + TalentWin = table.Column(type: "integer", nullable: false), + WhisperBlocked = table.Column(type: "boolean", nullable: false), + Act5RespawnType = table.Column(type: "integer", nullable: false), + PartnerInventory = table.Column>(type: "jsonb", nullable: true), + NosMates = table.Column>(type: "jsonb", nullable: true), + PartnerWarehouse = table.Column>(type: "jsonb", nullable: true), + Bonus = table.Column>(type: "jsonb", nullable: true), + StaticBuffs = table.Column>(type: "jsonb", nullable: true), + Quicklist = table.Column>(type: "jsonb", nullable: true), + LearnedSkills = table.Column>(type: "jsonb", nullable: true), + Titles = table.Column>(type: "jsonb", nullable: true), + CompletedScripts = table.Column>(type: "jsonb", nullable: true), + CompletedQuests = table.Column>(type: "jsonb", nullable: true), + ActiveQuests = table.Column>(type: "jsonb", nullable: true), + CompletedPeriodicQuests = table.Column>(type: "jsonb", nullable: true), + MinilandObjects = table.Column>(type: "jsonb", nullable: true), + ReturnPoint = table.Column(type: "jsonb", nullable: true), + Inventory = table.Column>(type: "jsonb", nullable: true), + EquippedStuffs = table.Column>(type: "jsonb", nullable: true), + LifetimeStats = table.Column(type: "jsonb", nullable: true), + RespawnType = table.Column(type: "integer", nullable: false), + CompletedTimeSpaces = table.Column>(type: "jsonb", nullable: true), + RaidRestrictionDto = table.Column(type: "jsonb", nullable: true), + RainbowBattleLeaverBusterDto = table.Column(type: "jsonb", nullable: true), + CreatedAt = table.Column(type: "timestamp without time zone", nullable: true), + UpdatedAt = table.Column(type: "timestamp without time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp without time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_characters", x => x.Id); + table.ForeignKey( + name: "FK_characters_accounts_AccountId", + column: x => x.AccountId, + principalSchema: "accounts", + principalTable: "accounts", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "families_logs", + schema: "families", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + FamilyId = table.Column(type: "bigint", nullable: false), + FamilyLogType = table.Column(type: "smallint", nullable: false), + Timestamp = table.Column(type: "timestamp without time zone", nullable: false), + Actor = table.Column(type: "character varying(32)", maxLength: 32, nullable: true), + Argument1 = table.Column(type: "character varying(16)", maxLength: 16, nullable: true), + Argument2 = table.Column(type: "character varying(16)", maxLength: 16, nullable: true), + Argument3 = table.Column(type: "character varying(16)", maxLength: 16, nullable: true), + CreatedAt = table.Column(type: "timestamp without time zone", nullable: true), + UpdatedAt = table.Column(type: "timestamp without time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp without time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_families_logs", x => x.Id); + table.ForeignKey( + name: "FK_families_logs_families_FamilyId", + column: x => x.FamilyId, + principalSchema: "families", + principalTable: "families", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "families_warehouses", + schema: "families", + columns: table => new + { + FamilyId = table.Column(type: "bigint", nullable: false), + Slot = table.Column(type: "smallint", nullable: false), + ItemInstance = table.Column(type: "jsonb", nullable: true), + CreatedAt = table.Column(type: "timestamp without time zone", nullable: true), + UpdatedAt = table.Column(type: "timestamp without time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp without time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_families_warehouses", x => new { x.FamilyId, x.Slot }); + table.ForeignKey( + name: "FK_families_warehouses_families_FamilyId", + column: x => x.FamilyId, + principalSchema: "families", + principalTable: "families", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "families_warehouses_logs", + schema: "families", + columns: table => new + { + FamilyId = table.Column(type: "bigint", nullable: false), + LogEntries = table.Column>(type: "jsonb", nullable: true), + CreatedAt = table.Column(type: "timestamp without time zone", nullable: true), + UpdatedAt = table.Column(type: "timestamp without time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp without time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_families_warehouses_logs", x => x.FamilyId); + table.ForeignKey( + name: "FK_families_warehouses_logs_families_FamilyId", + column: x => x.FamilyId, + principalSchema: "families", + principalTable: "families", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "characters_mails", + schema: "mails", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Date = table.Column(type: "timestamp without time zone", nullable: false), + SenderName = table.Column(type: "character varying(255)", maxLength: 255, nullable: true), + ReceiverId = table.Column(type: "bigint", nullable: false), + MailGiftType = table.Column(type: "integer", nullable: false), + ItemInstance = table.Column(type: "jsonb", nullable: true), + CreatedAt = table.Column(type: "timestamp without time zone", nullable: true), + UpdatedAt = table.Column(type: "timestamp without time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp without time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_characters_mails", x => x.Id); + table.ForeignKey( + name: "FK_characters_mails_characters_ReceiverId", + column: x => x.ReceiverId, + principalSchema: "characters", + principalTable: "characters", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "characters_notes", + schema: "mails", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Date = table.Column(type: "timestamp without time zone", nullable: false), + SenderId = table.Column(type: "bigint", nullable: false), + ReceiverId = table.Column(type: "bigint", nullable: false), + Title = table.Column(type: "character varying(255)", maxLength: 255, nullable: true), + Message = table.Column(type: "character varying(255)", maxLength: 255, nullable: true), + EquipmentPackets = table.Column(type: "character varying(255)", maxLength: 255, nullable: true), + IsSenderCopy = table.Column(type: "boolean", nullable: false), + IsOpened = table.Column(type: "boolean", nullable: false), + SenderGender = table.Column(type: "smallint", nullable: false), + SenderClass = table.Column(type: "smallint", nullable: false), + SenderHairColor = table.Column(type: "smallint", nullable: false), + SenderHairStyle = table.Column(type: "smallint", nullable: false), + SenderName = table.Column(type: "text", nullable: true), + ReceiverName = table.Column(type: "text", nullable: true), + CreatedAt = table.Column(type: "timestamp without time zone", nullable: true), + UpdatedAt = table.Column(type: "timestamp without time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp without time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_characters_notes", x => x.Id); + table.ForeignKey( + name: "FK_characters_notes_characters_ReceiverId", + column: x => x.ReceiverId, + principalSchema: "characters", + principalTable: "characters", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_characters_notes_characters_SenderId", + column: x => x.SenderId, + principalSchema: "characters", + principalTable: "characters", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "characters_relations", + schema: "characters", + columns: table => new + { + CharacterId = table.Column(type: "bigint", nullable: false), + RelatedCharacterId = table.Column(type: "bigint", nullable: false), + RelatedName = table.Column(type: "text", nullable: true), + RelationType = table.Column(type: "smallint", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_characters_relations", x => new { x.CharacterId, x.RelatedCharacterId }); + table.ForeignKey( + name: "FK_characters_relations_characters_CharacterId", + column: x => x.CharacterId, + principalSchema: "characters", + principalTable: "characters", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_characters_relations_characters_RelatedCharacterId", + column: x => x.RelatedCharacterId, + principalSchema: "characters", + principalTable: "characters", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "families_memberships", + schema: "families", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + CharacterId = table.Column(type: "bigint", nullable: true), + FamilyId = table.Column(type: "bigint", nullable: false), + Authority = table.Column(type: "smallint", nullable: false), + DailyMessage = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + Experience = table.Column(type: "bigint", nullable: false), + Title = table.Column(type: "smallint", nullable: false), + JoinDate = table.Column(type: "timestamp without time zone", nullable: false), + LastOnlineDate = table.Column(type: "timestamp without time zone", nullable: false), + CreatedAt = table.Column(type: "timestamp without time zone", nullable: true), + UpdatedAt = table.Column(type: "timestamp without time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp without time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_families_memberships", x => x.Id); + table.ForeignKey( + name: "FK_families_memberships_characters_CharacterId", + column: x => x.CharacterId, + principalSchema: "characters", + principalTable: "characters", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + table.ForeignKey( + name: "FK_families_memberships_families_FamilyId", + column: x => x.FamilyId, + principalSchema: "families", + principalTable: "families", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "items", + schema: "bazaar", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + CharacterId = table.Column(type: "bigint", nullable: false), + Amount = table.Column(type: "integer", nullable: false), + SoldAmount = table.Column(type: "integer", nullable: false), + PricePerItem = table.Column(type: "bigint", nullable: false), + SaleFee = table.Column(type: "bigint", nullable: false), + IsPackage = table.Column(type: "boolean", nullable: false), + UsedMedal = table.Column(type: "boolean", nullable: false), + ExpiryDate = table.Column(type: "timestamp without time zone", nullable: false), + DayExpiryAmount = table.Column(type: "smallint", nullable: false), + ItemInstance = table.Column(type: "jsonb", nullable: true), + CreatedAt = table.Column(type: "timestamp without time zone", nullable: true), + UpdatedAt = table.Column(type: "timestamp without time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp without time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_items", x => x.Id); + table.ForeignKey( + name: "FK_items_characters_CharacterId", + column: x => x.CharacterId, + principalSchema: "characters", + principalTable: "characters", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_accounts_bans_AccountId", + schema: "accounts", + table: "accounts_bans", + column: "AccountId"); + + migrationBuilder.CreateIndex( + name: "IX_accounts_penalties_AccountId", + schema: "accounts", + table: "accounts_penalties", + column: "AccountId"); + + migrationBuilder.CreateIndex( + name: "IX_characters_AccountId", + schema: "characters", + table: "characters", + column: "AccountId"); + + migrationBuilder.CreateIndex( + name: "IX_characters_mails_ReceiverId", + schema: "mails", + table: "characters_mails", + column: "ReceiverId"); + + migrationBuilder.CreateIndex( + name: "IX_characters_notes_ReceiverId", + schema: "mails", + table: "characters_notes", + column: "ReceiverId"); + + migrationBuilder.CreateIndex( + name: "IX_characters_notes_SenderId", + schema: "mails", + table: "characters_notes", + column: "SenderId"); + + migrationBuilder.CreateIndex( + name: "IX_characters_relations_RelatedCharacterId", + schema: "characters", + table: "characters_relations", + column: "RelatedCharacterId"); + + migrationBuilder.CreateIndex( + name: "IX_families_logs_FamilyId", + schema: "families", + table: "families_logs", + column: "FamilyId"); + + migrationBuilder.CreateIndex( + name: "IX_families_memberships_CharacterId", + schema: "families", + table: "families_memberships", + column: "CharacterId"); + + migrationBuilder.CreateIndex( + name: "IX_families_memberships_FamilyId", + schema: "families", + table: "families_memberships", + column: "FamilyId"); + + migrationBuilder.CreateIndex( + name: "IX_items_CharacterId", + schema: "bazaar", + table: "items", + column: "CharacterId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "accounts_bans", + schema: "accounts"); + + migrationBuilder.DropTable( + name: "accounts_penalties", + schema: "accounts"); + + migrationBuilder.DropTable( + name: "accounts_warehouse", + schema: "accounts"); + + migrationBuilder.DropTable( + name: "authorized_client_versions", + schema: "_config_auth"); + + migrationBuilder.DropTable( + name: "blacklisted_hardware_ids", + schema: "_config_auth"); + + migrationBuilder.DropTable( + name: "characters_mails", + schema: "mails"); + + migrationBuilder.DropTable( + name: "characters_notes", + schema: "mails"); + + migrationBuilder.DropTable( + name: "characters_relations", + schema: "characters"); + + migrationBuilder.DropTable( + name: "families_logs", + schema: "families"); + + migrationBuilder.DropTable( + name: "families_memberships", + schema: "families"); + + migrationBuilder.DropTable( + name: "families_warehouses", + schema: "families"); + + migrationBuilder.DropTable( + name: "families_warehouses_logs", + schema: "families"); + + migrationBuilder.DropTable( + name: "items", + schema: "bazaar"); + + migrationBuilder.DropTable( + name: "time_space_records", + schema: "characters"); + + migrationBuilder.DropTable( + name: "families", + schema: "families"); + + migrationBuilder.DropTable( + name: "characters", + schema: "characters"); + + migrationBuilder.DropTable( + name: "accounts", + schema: "accounts"); + } + } +} diff --git a/srcs/_plugins/Plugin.DB.EF/Migrations/GameContextModelSnapshot.cs b/srcs/_plugins/Plugin.DB.EF/Migrations/GameContextModelSnapshot.cs new file mode 100644 index 0000000..d24f9cd --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/Migrations/GameContextModelSnapshot.cs @@ -0,0 +1,1105 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Plugin.Database.DB; +using WingsAPI.Data.Character; +using WingsAPI.Data.Families; +using WingsAPI.Data.Miniland; +using WingsEmu.DTOs.Bonus; +using WingsEmu.DTOs.Buffs; +using WingsEmu.DTOs.Inventory; +using WingsEmu.DTOs.Items; +using WingsEmu.DTOs.Mates; +using WingsEmu.DTOs.Quests; +using WingsEmu.DTOs.Quicklist; +using WingsEmu.DTOs.Respawns; +using WingsEmu.DTOs.Skills; +using WingsEmu.DTOs.Titles; + +namespace Plugin.Database.Migrations +{ + [DbContext(typeof(GameContext))] + partial class GameContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Relational:MaxIdentifierLength", 63) + .HasAnnotation("ProductVersion", "5.0.7") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + modelBuilder.Entity("Plugin.Database.Auth.ClientVersion.AuthorizedClientVersionEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("ClientVersion") + .IsRequired() + .HasColumnType("text"); + + b.Property("DllHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExecutableHash") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("authorized_client_versions", "_config_auth"); + }); + + modelBuilder.Entity("Plugin.Database.Auth.HWID.BlacklistedHwidEntity", b => + { + b.Property("HardwareId") + .HasColumnType("text"); + + b.Property("Comment") + .IsRequired() + .HasColumnType("text"); + + b.Property("Judge") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("HardwareId"); + + b.ToTable("blacklisted_hardware_ids", "_config_auth"); + }); + + modelBuilder.Entity("Plugin.Database.Bazaar.DbBazaarItemEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Amount") + .HasColumnType("integer"); + + b.Property("CharacterId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("DayExpiryAmount") + .HasColumnType("smallint"); + + b.Property("DeletedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("ExpiryDate") + .HasColumnType("timestamp without time zone"); + + b.Property("IsPackage") + .HasColumnType("boolean"); + + b.Property("ItemInstance") + .HasColumnType("jsonb"); + + b.Property("PricePerItem") + .HasColumnType("bigint"); + + b.Property("SaleFee") + .HasColumnType("bigint"); + + b.Property("SoldAmount") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("UsedMedal") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("CharacterId"); + + b.ToTable("items", "bazaar"); + }); + + modelBuilder.Entity("Plugin.Database.Entities.Account.AccountBanEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("End") + .HasColumnType("timestamp without time zone"); + + b.Property("JudgeName") + .HasColumnType("text"); + + b.Property("Reason") + .HasColumnType("text"); + + b.Property("Start") + .HasColumnType("timestamp without time zone"); + + b.Property("TargetName") + .HasColumnType("text"); + + b.Property("UnlockReason") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("accounts_bans", "accounts"); + }); + + modelBuilder.Entity("Plugin.Database.Entities.Account.AccountEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Authority") + .HasColumnType("smallint"); + + b.Property("BankMoney") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("IsPrimaryAccount") + .HasColumnType("boolean"); + + b.Property("Language") + .HasColumnType("integer"); + + b.Property("MasterAccountId") + .HasColumnType("uuid"); + + b.Property("Name") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Password") + .HasMaxLength(255) + .IsUnicode(false) + .HasColumnType("character varying(255)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp without time zone"); + + b.HasKey("Id"); + + b.ToTable("accounts", "accounts"); + }); + + modelBuilder.Entity("Plugin.Database.Entities.Account.AccountPenaltyEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("JudgeName") + .HasColumnType("text"); + + b.Property("PenaltyType") + .HasColumnType("smallint"); + + b.Property("Reason") + .HasColumnType("text"); + + b.Property("RemainingTime") + .HasColumnType("integer"); + + b.Property("Start") + .HasColumnType("timestamp without time zone"); + + b.Property("TargetName") + .HasColumnType("text"); + + b.Property("UnlockReason") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("accounts_penalties", "accounts"); + }); + + modelBuilder.Entity("Plugin.Database.Entities.PlayersData.CharacterRelationEntity", b => + { + b.Property("CharacterId") + .HasColumnType("bigint"); + + b.Property("RelatedCharacterId") + .HasColumnType("bigint"); + + b.Property("RelatedName") + .HasColumnType("text"); + + b.Property("RelationType") + .HasColumnType("smallint"); + + b.HasKey("CharacterId", "RelatedCharacterId"); + + b.HasIndex("RelatedCharacterId"); + + b.ToTable("characters_relations", "characters"); + }); + + modelBuilder.Entity("Plugin.Database.Entities.PlayersData.DbCharacter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Act4Dead") + .HasColumnType("integer"); + + b.Property("Act4Kill") + .HasColumnType("integer"); + + b.Property("Act4Points") + .HasColumnType("integer"); + + b.Property("Act5RespawnType") + .HasColumnType("integer"); + + b.Property>("ActiveQuests") + .HasColumnType("jsonb"); + + b.Property("ArenaWinner") + .HasColumnType("integer"); + + b.Property("Biography") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property>("Bonus") + .HasColumnType("jsonb"); + + b.Property("BuffBlocked") + .HasColumnType("boolean"); + + b.Property("Class") + .HasColumnType("smallint"); + + b.Property>("CompletedPeriodicQuests") + .HasColumnType("jsonb"); + + b.Property>("CompletedQuests") + .HasColumnType("jsonb"); + + b.Property>("CompletedScripts") + .HasColumnType("jsonb"); + + b.Property>("CompletedTimeSpaces") + .HasColumnType("jsonb"); + + b.Property("Compliment") + .HasColumnType("smallint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("Dignity") + .HasColumnType("real"); + + b.Property("EmoticonsBlocked") + .HasColumnType("boolean"); + + b.Property>("EquippedStuffs") + .HasColumnType("jsonb"); + + 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("HideHat") + .HasColumnType("boolean"); + + b.Property("Hp") + .HasColumnType("integer"); + + b.Property("HpBlocked") + .HasColumnType("boolean"); + + b.Property>("Inventory") + .HasColumnType("jsonb"); + + b.Property("IsPartnerAutoRelive") + .HasColumnType("boolean"); + + b.Property("IsPetAutoRelive") + .HasColumnType("boolean"); + + b.Property("JobLevel") + .HasColumnType("smallint"); + + b.Property("JobLevelXp") + .HasColumnType("bigint"); + + b.Property>("LearnedSkills") + .HasColumnType("jsonb"); + + b.Property("Level") + .HasColumnType("smallint"); + + b.Property("LevelXp") + .HasColumnType("bigint"); + + b.Property("LifetimeStats") + .HasColumnType("jsonb"); + + b.Property("MapId") + .HasColumnType("integer"); + + b.Property("MapX") + .HasColumnType("smallint"); + + b.Property("MapY") + .HasColumnType("smallint"); + + b.Property("MasterPoints") + .HasColumnType("integer"); + + b.Property("MasterTicket") + .HasColumnType("integer"); + + b.Property("MaxPartnerCount") + .HasColumnType("smallint"); + + b.Property("MaxPetCount") + .HasColumnType("smallint"); + + b.Property("MinilandInviteBlocked") + .HasColumnType("boolean"); + + b.Property("MinilandMessage") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property>("MinilandObjects") + .HasColumnType("jsonb"); + + b.Property("MinilandPoint") + .HasColumnType("smallint"); + + b.Property("MinilandState") + .HasColumnType("smallint"); + + b.Property("MouseAimLock") + .HasColumnType("boolean"); + + b.Property("Mp") + .HasColumnType("integer"); + + b.Property("Name") + .HasMaxLength(30) + .IsUnicode(false) + .HasColumnType("character varying(30)"); + + b.Property>("NosMates") + .HasColumnType("jsonb"); + + b.Property>("PartnerInventory") + .HasColumnType("jsonb"); + + b.Property>("PartnerWarehouse") + .HasColumnType("jsonb"); + + b.Property("Prefix") + .HasMaxLength(25) + .HasColumnType("character varying(25)"); + + b.Property("QuickGetUp") + .HasColumnType("boolean"); + + b.Property>("Quicklist") + .HasColumnType("jsonb"); + + b.Property("RagePoint") + .HasColumnType("bigint"); + + b.Property("RaidRestrictionDto") + .HasColumnType("jsonb"); + + b.Property("RainbowBattleLeaverBusterDto") + .HasColumnType("jsonb"); + + b.Property("Reput") + .HasColumnType("bigint"); + + b.Property("RespawnType") + .HasColumnType("integer"); + + b.Property("ReturnPoint") + .HasColumnType("jsonb"); + + b.Property("Slot") + .HasColumnType("smallint"); + + b.Property("SpPointsBasic") + .HasColumnType("integer"); + + b.Property("SpPointsBonus") + .HasColumnType("integer"); + + b.Property>("StaticBuffs") + .HasColumnType("jsonb"); + + b.Property("TalentLose") + .HasColumnType("integer"); + + b.Property("TalentSurrender") + .HasColumnType("integer"); + + b.Property("TalentWin") + .HasColumnType("integer"); + + b.Property>("Titles") + .HasColumnType("jsonb"); + + b.Property("UiBlocked") + .HasColumnType("boolean"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("WhisperBlocked") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("characters", "characters"); + }); + + modelBuilder.Entity("Plugin.Database.Entities.ServerData.DbTimeSpaceRecord", b => + { + b.Property("TimeSpaceId") + .HasColumnType("bigint"); + + b.Property("CharacterName") + .HasColumnType("text"); + + b.Property("Date") + .HasColumnType("timestamp without time zone"); + + b.Property("Record") + .HasColumnType("bigint"); + + b.HasKey("TimeSpaceId"); + + b.ToTable("time_space_records", "characters"); + }); + + modelBuilder.Entity("Plugin.Database.Families.DbFamily", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Achievements") + .HasColumnType("jsonb"); + + b.Property("AssistantCanGetHistory") + .HasColumnType("boolean"); + + b.Property("AssistantCanInvite") + .HasColumnType("boolean"); + + b.Property("AssistantCanNotice") + .HasColumnType("boolean"); + + b.Property("AssistantCanShout") + .HasColumnType("boolean"); + + b.Property("AssistantWarehouseAuthorityType") + .HasColumnType("smallint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("Experience") + .HasColumnType("bigint"); + + b.Property("Faction") + .HasColumnType("smallint"); + + b.Property("HeadGender") + .HasColumnType("smallint"); + + b.Property("Level") + .HasColumnType("smallint"); + + b.Property("MemberCanGetHistory") + .HasColumnType("boolean"); + + b.Property("MemberWarehouseAuthorityType") + .HasColumnType("smallint"); + + b.Property("Message") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Missions") + .HasColumnType("jsonb"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(30) + .IsUnicode(true) + .HasColumnType("character varying(30)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("Upgrades") + .HasColumnType("jsonb"); + + b.HasKey("Id"); + + b.ToTable("families", "families"); + }); + + modelBuilder.Entity("Plugin.Database.Families.DbFamilyLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Actor") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Argument1") + .HasMaxLength(16) + .HasColumnType("character varying(16)"); + + b.Property("Argument2") + .HasMaxLength(16) + .HasColumnType("character varying(16)"); + + b.Property("Argument3") + .HasMaxLength(16) + .HasColumnType("character varying(16)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("FamilyId") + .HasColumnType("bigint"); + + b.Property("FamilyLogType") + .HasColumnType("smallint"); + + b.Property("Timestamp") + .HasColumnType("timestamp without time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp without time zone"); + + b.HasKey("Id"); + + b.HasIndex("FamilyId"); + + b.ToTable("families_logs", "families"); + }); + + modelBuilder.Entity("Plugin.Database.Families.DbFamilyMembership", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Authority") + .HasColumnType("smallint"); + + b.Property("CharacterId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("DailyMessage") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("Experience") + .HasColumnType("bigint"); + + b.Property("FamilyId") + .HasColumnType("bigint"); + + b.Property("JoinDate") + .HasColumnType("timestamp without time zone"); + + b.Property("LastOnlineDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Title") + .HasColumnType("smallint"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp without time zone"); + + b.HasKey("Id"); + + b.HasIndex("CharacterId"); + + b.HasIndex("FamilyId"); + + b.ToTable("families_memberships", "families"); + }); + + modelBuilder.Entity("Plugin.Database.Families.FamilyWarehouseItemEntity", b => + { + b.Property("FamilyId") + .HasColumnType("bigint"); + + b.Property("Slot") + .HasColumnType("smallint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("ItemInstance") + .HasColumnType("jsonb"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp without time zone"); + + b.HasKey("FamilyId", "Slot"); + + b.ToTable("families_warehouses", "families"); + }); + + modelBuilder.Entity("Plugin.Database.Families.FamilyWarehouseLogEntity", b => + { + b.Property("FamilyId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp without time zone"); + + b.Property>("LogEntries") + .HasColumnType("jsonb"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp without time zone"); + + b.HasKey("FamilyId"); + + b.ToTable("families_warehouses_logs", "families"); + }); + + modelBuilder.Entity("Plugin.Database.Mail.DbCharacterMail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("CreatedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("Date") + .HasColumnType("timestamp without time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("ItemInstance") + .HasColumnType("jsonb"); + + b.Property("MailGiftType") + .HasColumnType("integer"); + + b.Property("ReceiverId") + .HasColumnType("bigint"); + + b.Property("SenderName") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp without time zone"); + + b.HasKey("Id"); + + b.HasIndex("ReceiverId"); + + b.ToTable("characters_mails", "mails"); + }); + + modelBuilder.Entity("Plugin.Database.Mail.DbCharacterNote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("CreatedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("Date") + .HasColumnType("timestamp without time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("EquipmentPackets") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("IsOpened") + .HasColumnType("boolean"); + + b.Property("IsSenderCopy") + .HasColumnType("boolean"); + + b.Property("Message") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ReceiverId") + .HasColumnType("bigint"); + + b.Property("ReceiverName") + .HasColumnType("text"); + + b.Property("SenderClass") + .HasColumnType("smallint"); + + b.Property("SenderGender") + .HasColumnType("smallint"); + + b.Property("SenderHairColor") + .HasColumnType("smallint"); + + b.Property("SenderHairStyle") + .HasColumnType("smallint"); + + b.Property("SenderId") + .HasColumnType("bigint"); + + b.Property("SenderName") + .HasColumnType("text"); + + b.Property("Title") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp without time zone"); + + b.HasKey("Id"); + + b.HasIndex("ReceiverId"); + + b.HasIndex("SenderId"); + + b.ToTable("characters_notes", "mails"); + }); + + modelBuilder.Entity("Plugin.Database.Warehouse.AccountWarehouseItemEntity", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Slot") + .HasColumnType("smallint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("ItemInstance") + .HasColumnType("jsonb"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp without time zone"); + + b.HasKey("AccountId", "Slot"); + + b.ToTable("accounts_warehouse", "accounts"); + }); + + modelBuilder.Entity("Plugin.Database.Bazaar.DbBazaarItemEntity", b => + { + b.HasOne("Plugin.Database.Entities.PlayersData.DbCharacter", "DbCharacter") + .WithMany("BazaarItem") + .HasForeignKey("CharacterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DbCharacter"); + }); + + modelBuilder.Entity("Plugin.Database.Entities.Account.AccountBanEntity", b => + { + b.HasOne("Plugin.Database.Entities.Account.AccountEntity", "AccountEntity") + .WithMany("AccountBans") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AccountEntity"); + }); + + modelBuilder.Entity("Plugin.Database.Entities.Account.AccountPenaltyEntity", b => + { + b.HasOne("Plugin.Database.Entities.Account.AccountEntity", "AccountEntity") + .WithMany("AccountPenalties") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AccountEntity"); + }); + + modelBuilder.Entity("Plugin.Database.Entities.PlayersData.CharacterRelationEntity", b => + { + b.HasOne("Plugin.Database.Entities.PlayersData.DbCharacter", "Source") + .WithMany("SourceRelations") + .HasForeignKey("CharacterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Plugin.Database.Entities.PlayersData.DbCharacter", "Target") + .WithMany("TargetRelations") + .HasForeignKey("RelatedCharacterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Source"); + + b.Navigation("Target"); + }); + + modelBuilder.Entity("Plugin.Database.Entities.PlayersData.DbCharacter", b => + { + b.HasOne("Plugin.Database.Entities.Account.AccountEntity", "AccountEntity") + .WithMany("Character") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AccountEntity"); + }); + + modelBuilder.Entity("Plugin.Database.Families.DbFamilyLog", b => + { + b.HasOne("Plugin.Database.Families.DbFamily", "Family") + .WithMany("FamilyLogs") + .HasForeignKey("FamilyId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Family"); + }); + + modelBuilder.Entity("Plugin.Database.Families.DbFamilyMembership", b => + { + b.HasOne("Plugin.Database.Entities.PlayersData.DbCharacter", "DbCharacter") + .WithMany("FamilyCharacter") + .HasForeignKey("CharacterId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Plugin.Database.Families.DbFamily", "Family") + .WithMany("FamilyCharacters") + .HasForeignKey("FamilyId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("DbCharacter"); + + b.Navigation("Family"); + }); + + modelBuilder.Entity("Plugin.Database.Families.FamilyWarehouseItemEntity", b => + { + b.HasOne("Plugin.Database.Families.DbFamily", "Family") + .WithMany("WarehouseItems") + .HasForeignKey("FamilyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Family"); + }); + + modelBuilder.Entity("Plugin.Database.Families.FamilyWarehouseLogEntity", b => + { + b.HasOne("Plugin.Database.Families.DbFamily", "Family") + .WithOne("WarehouseLogs") + .HasForeignKey("Plugin.Database.Families.FamilyWarehouseLogEntity", "FamilyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Family"); + }); + + modelBuilder.Entity("Plugin.Database.Mail.DbCharacterMail", b => + { + b.HasOne("Plugin.Database.Entities.PlayersData.DbCharacter", "Receiver") + .WithMany("ReceivedMails") + .HasForeignKey("ReceiverId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Receiver"); + }); + + modelBuilder.Entity("Plugin.Database.Mail.DbCharacterNote", b => + { + b.HasOne("Plugin.Database.Entities.PlayersData.DbCharacter", "Receiver") + .WithMany("ReceivedNotes") + .HasForeignKey("ReceiverId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Plugin.Database.Entities.PlayersData.DbCharacter", "Sender") + .WithMany("SentNotes") + .HasForeignKey("SenderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Receiver"); + + b.Navigation("Sender"); + }); + + modelBuilder.Entity("Plugin.Database.Warehouse.AccountWarehouseItemEntity", b => + { + b.HasOne("Plugin.Database.Entities.Account.AccountEntity", "Account") + .WithMany("WarehouseItems") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("Plugin.Database.Entities.Account.AccountEntity", b => + { + b.Navigation("AccountBans"); + + b.Navigation("AccountPenalties"); + + b.Navigation("Character"); + + b.Navigation("WarehouseItems"); + }); + + modelBuilder.Entity("Plugin.Database.Entities.PlayersData.DbCharacter", b => + { + b.Navigation("BazaarItem"); + + b.Navigation("FamilyCharacter"); + + b.Navigation("ReceivedMails"); + + b.Navigation("ReceivedNotes"); + + b.Navigation("SentNotes"); + + b.Navigation("SourceRelations"); + + b.Navigation("TargetRelations"); + }); + + modelBuilder.Entity("Plugin.Database.Families.DbFamily", b => + { + b.Navigation("FamilyCharacters"); + + b.Navigation("FamilyLogs"); + + b.Navigation("WarehouseItems"); + + b.Navigation("WarehouseLogs"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/srcs/_plugins/Plugin.DB.EF/Plugin.DB.EF.csproj b/srcs/_plugins/Plugin.DB.EF/Plugin.DB.EF.csproj new file mode 100644 index 0000000..0b1cd9a --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/Plugin.DB.EF.csproj @@ -0,0 +1,36 @@ + + + + net5.0 + Plugin.Database + True + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + \ No newline at end of file diff --git a/srcs/_plugins/Plugin.DB.EF/Warehouse/AccountWarehouseItemEntity.cs b/srcs/_plugins/Plugin.DB.EF/Warehouse/AccountWarehouseItemEntity.cs new file mode 100644 index 0000000..cbd695e --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/Warehouse/AccountWarehouseItemEntity.cs @@ -0,0 +1,22 @@ +using System.ComponentModel.DataAnnotations.Schema; +using Plugin.Database.DB; +using Plugin.Database.Entities; +using Plugin.Database.Entities.Account; +using WingsEmu.DTOs.Items; + +namespace Plugin.Database.Warehouse +{ + [Table("accounts_warehouse", Schema = DatabaseSchemas.ACCOUNTS)] + public class AccountWarehouseItemEntity : BaseAuditableEntity + { + public long AccountId { get; set; } + + public short Slot { get; set; } + + [Column(TypeName = "jsonb")] + public ItemInstanceDTO ItemInstance { get; set; } + + [ForeignKey(nameof(AccountId))] + public virtual AccountEntity Account { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.DB.EF/Warehouse/AccountWarehouseItemEntityTypeConfiguration.cs b/srcs/_plugins/Plugin.DB.EF/Warehouse/AccountWarehouseItemEntityTypeConfiguration.cs new file mode 100644 index 0000000..aeedbd8 --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/Warehouse/AccountWarehouseItemEntityTypeConfiguration.cs @@ -0,0 +1,18 @@ +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Plugin.Database.DB.Configs; + +namespace Plugin.Database.Warehouse +{ + public class AccountWarehouseItemEntityTypeConfiguration : BaseAuditableEntityTypeConfiguration + { + protected override void ConfigureEntity(EntityTypeBuilder builder) + { + builder.HasKey(x => new { x.AccountId, x.Slot }); + + builder + .HasOne(s => s.Account) + .WithMany(s => s.WarehouseItems) + .HasForeignKey(s => s.AccountId); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.DB.EF/Warehouse/AccountWarehouseItemItemDao.cs b/srcs/_plugins/Plugin.DB.EF/Warehouse/AccountWarehouseItemItemDao.cs new file mode 100644 index 0000000..c5ce279 --- /dev/null +++ b/srcs/_plugins/Plugin.DB.EF/Warehouse/AccountWarehouseItemItemDao.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using PhoenixLib.DAL; +using PhoenixLib.Logging; +using Plugin.Database.DB; +using WingsAPI.Data.Account; +using WingsAPI.Data.Warehouse; + +namespace Plugin.Database.Warehouse +{ + public class AccountWarehouseItemItemDao : IAccountWarehouseItemDao + { + private readonly IDbContextFactory _contextFactory; + private readonly IMapper _mapper; + + public AccountWarehouseItemItemDao(IDbContextFactory contextFactory, IMapper mapper) + { + _contextFactory = contextFactory; + _mapper = mapper; + } + + public async Task SaveAsync(IReadOnlyList objs) + { + try + { + IEnumerable entities = _mapper.Map(objs); + await using GameContext context = _contextFactory.CreateDbContext(); + await context.AccountWarehouseItems.BulkMergeAsync(entities); + return await context.SaveChangesAsync(); + } + catch (Exception e) + { + Log.Error("[ACCOUNT_WAREHOUSE_ITEM_DAO][SaveAsync] ", e); + throw; + } + } + + public async Task DeleteAsync(IEnumerable objs) + { + try + { + IEnumerable entities = _mapper.Map(objs); + await using GameContext context = _contextFactory.CreateDbContext(); + await context.AccountWarehouseItems.BulkDeleteAsync(entities); + return await context.SaveChangesAsync(); + } + catch (Exception e) + { + Log.Error("[ACCOUNT_WAREHOUSE_ITEM_DAO][DeleteAsync] ", e); + throw; + } + } + + public async Task> GetByAccountIdAsync(long accountId) + { + try + { + await using GameContext context = _contextFactory.CreateDbContext(); + IEnumerable items = await context.AccountWarehouseItems.Where(s => s.AccountId == accountId).ToListAsync(); + return _mapper.Map(items); + } + catch (Exception e) + { + Log.Error("[ACCOUNT_WAREHOUSE_ITEM_DAO][GetByFamilyIdAsync] ", e); + throw; + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/Achievements/AdministratorFamilyModule.cs b/srcs/_plugins/Plugin.FamilyImpl/Achievements/AdministratorFamilyModule.cs new file mode 100644 index 0000000..6b785cc --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/Achievements/AdministratorFamilyModule.cs @@ -0,0 +1,132 @@ +using System.Linq; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; +using Qmmands; +using WingsAPI.Communication.Families; +using WingsEmu.Commands.Checks; +using WingsEmu.Commands.Entities; +using WingsEmu.DTOs.Account; +using WingsEmu.Game.Families; +using WingsEmu.Game.Networking; + +namespace Plugin.FamilyImpl.Achievements +{ + [Name("family-achievements-admin")] + [Group("family", "fam")] + [RequireAuthority(AuthorityType.GameAdmin)] + public sealed class AdministratorFamilyModule : SaltyModuleBase + { + [Group("mission", "missions", "ms")] + public sealed class MissionsModule : SaltyModuleBase + { + private readonly FamilyMissionsConfiguration _config; + private readonly IFamilyMissionManager _familyAchievementManager; + private readonly IMessagePublisher _message; + + public MissionsModule(FamilyMissionsConfiguration config, IFamilyMissionManager familyAchievementManager, IMessagePublisher message) + { + _config = config; + _familyAchievementManager = familyAchievementManager; + _message = message; + } + + [Command("reset")] + public async Task Reset() + { + await _message.PublishAsync(new FamilyMissionsResetMessage()); + return new SaltyCommandResult(true); + } + + [Command("unlock")] + public async Task UnlockAchievement([Description("Missions' Id")] int missionId) + { + IFamily family = Context.Player.PlayerEntity.Family; + if (family == null) + { + return new SaltyCommandResult(false, "You don't have a family"); + } + + FamilyMissionSpecificConfiguration? tmp = _config.FirstOrDefault(s => s.MissionId == missionId); + if (tmp == null) + { + return new SaltyCommandResult(false, $"family mission {missionId} configuration not found"); + } + + _familyAchievementManager.IncrementFamilyMission(family.Id, Context.Player.PlayerEntity.Id, missionId, tmp.Value); + return new SaltyCommandResult(true, $"Mission {missionId} will be unlocked soon"); + } + + + [Command("add")] + public async Task AddCounterToAchievement([Description("Missions' Id")] int missionId, int counter) + { + IFamily family = Context.Player.PlayerEntity.Family; + if (family == null) + { + return new SaltyCommandResult(false, "You don't have a family"); + } + + _familyAchievementManager.IncrementFamilyMission(family.Id, Context.Player.PlayerEntity.Id, missionId, counter); + return new SaltyCommandResult(true, $"Mission {missionId} will have {counter} increments added soon"); + } + } + + [Group("achievement", "achievements", "ac")] + public sealed class AchievementModule : SaltyModuleBase + { + private readonly FamilyAchievementsConfiguration _config; + private readonly IFamilyAchievementManager _familyAchievementManager; + + public AchievementModule(FamilyAchievementsConfiguration config, IFamilyAchievementManager familyAchievementManager) + { + _config = config; + _familyAchievementManager = familyAchievementManager; + } + + [Command("unlock")] + public async Task UnlockAchievement(int achievementId) + { + IFamily family = Context.Player.PlayerEntity.Family; + if (family == null) + { + return new SaltyCommandResult(false, "You don't have a family"); + } + + FamilyAchievementSpecificConfiguration? tmp = _config.Counters.FirstOrDefault(s => s.Id == achievementId); + if (tmp == null) + { + return new SaltyCommandResult(false, $"family achievement {achievementId} configuration not found"); + } + + _familyAchievementManager.IncrementFamilyAchievement(family.Id, achievementId, tmp.Value); + return new SaltyCommandResult(true, $"{achievementId} will be unlocked soon"); + } + + [Command("add")] + public async Task AddCounterToAchievement(int achievementId, int counter) + { + IFamily family = Context.Player.PlayerEntity.Family; + if (family == null) + { + return new SaltyCommandResult(false, "You don't have a family"); + } + + _familyAchievementManager.IncrementFamilyAchievement(family.Id, achievementId, counter); + return new SaltyCommandResult(true); + } + + [Command("add")] + public async Task AddCounterToAchievement(IClientSession target, int achievementId, int counter) + { + IFamily family = target.PlayerEntity.Family; + if (family == null) + { + return new SaltyCommandResult(false, "Player doesn't have a family"); + } + + _familyAchievementManager.IncrementFamilyAchievement(family.Id, achievementId, counter); + return new SaltyCommandResult(true); + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/Achievements/FamilyAchievementReward.cs b/srcs/_plugins/Plugin.FamilyImpl/Achievements/FamilyAchievementReward.cs new file mode 100644 index 0000000..11c16a6 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/Achievements/FamilyAchievementReward.cs @@ -0,0 +1,12 @@ +using WingsAPI.Data.Families; + +namespace Plugin.FamilyImpl.Achievements +{ + public class FamilyAchievementReward + { + public FamilyUpgradeType? FamilyUpgradeCategory { get; set; } + public short? UpgradeValue { get; set; } + public int? UpgradeId { get; set; } + public int? FamilyXp { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/Achievements/FamilyAchievementSpecificConfiguration.cs b/srcs/_plugins/Plugin.FamilyImpl/Achievements/FamilyAchievementSpecificConfiguration.cs new file mode 100644 index 0000000..e11b640 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/Achievements/FamilyAchievementSpecificConfiguration.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; + +namespace Plugin.FamilyImpl.Achievements +{ + public class FamilyAchievementSpecificConfiguration + { + public int Id { get; set; } + public int Value { get; set; } + public int? RequiredId { get; set; } + public List Rewards { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/Achievements/FamilyAchievementUnlockedMessage.cs b/srcs/_plugins/Plugin.FamilyImpl/Achievements/FamilyAchievementUnlockedMessage.cs new file mode 100644 index 0000000..b7de289 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/Achievements/FamilyAchievementUnlockedMessage.cs @@ -0,0 +1,12 @@ +using PhoenixLib.ServiceBus; +using PhoenixLib.ServiceBus.Routing; + +namespace Plugin.FamilyImpl.Achievements +{ + [MessageType("family.achievements.unlocked")] + public class FamilyAchievementUnlockedMessage : IMessage + { + public long FamilyId { get; set; } + public int AchievementId { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/Achievements/FamilyAchievementUnlockedMessageConsumer.cs b/srcs/_plugins/Plugin.FamilyImpl/Achievements/FamilyAchievementUnlockedMessageConsumer.cs new file mode 100644 index 0000000..d91b480 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/Achievements/FamilyAchievementUnlockedMessageConsumer.cs @@ -0,0 +1,58 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Families; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.FamilyImpl.Achievements +{ + public class FamilyAchievementUnlockedMessageConsumer : IMessageConsumer + { + private readonly IFamilyManager _familyManager; + private readonly IGameLanguageService _gameLanguage; + private readonly IItemsManager _itemsManager; + private readonly ISessionManager _sessionManager; + + public FamilyAchievementUnlockedMessageConsumer(ISessionManager sessionManager, IFamilyManager familyManager, IItemsManager itemsManager, IGameLanguageService gameLanguage) + { + _sessionManager = sessionManager; + _familyManager = familyManager; + _itemsManager = itemsManager; + _gameLanguage = gameLanguage; + } + + public async Task HandleAsync(FamilyAchievementUnlockedMessage notification, CancellationToken token) + { + Family family = _familyManager.GetFamilyByFamilyIdCache(notification.FamilyId); + if (family == null) + { + return; + } + + IGameItem quest = _itemsManager.GetItem(notification.AchievementId + 600); + if (quest == null) + { + return; + } + + foreach (FamilyMembership familyMember in family.Members) + { + IClientSession session = _sessionManager.GetSessionByCharacterId(familyMember.CharacterId); + if (session == null) + { + continue; + } + + string language = _gameLanguage.GetLanguage(GameDataType.Item, quest.Name, session.UserLanguage); + session.SendMsg(session.GetLanguageFormat(GameDialogKey.FAMILY_ACHIEVEMENT_UNLOCKED, language), MsgMessageType.Middle); + session.SendInformationChatMessage(session.GetLanguageFormat(GameDialogKey.FAMILY_ACHIEVEMENT_UNLOCKED, language)); + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/Achievements/FamilyAchievementsConfiguration.cs b/srcs/_plugins/Plugin.FamilyImpl/Achievements/FamilyAchievementsConfiguration.cs new file mode 100644 index 0000000..d85579a --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/Achievements/FamilyAchievementsConfiguration.cs @@ -0,0 +1,13 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; + +namespace Plugin.FamilyImpl.Achievements +{ + public class FamilyAchievementsConfiguration + { + public List Counters { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/Achievements/Handlers/Act4KillEventHandler.cs b/srcs/_plugins/Plugin.FamilyImpl/Achievements/Handlers/Act4KillEventHandler.cs new file mode 100644 index 0000000..a851352 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/Achievements/Handlers/Act4KillEventHandler.cs @@ -0,0 +1,27 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Families; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Revival; + +namespace Plugin.FamilyImpl.Achievements.Handlers +{ + public class Act4KillEventHandler : IAsyncEventProcessor + { + private readonly IFamilyMissionManager _familyManager; + + public Act4KillEventHandler(IFamilyMissionManager familyManager) => _familyManager = familyManager; + + public async Task HandleAsync(Act4KillEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + if (!session.PlayerEntity.IsInFamily()) + { + return; + } + + _familyManager.IncrementFamilyMission(session.PlayerEntity.Family.Id, (short)FamilyMissionVnums.DAILY_DEFEAT_10_ENEMIES_ACT4); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/Achievements/Handlers/FamilyAchievementHandlerAct4DungeonWon.cs b/srcs/_plugins/Plugin.FamilyImpl/Achievements/Handlers/FamilyAchievementHandlerAct4DungeonWon.cs new file mode 100644 index 0000000..1cc08c6 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/Achievements/Handlers/FamilyAchievementHandlerAct4DungeonWon.cs @@ -0,0 +1,63 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Act4; +using WingsEmu.Game.Act4.Event; +using WingsEmu.Game.Families; + +namespace Plugin.FamilyImpl.Achievements.Handlers +{ + public class FamilyAchievementHandlerAct4DungeonWon : IAsyncEventProcessor + { + private readonly IFamilyAchievementManager _familyManager; + private readonly IFamilyMissionManager _familyMissionManager; + + public FamilyAchievementHandlerAct4DungeonWon(IFamilyAchievementManager familyManager, IFamilyMissionManager familyMissionManager) + { + _familyManager = familyManager; + _familyMissionManager = familyMissionManager; + } + + public async Task HandleAsync(Act4DungeonWonEvent e, CancellationToken cancellation) + { + DungeonInstance dungeon = e.DungeonInstance; + long familyId = dungeon.FamilyId; + DungeonType dungeonType = dungeon.DungeonType; + if (!dungeon.PlayerDeathInBossRoom) + { + _familyMissionManager.IncrementFamilyMission(familyId, (int)FamilyMissionVnums.DAILY_DEFEAT_DUNGEON_BOSS_WITHOUT_DYING); + } + + TimeSpan time = DateTime.UtcNow - dungeon.StartInBoosRoom; + if (time < TimeSpan.FromMinutes(10)) + { + _familyMissionManager.IncrementFamilyMission(familyId, (int)FamilyMissionVnums.DAILY_DEFEAT_DUNGEON_BOSS_LESS_10MIN); + } + + _familyMissionManager.IncrementFamilyMission(familyId, (int)FamilyMissionVnums.DAILY_DEFEAT_ANY_ACT4_DUNGEON_1_TIME); + _familyManager.IncrementFamilyAchievement(familyId, (int)FamilyAchievementsVnum.DEFEAT_ANY_ACT4_DUNGEON_10_TIMES); + + FamilyAchievementsVnum dungeonSpecificAchievement = dungeonType switch + { + DungeonType.Berios => FamilyAchievementsVnum.DEFEAT_BERIOS_ACT4_DUNGEON_1_TIME, + DungeonType.Hatus => FamilyAchievementsVnum.DEFEAT_HATUS_ACT4_DUNGEON_1_TIME, + DungeonType.Calvinas => FamilyAchievementsVnum.DEFEAT_CALVINAS_ACT4_DUNGEON_1_TIME, + DungeonType.Morcos => FamilyAchievementsVnum.DEFEAT_MORCOS_ACT4_DUNGEON_1_TIME + }; + + _familyManager.IncrementFamilyAchievement(familyId, (int)dungeonSpecificAchievement); + + await e.DungeonLeader.EmitEventAsync(new Act4FamilyDungeonWonEvent + { + DungeonType = dungeonType, + FamilyId = familyId, + Members = e.Members + }); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/Achievements/Handlers/InstantBattleAchievementHandler.cs b/srcs/_plugins/Plugin.FamilyImpl/Achievements/Handlers/InstantBattleAchievementHandler.cs new file mode 100644 index 0000000..0823b0d --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/Achievements/Handlers/InstantBattleAchievementHandler.cs @@ -0,0 +1,28 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Families; +using WingsEmu.Game.GameEvent.InstantBattle; + +namespace Plugin.FamilyImpl.Achievements.Handlers +{ + public class InstantBattleAchievementHandler : IAsyncEventProcessor + { + private readonly IFamilyMissionManager _familyMissionManager; + + public InstantBattleAchievementHandler(IFamilyMissionManager familyMissionManager) => _familyMissionManager = familyMissionManager; + + public async Task HandleAsync(InstantBattleWonEvent e, CancellationToken cancellation) + { + IPlayerEntity player = e.Sender.PlayerEntity; + if (!player.IsInFamily()) + { + return; + } + + long familyId = player.Family.Id; + _familyMissionManager.IncrementFamilyMission(familyId, player.Id, (int)FamilyMissionVnums.DAILY_DEFEAT_10_INSTANT_BATTLES); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/Achievements/Handlers/RaidWonAchievementHandler.cs b/srcs/_plugins/Plugin.FamilyImpl/Achievements/Handlers/RaidWonAchievementHandler.cs new file mode 100644 index 0000000..d5d1139 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/Achievements/Handlers/RaidWonAchievementHandler.cs @@ -0,0 +1,45 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Packets.Enums; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Families; +using WingsEmu.Game.Raids.Events; + +namespace Plugin.FamilyImpl.Achievements.Handlers +{ + public class RaidWonAchievementHandler : IAsyncEventProcessor + { + private readonly IFamilyMissionManager _familyMissionManager; + + public RaidWonAchievementHandler(IFamilyMissionManager familyMissionManager) => _familyMissionManager = familyMissionManager; + + public async Task HandleAsync(RaidWonEvent e, CancellationToken cancellation) + { + IPlayerEntity player = e.Sender.PlayerEntity; + if (!player.IsInFamily()) + { + return; + } + + long familyId = player.Family.Id; + RaidType raidType = player.Raid.Type; + + FamilyMissionVnums dungeonSpecificAchievement = raidType switch + { + RaidType.Cuby => FamilyMissionVnums.DAILY_DEFEAT_5_CUBY_RAID, + RaidType.Ginseng => FamilyMissionVnums.DAILY_DEFEAT_5_GINSENG_RAID, + RaidType.Castra => FamilyMissionVnums.DAILY_DEFEAT_5_CASTRA_RAID, + RaidType.GiantBlackSpider => FamilyMissionVnums.DAILY_DEFEAT_5_GIANT_SPIDER_RAID, + RaidType.Slade => FamilyMissionVnums.DAILY_DEFEAT_5_GIANT_SLADE_RAID, + RaidType.RobberGang => FamilyMissionVnums.DAILY_DEFEAT_5_ROBBER_GANG_RAID, + RaidType.Kertos => FamilyMissionVnums.DAILY_DEFEAT_5_KERTOS_RAID, + RaidType.Valakus => FamilyMissionVnums.DAILY_DEFEAT_5_VALAKUS_RAID, + RaidType.Grenigas => FamilyMissionVnums.DAILY_DEFEAT_5_GRENIGAS_RAID, + RaidType.Namaju => FamilyMissionVnums.DAILY_DEFEAT_5_NAMAJU_RAID + }; + + _familyMissionManager.IncrementFamilyMission(familyId, player.Id, (int)dungeonSpecificAchievement); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/Commands/AdministratorFamilyModule.cs b/srcs/_plugins/Plugin.FamilyImpl/Commands/AdministratorFamilyModule.cs new file mode 100644 index 0000000..4a6b3b4 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/Commands/AdministratorFamilyModule.cs @@ -0,0 +1,107 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Threading.Tasks; +using Qmmands; +using WingsAPI.Data.Families; +using WingsEmu.Commands.Checks; +using WingsEmu.Commands.Entities; +using WingsEmu.DTOs.Account; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Families; +using WingsEmu.Game.Families.Enum; +using WingsEmu.Game.Families.Event; +using WingsEmu.Packets.Enums.Chat; +using WingsEmu.Packets.Enums.Families; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; + +namespace Plugin.FamilyImpl.Commands +{ + [Name("family-admin")] + [Group("family", "fam")] + [RequireAuthority(AuthorityType.GameAdmin)] + public sealed class AdministratorFamilyModule : SaltyModuleBase + { + private readonly IFamilyManager _familyManager; + + public AdministratorFamilyModule(IFamilyManager familyManager) => _familyManager = familyManager; + + [Command("showinfo")] + public async Task FamilyShoutAsync([Remainder] string familyName = null) + { + IFamily family = Context.Player.PlayerEntity.Family; + if (!string.IsNullOrEmpty(familyName)) + { + family = _familyManager.GetFamilyByFamilyName(familyName); + } + + if (family == null) + { + Context.Player.SendErrorChatMessage($"family {familyName} does not exists"); + return new SaltyCommandResult(false); + } + + + Context.Player.SendChatMessage($"[FamilyInfo] : {familyName}", ChatMessageColorType.Green); + Context.Player.SendChatMessage("===============================", ChatMessageColorType.Green); + ISerializer serializer = new SerializerBuilder().WithNamingConvention(CamelCaseNamingConvention.Instance).Build(); + string tmp = serializer.Serialize(family); + string[] lines = tmp.Split('\n'); + foreach (string line in lines) + { + Context.Player.SendChatMessage($"{line}", ChatMessageColorType.Green); + } + + Context.Player.SendChatMessage("===============================", ChatMessageColorType.Green); + return new SaltyCommandResult(true); + } + + + [Command("create")] + public async Task CreateFamilyAsync(string name) + { + await Context.Player.EmitEventAsync(new FamilyCreateEvent + { + Name = name + }); + return new SaltyCommandResult(true); + } + + [Command("logs-addxp")] + public async Task GenerateFamilyLog() + { + await Context.Player.EmitEventAsync(new FamilyAddLogEvent(new FamilyLogDto + { + FamilyLogType = FamilyLogType.FamilyXP, + Actor = Context.Player.PlayerEntity.Name, + Argument1 = 100.ToString() + })); + return new SaltyCommandResult(true); + } + + [Command("logs-addlevelup")] + public async Task GenerateFamilyLog2() + { + await Context.Player.EmitEventAsync(new FamilyAddLogEvent(new FamilyLogDto + { + FamilyLogType = FamilyLogType.FamilyLevelUp, + Argument1 = 2.ToString() + })); + return new SaltyCommandResult(true); + } + + [Command("addxp")] + public async Task ObtainFamilyXp(int xp) + { + await Context.Player.EmitEventAsync(new FamilyAddExperienceEvent(xp, FamXpObtainedFromType.Command)); + return new SaltyCommandResult(true, $"[FAMILY] Adding {xp} xp"); + } + + [Command("set-level", "level")] + public async Task SetFamilyLevel() => + // todo implementation on microservice + new SaltyCommandResult(true); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/Commands/FamilyKeeperChange.cs b/srcs/_plugins/Plugin.FamilyImpl/Commands/FamilyKeeperChange.cs new file mode 100644 index 0000000..2bd2f02 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/Commands/FamilyKeeperChange.cs @@ -0,0 +1,8 @@ +namespace Plugin.FamilyImpl.Commands +{ + public enum FamilyKeeperChange + { + Dismiss, + Appointment + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/Commands/FamilyModule.cs b/srcs/_plugins/Plugin.FamilyImpl/Commands/FamilyModule.cs new file mode 100644 index 0000000..523b736 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/Commands/FamilyModule.cs @@ -0,0 +1,169 @@ +using System; +using System.Threading.Tasks; +using Qmmands; +using WingsEmu.Commands.Checks; +using WingsEmu.Commands.Entities; +using WingsEmu.DTOs.Account; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Families.Event; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums.Families; + +namespace Plugin.FamilyImpl.Commands +{ + [Name("family-nostale-ui")] + [Group("family", "fam")] + [RequireAuthority(AuthorityType.User)] + public sealed class FamilyModule : SaltyModuleBase + { + private readonly IGameLanguageService _gameLanguage; + + public FamilyModule(IGameLanguageService gameLanguage) => _gameLanguage = gameLanguage; + + [Command("shout")] + public async Task FamilyShoutAsync([Remainder] string message) + { + await Context.Player.EmitEventAsync(new FamilyShoutEvent(message)); + return new SaltyCommandResult(true); + } + + [Command("dismiss", "kick")] + public async Task FamilyDismissAsync([Remainder] string nickname) + { + await Context.Player.EmitEventAsync(new FamilyRemoveMemberEvent(nickname)); + return new SaltyCommandResult(true); + } + + [Command("leave")] + public async Task FamilyLeaveAsync() + { + if (!Context.Player.PlayerEntity.IsInFamily()) + { + Context.Player.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.FAMILY_INFO_NO_FAMILY, Context.Player.UserLanguage)); + return new SaltyCommandResult(false); + } + + Context.Player.SendQnaPacket("gleave", _gameLanguage.GetLanguage(GameDialogKey.FAMILY_INFO_ASK_FAMILY_LEAVE, Context.Player.UserLanguage)); + return new SaltyCommandResult(true); + } + + [Command("members")] + public async Task FamilyListMembersAsync() + { + await Context.Player.EmitEventAsync(new FamilyListMembersEvent()); + return new SaltyCommandResult(true); + } + + [Command("notice")] + public async Task FamilyNoticeAsync() + { + await Context.Player.EmitEventAsync(new FamilyNoticeMessageEvent(string.Empty, true)); + return new SaltyCommandResult(true); + } + + [Command("notice")] + public async Task FamilyNoticeAsync([Remainder] string message) + { + await Context.Player.EmitEventAsync(new FamilyNoticeMessageEvent(message)); + return new SaltyCommandResult(true); + } + + [Command("gender")] + public async Task ResetSexAsync(byte gender) + { + await Context.Player.EmitEventAsync(new FamilyChangeSexEvent(gender)); + return new SaltyCommandResult(true); + } + + [Command("title")] + public async Task TitleChangeAsync(string nickname, FamilyTitle familyTitle) + { + await Context.Player.EmitEventAsync(new FamilyChangeTitleEvent(nickname, familyTitle)); + return new SaltyCommandResult(true); + } + + [Command("today")] + public async Task TodayMessageAsync([Remainder] string message) + { + await Context.Player.EmitEventAsync(new FamilyTodayEvent(message)); + return new SaltyCommandResult(true); + } + + [Command("invite")] + [Description("Invite player to family.")] + public async Task InviteFamilyAsync( + [Remainder] [Description("Player nickname")] + string nickname) + { + await Context.Player.EmitEventAsync(new FamilySendInviteEvent(nickname)); + return new SaltyCommandResult(true); + } + + [Command("deputy")] + public async Task FamilyDeputyAsync(string sourceName, [Remainder] string targetName) + { + await Context.Player.EmitEventAsync(new FamilyChangeDeputyEvent(sourceName, targetName)); + return new SaltyCommandResult(true); + } + + [Command("deputy")] + public async Task FamilyDeputyAsync([Remainder] string targetName) + { + await Context.Player.EmitEventAsync(new FamilyChangeAuthorityEvent(FamilyAuthority.Deputy, 0, 0, targetName)); + return new SaltyCommandResult(true); + } + + [Command("head")] + public async Task FamilyHeadAsync([Remainder] string nickname) + { + await Context.Player.EmitEventAsync(new FamilyChangeAuthorityEvent(FamilyAuthority.Head, 0, 0, nickname)); + return new SaltyCommandResult(true); + } + + [Command("keeper", "assistant")] + public async Task FamilyKeeperAsync(string familyKeeperChange, [Remainder] string nickname) + { + if (!Enum.TryParse(familyKeeperChange, out FamilyKeeperChange change)) + { + return new SaltyCommandResult(false); + } + + FamilyAuthority authority = change switch + { + FamilyKeeperChange.Dismiss => FamilyAuthority.Member, + FamilyKeeperChange.Appointment => FamilyAuthority.Keeper + }; + + await Context.Player.EmitEventAsync(new FamilyChangeAuthorityEvent(authority, 0, 0, nickname)); + return new SaltyCommandResult(true); + } + + [Command("keeper", "assistant")] + public async Task FamilyKeeperAsync([Remainder] string nickname) + { + await Context.Player.EmitEventAsync(new FamilyChangeAuthorityEvent(FamilyAuthority.Keeper, 0, 0, nickname)); + return new SaltyCommandResult(true); + } + + [Command("disband")] + public async Task FamilyDisbandAsync() + { + IClientSession session = Context.Player; + if (!session.PlayerEntity.IsInFamily()) + { + session.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.FAMILY_INFO_NOT_IN_FAMILY, session.UserLanguage)); + return new SaltyCommandResult(false); + } + + if (session.PlayerEntity.GetFamilyAuthority() != FamilyAuthority.Head) + { + session.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.FAMILY_INFO_NOT_FAMILY_HEAD, session.UserLanguage)); + return new SaltyCommandResult(false); + } + + session.SendQnaPacket("glrm 1", _gameLanguage.GetLanguage(GameDialogKey.FAMILY_DIALOG_ASK_DISMISS_FAMILY, session.UserLanguage)); + return new SaltyCommandResult(true); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/Commands/FamilyNostaleUiCommandsModule.cs b/srcs/_plugins/Plugin.FamilyImpl/Commands/FamilyNostaleUiCommandsModule.cs new file mode 100644 index 0000000..282b853 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/Commands/FamilyNostaleUiCommandsModule.cs @@ -0,0 +1,168 @@ +using System; +using System.Threading.Tasks; +using Qmmands; +using WingsEmu.Commands.Checks; +using WingsEmu.Commands.Entities; +using WingsEmu.DTOs.Account; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Families.Event; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums.Families; + +namespace Plugin.FamilyImpl.Commands +{ + [Name("family")] + [RequireAuthority(AuthorityType.User)] + public sealed class FamilyNostaleUiCommandsModule : SaltyModuleBase + { + private readonly IGameLanguageService _gameLanguage; + + public FamilyNostaleUiCommandsModule(IGameLanguageService gameLanguage) => _gameLanguage = gameLanguage; + + [Command("familyshout")] + public async Task FamilyShoutAsync([Remainder] string message) + { + await Context.Player.EmitEventAsync(new FamilyShoutEvent(message)); + return new SaltyCommandResult(true); + } + + [Command("familydismiss")] + public async Task FamilyDismissAsync([Remainder] string nickname) + { + await Context.Player.EmitEventAsync(new FamilyRemoveMemberEvent(nickname)); + return new SaltyCommandResult(true); + } + + [Command("familyleave")] + public async Task FamilyLeaveAsync() + { + if (!Context.Player.PlayerEntity.IsInFamily()) + { + Context.Player.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.FAMILY_INFO_NO_FAMILY, Context.Player.UserLanguage)); + return new SaltyCommandResult(false); + } + + Context.Player.SendQnaPacket("gleave", _gameLanguage.GetLanguage(GameDialogKey.FAMILY_INFO_ASK_FAMILY_LEAVE, Context.Player.UserLanguage)); + return new SaltyCommandResult(true); + } + + [Command("familymembers")] + public async Task FamilyListMembersAsync() + { + await Context.Player.EmitEventAsync(new FamilyListMembersEvent()); + return new SaltyCommandResult(true); + } + + [Command("notice")] + public async Task FamilyNoticeAsync() + { + await Context.Player.EmitEventAsync(new FamilyNoticeMessageEvent(string.Empty, true)); + return new SaltyCommandResult(true); + } + + [Command("notice")] + public async Task FamilyNoticeAsync([Remainder] string message) + { + await Context.Player.EmitEventAsync(new FamilyNoticeMessageEvent(message)); + return new SaltyCommandResult(true); + } + + [Command("gender")] + public async Task ResetSexAsync(byte gender) + { + await Context.Player.EmitEventAsync(new FamilyChangeSexEvent(gender)); + return new SaltyCommandResult(true); + } + + [Command("title")] + public async Task TitleChangeAsync(string nickname, FamilyTitle familyTitle) + { + await Context.Player.EmitEventAsync(new FamilyChangeTitleEvent(nickname, familyTitle)); + return new SaltyCommandResult(true); + } + + [Command("today")] + public async Task TodayMessageAsync([Remainder] string message) + { + await Context.Player.EmitEventAsync(new FamilyTodayEvent(message)); + return new SaltyCommandResult(true); + } + + [Command("familyinvite")] + [Description("Invite player to family.")] + public async Task InviteFamilyAsync( + [Remainder] [Description("Player nickname")] + string nickname) + { + await Context.Player.EmitEventAsync(new FamilySendInviteEvent(nickname)); + return new SaltyCommandResult(true); + } + + [Command("familydeputy")] + public async Task FamilyDeputyAsync(string sourceName, [Remainder] string targetName) + { + await Context.Player.EmitEventAsync(new FamilyChangeDeputyEvent(sourceName, targetName)); + return new SaltyCommandResult(true); + } + + [Command("familydeputy")] + public async Task FamilyDeputyAsync([Remainder] string targetName) + { + await Context.Player.EmitEventAsync(new FamilyChangeAuthorityEvent(FamilyAuthority.Deputy, 0, 0, targetName)); + return new SaltyCommandResult(true); + } + + [Command("familyhead")] + public async Task FamilyHeadAsync([Remainder] string nickname) + { + await Context.Player.EmitEventAsync(new FamilyChangeAuthorityEvent(FamilyAuthority.Head, 0, 0, nickname)); + return new SaltyCommandResult(true); + } + + [Command("familykeeper")] + public async Task FamilyKeeperAsync(string familyKeeperChange, [Remainder] string nickname) + { + if (!Enum.TryParse(familyKeeperChange, out FamilyKeeperChange change)) + { + return new SaltyCommandResult(false); + } + + FamilyAuthority authority = change switch + { + FamilyKeeperChange.Dismiss => FamilyAuthority.Member, + FamilyKeeperChange.Appointment => FamilyAuthority.Keeper + }; + + await Context.Player.EmitEventAsync(new FamilyChangeAuthorityEvent(authority, 0, 0, nickname)); + return new SaltyCommandResult(true); + } + + [Command("familykeeper")] + public async Task FamilyKeeperAsync([Remainder] string nickname) + { + await Context.Player.EmitEventAsync(new FamilyChangeAuthorityEvent(FamilyAuthority.Keeper, 0, 0, nickname)); + return new SaltyCommandResult(true); + } + + [Command("familydisband")] + public async Task FamilyDisbandAsync() + { + IClientSession session = Context.Player; + if (!session.PlayerEntity.IsInFamily()) + { + session.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.FAMILY_INFO_NOT_IN_FAMILY, session.UserLanguage)); + return new SaltyCommandResult(false); + } + + if (session.PlayerEntity.GetFamilyAuthority() != FamilyAuthority.Head) + { + session.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.FAMILY_INFO_NOT_FAMILY_HEAD, session.UserLanguage)); + return new SaltyCommandResult(false); + } + + session.SendQnaPacket("glrm 1", _gameLanguage.GetLanguage(GameDialogKey.FAMILY_DIALOG_ASK_DISMISS_FAMILY, session.UserLanguage)); + return new SaltyCommandResult(true); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/Consumers/FamilyAcknowledgeExperiencesMessageConsumer.cs b/srcs/_plugins/Plugin.FamilyImpl/Consumers/FamilyAcknowledgeExperiencesMessageConsumer.cs new file mode 100644 index 0000000..343ea51 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/Consumers/FamilyAcknowledgeExperiencesMessageConsumer.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; +using Plugin.FamilyImpl.Messages; +using WingsAPI.Game.Extensions.Families; +using WingsEmu.Game.Families; +using WingsEmu.Game.Managers; + +namespace Plugin.FamilyImpl.Consumers +{ + public class FamilyAcknowledgeExperiencesMessageConsumer : IMessageConsumer + { + private readonly IFamilyManager _familyManager; + private readonly ISessionManager _sessionManager; + + public FamilyAcknowledgeExperiencesMessageConsumer(IFamilyManager familyManager, ISessionManager sessionManager) + { + _familyManager = familyManager; + _sessionManager = sessionManager; + } + + public async Task HandleAsync(FamilyAcknowledgeExperienceGainedMessage e, CancellationToken cancellation) + { + IEnumerable familyIds = _familyManager.AddToFamilyExperiences(e.Experiences); + + foreach (long familyId in familyIds) + { + FamilyPacketExtensions.SendMembersExpToMembers(_familyManager.GetFamilyByFamilyId(familyId), _sessionManager); + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/Consumers/FamilyAcknowledgeLogsMessageConsumer.cs b/srcs/_plugins/Plugin.FamilyImpl/Consumers/FamilyAcknowledgeLogsMessageConsumer.cs new file mode 100644 index 0000000..ce62286 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/Consumers/FamilyAcknowledgeLogsMessageConsumer.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; +using Plugin.FamilyImpl.Messages; +using WingsAPI.Data.Families; +using WingsAPI.Game.Extensions.Families; +using WingsEmu.Game.Families; +using WingsEmu.Game.Managers; + +namespace Plugin.FamilyImpl.Consumers +{ + public class FamilyAcknowledgeLogsMessageConsumer : IMessageConsumer + { + private readonly IFamilyManager _familyManager; + private readonly ISessionManager _sessionManager; + + public FamilyAcknowledgeLogsMessageConsumer(IFamilyManager familyManager, ISessionManager sessionManager) + { + _familyManager = familyManager; + _sessionManager = sessionManager; + } + + public async Task HandleAsync(FamilyAcknowledgeLogsMessage e, CancellationToken cancellation) + { + _familyManager.AddToFamilyLogs(e.Logs); + foreach (KeyValuePair> pair in e.Logs) + { + FamilyPacketExtensions.SendFamilyLogsToMembers(_familyManager.GetFamilyByFamilyId(pair.Key), _sessionManager); + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/Consumers/FamilyChangeFactionMessageConsumer.cs b/srcs/_plugins/Plugin.FamilyImpl/Consumers/FamilyChangeFactionMessageConsumer.cs new file mode 100644 index 0000000..b3b3a8c --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/Consumers/FamilyChangeFactionMessageConsumer.cs @@ -0,0 +1,93 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; +using Plugin.FamilyImpl.Messages; +using WingsAPI.Communication.ServerApi.Protocol; +using WingsAPI.Game.Extensions.Families; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Families; +using WingsEmu.Game.Families.Configuration; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Networking; +using WingsEmu.Game.TimeSpaces.Events; +using WingsEmu.Packets.Enums; + +namespace Plugin.FamilyImpl.Consumers +{ + public class FamilyChangeFactionMessageConsumer : IMessageConsumer + { + private readonly FamilyConfiguration _familyConfiguration; + private readonly IFamilyManager _familyManager; + private readonly SerializableGameServer _serializableGameServer; + private readonly ISessionManager _sessionManager; + + public FamilyChangeFactionMessageConsumer(IFamilyManager familyManager, ISessionManager sessionManager, FamilyConfiguration familyConfiguration, SerializableGameServer serializableGameServer) + { + _familyManager = familyManager; + _sessionManager = sessionManager; + _familyConfiguration = familyConfiguration; + _serializableGameServer = serializableGameServer; + } + + public async Task HandleAsync(FamilyChangeFactionMessage notification, CancellationToken token) + { + long familyId = notification.FamilyId; + FactionType factionType = notification.NewFaction; + + Family family = _familyManager.GetFamilyByFamilyId(familyId); + if (family == null) + { + return; + } + + family.Faction = (byte)notification.NewFaction; + foreach (FamilyMembership member in family.Members.ToList()) + { + IClientSession memberSession = _sessionManager.GetSessionByCharacterId(member.CharacterId); + if (memberSession == null) + { + continue; + } + + if (memberSession.PlayerEntity.Faction == factionType) + { + continue; + } + + await memberSession.EmitEventAsync(new ChangeFactionEvent + { + NewFaction = factionType + }); + + if (memberSession.CurrentMapInstance == null) + { + continue; + } + + if (_serializableGameServer.ChannelType == GameChannelType.ACT_4) + { + if (memberSession.PlayerEntity.TimeSpaceComponent.IsInTimeSpaceParty) + { + await memberSession.EmitEventAsync(new TimeSpaceLeavePartyEvent()); + } + + await memberSession.EmitEventAsync(new PlayerReturnFromAct4Event()); + continue; + } + + if (memberSession.CurrentMapInstance.MapInstanceType == MapInstanceType.NormalInstance) + { + memberSession.ChangeToLastBaseMap(); + continue; + } + + await memberSession.EmitEventAsync(new PlayerReturnFromAct4Event()); + } + + FamilyPacketExtensions.SendFamilyInfoToMembers(family, _sessionManager, _familyConfiguration); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/Consumers/FamilyCharacterJoinMessageConsumer.cs b/srcs/_plugins/Plugin.FamilyImpl/Consumers/FamilyCharacterJoinMessageConsumer.cs new file mode 100644 index 0000000..ef64d38 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/Consumers/FamilyCharacterJoinMessageConsumer.cs @@ -0,0 +1,38 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; +using Plugin.FamilyImpl.Messages; +using WingsAPI.Game.Extensions.Families; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Families; +using WingsEmu.Game.Managers; + +namespace Plugin.FamilyImpl.Consumers +{ + public class FamilyCharacterJoinMessageConsumer : IMessageConsumer + { + private readonly IFamilyManager _family; + private readonly IGameLanguageService _gameLanguage; + private readonly ISessionManager _sessionManager; + + public FamilyCharacterJoinMessageConsumer(IFamilyManager family, IGameLanguageService gameLanguage, ISessionManager sessionManager) + { + _family = family; + _gameLanguage = gameLanguage; + _sessionManager = sessionManager; + } + + public async Task HandleAsync(FamilyCharacterJoinMessage notification, CancellationToken token) + { + long? familyId = notification.FamilyId; + + if (familyId == null) + { + return; + } + + IFamily family = _family.GetFamilyByFamilyId(familyId.Value); + family?.SendOnlineStatusToMembers(_sessionManager, notification.CharacterId, true, _gameLanguage); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/Consumers/FamilyCharacterLeaveMessageConsumer.cs b/srcs/_plugins/Plugin.FamilyImpl/Consumers/FamilyCharacterLeaveMessageConsumer.cs new file mode 100644 index 0000000..2c7e301 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/Consumers/FamilyCharacterLeaveMessageConsumer.cs @@ -0,0 +1,34 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; +using Plugin.FamilyImpl.Messages; +using WingsAPI.Game.Extensions.Families; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Families; +using WingsEmu.Game.Managers; + +namespace Plugin.FamilyImpl.Consumers +{ + public class FamilyCharacterLeaveMessageConsumer : IMessageConsumer + { + private readonly IFamilyManager _familyManager; + private readonly IGameLanguageService _gameLanguage; + private readonly ISessionManager _sessionManager; + + public FamilyCharacterLeaveMessageConsumer(IFamilyManager familyManager, IGameLanguageService gameLanguage, ISessionManager sessionManager) + { + _familyManager = familyManager; + _gameLanguage = gameLanguage; + _sessionManager = sessionManager; + } + + public async Task HandleAsync(FamilyCharacterLeaveMessage notification, CancellationToken token) + { + long characterId = notification.CharacterId; + long familyId = notification.FamilyId; + + IFamily family = _familyManager.GetFamilyByFamilyId(familyId); + family?.SendOnlineStatusToMembers(_sessionManager, characterId, false, _gameLanguage); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/Consumers/FamilyChatMessageConsumer.cs b/srcs/_plugins/Plugin.FamilyImpl/Consumers/FamilyChatMessageConsumer.cs new file mode 100644 index 0000000..3c495d2 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/Consumers/FamilyChatMessageConsumer.cs @@ -0,0 +1,46 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; +using Plugin.FamilyImpl.Messages; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.FamilyImpl.Consumers +{ + public class FamilyChatMessageConsumer : IMessageConsumer + { + private readonly IGameLanguageService _gameLanguage; + private readonly IServerManager _serverManager; + private readonly ISessionManager _sessionManager; + + public FamilyChatMessageConsumer(ISessionManager sessionManager, IServerManager serverManager, IGameLanguageService gameLanguage) + { + _sessionManager = sessionManager; + _serverManager = serverManager; + _gameLanguage = gameLanguage; + } + + public async Task HandleAsync(FamilyChatMessage e, CancellationToken cancellation) + { + string message = $"[{e.SenderNickname}]:{e.Message}"; + + if (_serverManager.ChannelId != e.SenderChannelId) + { + message = $": {e.SenderChannelId.ToString()}>" + message; + _sessionManager.BroadcastToFamily(e.SenderFamilyId, x => GetMessage(x, message)); + } + else + { + _sessionManager.BroadcastToFamily(e.SenderFamilyId, x => GenerateFamilyChatLocalChannel(message)); + } + } + + private static Task GenerateFamilyChatLocalChannel(string message) => Task.FromResult(UiPacketExtension.GenerateSayNoIdPacket(message, ChatMessageColorType.Blue)); + + private Task GetMessage(IClientSession session, string message) + => Task.FromResult(UiPacketExtension.GenerateSayNoIdPacket($"<{_gameLanguage.GetLanguage(GameDialogKey.INFORMATION_CHANNEL, session.UserLanguage)}" + message, ChatMessageColorType.Blue)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/Consumers/FamilyCreatedMessageConsumer.cs b/srcs/_plugins/Plugin.FamilyImpl/Consumers/FamilyCreatedMessageConsumer.cs new file mode 100644 index 0000000..d45242e --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/Consumers/FamilyCreatedMessageConsumer.cs @@ -0,0 +1,29 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; +using Plugin.FamilyImpl.Messages; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.FamilyImpl.Consumers +{ + public class FamilyCreatedMessageConsumer : IMessageConsumer + { + private readonly IGameLanguageService _gameLanguage; + private readonly ISessionManager _sessionManager; + + public FamilyCreatedMessageConsumer(ISessionManager sessionManager, IGameLanguageService gameLanguage) + { + _sessionManager = sessionManager; + _gameLanguage = gameLanguage; + } + + public async Task HandleAsync(FamilyCreatedMessage e, CancellationToken cancellation) + { + await _sessionManager.BroadcastAsync(async s => + s.GenerateMsgPacket(_gameLanguage.GetLanguageFormat(GameDialogKey.FAMILY_SHOUTMESSAGE_FAMILY_CREATED, s.UserLanguage, e.FamilyName), MsgMessageType.Middle)); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/Consumers/FamilyDisbandMessageConsumer.cs b/srcs/_plugins/Plugin.FamilyImpl/Consumers/FamilyDisbandMessageConsumer.cs new file mode 100644 index 0000000..222f3ed --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/Consumers/FamilyDisbandMessageConsumer.cs @@ -0,0 +1,45 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; +using Plugin.FamilyImpl.Messages; +using WingsAPI.Game.Extensions.Families; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Families; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; + +namespace Plugin.FamilyImpl.Consumers +{ + public class FamilyDisbandMessageConsumer : IMessageConsumer + { + private readonly IFamilyManager _familyManager; + private readonly IGameLanguageService _gameLanguageService; + private readonly ISessionManager _sessionManager; + + public FamilyDisbandMessageConsumer(IFamilyManager familyManager, ISessionManager sessionManager, IGameLanguageService gameLanguageService) + { + _familyManager = familyManager; + _sessionManager = sessionManager; + _gameLanguageService = gameLanguageService; + } + + public async Task HandleAsync(FamilyDisbandMessage notification, CancellationToken token) + { + long familyId = notification.FamilyId; + + IFamily family = _familyManager.GetFamilyByFamilyId(familyId); + if (family == null) + { + return; + } + + _familyManager.RemoveFamily(family.Id); + foreach (FamilyMembership member in family.Members) + { + IClientSession memberSession = _sessionManager.GetSessionByCharacterId(member.CharacterId); + memberSession?.SendResetFamilyInterface(); + memberSession?.BroadcastGidx(null, _gameLanguageService); + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/Consumers/FamilyMemberAddedMessageConsumer.cs b/srcs/_plugins/Plugin.FamilyImpl/Consumers/FamilyMemberAddedMessageConsumer.cs new file mode 100644 index 0000000..34d4fd7 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/Consumers/FamilyMemberAddedMessageConsumer.cs @@ -0,0 +1,71 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; +using Plugin.FamilyImpl.Messages; +using WingsAPI.Game.Extensions.Families; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Families; +using WingsEmu.Game.Families.Configuration; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; +using WingsEmu.Packets.Enums.Families; + +namespace Plugin.FamilyImpl.Consumers +{ + public class FamilyMemberAddedMessageConsumer : IMessageConsumer + { + private readonly FamilyConfiguration _familyConfiguration; + private readonly IFamilyManager _familyManager; + private readonly IGameLanguageService _gameLanguage; + private readonly ISessionManager _sessionManager; + + public FamilyMemberAddedMessageConsumer(IFamilyManager familyManager, ISessionManager sessionManager, IGameLanguageService gameLanguage, FamilyConfiguration familyConfiguration) + { + _familyManager = familyManager; + _sessionManager = sessionManager; + _gameLanguage = gameLanguage; + _familyConfiguration = familyConfiguration; + } + + public async Task HandleAsync(FamilyMemberAddedMessage e, CancellationToken cancellation) + { + long senderId = e.SenderId; + Family family = _familyManager.GetFamilyByFamilyId(e.AddedMember.FamilyId); + _familyManager.AddOrReplaceMember(e.AddedMember, family); + FamilyPacketExtensions.SendFamilyMembersInfoToMembers(family, _sessionManager, _familyConfiguration); + + _sessionManager.BroadcastToFamily(e.AddedMember.FamilyId, + async x => x.GenerateMsgPacket(_gameLanguage.GetLanguageFormat(GameDialogKey.FAMILY_SHOUTMESSAGE_MEMBER_JOINED, x.UserLanguage, e.Nickname), MsgMessageType.Middle)); + + IClientSession localAddedMemberSession = _sessionManager.GetSessionByCharacterId(e.AddedMember.CharacterId); + + if (localAddedMemberSession == null) + { + return; + } + + if (localAddedMemberSession.PlayerEntity.Faction != (FactionType)family.Faction) + { + await localAddedMemberSession.EmitEventAsync(new ChangeFactionEvent + { + NewFaction = (FactionType)family.Faction + }); + } + + localAddedMemberSession.BroadcastGidx(family, _gameLanguage); + + FamilyMembership senderMembership = family.Members.FirstOrDefault(x => x.CharacterId == senderId); + if (senderMembership == null) + { + return; + } + + await localAddedMemberSession.FamilyAddLogAsync(FamilyLogType.MemberJoin, senderMembership.Character.Name, localAddedMemberSession.PlayerEntity.Name); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/Consumers/FamilyMemberInviteMessageConsumer.cs b/srcs/_plugins/Plugin.FamilyImpl/Consumers/FamilyMemberInviteMessageConsumer.cs new file mode 100644 index 0000000..6a71a3e --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/Consumers/FamilyMemberInviteMessageConsumer.cs @@ -0,0 +1,34 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; +using Plugin.FamilyImpl.Messages; +using WingsEmu.Game.Families.Event; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; + +namespace Plugin.FamilyImpl.Consumers +{ + public class FamilyMemberInviteMessageConsumer : IMessageConsumer + { + private readonly ISessionManager _sessionManager; + + public FamilyMemberInviteMessageConsumer(ISessionManager sessionManager) => _sessionManager = sessionManager; + + public async Task HandleAsync(FamilyInviteMessage e, CancellationToken cancellation) + { + IClientSession localSession = _sessionManager.GetSessionByCharacterName(e.ReceiverNickname); + + if (localSession == null) + { + return; + } + + if (localSession.PlayerEntity.RainbowBattleComponent.IsInRainbowBattle) + { + return; + } + + await localSession.EmitEventAsync(new FamilyReceiveInviteEvent(e.FamilyName, e.SenderCharacterId, e.FamilyId)); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/Consumers/FamilyMemberRemovedMessageConsumer.cs b/srcs/_plugins/Plugin.FamilyImpl/Consumers/FamilyMemberRemovedMessageConsumer.cs new file mode 100644 index 0000000..29cd014 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/Consumers/FamilyMemberRemovedMessageConsumer.cs @@ -0,0 +1,41 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; +using Plugin.FamilyImpl.Messages; +using WingsAPI.Game.Extensions.Families; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Families; +using WingsEmu.Game.Families.Configuration; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; + +namespace Plugin.FamilyImpl.Consumers +{ + public class FamilyMemberRemovedMessageConsumer : IMessageConsumer + { + private readonly FamilyConfiguration _familyConfiguration; + private readonly IFamilyManager _familyManager; + private readonly IGameLanguageService _languageService; + private readonly ISessionManager _sessionManager; + + public FamilyMemberRemovedMessageConsumer(IFamilyManager familyManager, ISessionManager sessionManager, IGameLanguageService languageService, FamilyConfiguration familyConfiguration) + { + _familyManager = familyManager; + _sessionManager = sessionManager; + _languageService = languageService; + _familyConfiguration = familyConfiguration; + } + + public async Task HandleAsync(FamilyMemberRemovedMessage e, CancellationToken cancellation) + { + _familyManager.RemoveMember(e.CharacterId, e.FamilyId); + + IClientSession localSession = _sessionManager.GetSessionByCharacterId(e.CharacterId); + localSession?.SendResetFamilyInterface(); + localSession?.BroadcastGidx(null, _languageService); + + Family family = _familyManager.GetFamilyByFamilyId(e.FamilyId); + FamilyPacketExtensions.SendFamilyMembersInfoToMembers(family, _sessionManager, _familyConfiguration); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/Consumers/FamilyMemberUpdateMessageConsumer.cs b/srcs/_plugins/Plugin.FamilyImpl/Consumers/FamilyMemberUpdateMessageConsumer.cs new file mode 100644 index 0000000..16b73e7 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/Consumers/FamilyMemberUpdateMessageConsumer.cs @@ -0,0 +1,76 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; +using Plugin.FamilyImpl.Messages; +using WingsAPI.Data.Families; +using WingsAPI.Game.Extensions.Families; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Families; +using WingsEmu.Game.Families.Configuration; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums.Chat; +using WingsEmu.Packets.Enums.Families; + +namespace Plugin.FamilyImpl.Consumers +{ + public class FamilyMemberUpdateMessageConsumer : IMessageConsumer + { + private readonly FamilyConfiguration _familyConfiguration; + private readonly IFamilyManager _familyManager; + private readonly IGameLanguageService _languageService; + private readonly ISessionManager _sessionManager; + + public FamilyMemberUpdateMessageConsumer(ISessionManager sessionManager, IGameLanguageService languageService, IFamilyManager familyManager, FamilyConfiguration familyConfiguration) + { + _sessionManager = sessionManager; + _languageService = languageService; + _familyManager = familyManager; + _familyConfiguration = familyConfiguration; + } + + public async Task HandleAsync(FamilyMemberUpdateMessage notification, CancellationToken token) + { + var families = new HashSet(); + + foreach (FamilyMembershipDto member in notification.UpdatedMembers) + { + Family family = _familyManager.GetFamilyByFamilyId(member.FamilyId); + _familyManager.AddOrReplaceMember(member, family); + families.Add(family); + + switch (notification.ChangedInfoMemberUpdate) + { + case ChangedInfoMemberUpdate.Authority: + if (member.Authority == FamilyAuthority.Head) + { + _sessionManager.BroadcastToFamily(member.FamilyId, + async x => x.GenerateMsgPacket(_languageService.GetLanguage(GameDialogKey.FAMILY_SHOUTMESSAGE_CHANGED_HEAD, x.UserLanguage), MsgMessageType.Middle)); + } + + IClientSession localSession = _sessionManager.GetSessionByCharacterId(member.CharacterId); + localSession?.BroadcastGidx(family, _languageService); + break; + } + } + + foreach (Family family in families) + { + switch (notification.ChangedInfoMemberUpdate) + { + case ChangedInfoMemberUpdate.Authority: + FamilyPacketExtensions.SendFamilyMembersAuthorityToMembers(family, _sessionManager, _familyConfiguration); + break; + case ChangedInfoMemberUpdate.Experience: + FamilyPacketExtensions.SendMembersExpToMembers(family, _sessionManager); + break; + case ChangedInfoMemberUpdate.DailyMessage: + FamilyPacketExtensions.SendMembersDailyMessages(family, _sessionManager); + break; + } + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/Consumers/FamilyShoutMessageConsumer.cs b/srcs/_plugins/Plugin.FamilyImpl/Consumers/FamilyShoutMessageConsumer.cs new file mode 100644 index 0000000..4dbd4be --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/Consumers/FamilyShoutMessageConsumer.cs @@ -0,0 +1,37 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; +using Plugin.FamilyImpl.Messages; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking.Broadcasting; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.FamilyImpl.Consumers +{ + public class FamilyShoutMessageConsumer : IMessageConsumer + { + private readonly IGameLanguageService _gameLanguage; + private readonly ISessionManager _sessionManager; + + public FamilyShoutMessageConsumer(ISessionManager sessionManager, IGameLanguageService gameLanguage) + { + _sessionManager = sessionManager; + _gameLanguage = gameLanguage; + } + + public async Task HandleAsync(FamilyShoutMessage notification, CancellationToken token) + { + string message = notification.Message; + long familyId = notification.FamilyId; + GameDialogKey gameDialogKey = notification.GameDialogKey; + + await _sessionManager.BroadcastAsync(async s => + { + string msg = _gameLanguage.GetLanguageFormat(gameDialogKey, s.UserLanguage, notification.SenderName, message); + return s.GenerateMsgPacket(msg, MsgMessageType.Middle); + }, new FamilyBroadcast(familyId)); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/Consumers/FamilyUpdateMessageConsumer.cs b/srcs/_plugins/Plugin.FamilyImpl/Consumers/FamilyUpdateMessageConsumer.cs new file mode 100644 index 0000000..cef83c5 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/Consumers/FamilyUpdateMessageConsumer.cs @@ -0,0 +1,183 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; +using Plugin.FamilyImpl.Messages; +using WingsAPI.Data.Families; +using WingsAPI.Game.Extensions.Families; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Families; +using WingsEmu.Game.Families.Configuration; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Managers.StaticData; + +namespace Plugin.FamilyImpl.Consumers +{ + public class FamilyUpdateMessageConsumer : IMessageConsumer + { + private readonly FamilyConfiguration _familyConfiguration; + private readonly IFamilyManager _familyManager; + private readonly IItemsManager _itemsManager; + private readonly IGameLanguageService _languageService; + private readonly IMessagePublisher _messagePublisher; + private readonly ISessionManager _sessionManager; + + public FamilyUpdateMessageConsumer(IFamilyManager familyManager, ISessionManager sessionManager, IGameLanguageService languageService, FamilyConfiguration familyConfiguration, + IMessagePublisher messagePublisher, IItemsManager itemsManager) + { + _familyManager = familyManager; + _sessionManager = sessionManager; + _languageService = languageService; + _familyConfiguration = familyConfiguration; + _messagePublisher = messagePublisher; + _itemsManager = itemsManager; + } + + public async Task HandleAsync(FamilyUpdateMessage e, CancellationToken cancellation) + { + foreach (FamilyDTO familyDto in e.Families) + { + Family family = _familyManager.GetFamilyByFamilyIdCache(familyDto.Id); + if (family == null) + { + continue; + } + + switch (e.ChangedInfoFamilyUpdate) + { + case ChangedInfoFamilyUpdate.Experience: + HandleFamilyExperience(family, familyDto); + break; + case ChangedInfoFamilyUpdate.Notice: + HandleFamilyNotice(family, familyDto); + break; + case ChangedInfoFamilyUpdate.HeadSex: + await HandleHeadSex(family, familyDto); + break; + case ChangedInfoFamilyUpdate.Settings: + HandleSettings(family, familyDto); + break; + case ChangedInfoFamilyUpdate.Upgrades: + HandleFamilyUpgradeUpdate(family, familyDto); + break; + case ChangedInfoFamilyUpdate.AchievementsAndMissions: + HandleFamilyAchievementsUpdate(family, familyDto); + break; + } + } + } + + private void HandleSettings(Family family, FamilyDTO familyDto) + { + family.AssistantWarehouseAuthorityType = familyDto.AssistantWarehouseAuthorityType; + family.AssistantCanInvite = familyDto.AssistantCanInvite; + family.AssistantCanGetHistory = familyDto.AssistantCanGetHistory; + family.AssistantCanNotice = familyDto.AssistantCanNotice; + family.AssistantCanShout = familyDto.AssistantCanShout; + + family.MemberWarehouseAuthorityType = familyDto.MemberWarehouseAuthorityType; + family.MemberCanGetHistory = familyDto.MemberCanGetHistory; + + FamilyPacketExtensions.SendFamilyInfoToMembers(family, _sessionManager, _familyConfiguration); + } + + private async Task HandleHeadSex(Family family, FamilyDTO familyDto) + { + family.HeadGender = familyDto.HeadGender; + FamilyPacketExtensions.SendFamilyInfoToMembers(family, _sessionManager, _familyConfiguration); + + await _messagePublisher.PublishAsync(new FamilyShoutMessage + { + FamilyId = family.Id, + GameDialogKey = GameDialogKey.FAMILY_SHOUTMESSAGE_HEAD_CHANGE_SEX + }); + } + + private void HandleFamilyNotice(Family family, FamilyDTO familyDto) + { + family.Message = familyDto.Message; + FamilyPacketExtensions.SendFamilyNoticeMessage(family, _sessionManager, _familyConfiguration); + } + + private void HandleFamilyExperience(Family family, FamilyDTO familyDto) + { + family.Experience = familyDto.Experience; + if (family.Level == familyDto.Level) + { + FamilyPacketExtensions.SendFamilyInfoToMembers(family, _sessionManager, _familyConfiguration); + return; + } + + /*_familyManager.SendLogToFamilyServer(new FamilyLogDto + { + Actor = familyDto.Level.ToString(), + FamilyId = familyDto.Id, + FamilyLogType = FamilyLogType.FamilyLevelUp, + Timestamp = DateTime.UtcNow + });*/ + + family.Level = familyDto.Level; + FamilyPacketExtensions.SendFamilyLevelUpMessageToMembers(family, _sessionManager, _languageService, _familyConfiguration); + } + + private void HandleFamilyUpgradeUpdate(Family family, FamilyDTO familyDto) + { + familyDto.Upgrades ??= new FamilyUpgradeDto(); + familyDto.Upgrades.UpgradesBought ??= new HashSet(); + familyDto.Upgrades.UpgradeValues ??= new Dictionary(); + + foreach (int upgradeId in familyDto.Upgrades.UpgradesBought) + { + if (family.Upgrades.ContainsKey(upgradeId)) + { + continue; + } + + family.Upgrades.Add(upgradeId, new FamilyUpgrade + { + Id = upgradeId, + State = FamilyUpgradeState.PASSIVE + }); + } + + foreach ((FamilyUpgradeType upgradeType, short value) in familyDto.Upgrades.UpgradeValues) + { + family.UpgradeValues[upgradeType] = value; + } + + family.SendFmpPacket(_sessionManager, _itemsManager); + } + + private void HandleFamilyAchievementsUpdate(Family family, FamilyDTO familyDto) + { + if (familyDto.Achievements?.Achievements != null) + { + family.Achievements.Clear(); + foreach ((int achievementId, FamilyAchievementCompletionDto achievement) in familyDto.Achievements.Achievements) + { + family.Achievements[achievementId] = achievement; + } + } + + if (familyDto.Achievements?.Progress != null) + { + family.AchievementProgress.Clear(); + foreach ((int achievementId, FamilyAchievementProgressDto achievement) in familyDto.Achievements.Progress) + { + family.AchievementProgress[achievementId] = achievement; + } + } + + if (familyDto.Missions?.Missions != null) + { + family.Mission.Clear(); + foreach ((int achievementId, FamilyMissionDto achievement) in familyDto.Missions.Missions) + { + family.Mission[achievementId] = achievement; + } + } + + family.SendFmiPacket(_sessionManager); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/Consumers/FamilyWarehouseItemUpdateMessageConsumer.cs b/srcs/_plugins/Plugin.FamilyImpl/Consumers/FamilyWarehouseItemUpdateMessageConsumer.cs new file mode 100644 index 0000000..cb07f16 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/Consumers/FamilyWarehouseItemUpdateMessageConsumer.cs @@ -0,0 +1,57 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; +using Plugin.FamilyImpl.Messages; +using WingsAPI.Data.Families; +using WingsAPI.Game.Extensions.Families; +using WingsAPI.Packets.Enums.Families; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Networking; + +namespace Plugin.FamilyImpl.Consumers +{ + public class FamilyWarehouseItemUpdateMessageConsumer : IMessageConsumer + { + private readonly IFamilyWarehouseManager _familyWarehouseManager; + private readonly IItemsManager _itemsManager; + private readonly ISessionManager _sessionManager; + + public FamilyWarehouseItemUpdateMessageConsumer(ISessionManager sessionManager, IItemsManager itemsManager, IFamilyWarehouseManager familyWarehouseManager) + { + _sessionManager = sessionManager; + _itemsManager = itemsManager; + _familyWarehouseManager = familyWarehouseManager; + } + + public async Task HandleAsync(FamilyWarehouseItemUpdateMessage notification, CancellationToken token) + { + await _familyWarehouseManager.UpdateWarehouseItem(notification.FamilyId, notification.UpdatedItems); + + foreach (IClientSession session in _sessionManager.Sessions) + { + if (!session.PlayerEntity.IsFamilyWarehouseOpen || session.PlayerEntity.Family?.Id != notification.FamilyId) + { + continue; + } + + if (!session.CheckPutWithdrawPermission(FamilyWarehouseAuthorityType.Put)) + { + continue; + } + + foreach ((FamilyWarehouseItemDto dto, short slot) in notification.UpdatedItems) + { + if (dto == null) + { + session.SendFamilyWarehouseRemoveItem(slot); + } + else + { + session.SendFamilyWarehouseAddItem(_itemsManager, dto); + } + } + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/Consumers/FamilyWarehouseLogAddMessageConsumer.cs b/srcs/_plugins/Plugin.FamilyImpl/Consumers/FamilyWarehouseLogAddMessageConsumer.cs new file mode 100644 index 0000000..557d23a --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/Consumers/FamilyWarehouseLogAddMessageConsumer.cs @@ -0,0 +1,19 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; +using Plugin.FamilyImpl.Messages; + +namespace Plugin.FamilyImpl.Consumers +{ + public class FamilyWarehouseLogAddMessageConsumer : IMessageConsumer + { + private readonly IFamilyWarehouseManager _familyWarehouseManager; + + public FamilyWarehouseLogAddMessageConsumer(IFamilyWarehouseManager familyWarehouseManager) => _familyWarehouseManager = familyWarehouseManager; + + public async Task HandleAsync(FamilyWarehouseLogAddMessage notification, CancellationToken token) + { + await _familyWarehouseManager.AddWarehouseLog(notification.FamilyId, notification.LogToAdd); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/FamiliesModuleExtensions.cs b/srcs/_plugins/Plugin.FamilyImpl/FamiliesModuleExtensions.cs new file mode 100644 index 0000000..6fcf045 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/FamiliesModuleExtensions.cs @@ -0,0 +1,81 @@ +// WingsEmu +// +// Developed by NosWings Team + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using PhoenixLib.Configuration; +using PhoenixLib.ServiceBus.Extensions; +using Plugin.FamilyImpl.Achievements; +using Plugin.FamilyImpl.Consumers; +using Plugin.FamilyImpl.Logs; +using Plugin.FamilyImpl.Messages; +using Plugin.FamilyImpl.RecurrentJob; +using WingsEmu.Communication.gRPC.Extensions; +using WingsEmu.Game.Families; +using WingsEmu.Game.Families.Configuration; + +namespace Plugin.FamilyImpl +{ + public static class FamiliesModuleExtensions + { + public static void AddFamilyModule(this IServiceCollection services) + { + services.AddGrpcFamilyServiceClient(); + + services.AddFileConfiguration(); + + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + + services.AddHostedService(); + services.AddHostedService(); + + services.AddMessagePublisher(); + services.AddMessageSubscriber(); + services.AddSingleton(); + services.AddMessagePublisher(); + services.AddMessageSubscriber(); + + services.AddMessagePublisher(); + services.AddMessageSubscriber(); + + services.AddMessageSubscriber(); + services.AddMessageSubscriber(); + services.AddMessageSubscriber(); + services.AddMessageSubscriber(); + services.AddMessageSubscriber(); + services.AddMessageSubscriber(); + services.AddMessageSubscriber(); + services.AddMessageSubscriber(); + services.AddMessageSubscriber(); + services.AddMessageSubscriber(); + services.AddMessageSubscriber(); + + services.AddMessageSubscriber(); + services.AddMessageSubscriber(); + + services.AddMessagePublisher(); + services.AddMessagePublisher(); + services.AddMessagePublisher(); + services.AddMessagePublisher(); + services.AddMessagePublisher(); + + + // achievements + services.TryAddSingleton(); + services.AddSingleton(s => s.GetRequiredService()); + services.AddHostedService(s => s.GetRequiredService()); + services.AddFileConfiguration(); + services.AddMessagePublisher(); + services.AddMessageSubscriber(); + + // missions + services.AddSingleton(s => s.GetRequiredService()); + services.AddFileConfiguration(); + services.AddMessagePublisher(); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/FamilyAddExperienceEventHandler.cs b/srcs/_plugins/Plugin.FamilyImpl/FamilyAddExperienceEventHandler.cs new file mode 100644 index 0000000..cddab7f --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/FamilyAddExperienceEventHandler.cs @@ -0,0 +1,19 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Families; +using WingsEmu.Game.Families.Event; + +namespace Plugin.FamilyImpl +{ + public class FamilyAddExperienceEventHandler : IAsyncEventProcessor + { + private readonly IFamilyManager _familyManager; + + public FamilyAddExperienceEventHandler(IFamilyManager familyManager) => _familyManager = familyManager; + + public async Task HandleAsync(FamilyAddExperienceEvent e, CancellationToken cancellation) => + _familyManager.SendExperienceToFamilyServer(new ExperienceGainedSubMessage(e.Sender.PlayerEntity.Id, e.ExperienceGained, e.FamXpObtainedFromType, DateTime.UtcNow)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/FamilyAddLogEventHandler.cs b/srcs/_plugins/Plugin.FamilyImpl/FamilyAddLogEventHandler.cs new file mode 100644 index 0000000..678353d --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/FamilyAddLogEventHandler.cs @@ -0,0 +1,25 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Data.Families; +using WingsEmu.Game.Families; +using WingsEmu.Game.Families.Event; + +namespace Plugin.FamilyImpl +{ + public class FamilyAddLogEventHandler : IAsyncEventProcessor + { + private readonly IFamilyManager _familyManager; + + public FamilyAddLogEventHandler(IFamilyManager familyManager) => _familyManager = familyManager; + + public async Task HandleAsync(FamilyAddLogEvent e, CancellationToken cancellation) + { + FamilyLogDto log = e.Log; + log.Timestamp = DateTime.UtcNow; + log.FamilyId = e.Sender.PlayerEntity.Family.Id; + _familyManager.SendLogToFamilyServer(e.Log); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/FamilyAddMemberEventHandler.cs b/srcs/_plugins/Plugin.FamilyImpl/FamilyAddMemberEventHandler.cs new file mode 100644 index 0000000..6cfc66b --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/FamilyAddMemberEventHandler.cs @@ -0,0 +1,62 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Communication.Families; +using WingsAPI.Data.Families; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Families; +using WingsEmu.Game.Families.Event; +using WingsEmu.Game.Managers; +using WingsEmu.Packets.Enums.Families; + +namespace Plugin.FamilyImpl +{ + public class FamilyAddMemberEventHandler : IAsyncEventProcessor + { + private readonly IFamilyManager _familyManager; + private readonly IFamilyService _familyService; + private readonly IGameLanguageService _gameLanguage; + private readonly ISessionManager _sessionManager; + + public FamilyAddMemberEventHandler(IGameLanguageService gameLanguage, IFamilyManager familyManager, IFamilyService familyService, ISessionManager sessionManager) + { + _gameLanguage = gameLanguage; + _familyManager = familyManager; + _familyService = familyService; + _sessionManager = sessionManager; + } + + public async Task HandleAsync(FamilyAddMemberEvent e, CancellationToken cancellation) + { + IFamily family = _familyManager.GetFamilyByFamilyId(e.FamilyIdToJoin); + if (family == null) + { + return; + } + + if (family.Members.Count >= family.GetMaximumMembershipCapacity()) + { + e.Sender.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.FAMILY_INFO_FULL, e.Sender.UserLanguage)); + return; + } + + await _familyService.AddMemberToFamilyAsync(new FamilyAddMemberRequest + { + Member = new FamilyMembershipDto + { + FamilyId = e.FamilyIdToJoin, + CharacterId = e.Sender.PlayerEntity.Id, + Authority = e.FamilyAuthority, + DailyMessage = null, + Experience = 0, + Title = FamilyTitle.Nothing, + JoinDate = DateTime.UtcNow + }, + Nickname = e.Sender.PlayerEntity.Name, + SenderId = e.SenderId + }); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/FamilyChangeAuthorityEventHandler.cs b/srcs/_plugins/Plugin.FamilyImpl/FamilyChangeAuthorityEventHandler.cs new file mode 100644 index 0000000..cea0834 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/FamilyChangeAuthorityEventHandler.cs @@ -0,0 +1,236 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using PhoenixLib.Logging; +using WingsAPI.Communication; +using WingsAPI.Communication.DbServer.CharacterService; +using WingsAPI.Communication.Families; +using WingsAPI.Data.Character; +using WingsAPI.Game.Extensions.Families; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Families; +using WingsEmu.Game.Families.Configuration; +using WingsEmu.Game.Families.Event; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums.Families; + +namespace Plugin.FamilyImpl +{ + public class FamilyChangeAuthorityEventHandler : IAsyncEventProcessor + { + private readonly ICharacterService _characterService; + private readonly FamilyConfiguration _familyConfiguration; + private readonly IFamilyService _familyService; + private readonly IGameLanguageService _gameLanguage; + private readonly ISessionManager _sessionManager; + + public FamilyChangeAuthorityEventHandler(IGameLanguageService gameLanguage, FamilyConfiguration familyConfiguration, IFamilyService familyService, ISessionManager sessionManager, + ICharacterService characterService) + { + _gameLanguage = gameLanguage; + _familyConfiguration = familyConfiguration; + _familyService = familyService; + _sessionManager = sessionManager; + _characterService = characterService; + } + + public async Task HandleAsync(FamilyChangeAuthorityEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + IFamily sessionFamily = session.PlayerEntity.Family; + long memberId = e.MemberId; + + if (sessionFamily == null) + { + session.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.FAMILY_INFO_NO_FAMILY, session.UserLanguage)); + return; + } + + FamilyAuthority senderAuthority = session.PlayerEntity.GetFamilyAuthority(); + + if (senderAuthority != FamilyAuthority.Head && senderAuthority != FamilyAuthority.Deputy) + { + session.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.FAMILY_INFO_NO_FAMILY_RIGHT, session.UserLanguage)); + return; + } + + if (senderAuthority > e.FamilyAuthority) + { + session.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.FAMILY_INFO_NO_FAMILY_RIGHT, session.UserLanguage)); + return; + } + + string memberName = e.CharacterName; + + if (!string.IsNullOrEmpty(memberName)) + { + IClientSession target = _sessionManager.GetSessionByCharacterName(memberName); + if (target == null) + { + DbServerGetCharacterResponse characterResponse = null; + try + { + characterResponse = await _characterService.GetCharacterByName(new DbServerGetCharacterRequestByName + { + CharacterName = memberName + }); + } + catch (Exception ex) + { + Log.Error("[FAMILY_CHANGE_AUTHORITY] Unexpected error: ", ex); + } + + if (characterResponse?.RpcResponseType != RpcResponseType.SUCCESS) + { + return; + } + + CharacterDTO targetCharacter = characterResponse.CharacterDto; + memberId = targetCharacter.Id; + } + else + { + memberId = target.PlayerEntity.Id; + } + } + + + if (senderAuthority == FamilyAuthority.Head && e.FamilyAuthority == senderAuthority) + { + if (e.Confirmed != 1) + { + e.Sender.SendQnaPacket($"fmg {((int)e.FamilyAuthority).ToString()} {memberId.ToString()} 1", + _gameLanguage.GetLanguage(GameDialogKey.FAMILY_DIALOG_CHANGE_HEAD, session.UserLanguage)); + return; + } + } + + bool validAction = false; + int numberOfDeputies = 0; + int numberOfAssistants = 0; + + foreach (FamilyMembership member in sessionFamily.Members.ToArray()) + { + switch (member.Authority) + { + case FamilyAuthority.Deputy: + numberOfDeputies++; + break; + case FamilyAuthority.Keeper: + numberOfAssistants++; + break; + } + + if (member.CharacterId != memberId) + { + continue; + } + + if (memberId == session.PlayerEntity.Id) + { + session.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.FAMILY_DIALOG_CANNOT_CHANGE_YOUR_OWN_AUTHORITY, session.UserLanguage)); + break; + } + + if (member.Authority == e.FamilyAuthority) + { + session.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.FAMILY_DIALOG_ALREADY_HAS_THAT_AUTHORITY, session.UserLanguage)); + break; + } + + if (senderAuthority > member.Authority) + { + session.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.FAMILY_INFO_NO_FAMILY_RIGHT, session.UserLanguage)); + break; + } + + if (member.Authority == senderAuthority || senderAuthority == e.FamilyAuthority && senderAuthority != FamilyAuthority.Head) + { + session.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.FAMILY_INFO_CANT_GIVE_SAME_AUTHORITY, session.UserLanguage)); + break; + } + + if (senderAuthority != FamilyAuthority.Head && e.FamilyAuthority < senderAuthority) + { + session.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.FAMILY_INFO_NO_FAMILY_RIGHT, session.UserLanguage)); + break; + } + + if (senderAuthority == FamilyAuthority.Head && e.FamilyAuthority == senderAuthority) + { + if (member.Authority != FamilyAuthority.Deputy) + { + session.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.FAMILY_INFO_TARGET_NEEDS_TO_BE_DEPUTY, session.UserLanguage)); + break; + } + } + + validAction = true; + memberName = member.Character?.Name; + } + + if (!validAction) + { + return; + } + + switch (e.FamilyAuthority) + { + case FamilyAuthority.Deputy when _familyConfiguration.DeputyLimit <= numberOfDeputies: + e.Sender.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.FAMILY_INFO_DEPUTIES_LIMIT_REACHED, e.Sender.UserLanguage)); + return; + case FamilyAuthority.Keeper when _familyConfiguration.KeeperLimit <= numberOfAssistants: + e.Sender.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.FAMILY_INFO_KEEPER_LIMIT_REACHED, e.Sender.UserLanguage)); + return; + default: + DbServerGetCharacterResponse characterResponse = null; + try + { + characterResponse = await _characterService.GetCharacterById(new DbServerGetCharacterByIdRequest + { + CharacterId = e.MemberId + }); + } + catch (Exception ex) + { + Log.Error("[FAMILY_CHANGE_AUTHORITY] Unexpected error: ", ex); + } + + if (characterResponse?.RpcResponseType == RpcResponseType.SUCCESS) + { + memberName ??= characterResponse.CharacterDto.Name; + } + + var list = new List(); + if (e.FamilyAuthority == FamilyAuthority.Head) + { + list.Add(new FamilyChangeContainer + { + CharacterId = session.PlayerEntity.Id, + RequestedFamilyAuthority = FamilyAuthority.Deputy + }); + + await session.FamilyAddLogAsync(FamilyLogType.AuthorityChanged, session.PlayerEntity.Name, ((byte)FamilyAuthority.Deputy).ToString(), session.PlayerEntity.Name); + } + + list.Add(new FamilyChangeContainer + { + CharacterId = memberId, + RequestedFamilyAuthority = e.FamilyAuthority + }); + + await session.FamilyAddLogAsync(FamilyLogType.AuthorityChanged, session.PlayerEntity.Name, ((byte)e.FamilyAuthority).ToString(), memberName); + + await _familyService.ChangeAuthorityByIdAsync(new FamilyChangeAuthorityRequest + { + FamilyMembers = list + }); + break; + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/FamilyChangeDeputyEventHandler.cs b/srcs/_plugins/Plugin.FamilyImpl/FamilyChangeDeputyEventHandler.cs new file mode 100644 index 0000000..c350fe5 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/FamilyChangeDeputyEventHandler.cs @@ -0,0 +1,199 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using PhoenixLib.Logging; +using WingsAPI.Communication; +using WingsAPI.Communication.DbServer.CharacterService; +using WingsAPI.Communication.Families; +using WingsAPI.Data.Character; +using WingsAPI.Game.Extensions.Families; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Families; +using WingsEmu.Game.Families.Event; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums.Families; + +namespace Plugin.FamilyImpl +{ + public class FamilyChangeDeputyEventHandler : IAsyncEventProcessor + { + private readonly ICharacterService _characterService; + private readonly IFamilyService _familyService; + private readonly IGameLanguageService _gameLanguage; + private readonly ISessionManager _sessionManager; + + public FamilyChangeDeputyEventHandler(IGameLanguageService gameLanguage, ISessionManager sessionManager, IFamilyService familyService, ICharacterService characterService) + { + _gameLanguage = gameLanguage; + _sessionManager = sessionManager; + _familyService = familyService; + _characterService = characterService; + } + + public async Task HandleAsync(FamilyChangeDeputyEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + string targetName = e.TargetName; + string sourceName = e.SourceName; + + long targetId; + long sourceId; + + if (string.IsNullOrEmpty(targetName)) + { + return; + } + + if (string.IsNullOrEmpty(sourceName)) + { + return; + } + + if (sourceName == targetName) + { + return; + } + + if (!session.PlayerEntity.IsInFamily()) + { + session.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.FAMILY_INFO_NO_FAMILY, session.UserLanguage)); + return; + } + + if (!session.PlayerEntity.IsHeadOfFamily()) + { + session.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.FAMILY_INFO_NO_FAMILY_RIGHT, session.UserLanguage)); + return; + } + + IClientSession source = _sessionManager.GetSessionByCharacterName(sourceName); + if (source == null) + { + DbServerGetCharacterResponse characterResponse = null; + try + { + characterResponse = await _characterService.GetCharacterByName(new DbServerGetCharacterRequestByName + { + CharacterName = sourceName + }); + } + catch (Exception ex) + { + Log.Error("[FAMILY_CHANGE_DEPUTY] Unexpected error: ", ex); + } + + if (characterResponse?.RpcResponseType != RpcResponseType.SUCCESS) + { + session.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.INFORMATION_MESSAGE_USER_NOT_FOUND, session.UserLanguage)); + return; + } + + CharacterDTO sourceCharacter = characterResponse.CharacterDto; + sourceId = sourceCharacter.Id; + } + else + { + sourceId = source.PlayerEntity.Id; + } + + IClientSession target = _sessionManager.GetSessionByCharacterName(targetName); + if (target == null) + { + DbServerGetCharacterResponse characterResponse = null; + try + { + characterResponse = await _characterService.GetCharacterByName(new DbServerGetCharacterRequestByName + { + CharacterName = targetName + }); + } + catch (Exception ex) + { + Log.Error("[FAMILY_CHANGE_DEPUTY] Unexpected error: ", ex); + } + + if (characterResponse?.RpcResponseType != RpcResponseType.SUCCESS) + { + session.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.INFORMATION_MESSAGE_USER_NOT_FOUND, session.UserLanguage)); + return; + } + + CharacterDTO targetCharacter = characterResponse.CharacterDto; + targetId = targetCharacter.Id; + } + else + { + targetId = target.PlayerEntity.Id; + } + + IFamily family = session.PlayerEntity.Family; + FamilyMembership targetMembership = null; + FamilyMembership sourceMembership = null; + + foreach (FamilyMembership member in family.Members) + { + if (member.CharacterId == sourceId) + { + sourceMembership = member; + continue; + } + + if (member.CharacterId != targetId) + { + continue; + } + + targetMembership = member; + } + + if (sourceMembership == null) + { + return; + } + + if (targetMembership == null) + { + return; + } + + FamilyAuthority sourceAuthority = sourceMembership.Authority; + FamilyAuthority targetAuthority = targetMembership.Authority; + + if (sourceAuthority != FamilyAuthority.Deputy) + { + return; + } + + if (targetAuthority <= FamilyAuthority.Deputy) + { + return; + } + + var list = new List + { + new() + { + CharacterId = targetMembership.CharacterId, + RequestedFamilyAuthority = sourceAuthority + }, + new() + { + CharacterId = sourceMembership.CharacterId, + RequestedFamilyAuthority = targetAuthority + } + }; + + await session.FamilyAddLogAsync(FamilyLogType.AuthorityChanged, session.PlayerEntity.Name, ((byte)targetAuthority).ToString(), sourceName); + await session.FamilyAddLogAsync(FamilyLogType.AuthorityChanged, session.PlayerEntity.Name, ((byte)sourceAuthority).ToString(), targetName); + + await _familyService.ChangeAuthorityByIdAsync(new FamilyChangeAuthorityRequest + { + FamilyMembers = list + }); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/FamilyChangeFactionEventHandler.cs b/srcs/_plugins/Plugin.FamilyImpl/FamilyChangeFactionEventHandler.cs new file mode 100644 index 0000000..fee6433 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/FamilyChangeFactionEventHandler.cs @@ -0,0 +1,76 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Communication.Families; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Families; +using WingsEmu.Game.Families.Event; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; +using WingsEmu.Packets.Enums.Families; + +namespace Plugin.FamilyImpl +{ + public class FamilyChangeFactionEventHandler : IAsyncEventProcessor + { + private readonly IFamilyService _familyService; + private readonly IGameLanguageService _gameLanguageService; + + public FamilyChangeFactionEventHandler(IGameLanguageService gameLanguageService, IFamilyService familyService) + { + _gameLanguageService = gameLanguageService; + _familyService = familyService; + } + + public async Task HandleAsync(FamilyChangeFactionEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + if (!session.PlayerEntity.IsInFamily()) + { + return; + } + + if (session.PlayerEntity.GetFamilyAuthority() != FamilyAuthority.Head) + { + session.SendInfo(_gameLanguageService.GetLanguage(GameDialogKey.FAMILY_INFO_NOT_FAMILY_HEAD, session.UserLanguage)); + return; + } + + if (!Enum.TryParse(e.Faction.ToString(), out FactionType factionType)) + { + return; + } + + if (factionType == FactionType.Neutral) + { + return; + } + + IFamily family = session.PlayerEntity.Family; + + FamilyChangeFactionResponse response = await _familyService.ChangeFactionByIdAsync(new FamilyChangeFactionRequest + { + FamilyId = family.Id, + NewFaction = factionType + }); + + switch (response.Status) + { + case FamilyChangeFactionResponseType.SUCCESS: + await session.RemoveItemFromInventory(factionType == FactionType.Angel ? (int)ItemVnums.ANGEL_EGG_FAMILY : (int)ItemVnums.DEMON_EGG_FAMILY); + break; + case FamilyChangeFactionResponseType.ALREADY_THAT_FACTION: + session.SendMsg(_gameLanguageService.GetLanguage(GameDialogKey.FAMILY_SHOUTMESSAGE_ALREADY_THAT_FACTION, session.UserLanguage), MsgMessageType.Middle); + break; + case FamilyChangeFactionResponseType.UNDER_COOLDOWN: + session.SendMsg(_gameLanguageService.GetLanguage(GameDialogKey.FAMILY_SHOUTMESSAGE_CHANGE_FACTION_UNDER_COOLDOWN, session.UserLanguage), MsgMessageType.Middle); + break; + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/FamilyChangeSettingsEventHandler.cs b/srcs/_plugins/Plugin.FamilyImpl/FamilyChangeSettingsEventHandler.cs new file mode 100644 index 0000000..3d8d3b1 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/FamilyChangeSettingsEventHandler.cs @@ -0,0 +1,155 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Communication; +using WingsAPI.Communication.Families; +using WingsAPI.Game.Extensions.Families; +using WingsAPI.Packets.Enums.Families; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Families; +using WingsEmu.Game.Families.Event; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums.Families; + +namespace Plugin.FamilyImpl +{ + public class FamilyChangeSettingsEventHandler : IAsyncEventProcessor + { + private readonly IFamilyService _familyService; + private readonly IGameLanguageService _gameLanguage; + + public FamilyChangeSettingsEventHandler(IGameLanguageService gameLanguage, IFamilyService familyService) + { + _gameLanguage = gameLanguage; + _familyService = familyService; + } + + public async Task HandleAsync(FamilyChangeSettingsEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + + if (!session.PlayerEntity.IsInFamily()) + { + session.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.FAMILY_INFO_NO_FAMILY, session.UserLanguage)); + return; + } + + if (!session.PlayerEntity.IsHeadOfFamily() && session.PlayerEntity.GetFamilyAuthority() != FamilyAuthority.Deputy) + { + session.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.FAMILY_INFO_NO_FAMILY_RIGHT, session.UserLanguage)); + return; + } + + IFamily family = session.PlayerEntity.Family; + FamilyActionType actionType = e.FamilyActionType; + byte value = e.Value; + + bool familyOption = false; + bool option = value == 1; + + switch (e.Authority) + { + case FamilyAuthority.Keeper: + switch (actionType) + { + case FamilyActionType.FamilyWarehouse: + if (!Enum.TryParse(value.ToString(), out FamilyWarehouseAuthorityType authorityType)) + { + return; + } + + if (family.AssistantWarehouseAuthorityType == authorityType) + { + return; + } + + option = true; + + break; + default: + familyOption = actionType switch + { + FamilyActionType.SendInvite => family.AssistantCanInvite, + FamilyActionType.Notice => family.AssistantCanNotice, + FamilyActionType.FamilyShout => family.AssistantCanShout, + FamilyActionType.FamilyWarehouseHistory => family.AssistantCanGetHistory, + _ => option + }; + break; + } + + break; + + case FamilyAuthority.Member: + switch (actionType) + { + case FamilyActionType.Notice: // Member Warehouse Authority + if (!Enum.TryParse(value.ToString(), out FamilyWarehouseAuthorityType authorityType)) + { + return; + } + + if (family.MemberWarehouseAuthorityType == authorityType) + { + return; + } + + option = true; + + break; + default: + if (actionType != FamilyActionType.SendInvite) // Member History + { + return; + } + + familyOption = family.MemberCanGetHistory; + break; + } + + break; + default: + return; + } + + if (familyOption == option) + { + return; + } + + BasicRpcResponse response = await _familyService.UpdateFamilySettingsAsync(new FamilySettingsRequest + { + FamilyId = family.Id, + Authority = e.Authority, + FamilyActionType = e.FamilyActionType, + Value = value + }); + + if (response.ResponseType != RpcResponseType.SUCCESS) + { + return; + } + + if (e.Authority == FamilyAuthority.Member) + { + switch (actionType) + { + case FamilyActionType.SendInvite: + await session.FamilyAddLogAsync(FamilyLogType.RightChanged, session.PlayerEntity.Name, ((byte)e.Authority).ToString(), + ((byte)FamilyActionType.FamilyWarehouseHistory + 1).ToString(), value.ToString()); + break; + case FamilyActionType.Notice: + await session.FamilyAddLogAsync(FamilyLogType.RightChanged, session.PlayerEntity.Name, ((byte)e.Authority).ToString(), ((byte)FamilyActionType.FamilyWarehouse + 1).ToString(), + value.ToString()); + break; + } + + return; + } + + await session.FamilyAddLogAsync(FamilyLogType.RightChanged, session.PlayerEntity.Name, ((byte)e.Authority).ToString(), ((byte)e.FamilyActionType + 1).ToString(), value.ToString()); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/FamilyChangeSexEventHandler.cs b/srcs/_plugins/Plugin.FamilyImpl/FamilyChangeSexEventHandler.cs new file mode 100644 index 0000000..6f577c5 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/FamilyChangeSexEventHandler.cs @@ -0,0 +1,65 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using PhoenixLib.ServiceBus; +using Plugin.FamilyImpl.Messages; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Families.Event; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums.Character; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.FamilyImpl +{ + public class FamilyChangeSexEventHandler : IAsyncEventProcessor + { + private readonly IGameLanguageService _gameLanguage; + private readonly IMessagePublisher _messagePublisher; + private readonly ISessionManager _sessionManager; + + public FamilyChangeSexEventHandler(IGameLanguageService gameLanguage, IMessagePublisher messagePublisher, ISessionManager sessionManager) + { + _gameLanguage = gameLanguage; + _messagePublisher = messagePublisher; + _sessionManager = sessionManager; + } + + public async Task HandleAsync(FamilyChangeSexEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + + if (!Enum.TryParse(e.Gender.ToString(), out GenderType genderType)) + { + return; + } + + if (!session.PlayerEntity.IsInFamily()) + { + session.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.FAMILY_INFO_NO_FAMILY, session.UserLanguage)); + return; + } + + if (!session.PlayerEntity.IsHeadOfFamily()) + { + session.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.FAMILY_INFO_NO_FAMILY_RIGHT, session.UserLanguage)); + return; + } + + if (session.PlayerEntity.Family.HeadGender == genderType) + { + return; + } + + _sessionManager.Broadcast(x => { return session.GenerateMsgPacket(_gameLanguage.GetLanguage(GameDialogKey.FAMILY_SHOUTMESSAGE_HEAD_CHANGE_SEX, x.UserLanguage), MsgMessageType.Middle); }); + + await _messagePublisher.PublishAsync(new FamilyHeadSexMessage + { + FamilyId = session.PlayerEntity.Family.Id, + NewGenderType = genderType + }); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/FamilyChangeTitleEventHandler.cs b/srcs/_plugins/Plugin.FamilyImpl/FamilyChangeTitleEventHandler.cs new file mode 100644 index 0000000..03b3fc8 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/FamilyChangeTitleEventHandler.cs @@ -0,0 +1,56 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Communication.Families; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Families; +using WingsEmu.Game.Families.Event; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums.Families; + +namespace Plugin.FamilyImpl +{ + public class FamilyChangeTitleEventHandler : IAsyncEventProcessor + { + private readonly IFamilyService _familyService; + private readonly IGameLanguageService _gameLanguageService; + + public FamilyChangeTitleEventHandler(IFamilyService familyService, IGameLanguageService gameLanguageService) + { + _familyService = familyService; + _gameLanguageService = gameLanguageService; + } + + public async Task HandleAsync(FamilyChangeTitleEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + IFamily sessionFamily = session.PlayerEntity.Family; + + if (sessionFamily == null) + { + session.SendInfo(_gameLanguageService.GetLanguage(GameDialogKey.FAMILY_INFO_NO_FAMILY, session.UserLanguage)); + return; + } + + if (session.PlayerEntity.GetFamilyAuthority() != FamilyAuthority.Head) + { + session.SendInfo(_gameLanguageService.GetLanguage(GameDialogKey.FAMILY_INFO_NOT_FAMILY_HEAD, session.UserLanguage)); + return; + } + + FamilyMembership target = sessionFamily.Members.FirstOrDefault(x => x.Character?.Name == e.MemberNickname); + if (target == null) + { + return; + } + + await _familyService.ChangeTitleByIdAsync(new FamilyChangeTitleRequest + { + CharacterId = target.CharacterId, + RequestedFamilyTitle = e.Title + }); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/FamilyCharacterDisconnectEventHandler.cs b/srcs/_plugins/Plugin.FamilyImpl/FamilyCharacterDisconnectEventHandler.cs new file mode 100644 index 0000000..815bc97 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/FamilyCharacterDisconnectEventHandler.cs @@ -0,0 +1,37 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.Families; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Families; +using WingsEmu.Game.Managers; + +namespace Plugin.FamilyImpl +{ + public class FamilyCharacterDisconnectEventHandler : IAsyncEventProcessor + { + private readonly IFamilyManager _familyManager; + private readonly IGameLanguageService _gameLanguage; + private readonly ISessionManager _sessionManager; + + public FamilyCharacterDisconnectEventHandler(IFamilyManager familyManager, ISessionManager sessionManager, IGameLanguageService gameLanguage) + { + _familyManager = familyManager; + _sessionManager = sessionManager; + _gameLanguage = gameLanguage; + } + + public async Task HandleAsync(CharacterDisconnectedEvent e, CancellationToken cancellation) + { + IFamily family = e.Sender.PlayerEntity.Family; + if (family == null) + { + return; + } + + _familyManager.MemberDisconnectionUpdate(e.Sender.PlayerEntity.Id, e.DisconnectionTime); + family.SendOnlineStatusToMembers(_sessionManager, e.Sender.PlayerEntity.Id, false, _gameLanguage); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/FamilyChatMessageEventHandler.cs b/srcs/_plugins/Plugin.FamilyImpl/FamilyChatMessageEventHandler.cs new file mode 100644 index 0000000..67c6d72 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/FamilyChatMessageEventHandler.cs @@ -0,0 +1,67 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using PhoenixLib.ServiceBus; +using Plugin.FamilyImpl.Consumers; +using Plugin.FamilyImpl.Messages; +using WingsEmu.Game.Chat; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Families; +using WingsEmu.Game.InterChannel; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking.Broadcasting; +using WingsEmu.Packets.Enums.Chat; +using ChatType = WingsEmu.Game._playerActionLogs.ChatType; + +namespace Plugin.FamilyImpl +{ + public class FamilyChatMessageEventHandler : IAsyncEventProcessor + { + private readonly FamilyChatMessageConsumer _familyChatMessageConsumer; + private readonly IMessagePublisher _messagePublisher; + private readonly IServerManager _serverManager; + + public FamilyChatMessageEventHandler(IMessagePublisher messagePublisher, IServerManager serverManager, FamilyChatMessageConsumer familyChatMessageConsumer) + { + _messagePublisher = messagePublisher; + _serverManager = serverManager; + _familyChatMessageConsumer = familyChatMessageConsumer; + } + + public async Task HandleAsync(FamilyChatMessageEvent e, CancellationToken cancellation) + { + IFamily family = e.Sender.PlayerEntity.Family; + if (family == null) + { + return; + } + + if (family.Members.Count <= 1) + { + e.Sender.SendChatMessageNoId($"[{e.Sender.PlayerEntity.Name}]:{e.Message}", ChatMessageColorType.Blue); + e.Sender.SendSpeak(e.Message, SpeakType.Family); + return; + } + + e.Sender.BroadcastSpeak(e.Message, SpeakType.Family, new FamilyBroadcast(family.Id)); + + var messageToPublish = new FamilyChatMessage + { + SenderFamilyId = family.Id, + SenderChannelId = _serverManager.ChannelId, + SenderNickname = e.Sender.PlayerEntity.Name, + Message = e.Message + }; + + await e.Sender.EmitEventAsync(new ChatGenericEvent + { + Message = e.Message, + ChatType = ChatType.FamilyChat + }); + + await _messagePublisher.PublishAsync(messageToPublish, cancellation); + + await _familyChatMessageConsumer.HandleAsync(messageToPublish, cancellation); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/FamilyCreateEventHandler.cs b/srcs/_plugins/Plugin.FamilyImpl/FamilyCreateEventHandler.cs new file mode 100644 index 0000000..0ecc15b --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/FamilyCreateEventHandler.cs @@ -0,0 +1,217 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Communication.Families; +using WingsAPI.Data.Families; +using WingsAPI.Game.Extensions.Families; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Families; +using WingsEmu.Game.Families.Configuration; +using WingsEmu.Game.Families.Event; +using WingsEmu.Game.Groups; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Families; + +namespace Plugin.FamilyImpl +{ + public class FamilyCreateEventHandler : IAsyncEventProcessor + { + private readonly IForbiddenNamesManager _bannedNamesConfiguration; + private readonly FamilyConfiguration _familyConfiguration; + private readonly IFamilyManager _familyManager; + private readonly IFamilyService _familyService; + private readonly IGameLanguageService _gameLanguage; + private readonly IRandomGenerator _randomGenerator; + private readonly ISessionManager _sessionManager; + + public FamilyCreateEventHandler(IFamilyService familyService, IGameLanguageService gameLanguage, FamilyConfiguration familyConfiguration, + ISessionManager sessionManager, IFamilyManager familyManager, IRandomGenerator randomGenerator, IForbiddenNamesManager bannedNamesConfiguration) + { + _familyService = familyService; + _gameLanguage = gameLanguage; + _familyConfiguration = familyConfiguration; + _sessionManager = sessionManager; + _familyManager = familyManager; + _randomGenerator = randomGenerator; + _bannedNamesConfiguration = bannedNamesConfiguration; + } + + public async Task HandleAsync(FamilyCreateEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + if (!session.CurrentMapInstance.HasMapFlag(MapFlags.IS_BASE_MAP)) + { + return; + } + + if (session.CantPerformActionOnAct4()) + { + return; + } + + if (session.PlayerEntity.IsInRaidParty) + { + return; + } + + if (session.PlayerEntity.IsInFamily()) + { + session.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.FAMILY_INFO_ALREADY_IN_FAMILY, session.UserLanguage)); + return; + } + + if (!session.HasEnoughGold(_familyConfiguration.CreationPrice)) + { + session.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.INTERACTION_MESSAGE_NOT_ENOUGH_GOLD, session.UserLanguage)); + return; + } + + int nameLength = e.Name.Length; + if (nameLength < _familyConfiguration.MinimumNameLength || nameLength > _familyConfiguration.MaximumNameLength) + { + return; + } + + PlayerGroup playerGroup = session.PlayerEntity.GetGroup(); + + if (session.PlayerEntity.IsInGroup()) + { + if (playerGroup.Members.Any(x => x.IsInFamily())) + { + session.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.FAMILY_INFO_CREATION_SOMEONE_ALREADY_IN_FAMILY, session.UserLanguage)); + return; + } + + if (playerGroup.Members.Count < _familyConfiguration.CreationGroupMembersRequired) + { + session.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.FAMILY_INFO_GROUP_NOT_FULL, session.UserLanguage)); + return; + } + } + else if (_familyConfiguration.CreationIsGroupRequired) + { + session.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.FAMILY_INFO_CREATION_GROUP_REQUIRED, session.UserLanguage)); + return; + } + + var regex = new Regex(@"^[a-zA-Z0-9_\-\*]*$"); + if (regex.Matches(e.Name).Count != 1) + { + return; + } + + string lowerName = e.Name.ToLowerInvariant(); + + if (_bannedNamesConfiguration.IsBanned(lowerName, out string bannedName)) + { + session.SendInfo(_gameLanguage.GetLanguageFormat(GameDialogKey.FAMILY_INFO_CREATION_BANNED_NAME, session.UserLanguage, bannedName)); + return; + } + + var membersList = new List(); + DateTime now = DateTime.UtcNow; + + if (session.PlayerEntity.IsInGroup()) + { + foreach (IPlayerEntity member in playerGroup.Members) + { + if (member.Id == session.PlayerEntity.Id) + { + continue; + } + + membersList.Add(new FamilyMembershipDto + { + CharacterId = member.Id, + Authority = FamilyAuthority.Deputy, + DailyMessage = null, + Experience = 0, + Title = FamilyTitle.Nothing, + JoinDate = now + }); + } + } + + membersList.Add(new FamilyMembershipDto + { + CharacterId = session.PlayerEntity.Id, + Authority = FamilyAuthority.Head, + DailyMessage = null, + Experience = 0, + Title = FamilyTitle.Nothing, + JoinDate = now + }); + + FamilyCreateResponse response = await _familyService.CreateFamilyAsync(new FamilyCreateRequest + { + Name = e.Name, + Level = 1, + MembershipCapacity = _familyConfiguration.DefaultMembershipCapacity, + Faction = (byte)_randomGenerator.RandomNumber(1, 3), + Members = membersList + }); + + if (response.Status == FamilyCreateResponseType.NAME_ALREADY_TAKEN) + { + session.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.FAMILY_INFO_NAME_ALREADY_USED, session.UserLanguage)); + return; + } + + FamilyDTO familyDto = response.Family; + _familyManager.AddFamily(response.Family, membersList); + IFamily family = null; + + foreach (FamilyMembershipDto member in membersList) + { + member.FamilyId = familyDto.Id; + _familyManager.AddOrReplaceMember(member); + + IClientSession localSession = _sessionManager.GetSessionByCharacterId(member.CharacterId); + + if (localSession == null) + { + continue; + } + + family ??= localSession.PlayerEntity.Family; + + if (localSession.PlayerEntity.Faction != (FactionType)family.Faction) + { + await localSession.EmitEventAsync(new ChangeFactionEvent + { + NewFaction = (FactionType)familyDto.Faction + }); + } + + localSession.BroadcastGidx(family, _gameLanguage); + } + + if (family == null) + { + return; + } + + FamilyPacketExtensions.SendFamilyMembersInfoToMembers(family, _sessionManager, _familyConfiguration); + family.SendFmiPacket(_sessionManager); + + session.PlayerEntity.RemoveGold(_familyConfiguration.CreationPrice); + session.EmitEvent(new FamilyCreatedEvent()); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/FamilyDisbandEventHandler.cs b/srcs/_plugins/Plugin.FamilyImpl/FamilyDisbandEventHandler.cs new file mode 100644 index 0000000..d211851 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/FamilyDisbandEventHandler.cs @@ -0,0 +1,38 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Communication.Families; +using WingsEmu.Game.Families; +using WingsEmu.Game.Families.Event; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums.Families; + +namespace Plugin.FamilyImpl +{ + public class FamilyDisbandEventHandler : IAsyncEventProcessor + { + private readonly IFamilyService _familyService; + + public FamilyDisbandEventHandler(IFamilyService familyService) => _familyService = familyService; + + public async Task HandleAsync(FamilyDisbandEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + IFamily family = session.PlayerEntity.Family; + if (family == null || session.PlayerEntity.GetFamilyAuthority() != FamilyAuthority.Head) + { + return; + } + + await _familyService.DisbandFamilyAsync(new FamilyDisbandRequest + { + FamilyId = family.Id + }); + + await session.EmitEventAsync(new FamilyDisbandedEvent + { + FamilyId = family.Id + }); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/FamilyExperienceManager.cs b/srcs/_plugins/Plugin.FamilyImpl/FamilyExperienceManager.cs new file mode 100644 index 0000000..1669be8 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/FamilyExperienceManager.cs @@ -0,0 +1,34 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; +using WingsEmu.Game.Families; + +namespace Plugin.FamilyImpl +{ + public class FamilyExperienceManager : IFamilyExperienceManager + { + private readonly ConcurrentQueue _bufferFamilyExperience; + + public FamilyExperienceManager() => _bufferFamilyExperience = new ConcurrentQueue(); + + public void SaveFamilyExperienceToBuffer(ExperienceGainedSubMessage xpGained) + { + _bufferFamilyExperience.Enqueue(xpGained); + } + + public IReadOnlyCollection GetFamilyExperiencesInBuffer() + { + if (_bufferFamilyExperience.IsEmpty) + { + return null; + } + + var list = new List(); + while (_bufferFamilyExperience.TryDequeue(out ExperienceGainedSubMessage exp)) + { + list.Add(exp); + } + + return list; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/FamilyInviteResponseEventHandler.cs b/srcs/_plugins/Plugin.FamilyImpl/FamilyInviteResponseEventHandler.cs new file mode 100644 index 0000000..e42b5e7 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/FamilyInviteResponseEventHandler.cs @@ -0,0 +1,92 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Communication.Families; +using WingsAPI.Packets.Enums.Families; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Families; +using WingsEmu.Game.Families.Event; +using WingsEmu.Game.InterChannel; +using WingsEmu.Game.Managers; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.FamilyImpl +{ + public class FamilyInviteResponseEventHandler : IAsyncEventProcessor + { + private readonly IFamilyInvitationService _familyInvitation; + private readonly IFamilyManager _familyManager; + private readonly IGameLanguageService _languageService; + private readonly ISessionManager _sessionManager; + + public FamilyInviteResponseEventHandler(ISessionManager sessionManager, IGameLanguageService languageService, IFamilyInvitationService familyInvitation, IFamilyManager familyManager) + { + _sessionManager = sessionManager; + _languageService = languageService; + _familyInvitation = familyInvitation; + _familyManager = familyManager; + } + + public async Task HandleAsync(FamilyInviteResponseEvent e, CancellationToken cancellation) + { + if (e.Sender.PlayerEntity.IsInFamily()) + { + return; + } + + FamilyInvitationGetResponse getInvitation = await _familyInvitation.GetFamilyInvitationAsync(new FamilyInvitationRequest + { + SenderId = e.SenderCharacterId, + TargetId = e.Sender.PlayerEntity.Id + }); + + if (getInvitation.Invitation == null) + { + return; + } + + switch (e.FamilyJoinType) + { + case FamilyJoinType.Rejected: + await _familyInvitation.RemoveFamilyInvitationAsync(new FamilyInvitationRemoveRequest + { + SenderId = e.SenderCharacterId + }); + + await e.Sender.EmitEventAsync(new InterChannelSendChatMsgByCharIdEvent(e.SenderCharacterId, GameDialogKey.FAMILY_INFO_INVITATION_REFUSED, ChatMessageColorType.Red)); + return; + case FamilyJoinType.PreAccepted: + e.Sender.SendDialog($"gjoin {(byte)FamilyJoinType.Accepted} {e.SenderCharacterId}", + $"gjoin {(byte)FamilyJoinType.Rejected} {e.SenderCharacterId}", _languageService.GetLanguage(GameDialogKey.FAMILY_DIALOG_ASK_JOIN_CONFIRMATION, e.Sender.UserLanguage)); + return; + case FamilyJoinType.Accepted: + if (!await _familyManager.CanJoinNewFamilyAsync(e.Sender.PlayerEntity.Id)) + { + await _familyInvitation.RemoveFamilyInvitationAsync(new FamilyInvitationRemoveRequest + { + SenderId = e.SenderCharacterId + }); + + e.Sender.SendMsg(e.Sender.GetLanguage(GameDialogKey.FAMILY_SHOUTMESSAGE_CHANGE_FAMILY_ON_COOLDOWN), MsgMessageType.Middle); + return; + } + + await _familyInvitation.RemoveFamilyInvitationAsync(new FamilyInvitationRemoveRequest + { + SenderId = e.SenderCharacterId + }); + await e.Sender.EmitEventAsync(new FamilyAddMemberEvent(getInvitation.Invitation.SenderFamilyId, getInvitation.Invitation.SenderId)); + await e.Sender.EmitEventAsync(new FamilyJoinedEvent + { + FamilyId = getInvitation.Invitation.SenderFamilyId, + InviterId = getInvitation.Invitation.SenderId + }); + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/FamilyLeaveEventHandler.cs b/srcs/_plugins/Plugin.FamilyImpl/FamilyLeaveEventHandler.cs new file mode 100644 index 0000000..744fc5d --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/FamilyLeaveEventHandler.cs @@ -0,0 +1,64 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Communication.Families; +using WingsAPI.Game.Extensions.Families; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Families.Event; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums.Chat; +using WingsEmu.Packets.Enums.Families; + +namespace Plugin.FamilyImpl +{ + public class FamilyLeaveEventHandler : IAsyncEventProcessor + { + private readonly IFamilyService _familyService; + private readonly IGameLanguageService _gameLanguage; + + public FamilyLeaveEventHandler(IGameLanguageService gameLanguage, IFamilyService familyService) + { + _gameLanguage = gameLanguage; + _familyService = familyService; + } + + public async Task HandleAsync(FamilyLeaveEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + + if (!session.PlayerEntity.IsInFamily()) + { + session.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.FAMILY_INFO_NO_FAMILY, session.UserLanguage)); + return; + } + + if (session.PlayerEntity.IsHeadOfFamily()) + { + session.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.FAMILY_DIALOG_HEAD_CANNOT_LEAVE_FAMILY, session.UserLanguage)); + return; + } + + if (!session.CurrentMapInstance.HasMapFlag(MapFlags.IS_BASE_MAP)) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_MUST_BE_IN_CLASSIC_MAP, session.UserLanguage), MsgMessageType.Middle); + return; + } + + await session.FamilyAddLogAsync(FamilyLogType.MemberLeave, session.PlayerEntity.Name); + await session.EmitEventAsync(new FamilyLeftEvent + { + FamilyId = session.PlayerEntity.Family.Id + }); + + await _familyService.RemoveMemberToFamilyAsync(new FamilyRemoveMemberRequest + { + CharacterId = session.PlayerEntity.Id, + FamilyId = session.PlayerEntity.Family.Id + }); + + session.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.FAMILY_INFO_LEAVE, session.UserLanguage)); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/FamilyListMembersEventHandler.cs b/srcs/_plugins/Plugin.FamilyImpl/FamilyListMembersEventHandler.cs new file mode 100644 index 0000000..c74fa75 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/FamilyListMembersEventHandler.cs @@ -0,0 +1,56 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Families; +using WingsEmu.Game.Families.Event; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums.Character; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.FamilyImpl +{ + public class FamilyListMembersEventHandler : IAsyncEventProcessor + { + private readonly IGameLanguageService _gameLanguageService; + private readonly ISessionManager _sessionManager; + + public FamilyListMembersEventHandler(IGameLanguageService gameLanguageService, ISessionManager sessionManager) + { + _gameLanguageService = gameLanguageService; + _sessionManager = sessionManager; + } + + public async Task HandleAsync(FamilyListMembersEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + if (!session.PlayerEntity.IsInFamily()) + { + session.SendInfo(_gameLanguageService.GetLanguage(GameDialogKey.FAMILY_INFO_NOT_IN_FAMILY, e.Sender.UserLanguage)); + return; + } + + IFamily family = session.PlayerEntity.Family; + + session.SendChatMessage($"== Members of {family.Name} ==", ChatMessageColorType.Yellow); + foreach (FamilyMembership member in family.Members.OrderBy(s => s.Authority).ThenBy(s => s.Character.Name)) + { + string className = member.Character.Class switch + { + ClassType.Adventurer => _gameLanguageService.GetLanguage(GameDialogKey.CLASS_NAME_ADVENTURER, e.Sender.UserLanguage), + ClassType.Archer => _gameLanguageService.GetLanguage(GameDialogKey.CLASS_NAME_ARCHER, e.Sender.UserLanguage), + ClassType.Magician => _gameLanguageService.GetLanguage(GameDialogKey.CLASS_NAME_MAGE, e.Sender.UserLanguage), + ClassType.Swordman => _gameLanguageService.GetLanguage(GameDialogKey.CLASS_NAME_SWORDSMAN, e.Sender.UserLanguage), + ClassType.Wrestler => _gameLanguageService.GetLanguage(GameDialogKey.CLASS_NAME_MARTIAL_ARTIST, e.Sender.UserLanguage), + _ => "" + }; + string state = _sessionManager.IsOnline(member.CharacterId) ? $"ONLINE (Ch. {member.Character.ChannelId})" : "OFFLINE"; + session.SendChatMessage($"{member.Character.Name}({_gameLanguageService.GetLanguage(member.Authority.GetMemberLanguageKey(), session.UserLanguage)}) " + + $"- Lv. {member.Character.Level} - {className} - {state}", ChatMessageColorType.Yellow); + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/FamilyManager.cs b/srcs/_plugins/Plugin.FamilyImpl/FamilyManager.cs new file mode 100644 index 0000000..c5bf594 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/FamilyManager.cs @@ -0,0 +1,234 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Caching; +using PhoenixLib.DAL.Redis.Locks; +using PhoenixLib.Logging; +using Plugin.FamilyImpl.Logs; +using WingsAPI.Communication.Families; +using WingsAPI.Data.Families; +using WingsEmu.Game.Families; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums.Families; + +namespace Plugin.FamilyImpl +{ + public class FamilyManager : IFamilyManager + { + private readonly IExpirableLockService _expirableLockService; + private readonly IKeyValueCache _familyByName; + private readonly ILongKeyCachedRepository _familyCache; + private readonly IFamilyExperienceManager _familyExperienceManager; + private readonly IFamilyLogManager _familyLogManager; + private readonly IFamilyService _familyService; + + private readonly ReaderWriterLockSlim _lock = new(); + private readonly ILongKeyCachedRepository _membershipCache; + private readonly ISessionManager _sessionManager; + + public FamilyManager(ILongKeyCachedRepository familyCache, IFamilyService familyService, ISessionManager sessionManager, ILongKeyCachedRepository membershipCache, + IFamilyLogManager familyLogManager, IFamilyExperienceManager familyExperienceManager, IKeyValueCache familyByName, IExpirableLockService expirableLockService) + { + _familyCache = familyCache; + _familyService = familyService; + _sessionManager = sessionManager; + _membershipCache = membershipCache; + _familyLogManager = familyLogManager; + _familyExperienceManager = familyExperienceManager; + _familyByName = familyByName; + _expirableLockService = expirableLockService; + } + + public void AddFamily(FamilyDTO family, IReadOnlyCollection members) + { + IFamily oldFamily = _familyCache.Get(family.Id); + var newFamily = new Family(family, members, new List(), _sessionManager); + _familyCache.Set(family.Id, newFamily); + _familyByName.Set(family.Name, newFamily); + if (oldFamily == null) + { + return; + } + + newFamily.Members.AddRange(oldFamily.Members); + oldFamily.Members.Clear(); + } + + public void RemoveFamily(long familyId) + { + IFamily cachedFamily = _familyCache.Get(familyId); + _familyCache.Remove(familyId); + + foreach (FamilyMembership member in cachedFamily.Members) + { + _membershipCache.Remove(member.CharacterId); + IClientSession session = _sessionManager.GetSessionByCharacterId(member.CharacterId); + session?.PlayerEntity.SetFamilyMembership(null); + } + } + + public IFamily GetFamilyByFamilyName(string familyName) => _familyByName.Get(familyName); + + public void AddOrReplaceMember(FamilyMembershipDto membership, IFamily family) + { + if (family == null) + { + return; + } + + var newMember = new FamilyMembership(membership, _sessionManager); + family.Members.RemoveAll(x => x.CharacterId == membership.CharacterId); + _membershipCache.Set(newMember.CharacterId, newMember); + if (newMember.Authority == FamilyAuthority.Head) + { + family.Head = newMember; + } + + family.Members.Add(newMember); + IClientSession session = _sessionManager.GetSessionByCharacterId(membership.CharacterId); + session?.PlayerEntity.SetFamilyMembership(newMember); + } + + public void AddOrReplaceMember(FamilyMembershipDto membership) + { + AddOrReplaceMember(membership, GetFamilyByFamilyId(membership.FamilyId)); + } + + public void RemoveMember(long characterId, long familyId) + { + IFamily family = GetFamilyByFamilyId(familyId); + family?.Members.RemoveAll(x => x.CharacterId == characterId); + + _membershipCache.Remove(characterId); + IClientSession session = _sessionManager.GetSessionByCharacterId(characterId); + session?.PlayerEntity.SetFamilyMembership(null); + } + + public void MemberDisconnectionUpdate(long characterId, DateTime disconnectionTime) + { + FamilyMembership member = GetFamilyMembershipByCharacterId(characterId); + if (member == null) + { + return; + } + + member.LastOnlineDate = disconnectionTime; + AddOrReplaceMember(member); + } + + public FamilyMembership GetFamilyMembershipByCharacterId(long characterId) + { + FamilyMembership membership = _membershipCache.Get(characterId); + if (membership != null) + { + return membership; + } + + MembershipResponse response = _familyService.GetMembershipByCharacterIdAsync( + new MembershipRequest { CharacterId = characterId }).ConfigureAwait(false).GetAwaiter().GetResult(); + + return response.Membership == null ? null : _membershipCache.GetOrSet(response.Membership.CharacterId, () => new FamilyMembership(response.Membership, _sessionManager)); + } + + public void AddToFamilyLogs(IReadOnlyDictionary> logs) + { + try + { + foreach (KeyValuePair> familyLogs in logs) + { + IFamily family = _familyCache.Get(familyLogs.Key); + if (family == null) + { + continue; + } + + _lock.EnterWriteLock(); + try + { + foreach (FamilyLogDto log in familyLogs.Value) + { + family.Logs.Add(log); + } + + family.Logs.Sort((x, y) => DateTime.Compare(y.Timestamp, x.Timestamp)); // Newest -> Older + + if (family.Logs.Count <= 200) + { + continue; + } + + for (int i = 200; i < family.Logs.Count; i++) + { + family.Logs.RemoveAt(i); + } + } + finally + { + _lock.ExitWriteLock(); + } + } + } + catch (Exception e) + { + Log.Error("[FAMILY_MANAGER] AddToFamilyLogs", e); + } + } + + public void SendLogToFamilyServer(FamilyLogDto log) + { + _familyLogManager.SaveLogToBuffer(log); + } + + public IEnumerable AddToFamilyExperiences(Dictionary exps) + { + var familyIds = new HashSet(); + + foreach ((long characterId, long experience) in exps) + { + FamilyMembership membership = _membershipCache.Get(characterId); + if (membership == null) + { + continue; + } + + membership.Experience = experience; + familyIds.Add(membership.FamilyId); + } + + return familyIds; + } + + public void SendExperienceToFamilyServer(ExperienceGainedSubMessage experienceGainedSubMessage) + { + _familyExperienceManager.SaveFamilyExperienceToBuffer(experienceGainedSubMessage); + } + + public async Task CanJoinNewFamilyAsync(int playerEntityId) => + await _expirableLockService.TryAddTemporaryLockAsync($"game:locks:character:{playerEntityId}:join-family", DateTime.UtcNow.Date.AddDays(1)); + + public async Task RemovePlayerJoinCooldownAsync(int playerEntityId) => await _expirableLockService.TryRemoveTemporaryLock($"game:locks:character:{playerEntityId}:join-family"); + + public Family GetFamilyByFamilyId(long familyId) + { + Family family = _familyCache.Get(familyId); + if (family != null) + { + return family; + } + + FamilyIdResponse response = _familyService.GetFamilyByIdAsync(new FamilyIdRequest { FamilyId = familyId }).ConfigureAwait(false).GetAwaiter().GetResult(); + family = _familyCache.GetOrSet(familyId, () => new Family(response.Family, response.Members, response.Logs, _sessionManager)); + _familyByName.Set(family.Name, family); + foreach (FamilyMembership member in family.Members) + { + _membershipCache.Set(member.CharacterId, member); + } + + return family; + } + + public Family GetFamilyByFamilyIdCache(long familyId) => _familyCache.Get(familyId); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/FamilyNoticeMessageEventHandler.cs b/srcs/_plugins/Plugin.FamilyImpl/FamilyNoticeMessageEventHandler.cs new file mode 100644 index 0000000..3c103af --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/FamilyNoticeMessageEventHandler.cs @@ -0,0 +1,70 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using PhoenixLib.ServiceBus; +using Plugin.FamilyImpl.Messages; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Families; +using WingsEmu.Game.Families.Event; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums.Families; + +namespace Plugin.FamilyImpl +{ + public class FamilyNoticeMessageEventHandler : IAsyncEventProcessor + { + private readonly IGameLanguageService _gameLanguage; + private readonly IMessagePublisher _messagePublisher; + + public FamilyNoticeMessageEventHandler(IGameLanguageService gameLanguage, IMessagePublisher messagePublisher) + { + _gameLanguage = gameLanguage; + _messagePublisher = messagePublisher; + } + + public async Task HandleAsync(FamilyNoticeMessageEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + + if (!session.PlayerEntity.IsInFamily()) + { + session.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.FAMILY_INFO_NO_FAMILY, session.UserLanguage)); + return; + } + + IFamily family = session.PlayerEntity.Family; + FamilyAuthority authorityType = session.PlayerEntity.GetFamilyAuthority(); + + if (authorityType == FamilyAuthority.Keeper && !family.AssistantCanNotice || authorityType == FamilyAuthority.Member) + { + session.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.FAMILY_INFO_NO_FAMILY_RIGHT, session.UserLanguage)); + return; + } + + string message = e.Message; + + if (string.IsNullOrEmpty(message)) + { + message = string.Empty; + } + + if (message.Length > 50) + { + message = message.Substring(0, 50); + } + + await _messagePublisher.PublishAsync(new FamilyNoticeMessage + { + FamilyId = family.Id, + Message = e.CleanMessage ? null : message + }); + + await session.EmitEventAsync(new FamilyMessageSentEvent + { + Message = message, + MessageType = FamilyMessageType.Notice + }); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/FamilyPlugin.cs b/srcs/_plugins/Plugin.FamilyImpl/FamilyPlugin.cs new file mode 100644 index 0000000..7bdc85c --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/FamilyPlugin.cs @@ -0,0 +1,56 @@ +using System; +using PhoenixLib.Extensions; +using PhoenixLib.Logging; +using Plugin.FamilyImpl.Commands; +using WingsAPI.Plugins; +using WingsEmu.Commands.Interfaces; +using WingsEmu.Game._NpcDialog; + +namespace Plugin.FamilyImpl +{ + public class FamilyPlugin : IGamePlugin + { + private readonly ICommandContainer _commands; + private readonly IServiceProvider _container; + private readonly INpcDialogHandlerContainer _handlers; + + public FamilyPlugin(ICommandContainer commands, INpcDialogHandlerContainer handlers, IServiceProvider container) + { + _commands = commands; + _handlers = handlers; + _container = container; + } + + public string Name { get; } = nameof(FamilyPlugin); + + public void OnLoad() + { + _commands.AddModule(); + _commands.AddModule(); + _commands.AddModule(); + _commands.AddModule(); + _commands.AddModule(); + _commands.AddModule(); + + + foreach (Type handlerType in typeof(FamilyPlugin).Assembly.GetTypesImplementingInterface()) + { + try + { + object tmp = _container.GetService(handlerType); + if (tmp is not INpcDialogAsyncHandler real) + { + continue; + } + + Log.Debug($"[NPC_DIALOG][ADD_HANDLER] {handlerType}"); + _handlers.Register(real); + } + catch (Exception e) + { + Log.Error("[NPC_DIALOG][FAIL_ADD]", e); + } + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/FamilyPluginCore.cs b/srcs/_plugins/Plugin.FamilyImpl/FamilyPluginCore.cs new file mode 100644 index 0000000..a513bb9 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/FamilyPluginCore.cs @@ -0,0 +1,34 @@ +using Microsoft.Extensions.DependencyInjection; +using PhoenixLib.Events; +using PhoenixLib.ServiceBus.Extensions; +using Plugin.FamilyImpl.Logs; +using Plugin.FamilyImpl.RecurrentJob; +using WingsAPI.Communication.Families; +using WingsAPI.Plugins; +using WingsAPI.Plugins.Extensions; +using WingsEmu.Game._NpcDialog; +using WingsEmu.Game.Families; + +namespace Plugin.FamilyImpl +{ + public class FamilyPluginCore : IGameServerPlugin + { + public string Name => nameof(FamilyPluginCore); + + public void AddDependencies(IServiceCollection services, GameServerLoader gameServer) + { + services.AddFamilyModule(); + services.AddEventHandlersInAssembly(); + services.AddTypesImplementingInterfaceInAssembly(typeof(FamilyPluginCore).Assembly); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + services.AddHostedService(); + services.AddHostedService(); + + services.AddMessagePublisher(); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/FamilyReceiveInviteEventHandler.cs b/srcs/_plugins/Plugin.FamilyImpl/FamilyReceiveInviteEventHandler.cs new file mode 100644 index 0000000..32bb7d6 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/FamilyReceiveInviteEventHandler.cs @@ -0,0 +1,64 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Communication.Families; +using WingsAPI.Communication.Player; +using WingsAPI.Packets.Enums.Families; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Families.Event; +using WingsEmu.Game.InterChannel; +using WingsEmu.Game.Managers; + +namespace Plugin.FamilyImpl +{ + public class FamilyReceiveInviteEventHandler : IAsyncEventProcessor + { + private readonly IFamilyInvitationService _familyInvitation; + private readonly IGameLanguageService _languageService; + private readonly ISessionManager _sessionManager; + + public FamilyReceiveInviteEventHandler(IGameLanguageService languageService, IFamilyInvitationService familyInvitation, ISessionManager sessionManager) + { + _languageService = languageService; + _familyInvitation = familyInvitation; + _sessionManager = sessionManager; + } + + public async Task HandleAsync(FamilyReceiveInviteEvent e, CancellationToken cancellation) + { + if (e.Sender.PlayerEntity.IsInFamily()) + { + return; + } + + if (e.Sender.PlayerEntity.FamilyRequestBlocked) + { + await e.Sender.EmitEventAsync(new InterChannelSendInfoByCharIdEvent(e.SenderCharacterId, GameDialogKey.FAMILY_INFO_INVITATION_NOT_ALLOWED)); + return; + } + + if (e.Sender.PlayerEntity.IsBlocking(e.SenderCharacterId)) + { + await e.Sender.EmitEventAsync(new InterChannelSendInfoByCharIdEvent(e.SenderCharacterId, GameDialogKey.BLACKLIST_INFO_BLOCKED)); + return; + } + + await _familyInvitation.SaveFamilyInvitationAsync(new FamilyInvitationSaveRequest + { + Invitation = new FamilyInvitation + { + SenderId = e.SenderCharacterId, + SenderFamilyId = e.FamilyId, + TargetId = e.Sender.PlayerEntity.Id + } + }); + + ClusterCharacterInfo sender = await _sessionManager.GetOnlineCharacterById(e.SenderCharacterId); + string message = _languageService.GetLanguageFormat(GameDialogKey.FAMILY_DIALOG_ASK_JOIN_FAMILY, e.Sender.UserLanguage, e.FamilyName, sender?.Name ?? "?"); + + e.Sender.SendDialog($"gjoin {((byte)FamilyJoinType.PreAccepted).ToString()} {e.SenderCharacterId.ToString()}", + $"gjoin {((byte)FamilyJoinType.Rejected).ToString()} {e.SenderCharacterId.ToString()}", message); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/FamilyRemoveMemberEventHandler.cs b/srcs/_plugins/Plugin.FamilyImpl/FamilyRemoveMemberEventHandler.cs new file mode 100644 index 0000000..aea77a5 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/FamilyRemoveMemberEventHandler.cs @@ -0,0 +1,79 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Communication.Families; +using WingsAPI.Game.Extensions.Families; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Families; +using WingsEmu.Game.Families.Event; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums.Families; + +namespace Plugin.FamilyImpl +{ + public class FamilyRemoveMemberEventHandler : IAsyncEventProcessor + { + private readonly IFamilyService _familyService; + private readonly IGameLanguageService _gameLanguage; + + public FamilyRemoveMemberEventHandler(IGameLanguageService gameLanguage, IFamilyService familyService) + { + _gameLanguage = gameLanguage; + _familyService = familyService; + } + + public async Task HandleAsync(FamilyRemoveMemberEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + IFamily family = session.PlayerEntity.Family; + + if (family == null) + { + session.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.FAMILY_INFO_NO_FAMILY, session.UserLanguage)); + return; + } + + if (session.PlayerEntity.Name == e.Nickname) + { + session.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.FAMILY_DIALOG_CANNOT_KICK_YOURSELF, session.UserLanguage)); + return; + } + + FamilyAuthority sessionAuthority = session.PlayerEntity.GetFamilyAuthority(); + + if (!session.PlayerEntity.IsHeadOfFamily() && sessionAuthority != FamilyAuthority.Deputy) + { + session.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.FAMILY_INFO_NO_FAMILY_RIGHT, session.UserLanguage)); + return; + } + + FamilyMembership target = family.Members.FirstOrDefault(x => x.Character?.Name == e.Nickname); + if (target == null) + { + session.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.INFORMATION_MESSAGE_USER_NOT_FOUND, session.UserLanguage)); + return; + } + + if ((int)target.Authority <= (int)sessionAuthority) + { + session.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.FAMILY_INFO_NO_FAMILY_RIGHT, session.UserLanguage)); + return; + } + + await session.FamilyAddLogAsync(FamilyLogType.MemberLeave, e.Nickname); + + await _familyService.RemoveMemberToFamilyAsync(new FamilyRemoveMemberRequest + { + CharacterId = target.CharacterId, + FamilyId = target.FamilyId + }); + + await session.EmitEventAsync(new FamilyKickedMemberEvent + { + KickedMemberId = target.CharacterId + }); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/FamilySendInviteEventHandler.cs b/srcs/_plugins/Plugin.FamilyImpl/FamilySendInviteEventHandler.cs new file mode 100644 index 0000000..368ddc7 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/FamilySendInviteEventHandler.cs @@ -0,0 +1,98 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using PhoenixLib.ServiceBus; +using Plugin.FamilyImpl.Messages; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Families; +using WingsEmu.Game.Families.Event; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums.Families; + +namespace Plugin.FamilyImpl +{ + public class FamilySendInviteEventHandler : IAsyncEventProcessor + { + private readonly IGameLanguageService _languageService; + private readonly IMessagePublisher _messagePublisher; + private readonly ISessionManager _sessionManager; + + public FamilySendInviteEventHandler(ISessionManager sessionManager, IGameLanguageService languageService, IMessagePublisher messagePublisher) + { + _sessionManager = sessionManager; + _languageService = languageService; + _messagePublisher = messagePublisher; + } + + public async Task HandleAsync(FamilySendInviteEvent e, CancellationToken cancellation) + { + IFamily family = e.Sender.PlayerEntity.Family; + + if (family == null) + { + e.Sender.SendInfo(_languageService.GetLanguage(GameDialogKey.FAMILY_INFO_NOT_IN_FAMILY, e.Sender.UserLanguage)); + return; + } + + if (!e.Sender.CurrentMapInstance.HasMapFlag(MapFlags.IS_BASE_MAP)) + { + e.Sender.SendInfo(_languageService.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_MUST_BE_IN_CLASSIC_MAP, e.Sender.UserLanguage)); + return; + } + + if (e.Sender.CantPerformActionOnAct4()) + { + return; + } + + FamilyAuthority sessionAuthority = e.Sender.PlayerEntity.GetFamilyAuthority(); + if (sessionAuthority == FamilyAuthority.Member) + { + e.Sender.SendInfo(_languageService.GetLanguage(GameDialogKey.FAMILY_INFO_NO_FAMILY_RIGHT, e.Sender.UserLanguage)); + return; + } + + if (sessionAuthority == FamilyAuthority.Keeper && !family.AssistantCanInvite) + { + e.Sender.SendInfo(_languageService.GetLanguage(GameDialogKey.FAMILY_INFO_NO_FAMILY_RIGHT, e.Sender.UserLanguage)); + return; + } + + if (family.Members.Count >= family.GetMaximumMembershipCapacity()) + { + e.Sender.SendInfo(_languageService.GetLanguage(GameDialogKey.FAMILY_INFO_FULL, e.Sender.UserLanguage)); + return; + } + + IClientSession localReceiverSession = _sessionManager.GetSessionByCharacterName(e.ReceiverNickname); + if (localReceiverSession == null) + { + if (!_sessionManager.IsOnline(e.ReceiverNickname)) + { + e.Sender.SendInfo(_languageService.GetLanguage(GameDialogKey.INFORMATION_INFO_PLAYER_OFFLINE, e.Sender.UserLanguage)); + return; + } + + await _messagePublisher.PublishAsync(new FamilyInviteMessage + { + FamilyId = family.Id, + FamilyName = family.Name, + ReceiverNickname = e.ReceiverNickname, + SenderCharacterId = e.Sender.PlayerEntity.Id + }, cancellation); + return; + } + + if (localReceiverSession.PlayerEntity.RainbowBattleComponent.IsInRainbowBattle) + { + return; + } + + await e.Sender.EmitEventAsync(new FamilyInvitedEvent { TargetId = localReceiverSession.PlayerEntity.Id }); + await localReceiverSession.EmitEventAsync(new FamilyReceiveInviteEvent(family.Name, e.Sender.PlayerEntity.Id, family.Id)); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/FamilyShoutEventHandler.cs b/srcs/_plugins/Plugin.FamilyImpl/FamilyShoutEventHandler.cs new file mode 100644 index 0000000..c457ec3 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/FamilyShoutEventHandler.cs @@ -0,0 +1,80 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using PhoenixLib.ServiceBus; +using Plugin.FamilyImpl.Messages; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Families; +using WingsEmu.Game.Families.Event; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Networking.Broadcasting; +using WingsEmu.Packets.Enums.Chat; +using WingsEmu.Packets.Enums.Families; + +namespace Plugin.FamilyImpl +{ + public class FamilyShoutEventHandler : IAsyncEventProcessor + { + private readonly IGameLanguageService _gameLanguage; + private readonly IMessagePublisher _messagePublisher; + private readonly ISessionManager _sessionManager; + + public FamilyShoutEventHandler(IGameLanguageService gameLanguage, IMessagePublisher messagePublisher, ISessionManager sessionManager) + { + _gameLanguage = gameLanguage; + _messagePublisher = messagePublisher; + _sessionManager = sessionManager; + } + + public async Task HandleAsync(FamilyShoutEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + string message = e.Message; + + if (string.IsNullOrEmpty(message)) + { + return; + } + + if (message.Length > 50) + { + message = message.Substring(0, 50); + } + + if (!session.PlayerEntity.IsInFamily()) + { + session.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.FAMILY_INFO_NO_FAMILY, session.UserLanguage)); + return; + } + + FamilyAuthority senderAuthority = session.PlayerEntity.GetFamilyAuthority(); + if (senderAuthority == FamilyAuthority.Member || senderAuthority == FamilyAuthority.Keeper && !session.PlayerEntity.Family.AssistantCanShout) + { + session.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.FAMILY_INFO_NO_FAMILY_RIGHT, session.UserLanguage)); + return; + } + + await _sessionManager.BroadcastAsync(async s => + { + string msg = _gameLanguage.GetLanguageFormat(GameDialogKey.FAMILY_SHOUTMESSAGE_SHOUT, s.UserLanguage, session.PlayerEntity.Name, message); + return session.GenerateMsgPacket(msg, MsgMessageType.Middle); + }, new FamilyBroadcast(session.PlayerEntity.Family.Id)); + + await _messagePublisher.PublishAsync(new FamilyShoutMessage + { + Message = message, + SenderName = session.PlayerEntity.Name, + FamilyId = session.PlayerEntity.Family.Id, + GameDialogKey = GameDialogKey.FAMILY_SHOUTMESSAGE_SHOUT + }); + + await session.EmitEventAsync(new FamilyMessageSentEvent + { + Message = message, + MessageType = FamilyMessageType.Shout + }); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/FamilyTodayEventHandler.cs b/srcs/_plugins/Plugin.FamilyImpl/FamilyTodayEventHandler.cs new file mode 100644 index 0000000..7bdc1ff --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/FamilyTodayEventHandler.cs @@ -0,0 +1,97 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using PhoenixLib.ServiceBus; +using Plugin.FamilyImpl.Achievements; +using Plugin.FamilyImpl.Messages; +using WingsAPI.Communication.Families; +using WingsAPI.Game.Extensions.Families; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Families; +using WingsEmu.Game.Families.Event; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums.Families; + +namespace Plugin.FamilyImpl +{ + public class FamilyTodayEventHandler : IAsyncEventProcessor + { + private readonly IFamilyAchievementManager _familyAchievement; + private readonly IFamilyService _familyService; + private readonly IGameLanguageService _gameLanguage; + private readonly IMessagePublisher _messagePublisher; + + public FamilyTodayEventHandler(IGameLanguageService gameLanguage, IMessagePublisher messagePublisher, IFamilyService familyService, + IFamilyAchievementManager familyAchievement) + { + _gameLanguage = gameLanguage; + _messagePublisher = messagePublisher; + _familyService = familyService; + _familyAchievement = familyAchievement; + } + + public async Task HandleAsync(FamilyTodayEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + string message = e.Message; + + if (string.IsNullOrEmpty(message)) + { + return; + } + + if (message.Length > 50) + { + message = message.Substring(0, 50); + } + + if (!session.PlayerEntity.IsInFamily()) + { + session.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.FAMILY_INFO_NO_FAMILY, session.UserLanguage)); + return; + } + + if (session.PlayerEntity.Level < 30) + { + session.SendInfo(session.GetLanguage(GameDialogKey.FAMILY_INFO_TODAY_LOW_LEVEL)); + return; + } + + FamilyMembership membership = session.PlayerEntity.GetMembershipById(session.PlayerEntity.Id); + if (membership == null) + { + return; + } + + MembershipTodayResponse response = await _familyService.CanPerformTodayMessageAsync(new MembershipTodayRequest + { + CharacterId = session.PlayerEntity.Id, + CharacterName = session.PlayerEntity.Name + }); + + if (!response.CanPerformAction) + { + session.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.FAMILY_INFO_USED_DAILY_MESSAGE, session.UserLanguage)); + return; + } + + + await session.FamilyAddLogAsync(FamilyLogType.DailyMessage, session.PlayerEntity.Name, message); + await _messagePublisher.PublishAsync(new FamilyMemberTodayMessage + { + CharacterId = session.PlayerEntity.Id, + Message = message + }); + + // achievements part + _familyAchievement.IncrementFamilyAchievement(membership.FamilyId, (int)FamilyAchievementsVnum.ENTER_20_QUOTES_OF_THE_DAY); + + await session.EmitEventAsync(new FamilyMessageSentEvent + { + Message = message, + MessageType = FamilyMessageType.Quote + }); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/FamilyWarehouseAddItemEventHandler.cs b/srcs/_plugins/Plugin.FamilyImpl/FamilyWarehouseAddItemEventHandler.cs new file mode 100644 index 0000000..6a7c99a --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/FamilyWarehouseAddItemEventHandler.cs @@ -0,0 +1,99 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Data.Families; +using WingsAPI.Game.Extensions.Families; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsAPI.Packets.Enums.Families; +using WingsEmu.DTOs.Items; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Families.Event; +using WingsEmu.Game.Features; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Items; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace Plugin.FamilyImpl +{ + public class FamilyWarehouseAddItemEventHandler : IAsyncEventProcessor + { + private readonly IFamilyWarehouseManager _familyWarehouseManager; + private readonly IGameFeatureToggleManager _gameFeatureToggleManager; + private readonly IGameItemInstanceFactory _gameItemInstanceFactory; + private readonly IGameLanguageService _languageService; + + public FamilyWarehouseAddItemEventHandler(IGameLanguageService languageService, IGameItemInstanceFactory gameItemInstanceFactory, IFamilyWarehouseManager familyWarehouseManager, + IGameFeatureToggleManager gameFeatureToggleManager) + { + _languageService = languageService; + _gameItemInstanceFactory = gameItemInstanceFactory; + _familyWarehouseManager = familyWarehouseManager; + _gameFeatureToggleManager = gameFeatureToggleManager; + } + + public async Task HandleAsync(FamilyWarehouseAddItemEvent e, CancellationToken cancellation) + { + bool disabled = await _gameFeatureToggleManager.IsDisabled(GameFeature.FamilyWarehouse); + if (disabled) + { + e.Sender.SendInfo(_languageService.GetLanguage(GameDialogKey.GAME_FEATURE_DISABLED, e.Sender.UserLanguage)); + return; + } + + IClientSession session = e.Sender; + IPlayerEntity character = session.PlayerEntity; + InventoryItem inventoryItem = e.Item; + + if (!character.IsInFamily() || !character.IsFamilyWarehouseOpen || session.CantPerformActionOnAct4() || character.HasShopOpened || character.IsInExchange() + || inventoryItem.ItemInstance.Amount < e.Amount) + { + return; + } + + if (!session.CheckPutWithdrawPermission(FamilyWarehouseAuthorityType.Put)) + { + e.Sender.SendInfo(_languageService.GetLanguage(GameDialogKey.FAMILY_INFO_WAREHOUSE_NOT_ENOUGH_PERMISSION, e.Sender.UserLanguage)); + return; + } + + if (!inventoryItem.ItemInstance.GameItem.IsSoldable || !inventoryItem.ItemInstance.GameItem.IsTradable || inventoryItem.ItemInstance.IsBound + || inventoryItem.ItemInstance.GameItem.ItemType is ItemType.Specialist or ItemType.Quest1 || inventoryItem.ItemInstance.GameItem.ItemType == ItemType.Quest2) + { + e.Sender.SendInfo(_languageService.GetLanguage(GameDialogKey.FAMILY_INFO_WAREHOUSE_INVALID_ITEM, e.Sender.UserLanguage)); + return; + } + + ItemInstanceDTO mapped = _gameItemInstanceFactory.CreateDto(inventoryItem.ItemInstance); + mapped.Amount = e.Amount; + + await e.Sender.RemoveItemFromInventory(amount: e.Amount, item: inventoryItem); + + ManagerResponseType? response = await _familyWarehouseManager.AddWarehouseItem(new FamilyWarehouseItemDto + { + FamilyId = character.Family.Id, + ItemInstance = mapped, + Slot = e.DestinationSlot + }, character.Id, character.Name); + + if (response == ManagerResponseType.Success) + { + await session.EmitEventAsync(new FamilyWarehouseItemPlacedEvent + { + ItemInstance = mapped, + Amount = mapped.Amount, + DestinationSlot = e.DestinationSlot + }); + return; + } + + await e.Sender.AddNewItemToInventory(_gameItemInstanceFactory.CreateItem(mapped), sendGiftIsFull: true); + e.Sender.SendInfo(response == ManagerResponseType.Maintenance + ? _languageService.GetLanguage(GameDialogKey.FAMILY_INFO_SERVICE_MAINTENANCE_MODE, e.Sender.UserLanguage) + : _languageService.GetLanguage(GameDialogKey.FAMILY_INFO_WAREHOUSE_UNEXPECTED_ERROR, e.Sender.UserLanguage)); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/FamilyWarehouseCloseEventHandler.cs b/srcs/_plugins/Plugin.FamilyImpl/FamilyWarehouseCloseEventHandler.cs new file mode 100644 index 0000000..8cfa4b9 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/FamilyWarehouseCloseEventHandler.cs @@ -0,0 +1,21 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Families.Event; +using WingsEmu.Game.Features; + +namespace Plugin.FamilyImpl +{ + public class FamilyWarehouseCloseEventHandler : IAsyncEventProcessor + { + private readonly IGameFeatureToggleManager _gameFeatureToggleManager; + + public FamilyWarehouseCloseEventHandler(IGameFeatureToggleManager gameFeatureToggleManager) => _gameFeatureToggleManager = gameFeatureToggleManager; + + public async Task HandleAsync(FamilyWarehouseCloseEvent e, CancellationToken cancellation) + { + e.Sender.PlayerEntity.IsFamilyWarehouseOpen = false; + e.Sender.PlayerEntity.IsFamilyWarehouseLogsOpen = false; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/FamilyWarehouseLogsOpenEventHandler.cs b/srcs/_plugins/Plugin.FamilyImpl/FamilyWarehouseLogsOpenEventHandler.cs new file mode 100644 index 0000000..3578fb4 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/FamilyWarehouseLogsOpenEventHandler.cs @@ -0,0 +1,77 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Data.Families; +using WingsAPI.Game.Extensions.Families; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Families; +using WingsEmu.Game.Families.Event; +using WingsEmu.Game.Networking; + +namespace Plugin.FamilyImpl +{ + public class FamilyWarehouseLogsOpenEventHandler : IAsyncEventProcessor + { + private readonly IFamilyWarehouseManager _familyWarehouseManager; + private readonly IGameLanguageService _languageService; + + public FamilyWarehouseLogsOpenEventHandler(IGameLanguageService languageService, IFamilyWarehouseManager familyWarehouseManager) + { + _languageService = languageService; + _familyWarehouseManager = familyWarehouseManager; + } + + public async Task HandleAsync(FamilyWarehouseLogsOpenEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + IPlayerEntity character = e.Sender.PlayerEntity; + IFamily family = session.PlayerEntity.Family; + + if (e.Refresh && !character.IsFamilyWarehouseLogsOpen || character.IsFamilyWarehouseOpen || session.CantPerformActionOnAct4() || character.HasShopOpened || character.IsInExchange()) + { + return; + } + + if (family == null) + { + session.SendInfo(_languageService.GetLanguage(GameDialogKey.FAMILY_INFO_NO_FAMILY, session.UserLanguage)); + return; + } + + if (family.GetWarehouseCapacity() == 0) + { + session.SendInfo(_languageService.GetLanguage(GameDialogKey.FAMILY_INFO_NO_FAMILY_WAREHOUSE, session.UserLanguage)); + return; + } + + if (!session.CheckLogHistoryPermission()) + { + session.SendInfo(_languageService.GetLanguage(GameDialogKey.FAMILY_INFO_WAREHOUSE_NOT_ENOUGH_PERMISSION, session.UserLanguage)); + return; + } + + (IList logs, ManagerResponseType? responseType) = await _familyWarehouseManager.GetWarehouseLogs(family.Id, character.Id); + + if (responseType == null) + { + e.Sender.SendInfo(_languageService.GetLanguage(GameDialogKey.FAMILY_INFO_WAREHOUSE_UNEXPECTED_ERROR, e.Sender.UserLanguage)); + return; + } + + if (responseType == ManagerResponseType.Success) + { + session.PlayerEntity.IsFamilyWarehouseLogsOpen = true; + session.SendFamilyWarehouseLogs(logs ?? new List()); + return; + } + + e.Sender.SendInfo(responseType == ManagerResponseType.Maintenance + ? _languageService.GetLanguage(GameDialogKey.FAMILY_INFO_SERVICE_MAINTENANCE_MODE, e.Sender.UserLanguage) + : _languageService.GetLanguage(GameDialogKey.FAMILY_INFO_WAREHOUSE_UNEXPECTED_ERROR, e.Sender.UserLanguage)); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/FamilyWarehouseManager.cs b/srcs/_plugins/Plugin.FamilyImpl/FamilyWarehouseManager.cs new file mode 100644 index 0000000..86c6cbe --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/FamilyWarehouseManager.cs @@ -0,0 +1,272 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Caching; +using PhoenixLib.Logging; +using WingsAPI.Communication; +using WingsAPI.Communication.Families.Warehouse; +using WingsAPI.Data.Families; +using WingsAPI.Game.Extensions; +using WingsEmu.Core.Extensions; +using WingsEmu.DTOs.Items; +using WingsEmu.Game._enum; + +namespace Plugin.FamilyImpl +{ + public class FamilyWarehouseManager : IFamilyWarehouseManager + { + private const int MaximumAmountOfLogs = 200; + private static readonly TimeSpan LifeTime = TimeSpan.FromMinutes(15); + + private readonly ILongKeyCachedRepository> _cachedFamilyItems; + + private readonly ILongKeyCachedRepository> _cachedFamilyLogs; + private readonly ConcurrentDictionary _familyItemLocks = new(); + private readonly ConcurrentDictionary _familyLogsLocks = new(); + + private readonly IFamilyWarehouseService _familyWarehouseService; + + public FamilyWarehouseManager(ILongKeyCachedRepository> cachedFamilyItems, + ILongKeyCachedRepository> cachedFamilyLogs, IFamilyWarehouseService familyWarehouseService) + { + _cachedFamilyItems = cachedFamilyItems; + _cachedFamilyLogs = cachedFamilyLogs; + _familyWarehouseService = familyWarehouseService; + } + + public async Task<(IList, ManagerResponseType?)> GetWarehouseLogs(long familyId, long characterId) + { + SemaphoreSlim familyLogSemaphore = GetFamilyLogSemaphore(familyId); + + await familyLogSemaphore.WaitAsync(); + try + { + return await GetWarehouseLogsWithoutLock(familyId, characterId); + } + finally + { + familyLogSemaphore.Release(); + } + } + + public async Task<(IDictionary familyWarehouseItemDtos, ManagerResponseType?)> GetWarehouse(long familyId, long characterId) + { + SemaphoreSlim familyItemSemaphore = GetFamilyItemSemaphore(familyId); + + await familyItemSemaphore.WaitAsync(); + try + { + return await GetWarehouseWithoutLock(familyId, characterId); + } + finally + { + familyItemSemaphore.Release(); + } + } + + public async Task<(FamilyWarehouseItemDto, ManagerResponseType?)> GetWarehouseItem(long familyId, short slot, long characterId) + { + (IDictionary familyWarehouseItemDtos, ManagerResponseType? responseType) = await GetWarehouse(familyId, characterId); + return (familyWarehouseItemDtos.GetOrDefault(slot), responseType); + } + + public async Task AddWarehouseItem(FamilyWarehouseItemDto warehouseItemDtoToAdd, long characterId, string characterName) + { + FamilyWarehouseAddItemResponse response = null; + + try + { + response = await _familyWarehouseService.AddItem(new FamilyWarehouseAddItemRequest + { + CharacterId = characterId, + CharacterName = characterName, + Item = warehouseItemDtoToAdd + }); + } + catch (Exception ex) + { + Log.Error("[FAMILY_WAREHOUSE_MANAGER][ADD_ITEM] ", ex); + } + + return response?.ResponseType.ToManagerType(); + } + + public async Task<(ItemInstanceDTO, ManagerResponseType?)> WithdrawWarehouseItem(FamilyWarehouseItemDto warehouseItemDtoToWithdraw, int amount, long characterId, string characterName) + { + FamilyWarehouseWithdrawItemResponse response = null; + + try + { + response = await _familyWarehouseService.WithdrawItem(new FamilyWarehouseWithdrawItemRequest + { + ItemToWithdraw = warehouseItemDtoToWithdraw, + Amount = amount, + CharacterId = characterId, + CharacterName = characterName + }); + } + catch (Exception ex) + { + Log.Error("[FAMILY_WAREHOUSE_MANAGER][WITHDRAW_ITEM] ", ex); + } + + return (response?.WithdrawnItem, response?.ResponseType.ToManagerType()); + } + + public async Task MoveWarehouseItem(FamilyWarehouseItemDto warehouseItemDtoToMove, int amount, short newSlot, long characterId) + { + FamilyWarehouseMoveItemResponse response = null; + + try + { + response = await _familyWarehouseService.MoveItem(new FamilyWarehouseMoveItemRequest + { + WarehouseItemDtoToMove = warehouseItemDtoToMove, + Amount = amount, + NewSlot = newSlot, + CharacterId = characterId + }); + } + catch (Exception ex) + { + Log.Error("[FAMILY_WAREHOUSE_MANAGER][MOVE_ITEM] ", ex); + } + + return response?.ResponseType.ToManagerType(); + } + + public async Task UpdateWarehouseItem(long familyId, IEnumerable<(FamilyWarehouseItemDto, short)> warehouseItemDtosToUpdate) + { + SemaphoreSlim familyItemSemaphore = GetFamilyItemSemaphore(familyId); + + await familyItemSemaphore.WaitAsync(); + try + { + (IDictionary items, ManagerResponseType? responseType) = await GetWarehouseWithoutLock(familyId); + if (responseType != ManagerResponseType.Success) + { + return; + } + + foreach ((FamilyWarehouseItemDto dto, short slot) in warehouseItemDtosToUpdate) + { + if (dto == null) + { + items?.Remove(slot); + continue; + } + + items ??= new Dictionary(); + items[slot] = dto; + } + } + finally + { + familyItemSemaphore.Release(); + } + } + + public async Task AddWarehouseLog(long familyId, FamilyWarehouseLogEntryDto log) + { + SemaphoreSlim familyItemSemaphore = GetFamilyLogSemaphore(familyId); + + await familyItemSemaphore.WaitAsync(); + try + { + (List logs, ManagerResponseType? responseType) = await GetWarehouseLogsWithoutLock(familyId); + if (responseType != ManagerResponseType.Success) + { + return; + } + + logs.Add(log); + if (logs.Count <= MaximumAmountOfLogs) + { + return; + } + + logs.RemoveRange(0, logs.Count - MaximumAmountOfLogs); + } + finally + { + familyItemSemaphore.Release(); + } + } + + private SemaphoreSlim GetFamilyItemSemaphore(long familyId) => _familyItemLocks.GetOrAdd(familyId, new SemaphoreSlim(1, 1)); + + private SemaphoreSlim GetFamilyLogSemaphore(long familyId) => _familyLogsLocks.GetOrAdd(familyId, new SemaphoreSlim(1, 1)); + + private async Task<(List, ManagerResponseType?)> GetWarehouseLogsWithoutLock(long familyId, long? characterId = null) + { + List retrievedLogs = _cachedFamilyLogs.Get(familyId); + + if (retrievedLogs != null) + { + return (retrievedLogs, ManagerResponseType.Success); + } + + FamilyWarehouseGetLogsResponse response = null; + + try + { + response = await _familyWarehouseService.GetLogs(new FamilyWarehouseGetLogsRequest + { + FamilyId = familyId, + CharacterId = characterId + }); + } + catch (Exception ex) + { + Log.Error("[FAMILY_WAREHOUSE_MANAGER][GET_LOGS] ", ex); + } + + var logs = response?.Logs?.ToList(); + + if (response?.ResponseType == RpcResponseType.SUCCESS) + { + _cachedFamilyLogs.Set(familyId, logs ?? new List(), LifeTime); + } + + return (logs, response?.ResponseType.ToManagerType()); + } + + private async Task<(IDictionary, ManagerResponseType?)> GetWarehouseWithoutLock(long familyId, long? characterId = null) + { + Dictionary retrievedItems = _cachedFamilyItems.Get(familyId); + + if (retrievedItems != null) + { + _cachedFamilyItems.Set(familyId, retrievedItems, LifeTime); + return (retrievedItems, ManagerResponseType.Success); + } + + FamilyWarehouseGetItemsResponse response = null; + + try + { + response = await _familyWarehouseService.GetItems(new FamilyWarehouseGetItemsRequest + { + FamilyId = familyId, + CharacterId = characterId + }); + } + catch (Exception ex) + { + Log.Error("[FAMILY_WAREHOUSE_MANAGER][GET_ITEMS] ", ex); + } + + Dictionary dictionary = response?.Items?.ToDictionary(x => x.Slot) ?? new Dictionary(); + + if (response?.ResponseType == RpcResponseType.SUCCESS) + { + _cachedFamilyItems.Set(familyId, dictionary, LifeTime); + } + + return (dictionary, response?.ResponseType.ToManagerType()); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/FamilyWarehouseMoveItemEventHandler.cs b/srcs/_plugins/Plugin.FamilyImpl/FamilyWarehouseMoveItemEventHandler.cs new file mode 100644 index 0000000..677cd6f --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/FamilyWarehouseMoveItemEventHandler.cs @@ -0,0 +1,68 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Data.Families; +using WingsAPI.Game.Extensions.Families; +using WingsAPI.Packets.Enums.Families; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Families.Event; +using WingsEmu.Game.Features; +using WingsEmu.Game.Networking; + +namespace Plugin.FamilyImpl +{ + public class FamilyWarehouseMoveItemEventHandler : IAsyncEventProcessor + { + private readonly IFamilyWarehouseManager _familyWarehouseManager; + private readonly IGameFeatureToggleManager _gameFeatureToggleManager; + private readonly IGameLanguageService _languageService; + + public FamilyWarehouseMoveItemEventHandler(IGameLanguageService languageService, IFamilyWarehouseManager familyWarehouseManager, IGameFeatureToggleManager gameFeatureToggleManager) + { + _languageService = languageService; + _familyWarehouseManager = familyWarehouseManager; + _gameFeatureToggleManager = gameFeatureToggleManager; + } + + public async Task HandleAsync(FamilyWarehouseMoveItemEvent e, CancellationToken cancellation) + { + bool disabled = await _gameFeatureToggleManager.IsDisabled(GameFeature.FamilyWarehouse); + if (disabled) + { + e.Sender.SendInfo(_languageService.GetLanguage(GameDialogKey.GAME_FEATURE_DISABLED, e.Sender.UserLanguage)); + return; + } + + IClientSession session = e.Sender; + IPlayerEntity character = e.Sender.PlayerEntity; + if (!character.IsInFamily() || !character.IsFamilyWarehouseOpen || session.CantPerformActionOnAct4() || character.HasShopOpened || character.IsInExchange()) + { + return; + } + + if (!session.CheckPutWithdrawPermission(FamilyWarehouseAuthorityType.Put)) + { + e.Sender.SendInfo(_languageService.GetLanguage(GameDialogKey.FAMILY_INFO_WAREHOUSE_NOT_ENOUGH_PERMISSION, e.Sender.UserLanguage)); + return; + } + + ManagerResponseType? responseType = await _familyWarehouseManager.MoveWarehouseItem(new FamilyWarehouseItemDto + { + FamilyId = character.Family.Id, + Slot = e.OldSlot + }, e.Amount, e.NewSlot, character.Id); + + if (responseType == ManagerResponseType.Success) + { + return; + } + + e.Sender.SendInfo(responseType == ManagerResponseType.Maintenance + ? _languageService.GetLanguage(GameDialogKey.ACCOUNT_INFO_SERVICE_MAINTENANCE_MODE, e.Sender.UserLanguage) + : _languageService.GetLanguage(GameDialogKey.ACCOUNT_INFO_WAREHOUSE_UNEXPECTED_ERROR, e.Sender.UserLanguage)); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/FamilyWarehouseOpenEventHandler.cs b/srcs/_plugins/Plugin.FamilyImpl/FamilyWarehouseOpenEventHandler.cs new file mode 100644 index 0000000..bd784e5 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/FamilyWarehouseOpenEventHandler.cs @@ -0,0 +1,87 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Data.Families; +using WingsAPI.Game.Extensions.Families; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Families; +using WingsEmu.Game.Families.Event; +using WingsEmu.Game.Features; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Networking; + +namespace Plugin.FamilyImpl +{ + public class FamilyWarehouseOpenEventHandler : IAsyncEventProcessor + { + private readonly IFamilyWarehouseManager _familyWarehouseManager; + private readonly IGameFeatureToggleManager _gameFeatureToggleManager; + private readonly IItemsManager _itemsManager; + private readonly IGameLanguageService _languageService; + + public FamilyWarehouseOpenEventHandler(IGameLanguageService languageService, IItemsManager itemsManager, IFamilyWarehouseManager familyWarehouseManager, + IGameFeatureToggleManager gameFeatureToggleManager) + { + _languageService = languageService; + _itemsManager = itemsManager; + _familyWarehouseManager = familyWarehouseManager; + _gameFeatureToggleManager = gameFeatureToggleManager; + } + + public async Task HandleAsync(FamilyWarehouseOpenEvent e, CancellationToken cancellation) + { + bool disabled = await _gameFeatureToggleManager.IsDisabled(GameFeature.FamilyWarehouse); + if (disabled) + { + e.Sender.SendInfo(_languageService.GetLanguage(GameDialogKey.GAME_FEATURE_DISABLED, e.Sender.UserLanguage)); + return; + } + + IClientSession session = e.Sender; + IPlayerEntity character = e.Sender.PlayerEntity; + IFamily family = session.PlayerEntity.Family; + + if (session.CantPerformActionOnAct4() || character.HasShopOpened || character.IsInExchange()) + { + return; + } + + if (family == null) + { + session.SendInfo(_languageService.GetLanguage(GameDialogKey.FAMILY_INFO_NO_FAMILY, session.UserLanguage)); + return; + } + + int capacity = family.GetWarehouseCapacity(); + + if (capacity == 0) + { + session.SendInfo(_languageService.GetLanguage(GameDialogKey.FAMILY_INFO_NO_FAMILY_WAREHOUSE, session.UserLanguage)); + return; + } + + (IDictionary familyWarehouseItemDtos, ManagerResponseType? responseType) = await _familyWarehouseManager.GetWarehouse(family.Id, session.PlayerEntity.Id); + + if (responseType == null) + { + e.Sender.SendInfo(_languageService.GetLanguage(GameDialogKey.FAMILY_INFO_WAREHOUSE_UNEXPECTED_ERROR, e.Sender.UserLanguage)); + return; + } + + if (responseType == ManagerResponseType.Success) + { + session.PlayerEntity.IsFamilyWarehouseOpen = true; + session.SendFamilyWarehouseItems(_itemsManager, capacity, familyWarehouseItemDtos?.Values ?? new List()); + return; + } + + e.Sender.SendInfo(responseType == ManagerResponseType.Maintenance + ? _languageService.GetLanguage(GameDialogKey.FAMILY_INFO_SERVICE_MAINTENANCE_MODE, e.Sender.UserLanguage) + : _languageService.GetLanguage(GameDialogKey.FAMILY_INFO_WAREHOUSE_UNEXPECTED_ERROR, e.Sender.UserLanguage)); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/FamilyWarehouseShowItemEventHandler.cs b/srcs/_plugins/Plugin.FamilyImpl/FamilyWarehouseShowItemEventHandler.cs new file mode 100644 index 0000000..bf60014 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/FamilyWarehouseShowItemEventHandler.cs @@ -0,0 +1,75 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Data.Families; +using WingsAPI.Game.Extensions.Families; +using WingsAPI.Packets.Enums.Families; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Algorithm; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Families.Event; +using WingsEmu.Game.Features; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Networking; + +namespace Plugin.FamilyImpl +{ + public class FamilyWarehouseShowItemEventHandler : IAsyncEventProcessor + { + private readonly ICharacterAlgorithm _algorithm; + private readonly IFamilyWarehouseManager _familyWarehouseManager; + private readonly IGameFeatureToggleManager _gameFeatureToggleManager; + private readonly IGameItemInstanceFactory _gameItemInstanceFactory; + private readonly IItemsManager _itemsManager; + private readonly IGameLanguageService _languageService; + + public FamilyWarehouseShowItemEventHandler(IGameItemInstanceFactory gameItemInstanceFactory, IGameLanguageService languageService, IItemsManager itemsManager, + ICharacterAlgorithm algorithm, IFamilyWarehouseManager familyWarehouseManager, IGameFeatureToggleManager gameFeatureToggleManager) + { + _gameItemInstanceFactory = gameItemInstanceFactory; + _languageService = languageService; + _itemsManager = itemsManager; + _algorithm = algorithm; + _familyWarehouseManager = familyWarehouseManager; + _gameFeatureToggleManager = gameFeatureToggleManager; + } + + public async Task HandleAsync(FamilyWarehouseShowItemEvent e, CancellationToken cancellation) + { + bool disabled = await _gameFeatureToggleManager.IsDisabled(GameFeature.FamilyWarehouse); + if (disabled) + { + e.Sender.SendInfo(_languageService.GetLanguage(GameDialogKey.GAME_FEATURE_DISABLED, e.Sender.UserLanguage)); + return; + } + + IClientSession session = e.Sender; + IPlayerEntity character = e.Sender.PlayerEntity; + + if (!character.IsInFamily() || !character.IsFamilyWarehouseOpen || session.CantPerformActionOnAct4() || character.HasShopOpened || character.IsInExchange()) + { + return; + } + + if (!session.CheckPutWithdrawPermission(FamilyWarehouseAuthorityType.Put)) + { + e.Sender.SendInfo(_languageService.GetLanguage(GameDialogKey.FAMILY_INFO_WAREHOUSE_NOT_ENOUGH_PERMISSION, e.Sender.UserLanguage)); + return; + } + + (FamilyWarehouseItemDto item, ManagerResponseType? response) = await _familyWarehouseManager.GetWarehouseItem(character.Family.Id, e.Slot, character.Id); + + if (response != ManagerResponseType.Success || item?.ItemInstance == null) + { + e.Sender.SendInfo(_languageService.GetLanguage( + response == ManagerResponseType.Maintenance ? GameDialogKey.FAMILY_INFO_SERVICE_MAINTENANCE_MODE : GameDialogKey.FAMILY_INFO_WAREHOUSE_UNEXPECTED_ERROR, e.Sender.UserLanguage)); + return; + } + + session.SendEInfoPacket(_gameItemInstanceFactory.CreateItem(item.ItemInstance), _itemsManager, _algorithm); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/FamilyWarehouseWithdrawItemEventHandler.cs b/srcs/_plugins/Plugin.FamilyImpl/FamilyWarehouseWithdrawItemEventHandler.cs new file mode 100644 index 0000000..177b827 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/FamilyWarehouseWithdrawItemEventHandler.cs @@ -0,0 +1,110 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Data.Families; +using WingsAPI.Game.Extensions.Families; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsAPI.Packets.Enums.Families; +using WingsEmu.DTOs.Items; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Families.Event; +using WingsEmu.Game.Features; +using WingsEmu.Game.Items; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.FamilyImpl +{ + public class FamilyWarehouseWithdrawItemEventHandler : IAsyncEventProcessor + { + private readonly IFamilyWarehouseManager _familyWarehouseManager; + private readonly IGameFeatureToggleManager _gameFeatureToggleManager; + private readonly IGameItemInstanceFactory _gameItemInstanceFactory; + private readonly IGameLanguageService _languageService; + + public FamilyWarehouseWithdrawItemEventHandler(IGameItemInstanceFactory gameItemInstanceFactory, IGameLanguageService languageService, IFamilyWarehouseManager familyWarehouseManager, + IGameFeatureToggleManager gameFeatureToggleManager) + { + _gameItemInstanceFactory = gameItemInstanceFactory; + _languageService = languageService; + _familyWarehouseManager = familyWarehouseManager; + _gameFeatureToggleManager = gameFeatureToggleManager; + } + + public async Task HandleAsync(FamilyWarehouseWithdrawItemEvent e, CancellationToken cancellation) + { + bool disabled = await _gameFeatureToggleManager.IsDisabled(GameFeature.FamilyWarehouse); + if (disabled) + { + e.Sender.SendInfo(_languageService.GetLanguage(GameDialogKey.GAME_FEATURE_DISABLED, e.Sender.UserLanguage)); + return; + } + + IClientSession session = e.Sender; + IPlayerEntity character = session.PlayerEntity; + + if (!character.IsInFamily() || !character.IsFamilyWarehouseOpen || session.CantPerformActionOnAct4() || character.HasShopOpened || character.IsInExchange()) + { + return; + } + + long familyId = character.Family.Id; + + if (!session.CheckPutWithdrawPermission(FamilyWarehouseAuthorityType.PutAndWithdraw)) + { + e.Sender.SendInfo(_languageService.GetLanguage(GameDialogKey.FAMILY_INFO_WAREHOUSE_NOT_ENOUGH_PERMISSION, e.Sender.UserLanguage)); + return; + } + + (FamilyWarehouseItemDto itemToWithdraw, ManagerResponseType? responseType) = await _familyWarehouseManager.GetWarehouseItem(familyId, e.Slot, character.Id); + + if (responseType == null) + { + e.Sender.SendInfo(_languageService.GetLanguage(GameDialogKey.FAMILY_INFO_WAREHOUSE_UNEXPECTED_ERROR, e.Sender.UserLanguage)); + return; + } + + if (itemToWithdraw?.ItemInstance == null) + { + e.Sender.SendInfo(_languageService.GetLanguage(GameDialogKey.FAMILY_INFO_WAREHOUSE_UNEXPECTED_ERROR, e.Sender.UserLanguage)); + return; + } + + if (!character.HasSpaceFor(itemToWithdraw.ItemInstance.ItemVNum, (short)itemToWithdraw.ItemInstance.Amount)) + { + session.SendMsg(session.GetLanguage(GameDialogKey.INTERACTION_MESSAGE_NOT_ENOUGH_PLACE), MsgMessageType.Middle); + return; + } + + if (responseType != ManagerResponseType.Success) + { + e.Sender.SendInfo(responseType == ManagerResponseType.Maintenance + ? _languageService.GetLanguage(GameDialogKey.FAMILY_INFO_SERVICE_MAINTENANCE_MODE, e.Sender.UserLanguage) + : _languageService.GetLanguage(GameDialogKey.FAMILY_INFO_WAREHOUSE_UNEXPECTED_ERROR, e.Sender.UserLanguage)); + + return; + } + + (ItemInstanceDTO withdrawnItem, ManagerResponseType? responseType2) = await _familyWarehouseManager.WithdrawWarehouseItem(itemToWithdraw, e.Amount, character.Id, character.Name); + + if (responseType2 == ManagerResponseType.Success) + { + await session.AddNewItemToInventory(_gameItemInstanceFactory.CreateItem(withdrawnItem), sendGiftIsFull: true); + await session.EmitEventAsync(new FamilyWarehouseItemWithdrawnEvent + { + ItemInstance = withdrawnItem, + Amount = e.Amount, + FromSlot = e.Slot + }); + return; + } + + e.Sender.SendInfo(responseType2 == ManagerResponseType.Maintenance + ? _languageService.GetLanguage(GameDialogKey.FAMILY_INFO_SERVICE_MAINTENANCE_MODE, e.Sender.UserLanguage) + : _languageService.GetLanguage(GameDialogKey.FAMILY_INFO_WAREHOUSE_UNEXPECTED_ERROR, e.Sender.UserLanguage)); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/IFamilyExperienceManager.cs b/srcs/_plugins/Plugin.FamilyImpl/IFamilyExperienceManager.cs new file mode 100644 index 0000000..4b6dd43 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/IFamilyExperienceManager.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using WingsEmu.Game.Families; + +namespace Plugin.FamilyImpl +{ + public interface IFamilyExperienceManager + { + void SaveFamilyExperienceToBuffer(ExperienceGainedSubMessage xpGained); + IReadOnlyCollection GetFamilyExperiencesInBuffer(); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/IFamilyWarehouseManager.cs b/srcs/_plugins/Plugin.FamilyImpl/IFamilyWarehouseManager.cs new file mode 100644 index 0000000..cc81903 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/IFamilyWarehouseManager.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using WingsAPI.Data.Families; +using WingsEmu.DTOs.Items; +using WingsEmu.Game._enum; + +namespace Plugin.FamilyImpl +{ + public interface IFamilyWarehouseManager + { + public Task<(IList, ManagerResponseType?)> GetWarehouseLogs(long familyId, long characterId); + public Task<(IDictionary familyWarehouseItemDtos, ManagerResponseType?)> GetWarehouse(long familyId, long characterId); + public Task<(FamilyWarehouseItemDto, ManagerResponseType?)> GetWarehouseItem(long familyId, short slot, long characterId); + public Task AddWarehouseItem(FamilyWarehouseItemDto warehouseItemDtoToAdd, long characterId, string characterName); + public Task<(ItemInstanceDTO, ManagerResponseType?)> WithdrawWarehouseItem(FamilyWarehouseItemDto warehouseItemDtoToWithdraw, int amount, long characterId, string characterName); + public Task MoveWarehouseItem(FamilyWarehouseItemDto warehouseItemDtoToMove, int amount, short newSlot, long characterId); + public Task UpdateWarehouseItem(long familyId, IEnumerable<(FamilyWarehouseItemDto, short)> warehouseItemDtosToUpdate); + public Task AddWarehouseLog(long familyId, FamilyWarehouseLogEntryDto log); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/Logs/FamilyLogManager.cs b/srcs/_plugins/Plugin.FamilyImpl/Logs/FamilyLogManager.cs new file mode 100644 index 0000000..6bf368b --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/Logs/FamilyLogManager.cs @@ -0,0 +1,32 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; +using WingsAPI.Data.Families; + +namespace Plugin.FamilyImpl.Logs +{ + public class FamilyLogManager : IFamilyLogManager + { + private readonly ConcurrentQueue _familyBufferForLogs = new(); + + public void SaveLogToBuffer(FamilyLogDto log) + { + _familyBufferForLogs.Enqueue(log); + } + + public IReadOnlyList GetFamilyLogsInBuffer() + { + if (_familyBufferForLogs.IsEmpty) + { + return null; + } + + var list = new List(); + while (_familyBufferForLogs.TryDequeue(out FamilyLogDto log)) + { + list.Add(log); + } + + return list; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/Logs/IFamilyLogManager.cs b/srcs/_plugins/Plugin.FamilyImpl/Logs/IFamilyLogManager.cs new file mode 100644 index 0000000..14e49c5 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/Logs/IFamilyLogManager.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using WingsAPI.Data.Families; + +namespace Plugin.FamilyImpl.Logs +{ + public interface IFamilyLogManager + { + void SaveLogToBuffer(FamilyLogDto log); + IReadOnlyList GetFamilyLogsInBuffer(); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyAcknowledgeExperienceGainedMessage.cs b/srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyAcknowledgeExperienceGainedMessage.cs new file mode 100644 index 0000000..b7454b5 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyAcknowledgeExperienceGainedMessage.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using PhoenixLib.ServiceBus; +using PhoenixLib.ServiceBus.Routing; + +namespace Plugin.FamilyImpl.Messages +{ + [MessageType("family.acknowledge.experience_gained")] + public class FamilyAcknowledgeExperienceGainedMessage : IMessage + { + public Dictionary Experiences { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyAcknowledgeLogsMessage.cs b/srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyAcknowledgeLogsMessage.cs new file mode 100644 index 0000000..a63abd6 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyAcknowledgeLogsMessage.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using PhoenixLib.ServiceBus; +using PhoenixLib.ServiceBus.Routing; +using WingsAPI.Data.Families; + +namespace Plugin.FamilyImpl.Messages +{ + [MessageType("family.acknowledge.logs")] + public class FamilyAcknowledgeLogsMessage : IMessage + { + public IReadOnlyDictionary> Logs { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyChangeFactionMessage.cs b/srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyChangeFactionMessage.cs new file mode 100644 index 0000000..21ed20f --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyChangeFactionMessage.cs @@ -0,0 +1,13 @@ +using PhoenixLib.ServiceBus; +using PhoenixLib.ServiceBus.Routing; +using WingsEmu.Packets.Enums; + +namespace Plugin.FamilyImpl.Messages +{ + [MessageType("family.faction")] + public class FamilyChangeFactionMessage : IMessage + { + public long FamilyId { get; init; } + public FactionType NewFaction { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyCharacterJoinMessage.cs b/srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyCharacterJoinMessage.cs new file mode 100644 index 0000000..ad3c89f --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyCharacterJoinMessage.cs @@ -0,0 +1,12 @@ +using PhoenixLib.ServiceBus; +using PhoenixLib.ServiceBus.Routing; + +namespace Plugin.FamilyImpl.Messages +{ + [MessageType("family.character.join")] + public class FamilyCharacterJoinMessage : IMessage + { + public long CharacterId { get; set; } + public long? FamilyId { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyCharacterLeaveMessage.cs b/srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyCharacterLeaveMessage.cs new file mode 100644 index 0000000..1e3a573 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyCharacterLeaveMessage.cs @@ -0,0 +1,12 @@ +using PhoenixLib.ServiceBus; +using PhoenixLib.ServiceBus.Routing; + +namespace Plugin.FamilyImpl.Messages +{ + [MessageType("family.character.leave")] + public class FamilyCharacterLeaveMessage : IMessage + { + public long FamilyId { get; set; } + public long CharacterId { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyChatMessage.cs b/srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyChatMessage.cs new file mode 100644 index 0000000..195d842 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyChatMessage.cs @@ -0,0 +1,17 @@ +using PhoenixLib.ServiceBus; +using PhoenixLib.ServiceBus.Routing; + +namespace Plugin.FamilyImpl.Messages +{ + [MessageType("family.chatmessage")] + public class FamilyChatMessage : IMessage + { + public long SenderFamilyId { get; set; } + + public int SenderChannelId { get; set; } + + public string SenderNickname { get; set; } + + public string Message { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyCreatedMessage.cs b/srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyCreatedMessage.cs new file mode 100644 index 0000000..271833f --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyCreatedMessage.cs @@ -0,0 +1,11 @@ +using PhoenixLib.ServiceBus; +using PhoenixLib.ServiceBus.Routing; + +namespace Plugin.FamilyImpl.Messages +{ + [MessageType("family.created")] + public class FamilyCreatedMessage : IMessage + { + public string FamilyName { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyDeclareExperienceGainedMessage.cs b/srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyDeclareExperienceGainedMessage.cs new file mode 100644 index 0000000..abb31bc --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyDeclareExperienceGainedMessage.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using PhoenixLib.ServiceBus; +using PhoenixLib.ServiceBus.Routing; +using WingsEmu.Game.Families; + +namespace Plugin.FamilyImpl.Messages +{ + [MessageType("family.declare.experience_gained")] + public class FamilyDeclareExperienceGainedMessage : IMessage + { + public IReadOnlyCollection Experiences { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyDeclareLogsMessage.cs b/srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyDeclareLogsMessage.cs new file mode 100644 index 0000000..da88e5f --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyDeclareLogsMessage.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using PhoenixLib.ServiceBus; +using PhoenixLib.ServiceBus.Routing; +using WingsAPI.Data.Families; + +namespace Plugin.FamilyImpl.Messages +{ + [MessageType("family.declare.logs")] + public class FamilyDeclareLogsMessage : IMessage + { + public IReadOnlyList Logs { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyDisbandMessage.cs b/srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyDisbandMessage.cs new file mode 100644 index 0000000..cd2026b --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyDisbandMessage.cs @@ -0,0 +1,11 @@ +using PhoenixLib.ServiceBus; +using PhoenixLib.ServiceBus.Routing; + +namespace Plugin.FamilyImpl.Messages +{ + [MessageType("family.disband")] + public class FamilyDisbandMessage : IMessage + { + public long FamilyId { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyHeadSexMessage.cs b/srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyHeadSexMessage.cs new file mode 100644 index 0000000..1e222a9 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyHeadSexMessage.cs @@ -0,0 +1,14 @@ +using PhoenixLib.ServiceBus; +using PhoenixLib.ServiceBus.Routing; +using WingsEmu.Packets.Enums.Character; + +namespace Plugin.FamilyImpl.Messages +{ + [MessageType("family.head.sex")] + public class FamilyHeadSexMessage : IMessage + { + public long FamilyId { get; set; } + + public GenderType NewGenderType { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyInviteMessage.cs b/srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyInviteMessage.cs new file mode 100644 index 0000000..da4ba3f --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyInviteMessage.cs @@ -0,0 +1,17 @@ +using PhoenixLib.ServiceBus; +using PhoenixLib.ServiceBus.Routing; + +namespace Plugin.FamilyImpl.Messages +{ + [MessageType("family.member.invite")] + public class FamilyInviteMessage : IMessage + { + public string ReceiverNickname { get; set; } + + public long SenderCharacterId { get; set; } + + public long FamilyId { get; set; } + + public string FamilyName { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyMemberAddedMessage.cs b/srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyMemberAddedMessage.cs new file mode 100644 index 0000000..7499074 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyMemberAddedMessage.cs @@ -0,0 +1,16 @@ +using PhoenixLib.ServiceBus; +using PhoenixLib.ServiceBus.Routing; +using WingsAPI.Data.Families; + +namespace Plugin.FamilyImpl.Messages +{ + [MessageType("family.member.added")] + public class FamilyMemberAddedMessage : IMessage + { + public FamilyMembershipDto AddedMember { get; set; } + + public string Nickname { get; set; } + + public long SenderId { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyMemberRemovedMessage.cs b/srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyMemberRemovedMessage.cs new file mode 100644 index 0000000..e70e298 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyMemberRemovedMessage.cs @@ -0,0 +1,13 @@ +using PhoenixLib.ServiceBus; +using PhoenixLib.ServiceBus.Routing; + +namespace Plugin.FamilyImpl.Messages +{ + [MessageType("family.member.removed")] + public class FamilyMemberRemovedMessage : IMessage + { + public long CharacterId { get; set; } + + public long FamilyId { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyMemberTodayMessage.cs b/srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyMemberTodayMessage.cs new file mode 100644 index 0000000..93d2dc1 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyMemberTodayMessage.cs @@ -0,0 +1,12 @@ +using PhoenixLib.ServiceBus; +using PhoenixLib.ServiceBus.Routing; + +namespace Plugin.FamilyImpl.Messages +{ + [MessageType("family.member.today")] + public class FamilyMemberTodayMessage : IMessage + { + public long CharacterId { get; set; } + public string Message { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyMemberUpdateMessage.cs b/srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyMemberUpdateMessage.cs new file mode 100644 index 0000000..3291011 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyMemberUpdateMessage.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using PhoenixLib.ServiceBus; +using PhoenixLib.ServiceBus.Routing; +using WingsAPI.Data.Families; + +namespace Plugin.FamilyImpl.Messages +{ + [MessageType("family.member.update")] + public class FamilyMemberUpdateMessage : IMessage + { + public List UpdatedMembers { get; set; } + + public ChangedInfoMemberUpdate ChangedInfoMemberUpdate { get; set; } + } + + public enum ChangedInfoMemberUpdate + { + None, + Authority, + Experience, + DailyMessage + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyNoticeMessage.cs b/srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyNoticeMessage.cs new file mode 100644 index 0000000..8201736 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyNoticeMessage.cs @@ -0,0 +1,12 @@ +using PhoenixLib.ServiceBus; +using PhoenixLib.ServiceBus.Routing; + +namespace Plugin.FamilyImpl.Messages +{ + [MessageType("family.notice")] + public class FamilyNoticeMessage : IMessage + { + public long FamilyId { get; set; } + public string Message { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyShoutMessage.cs b/srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyShoutMessage.cs new file mode 100644 index 0000000..c51c258 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyShoutMessage.cs @@ -0,0 +1,15 @@ +using PhoenixLib.ServiceBus; +using PhoenixLib.ServiceBus.Routing; +using WingsEmu.Game._i18n; + +namespace Plugin.FamilyImpl.Messages +{ + [MessageType("interchannel.broadcast.msg")] + public class FamilyShoutMessage : IMessage + { + public string Message { get; set; } + public string SenderName { get; set; } + public long FamilyId { get; set; } + public GameDialogKey GameDialogKey { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyUpdateMessage.cs b/srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyUpdateMessage.cs new file mode 100644 index 0000000..657873a --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyUpdateMessage.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using PhoenixLib.ServiceBus; +using PhoenixLib.ServiceBus.Routing; +using WingsAPI.Data.Families; + +namespace Plugin.FamilyImpl.Messages +{ + [MessageType("family.update")] + public class FamilyUpdateMessage : IMessage + { + public IReadOnlyCollection Families { get; set; } + + public ChangedInfoFamilyUpdate ChangedInfoFamilyUpdate { get; set; } + } + + public enum ChangedInfoFamilyUpdate + { + None, + Experience, + Notice, + HeadSex, + Settings, + Upgrades, + AchievementsAndMissions + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyWarehouseItemUpdateMessage.cs b/srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyWarehouseItemUpdateMessage.cs new file mode 100644 index 0000000..884e0a3 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyWarehouseItemUpdateMessage.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using PhoenixLib.ServiceBus; +using PhoenixLib.ServiceBus.Routing; +using WingsAPI.Data.Families; + +namespace Plugin.FamilyImpl.Messages +{ + [MessageType("family.warehouse.item.update")] + public class FamilyWarehouseItemUpdateMessage : IMessage + { + public long FamilyId { get; set; } + + public IEnumerable<(FamilyWarehouseItemDto dto, short slot)> UpdatedItems { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyWarehouseLogAddMessage .cs b/srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyWarehouseLogAddMessage .cs new file mode 100644 index 0000000..de60dd1 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/Messages/FamilyWarehouseLogAddMessage .cs @@ -0,0 +1,14 @@ +using PhoenixLib.ServiceBus; +using PhoenixLib.ServiceBus.Routing; +using WingsAPI.Data.Families; + +namespace Plugin.FamilyImpl.Messages +{ + [MessageType("family.warehouse.log.add")] + public class FamilyWarehouseLogAddMessage : IMessage + { + public long FamilyId { get; set; } + + public FamilyWarehouseLogEntryDto LogToAdd { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/Missions/FamilyMissionReward.cs b/srcs/_plugins/Plugin.FamilyImpl/Missions/FamilyMissionReward.cs new file mode 100644 index 0000000..37e7279 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/Missions/FamilyMissionReward.cs @@ -0,0 +1,7 @@ +namespace Plugin.FamilyImpl.Achievements +{ + public class FamilyMissionReward + { + public int? FamilyXp { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/Missions/FamilyMissionSpecificConfiguration.cs b/srcs/_plugins/Plugin.FamilyImpl/Missions/FamilyMissionSpecificConfiguration.cs new file mode 100644 index 0000000..3dd00be --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/Missions/FamilyMissionSpecificConfiguration.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace Plugin.FamilyImpl.Achievements +{ + public class FamilyMissionSpecificConfiguration + { + public int MissionId { get; set; } + public int Value { get; set; } + public int MinimumRequiredLevel { get; set; } + public bool OncePerPlayerPerDay { get; set; } = false; + public List Rewards { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/Missions/FamilyMissionsConfiguration.cs b/srcs/_plugins/Plugin.FamilyImpl/Missions/FamilyMissionsConfiguration.cs new file mode 100644 index 0000000..e4a3da7 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/Missions/FamilyMissionsConfiguration.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; + +namespace Plugin.FamilyImpl.Achievements +{ + public class FamilyMissionsConfiguration : List + { + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/NpcDialogs/CreateFamilyHandler.cs b/srcs/_plugins/Plugin.FamilyImpl/NpcDialogs/CreateFamilyHandler.cs new file mode 100644 index 0000000..9d7c802 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/NpcDialogs/CreateFamilyHandler.cs @@ -0,0 +1,69 @@ +using System.Linq; +using System.Threading.Tasks; +using WingsEmu.Game._i18n; +using WingsEmu.Game._NpcDialog; +using WingsEmu.Game._NpcDialog.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Groups; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Families; + +namespace Plugin.FamilyImpl.NpcDialogs +{ + public class CreateFamilyHandler : INpcDialogAsyncHandler + { + private readonly IGameLanguageService _langService; + + public CreateFamilyHandler(IGameLanguageService langService) => _langService = langService; + + public NpcRunType[] NpcRunTypes => new[] { NpcRunType.FAMILY_DIALOGUE }; + + public async Task Execute(IClientSession session, NpcDialogEvent e) + { + if (session.CantPerformActionOnAct4()) + { + return; + } + + if (e.Argument == 0) + { + if (!session.PlayerEntity.IsInGroup()) + { + session.SendInfo(_langService.GetLanguage(GameDialogKey.FAMILY_INFO_CREATION_GROUP_REQUIRED, session.UserLanguage)); + return; + } + + PlayerGroup group = session.PlayerEntity.GetGroup(); + if (group.Members.Count != 3) + { + session.SendInfo(_langService.GetLanguage(GameDialogKey.FAMILY_INFO_GROUP_NOT_FULL, session.UserLanguage)); + return; + } + + if (group.Members.Any(s => s.Family != null)) + { + session.SendInfo(_langService.GetLanguage(GameDialogKey.FAMILY_INFO_GROUP_MEMBER_ALREADY_IN_FAMILY, session.UserLanguage)); + return; + } + + session.SendInboxPacket("#glmk^ 14 1 191"); // fuck gf i18n :pepega: + return; + } + + if (!session.PlayerEntity.IsInFamily()) + { + session.SendInfo(_langService.GetLanguage(GameDialogKey.FAMILY_INFO_NOT_IN_FAMILY, session.UserLanguage)); + return; + } + + if (session.PlayerEntity.GetFamilyAuthority() != FamilyAuthority.Head) + { + session.SendInfo(_langService.GetLanguage(GameDialogKey.FAMILY_INFO_NOT_FAMILY_HEAD, session.UserLanguage)); + return; + } + + session.SendQnaPacket("glrm 1", _langService.GetLanguage(GameDialogKey.FAMILY_DIALOG_ASK_DISMISS_FAMILY, session.UserLanguage)); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/NpcDialogs/OpenFamilyWarehouseHandler.cs b/srcs/_plugins/Plugin.FamilyImpl/NpcDialogs/OpenFamilyWarehouseHandler.cs new file mode 100644 index 0000000..8b6d043 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/NpcDialogs/OpenFamilyWarehouseHandler.cs @@ -0,0 +1,19 @@ +using System.Threading.Tasks; +using WingsEmu.Game._NpcDialog; +using WingsEmu.Game._NpcDialog.Event; +using WingsEmu.Game.Families.Event; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace Plugin.FamilyImpl.NpcDialogs +{ + public class OpenFamilyWarehouseHandler : INpcDialogAsyncHandler + { + public NpcRunType[] NpcRunTypes => new[] { NpcRunType.FAMILY_WAREHOUSE_OPEN }; + + public async Task Execute(IClientSession session, NpcDialogEvent e) + { + await session.EmitEventAsync(new FamilyWarehouseOpenEvent()); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/NpcDialogs/OpenFamilyWarehouseHistHandler.cs b/srcs/_plugins/Plugin.FamilyImpl/NpcDialogs/OpenFamilyWarehouseHistHandler.cs new file mode 100644 index 0000000..ac0d8e4 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/NpcDialogs/OpenFamilyWarehouseHistHandler.cs @@ -0,0 +1,19 @@ +using System.Threading.Tasks; +using WingsEmu.Game._NpcDialog; +using WingsEmu.Game._NpcDialog.Event; +using WingsEmu.Game.Families.Event; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace Plugin.FamilyImpl.NpcDialogs +{ + public class OpenFamilyWarehouseHistHandler : INpcDialogAsyncHandler + { + public NpcRunType[] NpcRunTypes => new[] { NpcRunType.FAMILY_WAREHOUSE_HISTORY }; + + public async Task Execute(IClientSession session, NpcDialogEvent e) + { + await session.EmitEventAsync(new FamilyWarehouseLogsOpenEvent()); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/Plugin.FamilyImpl.csproj b/srcs/_plugins/Plugin.FamilyImpl/Plugin.FamilyImpl.csproj new file mode 100644 index 0000000..3edd1e0 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/Plugin.FamilyImpl.csproj @@ -0,0 +1,27 @@ + + + + net5.0 + Plugin.FamilyImpl + + + + + + + + + + + + + + + + + + + + + + diff --git a/srcs/_plugins/Plugin.FamilyImpl/RecurrentJob/FamilyExperienceSystem.cs b/srcs/_plugins/Plugin.FamilyImpl/RecurrentJob/FamilyExperienceSystem.cs new file mode 100644 index 0000000..aaca863 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/RecurrentJob/FamilyExperienceSystem.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using PhoenixLib.Logging; +using PhoenixLib.ServiceBus; +using Plugin.FamilyImpl.Messages; +using WingsEmu.Game.Families; + +namespace Plugin.FamilyImpl.RecurrentJob +{ + public class FamilyExperienceSystem : BackgroundService + { + private static readonly TimeSpan Interval = TimeSpan.FromSeconds(10); + private readonly IFamilyExperienceManager _familyExperienceManager; + private readonly IMessagePublisher _messagePublisher; + + public FamilyExperienceSystem(IMessagePublisher messagePublisher, IFamilyExperienceManager familyExperienceManager) + { + _messagePublisher = messagePublisher; + _familyExperienceManager = familyExperienceManager; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + Log.Info("[FAMILY_EXPERIENCE_SYSTEM] Started!"); + while (!stoppingToken.IsCancellationRequested) + { + IReadOnlyCollection experiencesInBuffer = _familyExperienceManager.GetFamilyExperiencesInBuffer(); + + if (experiencesInBuffer?.Count > 0) + { + Log.Info($"[FAMILY_EXPERIENCE_SYSTEM] Sent: {experiencesInBuffer.Count.ToString()} experiences"); + await _messagePublisher.PublishAsync(new FamilyDeclareExperienceGainedMessage + { + Experiences = experiencesInBuffer + }, stoppingToken); + } + + await Task.Delay(Interval, stoppingToken); + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/RecurrentJob/FamilyLogSystem.cs b/srcs/_plugins/Plugin.FamilyImpl/RecurrentJob/FamilyLogSystem.cs new file mode 100644 index 0000000..1cf58bc --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/RecurrentJob/FamilyLogSystem.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using PhoenixLib.Logging; +using PhoenixLib.ServiceBus; +using Plugin.FamilyImpl.Logs; +using Plugin.FamilyImpl.Messages; +using WingsAPI.Data.Families; + +namespace Plugin.FamilyImpl.RecurrentJob +{ + public class FamilyLogSystem : BackgroundService + { + private static readonly TimeSpan Interval = TimeSpan.FromSeconds(10); + private readonly IFamilyLogManager _familyLogManager; + private readonly IMessagePublisher _messagePublisher; + + public FamilyLogSystem(IMessagePublisher messagePublisher, IFamilyLogManager familyLogManager) + { + _messagePublisher = messagePublisher; + _familyLogManager = familyLogManager; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + Log.Info("[FAMILY_LOG_SYSTEM] Started!"); + while (!stoppingToken.IsCancellationRequested) + { + IReadOnlyList logs = _familyLogManager.GetFamilyLogsInBuffer(); + + if (logs?.Count > 0) + { + Log.Info($"[FAMILY_LOG_SYSTEM] Sent: {logs.Count.ToString()} logs"); + await _messagePublisher.PublishAsync(new FamilyDeclareLogsMessage + { + Logs = logs + }, stoppingToken); + } + + await Task.Delay(Interval, stoppingToken); + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.FamilyImpl/Upgrades/FamilyUpgradeBuyHandler.cs b/srcs/_plugins/Plugin.FamilyImpl/Upgrades/FamilyUpgradeBuyHandler.cs new file mode 100644 index 0000000..ef62a89 --- /dev/null +++ b/srcs/_plugins/Plugin.FamilyImpl/Upgrades/FamilyUpgradeBuyHandler.cs @@ -0,0 +1,136 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Communication.Families; +using WingsAPI.Data.Families; +using WingsEmu.DTOs.Shops; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Families; +using WingsEmu.Game.Families.Configuration; +using WingsEmu.Game.Families.Event; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Shops; +using WingsEmu.Game.Shops.Event; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.FamilyImpl.Upgrades +{ + public class FamilyUpgradeBuyFromShopEventHandler : IAsyncEventProcessor + { + private readonly FamilyConfiguration _familyConfiguration; + private readonly IFamilyService _familyService; + private readonly IItemsManager _itemsManager; + + public FamilyUpgradeBuyFromShopEventHandler(IFamilyService familyService, IItemsManager itemsManager, FamilyConfiguration familyConfiguration) + { + _familyService = familyService; + _itemsManager = itemsManager; + _familyConfiguration = familyConfiguration; + } + + public async Task HandleAsync(FamilyUpgradeBuyFromShopEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + + if (!session.PlayerEntity.IsHeadOfFamily()) + { + return; + } + + INpcEntity npcId = session.CurrentMapInstance.GetNpcById(e.NpcId); + if (npcId == null) + { + return; + } + + if (npcId.ShopNpc.MenuType != ShopNpcMenuType.FAMILIES) + { + return; + } + + ShopItemDTO shopUpgrade = npcId.ShopNpc.ShopItems.FirstOrDefault(s => s.Slot == e.Slot); + + if (shopUpgrade == null) + { + return; + } + + IGameItem item = _itemsManager.GetItem(shopUpgrade.ItemVNum); + + if (item == null) + { + return; + } + + if (!Enum.TryParse(item.Data[2].ToString(), out FamilyUpgradeType familyUpgradeType)) + { + return; + } + + int minLevel = item.Data[0]; + int previousUpgrade = item.Data[1]; + + IFamily family = session.PlayerEntity.Family; + if (family.Level < minLevel) + { + return; + } + + if (session.PlayerEntity.Gold < item.Price) + { + session.SendMsg(session.GetLanguage(GameDialogKey.INTERACTION_MESSAGE_NOT_ENOUGH_GOLD), MsgMessageType.Middle); + return; + } + + if (previousUpgrade != 0 && !family.HasAlreadyBoughtUpgrade(previousUpgrade)) + { + session.SendMsg(session.GetLanguage(GameDialogKey.FAMILY_MESSAGE_UPGRADE_NO_PREVIOUS), MsgMessageType.Middle); + session.SendChatMessage(session.GetLanguage(GameDialogKey.FAMILY_MESSAGE_UPGRADE_NO_PREVIOUS), ChatMessageColorType.Red); + return; + } + + FamilyUpgradesConfiguration newUpgrade = _familyConfiguration.Upgrades.FirstOrDefault(x => x.UpgradeType == familyUpgradeType && x.UpgradeLevel == item.Data[3]); + if (newUpgrade == null) + { + return; + } + + FamilyUpgradeResponse response = await _familyService.TryAddFamilyUpgrade(new FamilyUpgradeRequest + { + FamilyId = family.Id, + UpgradeId = shopUpgrade.ItemVNum, + FamilyUpgradeType = familyUpgradeType, + Value = newUpgrade.Value + }); + + switch (response.ResponseType) + { + case FamilyUpgradeAddResponseType.SUCCESS: + session.PlayerEntity.RemoveGold(item.Price); + session.SendInformationChatMessage(session.GetLanguage(GameDialogKey.FAMILY_CHATMESSAGE_UPGRADE_BOUGHT)); + await session.EmitEventAsync(new ShopNpcListItemsEvent { NpcId = (int)e.NpcId, ShopType = shopUpgrade.Type }); + await session.EmitEventAsync(new FamilyUpgradeBoughtEvent + { + FamilyId = family.Id, + UpgradeVnum = shopUpgrade.ItemVNum, + FamilyUpgradeType = familyUpgradeType, + UpgradeValue = newUpgrade.Value + }); + break; + case FamilyUpgradeAddResponseType.MAINTENANCE_MODE: + session.SendErrorChatMessage(session.GetLanguage(GameDialogKey.FAMILY_INFO_SERVICE_MAINTENANCE_MODE)); + break; + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Act4/Act4DungeonStartedLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Act4/Act4DungeonStartedLogEntity.cs new file mode 100644 index 0000000..69c0e9b --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Act4/Act4DungeonStartedLogEntity.cs @@ -0,0 +1,20 @@ +using System; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.Act4; + +namespace Plugin.MongoLogs.Entities.Act4 +{ + [EntityFor(typeof(LogAct4DungeonStartedMessage))] + [CollectionName(CollectionNames.ACT4_DUNGEON_STARTED, DisplayCollectionNames.ACT4_DUNGEON_STARTED)] + public class Act4DungeonStartedLogEntity : IPlayerLogEntity + { + public string FactionType { get; set; } + public string DungeonType { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Act4/Act4FamilyDungeonWonLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Act4/Act4FamilyDungeonWonLogEntity.cs new file mode 100644 index 0000000..df66fd7 --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Act4/Act4FamilyDungeonWonLogEntity.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.Act4; +using WingsEmu.Game.Act4; + +namespace Plugin.MongoLogs.Entities.Act4 +{ + [EntityFor(typeof(LogAct4FamilyDungeonWonMessage))] + [CollectionName(CollectionNames.ACT4_DUNGEON_WON, DisplayCollectionNames.ACT4_DUNGEON_WON)] + public class Act4FamilyDungeonWonLogEntity : IPlayerLogEntity + { + public long FamilyId { get; set; } + public DungeonType DungeonType { get; set; } + public IEnumerable DungeonMembers { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Act4/Act4PvpKillLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Act4/Act4PvpKillLogEntity.cs new file mode 100644 index 0000000..55ec2c5 --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Act4/Act4PvpKillLogEntity.cs @@ -0,0 +1,20 @@ +using System; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.Act4; + +namespace Plugin.MongoLogs.Entities.Act4 +{ + [EntityFor(typeof(LogAct4PvpKillMessage))] + [CollectionName(CollectionNames.ACT4_KILL, DisplayCollectionNames.ACT4_KILL)] + public class Act4PvpKillLogEntity : IPlayerLogEntity + { + public long TargetId { get; set; } + public string KillerFaction { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Bazaar/BazaarBoughtItemsLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Bazaar/BazaarBoughtItemsLogEntity.cs new file mode 100644 index 0000000..69c551c --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Bazaar/BazaarBoughtItemsLogEntity.cs @@ -0,0 +1,25 @@ +using System; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.Bazaar; +using WingsEmu.DTOs.Items; + +namespace Plugin.MongoLogs.Entities.Bazaar +{ + [EntityFor(typeof(LogBazaarItemBoughtMessage))] + [CollectionName(CollectionNames.BAZAAR_ITEM_BOUGHT, DisplayCollectionNames.BAZAAR_ITEM_BOUGHT)] + public class BazaarItemBoughtLogEntity : IPlayerLogEntity + { + public long BazaarItemId { get; set; } + public long SellerId { get; set; } + public string SellerName { get; set; } + public long PricePerItem { get; set; } + public int Amount { get; set; } + public ItemInstanceDTO BoughtItem { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Bazaar/BazaarItemExpiredLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Bazaar/BazaarItemExpiredLogEntity.cs new file mode 100644 index 0000000..ed872d4 --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Bazaar/BazaarItemExpiredLogEntity.cs @@ -0,0 +1,23 @@ +using System; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.Bazaar; +using WingsEmu.DTOs.Items; + +namespace Plugin.MongoLogs.Entities.Bazaar +{ + [EntityFor(typeof(LogBazaarItemExpiredMessage))] + [CollectionName(CollectionNames.BAZAAR_ITEM_EXPIRED, DisplayCollectionNames.BAZAAR_ITEM_EXPIRED)] + public class BazaarItemExpiredLogEntity : IPlayerLogEntity + { + public long BazaarItemId { get; set; } + public long Price { get; set; } + public int Quantity { get; set; } + public ItemInstanceDTO Item { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Bazaar/BazaarItemInsertedLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Bazaar/BazaarItemInsertedLogEntity.cs new file mode 100644 index 0000000..a3c952d --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Bazaar/BazaarItemInsertedLogEntity.cs @@ -0,0 +1,24 @@ +using System; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.Bazaar; +using WingsEmu.DTOs.Items; + +namespace Plugin.MongoLogs.Entities.Bazaar +{ + [EntityFor(typeof(LogBazaarItemInsertedMessage))] + [CollectionName(CollectionNames.BAZAAR_ITEM_INSERTED, DisplayCollectionNames.BAZAAR_ITEM_INSERTED)] + public class BazaarItemInsertedLogEntity : IPlayerLogEntity + { + public long BazaarItemId { get; set; } + public long Price { get; set; } + public int Quantity { get; set; } + public long Taxes { get; set; } + public ItemInstanceDTO ItemInstance { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Bazaar/BazaarItemWithdrawnLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Bazaar/BazaarItemWithdrawnLogEntity.cs new file mode 100644 index 0000000..6664f1b --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Bazaar/BazaarItemWithdrawnLogEntity.cs @@ -0,0 +1,24 @@ +using System; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.Bazaar; +using WingsEmu.DTOs.Items; + +namespace Plugin.MongoLogs.Entities.Bazaar +{ + [EntityFor(typeof(LogBazaarItemWithdrawnMessage))] + [CollectionName(CollectionNames.BAZAAR_ITEM_WITHDRAWN, DisplayCollectionNames.BAZAAR_ITEM_WITHDRAWN)] + public class BazaarItemWithdrawnLogEntity : IPlayerLogEntity + { + public long BazaarItemId { get; set; } + public long Price { get; set; } + public int Quantity { get; set; } + public ItemInstanceDTO ItemInstance { get; set; } + public long ClaimedMoney { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Family/FamilyCreatedLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Family/FamilyCreatedLogEntity.cs new file mode 100644 index 0000000..917b64f --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Family/FamilyCreatedLogEntity.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.Family; + +namespace Plugin.MongoLogs.Entities.Family +{ + [EntityFor(typeof(LogFamilyCreatedMessage))] + [CollectionName(CollectionNames.FAMILY_MANAGEMENT_CREATED, DisplayCollectionNames.FAMILY_MANAGEMENT_CREATED)] + internal class FamilyCreatedLogEntity : IPlayerLogEntity + { + public long FamilyId { get; set; } + public string FamilyName { get; set; } + public List DeputiesIds { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Family/FamilyDisbandedLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Family/FamilyDisbandedLogEntity.cs new file mode 100644 index 0000000..10ce9f8 --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Family/FamilyDisbandedLogEntity.cs @@ -0,0 +1,19 @@ +using System; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.Family; + +namespace Plugin.MongoLogs.Entities.Family +{ + [EntityFor(typeof(LogFamilyDisbandedMessage))] + [CollectionName(CollectionNames.FAMILY_MANAGEMENT_DISBANDED, DisplayCollectionNames.FAMILY_MANAGEMENT_DISBANDED)] + internal class FamilyDisbandedLogEntity : IPlayerLogEntity + { + public long FamilyId { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Family/FamilyInvitedLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Family/FamilyInvitedLogEntity.cs new file mode 100644 index 0000000..8d5756f --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Family/FamilyInvitedLogEntity.cs @@ -0,0 +1,20 @@ +using System; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.Family; + +namespace Plugin.MongoLogs.Entities.Family +{ + [EntityFor(typeof(LogFamilyInvitedMessage))] + [CollectionName(CollectionNames.INVITATION_FAMILY, DisplayCollectionNames.INVITATION_FAMILY)] + public class FamilyInvitedLogEntity : IPlayerLogEntity + { + public long FamilyId { get; set; } + public long TargetId { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Family/FamilyJoinedLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Family/FamilyJoinedLogEntity.cs new file mode 100644 index 0000000..27db1ea --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Family/FamilyJoinedLogEntity.cs @@ -0,0 +1,20 @@ +using System; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.Family; + +namespace Plugin.MongoLogs.Entities.Family +{ + [EntityFor(typeof(LogFamilyJoinedMessage))] + [CollectionName(CollectionNames.FAMILY_MANAGEMENT_JOINED, DisplayCollectionNames.FAMILY_MANAGEMENT_JOINED)] + internal class FamilyJoinedLogEntity : IPlayerLogEntity + { + public long FamilyId { get; set; } + public long InviterId { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Family/FamilyKickedLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Family/FamilyKickedLogEntity.cs new file mode 100644 index 0000000..c8b750c --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Family/FamilyKickedLogEntity.cs @@ -0,0 +1,20 @@ +using System; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.Family; + +namespace Plugin.MongoLogs.Entities.Family +{ + [EntityFor(typeof(LogFamilyKickedMessage))] + [CollectionName(CollectionNames.FAMILY_MANAGEMENT_KICKED, DisplayCollectionNames.FAMILY_MANAGEMENT_KICKED)] + internal class FamilyKickedLogEntity : IPlayerLogEntity + { + public long FamilyId { get; set; } + public long KickedMemberId { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Family/FamilyLeftLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Family/FamilyLeftLogEntity.cs new file mode 100644 index 0000000..ab6c12e --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Family/FamilyLeftLogEntity.cs @@ -0,0 +1,19 @@ +using System; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.Family; + +namespace Plugin.MongoLogs.Entities.Family +{ + [EntityFor(typeof(LogFamilyLeftMessage))] + [CollectionName(CollectionNames.FAMILY_MANAGEMENT_LEFT, DisplayCollectionNames.FAMILY_MANAGEMENT_LEFT)] + internal class FamilyLeftLogEntity : IPlayerLogEntity + { + public long FamilyId { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Family/FamilyMessageLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Family/FamilyMessageLogEntity.cs new file mode 100644 index 0000000..c7ab9bd --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Family/FamilyMessageLogEntity.cs @@ -0,0 +1,21 @@ +using System; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.Family; + +namespace Plugin.MongoLogs.Entities.Family +{ + [EntityFor(typeof(LogFamilyMessageMessage))] + [CollectionName(CollectionNames.FAMILY_MANAGEMENT_MESSAGES, DisplayCollectionNames.FAMILY_MANAGEMENT_MESSAGES)] + internal class FamilyMessageLogEntity : IPlayerLogEntity + { + public string FamilyMessageType { get; set; } + public long FamilyId { get; set; } + public string Message { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Family/FamilyUpgradeBoughtLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Family/FamilyUpgradeBoughtLogEntity.cs new file mode 100644 index 0000000..3ffe95f --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Family/FamilyUpgradeBoughtLogEntity.cs @@ -0,0 +1,22 @@ +using System; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.Family; + +namespace Plugin.MongoLogs.Entities.Family +{ + [EntityFor(typeof(LogFamilyUpgradeBoughtMessage))] + [CollectionName(CollectionNames.FAMILY_MANAGEMENT_UPGRADE_BOUGHT, DisplayCollectionNames.FAMILY_MANAGEMENT_UPGRADE_BOUGHT)] + public class FamilyUpgradeBoughtLogEntity : IPlayerLogEntity + { + public long FamilyId { get; set; } + public int UpgradeVnum { get; set; } + public string FamilyUpgradeType { get; set; } + public short UpgradeValue { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Family/FamilyWarehouseItemPlacedLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Family/FamilyWarehouseItemPlacedLogEntity.cs new file mode 100644 index 0000000..b329348 --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Family/FamilyWarehouseItemPlacedLogEntity.cs @@ -0,0 +1,23 @@ +using System; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.Family; +using WingsEmu.DTOs.Items; + +namespace Plugin.MongoLogs.Entities.Family +{ + [EntityFor(typeof(LogFamilyWarehouseItemPlacedMessage))] + [CollectionName(CollectionNames.FAMILY_WAREHOUSE_ITEM_PLACED, DisplayCollectionNames.FAMILY_WAREHOUSE_ITEM_PLACED)] + public class FamilyWarehouseItemPlacedLogEntity : IPlayerLogEntity + { + public long FamilyId { get; set; } + public ItemInstanceDTO ItemInstance { get; set; } + public int Amount { get; set; } + public short DestinationSlot { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Family/FamilyWarehouseItemWithdrawnLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Family/FamilyWarehouseItemWithdrawnLogEntity.cs new file mode 100644 index 0000000..df2e8b9 --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Family/FamilyWarehouseItemWithdrawnLogEntity.cs @@ -0,0 +1,23 @@ +using System; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.Family; +using WingsEmu.DTOs.Items; + +namespace Plugin.MongoLogs.Entities.Family +{ + [EntityFor(typeof(LogFamilyWarehouseItemWithdrawnMessage))] + [CollectionName(CollectionNames.FAMILY_WAREHOUSE_ITEM_WITHDRAWN, DisplayCollectionNames.FAMILY_WAREHOUSE_ITEM_WITHDRAWN)] + public class FamilyWarehouseItemWithdrawnLogEntity : IPlayerLogEntity + { + public long FamilyId { get; set; } + public ItemInstanceDTO ItemInstance { get; set; } + public int Amount { get; set; } + public short FromSlot { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/IPlayerLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/IPlayerLogEntity.cs new file mode 100644 index 0000000..952aab6 --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/IPlayerLogEntity.cs @@ -0,0 +1,11 @@ +using System; + +namespace Plugin.MongoLogs.Entities +{ + public interface IPlayerLogEntity + { + public long CharacterId { get; set; } + public string IpAddress { get; set; } + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Inventory/InventoryItemDeletedLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Inventory/InventoryItemDeletedLogEntity.cs new file mode 100644 index 0000000..36926e5 --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Inventory/InventoryItemDeletedLogEntity.cs @@ -0,0 +1,21 @@ +using System; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.Inventory; +using WingsEmu.DTOs.Items; + +namespace Plugin.MongoLogs.Entities.Inventory +{ + [EntityFor(typeof(LogInventoryItemDeletedMessage))] + [CollectionName(CollectionNames.INVENTORY_ITEM_DELETED, DisplayCollectionNames.INVENTORY_ITEM_DELETED)] + public class InventoryItemDeletedLogEntity : IPlayerLogEntity + { + public ItemInstanceDTO ItemInstance { get; set; } + public int ItemAmount { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Inventory/InventoryItemUsedLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Inventory/InventoryItemUsedLogEntity.cs new file mode 100644 index 0000000..1545ee3 --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Inventory/InventoryItemUsedLogEntity.cs @@ -0,0 +1,19 @@ +using System; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.Inventory; + +namespace Plugin.MongoLogs.Entities.Inventory +{ + [EntityFor(typeof(LogInventoryItemUsedMessage))] + [CollectionName(CollectionNames.INVENTORY_ITEM_USED, DisplayCollectionNames.INVENTORY_ITEM_USED)] + public class InventoryItemUsedLogEntity : IPlayerLogEntity + { + public int ItemVnum { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Inventory/InventoryPickedUpItemLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Inventory/InventoryPickedUpItemLogEntity.cs new file mode 100644 index 0000000..6797444 --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Inventory/InventoryPickedUpItemLogEntity.cs @@ -0,0 +1,22 @@ +using System; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.Inventory; +using WingsEmu.Game.Helpers; + +namespace Plugin.MongoLogs.Entities.Inventory +{ + [EntityFor(typeof(LogInventoryPickedUpItemMessage))] + [CollectionName(CollectionNames.INVENTORY_PICKED_UP_ITEM, DisplayCollectionNames.INVENTORY_PICKED_UP_ITEM)] + public class InventoryPickedUpItemLogEntity : IPlayerLogEntity + { + public int ItemVnum { get; set; } + public int Amount { get; set; } + public Location Location { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Inventory/InventoryPickedUpPlayerItemLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Inventory/InventoryPickedUpPlayerItemLogEntity.cs new file mode 100644 index 0000000..b528ae2 --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Inventory/InventoryPickedUpPlayerItemLogEntity.cs @@ -0,0 +1,23 @@ +using System; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.Inventory; +using WingsEmu.DTOs.Items; +using WingsEmu.Game.Helpers; + +namespace Plugin.MongoLogs.Entities.Inventory +{ + [EntityFor(typeof(LogInventoryPickedUpPlayerItemMessage))] + [CollectionName(CollectionNames.INVENTORY_PICKED_UP_PLAYER_ITEM, DisplayCollectionNames.INVENTORY_PICKED_UP_PLAYER_ITEM)] + public class InventoryPickedUpPlayerItemLogEntity : IPlayerLogEntity + { + public ItemInstanceDTO ItemInstance { get; set; } + public int Amount { get; init; } + public Location Location { get; init; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Mate/LevelUpNosMateLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Mate/LevelUpNosMateLogEntity.cs new file mode 100644 index 0000000..dda2011 --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Mate/LevelUpNosMateLogEntity.cs @@ -0,0 +1,24 @@ +using System; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.LevelUp; +using WingsEmu.Game.Helpers; + +namespace Plugin.MongoLogs.Entities.Mate +{ + [EntityFor(typeof(LogLevelUpNosMateMessage))] + [CollectionName(CollectionNames.LEVEL_UP_NOSMATE, DisplayCollectionNames.LEVEL_UP_NOSMATE)] + internal class LevelUpNosMateLogEntity : IPlayerLogEntity + { + public int Level { get; set; } + public string LevelUpType { get; set; } + public Location Location { get; set; } + public int? ItemVnum { get; set; } + public int NosMateMonsterVnum { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Miniland/MinigameRewardClaimedLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Miniland/MinigameRewardClaimedLogEntity.cs new file mode 100644 index 0000000..e15c830 --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Miniland/MinigameRewardClaimedLogEntity.cs @@ -0,0 +1,26 @@ +using System; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.Miniland; +using WingsEmu.Game.Configurations.Miniland; + +namespace Plugin.MongoLogs.Entities.Miniland +{ + [EntityFor(typeof(LogMinigameRewardClaimedMessage))] + [CollectionName(CollectionNames.MINIGAME_REWARDS_CLAIMED, DisplayCollectionNames.MINIGAME_REWARDS_CLAIMED)] + internal class MinigameRewardClaimedLogEntity : IPlayerLogEntity + { + public long OwnerId { get; set; } + public int MinigameVnum { get; set; } + public string MinigameType { get; set; } + public RewardLevel RewardLevel { get; set; } + public bool Coupon { get; set; } + public int ItemVnum { get; set; } + public short Amount { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Miniland/MinigameScoreLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Miniland/MinigameScoreLogEntity.cs new file mode 100644 index 0000000..2b86e4d --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Miniland/MinigameScoreLogEntity.cs @@ -0,0 +1,25 @@ +using System; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.Miniland; + +namespace Plugin.MongoLogs.Entities.Miniland +{ + [EntityFor(typeof(LogMinigameScoreMessage))] + [CollectionName(CollectionNames.MINIGAME_SCORE, DisplayCollectionNames.MINIGAME_SCORE)] + internal class MinigameScoreLogEntity : IPlayerLogEntity + { + public string CharacterName { get; set; } + public long OwnerId { get; set; } + public TimeSpan CompletionTime { get; set; } + public int MinigameVnum { get; set; } + public string MinigameType { get; set; } + public long Score1 { get; set; } + public long Score2 { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Miniland/WarehouseItemPlacedLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Miniland/WarehouseItemPlacedLogEntity.cs new file mode 100644 index 0000000..ae46376 --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Miniland/WarehouseItemPlacedLogEntity.cs @@ -0,0 +1,22 @@ +using System; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.Miniland; +using WingsEmu.DTOs.Items; + +namespace Plugin.MongoLogs.Entities.Miniland +{ + [EntityFor(typeof(LogWarehouseItemPlacedMessage))] + [CollectionName(CollectionNames.WAREHOUSE_ITEM_PLACED, DisplayCollectionNames.WAREHOUSE_ITEM_PLACED)] + public class WarehouseItemPlacedLogEntity : IPlayerLogEntity + { + public ItemInstanceDTO ItemInstance { get; set; } + public int Amount { get; set; } + public short DestinationSlot { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Miniland/WarehouseItemWithdrawnLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Miniland/WarehouseItemWithdrawnLogEntity.cs new file mode 100644 index 0000000..ebc1ee4 --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Miniland/WarehouseItemWithdrawnLogEntity.cs @@ -0,0 +1,22 @@ +using System; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.Miniland; +using WingsEmu.DTOs.Items; + +namespace Plugin.MongoLogs.Entities.Miniland +{ + [EntityFor(typeof(LogWarehouseItemWithdrawnMessage))] + [CollectionName(CollectionNames.WAREHOUSE_ITEM_WITHDRAWN, DisplayCollectionNames.WAREHOUSE_ITEM_WITHDRAWN)] + public class WarehouseItemWithdrawnLogEntity : IPlayerLogEntity + { + public ItemInstanceDTO ItemInstance { get; set; } + public int Amount { get; set; } + public short FromSlot { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Npc/ItemProducedLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Npc/ItemProducedLogEntity.cs new file mode 100644 index 0000000..95fb1aa --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Npc/ItemProducedLogEntity.cs @@ -0,0 +1,21 @@ +using System; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.Npc; +using WingsEmu.DTOs.Items; + +namespace Plugin.MongoLogs.Entities.Npc +{ + [EntityFor(typeof(LogItemProducedMessage))] + [CollectionName(CollectionNames.NPC_ITEM_PRODUCED, DisplayCollectionNames.NPC_ITEM_PRODUCED)] + public class ItemProducedLogEntity : IPlayerLogEntity + { + public ItemInstanceDTO ItemInstance { get; set; } + public int ItemAmount { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Player/BoxOpenedLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Player/BoxOpenedLogEntity.cs new file mode 100644 index 0000000..fb9d6ed --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Player/BoxOpenedLogEntity.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.Player; +using WingsEmu.DTOs.Items; + +namespace Plugin.MongoLogs.Entities.Player +{ + [EntityFor(typeof(LogBoxOpenedMessage))] + [CollectionName(CollectionNames.RANDOM_BOX_OPENED, DisplayCollectionNames.RANDOM_BOX_OPENED)] + public class BoxOpenedLogEntity : IPlayerLogEntity + { + public ItemInstanceDTO Box { get; set; } + public List Rewards { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Player/GroupInvitedLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Player/GroupInvitedLogEntity.cs new file mode 100644 index 0000000..494da04 --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Player/GroupInvitedLogEntity.cs @@ -0,0 +1,20 @@ +using System; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.Player; + +namespace Plugin.MongoLogs.Entities.Player +{ + [EntityFor(typeof(LogGroupInvitedMessage))] + [CollectionName(CollectionNames.INVITATION_GROUP, DisplayCollectionNames.INVITATION_GROUP)] + public class GroupInvitedLogEntity : IPlayerLogEntity + { + public long GroupId { get; set; } + public long TargetId { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Player/MailClaimedLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Player/MailClaimedLogEntity.cs new file mode 100644 index 0000000..cd8fb3f --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Player/MailClaimedLogEntity.cs @@ -0,0 +1,22 @@ +using System; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.Mail; +using WingsEmu.DTOs.Items; + +namespace Plugin.MongoLogs.Entities.Player +{ + [EntityFor(typeof(LogMailClaimedMessage))] + [CollectionName(CollectionNames.MAIL_CLAIMED, DisplayCollectionNames.MAIL_CLAIMED)] + public class MailClaimedLogEntity : IPlayerLogEntity + { + public long MailId { get; set; } + public string SenderName { get; set; } + public ItemInstanceDTO ItemInstance { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Player/MailRemovedLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Player/MailRemovedLogEntity.cs new file mode 100644 index 0000000..6b82278 --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Player/MailRemovedLogEntity.cs @@ -0,0 +1,22 @@ +using System; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.Mail; +using WingsEmu.DTOs.Items; + +namespace Plugin.MongoLogs.Entities.Player +{ + [EntityFor(typeof(LogMailRemovedMessage))] + [CollectionName(CollectionNames.MAIL_REMOVED, DisplayCollectionNames.MAIL_REMOVED)] + public class MailRemovedLogEntity : IPlayerLogEntity + { + public long MailId { get; set; } + public string SenderName { get; set; } + public ItemInstanceDTO ItemInstance { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Player/NoteSentLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Player/NoteSentLogEntity.cs new file mode 100644 index 0000000..01af1d1 --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Player/NoteSentLogEntity.cs @@ -0,0 +1,22 @@ +using System; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.Mail; + +namespace Plugin.MongoLogs.Entities.Player +{ + [EntityFor(typeof(LogNoteSentMessage))] + [CollectionName(CollectionNames.NOTE_SENT, DisplayCollectionNames.NOTE_SENT)] + public class NoteSentLogEntity : IPlayerLogEntity + { + public long NoteId { get; set; } + public string ReceiverName { get; set; } + public string Title { get; set; } + public string Message { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Player/PlayerChatGeneralLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Player/PlayerChatGeneralLogEntity.cs new file mode 100644 index 0000000..a45aa0c --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Player/PlayerChatGeneralLogEntity.cs @@ -0,0 +1,20 @@ +using System; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.Player; + +namespace Plugin.MongoLogs.Entities.Player +{ + [EntityFor(typeof(LogPlayerChatMessage))] + [CollectionName(CollectionNames.CHAT, DisplayCollectionNames.CHAT)] + internal class PlayerChatGeneralLogEntity : IPlayerLogEntity + { + public long? TargetCharacterId { get; set; } + public string Message { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Player/PlayerDisconnectedLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Player/PlayerDisconnectedLogEntity.cs new file mode 100644 index 0000000..9135d3f --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Player/PlayerDisconnectedLogEntity.cs @@ -0,0 +1,30 @@ +using System; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.Player; + +namespace Plugin.MongoLogs.Entities.Player +{ + [EntityFor(typeof(LogPlayerDisconnectedMessage))] + [CollectionName(CollectionNames.CONNECTION_SESSION, DisplayCollectionNames.CONNECTION_SESSION)] + public class PlayerDisconnectedLogEntity : IPlayerLogEntity + { + public int ChannelId { get; set; } + + public string HardwareId { get; set; } + public string MasterAccountId { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime SessionStart { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime SessionEnd { get; set; } + + public TimeSpan SessionDuration { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Player/PlayerExchangeLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Player/PlayerExchangeLogEntity.cs new file mode 100644 index 0000000..9dcd3ee --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Player/PlayerExchangeLogEntity.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.Player; +using WingsAPI.Data.Exchanges; + +namespace Plugin.MongoLogs.Entities.Player +{ + [EntityFor(typeof(LogPlayerExchangeMessage))] + [CollectionName(CollectionNames.EXCHANGES, DisplayCollectionNames.EXCHANGES)] + internal class PlayerExchangeLogEntity : IPlayerLogEntity + { + public List Items { get; set; } + public long Gold { get; set; } + public long BankGold { get; set; } + public long TargetCharacterId { get; set; } + + public List TargetItems { get; set; } + public long TargetGold { get; set; } + public long TargetBankGold { get; set; } + public string TargetCharacterName { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Player/TradeRequestedLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Player/TradeRequestedLogEntity.cs new file mode 100644 index 0000000..9a37950 --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Player/TradeRequestedLogEntity.cs @@ -0,0 +1,19 @@ +using System; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.Player; + +namespace Plugin.MongoLogs.Entities.Player +{ + [EntityFor(typeof(LogTradeRequestedMessage))] + [CollectionName(CollectionNames.INVITATION_TRADE, DisplayCollectionNames.INVITATION_TRADE)] + public class TradeRequestedLogEntity : IPlayerLogEntity + { + public long TargetId { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Quest/QuestAbandonedLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Quest/QuestAbandonedLogEntity.cs new file mode 100644 index 0000000..59d5f53 --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Quest/QuestAbandonedLogEntity.cs @@ -0,0 +1,20 @@ +using System; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.Quest; + +namespace Plugin.MongoLogs.Entities.Quest +{ + [EntityFor(typeof(LogQuestAbandonedMessage))] + [CollectionName(CollectionNames.QUEST_ABANDONED, DisplayCollectionNames.QUEST_ABANDONED)] + internal class QuestAbandonedLogEntity : IPlayerLogEntity + { + public int QuestId { get; set; } + public string SlotType { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Quest/QuestAddedLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Quest/QuestAddedLogEntity.cs new file mode 100644 index 0000000..c95b6c7 --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Quest/QuestAddedLogEntity.cs @@ -0,0 +1,20 @@ +using System; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.Quest; + +namespace Plugin.MongoLogs.Entities.Quest +{ + [EntityFor(typeof(LogQuestAddedMessage))] + [CollectionName(CollectionNames.QUEST_ADDED, DisplayCollectionNames.QUEST_ADDED)] + internal class QuestAddedLogEntity : IPlayerLogEntity + { + public int QuestId { get; set; } + public string SlotType { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Quest/QuestCompletedLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Quest/QuestCompletedLogEntity.cs new file mode 100644 index 0000000..61107a5 --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Quest/QuestCompletedLogEntity.cs @@ -0,0 +1,22 @@ +using System; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.Quest; +using WingsEmu.Game.Helpers; + +namespace Plugin.MongoLogs.Entities.Quest +{ + [EntityFor(typeof(LogQuestCompletedMessage))] + [CollectionName(CollectionNames.QUEST_COMPLETED, DisplayCollectionNames.QUEST_COMPLETED)] + internal class QuestCompletedLogEntity : IPlayerLogEntity + { + public int QuestId { get; set; } + public string SlotType { get; set; } + public Location Location { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Quest/QuestObjectiveUpdatedLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Quest/QuestObjectiveUpdatedLogEntity.cs new file mode 100644 index 0000000..2d0730b --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Quest/QuestObjectiveUpdatedLogEntity.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.Quest; + +namespace Plugin.MongoLogs.Entities.Quest +{ + [EntityFor(typeof(LogQuestObjectiveUpdatedMessage))] + [CollectionName(CollectionNames.QUEST_OBJECTIVE_UPDATED, DisplayCollectionNames.QUEST_OBJECTIVE_UPDATED)] + internal class QuestObjectiveUpdatedLogEntity : IPlayerLogEntity + { + public int QuestId { get; set; } + public string SlotType { get; set; } + + public Dictionary UpdatedObjectivesAmount { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Raid/RaidAbandonedLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Raid/RaidAbandonedLogEntity.cs new file mode 100644 index 0000000..b576928 --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Raid/RaidAbandonedLogEntity.cs @@ -0,0 +1,19 @@ +using System; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.Raid; + +namespace Plugin.MongoLogs.Entities.Raid +{ + [EntityFor(typeof(LogRaidAbandonedMessage))] + [CollectionName(CollectionNames.RAID_MANAGEMENT_ABANDONED, DisplayCollectionNames.RAID_MANAGEMENT_ABANDONED)] + public class RaidAbandonedLogEntity : IPlayerLogEntity + { + public Guid RaidId { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Raid/RaidCreatedLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Raid/RaidCreatedLogEntity.cs new file mode 100644 index 0000000..ce080af --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Raid/RaidCreatedLogEntity.cs @@ -0,0 +1,20 @@ +using System; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.Raid; + +namespace Plugin.MongoLogs.Entities.Raid +{ + [EntityFor(typeof(LogRaidCreatedMessage))] + [CollectionName(CollectionNames.RAID_MANAGEMENT_CREATED, DisplayCollectionNames.RAID_MANAGEMENT_CREATED)] + public class RaidCreatedLogEntity : IPlayerLogEntity + { + public Guid RaidId { get; set; } + public string RaidType { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Raid/RaidDiedLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Raid/RaidDiedLogEntity.cs new file mode 100644 index 0000000..604b54c --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Raid/RaidDiedLogEntity.cs @@ -0,0 +1,19 @@ +using System; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.Raid; + +namespace Plugin.MongoLogs.Entities.Raid +{ + [EntityFor(typeof(LogRaidDiedMessage))] + [CollectionName(CollectionNames.RAID_ACTION_DIED, DisplayCollectionNames.RAID_ACTION_DIED)] + public class RaidDiedLogEntity : IPlayerLogEntity + { + public Guid RaidId { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Raid/RaidInvitedLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Raid/RaidInvitedLogEntity.cs new file mode 100644 index 0000000..4e2cf53 --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Raid/RaidInvitedLogEntity.cs @@ -0,0 +1,20 @@ +using System; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.Raid; + +namespace Plugin.MongoLogs.Entities.Raid +{ + [EntityFor(typeof(LogRaidInvitedMessage))] + [CollectionName(CollectionNames.INVITATION_RAID, DisplayCollectionNames.INVITATION_RAID)] + public class RaidInvitedLogEntity : IPlayerLogEntity + { + public Guid RaidId { get; set; } + public long TargetId { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Raid/RaidJoinedLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Raid/RaidJoinedLogEntity.cs new file mode 100644 index 0000000..850a6a6 --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Raid/RaidJoinedLogEntity.cs @@ -0,0 +1,20 @@ +using System; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.Raid; + +namespace Plugin.MongoLogs.Entities.Raid +{ + [EntityFor(typeof(LogRaidJoinedMessage))] + [CollectionName(CollectionNames.RAID_MANAGEMENT_JOINED, DisplayCollectionNames.RAID_MANAGEMENT_JOINED)] + public class RaidJoinedLogEntity : IPlayerLogEntity + { + public Guid RaidId { get; set; } + public string RaidJoinType { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Raid/RaidLeftLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Raid/RaidLeftLogEntity.cs new file mode 100644 index 0000000..6f6823f --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Raid/RaidLeftLogEntity.cs @@ -0,0 +1,19 @@ +using System; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.Raid; + +namespace Plugin.MongoLogs.Entities.Raid +{ + [EntityFor(typeof(LogRaidLeftMessage))] + [CollectionName(CollectionNames.RAID_MANAGEMENT_LEFT, DisplayCollectionNames.RAID_MANAGEMENT_LEFT)] + public class RaidLeftLogEntity : IPlayerLogEntity + { + public Guid RaidId { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Raid/RaidLostLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Raid/RaidLostLogEntity.cs new file mode 100644 index 0000000..cd4c055 --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Raid/RaidLostLogEntity.cs @@ -0,0 +1,20 @@ +using System; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.Raid; + +namespace Plugin.MongoLogs.Entities.Raid +{ + [EntityFor(typeof(LogRaidLostMessage))] + [CollectionName(CollectionNames.RAID_ACTION_LOST, DisplayCollectionNames.RAID_ACTION_LOST)] + public class RaidLostLogEntity : IPlayerLogEntity + { + public string RaidId { get; set; } + public string RaidType { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Raid/RaidRevivedLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Raid/RaidRevivedLogEntity.cs new file mode 100644 index 0000000..47e2c35 --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Raid/RaidRevivedLogEntity.cs @@ -0,0 +1,19 @@ +using System; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.Raid; + +namespace Plugin.MongoLogs.Entities.Raid +{ + [EntityFor(typeof(LogRaidRevivedMessage))] + [CollectionName(CollectionNames.RAID_ACTION_REVIVED, DisplayCollectionNames.RAID_ACTION_REVIVED)] + public class RaidRevivedLogEntity : IPlayerLogEntity + { + public Guid RaidId { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Raid/RaidRewardReceivedLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Raid/RaidRewardReceivedLogEntity.cs new file mode 100644 index 0000000..8c91f88 --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Raid/RaidRewardReceivedLogEntity.cs @@ -0,0 +1,20 @@ +using System; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.Raid; + +namespace Plugin.MongoLogs.Entities.Raid +{ + [EntityFor(typeof(LogRaidRewardReceivedMessage))] + [CollectionName(CollectionNames.RAID_ACTION_REWARD_RECEIVED, DisplayCollectionNames.RAID_ACTION_REWARD_RECEIVED)] + public class RaidRewardReceivedLogEntity : IPlayerLogEntity + { + public Guid RaidId { get; set; } + public byte BoxRarity { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Raid/RaidStartedLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Raid/RaidStartedLogEntity.cs new file mode 100644 index 0000000..09108ed --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Raid/RaidStartedLogEntity.cs @@ -0,0 +1,21 @@ +using System; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.Raid; + +namespace Plugin.MongoLogs.Entities.Raid +{ + [EntityFor(typeof(LogRaidStartedMessage))] + [CollectionName(CollectionNames.RAID_MANAGEMENT_STARTED, DisplayCollectionNames.RAID_MANAGEMENT_STARTED)] + public class RaidStartedLogEntity : IPlayerLogEntity + { + public Guid RaidId { get; set; } + public long[] MembersIds { get; set; } + public string RaidType { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Raid/RaidSwitchButtonToggledLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Raid/RaidSwitchButtonToggledLogEntity.cs new file mode 100644 index 0000000..703f370 --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Raid/RaidSwitchButtonToggledLogEntity.cs @@ -0,0 +1,20 @@ +using System; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.Raid; + +namespace Plugin.MongoLogs.Entities.Raid +{ + [EntityFor(typeof(LogRaidSwitchButtonToggledMessage))] + [CollectionName(CollectionNames.RAID_ACTION_LEVER_ACTIVATED, DisplayCollectionNames.RAID_ACTION_LEVER_ACTIVATED)] + public class RaidSwitchButtonToggledLogEntity : IPlayerLogEntity + { + public Guid RaidId { get; set; } + public long LeverId { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Raid/RaidTargetKilledLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Raid/RaidTargetKilledLogEntity.cs new file mode 100644 index 0000000..bb1c83b --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Raid/RaidTargetKilledLogEntity.cs @@ -0,0 +1,20 @@ +using System; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.Raid; + +namespace Plugin.MongoLogs.Entities.Raid +{ + [EntityFor(typeof(LogRaidTargetKilledMessage))] + [CollectionName(CollectionNames.RAID_ACTION_TARGET_KILLED, DisplayCollectionNames.RAID_ACTION_TARGET_KILLED)] + public class RaidTargetKilledLogEntity : IPlayerLogEntity + { + public Guid RaidId { get; set; } + public long[] DamagerCharactersIds { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Raid/RaidWonLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Raid/RaidWonLogEntity.cs new file mode 100644 index 0000000..bf2ca22 --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Raid/RaidWonLogEntity.cs @@ -0,0 +1,20 @@ +using System; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.Raid; + +namespace Plugin.MongoLogs.Entities.Raid +{ + [EntityFor(typeof(LogRaidWonMessage))] + [CollectionName(CollectionNames.RAID_ACTION_WON, DisplayCollectionNames.RAID_ACTION_WON)] + public class RaidWonLogEntity : IPlayerLogEntity + { + public string RaidId { get; set; } + public string RaidType { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/RainbowBattle/RainbowBattleFrozenLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/RainbowBattle/RainbowBattleFrozenLogEntity.cs new file mode 100644 index 0000000..b473729 --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/RainbowBattle/RainbowBattleFrozenLogEntity.cs @@ -0,0 +1,19 @@ +using System; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.RainbowBattle; +using WingsEmu.Game.RainbowBattle; + +namespace Plugin.MongoLogs.Entities.RainbowBattle +{ + [EntityFor(typeof(LogRainbowBattleFrozenMessage))] + [CollectionName(CollectionNames.RAINBOW_BATTLE_FROZEN, DisplayCollectionNames.RAINBOW_BATTLE_MANAGEMENT_FROZEN)] + public class RainbowBattleFrozenLogEntity : IPlayerLogEntity + { + public Guid RainbowBattleId { get; set; } + public RainbowBattlePlayerDump Killer { get; set; } + public RainbowBattlePlayerDump Killed { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/RainbowBattle/RainbowBattleJoinLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/RainbowBattle/RainbowBattleJoinLogEntity.cs new file mode 100644 index 0000000..182b02e --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/RainbowBattle/RainbowBattleJoinLogEntity.cs @@ -0,0 +1,15 @@ +using System; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.RainbowBattle; + +namespace Plugin.MongoLogs.Entities.RainbowBattle +{ + [EntityFor(typeof(LogRainbowBattleJoinMessage))] + [CollectionName(CollectionNames.RAINBOW_BATTLE_JOIN, DisplayCollectionNames.RAINBOW_BATTLE_MANAGEMENT_JOIN)] + public class RainbowBattleJoinLogEntity : IPlayerLogEntity + { + public long CharacterId { get; set; } + public string IpAddress { get; set; } + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/RainbowBattle/RainbowBattleLoseLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/RainbowBattle/RainbowBattleLoseLogEntity.cs new file mode 100644 index 0000000..9da1f7f --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/RainbowBattle/RainbowBattleLoseLogEntity.cs @@ -0,0 +1,17 @@ +using System; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.RainbowBattle; + +namespace Plugin.MongoLogs.Entities.RainbowBattle +{ + [EntityFor(typeof(LogRainbowBattleLoseMessage))] + [CollectionName(CollectionNames.RAINBOW_BATTLE_LOSE, DisplayCollectionNames.RAINBOW_BATTLE_MANAGEMENT_LOSE)] + public class RainbowBattleLoseLogEntity : IPlayerLogEntity + { + public Guid RainbowBattleId { get; set; } + public int[] Players { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/RainbowBattle/RainbowBattleTieLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/RainbowBattle/RainbowBattleTieLogEntity.cs new file mode 100644 index 0000000..e6fd241 --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/RainbowBattle/RainbowBattleTieLogEntity.cs @@ -0,0 +1,17 @@ +using System; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.RainbowBattle; + +namespace Plugin.MongoLogs.Entities.RainbowBattle +{ + [EntityFor(typeof(LogRainbowBattleTieMessage))] + [CollectionName(CollectionNames.RAINBOW_BATTLE_TIE, DisplayCollectionNames.RAINBOW_BATTLE_MANAGEMENT_TIE)] + public class RainbowBattleTieLogEntity : IPlayerLogEntity + { + public int[] RedTeam { get; set; } + public int[] BlueTeam { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/RainbowBattle/RainbowBattleWonLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/RainbowBattle/RainbowBattleWonLogEntity.cs new file mode 100644 index 0000000..333a3bb --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/RainbowBattle/RainbowBattleWonLogEntity.cs @@ -0,0 +1,17 @@ +using System; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.RainbowBattle; + +namespace Plugin.MongoLogs.Entities.RainbowBattle +{ + [EntityFor(typeof(LogRainbowBattleWonMessage))] + [CollectionName(CollectionNames.RAINBOW_BATTLE_WON, DisplayCollectionNames.RAINBOW_BATTLE_MANAGEMENT_WON)] + public class RainbowBattleWonLogEntity : IPlayerLogEntity + { + public Guid RainbowBattleId { get; set; } + public int[] Players { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Shop/ShopClosedLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Shop/ShopClosedLogEntity.cs new file mode 100644 index 0000000..629ecb6 --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Shop/ShopClosedLogEntity.cs @@ -0,0 +1,20 @@ +using System; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.Shop; +using WingsEmu.Game.Helpers; + +namespace Plugin.MongoLogs.Entities.Shop +{ + [EntityFor(typeof(LogShopClosedMessage))] + [CollectionName(CollectionNames.SHOP_CLOSED, DisplayCollectionNames.SHOP_CLOSED)] + public class ShopClosedLogEntity : IPlayerLogEntity + { + public Location Location { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Shop/ShopNpcBoughtItemLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Shop/ShopNpcBoughtItemLogEntity.cs new file mode 100644 index 0000000..58ffb48 --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Shop/ShopNpcBoughtItemLogEntity.cs @@ -0,0 +1,24 @@ +using System; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.Shop; +using WingsEmu.DTOs.Items; + +namespace Plugin.MongoLogs.Entities.Shop +{ + [EntityFor(typeof(LogShopNpcBoughtItemMessage))] + [CollectionName(CollectionNames.SHOP_NPC_ITEM_BOUGHT, DisplayCollectionNames.SHOP_NPC_ITEM_BOUGHT)] + public class ShopNpcBoughtItemLogEntity : IPlayerLogEntity + { + public long SellerId { get; set; } + public long TotalPrice { get; set; } + public int Quantity { get; set; } + public string CurrencyType { get; set; } + public ItemInstanceDTO ItemInstance { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Shop/ShopNpcSoldItemLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Shop/ShopNpcSoldItemLogEntity.cs new file mode 100644 index 0000000..d63d4c6 --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Shop/ShopNpcSoldItemLogEntity.cs @@ -0,0 +1,22 @@ +using System; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.Shop; +using WingsEmu.DTOs.Items; + +namespace Plugin.MongoLogs.Entities.Shop +{ + [EntityFor(typeof(LogShopNpcSoldItemMessage))] + [CollectionName(CollectionNames.SHOP_NPC_ITEM_SOLD, DisplayCollectionNames.SHOP_NPC_ITEM_SOLD)] + public class ShopNpcSoldItemLogEntity : IPlayerLogEntity + { + public ItemInstanceDTO ItemInstance { get; set; } + public short Amount { get; set; } + public long PricePerItem { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Shop/ShopOpenedLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Shop/ShopOpenedLogEntity.cs new file mode 100644 index 0000000..39b789d --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Shop/ShopOpenedLogEntity.cs @@ -0,0 +1,21 @@ +using System; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.Shop; +using WingsEmu.Game.Helpers; + +namespace Plugin.MongoLogs.Entities.Shop +{ + [EntityFor(typeof(LogShopOpenedMessage))] + [CollectionName(CollectionNames.SHOP_OPENED, DisplayCollectionNames.SHOP_OPENED)] + public class ShopOpenedLogEntity : IPlayerLogEntity + { + public string ShopName { get; set; } + public Location Location { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Shop/ShopPlayerBoughtItemLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Shop/ShopPlayerBoughtItemLogEntity.cs new file mode 100644 index 0000000..3101ea9 --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Shop/ShopPlayerBoughtItemLogEntity.cs @@ -0,0 +1,24 @@ +using System; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.Shop; +using WingsEmu.DTOs.Items; + +namespace Plugin.MongoLogs.Entities.Shop +{ + [EntityFor(typeof(LogShopPlayerBoughtItemMessage))] + [CollectionName(CollectionNames.SHOP_PLAYER_ITEM_BOUGHT, DisplayCollectionNames.SHOP_PLAYER_ITEM_BOUGHT)] + public class ShopPlayerBoughtItemLogEntity : IPlayerLogEntity + { + public long SellerId { get; set; } + public string SellerName { get; set; } + public long TotalPrice { get; set; } + public int Quantity { get; set; } + public ItemInstanceDTO ItemInstance { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Shop/ShopSkillBoughtLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Shop/ShopSkillBoughtLogEntity.cs new file mode 100644 index 0000000..734f84d --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Shop/ShopSkillBoughtLogEntity.cs @@ -0,0 +1,19 @@ +using System; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.Shop; + +namespace Plugin.MongoLogs.Entities.Shop +{ + [EntityFor(typeof(LogShopSkillBoughtMessage))] + [CollectionName(CollectionNames.SHOP_SKILL_BOUGHT, DisplayCollectionNames.SHOP_SKILL_BOUGHT)] + public class ShopSkillBoughtLogEntity : IPlayerLogEntity + { + public short SkillVnum { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Shop/ShopSkillSoldLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Shop/ShopSkillSoldLogEntity.cs new file mode 100644 index 0000000..860aa57 --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Shop/ShopSkillSoldLogEntity.cs @@ -0,0 +1,19 @@ +using System; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.Shop; + +namespace Plugin.MongoLogs.Entities.Shop +{ + [EntityFor(typeof(LogShopSkillSoldMessage))] + [CollectionName(CollectionNames.SHOP_SKILL_SOLD, DisplayCollectionNames.SHOP_SKILL_SOLD)] + public class ShopSkillSoldLogEntity : IPlayerLogEntity + { + public int SkillVnum { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Upgrade/CellonUpgradedLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Upgrade/CellonUpgradedLogEntity.cs new file mode 100644 index 0000000..d58b90a --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Upgrade/CellonUpgradedLogEntity.cs @@ -0,0 +1,22 @@ +using System; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.Upgrade; +using WingsEmu.DTOs.Items; + +namespace Plugin.MongoLogs.Entities.Upgrade +{ + [EntityFor(typeof(LogCellonUpgradedMessage))] + [CollectionName(CollectionNames.UPGRADE_CELLON, DisplayCollectionNames.UPGRADE_CELLON)] + internal class CellonUpgradedLogEntity : IPlayerLogEntity + { + public ItemInstanceDTO Item { get; set; } + public int CellonVnum { get; set; } + public bool Succeed { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Upgrade/ItemGambledLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Upgrade/ItemGambledLogEntity.cs new file mode 100644 index 0000000..47edda8 --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Upgrade/ItemGambledLogEntity.cs @@ -0,0 +1,25 @@ +using System; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.Upgrade; + +namespace Plugin.MongoLogs.Entities.Upgrade +{ + [EntityFor(typeof(LogItemGambledMessage))] + [CollectionName(CollectionNames.UPGRADE_ITEM_GAMBLED, DisplayCollectionNames.UPGRADE_ITEM_GAMBLED)] + internal class ItemGambledLogEntity : IPlayerLogEntity + { + public int ItemVnum { get; set; } + public string Mode { get; set; } + public string Protection { get; set; } + public int? Amulet { get; set; } + public bool Succeed { get; set; } + public short OriginalRarity { get; set; } + public short? FinalRarity { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Upgrade/ItemSummedLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Upgrade/ItemSummedLogEntity.cs new file mode 100644 index 0000000..d246103 --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Upgrade/ItemSummedLogEntity.cs @@ -0,0 +1,23 @@ +using System; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.Upgrade; +using WingsEmu.DTOs.Items; + +namespace Plugin.MongoLogs.Entities.Upgrade +{ + [EntityFor(typeof(LogItemSummedMessage))] + [CollectionName(CollectionNames.UPGRADE_RESISTANCE_SUMMED, DisplayCollectionNames.UPGRADE_RESISTANCE_SUMMED)] + internal class ItemSummedLogEntity : IPlayerLogEntity + { + public ItemInstanceDTO LeftItem { get; set; } + public ItemInstanceDTO RightItem { get; set; } + public bool Succeed { get; set; } + public int SumLevel { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Upgrade/ItemUpgradedLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Upgrade/ItemUpgradedLogEntity.cs new file mode 100644 index 0000000..8651469 --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Upgrade/ItemUpgradedLogEntity.cs @@ -0,0 +1,26 @@ +using System; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.Upgrade; +using WingsEmu.DTOs.Items; + +namespace Plugin.MongoLogs.Entities.Upgrade +{ + [EntityFor(typeof(LogItemUpgradedMessage))] + [CollectionName(CollectionNames.UPGRADE_ITEM_UPGRADED, DisplayCollectionNames.UPGRADE_ITEM_UPGRADED)] + internal class ItemUpgradedLogEntity : IPlayerLogEntity + { + public ItemInstanceDTO Item { get; set; } + public long TotalPrice { get; set; } + public string Mode { get; set; } + public string Protection { get; set; } + public bool HasAmulet { get; set; } + public short OriginalUpgrade { get; set; } + public string Result { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Upgrade/LevelUpCharacterLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Upgrade/LevelUpCharacterLogEntity.cs new file mode 100644 index 0000000..a0effa5 --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Upgrade/LevelUpCharacterLogEntity.cs @@ -0,0 +1,23 @@ +using System; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.LevelUp; +using WingsEmu.Game.Helpers; + +namespace Plugin.MongoLogs.Entities.Upgrade +{ + [EntityFor(typeof(LogLevelUpCharacterMessage))] + [CollectionName(CollectionNames.LEVEL_UP_CHARACTER, DisplayCollectionNames.LEVEL_UP_CHARACTER)] + internal class LevelUpCharacterLogEntity : IPlayerLogEntity + { + public int Level { get; set; } + public string LevelType { get; set; } + public Location Location { get; set; } + public int? ItemVnum { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Upgrade/ShellIdentifiedLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Upgrade/ShellIdentifiedLogEntity.cs new file mode 100644 index 0000000..70f67e8 --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Upgrade/ShellIdentifiedLogEntity.cs @@ -0,0 +1,20 @@ +using System; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.Upgrade; +using WingsEmu.DTOs.Items; + +namespace Plugin.MongoLogs.Entities.Upgrade +{ + [EntityFor(typeof(LogShellIdentifiedMessage))] + [CollectionName(CollectionNames.UPGRADE_SHELL_IDENTIFIED, DisplayCollectionNames.UPGRADE_SHELL_IDENTIFIED)] + internal class ShellIdentifiedLogEntity : IPlayerLogEntity + { + public ItemInstanceDTO Shell { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Upgrade/SpPerfectedLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Upgrade/SpPerfectedLogEntity.cs new file mode 100644 index 0000000..c6f0049 --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Upgrade/SpPerfectedLogEntity.cs @@ -0,0 +1,22 @@ +using System; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.Upgrade; +using WingsEmu.DTOs.Items; + +namespace Plugin.MongoLogs.Entities.Upgrade +{ + [EntityFor(typeof(LogSpPerfectedMessage))] + [CollectionName(CollectionNames.UPGRADE_SP_PERFECTED, DisplayCollectionNames.UPGRADE_SP_PERFECTED)] + public class SpPerfectedLogEntity : IPlayerLogEntity + { + public ItemInstanceDTO Sp { get; set; } + public bool Success { get; set; } + public byte SpPerfectionLevel { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Entities/Upgrade/SpUpgradedLogEntity.cs b/srcs/_plugins/Plugin.MongoLogs/Entities/Upgrade/SpUpgradedLogEntity.cs new file mode 100644 index 0000000..c5d9c0e --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Entities/Upgrade/SpUpgradedLogEntity.cs @@ -0,0 +1,24 @@ +using System; +using MongoDB.Bson.Serialization.Attributes; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs.Messages.Upgrade; +using WingsEmu.DTOs.Items; + +namespace Plugin.MongoLogs.Entities.Upgrade +{ + [EntityFor(typeof(LogSpUpgradedMessage))] + [CollectionName(CollectionNames.UPGRADE_SP_UPGRADED, DisplayCollectionNames.UPGRADE_SP_UPGRADED)] + internal class SpUpgradedLogEntity : IPlayerLogEntity + { + public ItemInstanceDTO Sp { get; set; } + public string Mode { get; set; } + public string Result { get; set; } + public short OriginalUpgrade { get; set; } + public bool IsProtected { get; set; } + public long CharacterId { get; set; } + public string IpAddress { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Extensions/IgnoreDefaultPropertiesConvention.cs b/srcs/_plugins/Plugin.MongoLogs/Extensions/IgnoreDefaultPropertiesConvention.cs new file mode 100644 index 0000000..5251ce4 --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Extensions/IgnoreDefaultPropertiesConvention.cs @@ -0,0 +1,28 @@ +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Conventions; + +namespace Plugin.MongoLogs.Extensions +{ + public class IgnoreDefaultPropertiesConvention : IMemberMapConvention + { + public string Name => "Ignore default properties for all classes"; + + public void Apply(BsonMemberMap memberMap) + { + memberMap.SetIgnoreIfDefault(true); + } + } + + public class TypedIgnoreDefaultPropertiesConvention : IMemberMapConvention + { + public string Name => $"Ignore Default Properties for {typeof(T)}"; + + public void Apply(BsonMemberMap memberMap) + { + if (memberMap.ClassMap.ClassType == typeof(T)) + { + memberMap.SetIgnoreIfDefault(true); + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Extensions/MongoDatabaseExtensions.cs b/srcs/_plugins/Plugin.MongoLogs/Extensions/MongoDatabaseExtensions.cs new file mode 100644 index 0000000..054137e --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Extensions/MongoDatabaseExtensions.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using MongoDB.Bson; +using Newtonsoft.Json; +using Plugin.MongoLogs.Entities; +using Plugin.MongoLogs.Entities.Player; +using Plugin.MongoLogs.Utils; + +namespace Plugin.MongoLogs.Extensions +{ + public static class MongoDatabaseExtensions + { + public static string ToValidJson(this List collection) + { + if (!collection.Any()) + { + return string.Empty; + } + + foreach (BsonDocument bson in collection.Where(bson => bson.Contains("_id"))) + { + bson.Remove("_id"); + } + + return JsonConvert.SerializeObject(collection.ConvertAll(BsonTypeMapper.MapToDotNetValue)); + } + + public static Dictionary GetCollectionNames() + { + Dictionary res = new(); + + // todo rework this shit, it's completely dumb + var collectionNamesFields = typeof(CollectionNames).GetFields(BindingFlags.Public | BindingFlags.Default | BindingFlags.Static).ToList(); + var displayNamesFields = typeof(DisplayCollectionNames).GetFields(BindingFlags.Public | BindingFlags.Default | BindingFlags.Static).ToList(); + foreach (Type entityType in typeof(IPlayerLogEntity).Assembly.GetTypes().Where(x => typeof(IPlayerLogEntity).IsAssignableFrom(x) && !x.IsInterface && !x.IsAbstract)) + { + CollectionNameAttribute attribute = entityType.GetCustomAttribute(); + if (attribute == null) + { + continue; + } + + if (entityType == typeof(PlayerDisconnectedLogEntity)) + { + } + + FieldInfo field = collectionNamesFields.Find(x => x.GetValue(null).ToString() == attribute.CollectionName); + + if (field == null) + { + continue; + } + + // this is the completely dumb part, comparing... + if (!displayNamesFields.Exists(x => string.Equals(x.Name, field.Name, StringComparison.InvariantCultureIgnoreCase))) + { + continue; + } + + res[field.Name] = (attribute.CollectionName, attribute.DisplayName); + } + + return res; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Extensions/MongoLoggerExtensions.cs b/srcs/_plugins/Plugin.MongoLogs/Extensions/MongoLoggerExtensions.cs new file mode 100644 index 0000000..41dea7d --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Extensions/MongoLoggerExtensions.cs @@ -0,0 +1,157 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using MongoDB.Bson.Serialization.Conventions; +using PhoenixLib.Logging; +using PhoenixLib.ServiceBus.Extensions; +using Plugin.MongoLogs.Services; +using Plugin.MongoLogs.Utils; +using Plugin.PlayerLogs; +using Plugin.PlayerLogs.Messages.Act4; +using Plugin.PlayerLogs.Messages.Bazaar; +using Plugin.PlayerLogs.Messages.Family; +using Plugin.PlayerLogs.Messages.Inventory; +using Plugin.PlayerLogs.Messages.LevelUp; +using Plugin.PlayerLogs.Messages.Mail; +using Plugin.PlayerLogs.Messages.Miniland; +using Plugin.PlayerLogs.Messages.Npc; +using Plugin.PlayerLogs.Messages.Player; +using Plugin.PlayerLogs.Messages.Quest; +using Plugin.PlayerLogs.Messages.Raid; +using Plugin.PlayerLogs.Messages.RainbowBattle; +using Plugin.PlayerLogs.Messages.Shop; +using Plugin.PlayerLogs.Messages.Upgrade; +using WingsEmu.DTOs.Items; + +namespace Plugin.MongoLogs.Extensions +{ + public static class MongoLoggerExtensions + { + public static void AddMongoLogs(this IServiceCollection services) where T : class, IPlayerActionLogMessage + { + Log.Info($"Registering {typeof(T).Name} logs into Mongo storage..."); + services.AddMessageSubscriber>(); + } + + public static void AddMongoLogsPlugin(this IServiceCollection services) + { + var pack = new ConventionPack + { + new TypedIgnoreDefaultPropertiesConvention() + }; + ConventionRegistry.Register("Ignore Default Pack", pack, t => true); + + + services.AddMongoLogs(); + services.AddMongoLogs(); + + // Connection + services.AddMongoLogs(); + + // Act4 + services.AddMongoLogs(); + services.AddMongoLogs(); + services.AddMongoLogs(); + + // Level ups + services.AddMongoLogs(); + services.AddMongoLogs(); + + // Family + services.AddMongoLogs(); + services.AddMongoLogs(); + services.AddMongoLogs(); + services.AddMongoLogs(); + services.AddMongoLogs(); + services.AddMongoLogs(); + services.AddMongoLogs(); + services.AddMongoLogs(); + services.AddMongoLogs(); + + // Raids + services.AddMongoLogs(); + services.AddMongoLogs(); + services.AddMongoLogs(); + services.AddMongoLogs(); + services.AddMongoLogs(); + services.AddMongoLogs(); + services.AddMongoLogs(); + services.AddMongoLogs(); + services.AddMongoLogs(); + services.AddMongoLogs(); + services.AddMongoLogs(); + services.AddMongoLogs(); + + // Rainbow Battle + services.AddMongoLogs(); + services.AddMongoLogs(); + services.AddMongoLogs(); + services.AddMongoLogs(); + services.AddMongoLogs(); + + // Mini-games + services.AddMongoLogs(); + services.AddMongoLogs(); + + // Items + services.AddMongoLogs(); + services.AddMongoLogs(); + services.AddMongoLogs(); + services.AddMongoLogs(); + services.AddMongoLogs(); + services.AddMongoLogs(); + services.AddMongoLogs(); + services.AddMongoLogs(); + + // Quests + //services.AddMongoLogs(); + services.AddMongoLogs(); + services.AddMongoLogs(); + //services.AddMongoLogs(); + + // Shops + services.AddMongoLogs(); + services.AddMongoLogs(); + services.AddMongoLogs(); + services.AddMongoLogs(); + services.AddMongoLogs(); + services.AddMongoLogs(); + services.AddMongoLogs(); + + // Inventory + services.AddMongoLogs(); + services.AddMongoLogs(); + services.AddMongoLogs(); + services.AddMongoLogs(); + + // Invitations + services.AddMongoLogs(); + services.AddMongoLogs(); + services.AddMongoLogs(); + services.AddMongoLogs(); + + // Bazaar + services.AddMongoLogs(); + services.AddMongoLogs(); + services.AddMongoLogs(); + services.AddMongoLogs(); + + // Warehouse + services.AddMongoLogs(); + services.AddMongoLogs(); + + // Mails + services.AddMongoLogs(); + services.AddMongoLogs(); + services.AddMongoLogs(); + + // Notes + services.AddMongoLogs(); + + // Npc + services.AddMongoLogs(); + + services.AddSingleton(new MongoLogsBackgroundService(MongoLogsConfiguration.FromEnv())); + services.AddSingleton(provider => provider.GetService()); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Plugin.MongoLogs.csproj b/srcs/_plugins/Plugin.MongoLogs/Plugin.MongoLogs.csproj new file mode 100644 index 0000000..1dd0305 --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Plugin.MongoLogs.csproj @@ -0,0 +1,18 @@ + + + + net5.0 + + + + + + + + + + + + + + diff --git a/srcs/_plugins/Plugin.MongoLogs/Services/MongoLogsBackgroundService.cs b/srcs/_plugins/Plugin.MongoLogs/Services/MongoLogsBackgroundService.cs new file mode 100644 index 0000000..49c3bc8 --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Services/MongoLogsBackgroundService.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Concurrent; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using MongoDB.Driver; +using PhoenixLib.Logging; +using Plugin.MongoLogs.Utils; +using WingsEmu.Game.Logs; + +namespace Plugin.MongoLogs.Services +{ + public sealed class MongoLogsBackgroundService : BackgroundService + { + private static readonly MethodInfo Method = typeof(MongoLogsBackgroundService).GetMethod(nameof(InsertLog), BindingFlags.Static | BindingFlags.NonPublic); + private static readonly TimeSpan Interval = TimeSpan.FromSeconds(5); + private readonly IMongoDatabase _database; + private readonly ConcurrentDictionary _methodsCache = new(); + private readonly ConcurrentQueue<(Type, IPlayerActionLog)> _queue = new(); + + public MongoLogsBackgroundService(MongoLogsConfiguration mongoLogsConfiguration) + { + var client = new MongoClient(mongoLogsConfiguration.ToString()); + _database = client.GetDatabase(mongoLogsConfiguration.DbName); + } + + public void AddLogsToInsertionQueue(T log) where T : IPlayerActionLog + { + _queue.Enqueue((typeof(T), log)); + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + await ProcessMain(stoppingToken); + } + } + + public async Task ProcessMain(CancellationToken stoppingToken) + { + if (_queue.IsEmpty) + { + Log.Debug("Queue is empty. Nothing to send to mongo."); + await Task.Delay(Interval, stoppingToken); + } + + while (_queue.TryDequeue(out (Type, IPlayerActionLog) log)) + { + try + { + Type type = log.Item1; + MethodInfo method = _methodsCache.GetOrAdd(type, Method.MakeGenericMethod(type)); + await (Task)method.Invoke(this, new object?[] { _database, log.Item2 }); + } + catch (Exception e) + { + Log.Error("Couldn't send that action log message to mongodb. See the following exception:", e); + } + } + } + + private static async Task InsertLog(IMongoDatabase database, T log) where T : IPlayerActionLog + { + await database.InsertLogAsync(log); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Services/MongoLogsService.cs b/srcs/_plugins/Plugin.MongoLogs/Services/MongoLogsService.cs new file mode 100644 index 0000000..3704065 --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Services/MongoLogsService.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MongoDB.Bson; +using MongoDB.Driver; +using Plugin.MongoLogs.Utils; + +namespace Plugin.MongoLogs.Services +{ + public class MongoLogsService + { + private readonly IMongoDatabase _database; + + public MongoLogsService(MongoLogsConfiguration mongoLogsConfiguration) + { + var client = new MongoClient(mongoLogsConfiguration.ToString()); + _database = client.GetDatabase(mongoLogsConfiguration.DbName); + } + + public async Task> GetLogsAsync(string collectionName, long characterId, DateTime minDate, DateTime maxDate, int? limit = null, int? skip = null) + { + FilterDefinition characterFilter = Builders.Filter.Eq("CharacterId", characterId); + FilterDefinition minDateFilter = Builders.Filter.Gte("CreatedAt", minDate); + FilterDefinition maxDateFilter = Builders.Filter.Lte("CreatedAt", maxDate); + SortDefinition sortByDate = Builders.Sort.Descending("CreatedAt"); + var findOptions = new FindOptions + { + Limit = limit, + Skip = skip, + Sort = sortByDate + }; + IMongoCollection collection = _database.GetCollection(collectionName); + return await (await collection.FindAsync(characterFilter & minDateFilter & maxDateFilter, findOptions)).ToListAsync(); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Utils/CollectionNameAttribute.cs b/srcs/_plugins/Plugin.MongoLogs/Utils/CollectionNameAttribute.cs new file mode 100644 index 0000000..6ba30b7 --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Utils/CollectionNameAttribute.cs @@ -0,0 +1,17 @@ +using System; + +namespace Plugin.MongoLogs.Utils +{ + [AttributeUsage(AttributeTargets.Class)] + public class CollectionNameAttribute : Attribute + { + public CollectionNameAttribute(string collectionName, string displayName) + { + CollectionName = collectionName; + DisplayName = displayName; + } + + public string CollectionName { get; } + public string DisplayName { get; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Utils/CollectionNames.cs b/srcs/_plugins/Plugin.MongoLogs/Utils/CollectionNames.cs new file mode 100644 index 0000000..1502c25 --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Utils/CollectionNames.cs @@ -0,0 +1,124 @@ +namespace Plugin.MongoLogs.Utils +{ + public static class CollectionNames + { + /* ACT4 */ + public const string ACT4_KILL = "act4.kill"; + public const string ACT4_DUNGEON_STARTED = "act4.dungeon-started"; + public const string ACT4_DUNGEON_WON = "act4.dungeon-won"; + + /* NPC */ + public const string NPC_ITEM_PRODUCED = "npc.item-produced"; + + /* BAZAAR */ + public const string BAZAAR_ITEM_BOUGHT = "bazaar.item-bought"; + public const string BAZAAR_ITEM_EXPIRED = "bazaar.item-expired"; + public const string BAZAAR_ITEM_INSERTED = "bazaar.item-inserted"; + public const string BAZAAR_ITEM_WITHDRAWN = "bazaar.item-withdrawn"; + + /* CONNECTIONS */ + public const string CONNECTION_SESSION = "connection.session"; + + /* COMMANDS */ + public const string COMMAND = "command"; + + /* FAMILY MANAGEMENT */ + public const string FAMILY_MANAGEMENT_CREATED = "family-management.created"; + public const string FAMILY_MANAGEMENT_DISBANDED = "family-management.disbanded"; + public const string FAMILY_MANAGEMENT_JOINED = "family-management.joined"; + public const string FAMILY_MANAGEMENT_KICKED = "family-management.kicked"; + public const string FAMILY_MANAGEMENT_LEFT = "family-management.left"; + public const string FAMILY_MANAGEMENT_MESSAGES = "family-management.messages"; + public const string FAMILY_MANAGEMENT_UPGRADE_BOUGHT = "family-management.upgrade-bought"; + public const string FAMILY_WAREHOUSE_ITEM_PLACED = "family-management.warehouse-item-placed"; + public const string FAMILY_WAREHOUSE_ITEM_WITHDRAWN = "family-management.warehouse-item-withdrawn"; + + /* INVENTORY */ + public const string INVENTORY_PICKED_UP_ITEM = "inventory.pickedupitem"; + public const string INVENTORY_PICKED_UP_PLAYER_ITEM = "inventory.picked-up-player-item"; + public const string INVENTORY_ITEM_USED = "inventory.item-used"; + public const string INVENTORY_ITEM_DELETED = "inventory.item-deleted"; + + /* INVITATIONS */ + public const string INVITATION_FAMILY = "invitation.family"; + public const string INVITATION_GROUP = "invitation.group"; + public const string INVITATION_RAID = "invitation.raid"; + public const string INVITATION_TRADE = "invitation.trade"; + + /* LEVEL UPS */ + public const string LEVEL_UP_CHARACTER = "level-up.character"; + public const string LEVEL_UP_NOSMATE = "level-up.nosmate"; + + /* MAILS */ + public const string MAIL_CLAIMED = "mail.claimed"; + public const string MAIL_REMOVED = "mail.removed"; + + /* NOTES */ + public const string NOTE_SENT = "note.sent"; + + /* QUESTS */ + public const string QUEST_ABANDONED = "quest.abandoned"; + public const string QUEST_ADDED = "quest.added"; + public const string QUEST_COMPLETED = "quest.completed"; + public const string QUEST_OBJECTIVE_UPDATED = "quest.objective-updated"; + + /* RAID ACTIONS */ + public const string RAID_ACTION_DIED = "raid-action.died"; + public const string RAID_ACTION_LEVER_ACTIVATED = "raid-action.lever-activated"; + public const string RAID_ACTION_LOST = "raid-action.lost"; + public const string RAID_ACTION_REVIVED = "raid-action.revived"; + public const string RAID_ACTION_REWARD_RECEIVED = "raid-action.reward-received"; + public const string RAID_ACTION_TARGET_KILLED = "raid-action.target-killed"; + public const string RAID_ACTION_WON = "raid-action.won"; + + /* RAID MANAGEMENT */ + public const string RAID_MANAGEMENT_ABANDONED = "raid-management.abandoned"; + public const string RAID_MANAGEMENT_CREATED = "raid-management.created"; + public const string RAID_MANAGEMENT_JOINED = "raid-management.joined"; + public const string RAID_MANAGEMENT_LEFT = "raid-management.left"; + public const string RAID_MANAGEMENT_STARTED = "raid-management.started"; + + /* RAINBOW BATTLE */ + public const string RAINBOW_BATTLE_WON = "rainbow-battle.won"; + public const string RAINBOW_BATTLE_LOSE = "rainbow-battle.lose"; + public const string RAINBOW_BATTLE_TIE = "rainbow-battle.tie"; + public const string RAINBOW_BATTLE_JOIN = "rainbow-battle.join"; + public const string RAINBOW_BATTLE_FROZEN = "rainbow-battle.frozen"; + + /* RANDOM BOXES */ + public const string RANDOM_BOX_OPENED = "random-box.opened"; + + /* SHOPS */ + public const string SHOP_CLOSED = "shop.closed"; + public const string SHOP_PLAYER_ITEM_BOUGHT = "shop.player-item-bought"; + public const string SHOP_NPC_ITEM_BOUGHT = "shop.npc-item-bought"; + public const string SHOP_NPC_ITEM_SOLD = "shop.npc-item-sold"; + public const string SHOP_OPENED = "shop.opened"; + public const string SHOP_SKILL_BOUGHT = "shop.skill-bought"; + public const string SHOP_SKILL_SOLD = "shop.skill-sold"; + + + /* UPGRADES */ + public const string UPGRADE_CELLON = "upgrade.cellon"; + public const string UPGRADE_ITEM_GAMBLED = "upgrade.item-gambled"; + public const string UPGRADE_ITEM_UPGRADED = "upgrade.item-upgraded"; + public const string UPGRADE_RESISTANCE_SUMMED = "upgrade.resistance-summed"; + public const string UPGRADE_SHELL_IDENTIFIED = "upgrade.shell-identified"; + public const string UPGRADE_SP_UPGRADED = "upgrade.sp-upgraded"; + public const string UPGRADE_SP_PERFECTED = "upgrade.sp-perfected"; + + /* WAREHOUSE */ + public const string WAREHOUSE_ITEM_PLACED = "warehouse.item-placed"; + public const string WAREHOUSE_ITEM_WITHDRAWN = "warehouse.item-withdrawn"; + + /* MINIGAMES */ + public const string MINIGAME_SCORE = "minigame.score"; + public const string MINIGAME_REWARDS_CLAIMED = "minigame.rewards-claimed"; + + /* CHAT */ + public const string CHAT = "chat"; + + /* EXCHANGES */ + public const string EXCHANGES = "exchanges"; + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Utils/DisplayCollectionNames.cs b/srcs/_plugins/Plugin.MongoLogs/Utils/DisplayCollectionNames.cs new file mode 100644 index 0000000..1b376f4 --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Utils/DisplayCollectionNames.cs @@ -0,0 +1,123 @@ +namespace Plugin.MongoLogs.Utils +{ + public static class DisplayCollectionNames + { + /* ACT4 */ + public const string ACT4_KILL = "Act4 Kill"; + public const string ACT4_DUNGEON_STARTED = "Act4 Dungeon Started"; + public const string ACT4_DUNGEON_WON = "Act4 Dungeon Won"; + + /* NPC */ + public const string NPC_ITEM_PRODUCED = "Npc Item Produced"; + + /* BAZAAR */ + public const string BAZAAR_ITEM_BOUGHT = "Bazaar Item Bought"; + public const string BAZAAR_ITEM_EXPIRED = "Bazaar Item Expired"; + public const string BAZAAR_ITEM_INSERTED = "Bazaar Item Inserted"; + public const string BAZAAR_ITEM_WITHDRAWN = "Bazaar Item Widthdrawn"; + + /* CONNECTIONS */ + public const string CONNECTION_SESSION = "Connection Session"; + + /* COMMANDS */ + public const string COMMAND = "Command"; + + /* FAMILY MANAGEMENT */ + public const string FAMILY_MANAGEMENT_CREATED = "Family Management Created"; + public const string FAMILY_MANAGEMENT_DISBANDED = "Family Management Disbanded"; + public const string FAMILY_MANAGEMENT_JOINED = "Family Management Joined"; + public const string FAMILY_MANAGEMENT_KICKED = "Family Management Kicked"; + public const string FAMILY_MANAGEMENT_LEFT = "Family Management Left"; + public const string FAMILY_MANAGEMENT_MESSAGES = "Family Management Messages"; + public const string FAMILY_MANAGEMENT_UPGRADE_BOUGHT = "Family Management Upgrade Bought"; + public const string FAMILY_WAREHOUSE_ITEM_PLACED = "Family Management Warehouse Item Placed"; + public const string FAMILY_WAREHOUSE_ITEM_WITHDRAWN = "Family Management Warehouse Item Withdrawn"; + + /* INVITATIONS */ + public const string INVITATION_FAMILY = "Invitation Family"; + public const string INVITATION_GROUP = "Invitation Group"; + public const string INVITATION_RAID = "Invitation Raid"; + public const string INVITATION_TRADE = "Invitation Trade"; + + /* INVENTORY */ + public const string INVENTORY_PICKED_UP_ITEM = "Inventory Picked Up Item"; + public const string INVENTORY_ITEM_USED = "Inventory Item Used"; + public const string INVENTORY_PICKED_UP_PLAYER_ITEM = "Inventory Picked Up Player Item"; + public const string INVENTORY_ITEM_DELETED = "Inventory Item Deleted"; + + /* LEVEL UPS */ + public const string LEVEL_UP_CHARACTER = "Level Up Character"; + public const string LEVEL_UP_NOSMATE = "Level Up NosMate"; + + /* MAILS */ + public const string MAIL_CLAIMED = "Mail Claimed"; + public const string MAIL_REMOVED = "Mail Removed"; + + /* NOTES */ + public const string NOTE_SENT = "Note Sent"; + + /* QUESTS */ + public const string QUEST_ABANDONED = "Quest Abandoned"; + public const string QUEST_ADDED = "Quest Added"; + public const string QUEST_COMPLETED = "Quest Completed"; + public const string QUEST_OBJECTIVE_UPDATED = "Quest Objective Updated"; + + /* RAID ACTIONS */ + public const string RAID_ACTION_DIED = "Raid Action Died"; + public const string RAID_ACTION_LEVER_ACTIVATED = "Raid Action Lever Activated"; + public const string RAID_ACTION_LOST = "Raid Action Lost"; + public const string RAID_ACTION_REVIVED = "Raid Action Revived"; + public const string RAID_ACTION_REWARD_RECEIVED = "Raid Action Reward Received"; + public const string RAID_ACTION_TARGET_KILLED = "Raid Action Target Killed"; + public const string RAID_ACTION_WON = "Raid Action Won"; + + /* RAID MANAGEMENT */ + public const string RAID_MANAGEMENT_ABANDONED = "Raid Management Abandoned"; + public const string RAID_MANAGEMENT_CREATED = "Raid Management Created"; + public const string RAID_MANAGEMENT_JOINED = "Raid Management Joined"; + public const string RAID_MANAGEMENT_LEFT = "Raid Management Left"; + public const string RAID_MANAGEMENT_STARTED = "Raid Management Started"; + + /* RAINBOW BATTLE */ + public const string RAINBOW_BATTLE_MANAGEMENT_WON = "Rainbow Battle Won"; + public const string RAINBOW_BATTLE_MANAGEMENT_LOSE = "Rainbow Battle Lose"; + public const string RAINBOW_BATTLE_MANAGEMENT_TIE = "Rainbow Battle Tie"; + public const string RAINBOW_BATTLE_MANAGEMENT_JOIN = "Rainbow Battle Join"; + public const string RAINBOW_BATTLE_MANAGEMENT_FROZEN = "Rainbow Battle Frozen"; + + /* RANDOM BOXES */ + public const string RANDOM_BOX_OPENED = "Random Box Opened"; + + /* SHOPS */ + public const string SHOP_CLOSED = "Shop Closed"; + public const string SHOP_PLAYER_ITEM_BOUGHT = "Shop Player Item Bought"; + public const string SHOP_NPC_ITEM_BOUGHT = "Shop NPC Item Bought"; + public const string SHOP_NPC_ITEM_SOLD = "Shop NPC Item Sold"; + public const string SHOP_OPENED = "Shop Opened"; + public const string SHOP_SKILL_BOUGHT = "Shop Skill Bought"; + public const string SHOP_SKILL_SOLD = "Shop Skill Sold"; + + /* UPGRADES */ + public const string UPGRADE_CELLON = "Upgrade Cellon"; + public const string UPGRADE_ITEM_GAMBLED = "Upgrade Item Gambled"; + public const string UPGRADE_ITEM_UPGRADED = "Upgrade Item Upgraded"; + public const string UPGRADE_RESISTANCE_SUMMED = "Upgrade Resistance Summed"; + public const string UPGRADE_SHELL_IDENTIFIED = "Upgrade Shell Identified"; + public const string UPGRADE_SP_UPGRADED = "Upgrade Sp Upgraded"; + public const string UPGRADE_SP_PERFECTED = "Upgrade Sp Perfected"; + + /* WAREHOUSE */ + public const string WAREHOUSE_ITEM_PLACED = "Warehouse Item Placed"; + public const string WAREHOUSE_ITEM_WITHDRAWN = "Warehouse Item Withdrawn"; + + /* MINIGAMES */ + public const string MINIGAME_SCORE = "Minigame Score"; + public const string MINIGAME_REWARDS_CLAIMED = "Minigame Rewards Claimed"; + + /* CHAT */ + public const string CHAT = "Chat"; + + /* EXCHANGES */ + public const string EXCHANGES = "Exchanges"; + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Utils/EntityForAttribute.cs b/srcs/_plugins/Plugin.MongoLogs/Utils/EntityForAttribute.cs new file mode 100644 index 0000000..b92a8be --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Utils/EntityForAttribute.cs @@ -0,0 +1,12 @@ +using System; + +namespace Plugin.MongoLogs.Utils +{ + [AttributeUsage(AttributeTargets.Class)] + public class EntityForAttribute : Attribute + { + public EntityForAttribute(Type messageType) => MessageType = messageType; + + public Type MessageType { get; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Utils/GenericLogConsumer.cs b/srcs/_plugins/Plugin.MongoLogs/Utils/GenericLogConsumer.cs new file mode 100644 index 0000000..01f8ed2 --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Utils/GenericLogConsumer.cs @@ -0,0 +1,21 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; +using Plugin.MongoLogs.Services; +using Plugin.PlayerLogs; + +namespace Plugin.MongoLogs.Utils +{ + public class GenericLogConsumer : IMessageConsumer where T : IPlayerActionLogMessage + { + private readonly MongoLogsBackgroundService _mongoLogsBackgroundService; + + public GenericLogConsumer(MongoLogsBackgroundService mongoLogsBackgroundService) => _mongoLogsBackgroundService = mongoLogsBackgroundService; + + public Task HandleAsync(T notification, CancellationToken token) + { + _mongoLogsBackgroundService.AddLogsToInsertionQueue(notification); + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Utils/LogType.cs b/srcs/_plugins/Plugin.MongoLogs/Utils/LogType.cs new file mode 100644 index 0000000..88d00e1 --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Utils/LogType.cs @@ -0,0 +1,9 @@ +namespace Plugin.MongoLogs.Utils +{ + internal enum LogType + { + // LOGS + Connected, + Disconnected + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Utils/MongoDatabaseHelper.cs b/srcs/_plugins/Plugin.MongoLogs/Utils/MongoDatabaseHelper.cs new file mode 100644 index 0000000..572f20d --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Utils/MongoDatabaseHelper.cs @@ -0,0 +1,60 @@ +using System; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using Mapster; +using MongoDB.Bson; +using MongoDB.Driver; +using PhoenixLib.Logging; +using Plugin.MongoLogs.Entities; + +namespace Plugin.MongoLogs.Utils +{ + internal static class MongoDatabaseHelper + { + public static async Task InsertLogAsync(this IMongoDatabase database, T log) + { + IMongoCollection collection = database.GetCollection(MongoHelper.CollectionName); + + if (MongoHelper.EntityType == null) + { + await collection.InsertOneAsync(log.ToBsonDocument()); + return; + } + + var document = log.Adapt(typeof(T), MongoHelper.EntityType).ToBsonDocument(); + document.Remove("_t"); + await collection.InsertOneAsync(document); + } + + private static class MongoHelper + { + static MongoHelper() + { + Type entityType = typeof(MongoHelper).Assembly.GetTypes() + .FirstOrDefault(s => typeof(IPlayerLogEntity).IsAssignableFrom(s) && s.GetCustomAttribute()?.MessageType == typeof(T)); + + CollectionName = "NOT_HANDLED_PLZ_DEVS_WTF"; + if (entityType == null) + { + Log.Warn($"{typeof(T)} has no mappable entity"); + return; + } + + EntityType = entityType; + + string collectionNameAttribute = entityType.GetCustomAttribute()?.CollectionName; + if (collectionNameAttribute == null) + { + Log.Warn($"{typeof(T)} has no defined collection"); + return; + } + + CollectionName = collectionNameAttribute; + } + + internal static string CollectionName { get; } + internal static Type EntityType { get; } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.MongoLogs/Utils/MongoLogsConfiguration.cs b/srcs/_plugins/Plugin.MongoLogs/Utils/MongoLogsConfiguration.cs new file mode 100644 index 0000000..9b30880 --- /dev/null +++ b/srcs/_plugins/Plugin.MongoLogs/Utils/MongoLogsConfiguration.cs @@ -0,0 +1,33 @@ +using System; + +namespace Plugin.MongoLogs.Utils +{ + public class MongoLogsConfiguration + { + public MongoLogsConfiguration(string host, short port, string dbName, string username, string password) + { + Host = host; + Port = port; + DbName = dbName; + Username = username; + Password = password; + } + + public string Host { get; } + public short Port { get; } + public string DbName { get; } + public string Username { get; } + public string Password { get; } + + public static MongoLogsConfiguration FromEnv() => + new( + Environment.GetEnvironmentVariable("WINGSEMU_MONGO_HOST") ?? "localhost", + short.Parse(Environment.GetEnvironmentVariable("WINGSEMU_MONGO_PORT") ?? "27017"), + Environment.GetEnvironmentVariable("WINGSEMU_MONGO_DB") ?? "wingsemu_logs", + Environment.GetEnvironmentVariable("WINGSEMU_MONGO_USERNAME") ?? "root", + Environment.GetEnvironmentVariable("WINGSEMU_MONGO_PWD") ?? "root" + ); + + public override string ToString() => $"mongodb://{Username}:{Password}@{Host}:{Port}"; + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Core/GenericPlayerEventLogMessageFactory.cs b/srcs/_plugins/Plugin.PlayerLogs/Core/GenericPlayerEventLogMessageFactory.cs new file mode 100644 index 0000000..6ca38ae --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Core/GenericPlayerEventLogMessageFactory.cs @@ -0,0 +1,35 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using WingsEmu.Game._packetHandling; +using WingsEmu.Game.Managers; + +namespace Plugin.PlayerLogs.Core +{ + public class GenericPlayerEventLogMessageFactory : IPlayerEventLogMessageFactory + where TEvent : PlayerEvent + where TLogMessage : IPlayerActionLogMessage, new() + { + private readonly ILogMessageEnricher _enricher; + + public GenericPlayerEventLogMessageFactory(ILogMessageEnricher enricher) => _enricher = enricher; + + public TLogMessage CreateMessage(TEvent e) + { + var message = new TLogMessage + { + ChannelId = StaticServerManager.Instance.ChannelId, + CharacterId = e.Sender.PlayerEntity?.Id ?? e.Sender.Account.Id, + CharacterName = e.Sender.PlayerEntity?.Name ?? $"account-noauth-{e.Sender.Account.Name}", + CreatedAt = DateTime.UtcNow, + IpAddress = e.Sender.IpAddress + }; + + _enricher.Enrich(message, e); + + return message; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Core/GenericPlayerGameEventToLogProcessor.cs b/srcs/_plugins/Plugin.PlayerLogs/Core/GenericPlayerGameEventToLogProcessor.cs new file mode 100644 index 0000000..f45fd9f --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Core/GenericPlayerGameEventToLogProcessor.cs @@ -0,0 +1,30 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game._packetHandling; +using WingsEmu.Game.Logs; + +namespace Plugin.PlayerLogs.Core +{ + public class GenericPlayerGameEventToLogProcessor : IAsyncEventProcessor + where TEvent : PlayerEvent + where TLogMessage : IPlayerActionLogMessage + { + private readonly IPlayerEventLogMessageFactory _factory; + private readonly IPlayerLogManager _playerLogManager; + + public GenericPlayerGameEventToLogProcessor(IPlayerLogManager playerLogManager, IPlayerEventLogMessageFactory factory) + { + _playerLogManager = playerLogManager; + _factory = factory; + } + + public Task HandleAsync(TEvent e, CancellationToken cancellation) + { + _playerLogManager.AddLog(_factory.CreateMessage(e)); + + + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Core/ILogMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Core/ILogMessageEnricher.cs new file mode 100644 index 0000000..f3bef5b --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Core/ILogMessageEnricher.cs @@ -0,0 +1,7 @@ +namespace Plugin.PlayerLogs.Core +{ + public interface ILogMessageEnricher where TLogMessage : IPlayerActionLogMessage + { + void Enrich(TLogMessage message, TEvent e); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Core/IPlayerEventLogMessageFactory.cs b/srcs/_plugins/Plugin.PlayerLogs/Core/IPlayerEventLogMessageFactory.cs new file mode 100644 index 0000000..ae1b23d --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Core/IPlayerEventLogMessageFactory.cs @@ -0,0 +1,11 @@ +using WingsEmu.Game._packetHandling; + +namespace Plugin.PlayerLogs.Core +{ + public interface IPlayerEventLogMessageFactory + where TEvent : PlayerEvent + where TLogMessage : IPlayerActionLogMessage + { + TLogMessage CreateMessage(TEvent e); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Core/LogDependencyInjectionExtensions.cs b/srcs/_plugins/Plugin.PlayerLogs/Core/LogDependencyInjectionExtensions.cs new file mode 100644 index 0000000..db5b865 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Core/LogDependencyInjectionExtensions.cs @@ -0,0 +1,21 @@ +using Microsoft.Extensions.DependencyInjection; +using PhoenixLib.Events; +using PhoenixLib.ServiceBus.Extensions; +using WingsEmu.Game._packetHandling; + +namespace Plugin.PlayerLogs.Core +{ + public static class LogDependencyInjectionExtensions + { + public static void AddPlayerLog(this IServiceCollection services) + where TMessage : IPlayerActionLogMessage, new() + where TEvent : PlayerEvent + where TEnricher : class, ILogMessageEnricher + { + services.AddSingleton, GenericPlayerGameEventToLogProcessor>(); + services.AddSingleton, GenericPlayerEventLogMessageFactory>(); + services.AddSingleton, TEnricher>(); + services.AddMessagePublisher(); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Act4/LogAct4DungeonStartedMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Act4/LogAct4DungeonStartedMessageEnricher.cs new file mode 100644 index 0000000..0904167 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Act4/LogAct4DungeonStartedMessageEnricher.cs @@ -0,0 +1,15 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Act4; +using WingsEmu.Game.Act4.Event; + +namespace Plugin.PlayerLogs.Enrichers.Act4 +{ + public class LogAct4DungeonStartedMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogAct4DungeonStartedMessage message, Act4DungeonStartedEvent e) + { + message.DungeonType = e.DungeonType.ToString(); + message.FactionType = e.FactionType.ToString(); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Act4/LogAct4FamilyDungeonWonMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Act4/LogAct4FamilyDungeonWonMessageEnricher.cs new file mode 100644 index 0000000..37525a2 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Act4/LogAct4FamilyDungeonWonMessageEnricher.cs @@ -0,0 +1,17 @@ +using System.Linq; +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Act4; +using WingsEmu.Game.Act4.Event; + +namespace Plugin.PlayerLogs.Enrichers.Act4 +{ + public class LogAct4FamilyDungeonWonMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogAct4FamilyDungeonWonMessage message, Act4FamilyDungeonWonEvent e) + { + message.FamilyId = e.FamilyId; + message.DungeonType = e.DungeonType; + message.DungeonMembers = e.Members.Select(x => x.PlayerEntity.Id); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Act4/LogAct4PvpKillMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Act4/LogAct4PvpKillMessageEnricher.cs new file mode 100644 index 0000000..4e08f70 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Act4/LogAct4PvpKillMessageEnricher.cs @@ -0,0 +1,15 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Act4; +using WingsEmu.Game.Revival; + +namespace Plugin.PlayerLogs.Enrichers.Act4 +{ + public class LogAct4PvpKillMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogAct4PvpKillMessage message, Act4KillEvent e) + { + message.TargetId = e.TargetId; + message.KillerFaction = e.Sender.PlayerEntity.Faction.ToString(); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Bazaar/LogBazaarItemBoughtMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Bazaar/LogBazaarItemBoughtMessageEnricher.cs new file mode 100644 index 0000000..982d468 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Bazaar/LogBazaarItemBoughtMessageEnricher.cs @@ -0,0 +1,19 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Bazaar; +using WingsEmu.Game.Bazaar.Events; + +namespace Plugin.PlayerLogs.Enrichers.Bazaar +{ + public class LogBazaarItemBoughtMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogBazaarItemBoughtMessage message, BazaarItemBoughtEvent e) + { + message.BazaarItemId = e.BazaarItemId; + message.SellerId = e.SellerId; + message.SellerName = e.SellerName; + message.PricePerItem = e.PricePerItem; + message.BoughtItem = e.BoughtItem; + message.Amount = e.Amount; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Bazaar/LogBazaarItemExpiredMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Bazaar/LogBazaarItemExpiredMessageEnricher.cs new file mode 100644 index 0000000..d1a75cf --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Bazaar/LogBazaarItemExpiredMessageEnricher.cs @@ -0,0 +1,17 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Bazaar; +using WingsEmu.Game.Bazaar.Events; + +namespace Plugin.PlayerLogs.Enrichers.Bazaar +{ + public class LogBazaarItemExpiredMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogBazaarItemExpiredMessage message, BazaarItemExpiredEvent e) + { + message.Item = e.Item; + message.Price = e.Price; + message.Quantity = e.Quantity; + message.BazaarItemId = e.BazaarItemId; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Bazaar/LogBazaarItemInsertedMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Bazaar/LogBazaarItemInsertedMessageEnricher.cs new file mode 100644 index 0000000..7e35bd0 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Bazaar/LogBazaarItemInsertedMessageEnricher.cs @@ -0,0 +1,18 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Bazaar; +using WingsEmu.Game.Bazaar.Events; + +namespace Plugin.PlayerLogs.Enrichers.Bazaar +{ + public class LogBazaarItemInsertedMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogBazaarItemInsertedMessage message, BazaarItemInsertedEvent e) + { + message.Price = e.Price; + message.Quantity = e.Quantity; + message.ItemInstance = e.ItemInstance; + message.BazaarItemId = e.BazaarItemId; + message.Taxes = e.Taxes; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Bazaar/LogBazaarItemWithdrawnMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Bazaar/LogBazaarItemWithdrawnMessageEnricher.cs new file mode 100644 index 0000000..d1a675b --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Bazaar/LogBazaarItemWithdrawnMessageEnricher.cs @@ -0,0 +1,18 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Bazaar; +using WingsEmu.Game.Bazaar.Events; + +namespace Plugin.PlayerLogs.Enrichers.Bazaar +{ + public class LogBazaarItemWithdrawnMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogBazaarItemWithdrawnMessage message, BazaarItemWithdrawnEvent e) + { + message.Price = e.Price; + message.Quantity = e.Quantity; + message.ItemInstance = e.ItemInstance; + message.BazaarItemId = e.BazaarItemId; + message.ClaimedMoney = e.ClaimedMoney; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Family/LogFamilyCreatedMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Family/LogFamilyCreatedMessageEnricher.cs new file mode 100644 index 0000000..07cbee7 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Family/LogFamilyCreatedMessageEnricher.cs @@ -0,0 +1,18 @@ +using System.Linq; +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Family; +using WingsEmu.Game.Families.Event; +using WingsEmu.Packets.Enums.Families; + +namespace Plugin.PlayerLogs.Enrichers.Family +{ + public sealed class LogFamilyCreatedMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogFamilyCreatedMessage message, FamilyCreatedEvent e) + { + message.DeputiesIds = e.Sender.PlayerEntity.Family.Members.Where(s => s.Authority == FamilyAuthority.Deputy).Select(s => s.CharacterId).ToList(); + message.FamilyName = e.Sender.PlayerEntity.Family.Name; + message.FamilyId = e.Sender.PlayerEntity.Family.Id; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Family/LogFamilyDisbandedMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Family/LogFamilyDisbandedMessageEnricher.cs new file mode 100644 index 0000000..665316b --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Family/LogFamilyDisbandedMessageEnricher.cs @@ -0,0 +1,14 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Family; +using WingsEmu.Game.Families.Event; + +namespace Plugin.PlayerLogs.Enrichers.Family +{ + public class LogFamilyDisbandedMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogFamilyDisbandedMessage message, FamilyDisbandedEvent e) + { + message.FamilyId = e.FamilyId; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Family/LogFamilyInvitedMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Family/LogFamilyInvitedMessageEnricher.cs new file mode 100644 index 0000000..35ec522 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Family/LogFamilyInvitedMessageEnricher.cs @@ -0,0 +1,15 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Family; +using WingsEmu.Game.Families.Event; + +namespace Plugin.PlayerLogs.Enrichers.Family +{ + public class LogFamilyInvitedMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogFamilyInvitedMessage message, FamilyInvitedEvent e) + { + message.FamilyId = e.Sender.PlayerEntity.Family.Id; + message.TargetId = e.TargetId; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Family/LogFamilyJoinedMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Family/LogFamilyJoinedMessageEnricher.cs new file mode 100644 index 0000000..cb466e0 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Family/LogFamilyJoinedMessageEnricher.cs @@ -0,0 +1,15 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Family; +using WingsEmu.Game.Families.Event; + +namespace Plugin.PlayerLogs.Enrichers.Family +{ + public sealed class LogFamilyJoinedMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogFamilyJoinedMessage message, FamilyJoinedEvent e) + { + message.FamilyId = e.FamilyId; + message.InviterId = e.InviterId; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Family/LogFamilyKickedMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Family/LogFamilyKickedMessageEnricher.cs new file mode 100644 index 0000000..e269d80 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Family/LogFamilyKickedMessageEnricher.cs @@ -0,0 +1,15 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Family; +using WingsEmu.Game.Families.Event; + +namespace Plugin.PlayerLogs.Enrichers.Family +{ + public class LogFamilyKickedMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogFamilyKickedMessage message, FamilyKickedMemberEvent e) + { + message.FamilyId = e.Sender.PlayerEntity.Family.Id; + message.KickedMemberId = e.KickedMemberId; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Family/LogFamilyLeftMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Family/LogFamilyLeftMessageEnricher.cs new file mode 100644 index 0000000..85cfbfe --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Family/LogFamilyLeftMessageEnricher.cs @@ -0,0 +1,14 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Family; +using WingsEmu.Game.Families.Event; + +namespace Plugin.PlayerLogs.Enrichers.Family +{ + public sealed class LogFamilyLeftMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogFamilyLeftMessage message, FamilyLeftEvent e) + { + message.FamilyId = e.FamilyId; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Family/LogFamilyMessageMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Family/LogFamilyMessageMessageEnricher.cs new file mode 100644 index 0000000..5bbc552 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Family/LogFamilyMessageMessageEnricher.cs @@ -0,0 +1,16 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Family; +using WingsEmu.Game.Families.Event; + +namespace Plugin.PlayerLogs.Enrichers.Family +{ + public class LogFamilyMessageMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogFamilyMessageMessage message, FamilyMessageSentEvent e) + { + message.FamilyId = e.Sender.PlayerEntity.Family.Id; + message.Message = e.Message; + message.FamilyMessageType = e.MessageType.ToString(); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Family/LogFamilyUpgradeBoughtMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Family/LogFamilyUpgradeBoughtMessageEnricher.cs new file mode 100644 index 0000000..e1a1158 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Family/LogFamilyUpgradeBoughtMessageEnricher.cs @@ -0,0 +1,17 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Family; +using WingsEmu.Game.Families.Event; + +namespace Plugin.PlayerLogs.Enrichers.Family +{ + public class LogFamilyUpgradeBoughtMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogFamilyUpgradeBoughtMessage message, FamilyUpgradeBoughtEvent e) + { + message.FamilyId = e.FamilyId; + message.UpgradeValue = e.UpgradeValue; + message.UpgradeVnum = e.UpgradeVnum; + message.FamilyUpgradeType = e.FamilyUpgradeType.ToString(); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Family/LogFamilyWarehouseItemPlacedMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Family/LogFamilyWarehouseItemPlacedMessageEnricher.cs new file mode 100644 index 0000000..abc101f --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Family/LogFamilyWarehouseItemPlacedMessageEnricher.cs @@ -0,0 +1,17 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Family; +using WingsEmu.Game.Families.Event; + +namespace Plugin.PlayerLogs.Enrichers.Family +{ + public class LogFamilyWarehouseItemPlacedMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogFamilyWarehouseItemPlacedMessage message, FamilyWarehouseItemPlacedEvent e) + { + message.FamilyId = e.Sender.PlayerEntity.Family.Id; + message.Amount = e.Amount; + message.ItemInstance = e.ItemInstance; + message.DestinationSlot = e.DestinationSlot; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Family/LogFamilyWarehouseItemWithdrawnMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Family/LogFamilyWarehouseItemWithdrawnMessageEnricher.cs new file mode 100644 index 0000000..27695e4 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Family/LogFamilyWarehouseItemWithdrawnMessageEnricher.cs @@ -0,0 +1,17 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Family; +using WingsEmu.Game.Families.Event; + +namespace Plugin.PlayerLogs.Enrichers.Family +{ + public class LogFamilyWarehouseItemWithdrawnMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogFamilyWarehouseItemWithdrawnMessage message, FamilyWarehouseItemWithdrawnEvent e) + { + message.FamilyId = e.Sender.PlayerEntity.Family.Id; + message.Amount = e.Amount; + message.ItemInstance = e.ItemInstance; + message.FromSlot = e.FromSlot; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Inventory/LogInventoryItemDeletedMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Inventory/LogInventoryItemDeletedMessageEnricher.cs new file mode 100644 index 0000000..7fde94b --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Inventory/LogInventoryItemDeletedMessageEnricher.cs @@ -0,0 +1,15 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Inventory; +using WingsEmu.Game.Inventory.Event; + +namespace Plugin.PlayerLogs.Enrichers.Inventory +{ + public class LogInventoryItemDeletedMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogInventoryItemDeletedMessage message, InventoryItemDeletedEvent e) + { + message.ItemAmount = e.ItemAmount; + message.ItemInstance = e.ItemInstance; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Inventory/LogInventoryItemUsedMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Inventory/LogInventoryItemUsedMessageEnricher.cs new file mode 100644 index 0000000..3502f2e --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Inventory/LogInventoryItemUsedMessageEnricher.cs @@ -0,0 +1,14 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Inventory; +using WingsEmu.Game.Inventory.Event; + +namespace Plugin.PlayerLogs.Enrichers.Inventory +{ + public class LogInventoryItemUsedMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogInventoryItemUsedMessage message, InventoryItemUsedEvent e) + { + message.ItemVnum = e.ItemVnum; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Inventory/LogInventoryPickedUpItemMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Inventory/LogInventoryPickedUpItemMessageEnricher.cs new file mode 100644 index 0000000..c1db3e2 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Inventory/LogInventoryPickedUpItemMessageEnricher.cs @@ -0,0 +1,16 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Inventory; +using WingsEmu.Game.Inventory.Event; + +namespace Plugin.PlayerLogs.Enrichers.Inventory +{ + public class LogInventoryPickedUpItemMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogInventoryPickedUpItemMessage message, InventoryPickedUpItemEvent e) + { + message.Amount = e.Amount; + message.ItemVnum = e.ItemVnum; + message.Location = e.Location; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Inventory/LogInventoryPickedUpPlayerItemMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Inventory/LogInventoryPickedUpPlayerItemMessageEnricher.cs new file mode 100644 index 0000000..8f18ad3 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Inventory/LogInventoryPickedUpPlayerItemMessageEnricher.cs @@ -0,0 +1,16 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Inventory; +using WingsEmu.Game.Inventory.Event; + +namespace Plugin.PlayerLogs.Enrichers.Inventory +{ + public class LogInventoryPickedUpPlayerItemMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogInventoryPickedUpPlayerItemMessage message, InventoryPickedUpPlayerItemEvent e) + { + message.Amount = e.Amount; + message.Location = e.Location; + message.ItemInstance = e.ItemInstance; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/LevelUp/LogLevelUpCharacterMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/LevelUp/LogLevelUpCharacterMessageEnricher.cs new file mode 100644 index 0000000..9baedd0 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/LevelUp/LogLevelUpCharacterMessageEnricher.cs @@ -0,0 +1,26 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.LevelUp; +using WingsAPI.Game.Extensions.CharacterExtensions; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Packets.Enums; + +namespace Plugin.PlayerLogs.Enrichers.LevelUp +{ + public sealed class LogLevelUpCharacterMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogLevelUpCharacterMessage message, LevelUpEvent e) + { + message.LevelType = e.LevelType.ToString(); + message.Level = e.LevelType switch + { + LevelType.Level => e.Sender.PlayerEntity.Level, + LevelType.JobLevel => e.Sender.PlayerEntity.JobLevel, + LevelType.Fairy => e.Sender.PlayerEntity.Fairy.ElementRate, + LevelType.Heroic => e.Sender.PlayerEntity.HeroLevel, + LevelType.SpJobLevel => e.Sender.PlayerEntity.Specialist.SpLevel + }; + message.ItemVnum = e.ItemVnum; + message.Location = e.Sender.GetLocation(); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/LevelUp/LogLevelUpNosMateMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/LevelUp/LogLevelUpNosMateMessageEnricher.cs new file mode 100644 index 0000000..1e6829c --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/LevelUp/LogLevelUpNosMateMessageEnricher.cs @@ -0,0 +1,18 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.LevelUp; +using WingsEmu.Game.Characters.Events; + +namespace Plugin.PlayerLogs.Enrichers.LevelUp +{ + public class LogLevelUpNosMateMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogLevelUpNosMateMessage message, LevelUpMateEvent e) + { + message.Level = e.Level; + message.Location = e.Location; + message.LevelUpType = e.LevelUpType.ToString(); + message.ItemVnum = e.ItemVnum; + message.NosMateMonsterVnum = e.NosMateMonsterVnum; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/LogGMCommandExecutedMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/LogGMCommandExecutedMessageEnricher.cs new file mode 100644 index 0000000..6aea6bd --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/LogGMCommandExecutedMessageEnricher.cs @@ -0,0 +1,16 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages; +using WingsEmu.Game; + +namespace Plugin.PlayerLogs.Enrichers +{ + public class LogGmCommandExecutedMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogGmCommandExecutedMessage message, GmCommandEvent e) + { + message.PlayerAuthority = e.PlayerAuthority; + message.Command = e.Command; + message.CommandAuthority = e.CommandAuthority; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/LogStrangeBehaviorMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/LogStrangeBehaviorMessageEnricher.cs new file mode 100644 index 0000000..c6e95b3 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/LogStrangeBehaviorMessageEnricher.cs @@ -0,0 +1,15 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages; +using WingsEmu.Game.Characters.Events; + +namespace Plugin.PlayerLogs.Enrichers +{ + public class LogStrangeBehaviorMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogStrangeBehaviorMessage message, StrangeBehaviorEvent e) + { + message.SeverityType = e.Severity.ToString(); + message.Message = e.Reason; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Mail/LogMailClaimedMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Mail/LogMailClaimedMessageEnricher.cs new file mode 100644 index 0000000..0c13a94 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Mail/LogMailClaimedMessageEnricher.cs @@ -0,0 +1,16 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Mail; +using WingsEmu.Game.Mails.Events; + +namespace Plugin.PlayerLogs.Enrichers.Mail +{ + public class LogMailClaimedMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogMailClaimedMessage message, MailClaimedEvent e) + { + message.MailId = e.MailId; + message.SenderName = e.SenderName; + message.ItemInstance = e.ItemInstance; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Mail/LogMailRemovedMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Mail/LogMailRemovedMessageEnricher.cs new file mode 100644 index 0000000..75cd73d --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Mail/LogMailRemovedMessageEnricher.cs @@ -0,0 +1,16 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Mail; +using WingsEmu.Game.Mails.Events; + +namespace Plugin.PlayerLogs.Enrichers.Mail +{ + public class LogMailRemovedMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogMailRemovedMessage message, MailRemovedEvent e) + { + message.ItemInstance = e.ItemInstance; + message.MailId = e.MailId; + message.SenderName = e.SenderName; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Mail/LogNoteSentMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Mail/LogNoteSentMessageEnricher.cs new file mode 100644 index 0000000..6d67f37 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Mail/LogNoteSentMessageEnricher.cs @@ -0,0 +1,17 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Mail; +using WingsEmu.Game.Mails.Events; + +namespace Plugin.PlayerLogs.Enrichers.Mail +{ + public class LogNoteSentMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogNoteSentMessage message, NoteSentEvent e) + { + message.Message = e.Message; + message.NoteId = e.NoteId; + message.ReceiverName = e.ReceiverName; + message.Title = e.Title; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Miniland/LogMinigameRewardClaimedMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Miniland/LogMinigameRewardClaimedMessageEnricher.cs new file mode 100644 index 0000000..5f8a231 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Miniland/LogMinigameRewardClaimedMessageEnricher.cs @@ -0,0 +1,20 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Miniland; +using WingsEmu.Game.Miniland.Events; + +namespace Plugin.PlayerLogs.Enrichers.Miniland +{ + public class LogMinigameRewardClaimedMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogMinigameRewardClaimedMessage message, MinigameRewardClaimedEvent e) + { + message.OwnerId = e.OwnerId; + message.MinigameVnum = e.MinigameVnum; + message.MinigameType = e.MinigameType.ToString(); + message.RewardLevel = e.RewardLevel; + message.Coupon = e.Coupon; + message.ItemVnum = e.ItemVnum; + message.Amount = e.Amount; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Miniland/LogMinigameScoreMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Miniland/LogMinigameScoreMessageEnricher.cs new file mode 100644 index 0000000..2935c38 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Miniland/LogMinigameScoreMessageEnricher.cs @@ -0,0 +1,19 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Miniland; +using WingsEmu.Game.Miniland.Events; + +namespace Plugin.PlayerLogs.Enrichers.Miniland +{ + public class LogMinigameScoreMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogMinigameScoreMessage message, MinigameScoreLogEvent e) + { + message.OwnerId = e.OwnerId; + message.CompletionTime = e.CompletionTime; + message.MinigameVnum = e.MinigameVnum; + message.MinigameType = e.MinigameType.ToString(); + message.Score1 = e.Score1; + message.Score2 = e.Score2; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Miniland/LogWarehouseItemPlacedMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Miniland/LogWarehouseItemPlacedMessageEnricher.cs new file mode 100644 index 0000000..8177856 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Miniland/LogWarehouseItemPlacedMessageEnricher.cs @@ -0,0 +1,16 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Miniland; +using WingsEmu.Game.Warehouse.Events; + +namespace Plugin.PlayerLogs.Enrichers.Miniland +{ + public class LogWarehouseItemPlacedMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogWarehouseItemPlacedMessage message, WarehouseItemPlacedEvent e) + { + message.Amount = e.Amount; + message.DestinationSlot = e.DestinationSlot; + message.ItemInstance = e.ItemInstance; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Miniland/LogWarehouseItemWithdrawnMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Miniland/LogWarehouseItemWithdrawnMessageEnricher.cs new file mode 100644 index 0000000..1ce124d --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Miniland/LogWarehouseItemWithdrawnMessageEnricher.cs @@ -0,0 +1,16 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Miniland; +using WingsEmu.Game.Warehouse.Events; + +namespace Plugin.PlayerLogs.Enrichers.Miniland +{ + public class LogWarehouseItemWithdrawnMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogWarehouseItemWithdrawnMessage message, WarehouseItemWithdrawnEvent e) + { + message.Amount = e.Amount; + message.FromSlot = e.FromSlot; + message.ItemInstance = e.ItemInstance; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Npc/LogItemProducedMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Npc/LogItemProducedMessageEnricher.cs new file mode 100644 index 0000000..ad3197c --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Npc/LogItemProducedMessageEnricher.cs @@ -0,0 +1,15 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Npc; +using WingsEmu.Game.Npcs.Event; + +namespace Plugin.PlayerLogs.Enrichers.Npc +{ + public class LogItemProducedMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogItemProducedMessage message, ItemProducedEvent e) + { + message.ItemAmount = e.ItemAmount; + message.ItemInstance = e.ItemInstance; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Player/LogBoxOpenedMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Player/LogBoxOpenedMessageEnricher.cs new file mode 100644 index 0000000..daf9f59 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Player/LogBoxOpenedMessageEnricher.cs @@ -0,0 +1,15 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Player; +using WingsEmu.Game.Characters.Events; + +namespace Plugin.PlayerLogs.Enrichers.Player +{ + public class LogBoxOpenedMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogBoxOpenedMessage message, BoxOpenedEvent e) + { + message.Box = e.Box; + message.Rewards = e.Rewards; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Player/LogGroupInvitedMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Player/LogGroupInvitedMessageEnricher.cs new file mode 100644 index 0000000..4acbc05 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Player/LogGroupInvitedMessageEnricher.cs @@ -0,0 +1,15 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Player; +using WingsEmu.Game.Groups.Events; + +namespace Plugin.PlayerLogs.Enrichers.Player +{ + public class LogGroupInvitedMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogGroupInvitedMessage message, GroupInvitedEvent e) + { + message.GroupId = e.Sender.PlayerEntity.GetGroupId(); + message.TargetId = e.TargetId; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Player/LogPlayerChatMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Player/LogPlayerChatMessageEnricher.cs new file mode 100644 index 0000000..c696b3f --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Player/LogPlayerChatMessageEnricher.cs @@ -0,0 +1,16 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Player; +using WingsEmu.Game.Chat; + +namespace Plugin.PlayerLogs.Enrichers.Player +{ + public sealed class LogPlayerChatMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogPlayerChatMessage message, ChatGenericEvent e) + { + message.Message = e.Message; + message.ChatType = e.ChatType; + message.TargetCharacterId = e.TargetCharacterId; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Player/LogPlayerCommandExecutedMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Player/LogPlayerCommandExecutedMessageEnricher.cs new file mode 100644 index 0000000..c989fb0 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Player/LogPlayerCommandExecutedMessageEnricher.cs @@ -0,0 +1,14 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Player; +using WingsEmu.Game; + +namespace Plugin.PlayerLogs.Enrichers.Player +{ + public sealed class LogPlayerCommandExecutedMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogPlayerCommandExecutedMessage message, PlayerCommandEvent e) + { + message.Command = e.Command; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Player/LogPlayerDisconnectedMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Player/LogPlayerDisconnectedMessageEnricher.cs new file mode 100644 index 0000000..28723b8 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Player/LogPlayerDisconnectedMessageEnricher.cs @@ -0,0 +1,18 @@ +using System; +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Player; +using WingsEmu.Game.Characters.Events; + +namespace Plugin.PlayerLogs.Enrichers.Player +{ + public class LogPlayerDisconnectedMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogPlayerDisconnectedMessage message, CharacterDisconnectedEvent e) + { + message.HardwareId = e.Sender.HardwareId; + message.SessionEnd = DateTime.UtcNow; + message.SessionStart = e.Sender.PlayerEntity.GameStartDate; + message.MasterAccountId = e.Sender.Account.MasterAccountId.ToString(); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Player/LogPlayerExchangeMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Player/LogPlayerExchangeMessageEnricher.cs new file mode 100644 index 0000000..6f49356 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Player/LogPlayerExchangeMessageEnricher.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Player; +using WingsAPI.Data.Exchanges; +using WingsEmu.Game.Exchange.Event; + +namespace Plugin.PlayerLogs.Enrichers.Player +{ + public class LogPlayerExchangeMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogPlayerExchangeMessage log, ExchangeCompletedEvent e) + { + var senderItems = new List(); + var targetItems = new List(); + + for (byte i = 0; i < e.SenderItems.Count; i++) + { + senderItems.Add(new LogPlayerExchangeItemInfo + { + Slot = i, + Amount = e.SenderItems[i].Item2, + ItemInstance = e.SenderItems[i].Item1 + }); + } + + for (byte i = 0; i < e.TargetItems.Count; i++) + { + targetItems.Add(new LogPlayerExchangeItemInfo + { + Slot = i, + Amount = e.TargetItems[i].Item2, + ItemInstance = e.TargetItems[i].Item1 + }); + } + + log.Gold = e.SenderGold; + log.Items = senderItems; + log.BankGold = e.SenderBankGold; + log.TargetGold = e.TargetGold; + log.TargetBankGold = e.TargetBankGold; + log.TargetCharacterId = e.Target.PlayerEntity.Id; + log.TargetCharacterName = e.Target.PlayerEntity.Name; + log.TargetItems = targetItems; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Player/LogTradeRequestedMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Player/LogTradeRequestedMessageEnricher.cs new file mode 100644 index 0000000..1aaf814 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Player/LogTradeRequestedMessageEnricher.cs @@ -0,0 +1,14 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Player; +using WingsEmu.Game.Exchange.Event; + +namespace Plugin.PlayerLogs.Enrichers.Player +{ + public class LogTradeRequestedMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogTradeRequestedMessage message, TradeRequestedEvent e) + { + message.TargetId = e.TargetId; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Quest/LogQuestAbandonedMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Quest/LogQuestAbandonedMessageEnricher.cs new file mode 100644 index 0000000..c6e9c20 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Quest/LogQuestAbandonedMessageEnricher.cs @@ -0,0 +1,15 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Quest; +using WingsEmu.Game.Quests.Event; + +namespace Plugin.PlayerLogs.Enrichers.Quest +{ + public class LogQuestAbandonedMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogQuestAbandonedMessage message, QuestAbandonedEvent e) + { + message.QuestId = e.QuestId; + message.SlotType = e.QuestSlotType.ToString(); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Quest/LogQuestAddedMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Quest/LogQuestAddedMessageEnricher.cs new file mode 100644 index 0000000..7a53118 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Quest/LogQuestAddedMessageEnricher.cs @@ -0,0 +1,15 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Quest; +using WingsEmu.Game.Quests.Event; + +namespace Plugin.PlayerLogs.Enrichers.Quest +{ + public class LogQuestAddedMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogQuestAddedMessage message, QuestAddedEvent e) + { + message.QuestId = e.QuestId; + message.SlotType = e.QuestSlotType.ToString(); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Quest/LogQuestCompletedMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Quest/LogQuestCompletedMessageEnricher.cs new file mode 100644 index 0000000..e4bb8a6 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Quest/LogQuestCompletedMessageEnricher.cs @@ -0,0 +1,16 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Quest; +using WingsEmu.Game.Quests.Event; + +namespace Plugin.PlayerLogs.Enrichers.Quest +{ + public class LogQuestCompletedMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogQuestCompletedMessage message, QuestCompletedLogEvent e) + { + message.QuestId = e.CharacterQuest.QuestId; + message.SlotType = e.CharacterQuest.SlotType.ToString(); + message.Location = e.Location; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Quest/LogQuestObjectiveUpdatedMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Quest/LogQuestObjectiveUpdatedMessageEnricher.cs new file mode 100644 index 0000000..7bcef3b --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Quest/LogQuestObjectiveUpdatedMessageEnricher.cs @@ -0,0 +1,16 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Quest; +using WingsEmu.Game.Quests.Event; + +namespace Plugin.PlayerLogs.Enrichers.Quest +{ + public class LogQuestObjectiveUpdatedMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogQuestObjectiveUpdatedMessage message, QuestObjectiveUpdatedEvent e) + { + message.QuestId = e.CharacterQuest.QuestId; + message.SlotType = e.CharacterQuest.SlotType.ToString(); + message.UpdatedObjectivesAmount = e.CharacterQuest.ObjectiveAmount; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Raid/LogRaidAbandonedMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Raid/LogRaidAbandonedMessageEnricher.cs new file mode 100644 index 0000000..1e36dcb --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Raid/LogRaidAbandonedMessageEnricher.cs @@ -0,0 +1,14 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Raid; +using WingsEmu.Game.Raids.Events; + +namespace Plugin.PlayerLogs.Enrichers.Raid +{ + public class LogRaidAbandonedMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogRaidAbandonedMessage message, RaidAbandonedEvent e) + { + message.RaidId = e.RaidId; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Raid/LogRaidCreatedMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Raid/LogRaidCreatedMessageEnricher.cs new file mode 100644 index 0000000..ffeaa57 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Raid/LogRaidCreatedMessageEnricher.cs @@ -0,0 +1,17 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Raid; +using WingsEmu.Game.Raids; +using WingsEmu.Game.Raids.Events; + +namespace Plugin.PlayerLogs.Enrichers.Raid +{ + public class LogRaidCreatedMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogRaidCreatedMessage message, RaidCreatedEvent e) + { + RaidParty raidParty = e.Sender.PlayerEntity.Raid; + message.RaidId = raidParty.Id; + message.RaidType = raidParty.Type.ToString(); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Raid/LogRaidDiedMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Raid/LogRaidDiedMessageEnricher.cs new file mode 100644 index 0000000..76a2d12 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Raid/LogRaidDiedMessageEnricher.cs @@ -0,0 +1,15 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Raid; +using WingsEmu.Game.Raids.Events; + +namespace Plugin.PlayerLogs.Enrichers.Raid +{ + public class LogRaidDiedMessageEnricher : ILogMessageEnricher + + { + public void Enrich(LogRaidDiedMessage message, RaidDiedEvent e) + { + message.RaidId = e.Sender.PlayerEntity.Raid.Id; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Raid/LogRaidInvitedMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Raid/LogRaidInvitedMessageEnricher.cs new file mode 100644 index 0000000..c2de65d --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Raid/LogRaidInvitedMessageEnricher.cs @@ -0,0 +1,15 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Raid; +using WingsEmu.Game.Raids.Events; + +namespace Plugin.PlayerLogs.Enrichers.Raid +{ + public class LogRaidInvitedMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogRaidInvitedMessage message, RaidInvitedEvent e) + { + message.RaidId = e.Sender.PlayerEntity.Raid.Id; + message.TargetId = e.TargetId; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Raid/LogRaidJoinedMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Raid/LogRaidJoinedMessageEnricher.cs new file mode 100644 index 0000000..82848e2 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Raid/LogRaidJoinedMessageEnricher.cs @@ -0,0 +1,15 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Raid; +using WingsEmu.Game.Raids.Events; + +namespace Plugin.PlayerLogs.Enrichers.Raid +{ + public class LogRaidJoinedMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogRaidJoinedMessage message, RaidJoinedEvent e) + { + message.RaidJoinType = e.JoinType.ToString(); + message.RaidId = e.Sender.PlayerEntity.Raid.Id; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Raid/LogRaidLeftMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Raid/LogRaidLeftMessageEnricher.cs new file mode 100644 index 0000000..f570dc3 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Raid/LogRaidLeftMessageEnricher.cs @@ -0,0 +1,14 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Raid; +using WingsEmu.Game.Raids.Events; + +namespace Plugin.PlayerLogs.Enrichers.Raid +{ + public class LogRaidLeftMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogRaidLeftMessage message, RaidLeftEvent e) + { + message.RaidId = e.RaidId; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Raid/LogRaidLostMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Raid/LogRaidLostMessageEnricher.cs new file mode 100644 index 0000000..4424653 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Raid/LogRaidLostMessageEnricher.cs @@ -0,0 +1,15 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Raid; +using WingsEmu.Game.Raids.Events; + +namespace Plugin.PlayerLogs.Enrichers.Raid +{ + public class LogRaidLostMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogRaidLostMessage message, RaidLostEvent e) + { + message.RaidId = e.Sender.PlayerEntity.Raid.Id.ToString(); + message.RaidType = e.Sender.PlayerEntity.Raid.Type.ToString(); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Raid/LogRaidRevivedMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Raid/LogRaidRevivedMessageEnricher.cs new file mode 100644 index 0000000..be89b13 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Raid/LogRaidRevivedMessageEnricher.cs @@ -0,0 +1,15 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Raid; +using WingsEmu.Game.Raids.Events; + +namespace Plugin.PlayerLogs.Enrichers.Raid +{ + public class LogRaidRevivedMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogRaidRevivedMessage message, RaidRevivedEvent e) + { + message.RaidId = e.Sender.PlayerEntity.Raid.Id; + message.RestoredLife = e.RestoredLife; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Raid/LogRaidRewardReceivedMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Raid/LogRaidRewardReceivedMessageEnricher.cs new file mode 100644 index 0000000..aba68bf --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Raid/LogRaidRewardReceivedMessageEnricher.cs @@ -0,0 +1,15 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Raid; +using WingsEmu.Game.Raids.Events; + +namespace Plugin.PlayerLogs.Enrichers.Raid +{ + public class LogRaidRewardReceivedMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogRaidRewardReceivedMessage message, RaidRewardReceivedEvent e) + { + message.RaidId = e.Sender.PlayerEntity.Raid.Id; + message.BoxRarity = e.BoxRarity; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Raid/LogRaidStartedMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Raid/LogRaidStartedMessageEnricher.cs new file mode 100644 index 0000000..ee72cac --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Raid/LogRaidStartedMessageEnricher.cs @@ -0,0 +1,20 @@ +using System.Linq; +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Raid; +using WingsEmu.Game.Raids; +using WingsEmu.Game.Raids.Events; + +namespace Plugin.PlayerLogs.Enrichers.Raid +{ + public class LogRaidStartedMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogRaidStartedMessage message, RaidStartedEvent e) + { + RaidParty raidParty = e.Sender.PlayerEntity.Raid; + message.MembersIds = raidParty.Members.Select(x => x.Account.Id).ToArray(); + message.RaidId = raidParty.Id; + ; + message.RaidType = raidParty.Type.ToString(); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Raid/LogRaidSwitchButtonToggledEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Raid/LogRaidSwitchButtonToggledEnricher.cs new file mode 100644 index 0000000..e78a1c4 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Raid/LogRaidSwitchButtonToggledEnricher.cs @@ -0,0 +1,20 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Raid; +using WingsEmu.Game.Raids.Events; + +namespace Plugin.PlayerLogs.Enrichers.Raid +{ + public class LogRaidSwitchButtonToggledEnricher : ILogMessageEnricher + { + public void Enrich(LogRaidSwitchButtonToggledMessage message, RaidSwitchButtonToggledEvent e) + { + if (!e.Sender.PlayerEntity.IsInRaidParty) + { + return; + } + + message.RaidId = e.Sender.PlayerEntity.Raid.Id; + message.LeverId = e.LeverId; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Raid/LogRaidTargetKilledMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Raid/LogRaidTargetKilledMessageEnricher.cs new file mode 100644 index 0000000..2545309 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Raid/LogRaidTargetKilledMessageEnricher.cs @@ -0,0 +1,15 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Raid; +using WingsEmu.Game.Raids.Events; + +namespace Plugin.PlayerLogs.Enrichers.Raid +{ + public class LogRaidTargetKilledMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogRaidTargetKilledMessage message, RaidTargetKilledEvent e) + { + message.RaidId = e.Sender.PlayerEntity.Raid.Id; + message.DamagerCharactersIds = e.DamagerCharactersIds; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Raid/LogRaidWonMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Raid/LogRaidWonMessageEnricher.cs new file mode 100644 index 0000000..e0e0135 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Raid/LogRaidWonMessageEnricher.cs @@ -0,0 +1,15 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Raid; +using WingsEmu.Game.Raids.Events; + +namespace Plugin.PlayerLogs.Enrichers.Raid +{ + public class LogRaidWonMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogRaidWonMessage message, RaidWonEvent e) + { + message.RaidId = e.Sender.PlayerEntity.Raid.Id.ToString(); + message.RaidType = e.Sender.PlayerEntity.Raid.Type.ToString(); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/RainbowBattle/LogRainbowBattleFrozenMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/RainbowBattle/LogRainbowBattleFrozenMessageEnricher.cs new file mode 100644 index 0000000..c783ce1 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/RainbowBattle/LogRainbowBattleFrozenMessageEnricher.cs @@ -0,0 +1,16 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.RainbowBattle; +using WingsEmu.Game.RainbowBattle.Event; + +namespace Plugin.PlayerLogs.Enrichers.RainbowBattle +{ + public class LogRainbowBattleFrozenMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogRainbowBattleFrozenMessage message, RainbowBattleFrozenEvent e) + { + message.RainbowBattleId = e.Id; + message.Killer = e.Killer; + message.Killed = e.Killed; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/RainbowBattle/LogRainbowBattleJoinMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/RainbowBattle/LogRainbowBattleJoinMessageEnricher.cs new file mode 100644 index 0000000..9724db7 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/RainbowBattle/LogRainbowBattleJoinMessageEnricher.cs @@ -0,0 +1,13 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.RainbowBattle; +using WingsEmu.Game.RainbowBattle.Event; + +namespace Plugin.PlayerLogs.Enrichers.RainbowBattle +{ + public class LogRainbowBattleJoinMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogRainbowBattleJoinMessage message, RainbowBattleJoinEvent e) + { + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/RainbowBattle/LogRainbowBattleLoseMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/RainbowBattle/LogRainbowBattleLoseMessageEnricher.cs new file mode 100644 index 0000000..5d9560d --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/RainbowBattle/LogRainbowBattleLoseMessageEnricher.cs @@ -0,0 +1,15 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.RainbowBattle; +using WingsEmu.Game.RainbowBattle.Event; + +namespace Plugin.PlayerLogs.Enrichers.RainbowBattle +{ + public class LogRainbowBattleLoseMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogRainbowBattleLoseMessage message, RainbowBattleLoseEvent e) + { + message.RainbowBattleId = e.Id; + message.Players = e.Players; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/RainbowBattle/LogRainbowBattleTieMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/RainbowBattle/LogRainbowBattleTieMessageEnricher.cs new file mode 100644 index 0000000..5758f65 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/RainbowBattle/LogRainbowBattleTieMessageEnricher.cs @@ -0,0 +1,15 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.RainbowBattle; +using WingsEmu.Game.RainbowBattle.Event; + +namespace Plugin.PlayerLogs.Enrichers.RainbowBattle +{ + public class LogRainbowBattleTieMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogRainbowBattleTieMessage message, RainbowBattleTieEvent e) + { + message.BlueTeam = e.BlueTeam; + message.RedTeam = e.RedTeam; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/RainbowBattle/LogRainbowBattleWonMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/RainbowBattle/LogRainbowBattleWonMessageEnricher.cs new file mode 100644 index 0000000..7f8e791 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/RainbowBattle/LogRainbowBattleWonMessageEnricher.cs @@ -0,0 +1,14 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.RainbowBattle; +using WingsEmu.Game.RainbowBattle.Event; + +namespace Plugin.PlayerLogs.Enrichers.RainbowBattle +{ + public class LogRainbowBattleWonMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogRainbowBattleWonMessage message, RainbowBattleWonEvent e) + { + message.Players = e.Players; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Shop/LogShopClosedMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Shop/LogShopClosedMessageEnricher.cs new file mode 100644 index 0000000..4dc4ba5 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Shop/LogShopClosedMessageEnricher.cs @@ -0,0 +1,15 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Shop; +using WingsAPI.Game.Extensions.CharacterExtensions; +using WingsEmu.Game.Shops.Event; + +namespace Plugin.PlayerLogs.Enrichers.Shop +{ + public class LogShopClosedMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogShopClosedMessage message, ShopClosedEvent e) + { + message.Location = e.Sender.GetLocation(); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Shop/LogShopNpcBoughtItemMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Shop/LogShopNpcBoughtItemMessageEnricher.cs new file mode 100644 index 0000000..475798b --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Shop/LogShopNpcBoughtItemMessageEnricher.cs @@ -0,0 +1,18 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Shop; +using WingsEmu.Game.Shops.Event; + +namespace Plugin.PlayerLogs.Enrichers.Shop +{ + public class LogShopNpcBoughtItemMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogShopNpcBoughtItemMessage message, ShopNpcBoughtItemEvent e) + { + message.TotalPrice = e.TotalPrice; + message.Quantity = e.Quantity; + message.SellerId = e.SellerId; + message.ItemInstance = e.ItemInstance; + message.CurrencyType = e.CurrencyType.ToString(); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Shop/LogShopNpcSoldItemMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Shop/LogShopNpcSoldItemMessageEnricher.cs new file mode 100644 index 0000000..d4fb440 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Shop/LogShopNpcSoldItemMessageEnricher.cs @@ -0,0 +1,16 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Shop; +using WingsEmu.Game.Shops.Event; + +namespace Plugin.PlayerLogs.Enrichers.Shop +{ + public class LogShopNpcSoldItemMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogShopNpcSoldItemMessage message, ShopNpcSoldItemEvent e) + { + message.Amount = e.Amount; + message.PricePerItem = e.PricePerItem; + message.ItemInstance = e.ItemInstance; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Shop/LogShopOpenedMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Shop/LogShopOpenedMessageEnricher.cs new file mode 100644 index 0000000..b419856 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Shop/LogShopOpenedMessageEnricher.cs @@ -0,0 +1,16 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Shop; +using WingsAPI.Game.Extensions.CharacterExtensions; +using WingsEmu.Game.Shops.Event; + +namespace Plugin.PlayerLogs.Enrichers.Shop +{ + public class LogShopOpenedMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogShopOpenedMessage message, ShopOpenedEvent e) + { + message.Location = e.Sender.GetLocation(); + message.ShopName = e.ShopName; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Shop/LogShopPlayerBoughtItemMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Shop/LogShopPlayerBoughtItemMessageEnricher.cs new file mode 100644 index 0000000..1ff21d7 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Shop/LogShopPlayerBoughtItemMessageEnricher.cs @@ -0,0 +1,18 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Shop; +using WingsEmu.Game.Shops.Event; + +namespace Plugin.PlayerLogs.Enrichers.Shop +{ + public class LogShopPlayerBoughtItemMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogShopPlayerBoughtItemMessage message, ShopPlayerBoughtItemEvent e) + { + message.TotalPrice = e.TotalPrice; + message.Quantity = e.Quantity; + message.SellerId = e.SellerId; + message.SellerName = e.SellerName; + message.ItemInstance = e.ItemInstance; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Shop/LogShopSkillBoughtMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Shop/LogShopSkillBoughtMessageEnricher.cs new file mode 100644 index 0000000..1dfb20d --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Shop/LogShopSkillBoughtMessageEnricher.cs @@ -0,0 +1,14 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Shop; +using WingsEmu.Game.Shops.Event; + +namespace Plugin.PlayerLogs.Enrichers.Shop +{ + public class LogShopSkillBoughtMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogShopSkillBoughtMessage boughtMessage, ShopSkillBoughtEvent e) + { + boughtMessage.SkillVnum = e.SkillVnum; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Shop/LogShopSkillSoldMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Shop/LogShopSkillSoldMessageEnricher.cs new file mode 100644 index 0000000..3563d67 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Shop/LogShopSkillSoldMessageEnricher.cs @@ -0,0 +1,14 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Shop; +using WingsEmu.Game.Shops.Event; + +namespace Plugin.PlayerLogs.Enrichers.Shop +{ + public class LogShopSkillSoldMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogShopSkillSoldMessage message, ShopSkillSoldEvent e) + { + message.SkillVnum = e.SkillVnum; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Upgrade/LogCellonUpgradedMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Upgrade/LogCellonUpgradedMessageEnricher.cs new file mode 100644 index 0000000..3d1843d --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Upgrade/LogCellonUpgradedMessageEnricher.cs @@ -0,0 +1,16 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Upgrade; +using WingsEmu.Game.Characters.Events; + +namespace Plugin.PlayerLogs.Enrichers.Upgrade +{ + public class LogCellonUpgradedMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogCellonUpgradedMessage message, CellonUpgradedEvent e) + { + message.Item = e.Item; + message.CellonVnum = e.CellonVnum; + message.Succeed = e.Succeed; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Upgrade/LogItemGambledMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Upgrade/LogItemGambledMessageEnricher.cs new file mode 100644 index 0000000..f5775b9 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Upgrade/LogItemGambledMessageEnricher.cs @@ -0,0 +1,20 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Upgrade; +using WingsEmu.Game.Characters.Events; + +namespace Plugin.PlayerLogs.Enrichers.Upgrade +{ + public class LogItemGambledMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogItemGambledMessage message, ItemGambledEvent e) + { + message.ItemVnum = e.ItemVnum; + message.Mode = e.Mode.ToString(); + message.Protection = e.Protection.ToString(); + message.Amulet = e.Amulet; + message.Succeed = e.Succeed; + message.OriginalRarity = e.OriginalRarity; + message.FinalRarity = e.FinalRarity; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Upgrade/LogItemSummedMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Upgrade/LogItemSummedMessageEnricher.cs new file mode 100644 index 0000000..11e25ca --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Upgrade/LogItemSummedMessageEnricher.cs @@ -0,0 +1,17 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Upgrade; +using WingsEmu.Game.Characters.Events; + +namespace Plugin.PlayerLogs.Enrichers.Upgrade +{ + public class LogItemSummedMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogItemSummedMessage message, ItemSummedEvent e) + { + message.LeftItem = e.LeftItem; + message.RightItem = e.RightItem; + message.Succeed = e.Succeed; + message.SumLevel = e.SumLevel; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Upgrade/LogItemUpgradedMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Upgrade/LogItemUpgradedMessageEnricher.cs new file mode 100644 index 0000000..f4e9f6a --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Upgrade/LogItemUpgradedMessageEnricher.cs @@ -0,0 +1,20 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Upgrade; +using WingsEmu.Game.Characters.Events; + +namespace Plugin.PlayerLogs.Enrichers.Upgrade +{ + public class LogItemUpgradedMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogItemUpgradedMessage message, ItemUpgradedEvent e) + { + message.Item = e.Item; + message.Mode = e.Mode.ToString(); + message.Protection = e.Protection.ToString(); + message.HasAmulet = e.HasAmulet; + message.OriginalUpgrade = e.OriginalUpgrade; + message.Result = e.Result.ToString(); + message.TotalPrice = e.TotalPrice; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Upgrade/LogShellIdentifiedMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Upgrade/LogShellIdentifiedMessageEnricher.cs new file mode 100644 index 0000000..c6cb95d --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Upgrade/LogShellIdentifiedMessageEnricher.cs @@ -0,0 +1,14 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Upgrade; +using WingsEmu.Game.Characters.Events; + +namespace Plugin.PlayerLogs.Enrichers.Upgrade +{ + public class LogShellIdentifiedMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogShellIdentifiedMessage message, ShellIdentifiedEvent e) + { + message.Shell = e.Shell; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Upgrade/LogSpPerfectedMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Upgrade/LogSpPerfectedMessageEnricher.cs new file mode 100644 index 0000000..4330ff9 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Upgrade/LogSpPerfectedMessageEnricher.cs @@ -0,0 +1,16 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Upgrade; +using WingsEmu.Game.Characters.Events; + +namespace Plugin.PlayerLogs.Enrichers.Upgrade +{ + public class LogSpPerfectedMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogSpPerfectedMessage message, SpPerfectedEvent e) + { + message.Success = e.Success; + message.Sp = e.Sp; + message.SpPerfectionLevel = e.SpPerfectionLevel; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Upgrade/LogSpUpgradedMessageEnricher.cs b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Upgrade/LogSpUpgradedMessageEnricher.cs new file mode 100644 index 0000000..6495a6e --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Enrichers/Upgrade/LogSpUpgradedMessageEnricher.cs @@ -0,0 +1,18 @@ +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Messages.Upgrade; +using WingsEmu.Game.Characters.Events; + +namespace Plugin.PlayerLogs.Enrichers.Upgrade +{ + public class LogSpUpgradedMessageEnricher : ILogMessageEnricher + { + public void Enrich(LogSpUpgradedMessage message, SpUpgradedEvent e) + { + message.Sp = e.Sp; + message.Mode = e.UpgradeMode.ToString(); + message.Result = e.UpgradeResult.ToString(); + message.OriginalUpgrade = e.OriginalUpgrade; + message.IsProtected = e.IsProtected; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/IPlayerActionLogMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/IPlayerActionLogMessage.cs new file mode 100644 index 0000000..706b437 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/IPlayerActionLogMessage.cs @@ -0,0 +1,9 @@ +using PhoenixLib.ServiceBus; +using WingsEmu.Game.Logs; + +namespace Plugin.PlayerLogs +{ + public interface IPlayerActionLogMessage : IMessage, IPlayerActionLog + { + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Act4/LogAct4DungeonStartedMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Act4/LogAct4DungeonStartedMessage.cs new file mode 100644 index 0000000..a1b9c0d --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Act4/LogAct4DungeonStartedMessage.cs @@ -0,0 +1,17 @@ +using System; +using PhoenixLib.ServiceBus.Routing; + +namespace Plugin.PlayerLogs.Messages.Act4 +{ + [MessageType("logs.act4.dungeon-started")] + public class LogAct4DungeonStartedMessage : IPlayerActionLogMessage + { + public string FactionType { get; set; } + public string DungeonType { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Act4/LogAct4FamilyDungeonWonMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Act4/LogAct4FamilyDungeonWonMessage.cs new file mode 100644 index 0000000..68eab25 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Act4/LogAct4FamilyDungeonWonMessage.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using PhoenixLib.ServiceBus.Routing; +using WingsEmu.Game.Act4; + +namespace Plugin.PlayerLogs.Messages.Act4 +{ + [MessageType("log.act4.dungeon-won")] + public class LogAct4FamilyDungeonWonMessage : IPlayerActionLogMessage + { + public long FamilyId { get; set; } + public DungeonType DungeonType { get; set; } + public IEnumerable DungeonMembers { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Act4/LogAct4PvpKillMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Act4/LogAct4PvpKillMessage.cs new file mode 100644 index 0000000..35fbf69 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Act4/LogAct4PvpKillMessage.cs @@ -0,0 +1,17 @@ +using System; +using PhoenixLib.ServiceBus.Routing; + +namespace Plugin.PlayerLogs.Messages.Act4 +{ + [MessageType("logs.act4.pvp-kill")] + public class LogAct4PvpKillMessage : IPlayerActionLogMessage + { + public long TargetId { get; set; } + public string KillerFaction { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Bazaar/LogBazaarItemBoughtMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Bazaar/LogBazaarItemBoughtMessage.cs new file mode 100644 index 0000000..e95e097 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Bazaar/LogBazaarItemBoughtMessage.cs @@ -0,0 +1,22 @@ +using System; +using PhoenixLib.ServiceBus.Routing; +using WingsEmu.DTOs.Items; + +namespace Plugin.PlayerLogs.Messages.Bazaar +{ + [MessageType("logs.bazaar.boughtitems")] + public class LogBazaarItemBoughtMessage : IPlayerActionLogMessage + { + public long BazaarItemId { get; set; } + public long SellerId { get; set; } + public string SellerName { get; set; } + public long PricePerItem { get; set; } + public int Amount { get; set; } + public ItemInstanceDTO BoughtItem { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Bazaar/LogBazaarItemExpiredMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Bazaar/LogBazaarItemExpiredMessage.cs new file mode 100644 index 0000000..8675c3b --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Bazaar/LogBazaarItemExpiredMessage.cs @@ -0,0 +1,20 @@ +using System; +using PhoenixLib.ServiceBus.Routing; +using WingsEmu.DTOs.Items; + +namespace Plugin.PlayerLogs.Messages.Bazaar +{ + [MessageType("logs.bazaar.itemexpired")] + public class LogBazaarItemExpiredMessage : IPlayerActionLogMessage + { + public long BazaarItemId { get; set; } + public long Price { get; set; } + public int Quantity { get; set; } + public ItemInstanceDTO Item { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Bazaar/LogBazaarItemInsertedMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Bazaar/LogBazaarItemInsertedMessage.cs new file mode 100644 index 0000000..e6bbbad --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Bazaar/LogBazaarItemInsertedMessage.cs @@ -0,0 +1,21 @@ +using System; +using PhoenixLib.ServiceBus.Routing; +using WingsEmu.DTOs.Items; + +namespace Plugin.PlayerLogs.Messages.Bazaar +{ + [MessageType("logs.bazaar.iteminserted")] + public class LogBazaarItemInsertedMessage : IPlayerActionLogMessage + { + public long BazaarItemId { get; set; } + public long Price { get; set; } + public int Quantity { get; set; } + public long Taxes { get; set; } + public ItemInstanceDTO ItemInstance { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Bazaar/LogBazaarItemWithdrawnMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Bazaar/LogBazaarItemWithdrawnMessage.cs new file mode 100644 index 0000000..37e30e1 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Bazaar/LogBazaarItemWithdrawnMessage.cs @@ -0,0 +1,21 @@ +using System; +using PhoenixLib.ServiceBus.Routing; +using WingsEmu.DTOs.Items; + +namespace Plugin.PlayerLogs.Messages.Bazaar +{ + [MessageType("logs.bazaar.itemwithdrawn")] + public class LogBazaarItemWithdrawnMessage : IPlayerActionLogMessage + { + public long BazaarItemId { get; set; } + public long Price { get; set; } + public int Quantity { get; set; } + public ItemInstanceDTO ItemInstance { get; set; } + public long ClaimedMoney { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Family/LogFamilyCreatedMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Family/LogFamilyCreatedMessage.cs new file mode 100644 index 0000000..12480e1 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Family/LogFamilyCreatedMessage.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using PhoenixLib.ServiceBus.Routing; + +namespace Plugin.PlayerLogs.Messages.Family +{ + [MessageType("logs.family.created")] + public class LogFamilyCreatedMessage : IPlayerActionLogMessage + { + public long FamilyId { get; set; } + public string FamilyName { get; set; } + public List DeputiesIds { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Family/LogFamilyDisbandedMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Family/LogFamilyDisbandedMessage.cs new file mode 100644 index 0000000..a7251ab --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Family/LogFamilyDisbandedMessage.cs @@ -0,0 +1,16 @@ +using System; +using PhoenixLib.ServiceBus.Routing; + +namespace Plugin.PlayerLogs.Messages.Family +{ + [MessageType("logs.family.disbanded")] + public class LogFamilyDisbandedMessage : IPlayerActionLogMessage + { + public long FamilyId { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Family/LogFamilyInvitedMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Family/LogFamilyInvitedMessage.cs new file mode 100644 index 0000000..761c7a8 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Family/LogFamilyInvitedMessage.cs @@ -0,0 +1,17 @@ +using System; +using PhoenixLib.ServiceBus.Routing; + +namespace Plugin.PlayerLogs.Messages.Family +{ + [MessageType("logs.family.invited")] + public class LogFamilyInvitedMessage : IPlayerActionLogMessage + { + public long FamilyId { get; set; } + public long TargetId { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Family/LogFamilyJoinedMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Family/LogFamilyJoinedMessage.cs new file mode 100644 index 0000000..866210b --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Family/LogFamilyJoinedMessage.cs @@ -0,0 +1,17 @@ +using System; +using PhoenixLib.ServiceBus.Routing; + +namespace Plugin.PlayerLogs.Messages.Family +{ + [MessageType("logs.family.joined")] + public class LogFamilyJoinedMessage : IPlayerActionLogMessage + { + public long FamilyId { get; set; } + public long InviterId { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Family/LogFamilyKickedMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Family/LogFamilyKickedMessage.cs new file mode 100644 index 0000000..721b1b2 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Family/LogFamilyKickedMessage.cs @@ -0,0 +1,18 @@ +using System; +using PhoenixLib.ServiceBus.Routing; + +namespace Plugin.PlayerLogs.Messages.Family +{ + [MessageType("logs.family.kicked")] + public class LogFamilyKickedMessage : IPlayerActionLogMessage + { + public long FamilyId { get; set; } + public long KickedMemberId { get; set; } + public string KickedMemberName { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Family/LogFamilyLeftMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Family/LogFamilyLeftMessage.cs new file mode 100644 index 0000000..23d9753 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Family/LogFamilyLeftMessage.cs @@ -0,0 +1,16 @@ +using System; +using PhoenixLib.ServiceBus.Routing; + +namespace Plugin.PlayerLogs.Messages.Family +{ + [MessageType("logs.family.left")] + public class LogFamilyLeftMessage : IPlayerActionLogMessage + { + public long FamilyId { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Family/LogFamilyMessageMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Family/LogFamilyMessageMessage.cs new file mode 100644 index 0000000..5a2dcd7 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Family/LogFamilyMessageMessage.cs @@ -0,0 +1,18 @@ +using System; +using PhoenixLib.ServiceBus.Routing; + +namespace Plugin.PlayerLogs.Messages.Family +{ + [MessageType("logs.family.message")] + public class LogFamilyMessageMessage : IPlayerActionLogMessage + { + public string FamilyMessageType { get; set; } + public long FamilyId { get; set; } + public string Message { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Family/LogFamilyUpgradeBoughtMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Family/LogFamilyUpgradeBoughtMessage.cs new file mode 100644 index 0000000..a6df616 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Family/LogFamilyUpgradeBoughtMessage.cs @@ -0,0 +1,19 @@ +using System; +using PhoenixLib.ServiceBus.Routing; + +namespace Plugin.PlayerLogs.Messages.Family +{ + [MessageType("logs.family.upgrade-bought")] + public class LogFamilyUpgradeBoughtMessage : IPlayerActionLogMessage + { + public long FamilyId { get; set; } + public int UpgradeVnum { get; set; } + public string FamilyUpgradeType { get; set; } + public short UpgradeValue { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Family/LogFamilyWarehouseItemPlacedMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Family/LogFamilyWarehouseItemPlacedMessage.cs new file mode 100644 index 0000000..a0ca51b --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Family/LogFamilyWarehouseItemPlacedMessage.cs @@ -0,0 +1,20 @@ +using System; +using PhoenixLib.ServiceBus.Routing; +using WingsEmu.DTOs.Items; + +namespace Plugin.PlayerLogs.Messages.Family +{ + [MessageType("logs.family.warehouse-item-placed")] + public class LogFamilyWarehouseItemPlacedMessage : IPlayerActionLogMessage + { + public long FamilyId { get; set; } + public ItemInstanceDTO ItemInstance { get; set; } + public int Amount { get; set; } + public short DestinationSlot { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Family/LogFamilyWarehouseItemWithdrawnMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Family/LogFamilyWarehouseItemWithdrawnMessage.cs new file mode 100644 index 0000000..7eaab71 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Family/LogFamilyWarehouseItemWithdrawnMessage.cs @@ -0,0 +1,20 @@ +using System; +using PhoenixLib.ServiceBus.Routing; +using WingsEmu.DTOs.Items; + +namespace Plugin.PlayerLogs.Messages.Family +{ + [MessageType("logs.family.warehouse-item-withdrawn")] + public class LogFamilyWarehouseItemWithdrawnMessage : IPlayerActionLogMessage + { + public long FamilyId { get; set; } + public ItemInstanceDTO ItemInstance { get; set; } + public int Amount { get; set; } + public short FromSlot { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Inventory/LogInventoryItemDeletedMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Inventory/LogInventoryItemDeletedMessage.cs new file mode 100644 index 0000000..447ab27 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Inventory/LogInventoryItemDeletedMessage.cs @@ -0,0 +1,18 @@ +using System; +using PhoenixLib.ServiceBus.Routing; +using WingsEmu.DTOs.Items; + +namespace Plugin.PlayerLogs.Messages.Inventory +{ + [MessageType("logs.inventory.item-deleted")] + public class LogInventoryItemDeletedMessage : IPlayerActionLogMessage + { + public ItemInstanceDTO ItemInstance { get; set; } + public int ItemAmount { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Inventory/LogInventoryItemUsedMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Inventory/LogInventoryItemUsedMessage.cs new file mode 100644 index 0000000..8dbecd8 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Inventory/LogInventoryItemUsedMessage.cs @@ -0,0 +1,16 @@ +using System; +using PhoenixLib.ServiceBus.Routing; + +namespace Plugin.PlayerLogs.Messages.Inventory +{ + [MessageType("logs.inventory.item-used")] + public class LogInventoryItemUsedMessage : IPlayerActionLogMessage + { + public int ItemVnum { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Inventory/LogInventoryPickedUpItemMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Inventory/LogInventoryPickedUpItemMessage.cs new file mode 100644 index 0000000..4ba6419 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Inventory/LogInventoryPickedUpItemMessage.cs @@ -0,0 +1,19 @@ +using System; +using PhoenixLib.ServiceBus.Routing; +using WingsEmu.Game.Helpers; + +namespace Plugin.PlayerLogs.Messages.Inventory +{ + [MessageType("logs.inventory.pickedupitem")] + public class LogInventoryPickedUpItemMessage : IPlayerActionLogMessage + { + public int ItemVnum { get; set; } + public int Amount { get; set; } + public Location Location { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Inventory/LogInventoryPickedUpPlayerItemMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Inventory/LogInventoryPickedUpPlayerItemMessage.cs new file mode 100644 index 0000000..ab3ba6e --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Inventory/LogInventoryPickedUpPlayerItemMessage.cs @@ -0,0 +1,20 @@ +using System; +using PhoenixLib.ServiceBus.Routing; +using WingsEmu.DTOs.Items; +using WingsEmu.Game.Helpers; + +namespace Plugin.PlayerLogs.Messages.Inventory +{ + [MessageType("logs.inventory.picked-up-player-item")] + public class LogInventoryPickedUpPlayerItemMessage : IPlayerActionLogMessage + { + public ItemInstanceDTO ItemInstance { get; set; } + public int Amount { get; set; } + public Location Location { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/LevelUp/LogLevelUpCharacterMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/LevelUp/LogLevelUpCharacterMessage.cs new file mode 100644 index 0000000..8fa21b5 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/LevelUp/LogLevelUpCharacterMessage.cs @@ -0,0 +1,20 @@ +using System; +using PhoenixLib.ServiceBus.Routing; +using WingsEmu.Game.Helpers; + +namespace Plugin.PlayerLogs.Messages.LevelUp +{ + [MessageType("logs.levelup.character")] + public class LogLevelUpCharacterMessage : IPlayerActionLogMessage + { + public int Level { get; set; } + public string LevelType { get; set; } + public Location Location { get; set; } + public int? ItemVnum { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/LevelUp/LogLevelUpNosMateMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/LevelUp/LogLevelUpNosMateMessage.cs new file mode 100644 index 0000000..bb11e8b --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/LevelUp/LogLevelUpNosMateMessage.cs @@ -0,0 +1,21 @@ +using System; +using PhoenixLib.ServiceBus.Routing; +using WingsEmu.Game.Helpers; + +namespace Plugin.PlayerLogs.Messages.LevelUp +{ + [MessageType("logs.levelup.nosmate")] + public class LogLevelUpNosMateMessage : IPlayerActionLogMessage + { + public byte Level { get; set; } + public int NosMateMonsterVnum { get; set; } + public string LevelUpType { get; set; } + public int? ItemVnum { get; set; } + public Location Location { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/LogGMCommandExecutedMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/LogGMCommandExecutedMessage.cs new file mode 100644 index 0000000..eb438c1 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/LogGMCommandExecutedMessage.cs @@ -0,0 +1,13 @@ +using PhoenixLib.ServiceBus.Routing; +using Plugin.PlayerLogs.Messages.Player; +using WingsEmu.DTOs.Account; + +namespace Plugin.PlayerLogs.Messages +{ + [MessageType("logs.gm.commands")] + public class LogGmCommandExecutedMessage : LogPlayerCommandExecutedMessage + { + public AuthorityType PlayerAuthority { get; set; } + public AuthorityType CommandAuthority { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/LogStrangeBehaviorMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/LogStrangeBehaviorMessage.cs new file mode 100644 index 0000000..af05495 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/LogStrangeBehaviorMessage.cs @@ -0,0 +1,17 @@ +using System; +using PhoenixLib.ServiceBus.Routing; + +namespace Plugin.PlayerLogs.Messages +{ + [MessageType("logs.strange.behavior")] + public class LogStrangeBehaviorMessage : IPlayerActionLogMessage + { + public string SeverityType { get; set; } + public string Message { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Mail/LogMailClaimedMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Mail/LogMailClaimedMessage.cs new file mode 100644 index 0000000..1eb0472 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Mail/LogMailClaimedMessage.cs @@ -0,0 +1,19 @@ +using System; +using PhoenixLib.ServiceBus.Routing; +using WingsEmu.DTOs.Items; + +namespace Plugin.PlayerLogs.Messages.Mail +{ + [MessageType("logs.mail.claimed")] + public class LogMailClaimedMessage : IPlayerActionLogMessage + { + public long MailId { get; set; } + public string SenderName { get; set; } + public ItemInstanceDTO ItemInstance { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Mail/LogMailRemovedMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Mail/LogMailRemovedMessage.cs new file mode 100644 index 0000000..c9fb99b --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Mail/LogMailRemovedMessage.cs @@ -0,0 +1,19 @@ +using System; +using PhoenixLib.ServiceBus.Routing; +using WingsEmu.DTOs.Items; + +namespace Plugin.PlayerLogs.Messages.Mail +{ + [MessageType("logs.mail.removed")] + public class LogMailRemovedMessage : IPlayerActionLogMessage + { + public long MailId { get; set; } + public string SenderName { get; set; } + public ItemInstanceDTO ItemInstance { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Mail/LogMailSentMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Mail/LogMailSentMessage.cs new file mode 100644 index 0000000..33279c4 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Mail/LogMailSentMessage.cs @@ -0,0 +1,19 @@ +using System; +using PhoenixLib.ServiceBus.Routing; +using WingsEmu.DTOs.Items; + +namespace Plugin.PlayerLogs.Messages.Mail +{ + [MessageType("logs.mail.sent")] + public class LogMailSentMessage : IPlayerActionLogMessage + { + public long MailId { get; set; } + public long ReceiverId { get; set; } + public ItemInstanceDTO ItemInstance { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Mail/LogNoteSentMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Mail/LogNoteSentMessage.cs new file mode 100644 index 0000000..40b6615 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Mail/LogNoteSentMessage.cs @@ -0,0 +1,19 @@ +using System; +using PhoenixLib.ServiceBus.Routing; + +namespace Plugin.PlayerLogs.Messages.Mail +{ + [MessageType("logs.note.sent")] + public class LogNoteSentMessage : IPlayerActionLogMessage + { + public long NoteId { get; set; } + public string ReceiverName { get; set; } + public string Title { get; set; } + public string Message { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Miniland/LogMinigameRewardClaimedMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Miniland/LogMinigameRewardClaimedMessage.cs new file mode 100644 index 0000000..810efab --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Miniland/LogMinigameRewardClaimedMessage.cs @@ -0,0 +1,23 @@ +using System; +using PhoenixLib.ServiceBus.Routing; +using WingsEmu.Game.Configurations.Miniland; + +namespace Plugin.PlayerLogs.Messages.Miniland +{ + [MessageType("logs.minigame.rewardclaimed")] + public class LogMinigameRewardClaimedMessage : IPlayerActionLogMessage + { + public long OwnerId { get; set; } + public int MinigameVnum { get; set; } + public string MinigameType { get; set; } + public RewardLevel RewardLevel { get; set; } + public bool Coupon { get; set; } + public int ItemVnum { get; set; } + public short Amount { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Miniland/LogMinigameScoreMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Miniland/LogMinigameScoreMessage.cs new file mode 100644 index 0000000..48c38d8 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Miniland/LogMinigameScoreMessage.cs @@ -0,0 +1,22 @@ +using System; +using PhoenixLib.ServiceBus.Routing; + +namespace Plugin.PlayerLogs.Messages.Miniland +{ + [MessageType("logs.minigame.score")] + public class LogMinigameScoreMessage : IPlayerActionLogMessage + { + public long OwnerId { get; set; } + public TimeSpan CompletionTime { get; set; } + public int MinigameVnum { get; set; } + public string MinigameType { get; set; } + public long Score1 { get; set; } + public long Score2 { get; set; } + public string ScoreValidity { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Miniland/LogWarehouseItemPlacedMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Miniland/LogWarehouseItemPlacedMessage.cs new file mode 100644 index 0000000..1a7e6d6 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Miniland/LogWarehouseItemPlacedMessage.cs @@ -0,0 +1,19 @@ +using System; +using PhoenixLib.ServiceBus.Routing; +using WingsEmu.DTOs.Items; + +namespace Plugin.PlayerLogs.Messages.Miniland +{ + [MessageType("logs.warehouse.item-placed")] + public class LogWarehouseItemPlacedMessage : IPlayerActionLogMessage + { + public ItemInstanceDTO ItemInstance { get; set; } + public int Amount { get; set; } + public short DestinationSlot { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Miniland/LogWarehouseItemWithdrawnMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Miniland/LogWarehouseItemWithdrawnMessage.cs new file mode 100644 index 0000000..c4f5b8c --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Miniland/LogWarehouseItemWithdrawnMessage.cs @@ -0,0 +1,19 @@ +using System; +using PhoenixLib.ServiceBus.Routing; +using WingsEmu.DTOs.Items; + +namespace Plugin.PlayerLogs.Messages.Miniland +{ + [MessageType("logs.warehouse.item-withdrawn")] + public class LogWarehouseItemWithdrawnMessage : IPlayerActionLogMessage + { + public ItemInstanceDTO ItemInstance { get; set; } + public int Amount { get; set; } + public short FromSlot { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Npc/LogItemProducedMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Npc/LogItemProducedMessage.cs new file mode 100644 index 0000000..a258b4a --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Npc/LogItemProducedMessage.cs @@ -0,0 +1,18 @@ +using System; +using PhoenixLib.ServiceBus.Routing; +using WingsEmu.DTOs.Items; + +namespace Plugin.PlayerLogs.Messages.Npc +{ + [MessageType("logs.npc.item-produced")] + public class LogItemProducedMessage : IPlayerActionLogMessage + { + public ItemInstanceDTO ItemInstance { get; set; } + public int ItemAmount { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Player/LogBoxOpenedMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Player/LogBoxOpenedMessage.cs new file mode 100644 index 0000000..822cb0f --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Player/LogBoxOpenedMessage.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using PhoenixLib.ServiceBus.Routing; +using WingsEmu.DTOs.Items; + +namespace Plugin.PlayerLogs.Messages.Player +{ + [MessageType("logs.box.opened")] + public class LogBoxOpenedMessage : IPlayerActionLogMessage + { + public ItemInstanceDTO Box { get; set; } + public List Rewards { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Player/LogGroupInvitedMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Player/LogGroupInvitedMessage.cs new file mode 100644 index 0000000..4b06c5b --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Player/LogGroupInvitedMessage.cs @@ -0,0 +1,17 @@ +using System; +using PhoenixLib.ServiceBus.Routing; + +namespace Plugin.PlayerLogs.Messages.Player +{ + [MessageType("logs.group.invited")] + public class LogGroupInvitedMessage : IPlayerActionLogMessage + { + public long GroupId { get; set; } + public long TargetId { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Player/LogPlayerChatMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Player/LogPlayerChatMessage.cs new file mode 100644 index 0000000..f2603d6 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Player/LogPlayerChatMessage.cs @@ -0,0 +1,23 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using PhoenixLib.ServiceBus.Routing; +using WingsEmu.Game._playerActionLogs; + +namespace Plugin.PlayerLogs.Messages.Player +{ + [MessageType("logs.chat.message")] + public class LogPlayerChatMessage : IPlayerActionLogMessage + { + public ChatType ChatType { get; set; } + public long? TargetCharacterId { get; set; } + public string Message { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Player/LogPlayerCommandExecutedMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Player/LogPlayerCommandExecutedMessage.cs new file mode 100644 index 0000000..98a0def --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Player/LogPlayerCommandExecutedMessage.cs @@ -0,0 +1,16 @@ +using System; +using PhoenixLib.ServiceBus.Routing; + +namespace Plugin.PlayerLogs.Messages.Player +{ + [MessageType("logs.player.commands")] + public class LogPlayerCommandExecutedMessage : IPlayerActionLogMessage + { + public string Command { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Player/LogPlayerDisconnectedMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Player/LogPlayerDisconnectedMessage.cs new file mode 100644 index 0000000..56fce75 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Player/LogPlayerDisconnectedMessage.cs @@ -0,0 +1,20 @@ +using System; +using PhoenixLib.ServiceBus.Routing; + +namespace Plugin.PlayerLogs.Messages.Player +{ + [MessageType("logs.connection.disconnected")] + public class LogPlayerDisconnectedMessage : IPlayerActionLogMessage + { + public string HardwareId { get; set; } + public string MasterAccountId { get; set; } + public DateTime SessionStart { get; set; } + public DateTime SessionEnd { get; set; } + public TimeSpan SessionDuration => SessionEnd - SessionStart; + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Player/LogPlayerExchangeMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Player/LogPlayerExchangeMessage.cs new file mode 100644 index 0000000..ec4e31e --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Player/LogPlayerExchangeMessage.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using PhoenixLib.ServiceBus.Routing; +using WingsAPI.Data.Exchanges; + +namespace Plugin.PlayerLogs.Messages.Player +{ + [MessageType("logs.trade.complete")] + public class LogPlayerExchangeMessage : IPlayerActionLogMessage + { + public List Items { get; set; } + public long Gold { get; set; } + public long BankGold { get; set; } + public long TargetCharacterId { get; set; } + public List TargetItems { get; set; } + public long TargetGold { get; set; } + public long TargetBankGold { get; set; } + public string TargetCharacterName { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Player/LogTradeRequestedMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Player/LogTradeRequestedMessage.cs new file mode 100644 index 0000000..d634c83 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Player/LogTradeRequestedMessage.cs @@ -0,0 +1,16 @@ +using System; +using PhoenixLib.ServiceBus.Routing; + +namespace Plugin.PlayerLogs.Messages.Player +{ + [MessageType("logs.trade.requested")] + public class LogTradeRequestedMessage : IPlayerActionLogMessage + { + public long TargetId { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Quest/LogQuestAbandonedMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Quest/LogQuestAbandonedMessage.cs new file mode 100644 index 0000000..fe482da --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Quest/LogQuestAbandonedMessage.cs @@ -0,0 +1,17 @@ +using System; +using PhoenixLib.ServiceBus.Routing; + +namespace Plugin.PlayerLogs.Messages.Quest +{ + [MessageType("logs.quest.abandoned")] + public class LogQuestAbandonedMessage : IPlayerActionLogMessage + { + public int QuestId { get; set; } + public string SlotType { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Quest/LogQuestAddedMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Quest/LogQuestAddedMessage.cs new file mode 100644 index 0000000..e9fe3ee --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Quest/LogQuestAddedMessage.cs @@ -0,0 +1,17 @@ +using System; +using PhoenixLib.ServiceBus.Routing; + +namespace Plugin.PlayerLogs.Messages.Quest +{ + [MessageType("logs.quest.added")] + public class LogQuestAddedMessage : IPlayerActionLogMessage + { + public int QuestId { get; set; } + public string SlotType { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Quest/LogQuestCompletedMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Quest/LogQuestCompletedMessage.cs new file mode 100644 index 0000000..f894ee8 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Quest/LogQuestCompletedMessage.cs @@ -0,0 +1,19 @@ +using System; +using PhoenixLib.ServiceBus.Routing; +using WingsEmu.Game.Helpers; + +namespace Plugin.PlayerLogs.Messages.Quest +{ + [MessageType("logs.quest.completed")] + public class LogQuestCompletedMessage : IPlayerActionLogMessage + { + public int QuestId { get; set; } + public string SlotType { get; set; } + public Location Location { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Quest/LogQuestObjectiveUpdatedMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Quest/LogQuestObjectiveUpdatedMessage.cs new file mode 100644 index 0000000..1cd2165 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Quest/LogQuestObjectiveUpdatedMessage.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using PhoenixLib.ServiceBus.Routing; +using WingsEmu.DTOs.Quests; + +namespace Plugin.PlayerLogs.Messages.Quest +{ + [MessageType("logs.quest.objectiveupdated")] + public class LogQuestObjectiveUpdatedMessage : IPlayerActionLogMessage + { + public int QuestId { get; set; } + public string SlotType { get; set; } + public Dictionary UpdatedObjectivesAmount { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Raid/LogRaidAbandonedMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Raid/LogRaidAbandonedMessage.cs new file mode 100644 index 0000000..8a7e033 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Raid/LogRaidAbandonedMessage.cs @@ -0,0 +1,16 @@ +using System; +using PhoenixLib.ServiceBus.Routing; + +namespace Plugin.PlayerLogs.Messages.Raid +{ + [MessageType("logs.raid.abandoned")] + public class LogRaidAbandonedMessage : IPlayerActionLogMessage + { + public Guid RaidId { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Raid/LogRaidCreatedMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Raid/LogRaidCreatedMessage.cs new file mode 100644 index 0000000..dada3f8 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Raid/LogRaidCreatedMessage.cs @@ -0,0 +1,17 @@ +using System; +using PhoenixLib.ServiceBus.Routing; + +namespace Plugin.PlayerLogs.Messages.Raid +{ + [MessageType("logs.raid.created")] + public class LogRaidCreatedMessage : IPlayerActionLogMessage + { + public Guid RaidId { get; set; } + public string RaidType { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Raid/LogRaidDiedMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Raid/LogRaidDiedMessage.cs new file mode 100644 index 0000000..da3ac66 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Raid/LogRaidDiedMessage.cs @@ -0,0 +1,16 @@ +using System; +using PhoenixLib.ServiceBus.Routing; + +namespace Plugin.PlayerLogs.Messages.Raid +{ + [MessageType("logs.raid.died")] + public class LogRaidDiedMessage : IPlayerActionLogMessage + { + public Guid RaidId { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Raid/LogRaidInvitedMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Raid/LogRaidInvitedMessage.cs new file mode 100644 index 0000000..071e0fe --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Raid/LogRaidInvitedMessage.cs @@ -0,0 +1,17 @@ +using System; +using PhoenixLib.ServiceBus.Routing; + +namespace Plugin.PlayerLogs.Messages.Raid +{ + [MessageType("logs.raid.invited")] + public class LogRaidInvitedMessage : IPlayerActionLogMessage + { + public Guid RaidId { get; set; } + public long TargetId { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Raid/LogRaidJoinedMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Raid/LogRaidJoinedMessage.cs new file mode 100644 index 0000000..36ca55b --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Raid/LogRaidJoinedMessage.cs @@ -0,0 +1,17 @@ +using System; +using PhoenixLib.ServiceBus.Routing; + +namespace Plugin.PlayerLogs.Messages.Raid +{ + [MessageType("logs.raid.joined")] + public class LogRaidJoinedMessage : IPlayerActionLogMessage + { + public string RaidJoinType { get; set; } + public Guid RaidId { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Raid/LogRaidLeftMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Raid/LogRaidLeftMessage.cs new file mode 100644 index 0000000..919631b --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Raid/LogRaidLeftMessage.cs @@ -0,0 +1,16 @@ +using System; +using PhoenixLib.ServiceBus.Routing; + +namespace Plugin.PlayerLogs.Messages.Raid +{ + [MessageType("logs.raid.left")] + public class LogRaidLeftMessage : IPlayerActionLogMessage + { + public Guid RaidId { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Raid/LogRaidLostMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Raid/LogRaidLostMessage.cs new file mode 100644 index 0000000..8099134 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Raid/LogRaidLostMessage.cs @@ -0,0 +1,17 @@ +using System; +using PhoenixLib.ServiceBus.Routing; + +namespace Plugin.PlayerLogs.Messages.Raid +{ + [MessageType("logs.raid.lost")] + public class LogRaidLostMessage : IPlayerActionLogMessage + { + public string RaidId { get; set; } + public string RaidType { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Raid/LogRaidRevivedMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Raid/LogRaidRevivedMessage.cs new file mode 100644 index 0000000..a4c20f5 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Raid/LogRaidRevivedMessage.cs @@ -0,0 +1,17 @@ +using System; +using PhoenixLib.ServiceBus.Routing; + +namespace Plugin.PlayerLogs.Messages.Raid +{ + [MessageType("logs.raid.revived")] + public class LogRaidRevivedMessage : IPlayerActionLogMessage + { + public Guid RaidId { get; set; } + public bool RestoredLife { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Raid/LogRaidRewardReceivedMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Raid/LogRaidRewardReceivedMessage.cs new file mode 100644 index 0000000..cb9def3 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Raid/LogRaidRewardReceivedMessage.cs @@ -0,0 +1,17 @@ +using System; +using PhoenixLib.ServiceBus.Routing; + +namespace Plugin.PlayerLogs.Messages.Raid +{ + [MessageType("logs.raid.rewardreceived")] + public class LogRaidRewardReceivedMessage : IPlayerActionLogMessage + { + public Guid RaidId { get; set; } + public byte BoxRarity { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Raid/LogRaidStartedMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Raid/LogRaidStartedMessage.cs new file mode 100644 index 0000000..1ffd579 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Raid/LogRaidStartedMessage.cs @@ -0,0 +1,18 @@ +using System; +using PhoenixLib.ServiceBus.Routing; + +namespace Plugin.PlayerLogs.Messages.Raid +{ + [MessageType("logs.raid.started")] + public class LogRaidStartedMessage : IPlayerActionLogMessage + { + public Guid RaidId { get; set; } + public long[] MembersIds { get; set; } + public string RaidType { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Raid/LogRaidSwitchButtonToggledMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Raid/LogRaidSwitchButtonToggledMessage.cs new file mode 100644 index 0000000..ee28265 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Raid/LogRaidSwitchButtonToggledMessage.cs @@ -0,0 +1,17 @@ +using System; +using PhoenixLib.ServiceBus.Routing; + +namespace Plugin.PlayerLogs.Messages.Raid +{ + [MessageType("logs.raid.buttontoggled")] + public class LogRaidSwitchButtonToggledMessage : IPlayerActionLogMessage + { + public Guid RaidId { get; set; } + public long LeverId { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Raid/LogRaidTargetKilledMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Raid/LogRaidTargetKilledMessage.cs new file mode 100644 index 0000000..4c369bc --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Raid/LogRaidTargetKilledMessage.cs @@ -0,0 +1,17 @@ +using System; +using PhoenixLib.ServiceBus.Routing; + +namespace Plugin.PlayerLogs.Messages.Raid +{ + [MessageType("logs.raid.targetkilled")] + public class LogRaidTargetKilledMessage : IPlayerActionLogMessage + { + public Guid RaidId { get; set; } + public long[] DamagerCharactersIds { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Raid/LogRaidWonMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Raid/LogRaidWonMessage.cs new file mode 100644 index 0000000..0648536 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Raid/LogRaidWonMessage.cs @@ -0,0 +1,18 @@ +using System; +using PhoenixLib.ServiceBus.Routing; + +namespace Plugin.PlayerLogs.Messages.Raid +{ + [MessageType("logs.raid.won")] + public class LogRaidWonMessage : IPlayerActionLogMessage + { + public string RaidId { get; set; } + + public string RaidType { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/RainbowBattle/LogRainbowBattleFrozenMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/RainbowBattle/LogRainbowBattleFrozenMessage.cs new file mode 100644 index 0000000..7ffcd43 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/RainbowBattle/LogRainbowBattleFrozenMessage.cs @@ -0,0 +1,19 @@ +using System; +using PhoenixLib.ServiceBus.Routing; +using WingsEmu.Game.RainbowBattle; + +namespace Plugin.PlayerLogs.Messages.RainbowBattle +{ + [MessageType("logs.rainbowbattle.frozen")] + public class LogRainbowBattleFrozenMessage : IPlayerActionLogMessage + { + public Guid RainbowBattleId { get; set; } + public RainbowBattlePlayerDump Killer { get; set; } + public RainbowBattlePlayerDump Killed { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/RainbowBattle/LogRainbowBattleJoinMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/RainbowBattle/LogRainbowBattleJoinMessage.cs new file mode 100644 index 0000000..33fafec --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/RainbowBattle/LogRainbowBattleJoinMessage.cs @@ -0,0 +1,15 @@ +using System; +using PhoenixLib.ServiceBus.Routing; + +namespace Plugin.PlayerLogs.Messages.RainbowBattle +{ + [MessageType("logs.rainbowbattle.join")] + public class LogRainbowBattleJoinMessage : IPlayerActionLogMessage + { + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/RainbowBattle/LogRainbowBattleLoseMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/RainbowBattle/LogRainbowBattleLoseMessage.cs new file mode 100644 index 0000000..f593ee6 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/RainbowBattle/LogRainbowBattleLoseMessage.cs @@ -0,0 +1,17 @@ +using System; +using PhoenixLib.ServiceBus.Routing; + +namespace Plugin.PlayerLogs.Messages.RainbowBattle +{ + [MessageType("logs.rainbowbattle.lose")] + public class LogRainbowBattleLoseMessage : IPlayerActionLogMessage + { + public Guid RainbowBattleId { get; set; } + public int[] Players { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/RainbowBattle/LogRainbowBattleTieMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/RainbowBattle/LogRainbowBattleTieMessage.cs new file mode 100644 index 0000000..5a233d3 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/RainbowBattle/LogRainbowBattleTieMessage.cs @@ -0,0 +1,17 @@ +using System; +using PhoenixLib.ServiceBus.Routing; + +namespace Plugin.PlayerLogs.Messages.RainbowBattle +{ + [MessageType("logs.rainbowbattle.tie")] + public class LogRainbowBattleTieMessage : IPlayerActionLogMessage + { + public int[] RedTeam { get; set; } + public int[] BlueTeam { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/RainbowBattle/LogRainbowBattleWonMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/RainbowBattle/LogRainbowBattleWonMessage.cs new file mode 100644 index 0000000..c31dc8e --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/RainbowBattle/LogRainbowBattleWonMessage.cs @@ -0,0 +1,17 @@ +using System; +using PhoenixLib.ServiceBus.Routing; + +namespace Plugin.PlayerLogs.Messages.RainbowBattle +{ + [MessageType("logs.rainbowbattle.won")] + public class LogRainbowBattleWonMessage : IPlayerActionLogMessage + { + public Guid RainbowBattleId { get; set; } + public int[] Players { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Shop/LogShopClosedMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Shop/LogShopClosedMessage.cs new file mode 100644 index 0000000..41e6fe8 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Shop/LogShopClosedMessage.cs @@ -0,0 +1,17 @@ +using System; +using PhoenixLib.ServiceBus.Routing; +using WingsEmu.Game.Helpers; + +namespace Plugin.PlayerLogs.Messages.Shop +{ + [MessageType("logs.shop.closed")] + public class LogShopClosedMessage : IPlayerActionLogMessage + { + public Location Location { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Shop/LogShopNpcBoughtItemMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Shop/LogShopNpcBoughtItemMessage.cs new file mode 100644 index 0000000..2f31869 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Shop/LogShopNpcBoughtItemMessage.cs @@ -0,0 +1,21 @@ +using System; +using PhoenixLib.ServiceBus.Routing; +using WingsEmu.DTOs.Items; + +namespace Plugin.PlayerLogs.Messages.Shop +{ + [MessageType("logs.shop.npcitembought")] + public class LogShopNpcBoughtItemMessage : IPlayerActionLogMessage + { + public long SellerId { get; set; } + public long TotalPrice { get; set; } + public int Quantity { get; set; } + public string CurrencyType { get; set; } + public ItemInstanceDTO ItemInstance { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Shop/LogShopNpcSoldItemMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Shop/LogShopNpcSoldItemMessage.cs new file mode 100644 index 0000000..8259cdb --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Shop/LogShopNpcSoldItemMessage.cs @@ -0,0 +1,19 @@ +using System; +using PhoenixLib.ServiceBus.Routing; +using WingsEmu.DTOs.Items; + +namespace Plugin.PlayerLogs.Messages.Shop +{ + [MessageType("logs.shop.npc-item-sold")] + public class LogShopNpcSoldItemMessage : IPlayerActionLogMessage + { + public ItemInstanceDTO ItemInstance { get; set; } + public short Amount { get; set; } + public long PricePerItem { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Shop/LogShopOpenedMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Shop/LogShopOpenedMessage.cs new file mode 100644 index 0000000..19a4e1f --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Shop/LogShopOpenedMessage.cs @@ -0,0 +1,18 @@ +using System; +using PhoenixLib.ServiceBus.Routing; +using WingsEmu.Game.Helpers; + +namespace Plugin.PlayerLogs.Messages.Shop +{ + [MessageType("logs.shop.opened")] + public class LogShopOpenedMessage : IPlayerActionLogMessage + { + public string ShopName { get; set; } + public Location Location { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Shop/LogShopPlayerBoughtItemMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Shop/LogShopPlayerBoughtItemMessage.cs new file mode 100644 index 0000000..520a005 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Shop/LogShopPlayerBoughtItemMessage.cs @@ -0,0 +1,21 @@ +using System; +using PhoenixLib.ServiceBus.Routing; +using WingsEmu.DTOs.Items; + +namespace Plugin.PlayerLogs.Messages.Shop +{ + [MessageType("logs.shop.playeritembought")] + public class LogShopPlayerBoughtItemMessage : IPlayerActionLogMessage + { + public long SellerId { get; set; } + public string SellerName { get; set; } + public long TotalPrice { get; set; } + public int Quantity { get; set; } + public ItemInstanceDTO ItemInstance { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Shop/LogShopSkillBoughtMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Shop/LogShopSkillBoughtMessage.cs new file mode 100644 index 0000000..e44001b --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Shop/LogShopSkillBoughtMessage.cs @@ -0,0 +1,16 @@ +using System; +using PhoenixLib.ServiceBus.Routing; + +namespace Plugin.PlayerLogs.Messages.Shop +{ + [MessageType("logs.shop.skill-bought")] + public class LogShopSkillBoughtMessage : IPlayerActionLogMessage + { + public short SkillVnum { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Shop/LogShopSkillSoldMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Shop/LogShopSkillSoldMessage.cs new file mode 100644 index 0000000..92dccbd --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Shop/LogShopSkillSoldMessage.cs @@ -0,0 +1,16 @@ +using System; +using PhoenixLib.ServiceBus.Routing; + +namespace Plugin.PlayerLogs.Messages.Shop +{ + [MessageType("logs.shop.skill-sold")] + public class LogShopSkillSoldMessage : IPlayerActionLogMessage + { + public int SkillVnum { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Upgrade/LogCellonUpgradedMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Upgrade/LogCellonUpgradedMessage.cs new file mode 100644 index 0000000..ed1b055 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Upgrade/LogCellonUpgradedMessage.cs @@ -0,0 +1,19 @@ +using System; +using PhoenixLib.ServiceBus.Routing; +using WingsEmu.DTOs.Items; + +namespace Plugin.PlayerLogs.Messages.Upgrade +{ + [MessageType("logs.upgraded.cellon")] + public class LogCellonUpgradedMessage : IPlayerActionLogMessage + { + public ItemInstanceDTO Item { get; set; } + public int CellonVnum { get; set; } + public bool Succeed { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Upgrade/LogItemGambledMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Upgrade/LogItemGambledMessage.cs new file mode 100644 index 0000000..1080d69 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Upgrade/LogItemGambledMessage.cs @@ -0,0 +1,22 @@ +using System; +using PhoenixLib.ServiceBus.Routing; + +namespace Plugin.PlayerLogs.Messages.Upgrade +{ + [MessageType("logs.upgraded.gambled")] + public class LogItemGambledMessage : IPlayerActionLogMessage + { + public int ItemVnum { get; set; } + public string Mode { get; set; } + public string Protection { get; set; } + public int? Amulet { get; set; } + public bool Succeed { get; set; } + public short OriginalRarity { get; set; } + public short? FinalRarity { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Upgrade/LogItemSummedMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Upgrade/LogItemSummedMessage.cs new file mode 100644 index 0000000..55df419 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Upgrade/LogItemSummedMessage.cs @@ -0,0 +1,20 @@ +using System; +using PhoenixLib.ServiceBus.Routing; +using WingsEmu.DTOs.Items; + +namespace Plugin.PlayerLogs.Messages.Upgrade +{ + [MessageType("logs.upgraded.itemsummed")] + public class LogItemSummedMessage : IPlayerActionLogMessage + { + public ItemInstanceDTO LeftItem { get; set; } + public ItemInstanceDTO RightItem { get; set; } + public bool Succeed { get; set; } + public int SumLevel { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Upgrade/LogItemUpgradedMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Upgrade/LogItemUpgradedMessage.cs new file mode 100644 index 0000000..aa010b4 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Upgrade/LogItemUpgradedMessage.cs @@ -0,0 +1,23 @@ +using System; +using PhoenixLib.ServiceBus.Routing; +using WingsEmu.DTOs.Items; + +namespace Plugin.PlayerLogs.Messages.Upgrade +{ + [MessageType("logs.upgraded.itemupgraded")] + public class LogItemUpgradedMessage : IPlayerActionLogMessage + { + public ItemInstanceDTO Item { get; set; } + public long TotalPrice { get; set; } + public string Mode { get; set; } + public string Protection { get; set; } + public bool HasAmulet { get; set; } + public short OriginalUpgrade { get; set; } + public string Result { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Upgrade/LogShellIdentifiedMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Upgrade/LogShellIdentifiedMessage.cs new file mode 100644 index 0000000..779101a --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Upgrade/LogShellIdentifiedMessage.cs @@ -0,0 +1,17 @@ +using System; +using PhoenixLib.ServiceBus.Routing; +using WingsEmu.DTOs.Items; + +namespace Plugin.PlayerLogs.Messages.Upgrade +{ + [MessageType("logs.upgraded.shellidentified")] + public class LogShellIdentifiedMessage : IPlayerActionLogMessage + { + public ItemInstanceDTO Shell { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Upgrade/LogSpPerfectedMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Upgrade/LogSpPerfectedMessage.cs new file mode 100644 index 0000000..bbeec0c --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Upgrade/LogSpPerfectedMessage.cs @@ -0,0 +1,20 @@ +using System; +using PhoenixLib.ServiceBus.Routing; +using WingsEmu.DTOs.Items; + +namespace Plugin.PlayerLogs.Messages.Upgrade +{ + [MessageType("logs.upgrade.sp-perfected")] + public class LogSpPerfectedMessage : IPlayerActionLogMessage + { + public ItemInstanceDTO Sp { get; set; } + public bool Success { get; set; } + public byte SpPerfectionLevel { get; set; } + + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Messages/Upgrade/LogSpUpgradedMessage.cs b/srcs/_plugins/Plugin.PlayerLogs/Messages/Upgrade/LogSpUpgradedMessage.cs new file mode 100644 index 0000000..42c4ba2 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Messages/Upgrade/LogSpUpgradedMessage.cs @@ -0,0 +1,21 @@ +using System; +using PhoenixLib.ServiceBus.Routing; +using WingsEmu.DTOs.Items; + +namespace Plugin.PlayerLogs.Messages.Upgrade +{ + [MessageType("logs.upgrade.sp-upgraded")] + public class LogSpUpgradedMessage : IPlayerActionLogMessage + { + public ItemInstanceDTO Sp { get; set; } + public string Mode { get; set; } + public string Result { get; set; } + public short OriginalUpgrade { get; set; } + public bool IsProtected { get; set; } + public DateTime CreatedAt { get; init; } + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public string IpAddress { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/PlayerLogManager.cs b/srcs/_plugins/Plugin.PlayerLogs/PlayerLogManager.cs new file mode 100644 index 0000000..f1d03c1 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/PlayerLogManager.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Concurrent; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using PhoenixLib.Logging; +using PhoenixLib.ServiceBus; +using WingsEmu.Game.Logs; + +namespace Plugin.PlayerLogs +{ + public sealed class PlayerLogManager : BackgroundService, IPlayerLogManager + { + private static readonly MethodInfo PublishLogAsyncMethod = typeof(PlayerLogManager).GetMethod(nameof(PublishLogAsync), BindingFlags.Instance | BindingFlags.NonPublic); + private static readonly ConcurrentDictionary _publishLogsCache = new(); + private static readonly TimeSpan Interval = TimeSpan.FromSeconds(5); + private readonly ConcurrentQueue<(IPlayerActionLog, Type)> _queue; + + private readonly IServiceProvider _serviceProvider; + + public PlayerLogManager(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + _queue = new ConcurrentQueue<(IPlayerActionLog, Type)>(); + } + + public void AddLog(T message) where T : IPlayerActionLog + { + _queue.Enqueue((message, typeof(T))); + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + if (_queue.IsEmpty) + { + await Task.Delay(Interval, stoppingToken); + } + + await PublishLogs(); + await Task.Delay(Interval, stoppingToken); + } + + await PublishLogs(); + } + + private async Task PublishLogs() + { + while (_queue.TryDequeue(out (IPlayerActionLog, Type) result)) + { + try + { + IPlayerActionLog log = result.Item1; + Type logType = result.Item2; + MethodInfo logMethod = _publishLogsCache.GetOrAdd(logType, PublishLogAsyncMethod.MakeGenericMethod(logType)); + await (Task)logMethod.Invoke(this, new object[] { log }); + } + catch (Exception e) + { + Log.Error("Couldn't publish that action log message. See the following exception:", e); + } + } + } + + private async Task PublishLogAsync(T log) where T : IPlayerActionLog, IMessage + { + IMessagePublisher publisher = _serviceProvider.GetRequiredService>(); + await publisher.PublishAsync(log); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/PlayerLoggingDependencyPlugin.cs b/srcs/_plugins/Plugin.PlayerLogs/PlayerLoggingDependencyPlugin.cs new file mode 100644 index 0000000..4a11b48 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/PlayerLoggingDependencyPlugin.cs @@ -0,0 +1,182 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Plugin.PlayerLogs.Core; +using Plugin.PlayerLogs.Enrichers; +using Plugin.PlayerLogs.Enrichers.Act4; +using Plugin.PlayerLogs.Enrichers.Bazaar; +using Plugin.PlayerLogs.Enrichers.Family; +using Plugin.PlayerLogs.Enrichers.Inventory; +using Plugin.PlayerLogs.Enrichers.LevelUp; +using Plugin.PlayerLogs.Enrichers.Mail; +using Plugin.PlayerLogs.Enrichers.Miniland; +using Plugin.PlayerLogs.Enrichers.Npc; +using Plugin.PlayerLogs.Enrichers.Player; +using Plugin.PlayerLogs.Enrichers.Quest; +using Plugin.PlayerLogs.Enrichers.Raid; +using Plugin.PlayerLogs.Enrichers.RainbowBattle; +using Plugin.PlayerLogs.Enrichers.Shop; +using Plugin.PlayerLogs.Enrichers.Upgrade; +using Plugin.PlayerLogs.Messages; +using Plugin.PlayerLogs.Messages.Act4; +using Plugin.PlayerLogs.Messages.Bazaar; +using Plugin.PlayerLogs.Messages.Family; +using Plugin.PlayerLogs.Messages.Inventory; +using Plugin.PlayerLogs.Messages.LevelUp; +using Plugin.PlayerLogs.Messages.Mail; +using Plugin.PlayerLogs.Messages.Miniland; +using Plugin.PlayerLogs.Messages.Npc; +using Plugin.PlayerLogs.Messages.Player; +using Plugin.PlayerLogs.Messages.Quest; +using Plugin.PlayerLogs.Messages.Raid; +using Plugin.PlayerLogs.Messages.RainbowBattle; +using Plugin.PlayerLogs.Messages.Shop; +using Plugin.PlayerLogs.Messages.Upgrade; +using WingsAPI.Plugins; +using WingsEmu.Game; +using WingsEmu.Game.Act4.Event; +using WingsEmu.Game.Bazaar.Events; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Chat; +using WingsEmu.Game.Exchange.Event; +using WingsEmu.Game.Families.Event; +using WingsEmu.Game.Groups.Events; +using WingsEmu.Game.Inventory.Event; +using WingsEmu.Game.Logs; +using WingsEmu.Game.Mails.Events; +using WingsEmu.Game.Miniland.Events; +using WingsEmu.Game.Npcs.Event; +using WingsEmu.Game.Quests.Event; +using WingsEmu.Game.Raids.Events; +using WingsEmu.Game.RainbowBattle.Event; +using WingsEmu.Game.Revival; +using WingsEmu.Game.Shops.Event; +using WingsEmu.Game.Warehouse.Events; + +namespace Plugin.PlayerLogs +{ + public class PlayerLoggingDependencyPlugin : IGameServerPlugin + { + public string Name => nameof(PlayerLoggingDependencyPlugin); + + + public void AddDependencies(IServiceCollection services, GameServerLoader gameServer) + { + services.AddSingleton(); + services.AddSingleton(provider => (PlayerLogManager)provider.GetService()); + + // Act4 + services.AddPlayerLog(); + services.AddPlayerLog(); + services.AddPlayerLog(); + + // anticheat + services.AddPlayerLog(); + services.AddPlayerLog(); + services.AddPlayerLog(); + services.AddPlayerLog(); + + // Commands + services.AddPlayerLog(); + services.AddPlayerLog(); + + // Level ups + services.AddPlayerLog(); + services.AddPlayerLog(); + + // Families + services.AddPlayerLog(); + services.AddPlayerLog(); + services.AddPlayerLog(); + services.AddPlayerLog(); + services.AddPlayerLog(); + services.AddPlayerLog(); + services.AddPlayerLog(); + services.AddPlayerLog(); + services.AddPlayerLog(); + + // Warehouse + services.AddPlayerLog(); + services.AddPlayerLog(); + + // Mini-games + services.AddPlayerLog(); + services.AddPlayerLog(); + + // Items + services.AddPlayerLog(); + services.AddPlayerLog(); + services.AddPlayerLog(); + services.AddPlayerLog(); + services.AddPlayerLog(); + services.AddPlayerLog(); + services.AddPlayerLog(); + services.AddPlayerLog(); + + // Raid Management + services.AddPlayerLog(); + services.AddPlayerLog(); + services.AddPlayerLog(); + services.AddPlayerLog(); + services.AddPlayerLog(); + + // Raid Actions + services.AddPlayerLog(); + services.AddPlayerLog(); + services.AddPlayerLog(); + services.AddPlayerLog(); + services.AddPlayerLog(); + services.AddPlayerLog(); + services.AddPlayerLog(); + + // Quests + //services.AddPlayerLog(); + services.AddPlayerLog(); + services.AddPlayerLog(); + //services.AddPlayerLog(); + + // Shops + services.AddPlayerLog(); + services.AddPlayerLog(); + services.AddPlayerLog(); + services.AddPlayerLog(); + services.AddPlayerLog(); + services.AddPlayerLog(); + services.AddPlayerLog(); + + // Inventory + services.AddPlayerLog(); + services.AddPlayerLog(); + services.AddPlayerLog(); + services.AddPlayerLog(); + + // Invitations + services.AddPlayerLog(); + services.AddPlayerLog(); + services.AddPlayerLog(); + services.AddPlayerLog(); + + // Bazaar + services.AddPlayerLog(); + services.AddPlayerLog(); + services.AddPlayerLog(); + services.AddPlayerLog(); + + // Mails + services.AddPlayerLog(); + services.AddPlayerLog(); + + // Notes + services.AddPlayerLog(); + + // Npc + services.AddPlayerLog(); + + // Rainbow Battle + services.AddPlayerLog(); + services.AddPlayerLog(); + services.AddPlayerLog(); + services.AddPlayerLog(); + services.AddPlayerLog(); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.PlayerLogs/Plugin.PlayerLogs.csproj b/srcs/_plugins/Plugin.PlayerLogs/Plugin.PlayerLogs.csproj new file mode 100644 index 0000000..b4442e4 --- /dev/null +++ b/srcs/_plugins/Plugin.PlayerLogs/Plugin.PlayerLogs.csproj @@ -0,0 +1,19 @@ + + + + net5.0 + + + + + + + + + + + + + + + diff --git a/srcs/_plugins/Plugin.QuestImpl/BaseRunScriptHandler.cs b/srcs/_plugins/Plugin.QuestImpl/BaseRunScriptHandler.cs new file mode 100644 index 0000000..e050cbb --- /dev/null +++ b/srcs/_plugins/Plugin.QuestImpl/BaseRunScriptHandler.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using PhoenixLib.Logging; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Quests; +using WingsEmu.Game.Quests.Event; + +namespace Plugin.QuestImpl +{ + public class BaseRunScriptHandler : IRunScriptHandlerContainer + { + private readonly Dictionary _handlersByRunId; + + public BaseRunScriptHandler() => _handlersByRunId = new Dictionary(); + + public async Task RegisterAsync(IRunScriptHandler handler) + { + foreach (int runId in handler.RunIds) + { + if (_handlersByRunId.ContainsKey(runId)) + { + Log.Debug($"[RUN_SCRIPT_HANDLER][REGISTER_WARNING] RUN_ID : {runId} IS ALREADY REGISTERED! IS IT DUPLICATED?"); + continue; + } + + Log.Debug($"[RUN_SCRIPT_HANDLER][REGISTER] RUN_ID : {runId} REGISTERED !"); + _handlersByRunId.Add(runId, handler); + } + } + + public async Task UnregisterAsync(long runId) + { + Log.Debug($"[RUN_SCRIPT_HANDLER][UNREGISTER] RUN_ID : {runId} UNREGISTERED !"); + _handlersByRunId.Remove(runId); + } + + public void Handle(IClientSession player, RunScriptEvent args) + { + HandleAsync(player, args).ConfigureAwait(false).GetAwaiter().GetResult(); + } + + public async Task HandleAsync(IClientSession player, RunScriptEvent args) + { + if (!_handlersByRunId.TryGetValue(args.RunId, out IRunScriptHandler handler)) + { + Log.Debug($"[RUN_SCRIPT_HANDLER] RUN_ID : {args.RunId} "); + return; + } + + Log.Debug($"[RUN_SCRIPT_HANDLER][HANDLING] : {args.RunId} "); + await handler.ExecuteAsync(player, args); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.QuestImpl/Handlers/AddGeneralQuestEventHandler.cs b/srcs/_plugins/Plugin.QuestImpl/Handlers/AddGeneralQuestEventHandler.cs new file mode 100644 index 0000000..885536a --- /dev/null +++ b/srcs/_plugins/Plugin.QuestImpl/Handlers/AddGeneralQuestEventHandler.cs @@ -0,0 +1,105 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using PhoenixLib.Logging; +using PhoenixLib.Scheduler; +using WingsAPI.Game.Extensions.Quests; +using WingsEmu.DTOs.Quests; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Quests; +using WingsEmu.Game.Quests.Event; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.QuestImpl.Handlers +{ + public class AddGeneralQuestEventHandler : IAsyncEventProcessor + { + private readonly IGameLanguageService _gameLanguageService; + private readonly INpcRunTypeQuestsConfiguration _npcRunTypeQuestsConfiguration; + private readonly IPeriodicQuestsConfiguration _periodicQuestsConfiguration; + private readonly IQuestFactory _questFactory; + private readonly IQuestManager _questManager; + private readonly IScheduler _scheduler; + + public AddGeneralQuestEventHandler(IQuestManager questManager, IScheduler scheduler, IGameLanguageService gameLanguageService, INpcRunTypeQuestsConfiguration npcRunTypeQuestsConfiguration, + IPeriodicQuestsConfiguration periodicQuestsConfiguration, IQuestFactory questFactory) + { + _questManager = questManager; + _scheduler = scheduler; + _gameLanguageService = gameLanguageService; + _npcRunTypeQuestsConfiguration = npcRunTypeQuestsConfiguration; + _periodicQuestsConfiguration = periodicQuestsConfiguration; + _questFactory = questFactory; + } + + public async Task HandleAsync(AddQuestEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + int questId = e.QuestId; + + if (e.QuestSlotType != QuestSlotType.GENERAL) + { + return; + } + + QuestDto quest = _questManager.GetQuestById(questId); + if (quest == null) + { + Log.Debug($"[ERROR] Quest not found: {questId.ToString()}"); + return; + } + + // Checks if the player has that quest active or any quest related to the same questline + if (session.HasAlreadyQuestOrQuestline(quest, _questManager, _npcRunTypeQuestsConfiguration)) + { + session.SendMsg(_gameLanguageService.GetLanguage(GameDialogKey.QUEST_SHOUTMESSAGE_ALREADY_HAVE_QUEST, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (quest.RequiredQuestId > 0 && !session.PlayerEntity.HasCompletedQuest(quest.RequiredQuestId)) + { + session.SendMsg(session.GetLanguage(GameDialogKey.QUEST_SHOUTMESSAGE_INCOMPLETE_QUESTS), MsgMessageType.Middle); + return; + } + + if (session.GetEmptyQuestSlot(e.QuestSlotType) == -1) + { + session.SendMsg(_gameLanguageService.GetLanguage(GameDialogKey.QUEST_SHOUTMESSAGE_SLOT_FULL, session.UserLanguage), MsgMessageType.Middle); + return; + } + + // Periodic quests check + bool hasCompletedPeriodicQuest = await session.HasCompletedPeriodicQuest(quest, _questManager, _npcRunTypeQuestsConfiguration, _periodicQuestsConfiguration); + if (_periodicQuestsConfiguration.IsDailyQuest(quest) && hasCompletedPeriodicQuest) + { + session.SendMsg(_gameLanguageService.GetLanguage(GameDialogKey.QUEST_SHOUTMESSAGE_ALREADY_COMPLETED_PERIODIC, session.UserLanguage), MsgMessageType.Middle); + return; + } + + CharacterQuest characterQuest = _questFactory.NewQuest(session.PlayerEntity.Id, questId, e.QuestSlotType); + session.PlayerEntity.AddActiveQuest(characterQuest); + session.RefreshQuestList(_questManager, characterQuest.QuestId); + + await session.EmitEventAsync(new QuestAddedEvent + { + QuestId = characterQuest.QuestId, + QuestSlotType = characterQuest.SlotType + }); + + if (characterQuest.Quest.QuestType != QuestType.COMPLETE_TIMESPACE && characterQuest.Quest.QuestType != QuestType.DROP_IN_TIMESPACE && characterQuest.Quest.QuestType != QuestType.NOTHING + && quest.TargetMapId != 0) + { + session.SendTargetQuest(quest.TargetMapX, quest.TargetMapY, quest.TargetMapId, quest.Id); + return; + } + + // Still gotta be present for NOTHING type quests + _scheduler.Schedule(TimeSpan.FromSeconds(1), s => session.EmitEvent(new QuestCompletedEvent(characterQuest))); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.QuestImpl/Handlers/AddMainQuestEventHandler.cs b/srcs/_plugins/Plugin.QuestImpl/Handlers/AddMainQuestEventHandler.cs new file mode 100644 index 0000000..a23207f --- /dev/null +++ b/srcs/_plugins/Plugin.QuestImpl/Handlers/AddMainQuestEventHandler.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using PhoenixLib.Logging; +using PhoenixLib.Scheduler; +using WingsAPI.Game.Extensions.Quests; +using WingsEmu.DTOs.Quests; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Quests; +using WingsEmu.Game.Quests.Event; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.QuestImpl.Handlers +{ + public class AddMainQuestEventHandler : IAsyncEventProcessor + { + private readonly IGameLanguageService _gameLanguageService; + private readonly IQuestFactory _questFactory; + private readonly IQuestManager _questManager; + private readonly IScheduler _scheduler; + + public AddMainQuestEventHandler(IQuestManager questManager, IQuestFactory questFactory, IScheduler scheduler, IGameLanguageService gameLanguageService) + { + _questManager = questManager; + _questFactory = questFactory; + _scheduler = scheduler; + _gameLanguageService = gameLanguageService; + } + + public async Task HandleAsync(AddQuestEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + int questId = e.QuestId; + + if (e.QuestSlotType != QuestSlotType.MAIN) + { + return; + } + + QuestDto quest = _questManager.GetQuestById(questId); + if (quest == null) + { + Log.Debug($"[ERROR] Quest not found: {questId.ToString()}"); + return; + } + + if (session.PlayerEntity.HasQuestWithId(quest.Id)) + { + session.SendMsg(_gameLanguageService.GetLanguage(GameDialogKey.QUEST_SHOUTMESSAGE_ALREADY_HAVE_QUEST, session.UserLanguage), MsgMessageType.Middle); + return; + } + + // Check for possible script exploit + if (IsPotentialMainQuestExploit(session, quest)) + { + return; + } + + if (session.GetEmptyQuestSlot(e.QuestSlotType) == -1) + { + session.SendMsg(_gameLanguageService.GetLanguage(GameDialogKey.QUEST_SHOUTMESSAGE_SLOT_FULL, session.UserLanguage), MsgMessageType.Middle); + return; + } + + CharacterQuest characterQuest = _questFactory.NewQuest(session.PlayerEntity.Id, questId, e.QuestSlotType); + session.PlayerEntity.AddActiveQuest(characterQuest); + session.RefreshQuestList(_questManager, characterQuest.QuestId); + + await session.EmitEventAsync(new QuestAddedEvent + { + QuestId = characterQuest.QuestId, + QuestSlotType = characterQuest.SlotType + }); + + if (characterQuest.Quest.QuestType != QuestType.COMPLETE_TIMESPACE && characterQuest.Quest.QuestType != QuestType.DROP_IN_TIMESPACE && quest.TargetMapId != 0) + { + session.SendTargetQuest(quest.TargetMapX, quest.TargetMapY, quest.TargetMapId, quest.Id); + } + + // Still gotta be present for NOTHING type quests + _scheduler.Schedule(TimeSpan.FromSeconds(1), s => session.EmitEvent(new QuestCompletedEvent(characterQuest))); + } + + private bool IsPotentialMainQuestExploit(IClientSession session, QuestDto quest) + { + CompletedScriptsDto lastCompletedScript = session.PlayerEntity.GetLastCompletedScript(); + TutorialDto lastCompletedTutorial = _questManager.GetScriptTutorialByIndex(lastCompletedScript.ScriptId, lastCompletedScript.ScriptIndex); + + IReadOnlyCollection questScripts = _questManager.GetScriptsTutorialByType(TutorialActionType.START_QUEST); + TutorialDto questScript = _questManager.GetScriptsTutorialByType(TutorialActionType.START_QUEST).FirstOrDefault(s => s.Data == quest.Id && s.Id > lastCompletedTutorial.Id); + if (questScript == null) + { + return false; + } + + if (lastCompletedTutorial != null && lastCompletedTutorial.Id > questScript.Id) + { + session.NotifyStrangeBehavior(StrangeBehaviorSeverity.SEVERE_ABUSE, + $"Tried to send an already completed quest script! | ScriptId: {questScript.ScriptId}, ScriptIndex: {questScript.ScriptIndex}"); + return true; + } + + IEnumerable mustBeCompletedScripts = + questScripts.Where(s => s.ScriptId < questScript.ScriptId || s.ScriptId == questScript.ScriptId && s.ScriptIndex < questScript.ScriptIndex); + foreach (TutorialDto tutorialDto in mustBeCompletedScripts) + { + if (!session.PlayerEntity.HasCompletedScriptByIndex(tutorialDto.ScriptId, tutorialDto.ScriptIndex)) + { + session.NotifyStrangeBehavior(StrangeBehaviorSeverity.SEVERE_ABUSE, + $"Tried to send a quest script without having completed previous ones! | ScriptId: {questScript.ScriptId}, ScriptIndex: {questScript.ScriptIndex} | " + + $"The script that was incompleted and triggered this message was: ScriptId: {tutorialDto.ScriptId} ; ScriptIndex: {tutorialDto.ScriptIndex}"); + return true; // Let's hope doesn't explode + } + } + + return false; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.QuestImpl/Handlers/AddSecondaryQuestEventHandler.cs b/srcs/_plugins/Plugin.QuestImpl/Handlers/AddSecondaryQuestEventHandler.cs new file mode 100644 index 0000000..1184e25 --- /dev/null +++ b/srcs/_plugins/Plugin.QuestImpl/Handlers/AddSecondaryQuestEventHandler.cs @@ -0,0 +1,107 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using PhoenixLib.Logging; +using PhoenixLib.Scheduler; +using WingsAPI.Game.Extensions.Quests; +using WingsEmu.DTOs.Quests; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Quests; +using WingsEmu.Game.Quests.Event; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.QuestImpl.Handlers +{ + public class AddSecondaryQuestEventHandler : IAsyncEventProcessor + { + private readonly IGameLanguageService _gameLanguageService; + private readonly INpcRunTypeQuestsConfiguration _npcRunTypeQuestsConfiguration; + private readonly IPeriodicQuestsConfiguration _periodicQuestsConfiguration; + private readonly IQuestFactory _questFactory; + private readonly IQuestManager _questManager; + private readonly IScheduler _scheduler; + + public AddSecondaryQuestEventHandler(IQuestManager questManager, IScheduler scheduler, IGameLanguageService gameLanguageService, INpcRunTypeQuestsConfiguration npcRunTypeQuestsConfiguration, + IPeriodicQuestsConfiguration periodicQuestsConfiguration, IQuestFactory questFactory) + { + _questManager = questManager; + _scheduler = scheduler; + _gameLanguageService = gameLanguageService; + _npcRunTypeQuestsConfiguration = npcRunTypeQuestsConfiguration; + _periodicQuestsConfiguration = periodicQuestsConfiguration; + _questFactory = questFactory; + } + + public async Task HandleAsync(AddQuestEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + int questId = e.QuestId; + + if (e.QuestSlotType != QuestSlotType.SECONDARY) + { + return; + } + + QuestDto quest = _questManager.GetQuestById(questId); + if (quest == null) + { + Log.Debug($"[ERROR] Quest not found: {questId.ToString()}"); + return; + } + + // Checks if the player has that quest active or any quest related to the same questline + if (session.HasAlreadyQuestOrQuestline(quest, _questManager, _npcRunTypeQuestsConfiguration)) + { + session.SendMsg(_gameLanguageService.GetLanguage(GameDialogKey.QUEST_SHOUTMESSAGE_ALREADY_HAVE_QUEST, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (session.GetEmptyQuestSlot(e.QuestSlotType) == -1) + { + session.SendMsg(_gameLanguageService.GetLanguage(GameDialogKey.QUEST_SHOUTMESSAGE_SLOT_FULL, session.UserLanguage), MsgMessageType.Middle); + return; + } + + // Periodic quests check + bool hasCompletedPeriodicQuest = await session.HasCompletedPeriodicQuest(quest, _questManager, _npcRunTypeQuestsConfiguration, _periodicQuestsConfiguration); + if (_periodicQuestsConfiguration.IsDailyQuest(quest) && hasCompletedPeriodicQuest) + { + session.SendMsg(_gameLanguageService.GetLanguage(GameDialogKey.QUEST_SHOUTMESSAGE_ALREADY_COMPLETED_PERIODIC, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (!_periodicQuestsConfiguration.IsDailyQuest(quest) && session.PlayerEntity.HasCompletedQuest(questId)) + { + await session.NotifyStrangeBehavior(StrangeBehaviorSeverity.DANGER, $"Player {session.PlayerEntity.Name} tried to take an already completed non-daily quest ({quest.Id})"); + session.SendMsg(_gameLanguageService.GetLanguage(GameDialogKey.QUEST_SHOUTMESSAGE_ALREADY_COMPLETED, session.UserLanguage), MsgMessageType.Middle); + return; + } + + CharacterQuest characterQuest = _questFactory.NewQuest(session.PlayerEntity.Id, questId, e.QuestSlotType); + session.PlayerEntity.AddActiveQuest(characterQuest); + session.RefreshQuestList(_questManager, characterQuest.QuestId); + + await session.EmitEventAsync(new QuestAddedEvent + { + QuestId = characterQuest.QuestId, + QuestSlotType = characterQuest.SlotType + }); + + if (characterQuest.Quest.QuestType != QuestType.COMPLETE_TIMESPACE && characterQuest.Quest.QuestType != QuestType.DROP_IN_TIMESPACE && characterQuest.Quest.QuestType != QuestType.NOTHING + && quest.TargetMapId != 0) + { + session.SendTargetQuest(quest.TargetMapX, quest.TargetMapY, quest.TargetMapId, quest.Id); + return; + } + + // Still gotta be present for NOTHING type quests + _scheduler.Schedule(TimeSpan.FromSeconds(1), s => session.EmitEvent(new QuestCompletedEvent(characterQuest))); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.QuestImpl/Handlers/AddSoundFlowerQuestEventHandler.cs b/srcs/_plugins/Plugin.QuestImpl/Handlers/AddSoundFlowerQuestEventHandler.cs new file mode 100644 index 0000000..2478325 --- /dev/null +++ b/srcs/_plugins/Plugin.QuestImpl/Handlers/AddSoundFlowerQuestEventHandler.cs @@ -0,0 +1,83 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.Quests; +using WingsEmu.DTOs.Quests; +using WingsEmu.Game; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Quests; +using WingsEmu.Game.Quests.Configurations; +using WingsEmu.Game.Quests.Event; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.QuestImpl.Handlers +{ + public class AddSoundFlowerQuestEventHandler : IAsyncEventProcessor + { + private readonly IGameLanguageService _gameLanguageService; + private readonly IQuestManager _questManager; + private readonly IRandomGenerator _randomGenerator; + private readonly SoundFlowerConfiguration _soundFlowerConfiguration; + + public AddSoundFlowerQuestEventHandler(IQuestManager questManager, SoundFlowerConfiguration soundFlowerConfiguration, IRandomGenerator randomGenerator, + IGameLanguageService gameLanguageService) + { + _questManager = questManager; + _soundFlowerConfiguration = soundFlowerConfiguration; + _randomGenerator = randomGenerator; + _gameLanguageService = gameLanguageService; + } + + public async Task HandleAsync(AddSoundFlowerQuestEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + IPlayerEntity player = session.PlayerEntity; + SoundFlowerType soundFlowerType = e.SoundFlowerType; + + IReadOnlyCollection flowerQuests = soundFlowerType switch + { + SoundFlowerType.SOUND_FLOWER => _soundFlowerConfiguration.SoundFlowerQuestVnums.Select(s => _questManager.GetQuestById(s)).ToList(), + SoundFlowerType.WILD_SOUND_FLOWER => _soundFlowerConfiguration.WildSoundFlowerQuestVnums.Select(s => _questManager.GetQuestById(s)).ToList(), + _ => new List() + }; + + if (!flowerQuests.Any()) + { + return; + } + + IReadOnlyCollection possibleQuests = + flowerQuests.Where(s => s.MinLevel <= player.Level && player.Level <= s.MaxLevel && player.GetCurrentQuests().All(t => t.QuestId != s.Id)).ToList(); + if (!possibleQuests.Any()) + { + session.SendMsg(session.GetLanguage(GameDialogKey.QUEST_SHOUTMESSAGE_ALREADY_HAVE_QUEST), MsgMessageType.Middle); + return; + } + + if (session.GetEmptyQuestSlot(QuestSlotType.GENERAL, true) == -1) + { + session.SendMsg(_gameLanguageService.GetLanguage(GameDialogKey.QUEST_SHOUTMESSAGE_SLOT_FULL, session.UserLanguage), MsgMessageType.Middle); + return; + } + + QuestDto rndQuest = possibleQuests.ElementAt(_randomGenerator.RandomNumber(possibleQuests.Count)); + if (rndQuest == null) + { + return; + } + + if (e.SoundFlowerType == SoundFlowerType.SOUND_FLOWER) + { + session.PlayerEntity.DecreasePendingSoundFlowerQuests(); + } + + await e.Sender.EmitEventAsync(new AddQuestEvent(rndQuest.Id, QuestSlotType.GENERAL)); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.QuestImpl/Handlers/QuestCompletedEventHandler.cs b/srcs/_plugins/Plugin.QuestImpl/Handlers/QuestCompletedEventHandler.cs new file mode 100644 index 0000000..9aabb16 --- /dev/null +++ b/srcs/_plugins/Plugin.QuestImpl/Handlers/QuestCompletedEventHandler.cs @@ -0,0 +1,127 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using PhoenixLib.Logging; +using WingsAPI.Game.Extensions.Quests; +using WingsEmu.DTOs.Quests; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Quests; +using WingsEmu.Game.Quests.Event; + +namespace Plugin.QuestImpl.Handlers +{ + public class QuestCompletedEventHandler : IAsyncEventProcessor + { + private readonly IPeriodicQuestsConfiguration _periodicQuestsConfiguration; + private readonly IQuestManager _questManager; + + public QuestCompletedEventHandler(IQuestManager questManager, IPeriodicQuestsConfiguration periodicQuestsConfiguration) + { + _questManager = questManager; + _periodicQuestsConfiguration = periodicQuestsConfiguration; + } + + public async Task HandleAsync(QuestCompletedEvent e, CancellationToken cancellation) + { + CharacterQuest characterQuest = e.CharacterQuest; + IClientSession session = e.Sender; + bool claimReward = e.ClaimReward; + bool refreshProgress = e.RefreshProgress; + bool giveNextQuest = e.GiveNextQuest; + + Log.Debug($"[INFO] Received petition to finish quest: {characterQuest.QuestId}"); + + if (!session.PlayerEntity.IsQuestCompleted(characterQuest) && !e.IgnoreNotCompletedQuest) + { + Log.Debug($"[INFO] Quest not completed: {characterQuest.QuestId}"); + return; + } + + await HandleQuest(session, characterQuest, claimReward, refreshProgress, giveNextQuest); + } + + private async Task HandleQuest(IClientSession session, CharacterQuest characterQuest, bool claimReward, bool refreshProgress, bool giveNextQuest) + { + session.DeleteQuestTarget(characterQuest); + if (refreshProgress) + { + session.RefreshQuestProgress(_questManager, characterQuest.QuestId); + } + + TutorialDto qPayScript = _questManager.GetQuestPayScriptByQuestId(characterQuest.QuestId); + if (characterQuest.Quest.Prizes.Any()) + { + if (!claimReward && qPayScript != null) + { + return; + } + } + + await session.EmitEventAsync(new QuestRewardEvent + { + QuestId = characterQuest.QuestId, + ClaimReward = claimReward + }); + + if (_periodicQuestsConfiguration.IsDailyQuest(characterQuest.Quest)) + { + session.PlayerEntity.AddCompletedPeriodicQuest(characterQuest); + } + + PeriodicQuestSet periodicQuestSet = characterQuest.Quest.GetPeriodicQuestSet(_questManager, _periodicQuestsConfiguration); + if (periodicQuestSet != null) // If it's a continuation from a daily quest + { + if (periodicQuestSet.PerNoswingsAccount is true) + { + await _questManager.TryTakeDailyQuest(session.Account.MasterAccountId, periodicQuestSet.Id); + } + else + { + await _questManager.TryTakeDailyQuest(session.PlayerEntity.Id, periodicQuestSet.Id); + } + } + + await session.EmitEventAsync(new QuestRemoveEvent(characterQuest, true)); + + session.UpdateQuestSqstPacket(_questManager, characterQuest.QuestId); + + if (characterQuest.SlotType != QuestSlotType.MAIN) + { + if (characterQuest.Quest.NextQuestId == -1) + { + return; + } + + QuestDto nextQuest = _questManager.GetQuestById(characterQuest.Quest.NextQuestId); + if (nextQuest == null) + { + Log.Debug($"The quest with ID: {characterQuest.Quest.NextQuestId} was not found."); + return; + } + + if (!giveNextQuest) + { + return; + } + + await session.EmitEventAsync(new AddQuestEvent(nextQuest.Id, characterQuest.SlotType)); + return; + } + + // This should never happen (having no prizes and having a qpayScript at the same time) + if (!characterQuest.Quest.Prizes.Any() && qPayScript != null) + { + session.PlayerEntity.SaveScript(qPayScript.ScriptId, qPayScript.ScriptIndex, qPayScript.Type, DateTime.UtcNow); + + TutorialDto nextScript = _questManager.GetScriptTutorialById(qPayScript.Id + 1); + session.SendScriptPacket(nextScript.ScriptId, nextScript.ScriptIndex); + return; + } + + session.SendNextQuestScriptPacket(characterQuest, _questManager); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.QuestImpl/Handlers/QuestDailyRefreshEventHandler.cs b/srcs/_plugins/Plugin.QuestImpl/Handlers/QuestDailyRefreshEventHandler.cs new file mode 100644 index 0000000..503c03b --- /dev/null +++ b/srcs/_plugins/Plugin.QuestImpl/Handlers/QuestDailyRefreshEventHandler.cs @@ -0,0 +1,31 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Quests; +using WingsEmu.Game.Quests.Event; + +namespace Plugin.QuestImpl.Handlers +{ + public class QuestDailyRefreshEventHandler : IAsyncEventProcessor + { + private readonly IQuestManager _questManager; + + public QuestDailyRefreshEventHandler(IQuestManager questManager) => _questManager = questManager; + + public async Task HandleAsync(QuestDailyRefreshEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + bool canRefresh = await _questManager.CanRefreshDailyQuests(session.PlayerEntity.Id); + + if (canRefresh == false && e.Force == false) + { + session.SendDebugMessage("Daily quests already refreshed for today"); + return; + } + + session.PlayerEntity.ClearCompletedPeriodicQuests(); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.QuestImpl/Handlers/QuestHarvestEventHandler.cs b/srcs/_plugins/Plugin.QuestImpl/Handlers/QuestHarvestEventHandler.cs new file mode 100644 index 0000000..61de3ca --- /dev/null +++ b/srcs/_plugins/Plugin.QuestImpl/Handlers/QuestHarvestEventHandler.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.Quests; +using WingsEmu.DTOs.Quests; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Quests; +using WingsEmu.Game.Quests.Event; +using WingsEmu.Packets.Enums; + +namespace Plugin.QuestImpl.Handlers +{ + public class QuestHarvestEventHandler : IAsyncEventProcessor + { + private readonly IQuestManager _questManager; + + public QuestHarvestEventHandler(IQuestManager questManager) => _questManager = questManager; + + public async Task HandleAsync(QuestHarvestEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + IReadOnlyCollection characterQuests = session.PlayerEntity.GetCurrentQuestsByType(QuestType.COLLECT).ToList(); + if (!characterQuests.Any()) + { + return; + } + + foreach (CharacterQuest characterQuest in characterQuests) + { + IReadOnlyCollection objectives = characterQuest.Quest.Objectives; + foreach (QuestObjectiveDto objective in objectives) + { + CharacterQuestObjectiveDto questObjectiveDto = characterQuest.ObjectiveAmount[objective.ObjectiveIndex]; + if (e.ItemVnum != objective.Data1 || e.NpcVnum != objective.Data0 + || questObjectiveDto.CurrentAmount >= questObjectiveDto.RequiredAmount) + { + continue; + } + + questObjectiveDto.CurrentAmount++; + await session.EmitEventAsync(new QuestObjectiveUpdatedEvent + { + CharacterQuest = characterQuest + }); + + if (session.PlayerEntity.IsQuestCompleted(characterQuest)) + { + await session.EmitEventAsync(new QuestCompletedEvent(characterQuest)); + } + else + { + session.RefreshQuestProgress(_questManager, characterQuest.QuestId); + } + } + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.QuestImpl/Handlers/QuestItemPickUpEventHandler.cs b/srcs/_plugins/Plugin.QuestImpl/Handlers/QuestItemPickUpEventHandler.cs new file mode 100644 index 0000000..1ccf082 --- /dev/null +++ b/srcs/_plugins/Plugin.QuestImpl/Handlers/QuestItemPickUpEventHandler.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.ItemExtension.Item; +using WingsAPI.Game.Extensions.Quests; +using WingsEmu.DTOs.Quests; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Quests; +using WingsEmu.Game.Quests.Event; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.QuestImpl.Handlers +{ + public class QuestItemPickUpEventHandler : IAsyncEventProcessor + { + private static readonly QuestType[] DropQuests = { QuestType.DROP_CHANCE, QuestType.DROP_CHANCE_2, QuestType.DROP_HARDCODED, QuestType.DROP_IN_TIMESPACE }; + private readonly IGameLanguageService _gameLanguageService; + private readonly IItemsManager _itemsManager; + private readonly IQuestManager _questManager; + + public QuestItemPickUpEventHandler(IGameLanguageService gameLanguageService, IItemsManager itemsManager, IQuestManager questManager) + { + _gameLanguageService = gameLanguageService; + _itemsManager = itemsManager; + _questManager = questManager; + } + + public async Task HandleAsync(QuestItemPickUpEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + IEnumerable characterQuests = session.PlayerEntity.GetCurrentQuestsByTypes(DropQuests).ToArray(); + if (!characterQuests.Any()) + { + return; + } + + foreach (CharacterQuest characterQuest in characterQuests) + { + IReadOnlyCollection objectives = characterQuest.Quest.Objectives; + foreach (QuestObjectiveDto objective in objectives) + { + CharacterQuestObjectiveDto questObjectiveDto = characterQuest.ObjectiveAmount[objective.ObjectiveIndex]; + if (e.ItemVnum != (characterQuest.Quest.QuestType == QuestType.DROP_HARDCODED ? objective.Data0 : objective.Data1)) + { + continue; + } + + int amountLeft = questObjectiveDto.RequiredAmount - questObjectiveDto.CurrentAmount; + if (amountLeft == 0) + { + break; + } + + int questObjective = Math.Min(amountLeft, e.Amount); + questObjectiveDto.CurrentAmount += questObjective; + + await session.EmitEventAsync(new QuestObjectiveUpdatedEvent + { + CharacterQuest = characterQuest + }); + + if (session.PlayerEntity.IsQuestCompleted(characterQuest)) + { + await session.EmitEventAsync(new QuestCompletedEvent(characterQuest)); + } + else + { + session.RefreshQuestProgress(_questManager, characterQuest.QuestId); + } + + if (!e.SendMessage) + { + continue; + } + + string itemName = _itemsManager.GetItem(e.ItemVnum).GetItemName(_gameLanguageService, session.UserLanguage); + session.SendChatMessage(session.GetLanguageFormat(GameDialogKey.QUEST_CHATMESSAGE_ITEM_PICK_UP, itemName, + questObjectiveDto.CurrentAmount, questObjectiveDto.RequiredAmount), ChatMessageColorType.Red); + } + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.QuestImpl/Handlers/QuestMonsterDeathEventHandler.cs b/srcs/_plugins/Plugin.QuestImpl/Handlers/QuestMonsterDeathEventHandler.cs new file mode 100644 index 0000000..780e30b --- /dev/null +++ b/srcs/_plugins/Plugin.QuestImpl/Handlers/QuestMonsterDeathEventHandler.cs @@ -0,0 +1,90 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.Quests; +using WingsEmu.DTOs.Quests; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Quests; +using WingsEmu.Game.Quests.Event; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.QuestImpl.Handlers +{ + public class QuestMonsterDeathEventHandler : IAsyncEventProcessor + { + private static readonly HashSet _questTypes = new() { QuestType.KILL_MONSTER_BY_VNUM, QuestType.KILL_X_MOBS_SOUND_FLOWER }; + private readonly IGameLanguageService _gameLanguage; + private readonly IQuestManager _questManager; + + public QuestMonsterDeathEventHandler(IGameLanguageService gameLanguage, IQuestManager questManager) + { + _gameLanguage = gameLanguage; + _questManager = questManager; + } + + public async Task HandleAsync(QuestMonsterDeathEvent e, CancellationToken cancellation) + { + IMonsterEntity monsterEntity = e.MonsterEntity; + IClientSession session = e.Sender; + + IEnumerable killingQuests = session.PlayerEntity.GetCurrentQuestsByTypes(_questTypes).ToArray(); + if (!killingQuests.Any()) + { + return; + } + + foreach (CharacterQuest characterQuest in killingQuests) + { + IReadOnlyCollection objectives = characterQuest.Quest.Objectives; + foreach (QuestObjectiveDto objective in objectives) + { + if (objective.Data0 != monsterEntity.MonsterVNum && characterQuest.Quest.QuestType != QuestType.KILL_X_MOBS_SOUND_FLOWER) + { + continue; + } + + if (characterQuest.Quest.QuestType == QuestType.KILL_X_MOBS_SOUND_FLOWER) + { + if (session.PlayerEntity.Level - monsterEntity.Level > 10) + { + continue; + } + } + + CharacterQuestObjectiveDto questObjectiveDto = characterQuest.ObjectiveAmount[objective.ObjectiveIndex]; + if (questObjectiveDto.CurrentAmount < questObjectiveDto.RequiredAmount) + { + questObjectiveDto.CurrentAmount++; + await session.EmitEventAsync(new QuestObjectiveUpdatedEvent + { + CharacterQuest = characterQuest + }); + + if (characterQuest.Quest.QuestType != QuestType.KILL_X_MOBS_SOUND_FLOWER) + { + string monsterName = _gameLanguage.GetLanguage(GameDataType.NpcMonster, monsterEntity.Name, session.UserLanguage); + session.SendChatMessage(string.Format(_gameLanguage + .GetLanguage(GameDialogKey.QUEST_CHATMESSAGE_X_HUNTING_Y_Z, session.UserLanguage), monsterName, questObjectiveDto.CurrentAmount, questObjectiveDto.RequiredAmount), + ChatMessageColorType.Red); + } + } + + if (session.PlayerEntity.IsQuestCompleted(characterQuest)) + { + await session.EmitEventAsync(new QuestCompletedEvent(characterQuest)); + } + else + { + session.RefreshQuestProgress(_questManager, characterQuest.QuestId); + } + } + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.QuestImpl/Handlers/QuestNpcTalkEventHandler.cs b/srcs/_plugins/Plugin.QuestImpl/Handlers/QuestNpcTalkEventHandler.cs new file mode 100644 index 0000000..f78cc4d --- /dev/null +++ b/srcs/_plugins/Plugin.QuestImpl/Handlers/QuestNpcTalkEventHandler.cs @@ -0,0 +1,265 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsAPI.Game.Extensions.Quests; +using WingsEmu.DTOs.Quests; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Quests; +using WingsEmu.Game.Quests.Event; +using WingsEmu.Packets.Enums; + +namespace Plugin.QuestImpl.Handlers +{ + public class QuestNpcTalkEventHandler : IAsyncEventProcessor + { + private readonly IItemsManager _itemsManager; + private readonly IQuestManager _questManager; + private readonly IServerManager _serverManager; + + public QuestNpcTalkEventHandler(IItemsManager itemsManager, IServerManager serverManager, IQuestManager questManager) + { + _itemsManager = itemsManager; + _serverManager = serverManager; + _questManager = questManager; + } + + public async Task HandleAsync(QuestNpcTalkEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + CharacterQuest characterQuest = e.CharacterQuest; + INpcEntity npcEntity = e.NpcEntity; + bool isByBlueAlertNrun = e.IsByBlueAlertNrun; + + if (!session.PlayerEntity.IsInAoeZone(npcEntity, _serverManager.MaxNpcTalkRange)) + { + return; + } + + switch (characterQuest.Quest.QuestType) + { + case QuestType.DIALOG: //12 + case QuestType.DIALOG_2: //22 + case QuestType.DELIVER_ITEM_TO_NPC: //4 + case QuestType.GIVE_ITEM_TO_NPC: //14 (Same than before but without random values) + case QuestType.GIVE_ITEM_TO_NPC_2: //24 + case QuestType.GIVE_NPC_GOLD: //18 + HandleDeliver(session, characterQuest, npcEntity, isByBlueAlertNrun); + break; + case QuestType.DIALOG_WHILE_WEARING: //15 + HandleWearing(session, characterQuest, npcEntity); + break; + case QuestType.DIALOG_WHILE_HAVING_ITEM: //16 + HandleHaving(session, characterQuest, npcEntity); + break; + case QuestType.WIN_RAID_AND_TALK_TO_NPC: //25 + await HandleRaid(session, characterQuest, npcEntity); + break; + } + } + + public void HandleDeliver(IClientSession session, CharacterQuest characterQuest, INpcEntity npcEntity, bool isByBlueAlertNrun) + { + IEnumerable objectives = characterQuest.Quest.Objectives; + foreach (QuestObjectiveDto objective in objectives) + { + if (npcEntity.NpcVNum != (characterQuest.Quest.QuestType is QuestType.DELIVER_ITEM_TO_NPC ? objective.Data1 : objective.Data0)) + { + continue; + } + + CharacterQuestObjectiveDto questObjectiveDto = characterQuest.ObjectiveAmount[objective.ObjectiveIndex]; + switch (characterQuest.Quest.QuestType) + { + case QuestType.DELIVER_ITEM_TO_NPC: + case QuestType.GIVE_ITEM_TO_NPC: + + int amountLeft = questObjectiveDto.RequiredAmount - questObjectiveDto.CurrentAmount; + if (amountLeft == 0) + { + break; + } + + int amountInPossession = session.PlayerEntity.CountItemWithVnum(characterQuest.Quest.QuestType is QuestType.DELIVER_ITEM_TO_NPC ? objective.Data0 : objective.Data1); + if (amountInPossession == 0) + { + break; + } + + int amountToRemove = Math.Min(amountLeft, amountInPossession); + session.RemoveItemFromInventory(characterQuest.Quest.QuestType is QuestType.DELIVER_ITEM_TO_NPC ? objective.Data0 : objective.Data1, (short)amountToRemove); + questObjectiveDto.CurrentAmount += amountToRemove; + + session.EmitEvent(new QuestObjectiveUpdatedEvent + { + CharacterQuest = characterQuest + }); + + session.RefreshQuestProgress(_questManager, characterQuest.QuestId); + + if (session.PlayerEntity.IsQuestCompleted(characterQuest)) + { + session.EmitEvent(new QuestCompletedEvent(characterQuest)); + } + + break; + case QuestType.GIVE_NPC_GOLD: + int totalGoldToGive = questObjectiveDto.RequiredAmount; // It is the way it's stored + if (session.PlayerEntity.Gold < totalGoldToGive) + { + continue; + } + + questObjectiveDto.CurrentAmount += totalGoldToGive; + session.EmitEvent(new QuestObjectiveUpdatedEvent + { + CharacterQuest = characterQuest + }); + + session.PlayerEntity.RemoveGold(totalGoldToGive); + session.RefreshQuestProgress(_questManager, characterQuest.QuestId); + + session.EmitEvent(new QuestCompletedEvent(characterQuest)); + break; + + case QuestType.DIALOG: + case QuestType.DIALOG_2: + + // They have a special behavior + if (_questManager.IsNpcBlueAlertQuest(characterQuest.QuestId) && !isByBlueAlertNrun) + { + continue; + } + + questObjectiveDto.CurrentAmount++; + session.EmitEvent(new QuestObjectiveUpdatedEvent + { + CharacterQuest = characterQuest + }); + + bool giveNextQuest = !_questManager.IsNpcBlueAlertQuest(characterQuest.QuestId) || + _questManager.IsNpcBlueAlertQuest(characterQuest.QuestId) && !session.PlayerEntity.HasCompletedQuest(characterQuest.Quest.NextQuestId); + if (session.PlayerEntity.IsQuestCompleted(characterQuest)) + { + session.EmitEvent(new QuestCompletedEvent(characterQuest, true, giveNextQuest: giveNextQuest)); + } + + break; + + case QuestType.GIVE_ITEM_TO_NPC_2: + + if (objective.Data0 != npcEntity.NpcVNum) + { + return; + } + + amountLeft = questObjectiveDto.RequiredAmount - questObjectiveDto.CurrentAmount; + if (amountLeft == 0) + { + break; + } + + questObjectiveDto.CurrentAmount++; + session.RefreshQuestProgress(_questManager, characterQuest.QuestId); + session.EmitEvent(new QuestObjectiveUpdatedEvent + { + CharacterQuest = characterQuest + }); + + if (session.PlayerEntity.IsQuestCompleted(characterQuest)) + { + session.EmitEvent(new QuestCompletedEvent(characterQuest, true, giveNextQuest: _questManager.IsNpcBlueAlertQuest(characterQuest.QuestId))); + } + + break; + } + } + } + + public async Task HandleRaid(IClientSession session, CharacterQuest characterQuest, INpcEntity npcEntity) + { + IEnumerable objectives = characterQuest.Quest.Objectives; + foreach (QuestObjectiveDto objective in objectives) + { + if (npcEntity.NpcVNum != objective.Data2) + { + continue; + } + + CharacterQuestObjectiveDto questObjectiveDto = characterQuest.ObjectiveAmount[objective.ObjectiveIndex]; + int amountLeft = questObjectiveDto.RequiredAmount - questObjectiveDto.CurrentAmount; + if (amountLeft != 0) + { + return; + } + + await session.EmitEventAsync(new QuestCompletedEvent(characterQuest, true, ignoreNotCompletedQuest: true)); + } + } + + public void HandleWearing(IClientSession session, CharacterQuest characterQuest, INpcEntity npcEntity) + { + IEnumerable objectives = characterQuest.Quest.Objectives; + foreach (QuestObjectiveDto objective in objectives) + { + if (npcEntity.NpcVNum != objective.Data0) + { + continue; + } + + IGameItem gameItem = _itemsManager.GetItem(objective.Data1); + if (gameItem == null) + { + continue; + } + + GameItemInstance inv = session.PlayerEntity.GetItemInstanceFromEquipmentSlot(gameItem.EquipmentSlot); + if (inv == null || inv.ItemVNum != objective.Data1) + { + continue; + } + + CharacterQuestObjectiveDto questObjectiveDto = characterQuest.ObjectiveAmount[objective.ObjectiveIndex]; + questObjectiveDto.CurrentAmount++; + } + + // Checks that all the requirements have been met + if (!session.PlayerEntity.IsQuestCompleted(characterQuest)) + { + characterQuest.ResetQuestProgress(); + } + + else + { + session.EmitEventAsync(new QuestObjectiveUpdatedEvent + { + CharacterQuest = characterQuest + }); + session.EmitEvent(new QuestCompletedEvent(characterQuest)); + } + } + + public void HandleHaving(IClientSession session, CharacterQuest characterQuest, INpcEntity npcEntity) + { + IEnumerable objectives = characterQuest.Quest.Objectives; + foreach (QuestObjectiveDto objective in objectives) + { + CharacterQuestObjectiveDto questObjectiveDto = characterQuest.ObjectiveAmount[objective.ObjectiveIndex]; + if (npcEntity.NpcVNum != objective.Data0 || session.PlayerEntity.CountItemWithVnum(objective.Data1) < questObjectiveDto.RequiredAmount) + { + return; + } + } + + session.EmitEvent(new QuestCompletedEvent(characterQuest)); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.QuestImpl/Handlers/QuestRemoveEventHandler.cs b/srcs/_plugins/Plugin.QuestImpl/Handlers/QuestRemoveEventHandler.cs new file mode 100644 index 0000000..77ac6a6 --- /dev/null +++ b/srcs/_plugins/Plugin.QuestImpl/Handlers/QuestRemoveEventHandler.cs @@ -0,0 +1,60 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.CharacterExtensions; +using WingsAPI.Game.Extensions.Quests; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Quests; +using WingsEmu.Game.Quests.Event; + +namespace Plugin.QuestImpl.Handlers +{ + public class QuestRemoveEventHandler : IAsyncEventProcessor + { + private readonly IPeriodicQuestsConfiguration _periodicQuestsConfiguration; + private readonly IQuestManager _questManager; + + public QuestRemoveEventHandler(IQuestManager questManager, IPeriodicQuestsConfiguration periodicQuestsConfiguration) + { + _questManager = questManager; + _periodicQuestsConfiguration = periodicQuestsConfiguration; + } + + public async Task HandleAsync(QuestRemoveEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + + if (e.CharacterQuest == null) + { + return; + } + + if (e.IsCompleted) + { + session.PlayerEntity.AddCompletedQuest(e.CharacterQuest); + await session.EmitEventAsync(new QuestCompletedLogEvent + { + CharacterQuest = e.CharacterQuest, + Location = e.Sender.GetLocation() + }); + } + else + { + if (_periodicQuestsConfiguration.IsDailyQuest(e.CharacterQuest.Quest)) + { + session.PlayerEntity.AddCompletedPeriodicQuest(e.CharacterQuest); + } + + await session.EmitEventAsync(new QuestAbandonedEvent + { + QuestId = e.CharacterQuest.QuestId, + QuestSlotType = e.CharacterQuest.SlotType + }); + } + + session.PlayerEntity.RemoveActiveQuest(e.CharacterQuest.QuestId); + session.RefreshQuestList(_questManager, null); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.QuestImpl/Handlers/QuestRewardEventHandler.cs b/srcs/_plugins/Plugin.QuestImpl/Handlers/QuestRewardEventHandler.cs new file mode 100644 index 0000000..40fbed5 --- /dev/null +++ b/srcs/_plugins/Plugin.QuestImpl/Handlers/QuestRewardEventHandler.cs @@ -0,0 +1,323 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsAPI.Game.Extensions.Quests; +using WingsAPI.Game.Extensions.Quicklist; +using WingsEmu.DTOs.Quests; +using WingsEmu.DTOs.Skills; +using WingsEmu.Game; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Algorithm; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Quests; +using WingsEmu.Game.Quests.Configurations; +using WingsEmu.Game.Quests.Event; +using WingsEmu.Game.Skills; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Character; + +namespace Plugin.QuestImpl.Handlers +{ + public class QuestRewardEventHandler : IAsyncEventProcessor + { + private static readonly HashSet SkillsToLearn = new() + { + (int)SkillsVnums.BEGINNER_PHYSICAL_STRENGTH, (int)SkillsVnums.BEGINNER_SPEED, + (int)SkillsVnums.BEGINNER_INTELLIGENCE, (int)SkillsVnums.BEGINNER_HP_RECOVERY + }; + + private static readonly HashSet TarotRewards = new() + { + (int)ItemVnums.TAROT_FOOL, (int)ItemVnums.TAROT_MAGICIAN, (int)ItemVnums.TAROT_LOVERS, (int)ItemVnums.TAROT_HERMIT, (int)ItemVnums.TAROT_DEATH, + (int)ItemVnums.TAROT_DEVIL, (int)ItemVnums.TAROT_TOWER, (int)ItemVnums.TAROT_STAR, (int)ItemVnums.TAROT_MOON, (int)ItemVnums.TAROT_SUN + }; + + private readonly IBuffFactory _buffFactory; + private readonly ICharacterAlgorithm _characterAlgorithm; + private readonly IDropRarityConfigurationProvider _dropRarityConfigurationProvider; + private readonly IGameItemInstanceFactory _gameItemInstanceFactory; + private readonly IGameLanguageService _gameLanguageService; + private readonly GameMinMaxConfiguration _gameMinMaxConfiguration; + private readonly IItemsManager _itemsManager; + private readonly IQuestManager _questManager; + private readonly QuestsRatesConfiguration _questsRates; + private readonly IRandomGenerator _randomGenerator; + private readonly IRankingManager _rankingManager; + private readonly IReputationConfiguration _reputationConfiguration; + private readonly ISkillsManager _skillsManager; + private readonly SoundFlowerConfiguration _soundFlowerConfiguration; + + public QuestRewardEventHandler(IGameItemInstanceFactory gameItemInstanceFactory, ICharacterAlgorithm characterAlgorithm, IRandomGenerator randomGenerator, ISkillsManager skillsManager, + IItemsManager itemsManager, QuestsRatesConfiguration questsRates, GameMinMaxConfiguration gameMinMaxConfiguration, IGameLanguageService gameLanguageService, + IDropRarityConfigurationProvider dropRarityConfigurationProvider, IQuestManager questManager, IReputationConfiguration reputationConfiguration, IRankingManager rankingManager, + SoundFlowerConfiguration soundFlowerConfiguration, IBuffFactory buffFactory) + { + _gameItemInstanceFactory = gameItemInstanceFactory; + _characterAlgorithm = characterAlgorithm; + _randomGenerator = randomGenerator; + _skillsManager = skillsManager; + _itemsManager = itemsManager; + _questsRates = questsRates; + _gameMinMaxConfiguration = gameMinMaxConfiguration; + _gameLanguageService = gameLanguageService; + _dropRarityConfigurationProvider = dropRarityConfigurationProvider; + _questManager = questManager; + _reputationConfiguration = reputationConfiguration; + _rankingManager = rankingManager; + _soundFlowerConfiguration = soundFlowerConfiguration; + _buffFactory = buffFactory; + } + + public async Task HandleAsync(QuestRewardEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + + CharacterQuest characterQuest = session.PlayerEntity.GetQuestById(e.QuestId); + if (characterQuest == null) + { + return; + } + + if (characterQuest.Quest.Prizes.Any()) + { + TutorialDto qPayScript = _questManager.GetQuestPayScriptByQuestId(characterQuest.QuestId); + if (!e.ClaimReward && qPayScript != null) + { + return; + } + + // We only really care about the random reward given to QuestRewardType.RandomReward + IReadOnlyCollection rndRewards = GiveRewards(session, characterQuest); + session.SendQrPacket(characterQuest, rndRewards, _questsRates); + if (qPayScript != null) + { + session.PlayerEntity.SaveScript(qPayScript.ScriptId, qPayScript.ScriptIndex, TutorialActionType.WAIT_FOR_REWARDS_CLAIM, DateTime.UtcNow); + } + } + + if (characterQuest.Quest.QuestType != QuestType.NOTHING) + { + session.PlayerEntity.AddDignity(50, _gameMinMaxConfiguration, _gameLanguageService, _reputationConfiguration, _rankingManager.TopReputation); + } + + if (_soundFlowerConfiguration.WildSoundFlowerQuestVnums.Contains(characterQuest.QuestId)) + { + int rndBuffVnum = _soundFlowerConfiguration.PossibleBuffs.ElementAt(_randomGenerator.RandomNumber(_soundFlowerConfiguration.PossibleBuffs.Count)); + + Buff rndBuff = _buffFactory.CreateBuff(rndBuffVnum, session.PlayerEntity); + await session.PlayerEntity.AddBuffsAsync(new[] { rndBuff }); + } + + HandleSpecialQuestsRewards(session, characterQuest); + } + + private IReadOnlyCollection GiveRewards(IClientSession session, CharacterQuest characterQuest) + { + IEnumerable prizes = characterQuest.Quest.Prizes; + if (prizes == null) + { + return Array.Empty(); + } + + List generatedRewards = new(); + foreach (QuestPrizeDto prize in prizes) + { + GameItemInstance itemToAdd; + switch (prize.RewardType) + { + case (byte)QuestRewardType.Gold: + session.EmitEventAsync(new GenerateGoldEvent(prize.Data0 * _questsRates.GoldRate, true)); + break; + case (byte)QuestRewardType.SecondGold: + session.EmitEventAsync(new GenerateGoldEvent(prize.Data0 * _questsRates.BaseGold * _questsRates.GoldRate, true)); + break; + case (byte)QuestRewardType.Exp: + session.EmitEventAsync(new AddExpEvent(_characterAlgorithm.GetLevelXpPercentage((short)prize.Data0, (short)prize.Data1) * _questsRates.XpRate, LevelType.Level)); + break; + case (byte)QuestRewardType.SecondExp: + session.EmitEventAsync(new AddExpEvent(prize.Data0 * _questsRates.BaseXp * _questsRates.XpRate, LevelType.Level)); + break; + case (byte)QuestRewardType.JobExp: + long exp = session.PlayerEntity.UseSp && session.PlayerEntity.Specialist != null + ? _characterAlgorithm.GetSpecialistJobXpPercentage((short)prize.Data0, (short)prize.Data1, session.PlayerEntity.Specialist.IsFunSpecialist()) * _questsRates.JobXpRate + : _characterAlgorithm.GetJobXpPercentage((short)prize.Data0, (short)prize.Data1) * _questsRates.JobXpRate; + session.EmitEventAsync(new AddExpEvent(exp, session.PlayerEntity.UseSp && session.PlayerEntity.Specialist != null ? LevelType.SpJobLevel : LevelType.JobLevel)); + break; + case (byte)QuestRewardType.RandomReward: + var possibleRewards = new[] { prize.Data0, prize.Data1, prize.Data2, prize.Data3 }.Where(s => s != -1).ToList(); + int rndRewardVnum = possibleRewards[_randomGenerator.RandomNumber(possibleRewards.Count)]; + + itemToAdd = _gameItemInstanceFactory.CreateItem(rndRewardVnum, prize.Data4); + session.AddNewItemToInventory(itemToAdd, sendGiftIsFull: true); + generatedRewards.Add(new CharacterQuestGeneratedReward + { + ItemVnum = rndRewardVnum, + Amount = prize.Data4 + }); + break; + case (byte)QuestRewardType.AllRewards: + foreach (int itemVnum in new[] { prize.Data0, prize.Data1, prize.Data2, prize.Data3 }) + { + if (itemVnum == -1) + { + continue; + } + + IGameItem item = _itemsManager.GetItem(itemVnum); + sbyte randomRarity = _dropRarityConfigurationProvider.GetRandomRarity(item.ItemType); + + itemToAdd = _gameItemInstanceFactory.CreateItem(itemVnum, 1, 0, randomRarity); + session.AddNewItemToInventory(itemToAdd, sendGiftIsFull: true); + } + + break; + case (byte)QuestRewardType.Reput: + session.EmitEventAsync(new GenerateReputationEvent { Amount = prize.Data0 * _questsRates.ReputRate, SendMessage = true }); + break; + case (byte)QuestRewardType.ThirdGold: + session.EmitEventAsync(new GenerateGoldEvent(prize.Data0 * characterQuest.ObjectiveAmount.Sum(s => s.Value.RequiredAmount) * session.PlayerEntity.Level * _questsRates.GoldRate, + true)); + break; + case (byte)QuestRewardType.ThirdExp: + session.EmitEventAsync(new AddExpEvent(prize.Data0 * characterQuest.ObjectiveAmount.Sum(s => s.Value.RequiredAmount) * session.PlayerEntity.Level * _questsRates.XpRate, + LevelType.Level)); + break; + case (byte)QuestRewardType.SecondJobExp: + session.EmitEventAsync(new AddExpEvent(prize.Data0 * characterQuest.ObjectiveAmount.Sum(s => s.Value.RequiredAmount) * session.PlayerEntity.Level * _questsRates.JobXpRate, + LevelType.JobLevel)); + break; + case (byte)QuestRewardType.Unknow: + break; + case (byte)QuestRewardType.ItemsDependingOnClass: + GameItemInstance itemDependingOnClass; + IGameItem gameItem; + sbyte randomItemRarity; + + switch (session.PlayerEntity.Class) + { + case ClassType.Swordman: + gameItem = _itemsManager.GetItem(prize.Data0); + if (gameItem == null) + { + continue; + } + + randomItemRarity = _dropRarityConfigurationProvider.GetRandomRarity(gameItem.ItemType); + + itemDependingOnClass = _gameItemInstanceFactory.CreateItem(prize.Data0, prize.Data4, 0, randomItemRarity); + break; + case ClassType.Archer: + gameItem = _itemsManager.GetItem(prize.Data1); + if (gameItem == null) + { + continue; + } + + randomItemRarity = _dropRarityConfigurationProvider.GetRandomRarity(gameItem.ItemType); + + itemDependingOnClass = _gameItemInstanceFactory.CreateItem(prize.Data1, prize.Data4, 0, randomItemRarity); + break; + case ClassType.Magician: + gameItem = _itemsManager.GetItem(prize.Data2); + if (gameItem == null) + { + continue; + } + + randomItemRarity = _dropRarityConfigurationProvider.GetRandomRarity(gameItem.ItemType); + + itemDependingOnClass = _gameItemInstanceFactory.CreateItem(prize.Data2, prize.Data4, 0, randomItemRarity); + break; + default: + gameItem = _itemsManager.GetItem(prize.Data3); + if (gameItem == null) + { + continue; + } + + randomItemRarity = _dropRarityConfigurationProvider.GetRandomRarity(gameItem.ItemType); + + itemDependingOnClass = _gameItemInstanceFactory.CreateItem(prize.Data3, prize.Data4, 0, randomItemRarity); + break; + } + + if (itemDependingOnClass == null) + { + return Array.Empty(); + } + + session.AddNewItemToInventory(itemDependingOnClass, sendGiftIsFull: true); + break; + } + } + + return generatedRewards; + } + + private void HandleSpecialQuestsRewards(IClientSession session, CharacterQuest characterQuest) + { + switch (characterQuest.QuestId) + { + case (short)QuestsVnums.SORAYA_LUNCH_TO_CALVIN: + GameItemInstance calvinLunch = _gameItemInstanceFactory.CreateItem((short)ItemVnums.DELICIOUS_LUNCH); + session.AddNewItemToInventory(calvinLunch, true, sendGiftIsFull: true); + break; + case (short)QuestsVnums.GIVE_MALCOM_ADVENTURER_SHOES: + GameItemInstance malcomShoes = _gameItemInstanceFactory.CreateItem((short)ItemVnums.ADVENTURER_SHOES, 1, 3); + malcomShoes.DarkResistance += (short)(malcomShoes.GameItem.DarkResistance * 3); + session.AddNewItemToInventory(malcomShoes, true, sendGiftIsFull: true); + break; + case (short)QuestsVnums.CALVIN_ADVENTURER_TRAINING_SKILLS: + foreach (SkillDTO ski in _skillsManager.GetSkills()) + { + if (!SkillsToLearn.Contains(ski.Id)) + { + continue; + } + + // Find higher passive already in PlayerEntity + CharacterSkill findHigherPassive = session.PlayerEntity.CharacterSkills.Values.FirstOrDefault(x => x.Skill.IsPassiveSkill() + && x.Skill.CastId == ski.CastId && x.Skill.Id > ski.Id); + + if (findHigherPassive != null) + { + continue; + } + + var passive = new CharacterSkill { SkillVNum = ski.Id }; + + session.PlayerEntity.CharacterSkills[ski.Id] = passive; + session.PlayerEntity.Skills.Add(passive); + } + + session.PlayerEntity.ClearSkillCooldowns(); + session.RefreshPassiveBCards(); + session.RefreshSkillList(); + session.RefreshQuicklist(); + break; + case (short)QuestsVnums.FORTUNE_TELLER_1: + case (short)QuestsVnums.FORTUNE_TELLER_2: + case (short)QuestsVnums.FORTUNE_TELLER_3: + case (short)QuestsVnums.FORTUNE_TELLER_4: + case (short)QuestsVnums.FORTUNE_TELLER_5: + int randomTarotVnum = TarotRewards.ElementAt(_randomGenerator.RandomNumber(TarotRewards.Count)); + GameItemInstance randomTarot = _gameItemInstanceFactory.CreateItem((short)randomTarotVnum); + session.AddNewItemToInventory(randomTarot, true, sendGiftIsFull: true); + break; + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.QuestImpl/Managers/QuestManager.cs b/srcs/_plugins/Plugin.QuestImpl/Managers/QuestManager.cs new file mode 100644 index 0000000..968085d --- /dev/null +++ b/srcs/_plugins/Plugin.QuestImpl/Managers/QuestManager.cs @@ -0,0 +1,222 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using PhoenixLib.Caching; +using PhoenixLib.DAL.Redis.Locks; +using PhoenixLib.Logging; +using WingsAPI.Data.GameData; +using WingsEmu.Core.Extensions; +using WingsEmu.DTOs.Quests; +using WingsEmu.Game.Quests; + +namespace Plugin.QuestImpl.Managers +{ + public class QuestManager : IQuestManager + { + private readonly IExpirableLockService _lockService; + private readonly ILongKeyCachedRepository _questCache; + private readonly Dictionary> _questlines = new(); + private readonly IKeyValueCache> _questNameCache; + private readonly Dictionary _questNpcById = new(); + private readonly IResourceLoader _questNpcResourceLoader; + + private readonly IResourceLoader _questResourceLoader; + private readonly Dictionary _tutorialDataById = new(); + private readonly Dictionary> _tutorialDataByScriptId = new(); + private readonly Dictionary> _tutorialDataByType = new(); + private readonly Dictionary _tutorialQuestPayScriptByQuestId = new(); + private readonly IResourceLoader _tutorialResourceLoader; + + + public QuestManager(ILongKeyCachedRepository questCache, IResourceLoader questResourceLoader, IResourceLoader tutorialResourceLoader, + IResourceLoader questNpcResourceLoader, IExpirableLockService lockService, IKeyValueCache> questNameCache) + { + _questCache = questCache; + _questResourceLoader = questResourceLoader; + _questNpcResourceLoader = questNpcResourceLoader; + _tutorialResourceLoader = tutorialResourceLoader; + _lockService = lockService; + _questNameCache = questNameCache; + } + + public IReadOnlyCollection GetNpcBlueAlertQuests() => _questNpcById.Values.Where(s => s.QuestId != null).ToList(); + public QuestNpcDto GetNpcBlueAlertQuestByQuestId(int questId) => _questNpcById.Values.FirstOrDefault(s => s.QuestId == questId); + + public async Task CanRefreshDailyQuests(long characterId) => + await _lockService.TryAddTemporaryLockAsync($"game:locks:quest-daily-refresh:{characterId}", DateTime.UtcNow.Date.AddDays(1)); + + public async Task TryTakeDailyQuest(Guid masterAccId, int questPackId) => + await _lockService.TryAddTemporaryLockAsync($"game:locks:quest-daily-master:{masterAccId}:{questPackId.ToString()}", DateTime.UtcNow.Date.AddDays(1)); + + public async Task TryTakeDailyQuest(long characterId, int questPackId) => + await _lockService.TryAddTemporaryLockAsync($"game:locks:quest-daily-char:{characterId}:{questPackId.ToString()}", DateTime.UtcNow.Date.AddDays(1)); + + public bool IsNpcBlueAlertQuest(int questId) => _questNpcById.Values.Any(s => s.QuestId == questId); + + public List GetQuestByName(string name) => _questNameCache.Get(name); + + public async Task InitializeAsync() + { + int questCounter = 0; + int scriptsCounter = 0; + int objectivesCounter = 0; + int prizesCounter = 0; + int questsNpcCounter = 0; + + IEnumerable quests = await _questResourceLoader.LoadAsync(); + IEnumerable tutorialScripts = await _tutorialResourceLoader.LoadAsync(); + IEnumerable questsNpc = await _questNpcResourceLoader.LoadAsync(); + + var nextQuests = new Dictionary>(); + foreach (QuestDto questDto in quests) + { + if (questDto.NextQuestId == -1) + { + continue; + } + + if (!nextQuests.ContainsKey(questDto.Id)) + { + nextQuests.Add(questDto.Id, new List()); + } + + nextQuests[questDto.Id].Add(questDto.NextQuestId); + } + + nextQuests = BuildQuestline(nextQuests); + foreach (QuestDto quest in quests) + { + _questlines.TryAdd(quest.Id, nextQuests.ContainsKey(quest.Id) ? nextQuests[quest.Id] : new List()); + + _questCache.Set(quest.Id, quest); + _questNameCache.GetOrSet(quest.Name, () => new List()).Add(quest); + if (quest.NextQuestId != -1) + { + if (!nextQuests.ContainsKey(quest.Id)) + { + nextQuests.Add(quest.Id, new List()); + } + + nextQuests[quest.Id].Add(quest.NextQuestId); + } + + questCounter++; + objectivesCounter += quest.Objectives.Count; + prizesCounter += quest.Prizes.Count; + } + + foreach (TutorialDto tutorialDto in tutorialScripts) + { + if (!_tutorialDataByType.ContainsKey(tutorialDto.Type)) + { + _tutorialDataByType.Add(tutorialDto.Type, new List()); + } + + if (!_tutorialDataByScriptId.ContainsKey(tutorialDto.ScriptId)) + { + _tutorialDataByScriptId.Add(tutorialDto.ScriptId, new List()); + } + + if (tutorialDto.Type == TutorialActionType.WAIT_FOR_REWARDS_CLAIM) + { + _tutorialQuestPayScriptByQuestId.Add(tutorialDto.Data, tutorialDto); + } + + _tutorialDataById.TryAdd(tutorialDto.Id, tutorialDto); + _tutorialDataByType[tutorialDto.Type].Add(tutorialDto); + _tutorialDataByScriptId[tutorialDto.ScriptId].Add(tutorialDto); + ; + scriptsCounter++; + } + + foreach (QuestNpcDto questNpcDto in questsNpc) + { + _questNpcById.TryAdd(questNpcDto.Id, questNpcDto); + questsNpcCounter++; + } + + Log.Info($"[RESOURCES] Loaded {questCounter} quests."); + Log.Info($"[RESOURCES] Loaded {objectivesCounter} quests objectives."); + Log.Info($"[RESOURCES] Loaded {prizesCounter} quests rewards."); + Log.Info($"[RESOURCES] Loaded {scriptsCounter} tutorial scripts."); + Log.Info($"[RESOURCES] Loaded {questsNpcCounter} NPC quests."); + } + + public QuestDto GetQuestById(int questId) => _questCache.Get(questId); + + public IReadOnlyCollection GetScriptsTutorialByType(TutorialActionType type) => _tutorialDataByType.ContainsKey(type) ? _tutorialDataByType[type] : Array.Empty(); + + public IReadOnlyCollection GetScriptsTutorialByScriptId(int scriptId) => + _tutorialDataByScriptId.ContainsKey(scriptId) ? _tutorialDataByScriptId[scriptId] : Array.Empty(); + + public IReadOnlyCollection GetScriptsTutorial() => _tutorialDataById.Values; + + public IReadOnlyCollection GetScriptsTutorialUntilIndex(int scriptId, int scriptIndex) => _tutorialDataById.Values + .Where(s => s.ScriptId < scriptId || s.ScriptId == scriptId && s.ScriptIndex <= scriptIndex) + .OrderBy(s => s.ScriptId).ThenBy(s => s.ScriptIndex).ToList(); + + public IReadOnlyCollection GetQuestlines(int questId) => _questlines.ContainsKey(questId) ? _questlines[questId] : Array.Empty(); + + public TutorialDto GetQuestPayScriptByQuestId(int questId) => _tutorialQuestPayScriptByQuestId.GetOrDefault(questId); + + public TutorialDto GetScriptTutorialById(int scriptId) => _tutorialDataById.ContainsKey(scriptId) ? _tutorialDataById[scriptId] : null; + public TutorialDto GetScriptTutorialByIndex(int scriptId, int index) => _tutorialDataById.FirstOrDefault(s => s.Value.ScriptId == scriptId && s.Value.ScriptIndex == index).Value; + + public TutorialDto GetFirstScriptFromIdByType(int scriptId, TutorialActionType type) => + _tutorialDataByType.ContainsKey(type) ? _tutorialDataByType[type].FirstOrDefault(s => s.Id > scriptId) : null; + + public QuestNpcDto GetQuestNpcByScriptId(int scriptId) => _questNpcById.FirstOrDefault(s => s.Value.StartingScript == scriptId).Value; + + private static Dictionary> BuildQuestline(IDictionary> quests) + { + bool built = true; + foreach ((int questId, List nextQuestIds) in quests.ToList()) + { + if (!quests.ContainsKey(questId)) + { + continue; + } + + foreach (int nextQuestId in nextQuestIds.ToList()) + { + if (!quests.ContainsKey(nextQuestId)) + { + continue; + } + + built = false; + quests[questId].AddRange(quests[nextQuestId]); + quests.Remove(nextQuestId); + } + } + + if (!built) + { + return BuildQuestline(quests); + } + + var invertedDictionary = new Dictionary>(); + foreach ((int questId, List nextQuestIds) in quests) + { + foreach (int nextQuestId in nextQuestIds) + { + if (!invertedDictionary.ContainsKey(nextQuestId)) + { + invertedDictionary.Add(nextQuestId, new List()); + } + + invertedDictionary[nextQuestId].Add(questId); + } + + invertedDictionary.Add(questId, new List { questId }); + } + + return invertedDictionary; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.QuestImpl/Plugin.QuestImpl.csproj b/srcs/_plugins/Plugin.QuestImpl/Plugin.QuestImpl.csproj new file mode 100644 index 0000000..c5ff114 --- /dev/null +++ b/srcs/_plugins/Plugin.QuestImpl/Plugin.QuestImpl.csproj @@ -0,0 +1,20 @@ + + + + net5.0 + + + + + + + + + + + + + + + + diff --git a/srcs/_plugins/Plugin.QuestImpl/QuestDependencyInjectionExtensions.cs b/srcs/_plugins/Plugin.QuestImpl/QuestDependencyInjectionExtensions.cs new file mode 100644 index 0000000..af83465 --- /dev/null +++ b/srcs/_plugins/Plugin.QuestImpl/QuestDependencyInjectionExtensions.cs @@ -0,0 +1,21 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using PhoenixLib.Extensions; +using WingsEmu.Game.Quests; + +namespace Plugin.QuestImpl +{ + public static class QuestDependencyInjectionExtensions + { + public static void AddRunScriptHandlers(this IServiceCollection services) + { + Type[] types = typeof(QuestPlugin).Assembly.GetTypesImplementingInterface(); + foreach (Type handlerType in types) + { + services.AddTransient(handlerType); + } + + services.AddSingleton(); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.QuestImpl/QuestFactory.cs b/srcs/_plugins/Plugin.QuestImpl/QuestFactory.cs new file mode 100644 index 0000000..55ed954 --- /dev/null +++ b/srcs/_plugins/Plugin.QuestImpl/QuestFactory.cs @@ -0,0 +1,24 @@ +using WingsEmu.DTOs.Quests; +using WingsEmu.Game; +using WingsEmu.Game.Quests; + +namespace Plugin.QuestImpl +{ + public class QuestFactory : IQuestFactory + { + private readonly IQuestManager _questManager; + private readonly IRandomGenerator _randomGenerator; + + public QuestFactory(IQuestManager questManager, IRandomGenerator randomGenerator) + { + _questManager = questManager; + _randomGenerator = randomGenerator; + } + + public CharacterQuest NewQuest(long characterId, int questId, QuestSlotType questSlotType) + { + QuestDto quest = _questManager.GetQuestById(questId); + return quest == null ? null : new CharacterQuest(quest, questSlotType, _randomGenerator); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.QuestImpl/QuestModule.cs b/srcs/_plugins/Plugin.QuestImpl/QuestModule.cs new file mode 100644 index 0000000..db0910f --- /dev/null +++ b/srcs/_plugins/Plugin.QuestImpl/QuestModule.cs @@ -0,0 +1,293 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Qmmands; +using WingsAPI.Game.Extensions.Quests; +using WingsEmu.Commands.Checks; +using WingsEmu.Commands.Entities; +using WingsEmu.DTOs.Account; +using WingsEmu.DTOs.Quests; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Quests; +using WingsEmu.Game.Quests.Event; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.QuestImpl +{ + [Name("Quests")] + [Group("quests", "quest")] + [Description("Module related to Quest commands.")] + [RequireAuthority(AuthorityType.SuperGameMaster)] + public class QuestModule : SaltyModuleBase + { + private readonly IQuestFactory _questFactory; + private readonly IQuestManager _questManager; + + public QuestModule(IQuestManager questManager, IQuestFactory questFactory) + { + _questManager = questManager; + _questFactory = questFactory; + } + + [Command("add")] + [Description("Add the quest linked to a given ID")] + public async Task AddQuest(int questId, string questSlotTypeName) + { + if (!Enum.TryParse(questSlotTypeName, out QuestSlotType questSlotType)) + { + return new SaltyCommandResult(false, "Wrong quest slot type"); + } + + QuestDto quest = _questManager.GetQuestById(questId); + if (quest == null) + { + return new SaltyCommandResult(false, "Quest null"); + } + + Context.Player.PlayerEntity.AddActiveQuest(_questFactory.NewQuest(Context.Player.PlayerEntity.Id, questId, questSlotType)); + Context.Player.RefreshQuestList(_questManager, quest.Id); + Context.Player.SendTargetQuest(quest.TargetMapX, quest.TargetMapY, quest.TargetMapId, quest.Id); + return new SaltyCommandResult(true, "Quest added!"); + } + + [Command("give")] + [Description("Gives the quest to a character on a slot (general, main, secondary)")] + public async Task AddQuest(IClientSession session, int questId, string questSlotTypeName) + { + if (!Enum.TryParse(questSlotTypeName, out QuestSlotType questSlotType)) + { + return new SaltyCommandResult(false, "Wrong quest slot type"); + } + + QuestDto quest = _questManager.GetQuestById(questId); + if (quest == null) + { + return new SaltyCommandResult(false, "Quest null"); + } + + if (session == null) + { + return new SaltyCommandResult(false, "Player null"); + } + + session.PlayerEntity.AddActiveQuest(_questFactory.NewQuest(session.PlayerEntity.Id, questId, questSlotType)); + session.RefreshQuestList(_questManager, quest.Id); + session.SendTargetQuest(quest.TargetMapX, quest.TargetMapY, quest.TargetMapId, quest.Id); + return new SaltyCommandResult(true, "Quest added!"); + } + + [Command("getscripts")] + [Description("Get the scripts completed by the player")] + public async Task GetPlayerCompletedScripts(IClientSession session) + { + if (session == null) + { + return new SaltyCommandResult(false, "Player null"); + } + + foreach (CompletedScriptsDto completedScriptsDto in session.PlayerEntity.GetCompletedScripts()) + { + Context.Player.SendChatMessage($"script {completedScriptsDto.ScriptId} {completedScriptsDto.ScriptIndex}", ChatMessageColorType.Yellow); + } + + return new SaltyCommandResult(true); + } + + [Command("completescripts", "cs")] + [Description("Forces the script completion, removing any MAIN quest running and any completed script after the specified, and sends the next one.")] + public async Task ForceCompleteScripts(IClientSession session, int scriptId, int scriptIndex) + { + if (session == null) + { + return new SaltyCommandResult(false, "Player null"); + } + + TutorialDto tutorialDto = _questManager.GetScriptTutorialByIndex(scriptId, scriptIndex); + if (tutorialDto == null) + { + return new SaltyCommandResult(false, "Script null"); + } + + IReadOnlyCollection scripts = _questManager.GetScriptsTutorialUntilIndex(scriptId, scriptIndex); + IEnumerable playerCompletedScripts = session.PlayerEntity.GetCompletedScripts(); + + foreach (TutorialDto script in scripts) + { + session.PlayerEntity.SaveScript(script.ScriptId, script.ScriptIndex, script.Type, DateTime.UtcNow); + if (script.Type == TutorialActionType.WAIT_FOR_QUEST_COMPLETION) + { + session.PlayerEntity.AddCompletedQuest(_questFactory.NewQuest(session.PlayerEntity.Id, script.Data, QuestSlotType.MAIN)); + } + } + + foreach (CompletedScriptsDto completedScript in playerCompletedScripts) + { + if (completedScript.ScriptId < scriptId || completedScript.ScriptId == scriptId && completedScript.ScriptIndex <= scriptIndex) + { + continue; + } + + session.PlayerEntity.RemoveCompletedScript(completedScript.ScriptId, completedScript.ScriptIndex); + + TutorialDto script = _questManager.GetScriptTutorialByIndex(completedScript.ScriptId, completedScript.ScriptIndex); + if (script.Type == TutorialActionType.WAIT_FOR_QUEST_COMPLETION) + { + session.PlayerEntity.RemoveCompletedQuest(script.Data); + } + } + + TutorialDto nextScript = _questManager.GetScriptTutorialById(scripts.Last().Id + 1); + if (nextScript == null) + { + return new SaltyCommandResult(true, "That was the last script,"); + } + + CharacterQuest characterQuest = session.GetQuestBySlot(5); + if (characterQuest != null) // Remove a MAIN quest if there is any + { + session.DeleteQuestTarget(characterQuest); + session.PlayerEntity.RemoveActiveQuest(characterQuest.QuestId); + session.RefreshQuestList(_questManager, null); + } + + if (nextScript.Type == TutorialActionType.WAIT_FOR_QUEST_COMPLETION && session.PlayerEntity.HasQuestWithId(nextScript.Data)) + { + QuestDto quest = _questManager.GetQuestById(nextScript.Data); + session.PlayerEntity.AddActiveQuest(_questFactory.NewQuest(session.PlayerEntity.Id, nextScript.Data, QuestSlotType.MAIN)); + session.RefreshQuestList(_questManager, quest.Id); + session.SendTargetQuest(quest.TargetMapX, quest.TargetMapY, quest.TargetMapId, quest.Id); + } + + session.SendScriptPacket(nextScript.ScriptId, nextScript.ScriptIndex); + + return new SaltyCommandResult(true, "Scripts completed"); + } + + [Command("remove")] + [Description("Remove the quest linked to a given ID")] + public async Task RemoveQuest(int questId) + { + CharacterQuest quest = Context.Player.PlayerEntity.GetQuestById(questId); + if (quest == null) + { + return new SaltyCommandResult(false, "Quest null"); + } + + Context.Player.DeleteQuestTarget(quest); + Context.Player.PlayerEntity.RemoveActiveQuest(quest.QuestId); + Context.Player.RefreshQuestList(_questManager, null); + return new SaltyCommandResult(true, "Quest removed!"); + } + + [Command("remove")] + [Description("Remove the quest linked to a given ID of a character")] + public async Task RemoveQuest(IClientSession session, int questId) + { + if (session == null) + { + return new SaltyCommandResult(false, "Player null"); + } + + CharacterQuest quest = session.PlayerEntity.GetQuestById(questId); + if (quest == null) + { + return new SaltyCommandResult(false, "Quest null"); + } + + session.DeleteQuestTarget(quest); + session.PlayerEntity.RemoveActiveQuest(quest.QuestId); + session.RefreshQuestList(_questManager, null); + return new SaltyCommandResult(true, "Quest removed!"); + } + + [Command("showinfo")] + [Description("Show info related to a player quests")] + public async Task ShowInfo(IClientSession session) + { + if (session == null) + { + return new SaltyCommandResult(false, "Player null"); + } + + Context.Player.SendChatMessage($"== Active quests from {session.PlayerEntity.Name} ==", ChatMessageColorType.Yellow); + foreach (CharacterQuest quest in session.PlayerEntity.GetCurrentQuests()) + { + Context.Player.SendChatMessage($"- Quest VNUM: {quest.QuestId}; Slot: {quest.SlotType}; Type: {quest.Quest.QuestType}", ChatMessageColorType.Yellow); + foreach (QuestObjectiveDto objective in quest.Quest.Objectives) + { + CharacterQuestObjectiveDto questObjectiveDto = quest.ObjectiveAmount[objective.ObjectiveIndex]; + Context.Player.SendChatMessage($"ObjectiveIndex: {objective.ObjectiveIndex}; CompletedAmount: {questObjectiveDto.CurrentAmount}", ChatMessageColorType.Yellow); + } + } + + return new SaltyCommandResult(true, "Scripts showed"); + } + + [Command("showinfo")] + [Description("Show info related to your quests")] + public async Task ShowInfo() + { + Context.Player.SendChatMessage($"== Active quests from {Context.Player.PlayerEntity.Name} ==", ChatMessageColorType.Yellow); + foreach (CharacterQuest quest in Context.Player.PlayerEntity.GetCurrentQuests()) + { + Context.Player.SendChatMessage($"- Quest VNUM: {quest.QuestId}; Slot: {quest.SlotType}; Type: {quest.Quest.QuestType}", ChatMessageColorType.Yellow); + foreach (QuestObjectiveDto objective in quest.Quest.Objectives) + { + CharacterQuestObjectiveDto questObjectiveDto = quest.ObjectiveAmount[objective.ObjectiveIndex]; + Context.Player.SendChatMessage($"ObjectiveIndex: {objective.ObjectiveIndex}; CompletedAmount: {questObjectiveDto.CurrentAmount}", ChatMessageColorType.Yellow); + } + } + + return new SaltyCommandResult(true); + } + + [Command("clear")] + [Description("Clear all quests")] + public async Task ClearAllQuests() + { + foreach (CharacterQuest quest in Context.Player.PlayerEntity.GetCurrentQuests()) + { + Context.Player.PlayerEntity.RemoveActiveQuest(quest.QuestId); + Context.Player.DeleteQuestTarget(quest); + } + + Context.Player.RefreshQuestList(_questManager, null); + return new SaltyCommandResult(true); + } + + [Command("refresh-dq", "refreshdailyquests", "rdq")] + [Description("Refresh your daily minigame points.")] + public async Task RefreshDailyQuests([Description("Force refresh")] bool force = false) + { + Context.Player.EmitEvent(new QuestDailyRefreshEvent { Force = force }); + return new SaltyCommandResult(true, "Daily quests refreshed!"); + } + + [Command("set-soundflowers")] + [Description("Gives the player X amount of unstarted soundflower quests")] + public async Task GiveSoundFlowers(IClientSession session, int amount) + { + IReadOnlyCollection generalQuests = session.PlayerEntity.GetCurrentQuests().Where(s => s.SlotType == QuestSlotType.MAIN).ToList(); + if (generalQuests.Count + amount > 5) + { + return new SaltyCommandResult(false, "You can't have more than 5 GENERAL quests active!"); + } + + int currentSoundFlowers = session.PlayerEntity.GetPendingSoundFlowerQuests(); + for (int i = 0; i < currentSoundFlowers; i++) + { + session.PlayerEntity.DecreasePendingSoundFlowerQuests(); + } + + for (int i = 0; i < amount; i++) + { + session.PlayerEntity.IncreasePendingSoundFlowerQuests(); + } + + session.RefreshQuestList(_questManager, null); + return new SaltyCommandResult(true, "Sound flower quests added!"); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.QuestImpl/QuestPlugin.cs b/srcs/_plugins/Plugin.QuestImpl/QuestPlugin.cs new file mode 100644 index 0000000..a644776 --- /dev/null +++ b/srcs/_plugins/Plugin.QuestImpl/QuestPlugin.cs @@ -0,0 +1,49 @@ +using System; +using PhoenixLib.Extensions; +using PhoenixLib.Logging; +using WingsAPI.Plugins; +using WingsEmu.Commands.Interfaces; +using WingsEmu.Game.Quests; + +namespace Plugin.QuestImpl +{ + public class QuestPlugin : IGamePlugin + { + private readonly ICommandContainer _commands; + private readonly IRunScriptHandlerContainer _runScriptHandlerContainer; + private readonly IServiceProvider _serviceProvider; + + public QuestPlugin(ICommandContainer commands, IRunScriptHandlerContainer runScriptHandlerContainer, IServiceProvider serviceProvider) + { + _commands = commands; + _runScriptHandlerContainer = runScriptHandlerContainer; + _serviceProvider = serviceProvider; + } + + public string Name => nameof(QuestPlugin); + + public void OnLoad() + { + _commands.AddModule(); + + foreach (Type handlerType in typeof(QuestPlugin).Assembly.GetTypesImplementingInterface()) + { + try + { + object tmp = _serviceProvider.GetService(handlerType); + if (!(tmp is IRunScriptHandler real)) + { + continue; + } + + Log.Debug($"[RUN_SCRIPT][ADD_HANDLER] {handlerType}"); + _runScriptHandlerContainer.RegisterAsync(real); + } + catch (Exception e) + { + Log.Error("[RUN_SCRIPT][FAIL_ADD]", e); + } + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.QuestImpl/QuestPluginCore.cs b/srcs/_plugins/Plugin.QuestImpl/QuestPluginCore.cs new file mode 100644 index 0000000..59664ab --- /dev/null +++ b/srcs/_plugins/Plugin.QuestImpl/QuestPluginCore.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using PhoenixLib.Configuration; +using PhoenixLib.Events; +using Plugin.QuestImpl.Managers; +using WingsAPI.Plugins; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Quests; +using WingsEmu.Game.Quests.Configurations; + +namespace Plugin.QuestImpl +{ + public class QuestPluginCore : IGameServerPlugin + { + public string Name => nameof(QuestPluginCore); + + public void AddDependencies(IServiceCollection services, GameServerLoader gameServer) + { + services.AddEventHandlersInAssembly(); + services.AddRunScriptHandlers(); + + services.TryAddSingleton(); + services.TryAddSingleton(); + + services.TryAddSingleton(); + services.TryAddSingleton(s => s.GetRequiredService()); + + services.AddFileConfiguration("quests_rates_configuration"); + services.AddFileConfiguration("general_quests_configuration"); + services.AddFileConfiguration("sound_flower_configuration"); + + services.AddMultipleConfigurationOneFile("npc_run_type_quests_configuration"); + + services.AddFileConfiguration("quest_teleport_dialog_configuration"); + services.AddFileConfiguration(new PeriodicQuestsConfiguration + { + DailyQuests = new HashSet + { + new() + } + }); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.QuestImpl/RunScriptEventHandler.cs b/srcs/_plugins/Plugin.QuestImpl/RunScriptEventHandler.cs new file mode 100644 index 0000000..2c8543c --- /dev/null +++ b/srcs/_plugins/Plugin.QuestImpl/RunScriptEventHandler.cs @@ -0,0 +1,20 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Quests; +using WingsEmu.Game.Quests.Event; + +namespace Plugin.QuestImpl +{ + public class RunScriptEventHandler : IAsyncEventProcessor + { + private readonly IRunScriptHandlerContainer _runScriptHandler; + + public RunScriptEventHandler(IRunScriptHandlerContainer runScriptHandler) => _runScriptHandler = runScriptHandler; + + public async Task HandleAsync(RunScriptEvent e, CancellationToken cancellation) + { + await Task.Run(() => _runScriptHandler.Handle(e.Sender, e), cancellation); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.QuestImpl/RunScriptHandlers/TeleportRunScriptHandler.cs b/srcs/_plugins/Plugin.QuestImpl/RunScriptHandlers/TeleportRunScriptHandler.cs new file mode 100644 index 0000000..ab12750 --- /dev/null +++ b/srcs/_plugins/Plugin.QuestImpl/RunScriptHandlers/TeleportRunScriptHandler.cs @@ -0,0 +1,74 @@ +using System.Linq; +using System.Threading.Tasks; +using WingsEmu.DTOs.Maps; +using WingsEmu.DTOs.Quests; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Quests; +using WingsEmu.Game.Quests.Event; + +namespace Plugin.QuestImpl.RunScriptHandlers +{ + public class TeleportRunScriptHandler : IRunScriptHandler + { + private readonly IGameLanguageService _gameLanguageService; + private readonly IMapManager _mapManager; + private readonly IQuestManager _questManager; + + private readonly QuestTeleportDialogConfiguration _questTeleportDialogConfiguration; + + public TeleportRunScriptHandler(QuestTeleportDialogConfiguration questTeleportDialogConfiguration, IQuestManager questManager, IGameLanguageService gameLanguageService, IMapManager mapManager) + { + _questTeleportDialogConfiguration = questTeleportDialogConfiguration; + _questManager = questManager; + _gameLanguageService = gameLanguageService; + _mapManager = mapManager; + } + + public int[] RunIds => new[] { 200, 201, 202, 203, 204 }; + + public async Task ExecuteAsync(IClientSession session, RunScriptEvent e) + { + QuestTeleportDialogInfo questTeleport = _questTeleportDialogConfiguration.FirstOrDefault(s => s.RunId == e.RunId); + if (questTeleport == null) + { + return; + } + + TutorialDto scriptWithTp = _questManager.GetScriptsTutorialByType(TutorialActionType.RUN).FirstOrDefault(s => s.Data == e.RunId); + if (scriptWithTp == null) + { + return; + } + + TutorialDto completedScript = _questManager.GetScriptTutorialById(scriptWithTp.Id - 1); + if (!session.PlayerEntity.HasCompletedScriptByIndex(completedScript.ScriptId, completedScript.ScriptIndex)) + { + return; + } + + if (!session.CurrentMapInstance.HasMapFlag(MapFlags.IS_BASE_MAP)) + { + return; + } + + if (session.CantPerformActionOnAct4()) + { + return; + } + + if (questTeleport.AskForTeleport) + { + session.SendDialog($"guri 1000 {questTeleport.MapId} {questTeleport.PositionX} {questTeleport.PositionY}", "guri 9999", + _gameLanguageService.GetLanguage(GameDialogKey.QUEST_DIALOG_TELEPORT_TO_OBJECTIVE, session.UserLanguage)); + return; + } + + IMapInstance mapInstance = _mapManager.GetBaseMapInstanceByMapId(questTeleport.MapId); + session.ChangeMap(mapInstance.Id, questTeleport.PositionX, questTeleport.PositionY); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Raids/Commands/RaidAdminCommandsModule.cs b/srcs/_plugins/Plugin.Raids/Commands/RaidAdminCommandsModule.cs new file mode 100644 index 0000000..9e11530 --- /dev/null +++ b/srcs/_plugins/Plugin.Raids/Commands/RaidAdminCommandsModule.cs @@ -0,0 +1,99 @@ +using System; +using System.Net; +using System.Threading.Tasks; +using PhoenixLib.Logging; +using Plugin.Raids.Const; +using Qmmands; +using WingsAPI.Scripting.LUA; +using WingsAPI.Scripting.ScriptManager; +using WingsEmu.Commands.Checks; +using WingsEmu.Commands.Entities; +using WingsEmu.DTOs.Account; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Raids; + +namespace Plugin.Raids.Commands; + +[Name("OwnerRaids")] +[Description("Module related to Raids files management commands.")] +[RequireAuthority(AuthorityType.Root)] +public class RaidAdminCommandsModule : SaltyModuleBase +{ + private readonly ScriptFactoryConfiguration _configuration; + private readonly IRaidScriptManager _raidScriptManager; + + public RaidAdminCommandsModule(IRaidScriptManager raidScriptManager, ScriptFactoryConfiguration configuration) + { + _raidScriptManager = raidScriptManager; + _configuration = configuration; + } + + [Command("download-raids", "download-raid", "raid-dl")] + public async Task DownloadRaid(string raidName, [Remainder] string raidUrl) + { + try + { + using var webClient = new WebClient(); + webClient.DownloadFile(raidUrl, _configuration.RaidsDirectory + '/' + raidName + ".lua"); + } + catch (Exception e) + { + Log.Error($"[DOWNLOAD_RAID] {raidName} {raidUrl}", e); + Context.Player.SendErrorChatMessage($"Couldn't download {raidUrl}"); + return new SaltyCommandResult(false); + } + + try + { + _raidScriptManager.Load(); + Context.Player.SendSuccessChatMessage("Raids reloaded, check your console output!"); + } + catch (Exception e) + { + Log.Error("[RELOAD_RAIDS]", e); + Context.Player.SendErrorChatMessage("Couldn't reload raids! :("); + return new SaltyCommandResult(false); + } + + return new SaltyCommandResult(true); + } + + [Command("raid-objective")] + public async Task CompleteObjectives() + { + IClientSession session = Context.Player; + + if (session.PlayerEntity.Raid?.Instance == null) + { + return new SaltyCommandResult(false); + } + + if (!session.PlayerEntity.Raid.Instance.RaidSubInstances.TryGetValue(session.CurrentMapInstance.Id, out RaidSubInstance subInstance)) + { + return new SaltyCommandResult(false); + } + + await subInstance.TriggerEvents(RaidConstEventKeys.ObjectivesCompleted); + + return new SaltyCommandResult(true, "Done."); + } + + [Command("reload-raids", "reloadraids", "raids-reload")] + public async Task ReloadRaids() + { + try + { + _raidScriptManager.Load(); + Context.Player.SendSuccessChatMessage("Raids reloaded, check your console output!"); + } + catch (Exception e) + { + Log.Error("[RELOAD_RAIDS]", e); + Context.Player.SendErrorChatMessage("Couldn't reload raids! :("); + return new SaltyCommandResult(false); + } + + return new SaltyCommandResult(true); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Raids/Commands/RaidAdminStartModule.cs b/srcs/_plugins/Plugin.Raids/Commands/RaidAdminStartModule.cs new file mode 100644 index 0000000..7d56c36 --- /dev/null +++ b/srcs/_plugins/Plugin.Raids/Commands/RaidAdminStartModule.cs @@ -0,0 +1,30 @@ +using System.Threading.Tasks; +using Qmmands; +using WingsEmu.Commands.Checks; +using WingsEmu.Commands.Entities; +using WingsEmu.DTOs.Account; +using WingsEmu.Game.Raids; +using WingsEmu.Game.Raids.Events; + +namespace Plugin.Raids.Commands; + +[Name("Admin-Raids")] +[Description("Module related to Raids Administrator commands.")] +[RequireAuthority(AuthorityType.GameAdmin)] +public class RaidAdminStartModule : SaltyModuleBase +{ + [Command("startRaid")] + public async Task StartRaidAsync() + { + RaidParty raidParty = Context.Player.PlayerEntity.Raid; + Context.Player.EmitEvent(new RaidInstanceStartEvent()); + return new SaltyCommandResult(true); + } + + [Command("createRaid")] + public async Task CreateRaid(byte raidType) + { + Context.Player.EmitEvent(new RaidPartyCreateEvent(raidType, null)); + return new SaltyCommandResult(true); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Raids/Configs/RaidStartConfiguration.cs b/srcs/_plugins/Plugin.Raids/Configs/RaidStartConfiguration.cs new file mode 100644 index 0000000..56d2d72 --- /dev/null +++ b/srcs/_plugins/Plugin.Raids/Configs/RaidStartConfiguration.cs @@ -0,0 +1,11 @@ +namespace Plugin.Raids.Configs; + +public class RaidStartConfiguration +{ + public byte MaximumSlots { get; set; } + public byte MinimumSlots { get; set; } + public byte MinimumLevel { get; set; } + public byte MaximumLevel { get; set; } + public byte MinimumHeroLevel { get; set; } + public byte MaximumHeroLevel { get; set; } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Raids/Configs/RaidStartFileConfiguration.cs b/srcs/_plugins/Plugin.Raids/Configs/RaidStartFileConfiguration.cs new file mode 100644 index 0000000..3b3e57b --- /dev/null +++ b/srcs/_plugins/Plugin.Raids/Configs/RaidStartFileConfiguration.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using WingsAPI.Packets.Enums; + +namespace Plugin.Raids.Configs; + +/// +/// By RaidType +/// +public class RaidStartFileConfiguration : Dictionary +{ +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Raids/Const/RaidConstEventKeys.cs b/srcs/_plugins/Plugin.Raids/Const/RaidConstEventKeys.cs new file mode 100644 index 0000000..e576ad1 --- /dev/null +++ b/srcs/_plugins/Plugin.Raids/Const/RaidConstEventKeys.cs @@ -0,0 +1,9 @@ +namespace Plugin.Raids.Const; + +public static class RaidConstEventKeys +{ + public const string ButtonSwitched = "Switched"; + public const string ButtonTriggered = "Triggered"; + public const string ObjectivesCompleted = "ObjectivesCompleted"; + public const string RaidSubInstanceAfterSlowMo = "AfterSlowMo"; +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Raids/Extension/ScriptExtensions.cs b/srcs/_plugins/Plugin.Raids/Extension/ScriptExtensions.cs new file mode 100644 index 0000000..6b979a2 --- /dev/null +++ b/srcs/_plugins/Plugin.Raids/Extension/ScriptExtensions.cs @@ -0,0 +1,9 @@ +using WingsAPI.Packets.Enums; +using WingsAPI.Scripting.Enum.Raid; + +namespace Plugin.Raids.Extension; + +public static class ScriptExtensions +{ + public static SRaidType ToSRaidType(this RaidType raidType) => (SRaidType)(byte)raidType; +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Raids/Handlers/PortalTriggerRaidEventHandler.cs b/srcs/_plugins/Plugin.Raids/Handlers/PortalTriggerRaidEventHandler.cs new file mode 100644 index 0000000..80cb312 --- /dev/null +++ b/srcs/_plugins/Plugin.Raids/Handlers/PortalTriggerRaidEventHandler.cs @@ -0,0 +1,56 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Packets.Enums; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Maps.Event; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Raids; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.Raids; + +public class PortalTriggerRaidEventHandler : IAsyncEventProcessor +{ + private readonly IGameLanguageService _gameLanguage; + + public PortalTriggerRaidEventHandler(IGameLanguageService gameLanguage) => _gameLanguage = gameLanguage; + + public async Task HandleAsync(PortalTriggerEvent e, CancellationToken cancellation) + { + if (e.Portal.Type != PortalType.Raid) + { + return; + } + + if (!e.Portal.RaidType.HasValue) + { + return; + } + + IClientSession session = e.Sender; + + if (!session.PlayerEntity.IsInRaidParty) + { + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.RAID_CHATMESSAGE_START_NOT_IN_RAID_PARTY, session.UserLanguage), ChatMessageColorType.PlayerSay); + return; + } + + if (!session.PlayerEntity.IsRaidLeader(session.PlayerEntity.Id)) + { + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.RAID_CHATMESSAGE_START_IS_NOT_LEADER, session.UserLanguage), ChatMessageColorType.PlayerSay); + return; + } + + if (session.PlayerEntity.Raid.Type != (RaidType)e.Portal.RaidType.Value) + { + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.RAID_CHATMESSAGE_START_RAID_TYPE_IS_WRONG, session.UserLanguage), ChatMessageColorType.PlayerSay); + return; + } + + string raidName = session.GenerateRaidName(_gameLanguage, session.PlayerEntity.Raid.Type); + session.SendQnaPacket("mkraid", _gameLanguage.GetLanguageFormat(GameDialogKey.RAID_DIALOG_ASK_START_RAID, session.UserLanguage, raidName)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Raids/Handlers/RaidGiveRewardsEventHandler.cs b/srcs/_plugins/Plugin.Raids/Handlers/RaidGiveRewardsEventHandler.cs new file mode 100644 index 0000000..bbbab44 --- /dev/null +++ b/srcs/_plugins/Plugin.Raids/Handlers/RaidGiveRewardsEventHandler.cs @@ -0,0 +1,122 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.DAL.Redis.Locks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.Families; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsAPI.Packets.Enums; +using WingsEmu.Game; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Families.Enum; +using WingsEmu.Game.Items; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Raids; +using WingsEmu.Game.Raids.Events; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.Raids; + +public class RaidGiveRewardsEventHandler : IAsyncEventProcessor +{ + private readonly IExpirableLockService _expirableLockService; + private readonly IGameItemInstanceFactory _gameItemInstance; + private readonly IRandomGenerator _randomGenerator; + + public RaidGiveRewardsEventHandler(IGameItemInstanceFactory gameItemInstance, IRandomGenerator randomGenerator, IExpirableLockService expirableLockService) + { + _gameItemInstance = gameItemInstance; + _randomGenerator = randomGenerator; + _expirableLockService = expirableLockService; + } + + public async Task HandleAsync(RaidGiveRewardsEvent e, CancellationToken cancellation) + { + RaidParty raidParty = e.RaidParty; + IMonsterEntity bossMap = e.MapBoss; + RaidReward raidReward = e.RaidReward; + + if (bossMap == null) + { + return; + } + + int reputation = 0; + if (raidReward.DefaultReputation) + { + reputation = raidParty.MinimumLevel * 30; + } + else + { + if (raidReward.FixedReputation.HasValue) + { + reputation = raidReward.FixedReputation.Value; + } + } + + long leaderId = raidParty.Leader.PlayerEntity.Id; + var randomBag = new RandomBag(_randomGenerator); + foreach (RaidBoxRarity toAdd in raidReward.RaidBox.RaidBoxRarities) + { + randomBag.AddEntry(toAdd, toAdd.Chance); + } + + foreach (IClientSession member in raidParty.Members.ToList()) + { + if (member == null) + { + continue; + } + + if (member.CurrentMapInstance?.Id != bossMap.MapInstance?.Id) + { + continue; + } + + + RaidBoxRarity box = randomBag.GetRandom(); + byte boxRarity = box.Rarity; + + if (member.PlayerEntity.Id == leaderId && boxRarity < 4) + { + boxRarity = 4; + } + + GameItemInstance rewardBox = _gameItemInstance.CreateItem(raidReward.RaidBox.RewardBox, 1, 0, (sbyte)boxRarity); + await member.AddNewItemToInventory(rewardBox, true, ChatMessageColorType.Yellow, true); + await member.EmitEventAsync(new RaidRewardReceivedEvent + { + BoxRarity = boxRarity + }); + + await ProcessFamilyExperience(member, raidParty.Type); + + await member.EmitEventAsync(new GenerateReputationEvent + { + Amount = reputation, + SendMessage = true + }); + } + } + + private async Task ProcessFamilyExperience(IClientSession member, RaidType raidPartyType) + { + if (!member.PlayerEntity.IsInFamily()) + { + return; + } + + if (!await _expirableLockService.TryAddTemporaryLockAsync( + $"game:locks:family:{member.PlayerEntity.Id}:raids:{(short)raidPartyType}:character:{member.PlayerEntity.Id}", DateTime.UtcNow.Date.AddDays(1))) + { + return; + } + + member.SendChatMessage(member.GetLanguageFormat(GameDialogKey.FAMILY_CHATMESSAGE_XP_GAINED, 200), ChatMessageColorType.Yellow); + await member.FamilyAddExperience(200, FamXpObtainedFromType.Raid); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Raids/Handlers/RaidInstanceActivateRaidWaves.cs b/srcs/_plugins/Plugin.Raids/Handlers/RaidInstanceActivateRaidWaves.cs new file mode 100644 index 0000000..a58ddd5 --- /dev/null +++ b/srcs/_plugins/Plugin.Raids/Handlers/RaidInstanceActivateRaidWaves.cs @@ -0,0 +1,42 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Maps.Event; +using WingsEmu.Game.Raids; + +namespace Plugin.Raids; + +public class RaidInstanceActivateRaidWaves : IAsyncEventProcessor +{ + private readonly IMapManager _mapManager; + private readonly IRaidManager _raidManager; + + public RaidInstanceActivateRaidWaves(IRaidManager raidManager, IMapManager mapManager) + { + _raidManager = raidManager; + _mapManager = mapManager; + } + + public async Task HandleAsync(JoinMapEndEvent e, CancellationToken cancellation) + { + if (e.JoinedMapInstance.MapInstanceType != MapInstanceType.RaidInstance) + { + return; + } + + if (!e.Sender.PlayerEntity.IsInRaidParty || !e.Sender.PlayerEntity.Raid.Started) + { + return; + } + + if (!e.Sender.PlayerEntity.Raid.Instance.RaidSubInstances.TryGetValue(e.JoinedMapInstance.Id, out RaidSubInstance raidSubInstance)) + { + return; + } + + raidSubInstance.RaidWavesActivated = true; + raidSubInstance.LastRaidWave = DateTime.UtcNow.AddSeconds(30); // sorry, quickwin :( + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Raids/Handlers/RaidInstanceDestroyEventHandler.cs b/srcs/_plugins/Plugin.Raids/Handlers/RaidInstanceDestroyEventHandler.cs new file mode 100644 index 0000000..d61e91e --- /dev/null +++ b/srcs/_plugins/Plugin.Raids/Handlers/RaidInstanceDestroyEventHandler.cs @@ -0,0 +1,57 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using PhoenixLib.Logging; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Raids; +using WingsEmu.Game.Raids.Events; +using WingsEmu.Game.Revival; + +namespace Plugin.Raids; + +public class RaidInstanceDestroyEventHandler : IAsyncEventProcessor +{ + private readonly IMapManager _mapManager; + private readonly IRaidManager _raidManager; + + public RaidInstanceDestroyEventHandler(IRaidManager raidManager, IMapManager mapManager) + { + _raidManager = raidManager; + _mapManager = mapManager; + } + + public async Task HandleAsync(RaidInstanceDestroyEvent e, CancellationToken cancellation) + { + Log.Warn("Destroying raid instance"); + _raidManager.RemoveRaid(e.RaidParty); + + if (e.RaidParty.Instance == null) + { + return; + } + + foreach (RaidSubInstance subInstance in e.RaidParty.Instance.RaidSubInstances.Values) + { + foreach (IClientSession session in subInstance.MapInstance.Sessions.ToList()) + { + RaidPartyLeaveEventHandler.InternalLeave(session); + + if (!session.PlayerEntity.IsAlive()) + { + await session.EmitEventAsync(new RevivalReviveEvent()); + } + + session.ChangeToLastBaseMap(); + } + + _mapManager.RemoveMapInstance(subInstance.MapInstance.Id); + subInstance.MapInstance.Destroy(); + } + + e.RaidParty.Destroy = true; + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Raids/Handlers/RaidInstanceFinishEventHandler.cs b/srcs/_plugins/Plugin.Raids/Handlers/RaidInstanceFinishEventHandler.cs new file mode 100644 index 0000000..1c6c2b1 --- /dev/null +++ b/srcs/_plugins/Plugin.Raids/Handlers/RaidInstanceFinishEventHandler.cs @@ -0,0 +1,188 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.Quests; +using WingsAPI.Packets.Enums; +using WingsEmu.DTOs.Quests; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Quests; +using WingsEmu.Game.Quests.Event; +using WingsEmu.Game.Raids; +using WingsEmu.Game.Raids.Configuration; +using WingsEmu.Game.Raids.Enum; +using WingsEmu.Game.Raids.Events; +using WingsEmu.Game.Revival; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.Raids; + +public class RaidInstanceFinishEventHandler : IAsyncEventProcessor +{ + private readonly IAsyncEventPipeline _eventPipeline; + private readonly IGameLanguageService _languageService; + private readonly IQuestManager _questManager; + private readonly RaidConfiguration _raidConfiguration; + private readonly IRaidManager _raidManager; + private readonly ISessionManager _sessionManager; + + public RaidInstanceFinishEventHandler(RaidConfiguration raidConfiguration, IRaidManager raidManager, IAsyncEventPipeline eventPipeline, ISessionManager sessionManager, + IGameLanguageService languageService, IQuestManager questManager) + { + _raidConfiguration = raidConfiguration; + _raidManager = raidManager; + _eventPipeline = eventPipeline; + _sessionManager = sessionManager; + _languageService = languageService; + _questManager = questManager; + } + + public async Task HandleAsync(RaidInstanceFinishEvent e, CancellationToken cancellation) + { + if (e.RaidParty.Finished) + { + return; + } + + _raidManager.UnregisterRaidFromRaidPublishList(e.RaidParty); + + RaidWindowType windowType = RaidWindowType.MISSION_FAIL; + DateTime currentTime = DateTime.UtcNow; + + if (e.RaidFinishType != RaidFinishType.Disbanded) + { + foreach (RaidSubInstance subInstance in e.RaidParty.Instance.RaidSubInstances.Values) + { + foreach (IMonsterEntity monsterEntity in subInstance.MapInstance.GetAliveMonsters()) + { + if (monsterEntity.IsBoss) + { + continue; + } + + subInstance.MapInstance.DespawnMonster(monsterEntity); + subInstance.MapInstance.RemoveMonster(monsterEntity); + } + } + } + + switch (e.RaidFinishType) + { + case RaidFinishType.Disbanded: + await _eventPipeline.ProcessEventAsync(new RaidInstanceDestroyEvent(e.RaidParty), cancellation); + return; + case RaidFinishType.MissionClear: + windowType = RaidWindowType.MISSION_CLEAR; + e.RaidParty.Instance.SetFinishSlowMoDate(currentTime + _raidConfiguration.RaidSlowMoDelay); + break; + case RaidFinishType.TimeIsUp: + windowType = RaidWindowType.TIMES_UP; + break; + case RaidFinishType.NoLivesLeft: + windowType = RaidWindowType.NO_LIVES_LEFT; + break; + } + + IClientSession[] sessions = e.RaidParty.Members.ToArray(); + + foreach (IClientSession session in sessions) + { + if (!session.PlayerEntity.IsAlive()) + { + await session.EmitEventAsync(new RevivalReviveEvent()); + } + + if (e.RaidFinishType == RaidFinishType.MissionClear) + { + session.TrySendRaidBossDeadPackets(); + await session.EmitEventAsync(new RaidWonEvent()); + await CheckRaidQuest(session, e.RaidParty.Type); + } + + session.SendRaidUiPacket(e.RaidParty.Type, windowType); + session.SendRemoveClockPacket(); + session.SendRaidPacket(RaidPacketType.LIST_MEMBERS); + } + + if (e.RaidFinishType == RaidFinishType.MissionClear) + { + IMonsterEntity raidBoss = FindBossMap(e.RaidParty); + BroadcastRaidFinishMessage(e.RaidParty); + await e.RaidParty.Leader.EmitEventAsync(new RaidTargetKilledEvent { DamagerCharactersIds = raidBoss.PlayersDamage.Keys.ToArray() }); + await _eventPipeline.ProcessEventAsync(new RaidGiveRewardsEvent(e.RaidParty, raidBoss, e.RaidParty.Instance.RaidReward), cancellation); + } + else if (e.RaidFinishType != RaidFinishType.Disbanded) + { + await e.RaidParty.Leader.EmitEventAsync(new RaidLostEvent()); + } + + foreach (IClientSession session in sessions) + { + session.SendRaidmbf(); + session.RefreshRaidMemberList(); + } + + e.RaidParty.FinishRaid(currentTime + _raidConfiguration.RaidMapDestroyDelay); + } + + private async Task CheckRaidQuest(IClientSession session, RaidType raidType) + { + IEnumerable characterQuests = session.PlayerEntity.GetCurrentQuestsByTypes(new[] { QuestType.WIN_RAID_AND_TALK_TO_NPC }); + foreach (CharacterQuest quest in characterQuests) + { + foreach (QuestObjectiveDto objective in quest.Quest.Objectives) + { + if (raidType != (RaidType)objective.Data0) + { + continue; + } + + CharacterQuestObjectiveDto questObjectiveDto = quest.ObjectiveAmount[objective.ObjectiveIndex]; + + int amountLeft = questObjectiveDto.RequiredAmount - questObjectiveDto.CurrentAmount; + if (amountLeft == 0) + { + break; + } + + questObjectiveDto.CurrentAmount++; + session.RefreshQuestProgress(_questManager, quest.QuestId); + await session.EmitEventAsync(new QuestObjectiveUpdatedEvent + { + CharacterQuest = quest + }); + } + } + } + + private void BroadcastRaidFinishMessage(RaidParty raidParty) + { + _sessionManager.Broadcast(x => x.GenerateMsgPacket(string.Format( + _languageService.GetLanguage(GameDialogKey.RAID_SHOUTMESSAGE_COMPLETED, x.UserLanguage), raidParty.Leader.PlayerEntity.Name, x.GenerateRaidName(_languageService, raidParty.Type)), + MsgMessageType.Middle)); + } + + private IMonsterEntity FindBossMap(RaidParty raidParty) + { + IMonsterEntity raidBoss = null; + + foreach (RaidSubInstance subInstance in raidParty.Instance.RaidSubInstances.Values) + { + foreach (IMonsterEntity monsterEntity in subInstance.DeadBossMonsters) + { + raidBoss = monsterEntity; + break; + } + } + + return raidBoss; + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Raids/Handlers/RaidInstanceLivesIncDecEventHandler.cs b/srcs/_plugins/Plugin.Raids/Handlers/RaidInstanceLivesIncDecEventHandler.cs new file mode 100644 index 0000000..f45b79f --- /dev/null +++ b/srcs/_plugins/Plugin.Raids/Handlers/RaidInstanceLivesIncDecEventHandler.cs @@ -0,0 +1,37 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Raids; +using WingsEmu.Game.Raids.Enum; +using WingsEmu.Game.Raids.Events; + +namespace Plugin.Raids; + +public class RaidInstanceLivesIncDecEventHandler : IAsyncEventProcessor +{ + private readonly IAsyncEventPipeline _eventPipeline; + + public RaidInstanceLivesIncDecEventHandler(IAsyncEventPipeline eventPipeline) => _eventPipeline = eventPipeline; + + public async Task HandleAsync(RaidInstanceLivesIncDecEvent e, CancellationToken cancellation) + { + if (e.Sender.PlayerEntity.Raid.Instance == null) + { + return; + } + + e.Sender.PlayerEntity.Raid.Instance.IncreaseOrDecreaseLives(e.Amount); + await e.Sender.EmitEventAsync(new RaidRevivedEvent { RestoredLife = e.Amount > 0 }); + if (e.Sender.PlayerEntity.Raid.Instance.Lives < 0) + { + await _eventPipeline.ProcessEventAsync(new RaidInstanceFinishEvent(e.Sender.PlayerEntity.Raid, RaidFinishType.NoLivesLeft), cancellation); + } + + foreach (IClientSession member in e.Sender.PlayerEntity.Raid.Members) + { + member.RefreshRaidMemberList(); + member.SendRaidmbf(); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Raids/Handlers/RaidInstanceRefreshInfoEventHandler.cs b/srcs/_plugins/Plugin.Raids/Handlers/RaidInstanceRefreshInfoEventHandler.cs new file mode 100644 index 0000000..2a117e7 --- /dev/null +++ b/srcs/_plugins/Plugin.Raids/Handlers/RaidInstanceRefreshInfoEventHandler.cs @@ -0,0 +1,29 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Raids; +using WingsEmu.Game.Raids.Events; + +namespace Plugin.Raids; + +public class RaidInstanceRefreshInfoEventHandler : IAsyncEventProcessor +{ + public async Task HandleAsync(RaidInstanceRefreshInfoEvent e, CancellationToken cancellation) + { + string raidHealthPacket = null; + + IClientSession[] members = e.RaidParty.Members.ToArray(); + var stPackets = members.Select(test => test.PlayerEntity.GenerateStPacket()).ToList(); + + foreach (IClientSession session in members) + { + raidHealthPacket ??= session.GenerateRaidPacket(RaidPacketType.REFRESH_MEMBERS_HP_MP); + session.SendPacket(raidHealthPacket); + session.TrySendRaidBossPackets(); + session.SendPackets(stPackets); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Raids/Handlers/RaidInstanceStartEventHandler.cs b/srcs/_plugins/Plugin.Raids/Handlers/RaidInstanceStartEventHandler.cs new file mode 100644 index 0000000..54eb9d9 --- /dev/null +++ b/srcs/_plugins/Plugin.Raids/Handlers/RaidInstanceStartEventHandler.cs @@ -0,0 +1,144 @@ +// WingsEmu +// +// Developed by NosWings Team + + +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using PhoenixLib.Logging; +using WingsAPI.Game.Extensions.ItemExtension.Item; +using WingsAPI.Packets.Enums; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Raids; +using WingsEmu.Game.Raids.Events; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.Raids; + +public class RaidInstanceStartEventHandler : IAsyncEventProcessor +{ + private readonly IGameLanguageService _gameLanguage; + private readonly IItemsManager _itemsManager; + private readonly IRaidFactory _raidFactory; + private readonly IRaidManager _raidManager; + + public RaidInstanceStartEventHandler(IRaidManager raidManager, IRaidFactory raidFactory, IGameLanguageService gameLanguage, IItemsManager itemsManager) + { + _raidManager = raidManager; + _raidFactory = raidFactory; + _gameLanguage = gameLanguage; + _itemsManager = itemsManager; + } + + public async Task HandleAsync(RaidInstanceStartEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + + if (session.PlayerEntity.HasRaidStarted) + { + return; + } + + if (!session.PlayerEntity.IsRaidLeader(session.PlayerEntity.Id)) + { + return; + } + + _raidManager.UnregisterRaidFromRaidPublishList(session.PlayerEntity.Raid); + + RaidInstance raidInstance = _raidFactory.CreateRaid(session.PlayerEntity.Raid); + + if (raidInstance == null) + { + Log.Warn("Failed to create raid instance"); + return; + } + + if (session.PlayerEntity.Raid.Members.Count > session.PlayerEntity.Raid.MaximumMembers) + { + IClientSession getLastJoinedMember = session.PlayerEntity.Raid.Members[^1]; + getLastJoinedMember?.EmitEvent(new RaidPartyLeaveEvent(false, false)); + } + + RaidType raidType = session.PlayerEntity.Raid.Type; + + if (e.ForceTeleport == false) + { + foreach (IClientSession raidSession in session.PlayerEntity.Raid.Members) + { + string getRaidName = session.GenerateRaidName(_gameLanguage, raidType); + if (raidSession.IsRaidTypeRestricted(raidType)) + { + if (!raidSession.IsPlayerWearingRaidAmulet(raidType)) + { + string amuletName = raidType == RaidType.LordDraco + ? _itemsManager.GetItem((short)ItemVnums.DRACO_AMULET)?.GetItemName(_gameLanguage, raidSession.UserLanguage) + : _itemsManager.GetItem((short)ItemVnums.GLACERUS_AMULET)?.GetItemName(_gameLanguage, raidSession.UserLanguage); + + raidSession.SendChatMessage(raidSession.GetLanguageFormat(GameDialogKey.RAID_CHATMESSAGE_AMULET_NEEDED, amuletName), ChatMessageColorType.Yellow); + raidSession.EmitEvent(new RaidPartyLeaveEvent(false, false)); + continue; + } + + if (!raidSession.CanPlayerJoinToRestrictedRaid(raidType)) + { + raidSession.SendChatMessage(raidSession.GetLanguageFormat(GameDialogKey.RAID_CHATMESSAGE_LIMIT_REACHED, getRaidName), ChatMessageColorType.Yellow); + raidSession.EmitEvent(new RaidPartyLeaveEvent(false, false)); + continue; + } + } + + if (raidSession.CurrentMapInstance.Id == session.CurrentMapInstance.Id) + { + continue; + } + + // leave raid + raidSession.EmitEvent(new RaidPartyLeaveEvent(false, false)); + } + } + + session.PlayerEntity.Raid.StartRaid(raidInstance); + + foreach (IClientSession raidSession in session.PlayerEntity.Raid.Members) + { + string getRaidName = raidSession.GenerateRaidName(_gameLanguage, raidType); + if (!raidSession.PlayerEntity.IsAlive()) + { + raidSession.PlayerEntity.Hp = 1; + raidSession.PlayerEntity.Mp = 1; + } + + raidSession.ChangeMap(raidInstance.SpawnInstance.MapInstance, raidInstance.SpawnPoint.X, raidInstance.SpawnPoint.Y); + raidSession.SendRaidUiPacket(raidType, RaidWindowType.MISSION_START); + + //Sending this last two seems useless, but sending them just to be sure + raidSession.SendRaidPacket(RaidPacketType.INSTANCE_START); + raidSession.SendRaidPacket(RaidPacketType.AFTER_INSTANCE_START_BUT_BEFORE_REFRESH_MEMBERS); + + switch (raidType) + { + case RaidType.LordDraco: + raidSession.PlayerEntity.RaidRestrictionDto.LordDraco--; + + raidSession.SendChatMessage(raidSession.GetLanguageFormat(GameDialogKey.RAID_CHATMESSAGE_LIMIT_LEFT, + getRaidName, raidSession.PlayerEntity.RaidRestrictionDto.LordDraco), ChatMessageColorType.Yellow); + break; + case RaidType.Glacerus: + raidSession.PlayerEntity.RaidRestrictionDto.Glacerus--; + + raidSession.SendChatMessage(raidSession.GetLanguageFormat(GameDialogKey.RAID_CHATMESSAGE_LIMIT_LEFT, + getRaidName, raidSession.PlayerEntity.RaidRestrictionDto.Glacerus), ChatMessageColorType.Yellow); + break; + } + } + + await session.EmitEventAsync(new RaidStartedEvent()); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Raids/Handlers/RaidJoinMapEndEventHandler.cs b/srcs/_plugins/Plugin.Raids/Handlers/RaidJoinMapEndEventHandler.cs new file mode 100644 index 0000000..c18be95 --- /dev/null +++ b/srcs/_plugins/Plugin.Raids/Handlers/RaidJoinMapEndEventHandler.cs @@ -0,0 +1,29 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Maps.Event; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Raids; + +namespace Plugin.Raids.Handlers; + +public class RaidJoinMapEndEventHandler : IAsyncEventProcessor +{ + public async Task HandleAsync(JoinMapEndEvent e, CancellationToken cancellation) + { + if (e.Sender.CurrentMapInstance.MapInstanceType != MapInstanceType.RaidInstance || e.Sender.PlayerEntity.Raid == null) + { + return; + } + + e.Sender.SendTsClockPacket(e.Sender.PlayerEntity.Raid.Instance.TimeUntilEnd, true); + e.Sender.SendRaidmbf(); + e.Sender.SendRaidPacket(RaidPacketType.REFRESH_MEMBERS_HP_MP); + foreach (IClientSession member in e.Sender.PlayerEntity.Raid.Members) + { + e.Sender.SendStPacket(member.PlayerEntity); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Raids/Handlers/RaidListJoinEventHandler.cs b/srcs/_plugins/Plugin.Raids/Handlers/RaidListJoinEventHandler.cs new file mode 100644 index 0000000..d455ee8 --- /dev/null +++ b/srcs/_plugins/Plugin.Raids/Handlers/RaidListJoinEventHandler.cs @@ -0,0 +1,66 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Raids; +using WingsEmu.Game.Raids.Events; + +namespace Plugin.Raids; + +public class RaidListJoinEventHandler : IAsyncEventProcessor +{ + private readonly IRaidManager _raidManager; + private readonly ISessionManager _sessionManager; + + public RaidListJoinEventHandler(ISessionManager sessionManager, IRaidManager raidManager) + { + _sessionManager = sessionManager; + _raidManager = raidManager; + } + + public async Task HandleAsync(RaidListJoinEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + string nickname = e.Nickname; + + if (string.IsNullOrEmpty(nickname)) + { + return; + } + + if (session.PlayerEntity.IsInRaidParty) + { + return; + } + + if (session.PlayerEntity.HasRaidStarted) + { + return; + } + + IClientSession leader = _sessionManager.GetSessionByCharacterName(nickname); + if (leader == null) + { + return; + } + + if (!leader.PlayerEntity.IsInRaidParty) + { + return; + } + + if (!leader.PlayerEntity.IsRaidLeader(leader.PlayerEntity.Id)) + { + return; + } + + RaidParty leaderRaid = leader.PlayerEntity.Raid; + if (!_raidManager.ContainsRaidInRaidPublishList(leaderRaid)) + { + return; + } + + await session.EmitEventAsync(new RaidPartyJoinEvent(leader.PlayerEntity.Id, true)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Raids/Handlers/RaidListOpenEventHandler.cs b/srcs/_plugins/Plugin.Raids/Handlers/RaidListOpenEventHandler.cs new file mode 100644 index 0000000..01beb84 --- /dev/null +++ b/srcs/_plugins/Plugin.Raids/Handlers/RaidListOpenEventHandler.cs @@ -0,0 +1,29 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Raids; +using WingsEmu.Game.Raids.Events; + +namespace Plugin.Raids; + +public class RaidListOpenEventHandler : IAsyncEventProcessor +{ + private readonly IRaidManager _raidManager; + + public RaidListOpenEventHandler(IRaidManager raidManager) => _raidManager = raidManager; + + public async Task HandleAsync(RaidListOpenEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + byte rlType = 0; + + if (session.PlayerEntity.IsRaidLeader(session.PlayerEntity.Id)) + { + RaidParty raid = session.PlayerEntity.Raid; + rlType = !_raidManager.ContainsRaidInRaidPublishList(raid) ? (byte)2 : (byte)1; + } + + session.SendRlPacket(rlType, _raidManager); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Raids/Handlers/RaidListRegisterEventHandler.cs b/srcs/_plugins/Plugin.Raids/Handlers/RaidListRegisterEventHandler.cs new file mode 100644 index 0000000..d72ef03 --- /dev/null +++ b/srcs/_plugins/Plugin.Raids/Handlers/RaidListRegisterEventHandler.cs @@ -0,0 +1,54 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Networking.Broadcasting; +using WingsEmu.Game.Raids; +using WingsEmu.Game.Raids.Events; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.Raids; + +public class RaidListRegisterEventHandler : IAsyncEventProcessor +{ + private readonly IGameLanguageService _gameLanguage; + private readonly IRaidManager _raidManager; + private readonly ISessionManager _sessionManager; + + public RaidListRegisterEventHandler(IRaidManager raidManager, IGameLanguageService gameLanguage, ISessionManager sessionManager) + { + _raidManager = raidManager; + _gameLanguage = gameLanguage; + _sessionManager = sessionManager; + } + + public async Task HandleAsync(RaidListRegisterEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + + if (!session.PlayerEntity.IsInRaidParty || !session.PlayerEntity.IsRaidLeader(session.PlayerEntity.Id)) + { + return; + } + + RaidParty raid = session.PlayerEntity.Raid; + if (_raidManager.ContainsRaidInRaidPublishList(raid) || raid.Started) + { + return; + } + + await _sessionManager.BroadcastAsync(async x => + { + string getRaidName = x.GenerateRaidName(_gameLanguage, raid.Type); + return x.GenerateEventAsk(QnamlType.Raid, "rl", + _gameLanguage.GetLanguageFormat(GameDialogKey.RAID_BROADCAST_LOOKING_FOR_TEAM_MEMBERS, x.UserLanguage, session.PlayerEntity.Name, getRaidName)); + }, new LevelBroadcast(raid.MinimumLevel), new ExceptSessionBroadcast(session), new InBaseMapBroadcast()); + + _raidManager.RegisterRaidInRaidPublishList(raid); + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.RAID_SHOUTMESSAGE_REGISTERED, session.UserLanguage), MsgMessageType.Middle); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Raids/Handlers/RaidListUnregisterEventHandler.cs b/srcs/_plugins/Plugin.Raids/Handlers/RaidListUnregisterEventHandler.cs new file mode 100644 index 0000000..5dfc9ba --- /dev/null +++ b/srcs/_plugins/Plugin.Raids/Handlers/RaidListUnregisterEventHandler.cs @@ -0,0 +1,47 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Raids; +using WingsEmu.Game.Raids.Events; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.Raids; + +public class RaidListUnregisterEventHandler : IAsyncEventProcessor +{ + private readonly IGameLanguageService _gameLanguage; + private readonly IRaidManager _raidManager; + + public RaidListUnregisterEventHandler(IRaidManager raidManager, IGameLanguageService gameLanguage) + { + _raidManager = raidManager; + _gameLanguage = gameLanguage; + } + + public async Task HandleAsync(RaidListUnregisterEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + + if (!session.PlayerEntity.IsInRaidParty) + { + return; + } + + if (!session.PlayerEntity.IsRaidLeader(session.PlayerEntity.Id)) + { + return; + } + + RaidParty raid = session.PlayerEntity.Raid; + if (!_raidManager.ContainsRaidInRaidPublishList(raid)) + { + return; + } + + _raidManager.UnregisterRaidFromRaidPublishList(raid); + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.RAID_SHOUTMESSAGE_UNREGISTERED, session.UserLanguage), MsgMessageType.Middle); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Raids/Handlers/RaidMonsterThrowEventHandler.cs b/srcs/_plugins/Plugin.Raids/Handlers/RaidMonsterThrowEventHandler.cs new file mode 100644 index 0000000..cb4311b --- /dev/null +++ b/srcs/_plugins/Plugin.Raids/Handlers/RaidMonsterThrowEventHandler.cs @@ -0,0 +1,68 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game; +using WingsEmu.Game._enum; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Items; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Raids; +using WingsEmu.Game.Raids.Events; + +namespace Plugin.Raids; + +public class RaidMonsterThrowEventHandler : IAsyncEventProcessor +{ + private readonly IGameItemInstanceFactory _gameItemInstanceFactory; + private readonly IRandomGenerator _randomGenerator; + + public RaidMonsterThrowEventHandler(IRandomGenerator randomGenerator, IGameItemInstanceFactory gameItemInstanceFactory) + { + _randomGenerator = randomGenerator; + _gameItemInstanceFactory = gameItemInstanceFactory; + } + + public async Task HandleAsync(RaidMonsterThrowEvent e, CancellationToken cancellation) + { + const byte minimumDistance = 0; + const byte maximumDistance = 10; + + int length = e.Drops.Count; + + for (int i = 0; i < e.ItemDropsAmount; i++) + { + int number = _randomGenerator.RandomNumber(0, length); + Drop drop = e.Drops[number]; + ThrowEvent(e.MonsterEntity, drop.ItemVNum, drop.Amount, minimumDistance, maximumDistance); + } + + for (int i = 0; i < e.GoldDropsAmount; i++) + { + int gold = _randomGenerator.RandomNumber(e.GoldDropRange.Minimum, e.GoldDropRange.Maximum + 1); + ThrowEvent(e.MonsterEntity, (int)ItemVnums.GOLD, gold, minimumDistance, maximumDistance); + } + } + + private void ThrowEvent(IBattleEntity battleEntity, int itemVNum, int quantity, byte minimumDistance, byte maximumDistance) + { + GameItemInstance newItem = _gameItemInstanceFactory.CreateItem(itemVNum, quantity); + + short rndX = -1; + short rndY = -1; + int count = 0; + while ((rndX == -1 || rndY == -1 || battleEntity.MapInstance.IsBlockedZone(rndX, rndY)) && count < 100) + { + rndX = (short)(battleEntity.PositionX + _randomGenerator.RandomNumber(minimumDistance, maximumDistance + 1) * (_randomGenerator.RandomNumber(0, 2) * 2 - 1)); + rndY = (short)(battleEntity.PositionY + _randomGenerator.RandomNumber(minimumDistance, maximumDistance + 1) * (_randomGenerator.RandomNumber(0, 2) * 2 - 1)); + count++; + } + + var position = new Position(rndX, rndY); + + var item = new MonsterMapItem(position.X, position.Y, newItem, battleEntity.MapInstance); + + battleEntity.MapInstance.AddDrop(item); + battleEntity.BroadcastThrow(item); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Raids/Handlers/RaidObjectiveIncreaseEventHandler.cs b/srcs/_plugins/Plugin.Raids/Handlers/RaidObjectiveIncreaseEventHandler.cs new file mode 100644 index 0000000..88bf399 --- /dev/null +++ b/srcs/_plugins/Plugin.Raids/Handlers/RaidObjectiveIncreaseEventHandler.cs @@ -0,0 +1,53 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using Plugin.Raids.Const; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Raids; +using WingsEmu.Game.Raids.Events; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.Raids; + +public class RaidObjectiveIncreaseEventHandler : IAsyncEventProcessor +{ + private readonly IGameLanguageService _languageService; + + public RaidObjectiveIncreaseEventHandler(IGameLanguageService languageService) => _languageService = languageService; + + public async Task HandleAsync(RaidObjectiveIncreaseEvent e, CancellationToken cancellation) + { + switch (e.RaidTargetType) + { + case RaidTargetType.Monster: + e.RaidSubInstance.CurrentCompletedTargetMonsters++; + break; + case RaidTargetType.Button: + e.RaidSubInstance.CurrentCompletedTargetButtons++; + break; + default: + throw new ArgumentOutOfRangeException(); + } + + if (e.RaidSubInstance.TargetsCompleted) + { + e.RaidSubInstance.MapInstance.Broadcast(x => x.GenerateMsgPacket( + _languageService.GetLanguage(GameDialogKey.RAID_SHOUTMESSAGE_TARGETS_COMPLETED, x.UserLanguage), MsgMessageType.Middle)); + + await e.RaidSubInstance.TriggerEvents(RaidConstEventKeys.ObjectivesCompleted); + } + else + { + e.RaidSubInstance.MapInstance.Broadcast(x => x.GenerateMsgPacket( + _languageService.GetLanguage(GameDialogKey.RAID_SHOUTMESSAGE_TARGETS_UPDATED, x.UserLanguage), MsgMessageType.Middle)); + } + + foreach (IClientSession member in e.RaidSubInstance.MapInstance.Sessions) + { + member.SendRaidmbf(); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Raids/Handlers/RaidPartyCreateEventHandler.cs b/srcs/_plugins/Plugin.Raids/Handlers/RaidPartyCreateEventHandler.cs new file mode 100644 index 0000000..8100bda --- /dev/null +++ b/srcs/_plugins/Plugin.Raids/Handlers/RaidPartyCreateEventHandler.cs @@ -0,0 +1,161 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using Plugin.Raids.Extension; +using Plugin.Raids.Scripting; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsAPI.Game.Extensions.ItemExtension.Item; +using WingsAPI.Packets.Enums; +using WingsAPI.Scripting.Object.Raid; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Raids; +using WingsEmu.Game.Raids.Events; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.Raids; + +public class RaidPartyCreateEventHandler : IAsyncEventProcessor +{ + private readonly IGameLanguageService _gameLanguage; + private readonly IItemsManager _itemsManager; + private readonly IRaidManager _raidManager; + private readonly RaidScriptManager _raidScriptManager; + + public RaidPartyCreateEventHandler(RaidScriptManager raidScriptManager, IGameLanguageService gameLanguage, IRaidManager raidManager, IItemsManager itemsManager) + { + _raidScriptManager = raidScriptManager; + _gameLanguage = gameLanguage; + _raidManager = raidManager; + _itemsManager = itemsManager; + } + + public async Task HandleAsync(RaidPartyCreateEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + if (session.PlayerEntity.IsInRaidParty) + { + return; + } + + if (session.PlayerEntity.HasRaidStarted) + { + return; + } + + if (session.PlayerEntity.IsInGroup()) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.RAID_SHOUTMESSAGE_CANT_CREATE_RAID_GROUP, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (session.PlayerEntity.HasShopOpened) + { + return; + } + + if (session.PlayerEntity.IsOnVehicle) + { + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.ITEM_MESSAGE_CANT_USE, session.UserLanguage), ChatMessageColorType.Yellow); + return; + } + + if (!session.PlayerEntity.MapInstance.HasMapFlag(MapFlags.IS_BASE_MAP)) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_MUST_BE_IN_CLASSIC_MAP, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (session.CantPerformActionOnAct4()) + { + return; + } + + if (!Enum.TryParse(e.RaidType.ToString(), out RaidType raidType)) + { + return; + } + + SRaidRequirement requirement = _raidScriptManager.GetScriptedRaid(raidType.ToSRaidType())?.Requirement; + if (requirement == null) + { + session.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.RAID_INFO_NO_EXIST, session.UserLanguage)); + return; + } + + if (session.PlayerEntity.Level < requirement.MinimumLevel) + { + session.SendChatMessage(_gameLanguage.GetLanguageFormat(GameDialogKey.RAID_CHATMESSAGE_LOW_LEVEL, session.UserLanguage, requirement.MinimumLevel.ToString()), ChatMessageColorType.Red); + return; + } + + if (session.PlayerEntity.HeroLevel < requirement.MinimumHeroLevel) + { + session.SendChatMessage(_gameLanguage.GetLanguageFormat(GameDialogKey.RAID_CHATMESSAGE_LOW_LEVEL, session.UserLanguage, requirement.MinimumHeroLevel.ToString()), ChatMessageColorType.Red); + return; + } + + if (session.IsRaidTypeRestricted(raidType)) + { + if (!session.IsPlayerWearingRaidAmulet(raidType)) + { + string amuletName = raidType == RaidType.LordDraco + ? _itemsManager.GetItem((short)ItemVnums.DRACO_AMULET)?.GetItemName(_gameLanguage, session.UserLanguage) + : _itemsManager.GetItem((short)ItemVnums.GLACERUS_AMULET)?.GetItemName(_gameLanguage, session.UserLanguage); + + session.SendChatMessage(session.GetLanguageFormat(GameDialogKey.RAID_CHATMESSAGE_AMULET_NEEDED, amuletName), ChatMessageColorType.Yellow); + return; + } + + string getRaidName = session.GenerateRaidName(_gameLanguage, raidType); + if (!session.CanPlayerJoinToRestrictedRaid(raidType)) + { + session.SendChatMessage(session.GetLanguageFormat(GameDialogKey.RAID_CHATMESSAGE_LIMIT_REACHED, getRaidName), ChatMessageColorType.Yellow); + return; + } + + switch (raidType) + { + case RaidType.LordDraco: + session.SendChatMessage(session.GetLanguageFormat(GameDialogKey.RAID_CHATMESSAGE_LIMIT_LEFT, + getRaidName, session.PlayerEntity.RaidRestrictionDto.LordDraco), ChatMessageColorType.Yellow); + break; + case RaidType.Glacerus: + session.SendChatMessage(session.GetLanguageFormat(GameDialogKey.RAID_CHATMESSAGE_LIMIT_LEFT, + getRaidName, session.PlayerEntity.RaidRestrictionDto.Glacerus), ChatMessageColorType.Yellow); + break; + } + } + + InventoryItem itemToRemove = e.ItemToRemove; + if (itemToRemove != null) + { + await session.RemoveItemFromInventory(item: itemToRemove); + } + + var raidInstance = new RaidParty(Guid.NewGuid(), raidType, requirement.MinimumLevel, requirement.MaximumLevel, requirement.MinimumHeroLevel, requirement.MaximumHeroLevel, + requirement.MinimumParticipant, + requirement.MaximumParticipant); + + raidInstance.AddMember(session); + _raidManager.AddRaid(raidInstance); + + session.PlayerEntity.SetRaidParty(raidInstance); + session.SendRaidPacket(RaidPacketType.LEADER_RELATED); + session.SendRaidPacket(RaidPacketType.LIST_MEMBERS); + session.SendRaidPacket(RaidPacketType.LEAVE); + session.RefreshRaidMemberList(); + + await session.EmitEventAsync(new RaidCreatedEvent()); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Raids/Handlers/RaidPartyDisbandEventHandler.cs b/srcs/_plugins/Plugin.Raids/Handlers/RaidPartyDisbandEventHandler.cs new file mode 100644 index 0000000..71a5ff2 --- /dev/null +++ b/srcs/_plugins/Plugin.Raids/Handlers/RaidPartyDisbandEventHandler.cs @@ -0,0 +1,87 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Raids; +using WingsEmu.Game.Raids.Enum; +using WingsEmu.Game.Raids.Events; +using WingsEmu.Game.Revival; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.Raids; + +public class RaidPartyDisbandEventHandler : IAsyncEventProcessor +{ + private readonly IAsyncEventPipeline _eventPipeline; + private readonly IGameLanguageService _gameLanguage; + private readonly IRaidManager _raidManager; + + public RaidPartyDisbandEventHandler(IGameLanguageService gameLanguage, IRaidManager raidManager, IAsyncEventPipeline eventPipeline) + { + _gameLanguage = gameLanguage; + _raidManager = raidManager; + _eventPipeline = eventPipeline; + } + + public async Task HandleAsync(RaidPartyDisbandEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + + if (!session.PlayerEntity.IsInRaidParty) + { + return; + } + + RaidParty raid = session.PlayerEntity.Raid; + + if (raid?.Members == null) + { + return; + } + + if (!session.PlayerEntity.IsRaidLeader(session.PlayerEntity.Id)) + { + return; + } + + if (session.PlayerEntity.Raid.Finished && e.IsByRdPacket) + { + return; + } + + if (session.PlayerEntity.HasRaidStarted && session.PlayerEntity.IsRaidLeader(session.PlayerEntity.Id) && raid.Members.Count > 1) + { + await session.EmitEventAsync(new RaidPartyLeaveEvent(false)); + return; + } + + foreach (IClientSession member in raid.Members) + { + RemoveFromRaid(member, raid); + } + + await _eventPipeline.ProcessEventAsync(new RaidInstanceFinishEvent(raid, RaidFinishType.Disbanded), cancellation); + await session.EmitEventAsync(new RaidAbandonedEvent { RaidId = raid.Id }); + } + + private void RemoveFromRaid(IClientSession session, RaidParty raidParty) + { + RaidPartyLeaveEventHandler.InternalLeave(session); + + if (raidParty.Started) + { + if (!session.PlayerEntity.IsAlive()) + { + session.EmitEvent(new RevivalReviveEvent()); + } + + session.ChangeToLastBaseMap(); + } + + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.RAID_MESSAGE_DISOLVED, session.UserLanguage), ChatMessageColorType.Red); + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.RAID_MESSAGE_DISOLVED, session.UserLanguage), MsgMessageType.Middle); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Raids/Handlers/RaidPartyInvitePlayerEventHandler.cs b/srcs/_plugins/Plugin.Raids/Handlers/RaidPartyInvitePlayerEventHandler.cs new file mode 100644 index 0000000..aa5b04a --- /dev/null +++ b/srcs/_plugins/Plugin.Raids/Handlers/RaidPartyInvitePlayerEventHandler.cs @@ -0,0 +1,122 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Raids.Events; +using WingsEmu.Game.Relations; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.Raids; + +public class RaidPartyInvitePlayerEventHandler : IAsyncEventProcessor +{ + private readonly IGameLanguageService _gameLanguage; + private readonly IInvitationManager _invitationManager; + private readonly ISessionManager _sessionManager; + + public RaidPartyInvitePlayerEventHandler(ISessionManager sessionManager, IGameLanguageService gameLanguage, IInvitationManager invitationManager) + { + _sessionManager = sessionManager; + _gameLanguage = gameLanguage; + _invitationManager = invitationManager; + } + + public async Task HandleAsync(RaidPartyInvitePlayerEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + long targetId = e.TargetId; + + if (session.PlayerEntity.Id == targetId) + { + return; + } + + if (!session.PlayerEntity.IsInRaidParty) + { + return; + } + + if (session.PlayerEntity.HasRaidStarted) + { + return; + } + + if (!session.PlayerEntity.IsRaidLeader(session.PlayerEntity.Id)) + { + return; + } + + if (session.CurrentMapInstance.MapInstanceType == MapInstanceType.ArenaInstance) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.RAID_SHOUTMESSAGE_CANT_JOIN_FROM_ARENA, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (!session.CurrentMapInstance.HasMapFlag(MapFlags.IS_BASE_MAP)) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_MUST_BE_IN_CLASSIC_MAP, session.UserLanguage), MsgMessageType.Middle); + return; + } + + IClientSession target = _sessionManager.GetSessionByCharacterId(targetId); + if (target == null) + { + return; + } + + if (session.PlayerEntity.IsBlocking(targetId)) + { + session.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.BLACKLIST_INFO_BLOCKING, session.UserLanguage)); + return; + } + + if (target.PlayerEntity.IsBlocking(session.PlayerEntity.Id)) + { + session.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.BLACKLIST_INFO_BLOCKED, session.UserLanguage)); + return; + } + + if (target.PlayerEntity.IsInGroup()) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.RAID_SHOUTMESSAGE_CANT_INVITE_GROUP, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (target.IsMuted()) + { + session.SendMsg(session.GetLanguage(GameDialogKey.MUTE_SHOUTMESSAGE_PLAYER_IS_MUTED), MsgMessageType.Middle); + return; + } + + if (target.PlayerEntity.IsInRaidParty) + { + return; + } + + if (target.PlayerEntity.HasRaidStarted) + { + return; + } + + if (target.PlayerEntity.HasShopOpened) + { + return; + } + + if (!session.CurrentMapInstance.HasMapFlag(MapFlags.IS_BASE_MAP)) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_USER_NOT_BASEMAP, session.UserLanguage), MsgMessageType.Middle); + return; + } + + session.SendChatMessage(_gameLanguage.GetLanguageFormat(GameDialogKey.RAID_CHATMESSAGE_GROUP_REQUEST, session.UserLanguage, target.PlayerEntity.Name), ChatMessageColorType.Yellow); + await session.EmitEventAsync(new InvitationEvent(targetId, InvitationType.Raid)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Raids/Handlers/RaidPartyJoinEventHandler.cs b/srcs/_plugins/Plugin.Raids/Handlers/RaidPartyJoinEventHandler.cs new file mode 100644 index 0000000..a98e60f --- /dev/null +++ b/srcs/_plugins/Plugin.Raids/Handlers/RaidPartyJoinEventHandler.cs @@ -0,0 +1,215 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using PhoenixLib.Logging; +using WingsAPI.Game.Extensions.ItemExtension.Item; +using WingsAPI.Packets.Enums; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Raids; +using WingsEmu.Game.Raids.Enum; +using WingsEmu.Game.Raids.Events; +using WingsEmu.Game.Relations; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.Raids; + +public class RaidPartyJoinEventHandler : IAsyncEventProcessor +{ + private static readonly SemaphoreSlim _semaphoreSlim = new(1, 1); + private readonly IGameLanguageService _gameLanguage; + private readonly IInvitationManager _invitationManager; + private readonly IItemsManager _itemsManager; + private readonly IRaidManager _raidManager; + private readonly ISessionManager _sessionManager; + + public RaidPartyJoinEventHandler(IGameLanguageService gameLanguage, ISessionManager sessionManager, IInvitationManager invitationManager, IRaidManager raidManager, IItemsManager itemsManager) + { + _gameLanguage = gameLanguage; + _sessionManager = sessionManager; + _invitationManager = invitationManager; + _raidManager = raidManager; + _itemsManager = itemsManager; + } + + public async Task HandleAsync(RaidPartyJoinEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + long raidOwnerId = e.RaidOwnerId; + bool isByRaidList = e.IsByRaidList; + + await _semaphoreSlim.WaitAsync(cancellation); + try + { + if (session.PlayerEntity.Id == raidOwnerId) + { + return; + } + + if (session.IsMuted()) + { + session.SendMsg(session.GetLanguage(GameDialogKey.MUTE_SHOUTMESSAGE_YOU_ARE_MUTED), MsgMessageType.Middle); + return; + } + + IClientSession raidOwner = _sessionManager.GetSessionByCharacterId(raidOwnerId); + if (raidOwner?.PlayerEntity.Raid == null) + { + Log.Debug($"[ERROR] Character with ID {raidOwnerId.ToString()} was not found."); + return; + } + + if (session.PlayerEntity.IsBlocking(raidOwnerId)) + { + session.SendInfo(session.GetLanguage(GameDialogKey.BLACKLIST_INFO_BLOCKING)); + return; + } + + if (raidOwner.PlayerEntity.IsBlocking(session.PlayerEntity.Id)) + { + session.SendInfo(session.GetLanguage(GameDialogKey.BLACKLIST_INFO_BLOCKED)); + return; + } + + if (raidOwner.PlayerEntity.HasRaidStarted) + { + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.RAID_CHATMESSAGE_NO_EXIST_OR_ALREADY_STARTED, session.UserLanguage), ChatMessageColorType.Yellow); + return; + } + + if (!_invitationManager.ContainsPendingInvitation(raidOwnerId, session.PlayerEntity.Id, InvitationType.Raid) && !isByRaidList) + { + Log.Debug($"[ERROR] Character with ID {session.PlayerEntity.Id.ToString()} to join a raid, but hadn't an invitation."); + return; + } + + if (!isByRaidList) + { + _invitationManager.RemovePendingInvitation(raidOwnerId, session.PlayerEntity.Id, InvitationType.Raid); + } + + RaidParty raidParty = raidOwner.PlayerEntity.Raid; + if (isByRaidList && !_raidManager.ContainsRaidInRaidPublishList(raidParty)) + { + session.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.RAID_CHATMESSAGE_NO_EXIST_OR_ALREADY_STARTED, session.UserLanguage)); + await session.EmitEventAsync(new RaidListOpenEvent()); + return; + } + + if (!session.CurrentMapInstance.HasMapFlag(MapFlags.IS_BASE_MAP)) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_MUST_BE_IN_CLASSIC_MAP, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (raidOwner.PlayerEntity.RaidTeamIsFull) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.RAID_SHOUTMESSAGE_RAID_TEAM_FULL, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (session.PlayerEntity.IsInRaidParty) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.RAID_SHOUTMESSAGE_ALREADY_IN_RAID, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (session.PlayerEntity.TimeSpaceComponent.IsInTimeSpaceParty) + { + return; + } + + if (session.PlayerEntity.RainbowBattleComponent.IsInRainbowBattle) + { + return; + } + + if (session.PlayerEntity.HasRaidStarted || session.PlayerEntity.HasShopOpened) + { + return; + } + + if (session.PlayerEntity.IsInGroup()) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.RAID_SHOUTMESSAGE_IN_GROUP, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (session.PlayerEntity.Level < raidParty.MinimumLevel) + { + session.SendChatMessage(_gameLanguage.GetLanguageFormat(GameDialogKey.RAID_CHATMESSAGE_LOW_LEVEL, session.UserLanguage, raidParty.MinimumLevel.ToString()), ChatMessageColorType.Red); + return; + } + + if (session.PlayerEntity.HeroLevel < raidParty.MinimumHeroLevel) + { + session.SendChatMessage(_gameLanguage.GetLanguageFormat(GameDialogKey.RAID_CHATMESSAGE_LOW_LEVEL, session.UserLanguage, raidParty.MinimumHeroLevel.ToString()), + ChatMessageColorType.Red); + return; + } + + if (session.IsRaidTypeRestricted(raidParty.Type)) + { + if (!session.IsPlayerWearingRaidAmulet(raidParty.Type)) + { + string amuletName = raidParty.Type == RaidType.LordDraco + ? _itemsManager.GetItem((short)ItemVnums.DRACO_AMULET)?.GetItemName(_gameLanguage, session.UserLanguage) + : _itemsManager.GetItem((short)ItemVnums.GLACERUS_AMULET)?.GetItemName(_gameLanguage, session.UserLanguage); + + session.SendChatMessage(session.GetLanguageFormat(GameDialogKey.RAID_CHATMESSAGE_AMULET_NEEDED, amuletName), ChatMessageColorType.Yellow); + return; + } + + string getRaidName = session.GenerateRaidName(_gameLanguage, raidParty.Type); + if (!session.CanPlayerJoinToRestrictedRaid(raidParty.Type)) + { + session.SendChatMessage(session.GetLanguageFormat(GameDialogKey.RAID_CHATMESSAGE_LIMIT_REACHED, getRaidName), ChatMessageColorType.Yellow); + return; + } + + switch (raidParty.Type) + { + case RaidType.LordDraco: + session.SendChatMessage(session.GetLanguageFormat(GameDialogKey.RAID_CHATMESSAGE_LIMIT_LEFT, + getRaidName, session.PlayerEntity.RaidRestrictionDto.LordDraco), ChatMessageColorType.Yellow); + break; + case RaidType.Glacerus: + session.SendChatMessage(session.GetLanguageFormat(GameDialogKey.RAID_CHATMESSAGE_LIMIT_LEFT, + getRaidName, session.PlayerEntity.RaidRestrictionDto.Glacerus), ChatMessageColorType.Yellow); + break; + } + } + + if (session.PlayerEntity.Level > raidParty.MaximumLevel || session.PlayerEntity.HeroLevel > raidParty.MaximumHeroLevel) + { + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.RAID_CHATMESSAGE_LEVEL_TOO_HIGH, session.UserLanguage), ChatMessageColorType.Yellow); + } + + raidParty.AddMember(session); + session.PlayerEntity.SetRaidParty(raidParty); + + raidOwner.SendRaidPacket(RaidPacketType.LIST_MEMBERS); + foreach (IClientSession member in session.PlayerEntity.Raid.Members) + { + member.SendChatMessage(_gameLanguage.GetLanguageFormat(GameDialogKey.RAID_CHATMESSAGE_X_JOINED, member.UserLanguage, session.PlayerEntity.Name), ChatMessageColorType.Yellow); + member.SendMsg(_gameLanguage.GetLanguageFormat(GameDialogKey.RAID_SHOUTMESSAGE_X_JOINED, member.UserLanguage, session.PlayerEntity.Name), MsgMessageType.Middle); + member.RefreshRaidMemberList(); + } + + session.SendRaidPacket(RaidPacketType.LEAVE); + session.SendRaidPacket(RaidPacketType.LEADER_RELATED); + + await session.EmitEventAsync(new RaidJoinedEvent { JoinType = e.IsByRaidList ? RaidJoinType.RAID_LIST : RaidJoinType.INVITATION }); + } + finally + { + _semaphoreSlim.Release(); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Raids/Handlers/RaidPartyKickPlayerEventHandler.cs b/srcs/_plugins/Plugin.Raids/Handlers/RaidPartyKickPlayerEventHandler.cs new file mode 100644 index 0000000..3189cf4 --- /dev/null +++ b/srcs/_plugins/Plugin.Raids/Handlers/RaidPartyKickPlayerEventHandler.cs @@ -0,0 +1,59 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Raids.Events; + +namespace Plugin.Raids; + +public class RaidPartyKickPlayerEventHandler : IAsyncEventProcessor +{ + private readonly ISessionManager _sessionManager; + + public RaidPartyKickPlayerEventHandler(ISessionManager sessionManager) => _sessionManager = sessionManager; + + public async Task HandleAsync(RaidPartyKickPlayerEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + long targetId = e.CharacterId; + + if (!session.PlayerEntity.IsInRaidParty) + { + return; + } + + if (session.PlayerEntity.Id == targetId) + { + return; + } + + if (!session.PlayerEntity.IsRaidLeader(session.PlayerEntity.Id)) + { + return; + } + + if (session.PlayerEntity.HasRaidStarted) + { + return; + } + + IClientSession target = _sessionManager.GetSessionByCharacterId(targetId); + if (target == null) + { + return; + } + + if (!target.PlayerEntity.IsInRaidParty) + { + return; + } + + if (target.PlayerEntity.Raid.Id != session.PlayerEntity.Raid.Id) + { + return; + } + + await target.EmitEventAsync(new RaidPartyLeaveEvent(true)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Raids/Handlers/RaidPartyLeaveEventHandler.cs b/srcs/_plugins/Plugin.Raids/Handlers/RaidPartyLeaveEventHandler.cs new file mode 100644 index 0000000..3460e66 --- /dev/null +++ b/srcs/_plugins/Plugin.Raids/Handlers/RaidPartyLeaveEventHandler.cs @@ -0,0 +1,137 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Raids; +using WingsEmu.Game.Raids.Configuration; +using WingsEmu.Game.Raids.Events; +using WingsEmu.Game.Revival; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.Raids; + +public class RaidPartyLeaveEventHandler : IAsyncEventProcessor +{ + private readonly IGameLanguageService _gameLanguage; + private readonly RaidConfiguration _raidConfiguration; + + public RaidPartyLeaveEventHandler(IGameLanguageService gameLanguage, RaidConfiguration raidConfiguration) + { + _gameLanguage = gameLanguage; + _raidConfiguration = raidConfiguration; + } + + public async Task HandleAsync(RaidPartyLeaveEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + IPlayerEntity character = session.PlayerEntity; + bool byKick = e.ByKick; + + if (session?.PlayerEntity == null) + { + return; + } + + if (!character.IsInRaidParty) + { + return; + } + + bool isLeader = character.IsRaidLeader(character.Id); + if (!character.HasRaidStarted && isLeader) + { + await session.EmitEventAsync(new RaidPartyDisbandEvent()); + return; + } + + RaidParty raidParty = character.Raid; + + if (raidParty == null) + { + return; + } + + if (raidParty.Members == null) + { + InternalLeave(session); + return; + } + + if (raidParty.Members.Count - 1 < 1) + { + await session.EmitEventAsync(new RaidPartyDisbandEvent()); + return; + } + + if (session.PlayerEntity.RaidDeaths >= _raidConfiguration.LivesPerCharacter && session.PlayerEntity.Raid.Instance.Lives > 0) + { + session.PlayerEntity.ShowRaidDeathInfo = true; + } + + InternalLeave(session); + + if (raidParty.Started) + { + if (!session.PlayerEntity.IsAlive()) + { + await session.EmitEventAsync(new RevivalReviveEvent()); + } + + session.ChangeToLastBaseMap(); + + if (e.RemoveLife) + { + raidParty.Instance.IncreaseOrDecreaseLives(-1); + } + } + + GameDialogKey chatMessageDialog = GameDialogKey.RAID_CHATMESSAGE_LEFT; + GameDialogKey msgDialog = GameDialogKey.RAID_SHOUTMESSAGE_LEFT; + + if (byKick) + { + chatMessageDialog = GameDialogKey.RAID_CHATMESSAGE_KICKED_OTHER; + msgDialog = GameDialogKey.RAID_SHOUTMESSAGE_KICKED_OTHER; + + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.RAID_CHATMESSAGE_KICKED, session.UserLanguage), ChatMessageColorType.Yellow); + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.RAID_SHOUTMESSAGE_KICKED, session.UserLanguage), MsgMessageType.Middle); + } + + await session.EmitEventAsync(new RaidLeftEvent { RaidId = raidParty.Id }); + + foreach (IClientSession member in raidParty.Members) + { + if (raidParty.Started && isLeader) + { + member.SendChatMessage(_gameLanguage.GetLanguageFormat(GameDialogKey.RAID_CHATMESSAGE_NEW_LEADER, member.UserLanguage, raidParty.Leader.PlayerEntity.Name), + ChatMessageColorType.Yellow); + member.SendMsg(_gameLanguage.GetLanguageFormat(GameDialogKey.RAID_SHOUTMESSAGE_NEW_LEADER, member.UserLanguage, raidParty.Leader.PlayerEntity.Name), MsgMessageType.Middle); + } + else + { + member.SendChatMessage(_gameLanguage.GetLanguageFormat(chatMessageDialog, member.UserLanguage, character.Name), ChatMessageColorType.Yellow); + member.SendMsg(_gameLanguage.GetLanguageFormat(msgDialog, member.UserLanguage, character.Name), MsgMessageType.Middle); + } + + member.SendRaidPacket(RaidPacketType.LIST_MEMBERS); + member.RefreshRaidMemberList(); + } + } + + public static void InternalLeave(IClientSession session) + { + if (session?.PlayerEntity == null) + { + return; + } + + session.PlayerEntity.Raid?.RemoveMember(session); + session.PlayerEntity.SetRaidParty(null); + session.SendRaidPacket(RaidPacketType.LEAVE, true); + session.SendRaidPacket(RaidPacketType.LEADER_RELATED, true); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Raids/Handlers/RaidPlayerSwitchButtonEventHandler.cs b/srcs/_plugins/Plugin.Raids/Handlers/RaidPlayerSwitchButtonEventHandler.cs new file mode 100644 index 0000000..c0dac87 --- /dev/null +++ b/srcs/_plugins/Plugin.Raids/Handlers/RaidPlayerSwitchButtonEventHandler.cs @@ -0,0 +1,80 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using Plugin.Raids.Const; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Raids; +using WingsEmu.Game.Raids.Events; +using WingsEmu.Game.TimeSpaces; +using WingsEmu.Game.TimeSpaces.Events; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.Raids.Handlers; + +public class RaidPlayerSwitchButtonEventHandler : IAsyncEventProcessor +{ + public async Task HandleAsync(RaidPlayerSwitchButtonEvent e, CancellationToken cancellation) + { + e.ButtonMapItem.State = !e.ButtonMapItem.State; + e.ButtonMapItem.ItemVNum = e.ButtonMapItem.State ? e.ButtonMapItem.ActivatedStateVNum : e.ButtonMapItem.DeactivatedStateVNum; + await CheckTimeSpaceMessage(e.Sender, e.ButtonMapItem); + await e.ButtonMapItem.TriggerEvents(RaidConstEventKeys.ButtonSwitched); + + if (e.ButtonMapItem.NonDefaultState) + { + await e.ButtonMapItem.TriggerEvents(RaidConstEventKeys.ButtonTriggered); + } + + if (e.ButtonMapItem.CanBeMovedOnlyOnce.HasValue && !e.ButtonMapItem.CanBeMovedOnlyOnce.Value) + { + e.ButtonMapItem.CanBeMovedOnlyOnce = true; + } + + e.ButtonMapItem.BroadcastIn(); + await e.Sender.EmitEventAsync(new RaidSwitchButtonToggledEvent + { + LeverId = e.ButtonMapItem.TransportId + }); + } + + private async Task CheckTimeSpaceMessage(IClientSession session, ButtonMapItem button) + { + TimeSpaceParty timeSpace = session.PlayerEntity.TimeSpaceComponent.TimeSpace; + if (timeSpace?.Instance == null) + { + return; + } + + foreach (IClientSession member in timeSpace.Members) + { + member.SendMsg(member.GetLanguage(GameDialogKey.TIMESPACE_SHOUTMESSAGE_LEVER_OPERATED), MsgMessageType.Middle); + } + + if (!timeSpace.Instance.TimeSpaceObjective.InteractObjectsVnum.HasValue) + { + return; + } + + // The lever has been already moved, so it using deactivated vnum + if (timeSpace.Instance.TimeSpaceObjective.InteractObjectsVnum.Value != button.DeactivatedStateVNum) + { + return; + } + + if (!button.IsObjective) + { + return; + } + + if (button.AlreadyMoved) + { + return; + } + + button.AlreadyMoved = true; + timeSpace.Instance.TimeSpaceObjective.InteractedObjectsAmount++; + await session.EmitEventAsync(new TimeSpaceRefreshObjectiveProgressEvent()); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Raids/Handlers/RaidPortalOpenEventHandler.cs b/srcs/_plugins/Plugin.Raids/Handlers/RaidPortalOpenEventHandler.cs new file mode 100644 index 0000000..1b64272 --- /dev/null +++ b/srcs/_plugins/Plugin.Raids/Handlers/RaidPortalOpenEventHandler.cs @@ -0,0 +1,18 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.PacketGeneration; +using WingsEmu.Game.Raids.Events; +using WingsEmu.Packets.Enums; + +namespace Plugin.Raids; + +public class RaidPortalOpenEventHandler : IAsyncEventProcessor +{ + public async Task HandleAsync(RaidPortalOpenEvent e, CancellationToken cancellation) + { + e.Portal.Type = PortalType.Open; + + e.RaidSubInstance.MapInstance.MapClear(); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Raids/Handlers/RaidResetRestrictionEventHandler.cs b/srcs/_plugins/Plugin.Raids/Handlers/RaidResetRestrictionEventHandler.cs new file mode 100644 index 0000000..32baf12 --- /dev/null +++ b/srcs/_plugins/Plugin.Raids/Handlers/RaidResetRestrictionEventHandler.cs @@ -0,0 +1,31 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.DAL.Redis.Locks; +using PhoenixLib.Events; +using WingsAPI.Data.Character; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Raids.Events; + +namespace Plugin.Raids.Handlers; + +public class RaidResetRestrictionEventHandler : IAsyncEventProcessor +{ + private readonly IExpirableLockService _lockService; + + public RaidResetRestrictionEventHandler(IExpirableLockService lockService) => _lockService = lockService; + + public async Task HandleAsync(RaidResetRestrictionEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + + if (!await _lockService.TryAddTemporaryLockAsync($"game:locks:raid-restriction:{session.PlayerEntity.Id}", DateTime.UtcNow.Date.AddDays(1))) + { + return; + } + + session.PlayerEntity.RaidRestrictionDto ??= new CharacterRaidRestrictionDto(); + session.PlayerEntity.RaidRestrictionDto.LordDraco = 5; + session.PlayerEntity.RaidRestrictionDto.Glacerus = 5; + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Raids/Handlers/RaidTeleportMemberEventHandler.cs b/srcs/_plugins/Plugin.Raids/Handlers/RaidTeleportMemberEventHandler.cs new file mode 100644 index 0000000..f4e285f --- /dev/null +++ b/srcs/_plugins/Plugin.Raids/Handlers/RaidTeleportMemberEventHandler.cs @@ -0,0 +1,62 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using Serilog; +using WingsEmu.Game; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Raids.Events; + +namespace Plugin.Raids.Handlers; + +public class RaidTeleportMemberEventHandler : IAsyncEventProcessor +{ + private readonly IRandomGenerator _randomGenerator; + + public RaidTeleportMemberEventHandler(IRandomGenerator randomGenerator) => _randomGenerator = randomGenerator; + + public async Task HandleAsync(RaidTeleportMemberEvent e, CancellationToken cancellation) + { + short sourceX = e.SourcePosition.X; + short sourceY = e.SourcePosition.Y; + + short destX = e.DestinationPosition.X; + short destY = e.DestinationPosition.Y; + byte range = e.Range; + IMapInstance mapInstance = e.MapInstance; + + if (mapInstance == null) + { + Log.Debug("MapInstance for RaidMemberTeleport is null."); + return; + } + + IEnumerable membersToTeleport = mapInstance.GetCharactersInRange(e.SourcePosition, range); + foreach (IPlayerEntity member in membersToTeleport) + { + if (member == null) + { + continue; + } + + if (!member.IsAlive()) + { + continue; + } + + short x = (short)(destX + _randomGenerator.RandomNumber(-3, 3)); + short y = (short)(destY + _randomGenerator.RandomNumber(-3, 3)); + + if (mapInstance.IsBlockedZone(x, y)) + { + x = destX; + y = destY; + } + + member.TeleportOnMap(x, y, true); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Raids/Handlers/RevivalEventRaidHandler.cs b/srcs/_plugins/Plugin.Raids/Handlers/RevivalEventRaidHandler.cs new file mode 100644 index 0000000..9f0bfd5 --- /dev/null +++ b/srcs/_plugins/Plugin.Raids/Handlers/RevivalEventRaidHandler.cs @@ -0,0 +1,35 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Revival; + +namespace Plugin.Raids; + +public class RevivalEventRaidHandler : IAsyncEventProcessor +{ + private readonly IBuffFactory _buffFactory; + + public RevivalEventRaidHandler(IBuffFactory buffFactory) => _buffFactory = buffFactory; + + public async Task HandleAsync(RevivalReviveEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + if (session.PlayerEntity.IsAlive() || session.CurrentMapInstance is not { MapInstanceType: MapInstanceType.RaidInstance }) + { + return; + } + + session.PlayerEntity.Hp = session.PlayerEntity.MaxHp; + session.PlayerEntity.Mp = session.PlayerEntity.MaxMp; + session.RefreshStat(); + await session.PlayerEntity.Restore(restoreMates: false); + session.BroadcastRevive(); + await session.CheckPartnerBuff(); + session.SendBuffsPacket(); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Raids/Handlers/RevivalStartProcedureEventRaidHandler.cs b/srcs/_plugins/Plugin.Raids/Handlers/RevivalStartProcedureEventRaidHandler.cs new file mode 100644 index 0000000..d97975f --- /dev/null +++ b/srcs/_plugins/Plugin.Raids/Handlers/RevivalStartProcedureEventRaidHandler.cs @@ -0,0 +1,61 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Networking.Broadcasting; +using WingsEmu.Game.Raids.Configuration; +using WingsEmu.Game.Raids.Events; +using WingsEmu.Game.Revival; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.Raids; + +public class RevivalStartProcedureEventRaidHandler : IAsyncEventProcessor +{ + private readonly IGameLanguageService _gameLanguage; + private readonly RaidConfiguration _raidConfiguration; + private readonly ISessionManager _sessionManager; + + public RevivalStartProcedureEventRaidHandler(RaidConfiguration raidConfiguration, ISessionManager sessionManager, IGameLanguageService gameLanguage) + { + _raidConfiguration = raidConfiguration; + _sessionManager = sessionManager; + _gameLanguage = gameLanguage; + } + + public async Task HandleAsync(RevivalStartProcedureEvent e, CancellationToken cancellation) + { + if (e.Sender.PlayerEntity.IsAlive() || e.Sender.CurrentMapInstance.MapInstanceType != MapInstanceType.RaidInstance || e.Sender.PlayerEntity.Raid?.Instance == null) + { + return; + } + + _sessionManager.Broadcast( + x => { return x.GenerateMsgPacket(_gameLanguage.GetLanguageFormat(GameDialogKey.RAID_SHOUTMESSAGE_PLAYER_DEATH, x.UserLanguage, e.Sender.PlayerEntity.Name), MsgMessageType.Middle); }, + new RaidBroadcast(e.Sender.PlayerEntity.Raid.Id)); + + e.Sender.PlayerEntity.AddRaidDeath(); + await e.Sender.EmitEventAsync(new RaidInstanceLivesIncDecEvent(-1)); + + if (e.Sender.PlayerEntity.Raid.Finished) + { + return; + } + + if (e.Sender.PlayerEntity.RaidDeaths >= _raidConfiguration.LivesPerCharacter) + { + await e.Sender.EmitEventAsync(new RaidPartyLeaveEvent(false, false)); + } + else + { + e.Sender.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.RAID_INFO_PLAYER_DEATH, e.Sender.UserLanguage)); + } + + e.Sender.PlayerEntity.UpdateRevival(DateTime.UtcNow + _raidConfiguration.RaidDeathRevivalDelay, RevivalType.DontPayRevival, ForcedType.Reconnect); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Raids/Plugin.Raids.csproj b/srcs/_plugins/Plugin.Raids/Plugin.Raids.csproj new file mode 100644 index 0000000..ca1337e --- /dev/null +++ b/srcs/_plugins/Plugin.Raids/Plugin.Raids.csproj @@ -0,0 +1,24 @@ + + + + net5.0 + latest + + + + + + + + + + + + + + + + + + + diff --git a/srcs/_plugins/Plugin.Raids/RaidFactory.cs b/srcs/_plugins/Plugin.Raids/RaidFactory.cs new file mode 100644 index 0000000..cc37bc5 --- /dev/null +++ b/srcs/_plugins/Plugin.Raids/RaidFactory.cs @@ -0,0 +1,336 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Microsoft.Extensions.DependencyInjection; +using PhoenixLib.Events; +using PhoenixLib.Logging; +using Plugin.Raids.Extension; +using WingsAPI.Game.Extensions.PacketGeneration; +using WingsAPI.Scripting.Attribute; +using WingsAPI.Scripting.Converter; +using WingsAPI.Scripting.Enum; +using WingsAPI.Scripting.Event; +using WingsAPI.Scripting.Object.Common; +using WingsAPI.Scripting.Object.Common.Map; +using WingsAPI.Scripting.Object.Raid; +using WingsAPI.Scripting.ScriptManager; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Entities.Event; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Monster; +using WingsEmu.Game.Portals; +using WingsEmu.Game.Raids; +using WingsEmu.Packets.Enums; + +namespace Plugin.Raids; + +public class RaidFactory : IRaidFactory +{ + private static readonly IEnumerable Converters; + private readonly IAsyncEventPipeline _eventPipeline; + private readonly IMapManager _mapManager; + private readonly IMonsterEntityFactory _monsterEntityFactory; + private readonly INpcMonsterManager _npcMonsterManager; + private readonly IPortalFactory _portalFactory; + private readonly IRaidScriptManager _raidScriptManager; + + static RaidFactory() + { + Converters = typeof(IScriptedEventConverter).Assembly.GetTypes() + .Concat(typeof(RaidFactory).Assembly.GetTypes()) + .Where(x => typeof(IScriptedEventConverter).IsAssignableFrom(x)) + .Where(x => !x.IsAbstract && !x.IsInterface); + } + + public RaidFactory(IRaidScriptManager raidScriptManager, IMapManager mapManager, IAsyncEventPipeline eventPipeline, IMonsterEntityFactory monsterEntityFactory, + INpcMonsterManager npcMonsterManager, IPortalFactory portalFactory) + { + _raidScriptManager = raidScriptManager; + _mapManager = mapManager; + _eventPipeline = eventPipeline; + _monsterEntityFactory = monsterEntityFactory; + _npcMonsterManager = npcMonsterManager; + _portalFactory = portalFactory; + } + + public RaidInstance CreateRaid(RaidParty raidParty) + { + SRaid scriptedRaid = _raidScriptManager.GetScriptedRaid(raidParty.Type.ToSRaidType()); + if (scriptedRaid == null) + { + Log.Warn($"Can't create raid {raidParty.Type}, Couldn't find it in RaidScriptManager"); + return default; + } + + IServiceCollection serviceLocator = new ServiceCollection(); + + foreach (Type converter in Converters) + { + serviceLocator.AddTransient(typeof(IScriptedEventConverter), converter); + } + + var raidSubInstances = new Dictionary(); + GenerateMaps(scriptedRaid, raidSubInstances); + + RaidReward raidReward = InitializeRaidRewards(scriptedRaid); + + serviceLocator.AddSingleton(raidSubInstances); + + var portals = new Dictionary(); + var monsters = new Dictionary(); + var buttons = new Dictionary(); + + // Register portals, monsters and buttons to service locator + serviceLocator.AddSingleton(portals); + serviceLocator.AddSingleton(monsters); + serviceLocator.AddSingleton(buttons); + + foreach (SMap scriptedMap in scriptedRaid.Maps) + { + RaidSubInstance raidSubInstance = raidSubInstances[scriptedMap.Id]; + IMapInstance mapInstance = raidSubInstance.MapInstance; + + serviceLocator.AddSingleton(raidParty); + serviceLocator.AddSingleton(raidSubInstance); + serviceLocator.AddSingleton(mapInstance); + + + // This dictionary will contains all events to attach later + var events = new Dictionary>> + { + [raidSubInstance] = scriptedMap.Events + }; + + // Add Raid Waves + AddWaves(scriptedMap, raidSubInstance); + AddPortals(scriptedMap, mapInstance, raidSubInstances, portals); + AddMonsters(scriptedMap, mapInstance, monsters, events, raidSubInstance); + AddMapObjects(scriptedMap, mapInstance, events, raidSubInstance, buttons); + + + // Get event converters using service locator + var converters = serviceLocator.BuildServiceProvider() + .GetServices() + .ToDictionary(x => x.EventType, x => x); + + // Add all events using event converters + foreach ((IEventTriggerContainer eventContainer, IDictionary> containerEvents) in events) + { + foreach ((string trigger, IEnumerable scriptedEvents) in containerEvents) + { + foreach (SEvent scriptedEvent in scriptedEvents) + { + IAsyncEvent e = converters.GetValueOrDefault(scriptedEvent.GetType())?.Convert(scriptedEvent); + if (e == null) + { + throw new InvalidOperationException($"Failed to convert {scriptedEvent.GetType().Name} to async event"); + } + + ScriptEventAttribute attribute = scriptedEvent.GetType().GetCustomAttribute(); + if (attribute == null) + { + throw new InvalidOperationException($"Failed to find attribute for: {scriptedEvent.GetType().Name}"); + } + + eventContainer.AddEvent(trigger, e, attribute.IsRemovedOnTrigger); + } + } + } + } + + return new RaidInstance(raidSubInstances.Values, raidSubInstances[scriptedRaid.Spawn.MapId], new Position(scriptedRaid.Spawn.Position.X, scriptedRaid.Spawn.Position.Y), + TimeSpan.FromSeconds(scriptedRaid.DurationInSeconds), (byte)raidParty.Members.Count, raidReward); + } + + private void AddWaves(SMap scriptedMap, RaidSubInstance raidSubInstance) + { + var raidWaves = new Dictionary(); + byte wave = 0; + foreach (SMonsterWave scriptedRaidWave in scriptedMap.MonsterWaves) + { + var waveMonsters = new List(); + + foreach (SMonster mobWave in scriptedRaidWave.Monsters) + { + IMonsterData monsterData = _npcMonsterManager.GetNpc(mobWave.Vnum); + if (monsterData == null) + { + continue; + } + + ConcurrentDictionary waypointsDictionary = null; + if (mobWave.Waypoints != null) + { + byte waypoints = 0; + foreach (SWaypoint waypoint in mobWave.Waypoints) + { + waypointsDictionary ??= new ConcurrentDictionary(); + + var newWaypoint = new Waypoint + { + X = waypoint.X, + Y = waypoint.Y, + WaitTime = waypoint.WaitTime + }; + + waypointsDictionary.TryAdd(waypoints, newWaypoint); + waypoints++; + } + } + + var newToSummon = new ToSummon + { + VNum = mobWave.Vnum, + SpawnCell = mobWave.IsRandomPosition ? null : new Position(mobWave.Position.X, mobWave.Position.Y), + IsHostile = true, + IsMoving = monsterData.CanWalk, + SetHitChance = 5, + GoToBossPosition = mobWave.GoToBossPosition == default ? null : new Position(mobWave.GoToBossPosition.X, mobWave.GoToBossPosition.Y), + Waypoints = waypointsDictionary, + SummonType = SummonType.MONSTER_WAVE, + Direction = mobWave.Direction + }; + + waveMonsters.Add(newToSummon); + } + + var newRaidWave = new RaidWave(waveMonsters, scriptedRaidWave.TimeInSeconds); + + raidWaves[wave] = newRaidWave; + wave++; + } + + raidSubInstance.AddRaidWave(raidWaves); + } + + private void AddMapObjects(SMap scriptedMap, IMapInstance mapInstance, Dictionary>> events, RaidSubInstance raidSubInstance, + Dictionary buttons) + { + foreach (SMapObject scriptedObject in scriptedMap.Objects) + { + if (scriptedObject is not SButton scriptedButton) + { + continue; + } + + var button = new ButtonMapItem(scriptedButton.Position.X, scriptedButton.Position.Y, scriptedButton.DeactivatedVnum, scriptedButton.ActivatedVnum, false, + mapInstance, _eventPipeline, isObjective: scriptedButton.IsObjective, onlyOnce: scriptedButton.OnlyOnce == false ? null : false); + + events[button] = scriptedButton.Events; + buttons[scriptedObject.Id] = button; + raidSubInstance.AddRaidButton(button); + } + } + + private void AddMonsters(SMap scriptedMap, IMapInstance mapInstance, Dictionary monsters, Dictionary>> events, + RaidSubInstance raidSubInstance) + { + // Add monsters + foreach (SMonster scriptedMonster in scriptedMap.Monsters) + { + var drop = new List(); + foreach (SDropChance dropChance in scriptedMonster.Drop) + { + drop.Add(new DropChance(dropChance.Chance, dropChance.ItemVnum, dropChance.Amount)); + } + + IMonsterEntity monster = _monsterEntityFactory.CreateMonster(scriptedMonster.Vnum, mapInstance, + new MonsterEntityBuilder + { + IsRespawningOnDeath = false, + IsBoss = scriptedMonster.IsBoss, + IsTarget = scriptedMonster.IsTarget, + IsWalkingAround = scriptedMonster.CanMove, + IsHostile = true, + SetHitChance = 5, + HpMultiplier = scriptedMonster.IsBoss ? null : scriptedMonster.IsTarget ? 21f : 6f, + MpMultiplier = scriptedMonster.IsBoss ? null : scriptedMonster.IsTarget ? 21f : 6f, + RaidDrop = drop, + GeneratedGuid = scriptedMonster.Id, + Direction = scriptedMonster.Direction + }); + + if (scriptedMonster.Waypoints != null) + { + byte waypoints = 0; + foreach (SWaypoint waypoint in scriptedMonster.Waypoints) + { + monster.Waypoints ??= new ConcurrentDictionary(); + + var newWaypoint = new Waypoint + { + X = waypoint.X, + Y = waypoint.Y, + WaitTime = waypoint.WaitTime + }; + + monster.Waypoints.TryAdd(waypoints, newWaypoint); + waypoints++; + } + } + + monster.EmitEventAsync(new MapJoinMonsterEntityEvent(monster, scriptedMonster.Position.X, scriptedMonster.Position.Y)); + + monsters[scriptedMonster.Id] = monster; + events[monster] = scriptedMonster.Events; + + raidSubInstance.AddRaidMonster(monster); + } + } + + private void AddPortals(SMap scriptedMap, IMapInstance mapInstance, Dictionary raidSubInstances, Dictionary portals) + { + // Add portals + foreach (SPortal scriptedPortal in scriptedMap.Portals) + { + var sourcePos = new Position(scriptedPortal.SourcePosition.X, scriptedPortal.SourcePosition.Y); + var destPos = new Position(scriptedPortal.DestinationPosition.X, scriptedPortal.DestinationPosition.Y); + IPortalEntity portal = _portalFactory.CreatePortal((PortalType)scriptedPortal.Type, mapInstance, sourcePos, + raidSubInstances[scriptedPortal.DestinationId].MapInstance.Id, destPos); + + portals[scriptedPortal.Id] = portal; + + mapInstance.AddPortalToMap(portal); + } + } + + private static RaidReward InitializeRaidRewards(SRaid scriptedRaid) + { + var raidBoxRarities = new List(); + + foreach (SRaidBoxRarity boxRarity in scriptedRaid.Reward.RaidBox.RaidBoxRarity) + { + var newRarity = new RaidBoxRarity(boxRarity.Rarity, boxRarity.Chance); + raidBoxRarities.Add(newRarity); + } + + var raidBox = new RaidBox(scriptedRaid.Reward.RaidBox.RewardBox, raidBoxRarities); + var raidReward = new RaidReward(raidBox, scriptedRaid.Reward.DefaultReputation, scriptedRaid.Reward.FixedReputation); + return raidReward; + } + + private void GenerateMaps(SRaid scriptedRaid, Dictionary raidSubInstances) + { + foreach (SMap scriptedMap in scriptedRaid.Maps) + { + IMapInstance map = scriptedMap.MapType == SMapType.MapId + ? _mapManager.GenerateMapInstanceByMapId(scriptedMap.MapIdVnum, MapInstanceType.RaidInstance) + : _mapManager.GenerateMapInstanceByMapVNum(new ServerMapDto + { + Flags = scriptedMap.Flags.Select(mapFlag => (MapFlags)(int)mapFlag).ToList(), + Id = scriptedMap.MapIdVnum, + MapVnum = scriptedMap.MapIdVnum, + NameId = scriptedMap.NameId, + MusicId = scriptedMap.MusicId + }, MapInstanceType.RaidInstance); + + raidSubInstances[scriptedMap.Id] = new RaidSubInstance(map, _eventPipeline); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Raids/RaidManager.cs b/srcs/_plugins/Plugin.Raids/RaidManager.cs new file mode 100644 index 0000000..a773aff --- /dev/null +++ b/srcs/_plugins/Plugin.Raids/RaidManager.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using WingsEmu.Game.Raids; + +namespace Plugin.Raids; + +public class RaidManager : IRaidManager +{ + private readonly HashSet _raidPublishList = new(RaidParty.IdComparer); + private readonly HashSet _raids = new(RaidParty.IdComparer); + public IReadOnlyCollection Raids => _raids; + public IReadOnlyCollection RaidPublishList => _raidPublishList; + + public void AddRaid(RaidParty raidParty) + { + if (_raids.Contains(raidParty)) + { + return; + } + + _raids.Add(raidParty); + } + + public void RemoveRaid(RaidParty raidParty) + { + _raids.Remove(raidParty); + } + + public bool ContainsRaidInRaidPublishList(RaidParty raidParty) => _raidPublishList.Contains(raidParty); + + public void RegisterRaidInRaidPublishList(RaidParty raidParty) + { + if (_raidPublishList.Contains(raidParty)) + { + return; + } + + _raidPublishList.Add(raidParty); + } + + public void UnregisterRaidFromRaidPublishList(RaidParty raidParty) + { + _raidPublishList.Remove(raidParty); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Raids/RaidsPlugin.cs b/srcs/_plugins/Plugin.Raids/RaidsPlugin.cs new file mode 100644 index 0000000..b454da3 --- /dev/null +++ b/srcs/_plugins/Plugin.Raids/RaidsPlugin.cs @@ -0,0 +1,20 @@ +using Plugin.Raids.Commands; +using WingsAPI.Plugins; +using WingsEmu.Commands.Interfaces; + +namespace Plugin.Raids; + +public class RaidsPlugin : IGamePlugin +{ + private readonly ICommandContainer _commands; + + public RaidsPlugin(ICommandContainer commands) => _commands = commands; + + public string Name => nameof(RaidsPlugin); + + public void OnLoad() + { + _commands.AddModule(); + _commands.AddModule(); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Raids/RaidsPluginCore.cs b/srcs/_plugins/Plugin.Raids/RaidsPluginCore.cs new file mode 100644 index 0000000..331ba71 --- /dev/null +++ b/srcs/_plugins/Plugin.Raids/RaidsPluginCore.cs @@ -0,0 +1,61 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using PhoenixLib.Configuration; +using PhoenixLib.Events; +using PhoenixLib.Logging; +using Plugin.Raids.RecurrentJob; +using Plugin.Raids.Scripting; +using Plugin.Raids.Scripting.Validator.Raid; +using WingsAPI.Communication.ServerApi.Protocol; +using WingsAPI.Plugins; +using WingsAPI.Scripting; +using WingsAPI.Scripting.LUA; +using WingsAPI.Scripting.ScriptManager; +using WingsEmu.Game.Raids; +using WingsEmu.Game.Raids.Configuration; + +namespace Plugin.Raids; + +public class RaidsPluginCore : IGameServerPlugin +{ + public string Name => nameof(RaidsPluginCore); + + public void AddDependencies(IServiceCollection services, GameServerLoader gameServer) + { + // TODO: Plz, when we have warmup we should move those "AddSingleton" down, so it only gets loaded when necessary + services.AddSingleton(); + + // Raid Script Cache + services.AddSingleton(); + services.AddSingleton(s => s.GetRequiredService()); + + if (gameServer.Type == GameChannelType.ACT_4) + { + Log.Debug("Not loading Raids plugin because this is an Act4 channel."); + return; + } + + services.AddEventHandlersInAssembly(); + + services.AddSingleton(); + services.AddHostedService(); + + services.TryAddSingleton(x => + { + IConfigurationPathProvider config = x.GetRequiredService(); + return new ScriptFactoryConfiguration + { + RootDirectory = config.GetConfigurationPath("scripts"), + LibDirectory = config.GetConfigurationPath("scripts/lib") + }; + }); + + // script factory + services.TryAddSingleton(); + + // factory + services.AddSingleton(); + + services.AddFileConfiguration(); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Raids/RecurrentJob/RaidSystem.cs b/srcs/_plugins/Plugin.Raids/RecurrentJob/RaidSystem.cs new file mode 100644 index 0000000..b7e877c --- /dev/null +++ b/srcs/_plugins/Plugin.Raids/RecurrentJob/RaidSystem.cs @@ -0,0 +1,158 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using PhoenixLib.Events; +using PhoenixLib.Logging; +using Plugin.Raids.Const; +using WingsEmu.Core.Extensions; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Monster.Event; +using WingsEmu.Game.Raids; +using WingsEmu.Game.Raids.Enum; +using WingsEmu.Game.Raids.Events; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.Raids.RecurrentJob; + +public class RaidSystem : BackgroundService +{ + private static readonly TimeSpan Interval = TimeSpan.FromSeconds(1); + private readonly IAsyncEventPipeline _eventPipeline; + private readonly IRaidManager _raidManager; + + public RaidSystem(IRaidManager raidManager, IAsyncEventPipeline eventPipeline) + { + _raidManager = raidManager; + _eventPipeline = eventPipeline; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + Log.Info("[RAID_SYSTEM] Started!"); + while (!stoppingToken.IsCancellationRequested) + { + try + { + foreach (RaidParty raid in _raidManager.Raids.ToArray()) + { + await ProcessRaid(raid); + } + } + catch (Exception e) + { + Log.Error("[RAID_SYSTEM]", e); + } + + await Task.Delay(Interval, stoppingToken); + } + } + + private async Task ProcessRaid(RaidParty raidParty) + { + DateTime currentDate = DateTime.UtcNow; + await TryFinish(raidParty, currentDate); + await TryRemove(raidParty, currentDate); + await TryExecuteEventsAfterSlowMo(raidParty, currentDate); + await TryRefreshInfo(raidParty); + await TrySpawnMonsterByRaidWave(raidParty, currentDate); + } + + private async Task TrySpawnMonsterByRaidWave(RaidParty raidParty, DateTime currentDate) + { + if (!raidParty.Started || raidParty.Finished || raidParty.Instance == null) + { + return; + } + + if (raidParty.Instance.RaidSubInstances == null) + { + return; + } + + foreach (RaidSubInstance raidSubInstance in raidParty.Instance.RaidSubInstances.Values) + { + if (raidSubInstance == null) + { + continue; + } + + if (!raidSubInstance.RaidWavesActivated) + { + continue; + } + + if (!raidSubInstance.RaidWaves.Any()) + { + continue; + } + + if (raidSubInstance.LastRaidWave > currentDate) + { + continue; + } + + RaidWave wave = raidSubInstance.RaidWaves.GetOrDefault(raidSubInstance.RaidWaveState); + if (wave == null) + { + raidSubInstance.RaidWaveState = 0; + raidSubInstance.LastRaidWave = currentDate; + continue; + } + + raidSubInstance.LastRaidWave = currentDate.AddSeconds(wave.TimeInSeconds); + raidSubInstance.RaidWaveState++; + + await _eventPipeline.ProcessEventAsync(new MonsterSummonEvent(raidSubInstance.MapInstance, wave.Monsters)); + raidSubInstance.MapInstance.Broadcast(x => + x.GenerateMsgPacket(x.GetLanguage(GameDialogKey.RAID_SHOUTMESSAGE_MONSTERS_WAVE), MsgMessageType.Middle)); + } + } + + private async Task TryFinish(RaidParty raidParty, DateTime currentTime) + { + if (!raidParty.Started || raidParty.Finished || raidParty.Instance.FinishDate > currentTime) + { + return; + } + + await _eventPipeline.ProcessEventAsync(new RaidInstanceFinishEvent(raidParty, RaidFinishType.TimeIsUp)); + } + + private async Task TryRemove(RaidParty raidParty, DateTime currentTime) + { + if (!raidParty.Started || !raidParty.Finished || raidParty.Instance.RemoveDate > currentTime) + { + return; + } + + Log.Warn($"REMOVING RAID MAP {raidParty.Instance.RemoveDate}"); + await _eventPipeline.ProcessEventAsync(new RaidInstanceDestroyEvent(raidParty)); + } + + private async Task TryExecuteEventsAfterSlowMo(RaidParty raidParty, DateTime currentTime) + { + if (!raidParty.Started || !raidParty.Finished || raidParty.Instance.FinishSlowMoDate == null || raidParty.Instance.FinishSlowMoDate > currentTime) + { + return; + } + + raidParty.Instance.SetFinishSlowMoDate(null); + foreach (RaidSubInstance subInstance in raidParty.Instance.RaidSubInstances.Values) + { + await subInstance.TriggerEvents(RaidConstEventKeys.RaidSubInstanceAfterSlowMo); + } + } + + private async Task TryRefreshInfo(RaidParty raidParty) + { + if (!raidParty.Started || raidParty.Destroy) + { + return; + } + + await _eventPipeline.ProcessEventAsync(new RaidInstanceRefreshInfoEvent(raidParty)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Raids/Scripting/Converter/SFinishRaidEventConverter.cs b/srcs/_plugins/Plugin.Raids/Scripting/Converter/SFinishRaidEventConverter.cs new file mode 100644 index 0000000..0c6ec8a --- /dev/null +++ b/srcs/_plugins/Plugin.Raids/Scripting/Converter/SFinishRaidEventConverter.cs @@ -0,0 +1,17 @@ +using PhoenixLib.Events; +using WingsAPI.Scripting.Converter; +using WingsAPI.Scripting.Event.Raid; +using WingsEmu.Game.Raids; +using WingsEmu.Game.Raids.Enum; +using WingsEmu.Game.Raids.Events; + +namespace Plugin.Raids.Scripting.Converter; + +public class SFinishRaidEventConverter : ScriptedEventConverter +{ + private readonly RaidParty raidParty; + + public SFinishRaidEventConverter(RaidParty raidParty) => this.raidParty = raidParty; + + protected override IAsyncEvent Convert(SFinishRaidEvent e) => new RaidInstanceFinishEvent(raidParty, (RaidFinishType)(byte)e.FinishType); +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Raids/Scripting/Converter/SMonsterSummonEventConverter.cs b/srcs/_plugins/Plugin.Raids/Scripting/Converter/SMonsterSummonEventConverter.cs new file mode 100644 index 0000000..0b2f2a9 --- /dev/null +++ b/srcs/_plugins/Plugin.Raids/Scripting/Converter/SMonsterSummonEventConverter.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Concurrent; +using System.Linq; +using PhoenixLib.Events; +using WingsAPI.Scripting.Converter; +using WingsAPI.Scripting.Event.Common; +using WingsAPI.Scripting.Object.Raid; +using WingsEmu.Game; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Monster; +using WingsEmu.Game.Monster.Event; + +namespace Plugin.Raids.Scripting.Converter; + +public class SMonsterSummonEventConverter : ScriptedEventConverter +{ + private readonly IMapInstance _mapInstance; + + public SMonsterSummonEventConverter(IMapInstance mapInstance) => _mapInstance = mapInstance; + + protected override IAsyncEvent Convert(SMonsterSummonEvent e) + { + return new MonsterSummonEvent(_mapInstance, e.Monsters.Select(x => + { + ConcurrentDictionary waypointsDictionary = null; + if (x.Waypoints != null) + { + byte waypoints = 0; + foreach (SWaypoint waypoint in x.Waypoints) + { + waypointsDictionary ??= new ConcurrentDictionary(); + + var newWaypoint = new Waypoint + { + X = waypoint.X, + Y = waypoint.Y, + WaitTime = waypoint.WaitTime + }; + + waypointsDictionary.TryAdd(waypoints, newWaypoint); + waypoints++; + } + } + + return new ToSummon + { + VNum = x.Vnum, + SpawnCell = x.IsRandomPosition ? null : new Position(x.Position.X, x.Position.Y), + IsMoving = x.CanMove, + IsTarget = x.IsTarget, + IsBossOrMate = x.IsBoss, + IsHostile = true, + AtAroundMobId = string.IsNullOrEmpty(x.AtAroundMobId) ? null : Guid.Parse(x.AtAroundMobId), + AtAroundMobRange = x.AtAroundMobRange, + Waypoints = waypointsDictionary, + Direction = x.Direction + }; + })); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Raids/Scripting/Converter/SOpenRaidPortalEventConverter.cs b/srcs/_plugins/Plugin.Raids/Scripting/Converter/SOpenRaidPortalEventConverter.cs new file mode 100644 index 0000000..4b38055 --- /dev/null +++ b/srcs/_plugins/Plugin.Raids/Scripting/Converter/SOpenRaidPortalEventConverter.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using PhoenixLib.Events; +using WingsAPI.Scripting.Converter; +using WingsAPI.Scripting.Event.Common; +using WingsEmu.Game; +using WingsEmu.Game.Raids; +using WingsEmu.Game.Raids.Events; + +namespace Plugin.Raids.Scripting.Converter; + +public class SOpenRaidPortalEventConverter : ScriptedEventConverter +{ + private readonly Dictionary portals; + private readonly RaidSubInstance raidSubInstance; + + public SOpenRaidPortalEventConverter(RaidSubInstance raidSubInstance, Dictionary portals) + { + this.raidSubInstance = raidSubInstance; + this.portals = portals; + } + + protected override IAsyncEvent Convert(SOpenPortalEvent e) => new RaidPortalOpenEvent(raidSubInstance, portals[e.Portal.Id]); +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Raids/Scripting/Converter/SRaidIncreaseObjectiveEventConverter.cs b/srcs/_plugins/Plugin.Raids/Scripting/Converter/SRaidIncreaseObjectiveEventConverter.cs new file mode 100644 index 0000000..2643ad5 --- /dev/null +++ b/srcs/_plugins/Plugin.Raids/Scripting/Converter/SRaidIncreaseObjectiveEventConverter.cs @@ -0,0 +1,16 @@ +using PhoenixLib.Events; +using WingsAPI.Scripting.Converter; +using WingsAPI.Scripting.Event.Raid; +using WingsEmu.Game.Raids; +using WingsEmu.Game.Raids.Events; + +namespace Plugin.Raids.Scripting.Converter; + +public class SRaidIncreaseObjectiveEventConverter : ScriptedEventConverter +{ + private readonly RaidSubInstance instance; + + public SRaidIncreaseObjectiveEventConverter(RaidSubInstance instance) => this.instance = instance; + + protected override IAsyncEvent Convert(SRaidIncreaseObjectiveEvent e) => new RaidObjectiveIncreaseEvent((RaidTargetType)(byte)e.ObjectiveType, instance); +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Raids/Scripting/Converter/STeleportMembersEventConverter.cs b/srcs/_plugins/Plugin.Raids/Scripting/Converter/STeleportMembersEventConverter.cs new file mode 100644 index 0000000..d84561d --- /dev/null +++ b/srcs/_plugins/Plugin.Raids/Scripting/Converter/STeleportMembersEventConverter.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using PhoenixLib.Events; +using WingsAPI.Scripting.Converter; +using WingsAPI.Scripting.Event.Common; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Raids; +using WingsEmu.Game.Raids.Events; + +namespace Plugin.Raids.Scripting.Converter; + +public class STeleportMembersEventConverter : ScriptedEventConverter +{ + private readonly Dictionary _raidSubInstances; + + public STeleportMembersEventConverter(Dictionary raidSubInstances) => _raidSubInstances = raidSubInstances; + + protected override IAsyncEvent Convert(STeleportMembersEvent e) + { + IMapInstance mapInstance = _raidSubInstances[e.MapInstanceId].MapInstance; + var sourcePosition = new Position(e.SourcePosition.X, e.SourcePosition.Y); + var destinationPosition = new Position(e.DestinationPosition.X, e.DestinationPosition.Y); + return new RaidTeleportMemberEvent(mapInstance, sourcePosition, destinationPosition, e.Range); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Raids/Scripting/RaidScriptManager.cs b/srcs/_plugins/Plugin.Raids/Scripting/RaidScriptManager.cs new file mode 100644 index 0000000..02d0677 --- /dev/null +++ b/srcs/_plugins/Plugin.Raids/Scripting/RaidScriptManager.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using FluentValidation.Results; +using MoonSharp.Interpreter; +using PhoenixLib.Logging; +using Plugin.Raids.Scripting.Validator.Raid; +using WingsAPI.Scripting; +using WingsAPI.Scripting.Enum.Raid; +using WingsAPI.Scripting.LUA; +using WingsAPI.Scripting.Object.Raid; +using WingsAPI.Scripting.ScriptManager; + +namespace Plugin.Raids.Scripting; + +public sealed class RaidScriptManager : IRaidScriptManager +{ + private readonly Dictionary _cache = new(); + private readonly IScriptFactory _scriptFactory; + private readonly ScriptFactoryConfiguration _scriptFactoryConfiguration; + private readonly SRaidValidator _validator; + + public RaidScriptManager(IScriptFactory scriptFactory, ScriptFactoryConfiguration scriptFactoryConfiguration, SRaidValidator validator) + { + _scriptFactory = scriptFactory; + _scriptFactoryConfiguration = scriptFactoryConfiguration; + _validator = validator; + } + + public SRaid GetScriptedRaid(SRaidType raidType) => _cache.GetValueOrDefault(raidType); + + public void Load() + { + IEnumerable files = Directory.GetFiles(_scriptFactoryConfiguration.RaidsDirectory, "*.lua"); + foreach (string file in files) + { + try + { + SRaid raid = _scriptFactory.LoadScript(file); + if (raid == null) + { + Log.Warn($"Failed to load raid script {file}"); + continue; + } + + ValidationResult result = _validator.Validate(raid); + if (!result.IsValid) + { + throw new InvalidScriptException(result.Errors.First().ErrorMessage); + } + + Log.Warn($"[RAID_SCRIPT_MANAGER] Loaded {Path.GetFileName(file)} for raid: {raid.RaidType}"); + _cache[raid.RaidType] = raid; + } + catch (InvalidScriptException e) + { + Log.Error($"[RAID_SCRIPT_MANAGER]InvalidScript: {file}", e); + } + catch (ScriptRuntimeException e) + { + Log.Error($"[RAID_SCRIPT_MANAGER][SCRIPT ERROR] {file}, {e.DecoratedMessage}", e); + } + catch (Exception e) + { + Log.Error($"[RAID_SCRIPT_MANAGER][SCRIPT ERROR] {file}", e); + } + } + + Log.Info($"Loaded {_cache.Count} raids from scripts"); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Raids/Scripting/Validator/Raid/SRaidRequirementValidator.cs b/srcs/_plugins/Plugin.Raids/Scripting/Validator/Raid/SRaidRequirementValidator.cs new file mode 100644 index 0000000..21acc2b --- /dev/null +++ b/srcs/_plugins/Plugin.Raids/Scripting/Validator/Raid/SRaidRequirementValidator.cs @@ -0,0 +1,8 @@ +using FluentValidation; +using WingsAPI.Scripting.Object.Raid; + +namespace Plugin.Raids.Scripting.Validator.Raid; + +public class SRaidRequirementValidator : AbstractValidator +{ +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Raids/Scripting/Validator/Raid/SRaidValidator.cs b/srcs/_plugins/Plugin.Raids/Scripting/Validator/Raid/SRaidValidator.cs new file mode 100644 index 0000000..fb37932 --- /dev/null +++ b/srcs/_plugins/Plugin.Raids/Scripting/Validator/Raid/SRaidValidator.cs @@ -0,0 +1,21 @@ +using FluentValidation; +using WingsAPI.Scripting.Object.Raid; +using WingsAPI.Scripting.Validator.Common; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Maps; + +namespace Plugin.Raids.Scripting.Validator.Raid; + +public class SRaidValidator : AbstractValidator +{ + public SRaidValidator(IMapManager mapManager, INpcMonsterManager npcManager, IItemsManager itemsManager) + { + RuleFor(x => x.Id).NotEmpty(); + RuleFor(x => x.Maps).NotEmpty(); + RuleFor(x => x.Requirement).SetValidator(new SRaidRequirementValidator()); + RuleFor(x => x.Spawn).SetValidator(new SLocationValidator()); + RuleFor(x => x.RaidType).IsInEnum(); + + RuleForEach(x => x.Maps).SetValidator(new SMapValidator(mapManager, npcManager, itemsManager)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.ResourceLoader/BattleEntityAlgorithmService.cs b/srcs/_plugins/Plugin.ResourceLoader/BattleEntityAlgorithmService.cs new file mode 100644 index 0000000..b3b4a94 --- /dev/null +++ b/srcs/_plugins/Plugin.ResourceLoader/BattleEntityAlgorithmService.cs @@ -0,0 +1,724 @@ +using System; +using WingsEmu.Game._enum; +using WingsEmu.Game.Algorithm; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Battle; +using WingsEmu.Packets.Enums.Character; + +namespace Plugin.ResourceLoader +{ + /* + * Coefficient of statistics for the class: + * Adventurer: 0, 0, 0 + * Swordsman: 8, 2, 0 + * Archer: 3, 6, 1 + * Mage: 0, 2, 8 + * Martial Artist: 5, 3, 2 + * + * Additional statistics: + * Adventurer: 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + * Swordsman: 0, 15, 0, 0, 0, 0, 0, 0, 0, 0 + * Archer: 0, 15, 50, 15, 0, 0, 10, 0, 10, 0 + * Magician: 0, 0, 0, 0, 0, 0, 10, 20, 0, 0 + * Martial Artist: 0, 0, 20, 0, 0, 0, 20, 10, 10, 0 + */ + public class BattleEntityAlgorithmService : IBattleEntityAlgorithmService + { + private static readonly double[] DefenseRace0 = { 16, 13.5, 11, 50, 50, 50 }; + private static readonly double[] DefenseRace1 = { 20, 17, 19, 100, 100, 100 }; + private static readonly double[] DefenseRace2 = { 15, 15, 15, 75, 50, 40 }; + private static readonly double[] DefenseRace3 = { 15, 15, 15, 50, 50, 50 }; + private static readonly double[] DefenseRace4 = { 17.4, 17.4, 17.4, 60, 60, 100 }; + private static readonly double[] DefenseRace5 = { 13.4, 13.4, 13.4, 40, 40, 40 }; + private static readonly double[] DefenseRace6 = { 11.5, 15, 25, 50, 50, 75 }; + private static readonly double[] DefenseRace8 = { 10, 10, 10, 100, 100, 100 }; + private static readonly double[] DefaultDefense = { 0, 0, 0, 0, 0, 0 }; + + private static readonly int[,] Statistics = + { + { 0, 0, 0 }, + { 8, 2, 0 }, + { 3, 6, 1 }, + { 0, 2, 8 }, + { 5, 3, 2 } + }; + + private static readonly int[,] AdditionalStatistics = + { + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, + { 0, 15, 0, 0, 0, 0, 0, 0, 0, 0 }, + { 0, 15, 50, 15, 0, 0, 10, 0, 10, 0 }, + { 0, 0, 0, 0, 0, 0, 10, 20, 0, 0 }, + { 0, 0, 20, 0, 0, 0, 20, 10, 10, 0 } + }; + + public int GetBasicHp(int race, int level, int modifier, int additionalHp = 0, bool isMonster = true) + { + double hp = 0; + int a = 0; + int b = 0; + int c = 0; + + if (isMonster) + { + modifier = 0; + } + + switch (race) + { + case 0: + a = 0; + b = 2; + c = 138; + break; + case 1: + a = 10; + b = 10; + c = 610; + break; + case 2: + a = 5; + b = 0; + c = 105; + break; + case 3: + a = 0; + b = 0; + c = 205; + break; + case 4: + a = 2; + b = 5; + c = 695; + break; + case 5: + a = -2; + b = -3; + c = 263; + break; + case 6: + a = 0; + b = -7; + c = 21; + break; + default: + a = 0; + b = 0; + c = 0; + break; + } + + if (race == 8) + { + hp = 7; + } + else + { + int x = level; + if ((modifier + a) != 0) + { + x += (int)Math.Floor((level - 1) / (decimal)(10.0 / (modifier + a))); + } + + hp = 0.5 * (x * x) + (15.5 + b) * x + c; + } + + if (!isMonster) + { + return (int)Math.Floor(hp + additionalHp); + } + + switch (level) + { + case >= 37 and <= 51: + hp *= 1.2; + break; + case >= 52 and <= 61: + hp *= 1.5; + break; + case >= 62 and <= 71: + hp *= 1.8; + break; + case >= 72 and <= 81: + hp *= 2.5; + break; + case >= 81: + hp *= 3.5; + break; + } + + return (int)Math.Floor(hp + additionalHp); + } + + public int GetBaseStatistic(int level, ClassType classType, StatisticType statisticType) + { + switch (statisticType) + { + case StatisticType.ATTACK_MELEE: + return (int)(level + 9 + Math.Floor((decimal)((level - 1) * Statistics[(int)classType, 0] / 10.0))) + AdditionalStatistics[(int)classType, 0]; + case StatisticType.ATTACK_RANGED: + return (int)(level + 9 + Math.Floor((decimal)((level - 1) * Statistics[(int)classType, 1] / 10.0))) + AdditionalStatistics[(int)classType, 2]; + case StatisticType.ATTACK_MAGIC: + return (int)(level + 9 + Math.Floor((decimal)((level - 1) * Statistics[(int)classType, 2] / 10.0))) + AdditionalStatistics[(int)classType, 4]; + case StatisticType.HITRATE_MELEE: + return (int)(level + 9 + Math.Floor((decimal)((level - 1) * Statistics[(int)classType, 1] / 10.0))) + AdditionalStatistics[(int)classType, 1]; + case StatisticType.HITRATE_RANGED: + return (int)((level + 9 + Math.Floor((decimal)((level - 1) * Statistics[(int)classType, 1] / 10.0))) * 2) + AdditionalStatistics[(int)classType, 3]; + case StatisticType.DEFENSE_MELEE: + return (int)((level + 9 + Math.Floor((decimal)((level - 1) * Statistics[(int)classType, 0] / 10.0))) * (decimal)0.5) + AdditionalStatistics[(int)classType, 5]; + case StatisticType.DEFENSE_RANGED: + return (int)((level + 9 + Math.Floor((decimal)((level - 1) * Statistics[(int)classType, 1] / 10.0))) * (decimal)0.5) + AdditionalStatistics[(int)classType, 7]; + case StatisticType.DEFENSE_MAGIC: + return (int)((level + 9 + Math.Floor((decimal)((level - 1) * Statistics[(int)classType, 2] / 10.0))) * (decimal)0.5) + AdditionalStatistics[(int)classType, 9]; + case StatisticType.DODGE_MELEE: + return (int)(level + 9 + Math.Floor((decimal)((level - 1) * Statistics[(int)classType, 1] / 10.0))) + AdditionalStatistics[(int)classType, 6]; + case StatisticType.DODGE_RANGED: + return (int)(level + 9 + Math.Floor((decimal)((level - 1) * Statistics[(int)classType, 1] / 10.0))) + AdditionalStatistics[(int)classType, 8]; + } + + return 0; + } + + public int GetBasicMp(int race, int level, int modifier, int additionalMp = 0, bool isMonster = true) + { + double mp = 0; + int z = 0; + double d = 0; + int a = 0; + int g = 0; + int x = 0; + + if (isMonster) + { + modifier = 0; + } + + switch (race) + { + case 0: + d = 4.75; + a = 0; + g = 0; + break; + case 1: + d = 178.75; + a = 10; + g = 8; + break; + case 2: + d = 50.75; + a = -2; + g = 4; + break; + case 3: + d = 50.75; + a = 0; + g = 4; + break; + case 4: + d = 385.75; + a = 10; + g = 6; + z = 1; + break; + case 5: + d = 23.75; + a = -2; + g = 2; + z = 1; + break; + case 6: + d = 705.75; + a = 5; + g = 14; + break; + case 8: + return (int)Math.Floor(4 + (double)additionalMp); + default: + d = 0; + a = 0; + g = 0; + break; + } + + x = level; + if ((modifier + a) != 0) + { + x += (int)Math.Floor((level - 1) / (decimal)(10.0 / (modifier + a)) + z); + } + + mp = Math.Floor((5.25 + g) * x + d) + (Math.Floor((double)(x - 6) / 4) + 1) * 2 * (Mod(x - 2, 4) + 1 + Math.Floor(((double)x - 6) / 4) * 2); + + return (int)Math.Floor(mp + additionalMp); + } + + public int GetBasicHpByClass(ClassType classType, int level) + { + int hp = classType switch + { + ClassType.Adventurer => GetBasicHp(3, level, 0, 0, false), + ClassType.Swordman => GetBasicHp(3, level, 8, 0, false), + ClassType.Archer => GetBasicHp(3, level, 3, 0, false), + ClassType.Magician => GetBasicHp(3, level, 0, 0, false), + ClassType.Wrestler => GetBasicHp(3, level, 5, 0, false) + }; + + return hp; + } + + public int GetBasicMpByClass(ClassType classType, int level) + { + int mp = classType switch + { + ClassType.Adventurer => GetBasicMp(3, level, 0, 0, false), + ClassType.Swordman => GetBasicMp(3, level, 0, 0, false), + ClassType.Archer => GetBasicMp(3, level, 1, 0, false), + ClassType.Magician => GetBasicMp(3, level, 8, 0, false), + ClassType.Wrestler => GetBasicMp(3, level, 2, 0, false) + }; + + return mp; + } + + public int GetAttack(bool isMin, int race, AttackType attackType, short weaponLevel, byte wInfo, short level, int modifier, int additional, bool isWild = true, short petLevel = 0, + MateType mateType = MateType.Pet) + { + int calcLevel; + int weaponMod; + int a; + int b; + int c; + + if (isWild) + { + modifier = 0; + calcLevel = level; + weaponMod = weaponLevel; + } + else + { + calcLevel = petLevel; + weaponMod = petLevel + level - weaponLevel; + } + + switch (attackType) + { + case AttackType.Melee: + switch (race) + { + case 0: + a = 35; + b = 0; + break; + case 1: + a = 43; + b = 10; + break; + case 2: + a = 33; + b = 5; + break; + case 3: + a = 33; + b = 0; + break; + case 4: + a = 38; + b = 2; + break; + case 5: + a = 30; + b = -2; + break; + case 6: + a = 26; + b = 0; + break; + case 8: + a = 23; + b = 0; + break; + default: + a = 0; + b = 0; + break; + } + + break; + case AttackType.Ranged: + switch (race) + { + case 0: + a = 30; + b = 0; + break; + case 1: + a = 38; + b = 10; + break; + case 2: + a = 33; + b = 0; + break; + case 3: + a = 33; + b = 0; + break; + case 4: + a = 38; + b = 2; + break; + case 5: + a = 30; + b = -2; + break; + case 6: + a = 33; + b = 0; + break; + case 8: + a = 23; + b = 0; + break; + default: + a = 0; + b = 0; + break; + } + + break; + case AttackType.Magical: + switch (race) + { + case 0: + a = 25; + b = 0; + break; + case 1: + a = 41; + b = 10; + break; + case 2: + a = 33; + b = -2; + break; + case 3: + a = 33; + b = 0; + break; + case 4: + a = 38; + b = 10; + break; + case 5: + a = 30; + b = -2; + break; + case 6: + a = 53; + b = 5; + break; + case 8: + a = 23; + b = 0; + break; + default: + a = 0; + b = 0; + break; + } + + break; + default: + a = 0; + b = 0; + break; + } + + if (wInfo > 1) + { + c = (int)Math.Floor((decimal)((calcLevel + 2.0) / (wInfo - 1))) + 1; + } + else + { + c = 0; + } + + if (!isWild && mateType == MateType.Partner) + { + return calcLevel + 9 + (int)Math.Floor((calcLevel - 1) * (modifier / 10.0)); + } + + if (isMin) + { + return (int)Math.Floor(calcLevel + (a - 7.2) + 3.2 * weaponMod + Math.Floor((calcLevel - 1) * ((modifier + b) / 10.0)) + additional + c); + } + + return (int)Math.Floor(calcLevel + a + 4.8 * weaponMod + Math.Floor((calcLevel - 1) * ((modifier + b) / 10.0)) + additional - c); + } + + public int GetHitrate(int race, AttackType attackType, short weaponLevel, short level, int modifier, int additional, bool isWild = true, short petLevel = 0, MateType mateType = MateType.Pet) + { + int calcLevel; + int weaponMod; + int a; + int b; + + if (isWild) + { + modifier = 0; + calcLevel = level; + weaponMod = weaponLevel; + } + else + { + calcLevel = petLevel; + weaponMod = petLevel + level - weaponLevel; + } + + if (!isWild && mateType == MateType.Partner) + { + switch (attackType) + { + case AttackType.Melee: + return calcLevel + 9 + (int)Math.Floor((calcLevel - 1) * (modifier / 10.0)); + case AttackType.Ranged: + return 2 * (calcLevel + 9 + (int)Math.Floor((calcLevel - 1) * (modifier / 10.0))); + default: + return 0; + } + } + + switch (attackType) + { + case AttackType.Melee: + switch (race) + { + case 0: + a = 22; + b = 0; + break; + case 1: + a = 30; + b = 10; + break; + case 2: + a = 25; + b = 0; + break; + case 3: + a = 25; + b = 0; + break; + case 4: + a = 30; + b = 2; + break; + case 5: + a = 22; + b = -2; + break; + case 6: + a = 25; + b = 0; + break; + case 8: + a = 15; + b = 0; + break; + default: + a = 0; + b = 0; + break; + } + + return (int)Math.Floor(calcLevel + 4 * weaponMod + a + Math.Floor((calcLevel - 1) * ((modifier + b) / 10.0)) + additional); + + case AttackType.Ranged: + switch (race) + { + case 0: + a = 28; + b = 0; + break; + case 1: + a = 44; + b = 10; + break; + case 2: + a = 34; + b = 0; + break; + case 3: + a = 34; + b = 0; + break; + case 4: + a = 44; + b = 2; + break; + case 5: + a = 28; + b = -2; + break; + case 6: + a = 34; + b = 0; + break; + case 8: + a = 15; + b = 0; + break; + default: + a = 0; + b = 0; + break; + } + + return (int)Math.Floor(2 * calcLevel + 4 * weaponMod + a + Math.Floor((calcLevel - 1) * ((modifier + b) / 10.0)) * 2 + additional); + case AttackType.Magical: + return 70 + additional; + default: + return 0; + } + } + + public int GetDodge(int race, short armorLevel, short level, int modifier, int additional, bool isWild = true, short petLevel = 0, MateType mateType = MateType.Partner) + { + int calcLevel; + int armorMod; + int a; + int b; + + if (isWild) + { + modifier = 0; + calcLevel = level; + armorMod = armorLevel; + } + else + { + calcLevel = petLevel; + armorMod = petLevel + level - armorLevel; + } + + if (!isWild && mateType == MateType.Partner) + { + return calcLevel + 9 + (int)Math.Floor((calcLevel - 1) * (modifier / 10.0)); + } + + switch (race) + { + case 0: + a = 26; + b = 0; + break; + case 1: + a = 34; + b = 10; + break; + case 2: + a = 29; + b = 0; + break; + case 3: + a = 29; + b = 0; + break; + case 4: + a = 34; + b = 2; + break; + case 5: + a = 26; + b = -2; + break; + case 6: + a = 29; + b = 0; + break; + case 8: + a = 19; + b = 0; + break; + default: + a = 0; + b = 0; + break; + } + + return (int)Math.Floor(calcLevel + 4 * armorMod + a + Math.Floor((calcLevel - 1) * ((modifier + b) / 10.0)) + additional); + } + + public int GetDefense(int race, AttackType attackType, short armorLevel, short level, int modifier, int additional, bool isWild = true, short petLevel = 0, MateType mateType = MateType.Pet) + { + int calcLevel; + int armorMod; + + if (isWild) + { + modifier = 0; + calcLevel = level; + armorMod = armorLevel; + } + else + { + calcLevel = petLevel; + armorMod = petLevel + level - armorLevel; + } + + if (!isWild && mateType == MateType.Partner) + { + return (int)(0.5 * (calcLevel + 9 + (int)Math.Floor((calcLevel - 1) * (modifier / 10.0)))); + } + + double[] raceInfo = race switch + { + 0 => DefenseRace0, + 1 => DefenseRace1, + 2 => DefenseRace2, + 3 => DefenseRace3, + 4 => DefenseRace4, + 5 => DefenseRace5, + 6 => DefenseRace6, + 8 => DefenseRace8, + _ => DefaultDefense + }; + + return attackType switch + { + AttackType.Melee => (int)Math.Floor(2 * armorMod + raceInfo[0] + Math.Floor((armorMod + 5) * 0.08) + (calcLevel - 1) * ((modifier * 10 + (raceInfo[3] - 5 * modifier)) / 100.0) + + additional), + AttackType.Ranged => (int)Math.Floor(2 * armorMod + raceInfo[1] + Math.Floor((armorMod + 5) * 0.36) + (calcLevel - 1) * ((modifier * 10 + (raceInfo[4] - 5 * modifier)) / 100.0) + + additional), + AttackType.Magical => (int)Math.Floor(2 * armorMod + raceInfo[2] + Math.Floor((armorMod + 5) * 0.04) + (calcLevel - 1) * ((modifier * 10 + (raceInfo[5] - 5 * modifier)) / 100.0) + + additional) + }; + } + + public byte GetSpeed(ClassType classType) + { + byte speed = classType switch + { + ClassType.Adventurer => 11, + ClassType.Swordman => 11, + ClassType.Archer => 12, + ClassType.Magician => 10, + ClassType.Wrestler => 11, + _ => 0 + }; + + return speed; + } + + private static double Mod(int a, int modulo) + { + if (a >= 0) + { + return a % modulo; + } + + return Math.Abs((a - 2) % modulo); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.ResourceLoader/FileResourceLoaderPlugin.cs b/srcs/_plugins/Plugin.ResourceLoader/FileResourceLoaderPlugin.cs new file mode 100644 index 0000000..2fa577f --- /dev/null +++ b/srcs/_plugins/Plugin.ResourceLoader/FileResourceLoaderPlugin.cs @@ -0,0 +1,56 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using Microsoft.Extensions.DependencyInjection; +using Plugin.ResourceLoader.Loaders; +using WingsAPI.Data.ActDesc; +using WingsAPI.Data.GameData; +using WingsAPI.Plugins; +using WingsEmu.DTOs.Buffs; +using WingsEmu.DTOs.Items; +using WingsEmu.DTOs.Maps; +using WingsEmu.DTOs.NpcMonster; +using WingsEmu.DTOs.Quests; +using WingsEmu.DTOs.Skills; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Algorithm; + +namespace Plugin.ResourceLoader +{ + public class FileResourceLoaderPlugin : IDependencyInjectorPlugin + { + public string Name => nameof(FileResourceLoaderPlugin); + + public void AddDependencies(IServiceCollection services) + { + services.AddSingleton(s => new ResourceLoadingConfiguration(Environment.GetEnvironmentVariable("WINGSEMU_RESOURCE_PATH") ?? "resources")); + services.AddSingleton, ItemResourceFileLoader>(); + services.AddSingleton, SkillResourceFileLoader>(); + services.AddSingleton, NpcMonsterFileLoader>(); + services.AddSingleton, CardResourceFileLoader>(); + services.AddSingleton, MapResourceFileLoader>(); + services.AddSingleton, QuestResourceFileLoader>(); + services.AddSingleton, TutorialResourceFileLoader>(); + services.AddSingleton, NpcQuestResourceFileLoader>(); + services.AddSingleton, ActDescResourceFileLoader>(); + services.AddSingleton, GameDataLanguageFileLoader>(); + + services.AddSingleton(); + + services.AddSingleton(); + } + } + + public class GameResourceLoaderPlugin : IGameServerPlugin + { + public string Name => nameof(FileResourceLoaderPlugin); + + public void AddDependencies(IServiceCollection services, GameServerLoader gameServer) + { + services.AddSingleton, GenericTranslationGrpcLoader>(); + services.AddSingleton(); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.ResourceLoader/InMemoryGameDataLanguageService.cs b/srcs/_plugins/Plugin.ResourceLoader/InMemoryGameDataLanguageService.cs new file mode 100644 index 0000000..d3bade7 --- /dev/null +++ b/srcs/_plugins/Plugin.ResourceLoader/InMemoryGameDataLanguageService.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Caching; +using PhoenixLib.Logging; +using PhoenixLib.MultiLanguage; +using WingsAPI.Data.GameData; +using WingsEmu.Game._i18n; + +namespace Plugin.ResourceLoader +{ + public class InMemoryGameDataLanguageService : IGameDataLanguageService + { + private static readonly string _dataPrefixGameData = "language:game-data:"; + private readonly IKeyValueCache _cacheClient; + private readonly IResourceLoader _dataTranslationsLoader; + + private Dictionary<(GameDataType DataType, RegionLanguageType Language), Dictionary> _gameDataLanguages = new(); + + public InMemoryGameDataLanguageService(IKeyValueCache cacheClient, IResourceLoader dataTranslationsLoader) + { + _cacheClient = cacheClient; + _dataTranslationsLoader = dataTranslationsLoader; + } + + + public string GetLanguage(GameDataType dataType, string dataName, RegionLanguageType lang) + { + return _cacheClient.GetOrSet(ToKey(dataType, dataName, lang), () => dataName); + } + + public Dictionary GetDataTranslations(GameDataType dataType, RegionLanguageType lang) => _gameDataLanguages[(dataType, lang)]; + + public async Task Reload(bool isFullReload = false) + { + Log.Info("[MULTILANGUAGE] Loading..."); + IReadOnlyList gameDataTranslations = await _dataTranslationsLoader.LoadAsync(); + foreach (GameDataTranslationDto tmp in gameDataTranslations) + { + _cacheClient.Set(ToKey(tmp.DataType, tmp.Key, tmp.Language), tmp.Value); + } + + var newDictionary = gameDataTranslations.GroupBy(s => (s.DataType, s.Language)).ToDictionary(s => s.Key, s => s.ToDictionary(p => p.Key, p => p.Value)); + + Interlocked.Exchange(ref _gameDataLanguages, newDictionary); + + Log.Info($"[MULTILANGUAGE] loaded {gameDataTranslations.Count.ToString()} game data translations"); + } + + private static string LangSuffix(RegionLanguageType lang) => lang.ToString().ToLower(); + private static string GameDataSuffix(GameDataType dataType) => dataType.ToString().ToLower(); + private static string ToKey(GameDataType dataType, string dataName, RegionLanguageType lang) => _dataPrefixGameData + LangSuffix(lang) + ':' + GameDataSuffix(dataType) + ':' + dataName; + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.ResourceLoader/InMemoryMultilanguageService.cs b/srcs/_plugins/Plugin.ResourceLoader/InMemoryMultilanguageService.cs new file mode 100644 index 0000000..b4a2726 --- /dev/null +++ b/srcs/_plugins/Plugin.ResourceLoader/InMemoryMultilanguageService.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using PhoenixLib.Caching; +using PhoenixLib.Logging; +using PhoenixLib.MultiLanguage; +using WingsAPI.Data.GameData; +using WingsEmu.Game._i18n; + +namespace Plugin.ResourceLoader +{ + public class InMemoryMultilanguageService : IGameLanguageService + { + private static readonly string _dataPrefixString = "language:generic:"; + + private readonly IKeyValueCache _cacheClient; + private readonly IGameDataLanguageService _gameDataLanguageService; + private readonly IResourceLoader _genericTranslationsLoader; + + public InMemoryMultilanguageService(IKeyValueCache cacheClient, IResourceLoader genericTranslationsLoader, IGameDataLanguageService gameDataLanguageService) + { + _cacheClient = cacheClient; + _genericTranslationsLoader = genericTranslationsLoader; + _gameDataLanguageService = gameDataLanguageService; + } + + public string GetLanguage(string key, RegionLanguageType lang) => _cacheClient.GetOrSet(ToKey(key, lang), key.ToUpperInvariant); + + public string GetLanguageFormat(string key, RegionLanguageType lang, params object[] formatParams) + { + string keyString = _cacheClient.GetOrSet(ToKey(key, lang), key.ToUpperInvariant); + try + { + return string.Format(keyString, formatParams); + } + catch (Exception e) + { + Log.Error($"{key} formatting issue in language {lang}", e); + return keyString; + } + } + + public string GetLanguage(T key, RegionLanguageType lang) where T : Enum + { + string tmp = _cacheClient.Get(ToKey(key, lang)); + return tmp ?? key.ToString().ToUpperInvariant(); + } + + public string GetLanguageFormat(T key, RegionLanguageType lang, params object[] formatParams) where T : Enum + { + string toFormat = GetLanguage(key, lang); + try + { + return string.Format(toFormat, formatParams); + } + catch (Exception e) + { + Log.Error($"{key} formatting issue in language {lang}", e); + return toFormat; + } + } + + public string GetLanguage(GameDataType dataType, string dataName, RegionLanguageType lang) => _gameDataLanguageService.GetLanguage(dataType, dataName, lang); + + public Dictionary GetDataTranslations(GameDataType dataType, RegionLanguageType lang) => _gameDataLanguageService.GetDataTranslations(dataType, lang); + + public async Task Reload(bool isFullReload = false) + { + if (isFullReload) + { + await _gameDataLanguageService.Reload(); + } + + Log.Info("[MULTILANGUAGE] Loading..."); + IReadOnlyList gameDialogTranslations = await _genericTranslationsLoader.LoadAsync(); + foreach (GenericTranslationDto tmp in gameDialogTranslations) + { + _cacheClient.Set(ToKey(tmp.Key, tmp.Language), tmp.Value); + } + + Log.Info($"[MULTILANGUAGE] loaded {gameDialogTranslations.Count.ToString()} generic translations"); + } + + private static string LangSuffix(RegionLanguageType lang) => lang.ToString().ToLower(); + private static string ToKey(string id, RegionLanguageType lang) => _dataPrefixString + LangSuffix(lang) + ':' + id.ToUpperInvariant(); + private static string ToKey(T id, RegionLanguageType lang) where T : Enum => _dataPrefixString + LangSuffix(lang) + ':' + id.ToString().ToUpperInvariant(); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.ResourceLoader/Loaders/ActDescResourceFileLoader.cs b/srcs/_plugins/Plugin.ResourceLoader/Loaders/ActDescResourceFileLoader.cs new file mode 100644 index 0000000..5c415ad --- /dev/null +++ b/srcs/_plugins/Plugin.ResourceLoader/Loaders/ActDescResourceFileLoader.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using PhoenixLib.Logging; +using WingsAPI.Data.ActDesc; +using WingsAPI.Data.GameData; + +namespace Plugin.ResourceLoader.Loaders +{ + public class ActDescResourceFileLoader : IResourceLoader + { + private readonly ResourceLoadingConfiguration _configuration; + + public ActDescResourceFileLoader(ResourceLoadingConfiguration configuration) => _configuration = configuration; + + public async Task> LoadAsync() + { + string filePath = Path.Combine(_configuration.GameDataPath, "act_desc.dat"); + + if (!File.Exists(filePath)) + { + throw new FileNotFoundException($"{filePath} should be present"); + } + + var actDescDatas = new List(); + var actNames = new Dictionary(); + + using var actDescStream = new StreamReader(filePath, Encoding.GetEncoding(1252)); + string line; + char[] splits = { ' ', '\t' }; + + while ((line = actDescStream.ReadLine()) != null) + { + string[] currentLine = line.Split(splits, StringSplitOptions.RemoveEmptyEntries); + + if (currentLine[0] != "Data" && currentLine[0] != "A") + { + continue; + } + + if (currentLine[0] == "Data") + { + var dto = new ActDescDTO + { + Act = byte.Parse(currentLine[2]), + SubAct = byte.Parse(currentLine[3]), + TsAmount = byte.Parse(currentLine[4]) + }; + + actDescDatas.Add(dto); + } + + if (currentLine[0] == "A") + { + actNames.TryAdd(byte.Parse(currentLine[1]), currentLine[2]); + } + } + + foreach (ActDescDTO actDescDto in actDescDatas) + { + actDescDto.ActName = actNames.GetValueOrDefault(actDescDto.Act); + } + + Log.Info($"[RESOURCE_LOADER] {actDescDatas.Count.ToString()} act desc loaded"); + return actDescDatas; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.ResourceLoader/Loaders/CardResourceFileLoader.cs b/srcs/_plugins/Plugin.ResourceLoader/Loaders/CardResourceFileLoader.cs new file mode 100644 index 0000000..19ff232 --- /dev/null +++ b/srcs/_plugins/Plugin.ResourceLoader/Loaders/CardResourceFileLoader.cs @@ -0,0 +1,203 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using PhoenixLib.Logging; +using WingsAPI.Data.GameData; +using WingsEmu.DTOs.BCards; +using WingsEmu.DTOs.Buffs; +using WingsEmu.Game._enum; +using WingsEmu.Packets.Enums; + +namespace Plugin.ResourceLoader.Loaders +{ + public class CardResourceFileLoader : IResourceLoader + { + private readonly ResourceLoadingConfiguration _config; + + public CardResourceFileLoader(ResourceLoadingConfiguration config) => _config = config; + + public async Task> LoadAsync() + { + string filePath = Path.Combine(_config.GameDataPath, "Card.dat"); + + if (!File.Exists(filePath)) + { + throw new FileNotFoundException($"{filePath} should be present"); + } + + + var card = new CardDTO(); + var cards = new List(); + + int counter = 0; + bool itemAreaBegin = false; + + using var npcIdStream = new StreamReader(filePath, Encoding.GetEncoding(1252)); + string line; + while ((line = await npcIdStream.ReadLineAsync()) != null) + { + string[] currentLine = line.Split('\t'); + + switch (currentLine.Length) + { + case > 2 when currentLine[1] == "VNUM": + card = new CardDTO + { + Id = Convert.ToInt16(currentLine[2]) + }; + itemAreaBegin = true; + break; + case > 2 when currentLine[1] == "NAME": + card.Name = currentLine[2]; + break; + case > 3 when currentLine[1] == "GROUP": + { + if (!itemAreaBegin) + { + continue; + } + + card.GroupId = Convert.ToInt32(currentLine[2]); + card.Level = Convert.ToByte(currentLine[3]); + break; + } + case > 3 when currentLine[1] == "STYLE": + card.BuffCategory = (BuffCategory)byte.Parse(currentLine[2]); + card.BuffType = Convert.ToByte(currentLine[3]); + card.ElementType = Convert.ToByte(currentLine[4]); + card.IsConstEffect = currentLine[5] == "1"; + card.BuffPartnerLevel = Convert.ToByte(currentLine[6]); + break; + case > 3 when currentLine[1] == "EFFECT": + card.EffectId = Convert.ToInt32(currentLine[2]); + break; + case > 3 when currentLine[1] == "TIME": + card.Duration = Convert.ToInt32(currentLine[2]); + card.SecondBCardsDelay = Convert.ToInt32(currentLine[3]); + break; + default: + { + BCardDTO bCard; + switch (currentLine.Length) + { + case > 3 when currentLine[1] == "1ST": + { + for (int i = 0; i < 3; i++) + { + if (currentLine[2 + i * 6] == "-1" || currentLine[2 + i * 6] == "0") + { + continue; + } + + int first = int.Parse(currentLine[6 + i * 6]); + int second = int.Parse(currentLine[7 + i * 6]); + + int firstModulo = first % 4; + firstModulo = firstModulo switch + { + -1 => 1, + -2 => 2, + -3 => 1, + _ => firstModulo + }; + + int secondModulo = second % 4; + secondModulo = secondModulo switch + { + -1 => 1, + -2 => 2, + -3 => 1, + _ => secondModulo + }; + + byte tickPeriod = byte.Parse(currentLine[5 + i * 6]); + bCard = new BCardDTO + { + CardId = card.Id, + Type = byte.Parse(currentLine[2 + i * 6]), + SubType = (byte)((Convert.ToByte(currentLine[3 + i * 6]) + 1) * 10 + 1 + (first < 0 ? 1 : 0)), + FirstData = (int)Math.Abs(Math.Floor(first / 4.0)), + SecondData = (int)Math.Abs(Math.Floor(second / 4.0)), + ProcChance = int.Parse(currentLine[4 + i * 6]), + TickPeriod = tickPeriod == 0 ? null : (byte?)(tickPeriod * 2), + FirstDataScalingType = (BCardScalingType)firstModulo, + SecondDataScalingType = (BCardScalingType)secondModulo, + IsSecondBCardExecution = false + }; + + card.Bcards.Add(bCard); + } + + break; + } + case > 3 when currentLine[1] == "2ST": + { + for (int i = 0; i < 2; i++) + { + if (currentLine[2 + i * 6] == "-1" || currentLine[2 + i * 6] == "0") + { + continue; + } + + int first = int.Parse(currentLine[6 + i * 6]); + int second = int.Parse(currentLine[7 + i * 6]); + + int firstModulo = first % 4; + firstModulo = firstModulo switch + { + -1 => 1, + -2 => 2, + -3 => 1, + _ => firstModulo + }; + + int secondModulo = second % 4; + secondModulo = secondModulo switch + { + -1 => 1, + -2 => 2, + -3 => 1, + _ => secondModulo + }; + + byte tickPeriod = byte.Parse(currentLine[5 + i * 6]); + bCard = new BCardDTO + { + CardId = card.Id, + Type = byte.Parse(currentLine[2 + i * 6]), + SubType = (byte)((Convert.ToByte(currentLine[3 + i * 6]) + 1) * 10 + 1 + (first < 0 ? 1 : 0)), + FirstData = (int)Math.Abs(Math.Floor(first / 4.0)), + SecondData = (int)Math.Abs(Math.Floor(second / 4.0)), + ProcChance = int.Parse(currentLine[4 + i * 6]), + TickPeriod = tickPeriod == 0 ? null : (byte?)(tickPeriod * 2), + FirstDataScalingType = (BCardScalingType)firstModulo, + SecondDataScalingType = (BCardScalingType)secondModulo, + IsSecondBCardExecution = true + }; + + card.Bcards.Add(bCard); + } + + break; + } + case > 3 when currentLine[1] == "LAST": + card.TimeoutBuff = short.Parse(currentLine[2]); + card.TimeoutBuffChance = byte.Parse(currentLine[3]); + itemAreaBegin = false; + cards.Add(card); + counter++; + break; + } + + break; + } + } + } + + Log.Info($"[RESOURCE_LOADER] {cards.Count.ToString()} act desc loaded"); + return cards; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.ResourceLoader/Loaders/GameDataLanguageFileLoader.cs b/srcs/_plugins/Plugin.ResourceLoader/Loaders/GameDataLanguageFileLoader.cs new file mode 100644 index 0000000..a63d65e --- /dev/null +++ b/srcs/_plugins/Plugin.ResourceLoader/Loaders/GameDataLanguageFileLoader.cs @@ -0,0 +1,157 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using PhoenixLib.Logging; +using PhoenixLib.MultiLanguage; +using WingsAPI.Data.GameData; +using WingsEmu.DTOs.Buffs; +using WingsEmu.DTOs.Items; +using WingsEmu.DTOs.NpcMonster; +using WingsEmu.DTOs.Quests; +using WingsEmu.DTOs.Skills; +using WingsEmu.Game._i18n; + +namespace Plugin.ResourceLoader.Loaders +{ + public class GameDataLanguageFileLoader : IResourceLoader + { + private static readonly List<(string, GameDataType)> _fileNames = new() + { + new ValueTuple("_code_{0}_Card.txt", GameDataType.Card), + new ValueTuple("_code_{0}_monster.txt", GameDataType.NpcMonster), + new ValueTuple("_code_{0}_Item.txt", GameDataType.Item), + new ValueTuple("_code_{0}_quest.txt", GameDataType.QuestName), + new ValueTuple("_code_{0}_Skill.txt", GameDataType.Skill) + }; + + private readonly IResourceLoader _cardLoader; + + private readonly ResourceLoadingConfiguration _config; + private readonly IResourceLoader _itemLoader; + private readonly IResourceLoader _npcLoader; + private readonly IResourceLoader _questLoader; + private readonly IResourceLoader _skillLoader; + + public GameDataLanguageFileLoader(ResourceLoadingConfiguration config, IResourceLoader itemLoader, IResourceLoader cardLoader, IResourceLoader skillLoader, + IResourceLoader questLoader, IResourceLoader npcLoader) + { + _config = config; + _itemLoader = itemLoader; + _cardLoader = cardLoader; + _skillLoader = skillLoader; + _questLoader = questLoader; + _npcLoader = npcLoader; + } + + public async Task> LoadAsync() + { + var translations = new List(); + + foreach ((string fileName, GameDataType dataType) in _fileNames) + { + translations.AddRange(await LoadAsync(fileName, dataType)); + } + + return translations; + } + + private static string ToNostaleRegionKey(RegionLanguageType type) + { + switch (type) + { + case RegionLanguageType.FR: + return "fr"; + case RegionLanguageType.EN: + return "uk"; + case RegionLanguageType.DE: + return "de"; + case RegionLanguageType.PL: + return "pl"; + case RegionLanguageType.IT: + return "it"; + case RegionLanguageType.ES: + return "es"; + case RegionLanguageType.CZ: + return "cz"; + case RegionLanguageType.TR: + return "tr"; + default: + return "uk"; + } + } + + public static Encoding GetEncoding(RegionLanguageType key) + { + switch (key) + { + case RegionLanguageType.EN: + case RegionLanguageType.FR: + case RegionLanguageType.ES: + return Encoding.GetEncoding(1252); + case RegionLanguageType.DE: + case RegionLanguageType.PL: + case RegionLanguageType.IT: + case RegionLanguageType.CZ: + return Encoding.GetEncoding(1250); + case RegionLanguageType.TR: + return Encoding.GetEncoding(1254); + default: + throw new ArgumentOutOfRangeException(nameof(key), key, null); + } + } + + private async Task> LoadAsync(string fileToParse, GameDataType dataType) + { + var translations = new List(); + HashSet _hashSet = dataType switch + { + GameDataType.Item => (await _itemLoader.LoadAsync()).Select(s => s.Name).ToHashSet(), + GameDataType.Card => (await _cardLoader.LoadAsync()).Select(s => s.Name).ToHashSet(), + GameDataType.NpcMonster => (await _npcLoader.LoadAsync()).Select(s => s.Name).ToHashSet(), + GameDataType.Skill => (await _skillLoader.LoadAsync()).Select(s => s.Name).ToHashSet(), + GameDataType.QuestName => (await _questLoader.LoadAsync()).Select(s => s.Name).ToHashSet(), + _ => new HashSet() + }; + + foreach (RegionLanguageType lang in Enum.GetValues()) + { + if (lang == RegionLanguageType.RU) + { + continue; + } + + string fileLang = $"{_config.GameLanguagePath}/{string.Format(fileToParse, ToNostaleRegionKey(lang))}"; + using var langFileStream = new StreamReader(fileLang, GetEncoding(lang)); + string line; + while ((line = await langFileStream.ReadLineAsync()) != null) + { + string[] lineSave = line.Split('\t'); + if (lineSave.Length <= 1) + { + continue; + } + + if (!_hashSet.Contains(lineSave[0])) + { + continue; + } + + translations.Add(new GameDataTranslationDto + { + DataType = dataType, + Language = lang, + Key = lineSave[0], + Value = lineSave[1] + }); + } + } + + Log.Info($"[RESOURCE_LOADER] Loaded {translations.Count} Game Data translations of {dataType.ToString()}"); + + return translations; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.ResourceLoader/Loaders/GenericTranslationGrpcLoader.cs b/srcs/_plugins/Plugin.ResourceLoader/Loaders/GenericTranslationGrpcLoader.cs new file mode 100644 index 0000000..51a10d3 --- /dev/null +++ b/srcs/_plugins/Plugin.ResourceLoader/Loaders/GenericTranslationGrpcLoader.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using WingsAPI.Communication; +using WingsAPI.Communication.Translations; +using WingsAPI.Data.GameData; + +namespace Plugin.ResourceLoader.Loaders +{ + public class GenericTranslationGrpcLoader : IResourceLoader + { + private readonly ITranslationService _config; + + public GenericTranslationGrpcLoader(ITranslationService config) => _config = config; + + public async Task> LoadAsync() => (await _config.GetTranslations(new EmptyRpcRequest())).Translations; + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.ResourceLoader/Loaders/ItemResourceFileLoader.cs b/srcs/_plugins/Plugin.ResourceLoader/Loaders/ItemResourceFileLoader.cs new file mode 100644 index 0000000..37c4e01 --- /dev/null +++ b/srcs/_plugins/Plugin.ResourceLoader/Loaders/ItemResourceFileLoader.cs @@ -0,0 +1,724 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using PhoenixLib.Logging; +using WingsAPI.Data.GameData; +using WingsAPI.Packets.Enums.Shells; +using WingsEmu.DTOs.BCards; +using WingsEmu.DTOs.Items; +using WingsEmu.Game._enum; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Battle; + +namespace Plugin.ResourceLoader.Loaders +{ + public class ItemResourceFileLoader : IResourceLoader + { + private const string FILE_NAME = "Item.dat"; + private readonly ResourceLoadingConfiguration _configuration; + private readonly List _itemDtos = new(); + + public ItemResourceFileLoader(ResourceLoadingConfiguration configuration) => _configuration = configuration; + + public async Task> LoadAsync() + { + if (_itemDtos.Any()) + { + return _itemDtos; + } + + string filePath = Path.Combine(_configuration.GameDataPath, FILE_NAME); + + if (!File.Exists(filePath)) + { + throw new FileNotFoundException($"{filePath} should be present"); + } + + using var npcIdStream = new StreamReader(filePath, Encoding.GetEncoding(1252)); + + var item = new ItemDTO(); + bool itemAreaBegin = false; + int itemCounter = 0; + + string line; + while ((line = await npcIdStream.ReadLineAsync()) != null) + { + try + { + string[] currentLine = line.Split('\t'); + + switch (currentLine.Length) + { + case > 3 when currentLine[1] == "VNUM": + itemAreaBegin = true; + item.Id = int.Parse(currentLine[2]); + item.Price = long.Parse(currentLine[3]); + break; + case > 1 when currentLine[1] == "END": + { + if (!itemAreaBegin) + { + continue; + } + + _itemDtos.Add(item); + itemCounter++; + + item = new ItemDTO(); + itemAreaBegin = false; + break; + } + case > 2 when currentLine[1] == "NAME": + item.Name = currentLine[2]; + break; + case > 7 when currentLine[1] == "INDEX": + FillMorphAndIndexValues(currentLine, item); + break; + case > 3 when currentLine[1] == "TYPE": + // currentLine[2] 0-range 2-range 3-magic + item.AttackType = (AttackType)Convert.ToByte(currentLine[2]); + item.Class = item.EquipmentSlot == EquipmentType.Fairy ? (byte)15 : Convert.ToByte(currentLine[3]); + break; + case > 3 when currentLine[1] == "FLAG": + FillFlags(item, currentLine); + break; + case > 1 when currentLine[1] == "DATA": + FillData(item, currentLine); + break; + case > 1 when currentLine[1] == "BUFF": + FillBuff(currentLine, item); + break; + } + } + catch (Exception e) + { + Log.Error("Error while loading Item.dat", e); + } + } + + Log.Info($"[RESOURCE_LOADER] {itemCounter.ToString()} Items loaded"); + return _itemDtos; + } + + private static void FillBuff(IReadOnlyList currentLine, ItemDTO item) + { + for (int i = 0; i < 5; i++) + { + byte type = (byte)int.Parse(currentLine[2 + 5 * i]); + if (type is 0 or 255) // 255 = -1 + { + continue; + } + + int first = int.Parse(currentLine[3 + 5 * i]); + int second = int.Parse(currentLine[4 + 5 * i]); + + int firstModulo = first % 4; + firstModulo = firstModulo switch + { + -1 => 1, + -2 => 2, + -3 => 1, + _ => firstModulo + }; + + int secondModulo = second % 4; + secondModulo = secondModulo switch + { + -1 => 1, + -2 => 2, + -3 => 1, + _ => secondModulo + }; + + var itemCard = new BCardDTO + { + ItemVNum = item.Id, + Type = type, + SubType = (byte)((int.Parse(currentLine[5 + i * 5]) + 1) * 10 + 1 + (first < 0 ? 1 : 0)), + FirstDataScalingType = (BCardScalingType)firstModulo, + SecondDataScalingType = (BCardScalingType)secondModulo, + FirstData = (int)Math.Abs(Math.Floor(first / 4.0)), + SecondData = (int)Math.Abs(Math.Floor(second / 4.0)), + CastType = byte.Parse(currentLine[6 + 5 * i]) + }; + + item.BCards.Add(itemCard); + } + } + + private static void FillFlags(ItemDTO item, IReadOnlyList currentLine) + { + // useless [2] + // useless [3] + // useless [4] + item.IsSoldable = currentLine[5] == "0"; + item.IsDroppable = currentLine[6] == "0"; + item.IsTradable = currentLine[7] == "0"; + item.IsMinilandActionable = currentLine[8] == "1"; + item.IsWarehouse = currentLine[9] == "1"; + item.ShowWarningOnUse = currentLine[10] == "1"; + item.IsTimeSpaceRewardBox = currentLine[11] == "1"; + item.ShowDescriptionOnHover = currentLine[12] == "1"; + item.Flag3 = currentLine[13] == "1"; + item.FollowMouseOnUse = currentLine[14] == "1"; + item.ShowSomethingOnHover = currentLine[15] == "1"; + item.IsColorable = currentLine[16] == "1"; + item.Sex = currentLine[18] == "1" + ? (byte)1 + : currentLine[17] == "1" + ? (byte)2 + : (byte)0; + //not used item.Flag6 = currentLine[19] == "1"; + item.PlaySoundOnPickup = currentLine[20] == "1"; + item.UseReputationAsPrice = currentLine[21] == "1"; + if (item.UseReputationAsPrice) + { + item.ReputPrice = item.Price; + } + + item.IsHeroic = currentLine[22] == "1"; + item.Flag7 = currentLine[23] == "1"; + item.IsLimited = currentLine[24] == "1"; + } + + private static void FillData(ItemDTO item, string[] currentLine) + { + item.Data = new int[20]; + + for (int i = 0; i < 20; i++) + { + item.Data[i] = Convert.ToInt32(currentLine[2 + i]); + } + + switch (item.ItemType) + { + case ItemType.Weapon: + item.LevelMinimum = Convert.ToByte(currentLine[2]); + item.DamageMinimum = Convert.ToInt16(currentLine[3]); + item.DamageMaximum = Convert.ToInt16(currentLine[4]); + item.HitRate = Convert.ToInt16(currentLine[5]); + item.CriticalLuckRate = Convert.ToSByte(currentLine[6]); + item.CriticalRate = Convert.ToInt16(currentLine[7]); + item.BasicUpgrade = Convert.ToByte(currentLine[10]); + item.MaximumAmmo = 100; + break; + + case ItemType.Armor: + item.LevelMinimum = Convert.ToByte(currentLine[2]); + item.CloseDefence = Convert.ToInt16(currentLine[3]); + item.DistanceDefence = Convert.ToInt16(currentLine[4]); + item.MagicDefence = Convert.ToInt16(currentLine[5]); + item.DefenceDodge = Convert.ToInt16(currentLine[6]); + item.DistanceDefenceDodge = Convert.ToInt16(currentLine[6]); + item.BasicUpgrade = Convert.ToByte(currentLine[10]); + break; + + case ItemType.Box: + item.Effect = Convert.ToInt16(currentLine[21]); + item.EffectValue = Convert.ToInt32(currentLine[3]); + item.LevelMinimum = Convert.ToByte(currentLine[4]); + + if (item.ItemSubType == 7) // Magic Speed Booster + { + long time = Convert.ToInt32(currentLine[4]); + item.ItemValidTime = time == 0 ? -1 : Convert.ToInt32(currentLine[4]) * 3600; + } + + break; + + case ItemType.Fashion: + item.LevelMinimum = Convert.ToByte(currentLine[2]); + item.CloseDefence = Convert.ToInt16(currentLine[3]); + item.DistanceDefence = Convert.ToInt16(currentLine[4]); + item.MagicDefence = Convert.ToInt16(currentLine[5]); + item.DefenceDodge = Convert.ToInt16(currentLine[6]); + item.DistanceDefenceDodge = Convert.ToInt16(currentLine[6]); + + if (item.EquipmentSlot == EquipmentType.CostumeHat || item.EquipmentSlot == EquipmentType.CostumeSuit || item.EquipmentSlot == EquipmentType.WeaponSkin) + { + long time = Convert.ToInt32(currentLine[13]); + item.ItemValidTime = time == 0 ? -1 : Convert.ToInt32(currentLine[13]) * 3600; + } + + break; + + case ItemType.Food: + item.Hp = Convert.ToInt16(currentLine[2]); + item.Mp = Convert.ToInt16(currentLine[4]); + break; + + case ItemType.Jewelry: + switch (item.EquipmentSlot) + { + case EquipmentType.Amulet: + item.LevelMinimum = Convert.ToByte(currentLine[2]); + item.ItemLeftType = Convert.ToInt16(currentLine[4]); + if (item.ItemLeftType == 100) + { + item.LeftUsages = Convert.ToInt32(currentLine[3]); + } + else if (item.ItemLeftType >= 1000) + { + item.ItemValidTime = Convert.ToInt64(currentLine[13]) * 3600; + } + else + { + item.ItemValidTime = Convert.ToInt64(currentLine[3]) == 0 ? -1 : Convert.ToInt64(currentLine[3]) / 10; + } + + break; + case EquipmentType.Fairy: + item.Element = Convert.ToByte(currentLine[2]); + item.ElementRate = Convert.ToInt16(currentLine[3]); + if (item.Id <= 256) + { + item.MaxElementRate = 50; + } + else if (item.ElementRate == 0) + { + if (item.Id >= 800 && item.Id <= 804) + { + item.MaxElementRate = 50; + } + else + { + item.MaxElementRate = 70; + } + } + else if (item.ElementRate == 30) + { + item.MaxElementRate = 30; + } + else if (item.ElementRate == 35) + { + item.MaxElementRate = 35; + } + else if (item.ElementRate == 40) + { + item.MaxElementRate = 70; + } + else if (item.ElementRate == 50) + { + item.MaxElementRate = 80; + } + + break; + default: + item.LevelMinimum = Convert.ToByte(currentLine[2]); + item.MaxCellonLvl = Convert.ToByte(currentLine[3]); + item.MaxCellon = Convert.ToByte(currentLine[4]); + break; + } + + break; + + case ItemType.Event: + item.Effect = Convert.ToInt16(currentLine[2]); + item.EffectValue = Convert.ToInt16(currentLine[3]); + break; + + case ItemType.Special: + switch (item.Id) + { + case 5853: + item.Effect = 1717; + item.EffectValue = 1; + break; + case 5854: + item.Effect = 1717; + item.EffectValue = 2; + break; + case 5855: + item.Effect = 1717; + item.EffectValue = 3; + break; + case 1272: + case 1858: + case 9047: + item.Effect = 1005; + item.EffectValue = 10; + break; + + case 1273: + case 9024: + item.Effect = 1005; + item.EffectValue = 30; + break; + + case 1274: + case 9025: + item.Effect = 1005; + item.EffectValue = 60; + break; + + case 1279: + case 9029: + item.Effect = 1007; + item.EffectValue = 30; + break; + + case 1280: + case 9030: + item.Effect = 1007; + item.EffectValue = 60; + break; + + case 1923: + case 9056: + item.Effect = 1007; + item.EffectValue = 10; + break; + + case 1275: + case 1886: + case 9026: + item.Effect = 1008; + item.EffectValue = 10; + break; + + case 1276: + case 9027: + item.Effect = 1008; + item.EffectValue = 30; + break; + + case 1277: + case 9028: + item.Effect = 1008; + item.EffectValue = 60; + break; + + case 5060: + case 9066: + item.Effect = 1003; + item.EffectValue = 30; + break; + + case 5061: + case 9067: + item.Effect = 1004; + item.EffectValue = 7; + break; + + case 5062: + case 9068: + item.Effect = 1004; + item.EffectValue = 1; + break; + case 5115: + item.Effect = 652; + break; + + case 1981: + item.Effect = 34; // imagined number as for I = √(-1), complex z = a + bi + break; + + case 1982: + item.Effect = 6969; // imagined number as for I = √(-1), complex z = a + bi + break; + + case 9071: + case 5119: // Speed booster + item.Effect = 998; + break; + + case 180: // attack amulet + item.Effect = 932; + break; + + case 181: // defense amulet + item.Effect = 933; + break; + + default: + if (item.Id > 5891 && item.Id < 5900 || item.Id > 9100 && item.Id < 9109) + { + item.Effect = 69; // imagined number as for I = √(-1), complex z = a + bi + } + else + { + item.Effect = Convert.ToInt16(currentLine[2]); + } + + break; + } + + switch (item.Effect) + { + case 305: + item.EffectValue = Convert.ToInt32(currentLine[5]); + item.Morph = Convert.ToInt16(currentLine[4]); + break; + + default: + item.EffectValue = item.EffectValue == 0 ? Convert.ToInt32(currentLine[4]) : item.EffectValue; + break; + } + + item.WaitDelay = 5000; + break; + + case ItemType.Magical: + item.Effect = Convert.ToInt16(currentLine[2]); + + if (item.Effect == 99) + { + item.LevelMinimum = Convert.ToByte(currentLine[4]); + item.EffectValue = Convert.ToByte(currentLine[5]); + } + else + { + item.EffectValue = Convert.ToInt32(currentLine[4]); + } + + break; + + case ItemType.Specialist: + item.IsPartnerSpecialist = item.ItemSubType == 4; + item.Speed = Convert.ToByte(currentLine[5]); + if (item.IsPartnerSpecialist) + { + item.Element = Convert.ToByte(currentLine[3]); + item.ElementRate = Convert.ToInt16(currentLine[4]); + item.PartnerClass = Convert.ToByte(currentLine[19]); + item.LevelMinimum = Convert.ToByte(currentLine[20]); + } + else + { + item.LevelJobMinimum = Convert.ToByte(currentLine[20]); + item.ReputationMinimum = Convert.ToByte(currentLine[21]); + } + + item.SpPointsUsage = Convert.ToByte(currentLine[13]); + item.SpMorphId = item.IsPartnerSpecialist ? (byte)(1 + Convert.ToByte(currentLine[14])) : Convert.ToByte(currentLine[14]); + item.FireResistance = Convert.ToByte(currentLine[15]); + item.WaterResistance = Convert.ToByte(currentLine[16]); + item.LightResistance = Convert.ToByte(currentLine[17]); + item.DarkResistance = Convert.ToByte(currentLine[18]); + + var elementdic = new Dictionary { { 0, 0 } }; + if (item.FireResistance != 0) + { + elementdic.Add(1, item.FireResistance); + } + + if (item.WaterResistance != 0) + { + elementdic.Add(2, item.WaterResistance); + } + + if (item.LightResistance != 0) + { + elementdic.Add(3, item.LightResistance); + } + + if (item.DarkResistance != 0) + { + elementdic.Add(4, item.DarkResistance); + } + + if (!item.IsPartnerSpecialist) + { + item.Element = (byte)elementdic.OrderByDescending(s => s.Value).First().Key; + } + + // needs to be hardcoded + switch (item.Id) + { + case 901: + item.Element = 1; + break; + + case 903: + item.Element = 2; + break; + + case 906: + item.Element = 3; + break; + + case 909: + item.Element = 3; + break; + } + + break; + + case ItemType.Shell: + byte shellType = Convert.ToByte(currentLine[5]); + + item.ShellMinimumLevel = Convert.ToInt16(currentLine[3]); + item.ShellMaximumLevel = Convert.ToInt16(currentLine[4]); + item.ShellType = (ShellType)(item.ItemSubType == 1 ? shellType + 50 : shellType); + break; + + case ItemType.Main: + item.Effect = Convert.ToInt16(currentLine[2]); + item.EffectValue = Convert.ToInt32(currentLine[4]); + break; + + case ItemType.Upgrade: + item.Effect = Convert.ToInt16(currentLine[2]); + switch (item.Id) + { + // UpgradeItems (needed to be hardcoded) + case (int)ItemVnums.EQ_NORMAL_SCROLL: + item.EffectValue = 26; + break; + + case (int)ItemVnums.LOWER_SP_SCROLL: + item.EffectValue = 27; + break; + + case (int)ItemVnums.HIGHER_SP_SCROLL: + item.EffectValue = 28; + break; + + case (int)ItemVnums.SCROLL_CHICKEN: + item.EffectValue = 47; + break; + + case (int)ItemVnums.SCROLL_PYJAMA: + item.EffectValue = 50; + break; + + case (int)ItemVnums.EQ_GOLD_SCROLL: + item.EffectValue = 61; + break; + + case (int)ItemVnums.SCROLL_PIRATE: + item.EffectValue = 60; + break; + + default: + item.EffectValue = Convert.ToInt32(currentLine[4]); + break; + } + + break; + + case ItemType.Production: + item.Effect = Convert.ToInt16(currentLine[2]); + item.EffectValue = Convert.ToInt32(currentLine[4]); + break; + + case ItemType.Map: + item.Effect = Convert.ToInt16(currentLine[2]); + item.EffectValue = Convert.ToInt32(currentLine[4]); + break; + + case ItemType.Potion: + item.Hp = Convert.ToInt16(currentLine[2]); + item.Mp = Convert.ToInt16(currentLine[4]); + break; + + case ItemType.Snack: + item.Hp = Convert.ToInt16(currentLine[2]); + item.Mp = Convert.ToInt16(currentLine[4]); + break; + + case ItemType.PetPartnerItem: + item.Effect = Convert.ToInt16(currentLine[2]); + item.EffectValue = Convert.ToInt32(currentLine[4]); + break; + + case ItemType.Material: + case ItemType.Sell: + case ItemType.Quest2: + case ItemType.Quest1: + case ItemType.Ammo: + // nothing to parse + break; + } + + if (item.Type == InventoryType.Miniland) + { + item.MinilandObjectPoint = int.Parse(currentLine[2]); + item.EffectValue = short.Parse(currentLine[8]); + item.Width = Convert.ToByte(currentLine[9]) == 0 ? (byte)1 : Convert.ToByte(currentLine[9]); + item.Height = Convert.ToByte(currentLine[10]) == 0 ? (byte)1 : Convert.ToByte(currentLine[10]); + } + + if (item.EquipmentSlot != EquipmentType.Boots && item.EquipmentSlot != EquipmentType.Gloves || item.Type != 0) + { + return; + } + + item.FireResistance = Convert.ToByte(currentLine[7]); + item.WaterResistance = Convert.ToByte(currentLine[8]); + item.LightResistance = Convert.ToByte(currentLine[9]); + item.DarkResistance = Convert.ToByte(currentLine[11]); + } + + private static void FillMorphAndIndexValues(string[] currentLine, ItemDTO item) + { + switch (Convert.ToByte(currentLine[2])) + { + case 4: + item.Type = InventoryType.Equipment; + break; + + case 8: + item.Type = InventoryType.Equipment; + break; + + case 9: + item.Type = InventoryType.Main; + break; + + case 10: + item.Type = InventoryType.Etc; + break; + + default: + item.Type = (InventoryType)Enum.Parse(typeof(InventoryType), currentLine[2]); + break; + } + + item.ItemType = currentLine[3] != "-1" ? (ItemType)Enum.Parse(typeof(ItemType), $"{(short)item.Type}{currentLine[3]}") : ItemType.Weapon; + item.ItemSubType = Convert.ToByte(currentLine[4]); + item.EquipmentSlot = (EquipmentType)Enum.Parse(typeof(EquipmentType), currentLine[5] != "-1" ? currentLine[5] : "0"); + + item.IconId = Convert.ToInt32(currentLine[6]); + switch (item.Id) + { + case 4101: + case 4102: + case 4103: + case 4104: + case 4105: + item.EquipmentSlot = 0; + break; + + default: + if (item.EquipmentSlot.Equals(EquipmentType.Amulet)) + { + switch (item.Id) + { + case 4503: + item.EffectValue = 4544; + break; + + case 4504: + item.EffectValue = 4294; + break; + + default: + item.EffectValue = Convert.ToInt16(currentLine[7]); + break; + } + } + else + { + item.Morph = Convert.ToInt16(currentLine[7]); + } + + break; + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.ResourceLoader/Loaders/MapResourceFileLoader.cs b/srcs/_plugins/Plugin.ResourceLoader/Loaders/MapResourceFileLoader.cs new file mode 100644 index 0000000..ad4b79b --- /dev/null +++ b/srcs/_plugins/Plugin.ResourceLoader/Loaders/MapResourceFileLoader.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using PhoenixLib.Logging; +using WingsAPI.Data.GameData; +using WingsEmu.DTOs.Maps; + +namespace Plugin.ResourceLoader.Loaders +{ + public class MapResourceFileLoader : IResourceLoader + { + private readonly ResourceLoadingConfiguration _config; + + + public MapResourceFileLoader(ResourceLoadingConfiguration config) => _config = config; + + public async Task> LoadAsync() + { + string filePath = Path.Combine(_config.GameDataPath, "MapIDData.dat"); + + if (!File.Exists(filePath)) + { + throw new FileNotFoundException($"{filePath} should be present"); + } + + var maps = new List(); + var dictionaryId = new Dictionary(); + + int i = 0; + using (var mapIdStream = new StreamReader(filePath, Encoding.GetEncoding(1252))) + { + string line; + while ((line = await mapIdStream.ReadLineAsync()) != null) + { + string[] values = line.Split(' '); + if (values.Length <= 1) + { + continue; + } + + if (!int.TryParse(values[0], out int mapId)) + { + continue; + } + + if (!dictionaryId.ContainsKey(mapId)) + { + dictionaryId.Add(mapId, values[4]); + } + } + } + + foreach (FileInfo file in new DirectoryInfo(_config.GameMapsPath).GetFiles()) + { + string name = string.Empty; + + if (dictionaryId.TryGetValue(int.Parse(file.Name), out string value)) + { + name = value; + } + + + byte[] data = await File.ReadAllBytesAsync(file.FullName); + short width = BitConverter.ToInt16(data, 0); + short height = BitConverter.ToInt16(data, 2); + + maps.Add(new MapDataDTO + { + Id = short.Parse(file.Name), + Name = name, + Width = width, + Height = height, + Grid = data.Skip(4).ToArray() + }); + i++; + } + + Log.Info($"[RESOURCE_LOADER] {maps.Count} Maps loaded"); + return maps; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.ResourceLoader/Loaders/NpcMonsterFileLoader.cs b/srcs/_plugins/Plugin.ResourceLoader/Loaders/NpcMonsterFileLoader.cs new file mode 100644 index 0000000..a99686f --- /dev/null +++ b/srcs/_plugins/Plugin.ResourceLoader/Loaders/NpcMonsterFileLoader.cs @@ -0,0 +1,487 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using PhoenixLib.Logging; +using WingsAPI.Data.Drops; +using WingsAPI.Data.GameData; +using WingsEmu.DTOs.BCards; +using WingsEmu.DTOs.NpcMonster; +using WingsEmu.DTOs.Skills; +using WingsEmu.Game._enum; +using WingsEmu.Game.Algorithm; +using WingsEmu.Packets.Enums.Battle; + +namespace Plugin.ResourceLoader.Loaders +{ + public class NpcMonsterFileLoader : IResourceLoader + { + private readonly IBattleEntityAlgorithmService _algorithm; + private readonly ResourceLoadingConfiguration _config; + private readonly ILogger _logger; + private readonly List _npcMonsters = new(); + + public NpcMonsterFileLoader(ResourceLoadingConfiguration config, ILogger logger, IBattleEntityAlgorithmService algorithm) + { + _config = config; + _logger = logger; + _algorithm = algorithm; + } + + public async Task> LoadAsync() + { + if (_npcMonsters.Any()) + { + return _npcMonsters; + } + + string filePath = Path.Combine(_config.GameDataPath, "monster.dat"); + + if (!File.Exists(filePath)) + { + throw new FileNotFoundException($"{filePath} should be present"); + } + + var npc = new NpcMonsterDto(); + + bool itemAreaBegin = false; + int counter = 0; + + using var npcIdStream = new StreamReader(filePath, Encoding.GetEncoding(1252)); + string line; + while ((line = await npcIdStream.ReadLineAsync()) != null) + { + string[] currentLine = line.Split('\t'); + + switch (currentLine.Length) + { + case > 2 when currentLine[1] == "VNUM": + npc = new NpcMonsterDto + { + Id = Convert.ToInt16(currentLine[2]) + }; + itemAreaBegin = true; + break; + case > 2 when currentLine[1] == "NAME": + npc.Name = currentLine[2]; + break; + case > 2 when currentLine[1] == "LEVEL": + { + if (!itemAreaBegin) + { + continue; + } + + npc.Level = Convert.ToByte(currentLine[2]); + break; + } + case > 3 when currentLine[1] == "RACE": + npc.Race = Convert.ToByte(currentLine[2]); + npc.RaceType = Convert.ToByte(currentLine[3]); + break; + case > 7 when currentLine[1] == "ATTRIB": + npc.Element = Convert.ToByte(currentLine[2]); + npc.ElementRate = Convert.ToInt16(currentLine[3]); + npc.FireResistance = Convert.ToInt16(currentLine[4]); + npc.WaterResistance = Convert.ToInt16(currentLine[5]); + npc.LightResistance = Convert.ToInt16(currentLine[6]); + npc.DarkResistance = Convert.ToInt16(currentLine[7]); + break; + case > 2 when currentLine[1] == "EXP": + { + npc.BaseXp = Convert.ToInt32(currentLine[2]); + npc.BaseJobXp = Convert.ToInt32(currentLine[3]); + + npc.Xp = npc.Level < 20 ? 60 * npc.Level + Convert.ToInt32(currentLine[2]) : 70 * npc.Level + Convert.ToInt32(currentLine[2]); + npc.JobXp = npc.Level > 60 ? 105 + Convert.ToInt32(currentLine[3]) : 120 + Convert.ToInt32(currentLine[3]); + + if (npc.Xp < 0) + { + npc.Xp = 0; + } + + if (npc.JobXp < 0) + { + npc.JobXp = 0; + } + + break; + } + case > 6 when currentLine[1] == "PREATT": + npc.HostilityType = Convert.ToInt32(currentLine[2]); + npc.GroupAttack = Convert.ToInt32(currentLine[3]); + npc.NoticeRange = Convert.ToByte(currentLine[4]); + npc.Speed = Convert.ToByte(currentLine[5]); + npc.RespawnTime = Convert.ToInt32(currentLine[6]); + break; + case > 4 when currentLine[1] == "WINFO": + npc.WinfoValue = Convert.ToByte(currentLine[3]); + npc.AttackUpgrade = Convert.ToByte(currentLine[4]); + break; + case > 3 when currentLine[1] == "AINFO": + npc.DefenceUpgrade = Convert.ToByte(currentLine[3]); + break; + case > 4 when currentLine[1] == "PETINFO": + { + if (npc.Race == 8) + { + switch (npc.RaceType) + { + case 7: //collectable NPC + npc.MaxTries = Convert.ToByte(currentLine[2]); + npc.CollectionCooldown = Convert.ToInt16(currentLine[3]); + npc.AmountRequired = Convert.ToInt16(currentLine[4]); + npc.CollectionDanceTime = Convert.ToByte(currentLine[5]); + break; + case 5: //teleporters + npc.VNumRequired = Convert.ToInt16(currentLine[2]); + npc.AmountRequired = Convert.ToInt16(currentLine[3]); + npc.TeleportRemoveFromInventory = currentLine[4] != "0"; + break; + } + } + else + { + npc.MeleeHpFactor = Convert.ToInt16(currentLine[2]); + npc.RangeDodgeFactor = Convert.ToInt16(currentLine[3]); + npc.MagicMpFactor = Convert.ToInt16(currentLine[4]); + } + + break; + } + case > 3 when currentLine[1] == "HP/MP": + npc.CleanHp = Convert.ToInt32(currentLine[2]); + npc.CleanMp = Convert.ToInt32(currentLine[3]); + + npc.MaxHp = _algorithm.GetBasicHp(npc.Race, npc.Level, npc.MeleeHpFactor, Convert.ToInt32(currentLine[2])); + npc.MaxMp = _algorithm.GetBasicMp(npc.Race, npc.Level, npc.MagicMpFactor, Convert.ToInt32(currentLine[3])); + break; + case > 6 when currentLine[1] == "WEAPON": + npc.WeaponLevel = Convert.ToByte(currentLine[2]); + npc.CleanDamageMin = Convert.ToInt32(currentLine[4]); + npc.CleanDamageMax = Convert.ToInt32(currentLine[5]); + npc.CleanHitRate = Convert.ToInt32(currentLine[6]); + + npc.DamageMinimum = _algorithm.GetAttack(true, npc.Race, npc.AttackType, npc.WeaponLevel, npc.WinfoValue, npc.Level, GetModifier(npc), + Convert.ToInt16(currentLine[4])); + npc.DamageMaximum = _algorithm.GetAttack(false, npc.Race, npc.AttackType, npc.WeaponLevel, npc.WinfoValue, npc.Level, GetModifier(npc), + Convert.ToInt16(currentLine[5])); + npc.Concentrate = (short)_algorithm.GetHitrate(npc.Race, npc.AttackType, npc.WeaponLevel, npc.Level, GetModifier(npc), + Convert.ToInt16(currentLine[6])); + npc.CriticalChance = (short)(Convert.ToInt16(currentLine[7]) + 4); + npc.CriticalRate = (short)(Convert.ToInt16(currentLine[8]) + 70); + break; + case > 6 when currentLine[1] == "ARMOR": + npc.ArmorLevel = Convert.ToByte(currentLine[2]); + npc.CleanMeleeDefence = Convert.ToInt32(currentLine[3]); + npc.CleanRangeDefence = Convert.ToInt32(currentLine[4]); + npc.CleanMagicDefence = Convert.ToInt32(currentLine[5]); + npc.CleanDodge = Convert.ToInt32(currentLine[6]); + + npc.CloseDefence = (short)_algorithm.GetDefense(npc.Race, AttackType.Melee, npc.ArmorLevel, npc.Level, GetModifier(npc), Convert.ToInt16(currentLine[3])); + npc.DistanceDefence = (short)_algorithm.GetDefense(npc.Race, AttackType.Ranged, npc.ArmorLevel, npc.Level, GetModifier(npc), Convert.ToInt16(currentLine[4])); + npc.MagicDefence = (short)_algorithm.GetDefense(npc.Race, AttackType.Magical, npc.ArmorLevel, npc.Level, GetModifier(npc), Convert.ToInt16(currentLine[5])); + npc.DefenceDodge = (short)_algorithm.GetDodge(npc.Race, npc.ArmorLevel, npc.Level, GetModifier(npc), Convert.ToInt16(currentLine[6])); + npc.DistanceDefenceDodge = (short)_algorithm.GetDodge(npc.Race, npc.ArmorLevel, npc.Level, GetModifier(npc), Convert.ToInt16(currentLine[6])); + break; + case > 7 when currentLine[1] == "ETC": + { + long bitFlag = Convert.ToInt64(currentLine[2]); + + npc.CanWalk = Convert.ToBoolean(bitFlag & (long)MobFlag.CANT_WALK) == false; + npc.CanBeCollected = Convert.ToBoolean(bitFlag & (long)MobFlag.CAN_BE_COLLECTED); + npc.CanBeDebuffed = Convert.ToBoolean(bitFlag & (long)MobFlag.CANT_BE_DEBUFFED) == false; + npc.CanBeCaught = Convert.ToBoolean(bitFlag & (long)MobFlag.CAN_BE_CAUGHT); + npc.DisappearAfterSeconds = Convert.ToBoolean(bitFlag & (long)MobFlag.DISSAPPEAR_AFTER_SECONDS); + npc.DisappearAfterHitting = Convert.ToBoolean(bitFlag & (long)MobFlag.DISSAPPEAR_AFTER_HITTING); + npc.HasMode = Convert.ToBoolean(bitFlag & (long)MobFlag.HAS_MODE); + npc.DisappearAfterSecondsMana = Convert.ToBoolean(bitFlag & (long)MobFlag.DISSAPPEAR_AFTER_SECONDS_MANA); + npc.OnDefenseOnlyOnce = Convert.ToBoolean(bitFlag & (long)MobFlag.ON_DEFENSE_ONLY_ONCE); + npc.HasDash = Convert.ToBoolean(bitFlag & (long)MobFlag.HAS_DASH); + npc.CanRegenMp = Convert.ToBoolean(bitFlag & (long)MobFlag.CAN_REGEN_MP); + npc.CanBePushed = Convert.ToBoolean(bitFlag & (long)MobFlag.CAN_BE_PUSHED) == false; + + npc.IsPercent = currentLine[4] == "1"; + npc.DamagedOnlyLastJajamaruSkill = currentLine[5] == "1"; + npc.DropToInventory = currentLine[7] == "1"; + break; + } + case > 6 when currentLine[1] == "SETTING": + { + npc.IconId = Convert.ToInt32(currentLine[2]); + npc.SpawnMobOrColor = Convert.ToInt32(currentLine[3]); + npc.SpriteSize = Convert.ToInt32(currentLine[5]); + npc.CellSize = Convert.ToInt32(currentLine[6]); + + if (npc.Race == 8 && (npc.RaceType == 7 || npc.RaceType == 6)) + { + npc.VNumRequired = Convert.ToInt16(currentLine[4]); + } + + break; + } + case > 2 when currentLine[1] == "EFF": + npc.AttackEffect = Convert.ToInt16(currentLine[2]); + npc.PermanentEffect = Convert.ToInt16(currentLine[3]); + npc.DeathEffect = Convert.ToInt16(currentLine[4]); + break; + case > 8 when currentLine[1] == "ZSKILL": + npc.AttackType = (AttackType)Convert.ToByte(currentLine[2]); + npc.BasicRange = Convert.ToByte(currentLine[3]); + npc.BasicHitChance = Convert.ToByte(currentLine[4]); + npc.BasicCastTime = Convert.ToByte(currentLine[5]); + npc.BasicCooldown = Convert.ToInt16(currentLine[6]); + npc.BasicDashSpeed = Convert.ToInt16(currentLine[7]); + break; + case > 1 when currentLine[1] == "SKILL": + { + for (int i = 2; i < currentLine.Length - 3; i += 3) + { + short vnum = short.Parse(currentLine[i]); + if (vnum is -1 or 0) + { + continue; + } + + npc.Skills.Add(new NpcMonsterSkillDTO + { + SkillVNum = vnum, + Rate = Convert.ToInt16(currentLine[i + 1]), + NpcMonsterVNum = npc.Id, + IsBasicAttack = currentLine[i + 2] == "2", + IsIgnoringHitChance = currentLine[i + 2] == "1" + }); + } + + break; + } + case > 1 when currentLine[1] == "MODE": + { + for (int i = 0; i < 5; i++) + { + byte type = (byte)int.Parse(currentLine[5 * i + 2]); + if (type == 0) + { + continue; + } + + int first = int.Parse(currentLine[3 + 5 * i]); + int second = int.Parse(currentLine[4 + 5 * i]); + + int firstModulo = first % 4; + firstModulo = firstModulo switch + { + -1 => 1, + -2 => 2, + -3 => 1, + _ => firstModulo + }; + + int secondModulo = second % 4; + secondModulo = secondModulo switch + { + -1 => 1, + -2 => 2, + -3 => 1, + _ => secondModulo + }; + + var modeBCard = new BCardDTO + { + NpcMonsterVNum = npc.Id, + Type = type, + SubType = (byte)((int.Parse(currentLine[5 + 5 * i]) + 1) * 10 + 1 + (first >= 0 ? 0 : 1)), + FirstDataScalingType = (BCardScalingType)firstModulo, + SecondDataScalingType = (BCardScalingType)secondModulo, + FirstData = (int)Math.Abs(Math.Floor(first / 4.0)), + SecondData = (int)Math.Abs(Math.Floor(second / 4.0)), + CastType = byte.Parse(currentLine[6 + 5 * i]), + IsMonsterMode = true + }; + + npc.ModeBCards.Add(modeBCard); + } + + npc.ModeIsHpTriggered = currentLine[27] == "0"; + npc.ModeLimiterType = Convert.ToByte(currentLine[28]); + npc.ModeHpTresholdOrItemVnum = Convert.ToInt16(currentLine[29]); + npc.ModeRangeTreshold = Convert.ToInt16(currentLine[30]); + npc.ModeCModeVnum = Convert.ToInt16(currentLine[31]); + + npc.MinimumAttackRange = sbyte.Parse(currentLine[32]); + npc.MidgardDamage = Convert.ToInt16(currentLine[33]); + break; + } + case > 1 when currentLine[1] == "CARD": + { + for (int i = 0; i < 4; i++) + { + byte type = (byte)int.Parse(currentLine[2 + 5 * i]); + if (type is 0 or 255) + { + continue; + } + + int first = int.Parse(currentLine[3 + 5 * i]); + int second = int.Parse(currentLine[4 + 5 * i]); + + int firstModulo = first % 4; + firstModulo = firstModulo switch + { + -1 => 1, + -2 => 2, + -3 => 1, + _ => firstModulo + }; + + int secondModulo = second % 4; + secondModulo = secondModulo switch + { + -1 => 1, + -2 => 2, + -3 => 1, + _ => secondModulo + }; + + var itemCard = new BCardDTO + { + NpcMonsterVNum = npc.Id, + Type = type, + SubType = (byte)((int.Parse(currentLine[5 + 5 * i]) + 1) * 10 + 1 + (first >= 0 ? 0 : 1)), + FirstDataScalingType = (BCardScalingType)firstModulo, + SecondDataScalingType = (BCardScalingType)secondModulo, + FirstData = (int)Math.Abs(Math.Floor(first / 4.0)), + SecondData = (int)Math.Abs(Math.Floor(second / 4.0)), + CastType = byte.Parse(currentLine[6 + 5 * i]), + TriggerType = i switch + { + 0 => BCardNpcMonsterTriggerType.ON_FIRST_ATTACK, + 1 => BCardNpcMonsterTriggerType.ON_DEATH, + _ => BCardNpcMonsterTriggerType.ON_DEATH // Custom + } + }; + + npc.BCards.Add(itemCard); + } + + break; + } + case > 1 when currentLine[1] == "BASIC": + { + for (int i = 0; i < 10; i++) + { + byte type = (byte)int.Parse(currentLine[5 * i + 2]); + if (type == 0) + { + continue; + } + + int first = int.Parse(currentLine[3 + 5 * i]); + int second = int.Parse(currentLine[4 + 5 * i]); + + int firstModulo = first % 4; + firstModulo = firstModulo switch + { + -1 => 1, + -2 => 2, + -3 => 1, + _ => firstModulo + }; + + int secondModulo = second % 4; + secondModulo = secondModulo switch + { + -1 => 1, + -2 => 2, + -3 => 1, + _ => secondModulo + }; + + var itemCard = new BCardDTO + { + NpcMonsterVNum = npc.Id, + Type = type, + SubType = (byte)((int.Parse(currentLine[5 + 5 * i]) + 1) * 10 + 1 + (first >= 0 ? 0 : 1)), + FirstDataScalingType = (BCardScalingType)firstModulo, + SecondDataScalingType = (BCardScalingType)secondModulo, + FirstData = (int)Math.Abs(Math.Floor(first / 4.0)), + SecondData = (int)Math.Abs(Math.Floor(second / 4.0)), + CastType = byte.Parse(currentLine[6 + 5 * i]), + NpcTriggerType = (i % 2) switch + { + 0 => BCardNpcTriggerType.ON_ATTACK, + 1 => BCardNpcTriggerType.ON_DEFENSE, + _ => null + } + }; + + npc.BCards.Add(itemCard); + } + + break; + } + case > 3 when currentLine[1] == "ITEM": + { + _npcMonsters.Add(npc); + counter++; + + for (int i = 2; i < currentLine.Length - 3; i += 3) + { + short vnum = Convert.ToInt16(currentLine[i]); + if (vnum is -1 or 0) + { + continue; + } + + npc.Drops ??= new List(); + + // add to monster vnum + npc.Drops.Add(new DropDTO + { + ItemVNum = vnum, + Amount = Convert.ToInt32(currentLine[i + 2]), + MonsterVNum = npc.Id, + DropChance = Convert.ToInt32(currentLine[i + 1]) + }); + } + + itemAreaBegin = false; + break; + } + } + } + + Log.Info($"[RESOURCE_LOADER] {counter.ToString()} Monster Data loaded"); + return _npcMonsters; + } + + private static int GetModifier(NpcMonsterDto npc) + { + return npc.AttackType switch + { + AttackType.Melee => npc.MeleeHpFactor, + AttackType.Ranged => npc.RangeDodgeFactor, + AttackType.Magical => npc.MagicMpFactor + }; + } + + private enum MobFlag : long + { + CANT_WALK = 1, + CAN_BE_COLLECTED = 2, + CANT_BE_DEBUFFED = 4, + CAN_BE_CAUGHT = 8, + DISSAPPEAR_AFTER_SECONDS = 16, + DISSAPPEAR_AFTER_HITTING = 32, + HAS_MODE = 64, + DISSAPPEAR_AFTER_SECONDS_MANA = 128, + ON_DEFENSE_ONLY_ONCE = 256, + HAS_DASH = 512, + CAN_REGEN_MP = 1024, + CAN_BE_PUSHED = 2048 + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.ResourceLoader/Loaders/NpcQuestResourceFileLoader.cs b/srcs/_plugins/Plugin.ResourceLoader/Loaders/NpcQuestResourceFileLoader.cs new file mode 100644 index 0000000..253dda4 --- /dev/null +++ b/srcs/_plugins/Plugin.ResourceLoader/Loaders/NpcQuestResourceFileLoader.cs @@ -0,0 +1,70 @@ +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using PhoenixLib.Logging; +using WingsAPI.Data.GameData; +using WingsEmu.DTOs.Quests; + +namespace Plugin.ResourceLoader.Loaders +{ + public class NpcQuestResourceFileLoader : IResourceLoader + { + private readonly ResourceLoadingConfiguration _configuration; + + public NpcQuestResourceFileLoader(ResourceLoadingConfiguration configuration) => _configuration = configuration; + + public async Task> LoadAsync() + { + string filePath = Path.Combine(_configuration.GameDataPath, "qstnpc.dat"); + + if (!File.Exists(filePath)) + { + throw new FileNotFoundException($"{filePath} should be present"); + } + + var npcQuests = new List(); + using var idStream = new StreamReader(filePath, Encoding.GetEncoding(1252)); + string line; + int counter = 0; + int idCounter = 1; + + while ((line = idStream.ReadLine()) != null) + { + string[] currentLine = line.Split(' '); + + if (currentLine.Length < 5 || line.StartsWith('#')) + { + continue; + } + + counter++; + var dto = new QuestNpcDto + { + Id = idCounter++, + NpcVnum = short.Parse(currentLine[0]), + Level = short.Parse(currentLine[4]) + }; + + bool isMainQuest = short.Parse(currentLine[1]) == 0; + if (isMainQuest) + { + dto.IsMainQuest = true; + dto.StartingScript = short.Parse(currentLine[2]); + dto.RequiredCompletedScript = short.Parse(currentLine[3]); + dto.MapId = short.Parse(currentLine[5]); + } + else + { + dto.QuestId = int.Parse(currentLine[2]); + dto.IsMainQuest = false; + } + + npcQuests.Add(dto); + } + + Log.Info($"[RESOURCE_LOADER] {npcQuests.Count.ToString()} NPC quests loaded"); + return npcQuests; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.ResourceLoader/Loaders/QuestResourceFileLoader.cs b/srcs/_plugins/Plugin.ResourceLoader/Loaders/QuestResourceFileLoader.cs new file mode 100644 index 0000000..8528cb5 --- /dev/null +++ b/srcs/_plugins/Plugin.ResourceLoader/Loaders/QuestResourceFileLoader.cs @@ -0,0 +1,189 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using CloneExtensions; +using PhoenixLib.Logging; +using WingsAPI.Data.GameData; +using WingsEmu.DTOs.Quests; +using WingsEmu.Packets.Enums; + +namespace Plugin.ResourceLoader.Loaders +{ + public class QuestResourceFileLoader : IResourceLoader + { + private readonly ResourceLoadingConfiguration _configuration; + private readonly List _quests = new(); + + public QuestResourceFileLoader(ResourceLoadingConfiguration configuration) => _configuration = configuration; + + public async Task> LoadAsync() + { + if (_quests.Any()) + { + return _quests; + } + + string fileQuestPath = Path.Combine(_configuration.GameDataPath, "quest.dat"); + string fileRewardsPath = Path.Combine(_configuration.GameDataPath, "qstprize.dat"); + + if (!File.Exists(fileQuestPath)) + { + throw new FileNotFoundException($"{fileQuestPath} should be present"); + } + + if (!File.Exists(fileRewardsPath)) + { + throw new FileNotFoundException($"{fileRewardsPath} should be present"); + } + + var dictionaryRewards = new Dictionary(); + string line; + + FillRewards(fileRewardsPath, dictionaryRewards); + + // Current + var quest = new QuestDto(); + + byte objectiveIndex = 0; + using var questStream = new StreamReader(fileQuestPath, Encoding.GetEncoding(1252)); + while ((line = await questStream.ReadLineAsync()) != null) + { + string[] currentLine = line.Split('\t'); + if (currentLine.Length > 1 || currentLine[0] == "END") + { + switch (currentLine[0]) + { + case "VNUM": + quest = new QuestDto + { + Id = int.Parse(currentLine[1]), + QuestType = (QuestType)int.Parse(currentLine[2]), + AutoFinish = Convert.ToInt32(currentLine[3]) == 0, + Unknown1 = Convert.ToInt32(currentLine[4]), + RequiredQuestId = Convert.ToInt32(currentLine[5]), + IsBlue = Convert.ToInt32(currentLine[6]) == 1 + }; + + objectiveIndex = 0; + break; + + case "LINK": + quest.NextQuestId = int.Parse(currentLine[1]); + break; + + case "LEVEL": + quest.MinLevel = byte.Parse(currentLine[1]); + quest.MaxLevel = byte.Parse(currentLine[2]); + break; + + case "TALK": + quest.DialogStarting = int.Parse(currentLine[1]); + quest.DialogFinish = int.Parse(currentLine[2]); + quest.TalkerVnum = int.Parse(currentLine[3]); + quest.DialogDuring = int.Parse(currentLine[4]); + break; + + case "TARGET": + quest.TargetMapX = short.Parse(currentLine[1]); + quest.TargetMapY = short.Parse(currentLine[2]); + quest.TargetMapId = short.Parse(currentLine[3]); + break; + + case "TITLE": + quest.Name = currentLine[1]; + break; + + case "DESC": + quest.Description = currentLine[1]; + break; + + case "DATA": + objectiveIndex++; + int data0 = int.Parse(currentLine[1]); + int data1 = int.Parse(currentLine[2]); + int data2 = int.Parse(currentLine[3]); + int data3 = int.Parse(currentLine[4]); + quest.Objectives.Add(new QuestObjectiveDto + { + Data0 = data0, + Data1 = data1, + Data2 = data2, + Data3 = data3, + ObjectiveIndex = objectiveIndex, + QuestId = quest.Id + }); + break; + + case "PRIZE": + for (int a = 1; a < 5; a++) + { + if (!dictionaryRewards.ContainsKey(long.Parse(currentLine[a]))) + { + continue; + } + + QuestPrizeDto currentReward = dictionaryRewards[long.Parse(currentLine[a])].GetClone(); + currentReward.QuestId = quest.Id; + quest.Prizes.Add(currentReward); + } + + break; + + case "END": + _quests.Add(quest); + break; + } + } + } + + questStream.Close(); + Log.Info($"[RESOURCE_LOADER] {_quests.Count.ToString()} Quests loaded"); + return _quests; + } + + private void FillRewards(string fileRewardsPath, Dictionary dictionaryRewards) + { + using var questRewardStream = new StreamReader(fileRewardsPath, Encoding.GetEncoding(1252)); + string line; + var reward = new QuestPrizeDto(); + int currentRewardId = 0; + while ((line = questRewardStream.ReadLine()) != null) + { + string[] currentLine = line.Split('\t'); + if (currentLine.Length <= 1 && currentLine[0] != "END") + { + continue; + } + + switch (currentLine[0]) + { + case "VNUM": + reward = new QuestPrizeDto + { + RewardType = byte.Parse(currentLine[2]) + }; + currentRewardId = int.Parse(currentLine[1]); + break; + + case "DATA": + reward.Data0 = int.Parse(currentLine[1]); + reward.Data1 = int.Parse(currentLine[2]); + reward.Data2 = int.Parse(currentLine[3]); + reward.Data3 = int.Parse(currentLine[4]); + reward.Data4 = int.Parse(currentLine[5]); + + break; + + case "END": + dictionaryRewards[currentRewardId] = reward; + break; + } + } + + questRewardStream.Close(); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.ResourceLoader/Loaders/SkillResourceFileLoader.cs b/srcs/_plugins/Plugin.ResourceLoader/Loaders/SkillResourceFileLoader.cs new file mode 100644 index 0000000..8a36a57 --- /dev/null +++ b/srcs/_plugins/Plugin.ResourceLoader/Loaders/SkillResourceFileLoader.cs @@ -0,0 +1,317 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using PhoenixLib.Logging; +using WingsAPI.Data.GameData; +using WingsEmu.DTOs.BCards; +using WingsEmu.DTOs.Skills; +using WingsEmu.Game._enum; +using WingsEmu.Packets.Enums.Battle; + +namespace Plugin.ResourceLoader.Loaders +{ + public class SkillResourceFileLoader : IResourceLoader + { + private readonly ResourceLoadingConfiguration _config; + private readonly ILogger _logger; + private readonly List _skills = new(); + + public SkillResourceFileLoader(ResourceLoadingConfiguration config, ILogger logger) + { + _config = config; + _logger = logger; + } + + public async Task> LoadAsync() + { + if (_skills.Any()) + { + return _skills; + } + + string filePath = Path.Combine(_config.GameDataPath, "Skill.dat"); + + if (!File.Exists(filePath)) + { + throw new FileNotFoundException($"{filePath} should be present"); + } + + + var skill = new SkillDTO(); + + int counter = 0; + using var skillIdStream = new StreamReader(filePath, Encoding.GetEncoding(1252)); + string line; + while ((line = await skillIdStream.ReadLineAsync()) != null) + { + string[] currentLine = line.Split('\t'); + + switch (currentLine.Length) + { + case > 2 when currentLine[1] == "VNUM": + skill = new SkillDTO + { + Id = short.Parse(currentLine[2]) + }; + break; + case > 2 when currentLine[1] == "NAME": + skill.Name = currentLine[2]; + break; + case > 2 when currentLine[1] == "TYPE": + skill.SkillType = (SkillType)byte.Parse(currentLine[2]); + skill.CastId = short.Parse(currentLine[3]); + skill.Class = byte.Parse(currentLine[4]); + skill.AttackType = (AttackType)byte.Parse(currentLine[5]); + skill.IsUsingSecondWeapon = currentLine[6] == "1"; + skill.Element = byte.Parse(currentLine[7]); + break; + case > 3 when currentLine[1] == "COST": + skill.CPCost = currentLine[2] == "-1" ? (byte)0 : byte.Parse(currentLine[2]); + skill.Price = int.Parse(currentLine[3]); + skill.SpecialCost = int.Parse(currentLine[4]); + break; + case > 2 when currentLine[1] == "LEVEL": + FillLevelInformation(skill, currentLine, _skills); + break; + case > 2 when currentLine[1] == "EFFECT": + skill.CtEffect = short.Parse(currentLine[3]); + skill.CtAnimation = short.Parse(currentLine[4]); + skill.SuEffect = short.Parse(currentLine[5]); + skill.SuAnimation = short.Parse(currentLine[6]); + break; + case > 2 when currentLine[1] == "TARGET": + skill.TargetType = (TargetType)byte.Parse(currentLine[2]); + skill.HitType = (TargetHitType)byte.Parse(currentLine[3]); + skill.Range = byte.Parse(currentLine[4]); + skill.AoERange = short.Parse(currentLine[5]); + skill.TargetAffectedEntities = (TargetAffectedEntities)byte.Parse(currentLine[6]); + break; + case > 2 when currentLine[1] == "DATA": + skill.UpgradeSkill = short.Parse(currentLine[2]); + skill.UpgradeType = short.Parse(currentLine[3]); + skill.CastTime = short.Parse(currentLine[6]); + skill.Cooldown = short.Parse(currentLine[7]); + skill.MpCost = short.Parse(currentLine[10]); + skill.DashSpeed = short.Parse(currentLine[11]); + skill.ItemVNum = short.Parse(currentLine[12]); + break; + case > 2 when currentLine[1] == "BASIC": + { + byte type = (byte)int.Parse(currentLine[3]); + if (type is 0 or 255) + { + continue; + } + + int first = int.Parse(currentLine[5]); + int second = int.Parse(currentLine[6]); + + int firstModulo = first % 4; + firstModulo = firstModulo switch + { + -1 => 1, + -2 => 2, + -3 => 1, + _ => firstModulo + }; + + int secondModulo = second % 4; + secondModulo = secondModulo switch + { + -1 => 1, + -2 => 2, + -3 => 1, + _ => secondModulo + }; + + var bcard = new BCardDTO + { + SkillVNum = skill.Id, + Type = type, + SubType = (byte)((int.Parse(currentLine[4]) + 1) * 10 + 1 + (first < 0 ? 1 : 0)), + FirstDataScalingType = (BCardScalingType)firstModulo, + SecondDataScalingType = (BCardScalingType)secondModulo, + FirstData = (int)Math.Abs(Math.Floor(first / 4.0)), + SecondData = (int)Math.Abs(Math.Floor(second / 4.0)), + CastType = byte.Parse(currentLine[7]) + }; + + skill.BCards.Add(bcard); + break; + } + case > 2 when currentLine[1] == "FCOMBO": + { + if (int.Parse(currentLine[2]) == 0) + { + continue; + } + + for (int i = 3; i < currentLine.Length - 4; i += 3) + { + var comb = new ComboDTO + { + SkillVNum = skill.Id, + Hit = short.Parse(currentLine[i]), + Animation = short.Parse(currentLine[i + 1]), + Effect = short.Parse(currentLine[i + 2]) + }; + + if (comb.Hit == 0 && comb.Animation == 0 && comb.Effect == 0) + { + continue; + } + + /* + * Idk if it's preferable to load them all instead of one by one (idk if there is lot of combo skills) + */ + skill.Combos.Add(comb); + } + + break; + } + case > 2 when currentLine[1] == "CELL": + // investigate + break; + case > 1 when currentLine[1] == "Z_DESC": + _skills.Add(skill); + counter++; + break; + } + } + + Log.Info($"[RESOURCE_LOADER] {counter.ToString()} Skills loaded"); + return _skills; + } + + private static void FillLevelInformation(SkillDTO skill, IReadOnlyList currentLine, IReadOnlyCollection skills) + { + skill.LevelMinimum = currentLine[2] != "-1" ? byte.Parse(currentLine[2]) : (byte)0; + if (skill.Class > 31) + { + SkillDTO firstSkill = skills.FirstOrDefault(s => s.Class == skill.Class); + if (firstSkill == null || skill.Id <= firstSkill.Id + 10) + { + switch (skill.Class) + { + case 8: + switch (skills.Count(s => s.Class == skill.Class)) + { + case 3: + skill.LevelMinimum = 20; + break; + + case 2: + skill.LevelMinimum = 10; + break; + + default: + skill.LevelMinimum = 0; + break; + } + + break; + + case 9: + switch (skills.Count(s => s.Class == skill.Class)) + { + case 9: + skill.LevelMinimum = 20; + break; + + case 8: + skill.LevelMinimum = 16; + break; + + case 7: + skill.LevelMinimum = 12; + break; + + case 6: + skill.LevelMinimum = 8; + break; + + case 5: + skill.LevelMinimum = 4; + break; + + default: + skill.LevelMinimum = 0; + break; + } + + break; + + case 16: + switch (skills.Count(s => s.Class == skill.Class)) + { + case 6: + skill.LevelMinimum = 20; + break; + + case 5: + skill.LevelMinimum = 15; + break; + + case 4: + skill.LevelMinimum = 10; + break; + + case 3: + skill.LevelMinimum = 5; + break; + + case 2: + skill.LevelMinimum = 3; + break; + + default: + skill.LevelMinimum = 0; + break; + } + + break; + + default: + switch (skills.Count(s => s.Class == skill.Class)) + { + case 10: + skill.LevelMinimum = 20; + break; + + case 9: + skill.LevelMinimum = 16; + break; + + case 8: + skill.LevelMinimum = 12; + break; + + case 7: + skill.LevelMinimum = 8; + break; + + case 6: + skill.LevelMinimum = 4; + break; + + default: + skill.LevelMinimum = 0; + break; + } + + break; + } + } + } + + skill.MinimumAdventurerLevel = currentLine[3] != "-1" ? byte.Parse(currentLine[3]) : (byte)0; + skill.MinimumSwordmanLevel = currentLine[4] != "-1" ? byte.Parse(currentLine[4]) : (byte)0; + skill.MinimumArcherLevel = currentLine[5] != "-1" ? byte.Parse(currentLine[5]) : (byte)0; + skill.MinimumMagicianLevel = currentLine[6] != "-1" ? byte.Parse(currentLine[6]) : (byte)0; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.ResourceLoader/Loaders/TutorialResourceFileLoader.cs b/srcs/_plugins/Plugin.ResourceLoader/Loaders/TutorialResourceFileLoader.cs new file mode 100644 index 0000000..dfd875d --- /dev/null +++ b/srcs/_plugins/Plugin.ResourceLoader/Loaders/TutorialResourceFileLoader.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using PhoenixLib.Logging; +using WingsAPI.Data.GameData; +using WingsEmu.DTOs.Quests; + +namespace Plugin.ResourceLoader.Loaders +{ + public class TutorialResourceFileLoader : IResourceLoader + { + private readonly ResourceLoadingConfiguration _configuration; + + public TutorialResourceFileLoader(ResourceLoadingConfiguration configuration) => _configuration = configuration; + + public async Task> LoadAsync() + { + string filePath = Path.Combine(_configuration.GameDataPath, "tutorial.dat"); + + if (!File.Exists(filePath)) + { + throw new FileNotFoundException($"{filePath} should be present"); + } + + var scriptDatas = new List(); + using var tutorialIdStream = new StreamReader(filePath, Encoding.GetEncoding(1252)); + string line; + int scriptId = 1; + int tutorialId = 1; + char[] splits = { ' ', '\t' }; + + while ((line = tutorialIdStream.ReadLine()) != null) + { + string[] currentLine = line.Split(splits, StringSplitOptions.RemoveEmptyEntries); + + if (currentLine.Length < 2 && !currentLine.Contains("end")) + { + continue; + } + + if (currentLine[0] == "end") + { + scriptId++; + continue; + } + + if (!int.TryParse(currentLine[0], out int index)) + { + continue; + } + + + string[] scriptIndexData = new string[currentLine.Length - 1]; + Array.Copy(currentLine, 1, scriptIndexData, 0, currentLine.Length - 1); + + if (scriptIndexData.Length < 2 && !scriptIndexData.Contains("targetoff")) + { + continue; + } + + string actionType = scriptIndexData[0]; + + TutorialActionType type = actionType switch + { + "talk" => TutorialActionType.TALK, + "quest" => TutorialActionType.START_QUEST, + "web" => TutorialActionType.WEB_DISPLAY, + "q_complete" => TutorialActionType.WAIT_FOR_QUEST_COMPLETION, + "openwin" => TutorialActionType.OPEN_WINDOW, + "q_pay" => TutorialActionType.WAIT_FOR_REWARDS_CLAIM, + "run" => TutorialActionType.RUN, + "time" => TutorialActionType.DELAY, + "target" => TutorialActionType.SHOW_TARGET, + "targetoff" => TutorialActionType.REMOVE_TARGET, + _ => TutorialActionType.NONE + }; + + int data = 0; + if (type != TutorialActionType.REMOVE_TARGET) + { + int.TryParse(scriptIndexData[1], out data); + } + + var dto = new TutorialDto + { + Id = tutorialId++, + ScriptId = scriptId, + ScriptIndex = index, + Type = type, + Data = data + }; + + scriptDatas.Add(dto); + } + + Log.Info($"[RESOURCE_LOADER] {scriptDatas.Count.ToString()} Tutorial Scripts loaded"); + return scriptDatas; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.ResourceLoader/Plugin.ResourceLoader.csproj b/srcs/_plugins/Plugin.ResourceLoader/Plugin.ResourceLoader.csproj new file mode 100644 index 0000000..f3fa26a --- /dev/null +++ b/srcs/_plugins/Plugin.ResourceLoader/Plugin.ResourceLoader.csproj @@ -0,0 +1,15 @@ + + + + net5.0 + + + + + + + + + + + diff --git a/srcs/_plugins/Plugin.ResourceLoader/ResourceLoadingConfiguration.cs b/srcs/_plugins/Plugin.ResourceLoader/ResourceLoadingConfiguration.cs new file mode 100644 index 0000000..807e7f7 --- /dev/null +++ b/srcs/_plugins/Plugin.ResourceLoader/ResourceLoadingConfiguration.cs @@ -0,0 +1,15 @@ +using System.IO; + +namespace Plugin.ResourceLoader +{ + public class ResourceLoadingConfiguration + { + public ResourceLoadingConfiguration(string resourcesPath) => ResourcePaths = resourcesPath; + + public string ResourcePaths { get; } + public string GameDataPath => Path.Combine(ResourcePaths, "dat"); + public string GameMapsPath => Path.Combine(ResourcePaths, "maps"); + public string GameLanguagePath => Path.Combine(ResourcePaths, "lang"); + public string GenericTranslationsPath => Path.Combine(ResourcePaths, "translations"); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.TimeSpaces/Commands/TimeSpaceAdminStartModule.cs b/srcs/_plugins/Plugin.TimeSpaces/Commands/TimeSpaceAdminStartModule.cs new file mode 100644 index 0000000..bce7496 --- /dev/null +++ b/srcs/_plugins/Plugin.TimeSpaces/Commands/TimeSpaceAdminStartModule.cs @@ -0,0 +1,55 @@ +using System.Threading.Tasks; +using Qmmands; +using WingsAPI.Scripting.ScriptManager; +using WingsEmu.Commands.Checks; +using WingsEmu.Commands.Entities; +using WingsEmu.DTOs.Account; +using WingsEmu.Game.TimeSpaces.Enums; +using WingsEmu.Game.TimeSpaces.Events; + +namespace Plugin.TimeSpaces.Commands; + +[Name("Admin-TimeSpaces")] +[Description("Module related to TimeSpaces Administrator commands.")] +[Group("ts", "timespace")] +[RequireAuthority(AuthorityType.GameAdmin)] +public class TimeSpaceAdminStartModule : SaltyModuleBase +{ + private readonly ITimeSpaceScriptManager _scriptManager; + + public TimeSpaceAdminStartModule(ITimeSpaceScriptManager scriptManager) => _scriptManager = scriptManager; + + [Command("start")] + public async Task StartTimeSpace(int timeSpaceId = 0) + { + Context.Player.EmitEvent(new TimeSpacePartyCreateEvent(timeSpaceId)); + if (!Context.Player.PlayerEntity.TimeSpaceComponent.IsInTimeSpaceParty) + { + return new SaltyCommandResult(false, "Failed"); + } + + Context.Player.EmitEvent(new TimeSpaceInstanceStartEvent()); + + return new SaltyCommandResult(true, $"Started TimeSpace with Id {timeSpaceId}"); + } + + [Command("end")] + public async Task End(TimeSpaceFinishType type) + { + if (!Context.Player.PlayerEntity.TimeSpaceComponent.IsInTimeSpaceParty) + { + return new SaltyCommandResult(false, "Failed"); + } + + await Context.Player.EmitEventAsync(new TimeSpaceInstanceFinishEvent(Context.Player.PlayerEntity.TimeSpaceComponent.TimeSpace, type)); + return new SaltyCommandResult(true); + } + + + [Command("reload")] + public async Task ReloadScripts() + { + _scriptManager.Load(); + return new SaltyCommandResult(true, "Reloaded TimeSpace scripts"); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.TimeSpaces/Handlers/RevivalAskEventTimeSpaceHandler.cs b/srcs/_plugins/Plugin.TimeSpaces/Handlers/RevivalAskEventTimeSpaceHandler.cs new file mode 100644 index 0000000..c93a7b0 --- /dev/null +++ b/srcs/_plugins/Plugin.TimeSpaces/Handlers/RevivalAskEventTimeSpaceHandler.cs @@ -0,0 +1,38 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Revival; + +namespace Plugin.TimeSpaces.Handlers; + +public class RevivalAskEventTimeSpaceHandler : IAsyncEventProcessor +{ + public async Task HandleAsync(RevivalAskEvent e, CancellationToken cancellation) + { + if (e.Sender.PlayerEntity.IsAlive()) + { + return; + } + + if (e.AskRevivalType != AskRevivalType.TimeSpaceRevival) + { + return; + } + + if (!e.Sender.PlayerEntity.TimeSpaceComponent.IsInTimeSpaceParty) + { + return; + } + + if (e.Sender.PlayerEntity.TimeSpaceComponent.TimeSpace.Finished) + { + return; + } + + e.Sender.SendDialog(CharacterPacketExtension.GenerateRevivalPacket(RevivalType.DontPayRevival), CharacterPacketExtension.GenerateRevivalPacket(RevivalType.DontPayRevival), + e.Sender.GetLanguageFormat(GameDialogKey.TIMESPACE_DIALOG_ASK_REVIVAL)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.TimeSpaces/Handlers/RevivalEventTimeSpaceHandler.cs b/srcs/_plugins/Plugin.TimeSpaces/Handlers/RevivalEventTimeSpaceHandler.cs new file mode 100644 index 0000000..b95d2ac --- /dev/null +++ b/srcs/_plugins/Plugin.TimeSpaces/Handlers/RevivalEventTimeSpaceHandler.cs @@ -0,0 +1,48 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.Groups; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage.Configuration; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Revival; + +namespace Plugin.TimeSpaces.Handlers; + +public class RevivalEventTimeSpaceHandler : IAsyncEventProcessor +{ + private readonly IGameLanguageService _gameLanguage; + private readonly ISpPartnerConfiguration _spPartner; + + public RevivalEventTimeSpaceHandler(IGameLanguageService gameLanguage, ISpPartnerConfiguration spPartner) + { + _gameLanguage = gameLanguage; + _spPartner = spPartner; + } + + public async Task HandleAsync(RevivalReviveEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + + if (session.CurrentMapInstance is not { MapInstanceType: MapInstanceType.TimeSpaceInstance }) + { + return; + } + + if (session.PlayerEntity.IsAlive()) + { + return; + } + + e.Sender.UpdateVisibility(); + await e.Sender.PlayerEntity.Restore(restoreMates: false); + e.Sender.BroadcastRevive(); + e.Sender.BroadcastInTeamMembers(_gameLanguage, _spPartner); + e.Sender.RefreshParty(_spPartner); + await e.Sender.CheckPartnerBuff(); + e.Sender.SendBuffsPacket(); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.TimeSpaces/Handlers/RevivalStartProcedureEventTimeSpaceHandler.cs b/srcs/_plugins/Plugin.TimeSpaces/Handlers/RevivalStartProcedureEventTimeSpaceHandler.cs new file mode 100644 index 0000000..e69fb4b --- /dev/null +++ b/srcs/_plugins/Plugin.TimeSpaces/Handlers/RevivalStartProcedureEventTimeSpaceHandler.cs @@ -0,0 +1,37 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Revival; + +namespace Plugin.TimeSpaces.Handlers; + +public class RevivalStartProcedureEventTimeSpaceHandler : IAsyncEventProcessor +{ + public async Task HandleAsync(RevivalStartProcedureEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + + if (!session.PlayerEntity.TimeSpaceComponent.IsInTimeSpaceParty) + { + return; + } + + if (session.CurrentMapInstance.MapInstanceType != MapInstanceType.TimeSpaceInstance) + { + return; + } + + if (session.PlayerEntity.IsAlive()) + { + return; + } + + DateTime currentTime = DateTime.UtcNow; + e.Sender.PlayerEntity.UpdateRevival(currentTime + TimeSpan.FromSeconds(10), RevivalType.DontPayRevival, ForcedType.Reconnect); + e.Sender.PlayerEntity.UpdateAskRevival(currentTime + TimeSpan.FromSeconds(2), AskRevivalType.TimeSpaceRevival); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceAddTimeToTimerEventHandler.cs b/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceAddTimeToTimerEventHandler.cs new file mode 100644 index 0000000..9593123 --- /dev/null +++ b/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceAddTimeToTimerEventHandler.cs @@ -0,0 +1,45 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Game.TimeSpaces; +using WingsEmu.Game.TimeSpaces.Events; + +namespace Plugin.TimeSpaces.Handlers; + +public class TimeSpaceAddTimeToTimerEventHandler : IAsyncEventProcessor +{ + private readonly ITimeSpaceManager _timeSpaceManager; + + public TimeSpaceAddTimeToTimerEventHandler(ITimeSpaceManager timeSpaceManager) => _timeSpaceManager = timeSpaceManager; + + public async Task HandleAsync(TimeSpaceAddTimeToTimerEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + TimeSpan time = e.Time; + + if (session != null && !session.PlayerEntity.TimeSpaceComponent.IsInTimeSpaceParty) + { + return; + } + + TimeSpaceParty timeSpace = session != null ? session.PlayerEntity.TimeSpaceComponent.TimeSpace : e.TimeSpaceParty; + if (timeSpace?.Instance == null) + { + return; + } + + if (!timeSpace.Started || timeSpace.Finished || timeSpace.Instance.InfiniteDuration) + { + return; + } + + timeSpace.Instance.AddTimeToFinishDate(time); + foreach (IClientSession member in timeSpace.Members) + { + member.SendTsClockPacket(timeSpace.Instance.TimeUntilEnd, true); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceBonusMonsterEventHandler.cs b/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceBonusMonsterEventHandler.cs new file mode 100644 index 0000000..1771264 --- /dev/null +++ b/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceBonusMonsterEventHandler.cs @@ -0,0 +1,44 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game; +using WingsEmu.Game.Entities; +using WingsEmu.Game.TimeSpaces; +using WingsEmu.Game.TimeSpaces.Events; +using WingsEmu.Packets.Enums; + +namespace Plugin.TimeSpaces.Handlers; + +public class TimeSpaceBonusMonsterEventHandler : IAsyncEventProcessor +{ + private readonly IRandomGenerator _randomGenerator; + + public TimeSpaceBonusMonsterEventHandler(IRandomGenerator randomGenerator) => _randomGenerator = randomGenerator; + + public async Task HandleAsync(TimeSpaceBonusMonsterEvent e, CancellationToken cancellation) + { + IMonsterEntity[] monsters = e.MonsterEntities.Where(x => x.SummonerType is not VisualType.Player).ToArray(); + TimeSpaceSubInstance timeSpaceSubInstance = e.TimeSpaceSubInstance; + if (!monsters.Any()) + { + return; + } + + IMonsterEntity anotherMonster = monsters.FirstOrDefault(x => x.IsBonus); + if (anotherMonster != null) + { + return; + } + + int randomNumber = _randomGenerator.RandomNumber(monsters.Length); + IMonsterEntity mob = monsters[randomNumber]; + if (mob == null) + { + return; + } + + mob.IsBonus = true; + timeSpaceSubInstance.MonsterBonusId = mob.Id; + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceCheckForTasksCompletedEventHandler.cs b/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceCheckForTasksCompletedEventHandler.cs new file mode 100644 index 0000000..daf7f3b --- /dev/null +++ b/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceCheckForTasksCompletedEventHandler.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.TimeSpaces; +using WingsEmu.Game.TimeSpaces.Events; + +namespace Plugin.TimeSpaces.Handlers; + +public class TimeSpaceCheckForTasksCompletedEventHandler : IAsyncEventProcessor +{ + private readonly IAsyncEventPipeline _asyncEventPipeline; + + public TimeSpaceCheckForTasksCompletedEventHandler(IAsyncEventPipeline asyncEventPipeline) => _asyncEventPipeline = asyncEventPipeline; + + public async Task HandleAsync(TimeSpaceCheckForTasksCompletedEvent e, CancellationToken cancellation) + { + if (e.Completed) + { + return; + } + + IEnumerable timeSpaceSubInstances = e.TimeSpaceSubInstances; + IEnumerable events = e.Events; + + bool isFinished = true; + + foreach (TimeSpaceSubInstance timeSpaceSubInstance in timeSpaceSubInstances) + { + if (timeSpaceSubInstance.Task == null) + { + continue; + } + + if (timeSpaceSubInstance.Task.IsFinished) + { + continue; + } + + isFinished = false; + break; + } + + if (!isFinished) + { + return; + } + + e.Completed = true; + foreach (IAsyncEvent asyncEvent in events) + { + await _asyncEventPipeline.ProcessEventAsync(asyncEvent); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceCheckMonsterEventHandler.cs b/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceCheckMonsterEventHandler.cs new file mode 100644 index 0000000..6a4a4c8 --- /dev/null +++ b/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceCheckMonsterEventHandler.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Entities.Event; +using WingsEmu.Game.TimeSpaces; +using WingsEmu.Game.TimeSpaces.Events; + +namespace Plugin.TimeSpaces.Handlers; + +public class TimeSpaceCheckMonsterEventHandler : IAsyncEventProcessor +{ + private readonly ITimeSpaceManager _timeSpaceManager; + + public TimeSpaceCheckMonsterEventHandler(ITimeSpaceManager timeSpaceManager) => _timeSpaceManager = timeSpaceManager; + + public async Task HandleAsync(TimeSpaceCheckMonsterEvent e, CancellationToken cancellation) + { + Guid guid = e.MonsterEntity.MapInstance.Id; + + TimeSpaceSubInstance timeSpaceSubInstance = _timeSpaceManager.GetSubInstance(guid); + if (timeSpaceSubInstance == null) + { + return; + } + + await CheckTask(timeSpaceSubInstance); + if (timeSpaceSubInstance.SpawnAfterMobsKilled.Count < 1) + { + return; + } + + long killedMonsters = timeSpaceSubInstance.MapInstance.MonsterDeathsOnMap(); + HashSet toRemove = new(); + foreach ((int toSpawn, List monsters) in timeSpaceSubInstance.SpawnAfterMobsKilled) + { + if (monsters == null) + { + continue; + } + + if (toSpawn > killedMonsters) + { + continue; + } + + foreach (IMonsterEntity monster in monsters) + { + await monster.EmitEventAsync(new MapJoinMonsterEntityEvent(monster, monster.Position.X, monster.Position.Y, true)); + } + + toRemove.Add(toSpawn); + } + + foreach (int toSpawn in toRemove) + { + timeSpaceSubInstance.SpawnAfterMobsKilled.Remove(toSpawn); + } + } + + private async Task CheckTask(TimeSpaceSubInstance instance) + { + TimeSpaceTask task = instance.Task; + + if (task == null) + { + return; + } + + if (task.IsFinished) + { + return; + } + + if (!task.IsActivated) + { + return; + } + + if (task.MonstersAfterTaskStart.Count < 1 || task.MonstersAfterTaskStart.All(x => x.Item1 == null)) + { + return; + } + + long killedMonsters = instance.MapInstance.MonsterDeathsOnMap(); + + var toRemove = new List<(int?, IMonsterEntity)>(); + foreach ((int? toKill, IMonsterEntity monster) in task.MonstersAfterTaskStart) + { + if (!toKill.HasValue) + { + continue; + } + + if (toKill.Value > killedMonsters) + { + continue; + } + + await monster.EmitEventAsync(new MapJoinMonsterEntityEvent(monster, monster.Position.X, monster.Position.Y, true)); + toRemove.Add((toKill.Value, monster)); + } + + foreach ((int? toKill, IMonsterEntity monster) in toRemove) + { + task.MonstersAfterTaskStart.Remove((toKill, monster)); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceCheckObjectivesEventHandler.cs b/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceCheckObjectivesEventHandler.cs new file mode 100644 index 0000000..6dcdec6 --- /dev/null +++ b/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceCheckObjectivesEventHandler.cs @@ -0,0 +1,101 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Game.TimeSpaces; +using WingsEmu.Game.TimeSpaces.Enums; +using WingsEmu.Game.TimeSpaces.Events; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.TimeSpaces.Handlers; + +public class TimeSpaceCheckObjectivesEventHandler : IAsyncEventProcessor +{ + private readonly IAsyncEventPipeline _asyncEventPipeline; + + public TimeSpaceCheckObjectivesEventHandler(IAsyncEventPipeline asyncEventPipeline) => _asyncEventPipeline = asyncEventPipeline; + + public async Task HandleAsync(TimeSpaceCheckObjectivesEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + TimeSpaceParty timeSpace = e.TimeSpaceParty; + TimeSpaceObjective objectives = timeSpace.Instance.TimeSpaceObjective; + bool playerEnteredToEndPortal = e.PlayerEnteredToEndPortal; + + if (timeSpace.Finished) + { + return; + } + + bool finishTimeSpace = true; + + if (objectives.KillAllMonsters) + { + foreach (TimeSpaceSubInstance map in timeSpace.Instance.TimeSpaceSubInstances.Values) + { + if (map.MapInstance.GetAliveMonsters(m => m.SummonerType is not VisualType.Player).Count == 0) + { + if (map.SpawnAfterMobsKilled.Count == 0 && map.Task?.MonstersAfterTaskStart.Count == 0) + { + continue; + } + } + + finishTimeSpace = false; + break; + } + } + + if (objectives.KillMonsterAmount.HasValue) + { + finishTimeSpace = objectives.KillMonsterAmount.Value == objectives.KilledMonsterAmount; + } + + if (objectives.CollectItemAmount.HasValue) + { + finishTimeSpace = objectives.CollectItemAmount.Value == objectives.CollectedItemAmount; + } + + if (objectives.Conversation.HasValue) + { + finishTimeSpace = objectives.Conversation.Value == objectives.ConversationsHad; + } + + if (objectives.InteractObjectsAmount.HasValue) + { + finishTimeSpace = objectives.InteractObjectsAmount.Value == objectives.InteractedObjectsAmount; + } + + if (!finishTimeSpace) + { + if (e.SendMessageWithNotFinishedObjects && session != null) + { + session.SendChatMessage(session.GetLanguage(GameDialogKey.TIMESPACE_CHATMESSAGE_NOT_FINISHED_TASKS), ChatMessageColorType.PlayerSay); + } + + return; + } + + if (objectives.GoToExit && !playerEnteredToEndPortal) + { + return; + } + + if (timeSpace.Instance.PreFinishDialog.HasValue) + { + if (timeSpace.Instance.PreFinishDialogTime.HasValue) + { + return; + } + + timeSpace.Instance.PreFinishDialogTime = DateTime.UtcNow; + return; + } + + await _asyncEventPipeline.ProcessEventAsync(new TimeSpaceInstanceFinishEvent(timeSpace, TimeSpaceFinishType.SUCCESS)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceClosePortalEventHandler.cs b/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceClosePortalEventHandler.cs new file mode 100644 index 0000000..dba2195 --- /dev/null +++ b/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceClosePortalEventHandler.cs @@ -0,0 +1,28 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.PacketGeneration; +using WingsEmu.Game; +using WingsEmu.Game.TimeSpaces.Events; +using WingsEmu.Packets.Enums; + +namespace Plugin.TimeSpaces.Handlers; + +public class TimeSpaceClosePortalEventHandler : IAsyncEventProcessor +{ + public async Task HandleAsync(TimeSpaceClosePortalEvent e, CancellationToken cancellation) + { + IPortalEntity portal = e.PortalEntity; + + PortalType type = portal.Type switch + { + PortalType.Open => PortalType.Closed, + PortalType.TSEnd => PortalType.TSEndClosed, + _ => PortalType.Closed + }; + + portal.Type = type; + portal.MapInstance.MapClear(true); + portal.MapInstance.BroadcastTimeSpacePartnerInfo(); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceDeathEventHandler.cs b/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceDeathEventHandler.cs new file mode 100644 index 0000000..3308f95 --- /dev/null +++ b/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceDeathEventHandler.cs @@ -0,0 +1,27 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Networking; +using WingsEmu.Game.TimeSpaces.Events; + +namespace Plugin.TimeSpaces.Handlers; + +public class TimeSpaceTimeSpaceDeathEventHandler : IAsyncEventProcessor +{ + public async Task HandleAsync(TimeSpaceDeathEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + if (!session.PlayerEntity.TimeSpaceComponent.IsInTimeSpaceParty) + { + return; + } + + if (session.CurrentMapInstance.MapInstanceType != MapInstanceType.TimeSpaceInstance) + { + return; + } + + await e.Sender.EmitEventAsync(new TimeSpaceDecreaseLiveEvent()); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceDecreaseLiveEventHandler.cs b/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceDecreaseLiveEventHandler.cs new file mode 100644 index 0000000..3ff7a01 --- /dev/null +++ b/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceDecreaseLiveEventHandler.cs @@ -0,0 +1,50 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Networking; +using WingsEmu.Game.TimeSpaces; +using WingsEmu.Game.TimeSpaces.Enums; +using WingsEmu.Game.TimeSpaces.Events; + +namespace Plugin.TimeSpaces.Handlers; + +public class TimeSpaceDecreaseLiveEventHandler : IAsyncEventProcessor +{ + private readonly ITimeSpaceManager _timeSpaceManager; + + public TimeSpaceDecreaseLiveEventHandler(ITimeSpaceManager timeSpaceManager) => _timeSpaceManager = timeSpaceManager; + + public async Task HandleAsync(TimeSpaceDecreaseLiveEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + + if (!session.PlayerEntity.TimeSpaceComponent.IsInTimeSpaceParty) + { + return; + } + + TimeSpaceParty timeSpace = session.PlayerEntity.TimeSpaceComponent.TimeSpace; + if (timeSpace == null) + { + return; + } + + if (timeSpace.Finished) + { + return; + } + + if (!timeSpace.Started) + { + return; + } + + timeSpace.Instance.IncreaseOrDecreaseLives(-1); + if (timeSpace.Instance.Lives > 0) + { + return; + } + + await session.EmitEventAsync(new TimeSpaceInstanceFinishEvent(timeSpace, TimeSpaceFinishType.OUT_OF_LIVES, session.PlayerEntity.Id)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceDespawnMonstersInRoomEventHandler.cs b/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceDespawnMonstersInRoomEventHandler.cs new file mode 100644 index 0000000..d3d956e --- /dev/null +++ b/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceDespawnMonstersInRoomEventHandler.cs @@ -0,0 +1,40 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Entities.Extensions; +using WingsEmu.Game.Monster.Event; +using WingsEmu.Game.TimeSpaces; +using WingsEmu.Game.TimeSpaces.Events; +using WingsEmu.Packets.Enums; + +namespace Plugin.TimeSpaces.Handlers; + +public class TimeSpaceDespawnMonstersInRoomEventHandler : IAsyncEventProcessor +{ + private readonly IAsyncEventPipeline _asyncEventPipeline; + + public TimeSpaceDespawnMonstersInRoomEventHandler(IAsyncEventPipeline asyncEventPipeline) => _asyncEventPipeline = asyncEventPipeline; + + public async Task HandleAsync(TimeSpaceDespawnMonstersInRoomEvent e, CancellationToken cancellation) + { + TimeSpaceSubInstance timeSpaceSubInstance = e.TimeSpaceSubInstance; + + if (timeSpaceSubInstance == null) + { + return; + } + + timeSpaceSubInstance.Task?.MonstersAfterTaskStart.Clear(); + timeSpaceSubInstance.SpawnAfterMobsKilled.Clear(); + + foreach (IMonsterEntity monster in timeSpaceSubInstance.MapInstance.GetAliveMonsters(x => x.SummonerType is not VisualType.Player)) + { + monster.MapInstance.Broadcast(monster.GenerateOut()); + await _asyncEventPipeline.ProcessEventAsync(new MonsterDeathEvent(monster) + { + IsByCommand = true + }); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceDestroyEventHandler.cs b/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceDestroyEventHandler.cs new file mode 100644 index 0000000..43b3da1 --- /dev/null +++ b/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceDestroyEventHandler.cs @@ -0,0 +1,49 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Revival; +using WingsEmu.Game.TimeSpaces; +using WingsEmu.Game.TimeSpaces.Events; + +namespace Plugin.TimeSpaces.Handlers; + +public class TimeSpaceDestroyEventHandler : IAsyncEventProcessor +{ + private readonly IMapManager _mapManager; + private readonly ITimeSpaceManager _timeSpaceManager; + + public TimeSpaceDestroyEventHandler(IMapManager mapManager, ITimeSpaceManager timeSpaceManager) + { + _mapManager = mapManager; + _timeSpaceManager = timeSpaceManager; + } + + public async Task HandleAsync(TimeSpaceDestroyEvent e, CancellationToken cancellation) + { + TimeSpaceParty timeSpaceParty = e.TimeSpace; + _timeSpaceManager.RemoveTimeSpace(timeSpaceParty); + foreach (TimeSpaceSubInstance subInstance in timeSpaceParty.Instance.TimeSpaceSubInstances.Values) + { + Guid mapInstanceId = subInstance.MapInstance.Id; + _timeSpaceManager.RemoveTimeSpacePartyByMapInstanceId(mapInstanceId); + _timeSpaceManager.RemoveTimeSpaceSubInstance(mapInstanceId); + foreach (IClientSession session in subInstance.MapInstance.Sessions) + { + session.EmitEvent(new TimeSpaceLeavePartyEvent()); + if (!session.PlayerEntity.IsAlive()) + { + await session.EmitEventAsync(new RevivalReviveEvent()); + } + + session.ChangeToLastBaseMap(); + } + + _mapManager.RemoveMapInstance(subInstance.MapInstance.Id); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceGroupTryJoinEventHandler.cs b/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceGroupTryJoinEventHandler.cs new file mode 100644 index 0000000..a51a0c9 --- /dev/null +++ b/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceGroupTryJoinEventHandler.cs @@ -0,0 +1,167 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsAPI.Game.Extensions.ItemExtension.Item; +using WingsAPI.Game.Extensions.PacketGeneration; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Portals; +using WingsEmu.Game.TimeSpaces; +using WingsEmu.Game.TimeSpaces.Enums; +using WingsEmu.Game.TimeSpaces.Events; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.TimeSpaces.Handlers; + +public class TimeSpaceGroupTryJoinEventHandler : IAsyncEventProcessor +{ + private readonly IGameLanguageService _gameLanguage; + private readonly IItemsManager _itemsManager; + private readonly ISubActConfiguration _subActConfiguration; + private readonly ITimeSpaceConfiguration _timeSpaceConfiguration; + + public TimeSpaceGroupTryJoinEventHandler(IItemsManager itemsManager, IGameLanguageService gameLanguage, ISubActConfiguration subActConfiguration, ITimeSpaceConfiguration timeSpaceConfiguration) + { + _itemsManager = itemsManager; + _gameLanguage = gameLanguage; + _subActConfiguration = subActConfiguration; + _timeSpaceConfiguration = timeSpaceConfiguration; + } + + public async Task HandleAsync(TimeSpaceGroupTryJoinEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + ITimeSpacePortalEntity portal = e.PortalEntity; + long characterId = e.CharacterId; + + if (session.PlayerEntity.Id == characterId) + { + return; + } + + if (!session.PlayerEntity.IsInGroup()) + { + return; + } + + if (session.PlayerEntity.IsInRaidParty) + { + return; + } + + if (session.IsMuted()) + { + session.SendMsg(session.GetLanguage(GameDialogKey.MUTE_SHOUTMESSAGE_YOU_ARE_MUTED), MsgMessageType.Middle); + return; + } + + if (session.PlayerEntity.TimeSpaceComponent.IsInTimeSpaceParty) + { + return; + } + + IPlayerEntity groupMember = session.PlayerEntity.GetGroup().Members.FirstOrDefault(x => x.Id == characterId); + if (groupMember == null) + { + return; + } + + if (!groupMember.TimeSpaceComponent.IsInTimeSpaceParty) + { + return; + } + + TimeSpaceParty timeSpace = groupMember.TimeSpaceComponent.TimeSpace; + + if (timeSpace.Instance == null) + { + return; + } + + if (timeSpace.TimeSpaceId != portal.TimeSpaceId) + { + return; + } + + if (groupMember.TimeSpaceComponent.TimeSpaceTeamIsFull) + { + session.SendMsg(session.GetLanguage(GameDialogKey.TIMESPACE_SHOUTMESSAGE_TS_FULL), MsgMessageType.Middle); + return; + } + + if (timeSpace.Started || timeSpace.Finished) + { + session.SendMsg(session.GetLanguage(GameDialogKey.TIMESPACE_SHOUTMESSAGE_ALREADY_STARTED), MsgMessageType.Middle); + return; + } + + if (session.PlayerEntity.Level < timeSpace.TimeSpaceInformation.MinLevel) + { + session.SendChatMessage(session.GetLanguage(GameDialogKey.TIMESPACE_CHATMESSAGE_LOW_LEVEL), ChatMessageColorType.PlayerSay); + return; + } + + if (!session.CanJoinToTimeSpace(timeSpace.TimeSpaceId, _subActConfiguration, _timeSpaceConfiguration) && !session.IsGameMaster() + && !timeSpace.TimeSpaceInformation.IsSpecial && !timeSpace.TimeSpaceInformation.IsHidden) + { + session.SendMsg(session.GetLanguage(GameDialogKey.TIMESPACE_SHOUTMESSAGE_WRONG_ACT), MsgMessageType.Middle); + return; + } + + if (session.PlayerEntity.Level > timeSpace.TimeSpaceInformation.MaxLevel) + { + session.SendChatMessage(session.GetLanguage(GameDialogKey.TIMESPACE_CHATMESSAGE_HIGH_LEVEL), ChatMessageColorType.PlayerSay); + return; + } + + if (timeSpace.TimeSpaceInformation.SeedsOfPowerRequired != 0) + { + if (!session.PlayerEntity.HasItem((short)ItemVnums.SEED_OF_POWER, timeSpace.TimeSpaceInformation.SeedsOfPowerRequired)) + { + string itemName = _itemsManager.GetItem((short)ItemVnums.SEED_OF_POWER).GetItemName(_gameLanguage, session.UserLanguage); + session.SendChatMessage(session.GetLanguageFormat(GameDialogKey.INVENTORY_SHOUTMESSAGE_NOT_ENOUGH_ITEMS, + timeSpace.TimeSpaceInformation.SeedsOfPowerRequired, itemName), ChatMessageColorType.PlayerSay); + return; + } + + await session.RemoveItemFromInventory((short)ItemVnums.SEED_OF_POWER, timeSpace.TimeSpaceInformation.SeedsOfPowerRequired); + } + + session.PlayerEntity.TimeSpaceComponent.SetTimeSpaceParty(timeSpace); + timeSpace.AddMember(session); + + IMapInstance mapStart = timeSpace.Instance.SpawnInstance.MapInstance; + + session.ChangeMap(mapStart, timeSpace.Instance.SpawnPoint.X, timeSpace.Instance.SpawnPoint.Y); + session.SendPacket(mapStart.GenerateRsfn(true, false)); + foreach (KeyValuePair mapInstance in timeSpace.Instance.TimeSpaceSubInstances) + { + session.SendPacket(mapInstance.Value.MapInstance.GenerateRsfn(isVisit: false)); + } + + session.SendRsfpPacket(); + mapStart.MapClear(true); + mapStart.BroadcastTimeSpacePartnerInfo(); + session.SendRsfmPacket(TimeSpaceAction.CAMERA_ADJUST); + session.SendMinfo(); + if (timeSpace.IsEasyMode) + { + session.SendChatMessage(session.GetLanguage(GameDialogKey.TIMESPACE_CHATMESSAGE_EASY_MODE), ChatMessageColorType.Yellow); + } + + if (session.PlayerEntity.Level > timeSpace.HigherLevel) + { + timeSpace.HigherLevel = session.PlayerEntity.Level; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceIncreaseScoreEventHandler.cs b/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceIncreaseScoreEventHandler.cs new file mode 100644 index 0000000..456fd82 --- /dev/null +++ b/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceIncreaseScoreEventHandler.cs @@ -0,0 +1,42 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Networking; +using WingsEmu.Game.TimeSpaces; +using WingsEmu.Game.TimeSpaces.Events; + +namespace Plugin.TimeSpaces.Handlers; + +public class TimeSpaceIncreaseScoreEventHandler : IAsyncEventProcessor +{ + private readonly ITimeSpaceManager _timeSpaceManager; + + public TimeSpaceIncreaseScoreEventHandler(ITimeSpaceManager timeSpaceManager) => _timeSpaceManager = timeSpaceManager; + + public async Task HandleAsync(TimeSpaceIncreaseScoreEvent e, CancellationToken cancellation) + { + int amountToIncrease = e.AmountToIncrease; + + TimeSpaceParty timeSpaceParty = null; + if (e.TimeSpaceParty != null) + { + timeSpaceParty = e.TimeSpaceParty; + } + + if (timeSpaceParty == null) + { + return; + } + + if (!timeSpaceParty.Started || timeSpaceParty.Finished) + { + return; + } + + timeSpaceParty.Instance.IncreaseScoreByAmount(amountToIncrease); + foreach (IClientSession member in timeSpaceParty.Members) + { + member.RefreshTimespaceScoreUi(); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceInstanceFinishEventHandler.cs b/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceInstanceFinishEventHandler.cs new file mode 100644 index 0000000..c122e64 --- /dev/null +++ b/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceInstanceFinishEventHandler.cs @@ -0,0 +1,276 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Communication.DbServer.TimeSpaceService; +using WingsAPI.Data.TimeSpace; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.DTOs.Quests; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Quests; +using WingsEmu.Game.Quests.Event; +using WingsEmu.Game.TimeSpaces; +using WingsEmu.Game.TimeSpaces.Enums; +using WingsEmu.Game.TimeSpaces.Events; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.TimeSpaces.Handlers; + +public class TimeSpaceInstanceFinishEventHandler : IAsyncEventProcessor +{ + private static readonly HashSet TimeSpaceCompletionQuestTypes = new() { QuestType.COMPLETE_TIMESPACE, QuestType.COMPLETE_TIMESPACE_WITH_ATLEAST_X_POINTS }; + private readonly IGameLanguageService _gameLanguageService; + private readonly ISubActConfiguration _subActConfiguration; + private readonly ITimeSpaceConfiguration _timeSpaceConfiguration; + private readonly ITimeSpaceService _timeSpaceService; + + public TimeSpaceInstanceFinishEventHandler(IGameLanguageService gameLanguageService, ITimeSpaceService timeSpaceService, ISubActConfiguration subActConfiguration, + ITimeSpaceConfiguration timeSpaceConfiguration) + { + _gameLanguageService = gameLanguageService; + _timeSpaceService = timeSpaceService; + _subActConfiguration = subActConfiguration; + _timeSpaceConfiguration = timeSpaceConfiguration; + } + + public async Task HandleAsync(TimeSpaceInstanceFinishEvent e, CancellationToken cancellation) + { + TimeSpaceParty timeSpaceParty = e.TimeSpaceParty; + long? victimId = e.VictimId; + + if (timeSpaceParty == null || timeSpaceParty.Finished) + { + return; + } + + switch (e.TimeSpaceFinishType) + { + case TimeSpaceFinishType.SUCCESS: + await ProcessSuccessTimeSpace(timeSpaceParty); + break; + default: + ProcessFailTimeSpace(timeSpaceParty, e.TimeSpaceFinishType, victimId); + break; + } + + foreach (TimeSpaceSubInstance subInstance in timeSpaceParty.Instance.TimeSpaceSubInstances.Values) + { + subInstance.MapInstance.AIDisabled = true; + subInstance.TimeSpaceWaves.Clear(); + foreach (IMonsterEntity monsterEntity in subInstance.MapInstance.GetAliveMonsters()) + { + subInstance.MapInstance.DespawnMonster(monsterEntity); + subInstance.MapInstance.RemoveMonster(monsterEntity); + } + } + + timeSpaceParty.Destroy = true; + timeSpaceParty.FinishTimeSpace(DateTime.UtcNow.AddMinutes(1)); + } + + private void ProcessFailTimeSpace(TimeSpaceParty timeSpaceParty, TimeSpaceFinishType timeSpaceFinishType, long? victimId = null) + { + foreach (IClientSession member in timeSpaceParty.Members) + { + member.SendRemoveRedClockPacket(); + + if (victimId.HasValue && member.PlayerEntity.Id != victimId.Value && timeSpaceFinishType == TimeSpaceFinishType.OUT_OF_LIVES) + { + member.SendScorePacket(TimeSpaceFinishType.TEAM_MEMBER_OUT_OF_LIVES); + continue; + } + + member.SendScorePacket(timeSpaceFinishType); + } + } + + private async Task ProcessSuccessTimeSpace(TimeSpaceParty timeSpaceParty) + { + CalculatePenalty(timeSpaceParty); + bool isNewRecord = false; + + if (timeSpaceParty.IsChallengeMode) + { + TimeSpaceIsNewRecordResponse newRecord = await _timeSpaceService.IsNewRecord(new TimeSpaceIsNewRecordRequest + { + TimeSpaceId = timeSpaceParty.TimeSpaceId, + Record = timeSpaceParty.Instance.Score + }); + isNewRecord = newRecord.IsNewRecord; + } + + bool perfectMonsters = true; + + foreach (TimeSpaceSubInstance map in timeSpaceParty.Instance.TimeSpaceSubInstances.Values) + { + if (!map.MapInstance.GetAliveMonsters(m => m.SummonerType is not VisualType.Player).Any()) + { + continue; + } + + perfectMonsters = false; + break; + } + + bool perfectMaps = timeSpaceParty.Instance.TimeSpaceSubInstances.Values.Where(map + => map.MapInstance.Id != timeSpaceParty.Instance.SpawnInstance.MapInstance.Id).All(map + => timeSpaceParty.Instance.VisitedRooms.Contains(map.MapInstance.Id)); + + bool perfectProtectedNpcs = timeSpaceParty.Instance.KilledProtectedNpcs == 0; + timeSpaceParty.Instance.SavedNpcs = timeSpaceParty.Instance.ProtectedNpcs.Count - timeSpaceParty.Instance.KilledProtectedNpcs; + + if (timeSpaceParty.Instance.SavedNpcs < 0) + { + timeSpaceParty.Instance.SavedNpcs = 0; + } + + TimeSpaceFinishType type = TimeSpaceFinishType.SUCCESS; + if (isNewRecord) + { + type = TimeSpaceFinishType.SUCCESS_HIGH_SCORE; + await _timeSpaceService.SetNewRecord(new TimeSpaceNewRecordRequest + { + TimeSpaceRecordDto = new TimeSpaceRecordDto + { + TimeSpaceId = timeSpaceParty.TimeSpaceId, + CharacterName = timeSpaceParty.Leader.PlayerEntity.Name, + Date = DateTime.UtcNow, + Record = timeSpaceParty.Instance.Score + } + }); + } + + int goldReward = timeSpaceParty.CalculateGoldReward(); + long generateExp = timeSpaceParty.CalculateExperience(); + foreach (IClientSession member in timeSpaceParty.Members) + { + member.SendRemoveRedClockPacket(); + + double penalty = member.GetTimeSpacePenalty(); + if (penalty != 0) + { + member.SendChatMessage(member.GetLanguageFormat(GameDialogKey.TIMESPACE_CHATMESSAGE_TIME_SPACE_PENALTY, penalty), ChatMessageColorType.Yellow); + } + + if (timeSpaceParty.ItemVnumToRemove.HasValue) + { + InventoryItem itemToRemove = member.PlayerEntity.GetFirstItemByVnum(timeSpaceParty.ItemVnumToRemove.Value); + if (itemToRemove != null) + { + await member.RemoveItemFromInventory(item: itemToRemove); + } + } + + member.SendMsg(_gameLanguageService.GetLanguage(GameDialogKey.TIMESPACE_SHOUTMESSAGE_WAIT_REWARD, member.UserLanguage), MsgMessageType.Middle); + goldReward = (int)(goldReward * (1 - penalty / 100.0)); + if (goldReward > 0 && !timeSpaceParty.IsEasyMode) + { + await member.EmitEventAsync(new GenerateGoldEvent(goldReward)); + } + + if (generateExp != 0 && !timeSpaceParty.IsEasyMode) + { + await member.EmitEventAsync(new AddExpEvent(generateExp, LevelType.Level)); + } + + if (timeSpaceParty.TimeSpaceInformation.IsHidden || timeSpaceParty.TimeSpaceInformation.IsSpecial) + { + await member.EmitEventAsync(new GenerateReputationEvent + { + Amount = (timeSpaceParty.TimeSpaceInformation.ReputationMultiplier ?? 30) * timeSpaceParty.TimeSpaceInformation.MinLevel, + SendMessage = true + }); + + if (!member.PlayerEntity.CompletedTimeSpaces.Contains(timeSpaceParty.TimeSpaceId)) + { + member.PlayerEntity.CompletedTimeSpaces.Add(timeSpaceParty.TimeSpaceId); + timeSpaceParty.FirstCompletedTimeSpaceIds.Add(member.PlayerEntity.Id); + } + } + else if (!member.PlayerEntity.CompletedTimeSpaces.Contains(timeSpaceParty.TimeSpaceId)) + { + timeSpaceParty.FirstCompletedTimeSpaceIds.Add(member.PlayerEntity.Id); + member.PlayerEntity.CompletedTimeSpaces.Add(timeSpaceParty.TimeSpaceId); + await member.EmitEventAsync(new GenerateReputationEvent + { + Amount = (timeSpaceParty.TimeSpaceInformation.ReputationMultiplier ?? 30) * timeSpaceParty.TimeSpaceInformation.MinLevel, + SendMessage = true + }); + + member.SendRsfiPacket(_subActConfiguration, _timeSpaceConfiguration); + } + + member.SendScorePacket(type, perfectMonsters, perfectProtectedNpcs, perfectMaps); + member.SendRsfmPacket(TimeSpaceAction.TIMESPACE_COMPLETE); + + HandleTsQuests(member, timeSpaceParty.TimeSpaceId, timeSpaceParty.IsChallengeMode, timeSpaceParty.Instance.Score); + } + } + + private void CalculatePenalty(TimeSpaceParty timeSpaceParty) + { + double penalty = timeSpaceParty.GetTimeSpaceScorePenalty() / 100.0; + + if (penalty <= 0) + { + return; + } + + int finalScore = (int)(timeSpaceParty.Instance.Score * (1 - penalty)); + timeSpaceParty.Instance.UpdateFinalScore(finalScore); + } + + private void HandleTsQuests(IClientSession member, long timeSpaceId, bool isChallengeMode, int score) + { + IEnumerable tsQuests = member.PlayerEntity.GetCurrentQuestsByTypes(TimeSpaceCompletionQuestTypes).ToArray(); + if (!tsQuests.Any()) + { + return; + } + + foreach (CharacterQuest characterQuest in tsQuests) + { + foreach (QuestObjectiveDto objective in characterQuest.Quest.Objectives) + { + if (objective.Data0 != timeSpaceId) + { + continue; + } + + CharacterQuestObjectiveDto questObjectiveDto = characterQuest.ObjectiveAmount[objective.ObjectiveIndex]; + switch (characterQuest.Quest.QuestType) + { + case QuestType.COMPLETE_TIMESPACE: + + if (questObjectiveDto.CurrentAmount < questObjectiveDto.RequiredAmount) + { + questObjectiveDto.CurrentAmount++; + } + + member.EmitEventAsync(new QuestCompletedEvent(characterQuest)); + break; + + case QuestType.COMPLETE_TIMESPACE_WITH_ATLEAST_X_POINTS: + if (!isChallengeMode) + { + break; + } + + questObjectiveDto.CurrentAmount = score; + + member.EmitEventAsync(new QuestCompletedEvent(characterQuest)); + break; + } + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceInstanceStartEventHandler.cs b/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceInstanceStartEventHandler.cs new file mode 100644 index 0000000..9ba921a --- /dev/null +++ b/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceInstanceStartEventHandler.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.PacketGeneration; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Networking; +using WingsEmu.Game.TimeSpaces; +using WingsEmu.Game.TimeSpaces.Enums; +using WingsEmu.Game.TimeSpaces.Events; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.TimeSpaces.Handlers; + +public class TimeSpaceInstanceStartEventHandler : IAsyncEventProcessor +{ + private readonly IAsyncEventPipeline _eventPipeline; + private readonly IGameLanguageService _gameLanguageService; + private readonly ITimeSpaceFactory _timeSpaceFactory; + private readonly ITimeSpaceManager _timeSpaceManager; + + public TimeSpaceInstanceStartEventHandler(IAsyncEventPipeline eventPipeline, ITimeSpaceManager timeSpaceManager, IGameLanguageService gameLanguageService, ITimeSpaceFactory timeSpaceFactory) + { + _eventPipeline = eventPipeline; + _timeSpaceManager = timeSpaceManager; + _gameLanguageService = gameLanguageService; + _timeSpaceFactory = timeSpaceFactory; + } + + public async Task HandleAsync(TimeSpaceInstanceStartEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + TimeSpaceParty timeSpace = session.PlayerEntity.TimeSpaceComponent.TimeSpace; + + if (timeSpace == null || timeSpace.Instance != null) + { + return; + } + + TimeSpaceInstance timeSpaceInstance = _timeSpaceFactory.Create(timeSpace); + + if (timeSpaceInstance == null) + { + _timeSpaceManager.RemoveTimeSpace(timeSpace); + foreach (IClientSession member in timeSpace.Members) + { + member.PlayerEntity.TimeSpaceComponent.RemoveTimeSpaceParty(); + } + + return; + } + + foreach (KeyValuePair timeSpaceSubInstance in timeSpaceInstance.TimeSpaceSubInstances) + { + _timeSpaceManager.AddTimeSpaceByMapInstanceId(timeSpaceSubInstance.Value.MapInstance.Id, timeSpace); + _timeSpaceManager.AddTimeSpaceSubInstance(timeSpaceSubInstance.Key, timeSpaceSubInstance.Value); + } + + session.PlayerEntity.TimeSpaceComponent.TimeSpace.SetEnteredTimeSpace(timeSpaceInstance); + + IMapInstance mapStart = timeSpaceInstance.SpawnInstance.MapInstance; + + session.ChangeMap(mapStart, timeSpaceInstance.SpawnPoint.X, timeSpaceInstance.SpawnPoint.Y); + + // Time to draw the layout (starting as (0, 0) coordinates the top-left corner). We will need to have a list + // of mapInstances and iterate over it. Hardcoded for now. + session.SendPacket(mapStart.GenerateRsfn(true, false)); + foreach (KeyValuePair mapInstance in timeSpaceInstance.TimeSpaceSubInstances) + { + session.SendPacket(mapInstance.Value.MapInstance.GenerateRsfn(isVisit: false)); + } + + session.SendRsfpPacket(); + mapStart.MapClear(true); + mapStart.BroadcastTimeSpacePartnerInfo(); + + // Rsfm packet for camera alignment and minimap appearing. The same than before. In the end we have to get the maxX+1 and maxY+1 + // from the MapInstances + session.SendRsfmPacket(TimeSpaceAction.CAMERA_ADJUST); + session.SendMinfo(); + session.SendMsg(_gameLanguageService.GetLanguage(GameDialogKey.TIMESPACE_SHOUTMESSAGE_TIMESPACE_ENTER, session.UserLanguage), MsgMessageType.Middle); + + if (timeSpace.IsEasyMode) + { + session.SendChatMessage(session.GetLanguage(GameDialogKey.TIMESPACE_CHATMESSAGE_EASY_MODE), ChatMessageColorType.Yellow); + } + + if (session.PlayerEntity.Level > timeSpace.HigherLevel) + { + timeSpace.HigherLevel = session.PlayerEntity.Level; + } + + await _eventPipeline.ProcessEventAsync(new TimeSpaceStartClockEvent(session.PlayerEntity.TimeSpaceComponent.TimeSpace, false)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceJoinMapEndEventHandler.cs b/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceJoinMapEndEventHandler.cs new file mode 100644 index 0000000..a97abe2 --- /dev/null +++ b/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceJoinMapEndEventHandler.cs @@ -0,0 +1,138 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Scripting.Object.Timespace; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Maps.Event; +using WingsEmu.Game.Networking; +using WingsEmu.Game.TimeSpaces; +using WingsEmu.Game.TimeSpaces.Enums; +using WingsEmu.Game.TimeSpaces.Events; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.TimeSpaces.Handlers; + +public class TimeSpaceJoinMapEndEventHandler : IAsyncEventProcessor +{ + private readonly ITimeSpaceManager _timeSpaceManager; + + public TimeSpaceJoinMapEndEventHandler(ITimeSpaceManager timeSpaceManager) => _timeSpaceManager = timeSpaceManager; + + public async Task HandleAsync(JoinMapEndEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + if (e.JoinedMapInstance.MapInstanceType != MapInstanceType.TimeSpaceInstance) + { + return; + } + + if (!session.PlayerEntity.TimeSpaceComponent.IsInTimeSpaceParty) + { + return; + } + + TimeSpaceParty timeSpace = session.PlayerEntity.TimeSpaceComponent.TimeSpace; + + if (timeSpace?.Instance == null) + { + return; + } + + if (timeSpace.Instance.StartTimeFreeze.HasValue) + { + await session.EmitEventAsync(new TimeSpaceAddTimeToTimerEvent + { + Time = DateTime.UtcNow - timeSpace.Instance.StartTimeFreeze.Value + }); + + timeSpace.Instance.StartTimeFreeze = null; + } + + session.RefreshTimespaceScoreUi(); + if (!timeSpace.Instance.VisitedRooms.Contains(e.JoinedMapInstance.Id) && timeSpace.Instance.SpawnInstance.MapInstance.Id != e.JoinedMapInstance.Id) + { + timeSpace.Instance.IncreaseEnteredRooms(); + timeSpace.Instance.VisitedRooms.Add(e.JoinedMapInstance.Id); + } + + if (!timeSpace.Instance.TimeSpaceSubInstances.TryGetValue(e.JoinedMapInstance.Id, out TimeSpaceSubInstance room)) + { + return; + } + + if (room.SendPortalOpenMessage) + { + room.SendPortalOpenMessage = false; + session.SendMsg(session.GetLanguage(GameDialogKey.TIMESPACE_SHOUTMESSAGE_DOOR_OPENED), MsgMessageType.Middle); + } + + if (!string.IsNullOrEmpty(room.Task.StartDialogShout)) + { + session.SendMsg(session.GetLanguage(room.Task.StartDialogShout), MsgMessageType.Middle); + } + + if (room.Task?.StartDialog != null) + { + session.SendNpcReqPacket(room.Task.StartDialog.Value); + + if (timeSpace.Instance.StartTimeFreeze.HasValue) + { + return; + } + + if (!timeSpace.Started || timeSpace.Finished) + { + return; + } + + timeSpace.Instance.StartTimeFreeze = DateTime.UtcNow; + foreach (IClientSession member in timeSpace.Members) + { + member.SendTimerFreeze(); + } + } + + SendWarnings(session, room); + await room.TriggerEvents(TimespaceConstEventKeys.OnMapJoin); + } + + private void SendWarnings(IClientSession session, TimeSpaceSubInstance timeSpaceSubInstance) + { + TimeSpaceTask task = timeSpaceSubInstance.Task; + if (task == null) + { + return; + } + + if (task.IsFinished || !task.IsActivated) + { + return; + } + + if (task.TimeLeft.HasValue && task.Time.HasValue) + { + TimeSpan timeLeft = task.TimeLeft.Value - DateTime.UtcNow; + if (timeLeft.TotalMilliseconds > 0) + { + session.SendClockPacket(ClockType.RedMiddle, 0, timeLeft, task.Time.Value); + } + } + + if (timeSpaceSubInstance.Task.TaskType != TimeSpaceTaskType.None) + { + session.SendRsfmPacket(!timeSpaceSubInstance.Task.IsActivated ? TimeSpaceAction.WARNING_WITH_SOUND : TimeSpaceAction.WARNING_WITHOUT_SOUND); + } + + if (string.IsNullOrEmpty(task.GameDialogKey)) + { + return; + } + + string message = session.GetLanguage(task.GameDialogKey); + session.SendMissionTargetMessage(message); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceLeavePartyEventHandler.cs b/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceLeavePartyEventHandler.cs new file mode 100644 index 0000000..b34c2ab --- /dev/null +++ b/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceLeavePartyEventHandler.cs @@ -0,0 +1,105 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsAPI.Game.Extensions.ItemExtension.Item; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Revival; +using WingsEmu.Game.TimeSpaces; +using WingsEmu.Game.TimeSpaces.Events; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.TimeSpaces.Handlers; + +public class TimeSpaceLeavePartyEventHandler : IAsyncEventProcessor +{ + private readonly IGameLanguageService _gameLanguage; + private readonly GameMinMaxConfiguration _gameMinMaxConfiguration; + private readonly IItemsManager _itemsManager; + private readonly IRankingManager _rankingManager; + private readonly IReputationConfiguration _reputationConfiguration; + + public TimeSpaceLeavePartyEventHandler(IItemsManager itemsManager, IGameLanguageService gameLanguage, + GameMinMaxConfiguration gameMinMaxConfiguration, IReputationConfiguration reputationConfiguration, IRankingManager rankingManager) + { + _itemsManager = itemsManager; + _gameLanguage = gameLanguage; + _gameMinMaxConfiguration = gameMinMaxConfiguration; + _reputationConfiguration = reputationConfiguration; + _rankingManager = rankingManager; + } + + public async Task HandleAsync(TimeSpaceLeavePartyEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + + if (!session.PlayerEntity.TimeSpaceComponent.IsInTimeSpaceParty) + { + return; + } + + TimeSpaceParty timeSpace = session.PlayerEntity.TimeSpaceComponent.TimeSpace; + if (timeSpace?.Instance == null) + { + return; + } + + if (e.CheckFinished && !timeSpace.Finished) + { + return; + } + + if (timeSpace.Started && e.CheckForSeeds) + { + TimeSpaceSubInstance getCurrentInstance = timeSpace.Instance.TimeSpaceSubInstances.TryGetValue(session.CurrentMapInstance.Id, out TimeSpaceSubInstance instance) ? instance : null; + if (getCurrentInstance != null && timeSpace.Instance.SpawnInstance != getCurrentInstance) + { + if (!session.PlayerEntity.HasItem((short)ItemVnums.SEED_OF_POWER, 5)) + { + string itemName = _itemsManager.GetItem((short)ItemVnums.SEED_OF_POWER).GetItemName(_gameLanguage, session.UserLanguage); + session.SendChatMessage(session.GetLanguageFormat(GameDialogKey.INVENTORY_SHOUTMESSAGE_NOT_ENOUGH_ITEMS, 5, itemName), ChatMessageColorType.PlayerSay); + return; + } + + await session.RemoveItemFromInventory((short)ItemVnums.SEED_OF_POWER, 5); + } + + int dignityToRemove = session.PlayerEntity.Level < 20 ? session.PlayerEntity.Level : 20; + await session.PlayerEntity.RemoveDignity(dignityToRemove, _gameMinMaxConfiguration, _gameLanguage, _reputationConfiguration, _rankingManager.TopReputation); + } + + if (!session.PlayerEntity.IsAlive()) + { + await session.EmitEventAsync(new RevivalReviveEvent()); + } + + if (timeSpace.Finished) + { + session.EmitEvent(new TimeSpaceSelectRewardEvent()); + timeSpace.RemoveMember(session); + session.PlayerEntity.TimeSpaceComponent.RemoveTimeSpaceParty(); + session.ChangeToLastBaseMap(); + return; + } + + if (e.RemoveLive) + { + await session.EmitEventAsync(new TimeSpaceDecreaseLiveEvent()); + if (!session.PlayerEntity.TimeSpaceComponent.IsInTimeSpaceParty) + { + return; + } + } + + timeSpace.RemoveMember(session); + session.PlayerEntity.TimeSpaceComponent.RemoveTimeSpaceParty(); + session.ChangeToLastBaseMap(); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceMonsterDeathHandler.cs b/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceMonsterDeathHandler.cs new file mode 100644 index 0000000..b178389 --- /dev/null +++ b/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceMonsterDeathHandler.cs @@ -0,0 +1,97 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Monster.Event; +using WingsEmu.Game.TimeSpaces; +using WingsEmu.Game.TimeSpaces.Events; + +namespace Plugin.TimeSpaces.Handlers; + +public class TimeSpaceMonsterDeathHandler : IAsyncEventProcessor +{ + private readonly IAsyncEventPipeline _asyncEventPipeline; + private readonly ITimeSpaceManager _timeSpaceManager; + + public TimeSpaceMonsterDeathHandler(IAsyncEventPipeline asyncEventPipeline, ITimeSpaceManager timeSpaceManager) + { + _asyncEventPipeline = asyncEventPipeline; + _timeSpaceManager = timeSpaceManager; + } + + public async Task HandleAsync(MonsterDeathEvent e, CancellationToken cancellation) + { + IMonsterEntity monster = e.MonsterEntity; + if (monster.MapInstance.MapInstanceType != MapInstanceType.TimeSpaceInstance) + { + return; + } + + if (e.IsByCommand) + { + return; + } + + Guid guid = monster.MapInstance.Id; + TimeSpaceParty timeSpace = _timeSpaceManager.GetTimeSpaceByMapInstanceId(guid); + if (timeSpace == null) + { + return; + } + + if (!timeSpace.Instance.TimeSpaceSubInstances.TryGetValue(guid, out TimeSpaceSubInstance timeSpaceSubInstance)) + { + return; + } + + timeSpace.Instance.IncreaseKilledMonsters(); + + DateTime now = DateTime.UtcNow; + if (timeSpaceSubInstance.LastTryFinishTime < now) + { + timeSpaceSubInstance.LastTryFinishTime = now.AddMilliseconds(300); + } + else + { + timeSpaceSubInstance.LastTryFinishTime += TimeSpan.FromMilliseconds(300); + } + + timeSpace.LastObjectivesCheck = now + TimeSpan.FromSeconds(1); + + if (timeSpace.IsEasyMode) + { + return; + } + + if (!monster.IsBonus) + { + await _asyncEventPipeline.ProcessEventAsync(new TimeSpaceIncreaseScoreEvent + { + AmountToIncrease = 1, + TimeSpaceParty = timeSpace + }); + + timeSpaceSubInstance.MonsterBonusCombo = 0; + return; + } + + timeSpaceSubInstance.MonsterBonusCombo++; + + int combo = 1 + 5 * timeSpaceSubInstance.MonsterBonusCombo; + + await _asyncEventPipeline.ProcessEventAsync(new TimeSpaceIncreaseScoreEvent + { + AmountToIncrease = combo, + TimeSpaceParty = timeSpace + }); + + timeSpaceSubInstance.MonsterBonusId = null; + await _asyncEventPipeline.ProcessEventAsync(new TimeSpaceBonusMonsterEvent + { + MonsterEntities = monster.MapInstance.GetAliveMonsters(), + TimeSpaceSubInstance = timeSpaceSubInstance + }); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpacePartyCreateEventHandler.cs b/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpacePartyCreateEventHandler.cs new file mode 100644 index 0000000..595cefb --- /dev/null +++ b/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpacePartyCreateEventHandler.cs @@ -0,0 +1,138 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsAPI.Game.Extensions.ItemExtension.Item; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Networking; +using WingsEmu.Game.TimeSpaces; +using WingsEmu.Game.TimeSpaces.Events; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.TimeSpaces.Handlers; + +public class TimeSpacePartyCreateEventHandler : IAsyncEventProcessor +{ + private readonly IGameLanguageService _gameLanguage; + private readonly IItemsManager _itemsManager; + private readonly ISubActConfiguration _subActConfiguration; + private readonly ITimeSpaceConfiguration _timeSpaceConfig; + private readonly ITimeSpaceConfiguration _timeSpaceConfiguration; + private readonly ITimeSpaceManager _timeSpaceManager; + + public TimeSpacePartyCreateEventHandler(ITimeSpaceManager timeSpaceManager, IGameLanguageService gameLanguage, ITimeSpaceConfiguration timeSpaceConfiguration, IItemsManager itemsManager, + ISubActConfiguration subActConfiguration, ITimeSpaceConfiguration timeSpaceConfig) + { + _timeSpaceManager = timeSpaceManager; + _gameLanguage = gameLanguage; + _timeSpaceConfiguration = timeSpaceConfiguration; + _itemsManager = itemsManager; + _subActConfiguration = subActConfiguration; + _timeSpaceConfig = timeSpaceConfig; + } + + public async Task HandleAsync(TimeSpacePartyCreateEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + + if (session.PlayerEntity.IsInRaidParty) + { + return; + } + + if (session.PlayerEntity.HasShopOpened) + { + return; + } + + if (session.IsMuted()) + { + session.SendMsg(session.GetLanguage(GameDialogKey.MUTE_SHOUTMESSAGE_YOU_ARE_MUTED), MsgMessageType.Middle); + return; + } + + if (!session.PlayerEntity.MapInstance.HasMapFlag(MapFlags.IS_BASE_MAP)) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_MUST_BE_IN_CLASSIC_MAP, session.UserLanguage), MsgMessageType.Middle); + return; + } + + TimeSpaceFileConfiguration timeSpace = _timeSpaceConfiguration.GetTimeSpaceConfiguration(e.TimeSpaceId); + if (timeSpace == null) + { + return; + } + + if (!session.CanJoinToTimeSpace(e.TimeSpaceId, _subActConfiguration, _timeSpaceConfig) && !session.IsGameMaster() && !timeSpace.IsSpecial && !timeSpace.IsHidden) + { + session.SendChatMessage(session.GetLanguage(GameDialogKey.TIMESPACE_SHOUTMESSAGE_WRONG_ACT), ChatMessageColorType.Red); + session.SendMsg(session.GetLanguage(GameDialogKey.TIMESPACE_SHOUTMESSAGE_WRONG_ACT), MsgMessageType.Middle); + return; + } + + if (session.PlayerEntity.Level < timeSpace.MinLevel) + { + session.SendChatMessage(session.GetLanguage(GameDialogKey.TIMESPACE_CHATMESSAGE_LOW_LEVEL), ChatMessageColorType.Red); + session.SendMsg(session.GetLanguage(GameDialogKey.TIMESPACE_CHATMESSAGE_LOW_LEVEL), MsgMessageType.Middle); + return; + } + + if (session.PlayerEntity.Level > timeSpace.MaxLevel) + { + session.SendChatMessage(session.GetLanguage(GameDialogKey.TIMESPACE_CHATMESSAGE_HIGH_LEVEL), ChatMessageColorType.PlayerSay); + session.SendMsg(session.GetLanguage(GameDialogKey.TIMESPACE_CHATMESSAGE_HIGH_LEVEL), MsgMessageType.Middle); + return; + } + + if (timeSpace.SeedsOfPowerRequired != 0) + { + if (!session.PlayerEntity.HasItem((short)ItemVnums.SEED_OF_POWER, timeSpace.SeedsOfPowerRequired)) + { + string itemName = _itemsManager.GetItem((short)ItemVnums.SEED_OF_POWER).GetItemName(_gameLanguage, session.UserLanguage); + session.SendChatMessage(session.GetLanguageFormat(GameDialogKey.INVENTORY_SHOUTMESSAGE_NOT_ENOUGH_ITEMS, timeSpace.SeedsOfPowerRequired, itemName), ChatMessageColorType.Red); + session.SendMsg(session.GetLanguageFormat(GameDialogKey.INVENTORY_SHOUTMESSAGE_NOT_ENOUGH_ITEMS, timeSpace.SeedsOfPowerRequired, itemName), MsgMessageType.Middle); + return; + } + } + + if (e.IsChallengeMode) + { + if (session.IsActionForbidden()) + { + return; + } + + if (!session.PlayerEntity.RemoveGold(timeSpace.MinLevel * 50)) + { + session.SendChatMessage(session.GetLanguage(GameDialogKey.INTERACTION_MESSAGE_NOT_ENOUGH_GOLD), ChatMessageColorType.Red); + session.SendMsg(session.GetLanguage(GameDialogKey.INTERACTION_MESSAGE_NOT_ENOUGH_GOLD), MsgMessageType.Middle); + return; + } + + ; + } + + if (timeSpace.SeedsOfPowerRequired != 0) + { + await session.RemoveItemFromInventory((short)ItemVnums.SEED_OF_POWER, timeSpace.SeedsOfPowerRequired); + } + + var timeSpaceParty = new TimeSpaceParty(timeSpace, e.IsEasyMode, e.IsChallengeMode); + + InventoryItem itemToRemove = e.ItemToRemove; + if (itemToRemove != null) + { + timeSpaceParty.ItemVnumToRemove = itemToRemove.ItemInstance.ItemVNum; + } + + timeSpaceParty.AddMember(session); + _timeSpaceManager.AddTimeSpace(timeSpaceParty); + session.PlayerEntity.TimeSpaceComponent.SetTimeSpaceParty(timeSpaceParty); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpacePickUpItemEventHandler.cs b/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpacePickUpItemEventHandler.cs new file mode 100644 index 0000000..3e35565 --- /dev/null +++ b/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpacePickUpItemEventHandler.cs @@ -0,0 +1,117 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsAPI.Game.Extensions.ItemExtension.Item; +using WingsAPI.Scripting.Object.Timespace; +using WingsEmu.Game; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Extensions.Mates; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Networking; +using WingsEmu.Game.TimeSpaces; +using WingsEmu.Game.TimeSpaces.Events; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.TimeSpaces.Handlers; + +public class TimeSpacePickUpItemEventHandler : IAsyncEventProcessor +{ + private readonly IBCardEffectHandlerContainer _bCardEffectHandler; + private readonly IChestDropItemConfig _chestDropItemConfig; + private readonly IGameItemInstanceFactory _gameItemInstanceFactory; + private readonly IGameLanguageService _gameLanguage; + private readonly IItemsManager _itemsManager; + private readonly IRandomGenerator _randomGenerator; + + public TimeSpacePickUpItemEventHandler(IGameLanguageService gameLanguage, IItemsManager itemsManager, + IGameItemInstanceFactory gameItemInstanceFactory, IRandomGenerator randomGenerator, IChestDropItemConfig chestDropItemConfig, + IBCardEffectHandlerContainer bCardEffectHandler) + { + _gameLanguage = gameLanguage; + _itemsManager = itemsManager; + _gameItemInstanceFactory = gameItemInstanceFactory; + _randomGenerator = randomGenerator; + _chestDropItemConfig = chestDropItemConfig; + _bCardEffectHandler = bCardEffectHandler; + } + + public async Task HandleAsync(TimeSpacePickUpItemEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + TimeSpaceMapItem item = e.TimeSpaceMapItem; + IMateEntity mateEntity = e.MateEntity; + GameItemInstance mapItemInstance = item.GetItemInstance(); + + TimeSpaceParty timeSpace = session.PlayerEntity.TimeSpaceComponent.TimeSpace; + if (timeSpace?.Instance == null) + { + return; + } + + short? objectiveItem = timeSpace.Instance.TimeSpaceObjective.CollectItemVnum; + if (item.IsObjective && objectiveItem.HasValue && item.ItemVNum == objectiveItem.Value) + { + timeSpace.Instance.TimeSpaceObjective.CollectedItemAmount++; + await session.EmitEventAsync(new TimeSpaceRefreshObjectiveProgressEvent + { + MapInstanceId = session.CurrentMapInstance.Id + }); + } + + if (mapItemInstance.GameItem.IsTimeSpaceChest()) + { + switch (mapItemInstance.GameItem.Data[0]) + { + case 4: + + ChestDropItemConfiguration config = _chestDropItemConfig.GetChestByDataId(mapItemInstance.GameItem.Data[2]); + + if (config?.PossibleItems == null) + { + break; + } + + if (_randomGenerator.RandomNumber() > config.ItemChance) + { + session.SendMsg(session.GetLanguage(GameDialogKey.ITEM_SHOUTMESSAGE_CHEST_EMPTY), MsgMessageType.Middle); + break; + } + + ChestDropItemDrop getRandomItem = config.PossibleItems[_randomGenerator.RandomNumber(config.PossibleItems.Count)]; + if (getRandomItem == null) + { + break; + } + + GameItemInstance newItem = _gameItemInstanceFactory.CreateItem(getRandomItem.ItemVnum, getRandomItem.Amount); + await session.AddNewItemToInventory(newItem, sendGiftIsFull: true); + + string itemName = newItem.GameItem.GetItemName(_gameLanguage, session.UserLanguage); + session.SendMsg(session.GetLanguageFormat(GameDialogKey.INVENTORY_CHATMESSAGE_X_ITEM_ACQUIRED, getRandomItem.Amount, itemName), MsgMessageType.Middle); + break; + } + } + + session.CurrentMapInstance.RemoveDrop(item.TransportId); + await item.TriggerEvents(TimespaceConstEventKeys.PickedUp); + + if (mateEntity == null) + { + session.BroadcastGetPacket(item.TransportId); + } + else + { + mateEntity.BroadcastMateGetPacket(item.TransportId); + mateEntity.Owner.Session.SendCondMate(mateEntity); + mateEntity.Owner?.Session.SendPacket(mateEntity.GenerateEffectPacket(EffectType.PetPickUp)); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpacePortalOpenEventHandler.cs b/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpacePortalOpenEventHandler.cs new file mode 100644 index 0000000..aa069b4 --- /dev/null +++ b/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpacePortalOpenEventHandler.cs @@ -0,0 +1,86 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.PacketGeneration; +using WingsEmu.Game; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Game.TimeSpaces; +using WingsEmu.Game.TimeSpaces.Events; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.TimeSpaces.Handlers; + +public class TimeSpacePortalOpenEventHandler : IAsyncEventProcessor +{ + private readonly ITimeSpaceManager _timeSpaceManager; + + public TimeSpacePortalOpenEventHandler(ITimeSpaceManager timeSpaceManager) => _timeSpaceManager = timeSpaceManager; + + public async Task HandleAsync(TimeSpacePortalOpenEvent e, CancellationToken cancellation) + { + IPortalEntity portal = e.PortalEntity; + + PortalType type = portal.Type switch + { + PortalType.Closed => PortalType.Open, + PortalType.TSEndClosed => PortalType.TSEnd, + _ => PortalType.Open + }; + + portal.Type = type; + + portal.MapInstance.MapClear(true); + portal.MapInstance.BroadcastTimeSpacePartnerInfo(); + + TimeSpaceSubInstance timeSpaceSubInstance = _timeSpaceManager.GetSubInstance(portal.MapInstance.Id); + + if (timeSpaceSubInstance == null) + { + return; + } + + bool sendClock = false; + if (timeSpaceSubInstance.Task != null) + { + if (timeSpaceSubInstance.Task.IsFinished && timeSpaceSubInstance.Task.TimeLeft.HasValue) + { + sendClock = true; + timeSpaceSubInstance.Task.TimeLeft = null; + } + } + + if (!portal.MapInstance.Sessions.Any()) + { + timeSpaceSubInstance.SendPortalOpenMessage = true; + + TimeSpaceParty timeSpace = _timeSpaceManager.GetTimeSpaceByMapInstanceId(portal.MapInstance.Id); + if (timeSpace == null) + { + return; + } + + foreach (IClientSession session in timeSpace.Members.Where(session => session.CurrentMapInstance.Id != portal.MapInstance.Id)) + { + session.SendMsg(session.GetLanguage(GameDialogKey.TIMESPACE_SHOUTMESSAGE_DOOR_OPENED_SOMEWHERE), MsgMessageType.Middle); + } + + return; + } + + foreach (IClientSession session in portal.MapInstance.Sessions) + { + session.SendMsg(session.GetLanguage(GameDialogKey.TIMESPACE_SHOUTMESSAGE_DOOR_OPENED), MsgMessageType.Middle); + + if (!sendClock) + { + continue; + } + + session.SendRemoveRedClockPacket(); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpacePortalTriggerEventHandler.cs b/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpacePortalTriggerEventHandler.cs new file mode 100644 index 0000000..ccc42f5 --- /dev/null +++ b/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpacePortalTriggerEventHandler.cs @@ -0,0 +1,102 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Maps.Event; +using WingsEmu.Game.Networking; +using WingsEmu.Game.TimeSpaces; +using WingsEmu.Game.TimeSpaces.Events; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.TimeSpaces.Handlers; + +public class TimeSpacePortalTriggerEventHandler : IAsyncEventProcessor +{ + private readonly IGameLanguageService _language; + private readonly IMapManager _mapManager; + + public TimeSpacePortalTriggerEventHandler(IGameLanguageService language, IMapManager mapManager) + { + _language = language; + _mapManager = mapManager; + } + + public async Task HandleAsync(PortalTriggerEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + if (session.CurrentMapInstance.MapInstanceType != MapInstanceType.TimeSpaceInstance) + { + return; + } + + TimeSpaceParty timeSpaceParty = session.PlayerEntity.TimeSpaceComponent.TimeSpace; + if (timeSpaceParty?.Instance == null) + { + return; + } + + if (timeSpaceParty.Finished) + { + return; + } + + switch (e.Portal.Type) + { + case PortalType.TSNormal: + + if (timeSpaceParty.Started) + { + break; + } + + if (timeSpaceParty.Leader.PlayerEntity.Id == session.PlayerEntity.Id && !timeSpaceParty.Started) + { + session.SendDialog("rstart 1", "rstart", _language.GetLanguage(GameDialogKey.TIMESPACE_DIALOG_ASK_START, session.UserLanguage)); + return; + } + else if (!timeSpaceParty.Started) + { + session.SendChatMessage(_language.GetLanguage(GameDialogKey.TIMESPACE_CHATMESSAGE_ONLY_TEAM_LEADER_START, session.UserLanguage), ChatMessageColorType.Yellow); + return; + } + + break; + case PortalType.TSEnd: + await session.EmitEventAsync(new TimeSpaceCheckObjectivesEvent + { + TimeSpaceParty = session.PlayerEntity.TimeSpaceComponent.TimeSpace, + PlayerEnteredToEndPortal = true, + SendMessageWithNotFinishedObjects = true + }); + return; + case PortalType.Closed: + case PortalType.TSEndClosed: + session.SendChatMessage(session.GetLanguage(GameDialogKey.TIMESPACE_CHATMESSAGE_GATE_LOCKED), ChatMessageColorType.PlayerSay); + return; + } + + if (e.Portal.DestinationMapInstance == null) + { + return; + } + + e.Sender.PlayerEntity.LastPortal = DateTime.UtcNow; + if (e.Portal.DestinationX == -1 && e.Portal.DestinationY == -1) + { + await _mapManager.TeleportOnRandomPlaceInMapAsync(session, e.Portal.DestinationMapInstance); + return; + } + + if (e.Portal.DestinationMapInstance.Id == session.PlayerEntity.MapInstanceId && e.Portal.DestinationX.HasValue && e.Portal.DestinationY.HasValue) + { + session.PlayerEntity.TeleportOnMap(e.Portal.DestinationX.Value, e.Portal.DestinationY.Value, true); + return; + } + + session.ChangeMap(e.Portal.DestinationMapInstance, e.Portal.DestinationX, e.Portal.DestinationY); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceRefreshObjectiveProgressEventHandler.cs b/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceRefreshObjectiveProgressEventHandler.cs new file mode 100644 index 0000000..834fe72 --- /dev/null +++ b/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceRefreshObjectiveProgressEventHandler.cs @@ -0,0 +1,37 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Game.TimeSpaces; +using WingsEmu.Game.TimeSpaces.Events; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.TimeSpaces.Handlers; + +public class TimeSpaceRefreshObjectiveProgressEventHandler : IAsyncEventProcessor +{ + private readonly ITimeSpaceManager _timeSpaceManager; + + public TimeSpaceRefreshObjectiveProgressEventHandler(ITimeSpaceManager timeSpaceManager) => _timeSpaceManager = timeSpaceManager; + + public async Task HandleAsync(TimeSpaceRefreshObjectiveProgressEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + Guid mapInstanceId = e.MapInstanceId; + + TimeSpaceParty timeSpace = session == null ? _timeSpaceManager.GetTimeSpaceByMapInstanceId(mapInstanceId) : session.PlayerEntity.TimeSpaceComponent.TimeSpace; + if (timeSpace == null) + { + return; + } + + foreach (IClientSession member in timeSpace.Members) + { + member.SendMsg(member.GetLanguage(GameDialogKey.TIMESPACE_SHOUTMESSAGE_MISSION_UPDATED), MsgMessageType.Middle); + member.SendMinfo(); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceRemoveItemsEventHandler.cs b/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceRemoveItemsEventHandler.cs new file mode 100644 index 0000000..a5de010 --- /dev/null +++ b/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceRemoveItemsEventHandler.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Maps; +using WingsEmu.Game.TimeSpaces.Events; + +namespace Plugin.TimeSpaces.Handlers; + +public class TimeSpaceRemoveItemsEventHandler : IAsyncEventProcessor +{ + public async Task HandleAsync(TimeSpaceRemoveItemsEvent e, CancellationToken cancellation) + { + IEnumerable items = e.ItemsToRemove; + foreach (TimeSpaceMapItem item in items) + { + if (!item.MapInstance.RemoveDrop(item.TransportId)) + { + continue; + } + + item.BroadcastOut(); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceSelectRewardEventHandler.cs b/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceSelectRewardEventHandler.cs new file mode 100644 index 0000000..53b0d9c --- /dev/null +++ b/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceSelectRewardEventHandler.cs @@ -0,0 +1,156 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using PhoenixLib.Logging; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsAPI.Game.Extensions.Quests; +using WingsEmu.DTOs.Quests; +using WingsEmu.Game; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Quests; +using WingsEmu.Game.TimeSpaces; +using WingsEmu.Game.TimeSpaces.Events; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.TimeSpaces.Handlers; + +public class TimeSpaceSelectRewardEventHandler : IAsyncEventProcessor +{ + private readonly IDropRarityConfigurationProvider _dropRarityConfigurationProvider; + private readonly IGameItemInstanceFactory _gameItemInstanceFactory; + private readonly IGameLanguageService _gameLanguageService; + private readonly IItemsManager _itemsManager; + private readonly IQuestManager _questManager; + private readonly IRandomGenerator _randomGenerator; + + public TimeSpaceSelectRewardEventHandler(IRandomGenerator randomGenerator, IGameItemInstanceFactory gameItemInstanceFactory, IItemsManager itemsManager, + IDropRarityConfigurationProvider dropRarityConfigurationProvider, IGameLanguageService gameLanguageService, IQuestManager questManager) + { + _randomGenerator = randomGenerator; + _gameItemInstanceFactory = gameItemInstanceFactory; + _itemsManager = itemsManager; + _dropRarityConfigurationProvider = dropRarityConfigurationProvider; + _gameLanguageService = gameLanguageService; + _questManager = questManager; + } + + public async Task HandleAsync(TimeSpaceSelectRewardEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + + if (session.CurrentMapInstance.MapInstanceType != MapInstanceType.TimeSpaceInstance) + { + return; + } + + TimeSpaceParty timeSpace = session.PlayerEntity.TimeSpaceComponent.TimeSpace; + if (timeSpace?.Instance == null) + { + return; + } + + if (!timeSpace.Finished) + { + return; + } + + if (timeSpace.ClaimedRewards != null && timeSpace.ClaimedRewards.Contains(session.PlayerEntity.Id)) + { + return; + } + + var rewardsToGive = new List(); + TimeSpaceRewards timeSpaceReward = timeSpace.TimeSpaceInformation?.Rewards; + + if (timeSpaceReward == null) + { + Log.Error($"Couldn't take information about the Time-Space rewards for TS-Id: {timeSpace.TimeSpaceId} - PlayerId: {session.PlayerEntity.Id}", new Exception()); + return; + } + + TimeSpaceRewardItem randomDrawItem = null; + + if (timeSpaceReward.Draw != null && timeSpaceReward.Draw.Any() && !timeSpace.IsEasyMode) + { + int rand = _randomGenerator.RandomNumber(0, timeSpaceReward.Draw.Count); + TimeSpaceItemConfiguration randomDraw = timeSpaceReward.Draw[rand]; + randomDrawItem = new TimeSpaceRewardItem + { + Type = TimeSpaceRewardType.DRAW, + ItemVnum = randomDraw.ItemVnum, + Amount = randomDraw.Amount + }; + rewardsToGive.Add(randomDrawItem); + } + + if (timeSpace.FirstCompletedTimeSpaceIds.Contains(session.PlayerEntity.Id) && timeSpaceReward.Special != null) + { + foreach (TimeSpaceItemConfiguration special in timeSpaceReward.Special) + { + rewardsToGive.Add(new TimeSpaceRewardItem + { + Type = TimeSpaceRewardType.SPECIAL, + ItemVnum = special.ItemVnum, + Amount = special.Amount + }); + } + } + + if (timeSpaceReward.Bonus != null && !timeSpace.IsEasyMode) + { + foreach (TimeSpaceItemConfiguration bonus in timeSpaceReward.Bonus) + { + if (bonus.ItemVnum == (int)ItemVnums.SOUND_FLOWER) + { + if (session.GetEmptyQuestSlot(QuestSlotType.GENERAL) == -1) + { + session.SendMsg(_gameLanguageService.GetLanguage(GameDialogKey.QUEST_SHOUTMESSAGE_SLOT_FULL, session.UserLanguage), MsgMessageType.Middle); + } + else + { + session.PlayerEntity.IncreasePendingSoundFlowerQuests(); + session.RefreshQuestList(_questManager, null); + } + + continue; + } + + rewardsToGive.Add(new TimeSpaceRewardItem + { + Type = TimeSpaceRewardType.BONUS, + ItemVnum = bonus.ItemVnum, + Amount = bonus.Amount + }); + } + } + + timeSpace.ClaimedRewards?.Add(session.PlayerEntity.Id); + + foreach (TimeSpaceRewardItem item in rewardsToGive) + { + IGameItem gameItem = _itemsManager.GetItem(item.ItemVnum); + sbyte randomRarity = _dropRarityConfigurationProvider.GetRandomRarity(gameItem.ItemType); + item.Rarity = randomRarity; + + GameItemInstance itemToAdd = _gameItemInstanceFactory.CreateItem(item.ItemVnum, item.Amount, 0, randomRarity); + await session.AddNewItemToInventory(itemToAdd, sendGiftIsFull: true); + } + + if (e.SendRepayPacket) + { + session.SendRepayPacket(rewardsToGive, randomDrawItem); + } + + timeSpace.FinishTimeSpace(DateTime.UtcNow.AddMinutes(1)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceSetTimeEventHandler.cs b/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceSetTimeEventHandler.cs new file mode 100644 index 0000000..8b1456a --- /dev/null +++ b/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceSetTimeEventHandler.cs @@ -0,0 +1,36 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Game.TimeSpaces; +using WingsEmu.Game.TimeSpaces.Events; + +namespace Plugin.TimeSpaces.Handlers; + +public class TimeSpaceSetTimeEventHandler : IAsyncEventProcessor +{ + public async Task HandleAsync(TimeSpaceSetTimeEvent e, CancellationToken cancellation) + { + TimeSpaceParty timeSpace = e.TimeSpaceParty; + TimeSpan time = e.Time; + + if (timeSpace?.Instance == null) + { + return; + } + + if (timeSpace.Finished || !timeSpace.Started) + { + return; + } + + timeSpace.Instance.UpdateFinishDate(time); + timeSpace.Instance.InfiniteDuration = false; + foreach (IClientSession member in timeSpace.Members) + { + member.SendTsClockPacket(timeSpace.Instance.TimeUntilEnd, true); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceStartClockEventHandler.cs b/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceStartClockEventHandler.cs new file mode 100644 index 0000000..f03e56b --- /dev/null +++ b/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceStartClockEventHandler.cs @@ -0,0 +1,38 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Networking; +using WingsEmu.Game.TimeSpaces; +using WingsEmu.Game.TimeSpaces.Events; + +namespace Plugin.TimeSpaces.Handlers; + +public class TimeSpaceStartClockEventHandler : IAsyncEventProcessor +{ + public async Task HandleAsync(TimeSpaceStartClockEvent e, CancellationToken cancellation) + { + TimeSpaceParty timeSpaceParty = e.TimeSpaceParty; + + if (timeSpaceParty?.Instance == null) + { + return; + } + + if (timeSpaceParty.Instance.InfiniteDuration) + { + return; + } + + foreach (IClientSession session in timeSpaceParty.Members) + { + if (session.CurrentMapInstance.MapInstanceType != MapInstanceType.TimeSpaceInstance) + { + continue; + } + + session.SendTsClockPacket(timeSpaceParty.Instance.TimeUntilEnd, e.IsVisible); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceStartPortalEventHandler.cs b/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceStartPortalEventHandler.cs new file mode 100644 index 0000000..e294d78 --- /dev/null +++ b/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceStartPortalEventHandler.cs @@ -0,0 +1,81 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using PhoenixLib.Logging; +using WingsEmu.Game; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Networking; +using WingsEmu.Game.TimeSpaces; +using WingsEmu.Game.TimeSpaces.Events; + +namespace Plugin.TimeSpaces.Handlers; + +public class TimeSpaceStartPortalEventHandler : IAsyncEventProcessor +{ + private readonly IAsyncEventPipeline _eventPipeline; + private readonly ITimeSpaceManager _timeSpaceManager; + + public TimeSpaceStartPortalEventHandler(IAsyncEventPipeline eventPipeline, ITimeSpaceManager timeSpaceManager) + { + _eventPipeline = eventPipeline; + _timeSpaceManager = timeSpaceManager; + } + + public async Task HandleAsync(TimeSpaceStartPortalEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + + if (!session.HasCurrentMapInstance || session.CurrentMapInstance.MapInstanceType != MapInstanceType.TimeSpaceInstance) + { + return; + } + + IPortalEntity portal = session.CurrentMapInstance.Portals.FirstOrDefault(s => + session.PlayerEntity.PositionY >= s.PositionY - 1 && + session.PlayerEntity.PositionY <= s.PositionY + 1 && + session.PlayerEntity.PositionX >= s.PositionX - 1 && + session.PlayerEntity.PositionX <= s.PositionX + 1); + + if (portal == null) + { + Log.Debug("Portal not found"); + return; + } + + if (portal.DestinationMapInstance == null) + { + return; + } + + if (!session.PlayerEntity.TimeSpaceComponent.IsInTimeSpaceParty) + { + return; + } + + TimeSpaceParty timeSpace = session.PlayerEntity.TimeSpaceComponent.TimeSpace; + if (timeSpace == null) + { + return; + } + + if (timeSpace.Leader?.PlayerEntity.Id != session.PlayerEntity.Id) + { + return; + } + + session.PlayerEntity.LastPortal = DateTime.UtcNow; + + if (!timeSpace.Started) + { + timeSpace.StartTimeSpace(); + await _eventPipeline.ProcessEventAsync(new TimeSpaceStartClockEvent(session.PlayerEntity.TimeSpaceComponent.TimeSpace, true)); + session.SendPacket(session.CurrentMapInstance.GenerateRsfn(isVisit: true)); + } + + // This is how a normal TS portal should work. Anyways, it can be modified to add portals that TP on the same map, etc... + session.ChangeMap(portal.DestinationMapInstance, portal.DestinationX, portal.DestinationY); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceStartTaskEventHandler.cs b/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceStartTaskEventHandler.cs new file mode 100644 index 0000000..9077863 --- /dev/null +++ b/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceStartTaskEventHandler.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Entities.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Game.TimeSpaces; +using WingsEmu.Game.TimeSpaces.Enums; +using WingsEmu.Game.TimeSpaces.Events; +using WingsEmu.Packets.Enums; + +namespace Plugin.TimeSpaces.Handlers; + +public class TimeSpaceStartTaskEventHandler : IAsyncEventProcessor +{ + private readonly IAsyncEventPipeline _asyncEventPipeline; + private readonly IRandomGenerator _randomGenerator; + + public TimeSpaceStartTaskEventHandler(IAsyncEventPipeline asyncEventPipeline, IRandomGenerator randomGenerator) + { + _asyncEventPipeline = asyncEventPipeline; + _randomGenerator = randomGenerator; + } + + public async Task HandleAsync(TimeSpaceStartTaskEvent e, CancellationToken cancellation) + { + TimeSpaceSubInstance room = e.TimeSpaceSubInstance; + if (room.Task == null) + { + return; + } + + if (room.Task.IsFinished) + { + return; + } + + room.TimeSpaceWave ??= DateTime.UtcNow; + if (room.Task.IsActivated) + { + SendWarnings(room); + return; + } + + TimeSpaceTask task = room.Task; + DateTime now = DateTime.UtcNow; + + if (!task.IsActivated && task.TimeLeft == null && task.Time.HasValue) + { + task.TimeLeft = now + task.Time.Value; + } + + SendWarnings(room); + task.IsActivated = true; + task.TaskStart = now; + room.MapInstance.AIDisabled = false; + + foreach (IMonsterEntity monster in room.MapInstance.GetAliveMonsters()) + { + monster.NextTick = now + TimeSpan.FromMilliseconds(_randomGenerator.RandomNumber(1000)); + } + + foreach (INpcEntity npc in room.MapInstance.GetAliveNpcs()) + { + npc.NextTick = now + TimeSpan.FromMilliseconds(_randomGenerator.RandomNumber(1000)); + } + + if (!room.Task.MonstersAfterTaskStart.Any()) + { + return; + } + + var toRemove = new List<(int?, IMonsterEntity)>(); + foreach ((int? neededKills, IMonsterEntity monster) in room.Task.MonstersAfterTaskStart) + { + if (neededKills.HasValue) + { + continue; + } + + await monster.EmitEventAsync(new MapJoinMonsterEntityEvent(monster, monster.Position.X, monster.Position.Y)); + toRemove.Add((null, monster)); + } + + foreach ((int? kills, IMonsterEntity monster) in toRemove) + { + room.Task.MonstersAfterTaskStart.Remove((kills, monster)); + } + + await _asyncEventPipeline.ProcessEventAsync(new TimeSpaceBonusMonsterEvent + { + TimeSpaceSubInstance = room, + MonsterEntities = room.MapInstance.GetAliveMonsters() + }); + } + + private void SendWarnings(TimeSpaceSubInstance timeSpaceSubInstance) + { + TimeSpaceTask task = timeSpaceSubInstance.Task; + if (task.TimeLeft.HasValue && task.Time.HasValue) + { + TimeSpan timeLeft = task.TimeLeft.Value - DateTime.UtcNow; + if (timeLeft.TotalMilliseconds > 0) + { + foreach (IClientSession sessionOnMap in timeSpaceSubInstance.MapInstance.Sessions) + { + sessionOnMap.SendClockPacket(ClockType.RedMiddle, 0, timeLeft, task.Time.Value); + } + } + } + + if (timeSpaceSubInstance.Task.TaskType != TimeSpaceTaskType.None) + { + foreach (IClientSession sessionOnMap in timeSpaceSubInstance.MapInstance.Sessions) + { + sessionOnMap.SendRsfmPacket(!timeSpaceSubInstance.Task.IsActivated ? TimeSpaceAction.WARNING_WITH_SOUND : TimeSpaceAction.WARNING_WITHOUT_SOUND); + } + } + + if (string.IsNullOrEmpty(task.GameDialogKey)) + { + return; + } + + foreach (IClientSession sessionOnMap in timeSpaceSubInstance.MapInstance.Sessions) + { + string message = sessionOnMap.GetLanguage(task.GameDialogKey); + sessionOnMap.SendMissionTargetMessage(message); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceTogglePortalEventHandler.cs b/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceTogglePortalEventHandler.cs new file mode 100644 index 0000000..955a181 --- /dev/null +++ b/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceTogglePortalEventHandler.cs @@ -0,0 +1,74 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.PacketGeneration; +using WingsEmu.Game; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Game.TimeSpaces; +using WingsEmu.Game.TimeSpaces.Events; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.TimeSpaces.Handlers; + +public class TimeSpaceTogglePortalEventHandler : IAsyncEventProcessor +{ + private readonly ITimeSpaceManager _timeSpaceManager; + + public TimeSpaceTogglePortalEventHandler(ITimeSpaceManager timeSpaceManager) => _timeSpaceManager = timeSpaceManager; + + public async Task HandleAsync(TimeSpaceTogglePortalEvent e, CancellationToken cancellation) + { + IPortalEntity portal = e.PortalEntity; + TimeSpaceSubInstance timeSpaceSubInstance = _timeSpaceManager.GetSubInstance(portal.MapInstance.Id); + + if (timeSpaceSubInstance == null) + { + return; + } + + PortalType type = portal.Type switch + { + PortalType.Closed => PortalType.Open, + PortalType.Open => PortalType.Closed, + PortalType.TSEndClosed => PortalType.TSEnd, + PortalType.TSEnd => PortalType.TSEndClosed, + _ => PortalType.Open + }; + + portal.Type = type; + timeSpaceSubInstance.MapInstance.MapClear(true); + timeSpaceSubInstance.MapInstance.BroadcastTimeSpacePartnerInfo(); + + if (type is PortalType.Closed or PortalType.TSEndClosed) + { + return; + } + + if (!timeSpaceSubInstance.MapInstance.Sessions.Any()) + { + timeSpaceSubInstance.SendPortalOpenMessage = true; + + TimeSpaceParty timeSpace = _timeSpaceManager.GetTimeSpaceByMapInstanceId(portal.MapInstance.Id); + if (timeSpace == null) + { + return; + } + + foreach (IClientSession session in timeSpace.Members.Where(session => session.CurrentMapInstance.Id != portal.MapInstance.Id)) + { + session.SendMsg(session.GetLanguage(GameDialogKey.TIMESPACE_SHOUTMESSAGE_DOOR_OPENED_SOMEWHERE), MsgMessageType.Middle); + } + + return; + } + + foreach (IClientSession session in portal.MapInstance.Sessions) + { + session.SendMsg(session.GetLanguage(GameDialogKey.TIMESPACE_SHOUTMESSAGE_DOOR_OPENED), MsgMessageType.Middle); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceTryFinishTaskEventHandler.cs b/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceTryFinishTaskEventHandler.cs new file mode 100644 index 0000000..d204c2f --- /dev/null +++ b/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceTryFinishTaskEventHandler.cs @@ -0,0 +1,153 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Scripting.Object.Timespace; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Game.TimeSpaces; +using WingsEmu.Game.TimeSpaces.Enums; +using WingsEmu.Game.TimeSpaces.Events; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.TimeSpaces.Handlers; + +public class TimeSpaceTryFinishTaskEventHandler : IAsyncEventProcessor +{ + private readonly IAsyncEventPipeline _asyncEventPipeline; + + public TimeSpaceTryFinishTaskEventHandler(IAsyncEventPipeline asyncEventPipeline) => _asyncEventPipeline = asyncEventPipeline; + + public async Task HandleAsync(TimeSpaceTryFinishTaskEvent e, CancellationToken cancellation) + { + TimeSpaceSubInstance room = e.TimeSpaceSubInstance; + TimeSpaceTask task = room.Task; + TimeSpaceParty timeSpaceParty = e.TimeSpaceParty; + + if (task.TaskStart.AddSeconds(1) > DateTime.UtcNow && task.TaskType != TimeSpaceTaskType.None) + { + return; + } + + switch (task.TaskType) + { + case TimeSpaceTaskType.None: + room.Task.IsFinished = true; + await room.TriggerEvents(TimespaceConstEventKeys.OnTaskFinish); + BroadcastEndDialog(room, timeSpaceParty); + return; + case TimeSpaceTaskType.KillAllMonsters: + + bool noMonsterToSpawn = room.SpawnAfterMobsKilled.Count == 0 && task.MonstersAfterTaskStart.Count == 0; + int monsters = room.MapInstance.GetAliveMonsters(m => m.SummonerType is not VisualType.Player).Count; + + if (task.Time == null || task.TimeLeft == null) + { + if (monsters > 0 || !noMonsterToSpawn) + { + return; + } + + room.Task.IsFinished = true; + await room.TriggerEvents(TimespaceConstEventKeys.OnTaskFinish); + BroadcastEndDialog(room, timeSpaceParty); + await IncreaseScore(timeSpaceParty); + room.MapInstance.Broadcast(x => x.GenerateRemoveRedClock()); + return; + } + + if (monsters <= 0 && noMonsterToSpawn) + { + room.Task.IsFinished = true; + await room.TriggerEvents(TimespaceConstEventKeys.OnTaskFinish); + BroadcastEndDialog(room, timeSpaceParty); + await IncreaseScore(timeSpaceParty); + room.MapInstance.Broadcast(x => x.GenerateRemoveRedClock()); + return; + } + + TimeSpan timeLeft = task.TimeLeft.Value - DateTime.UtcNow; + if (timeLeft.TotalMilliseconds > 0) + { + return; + } + + room.Task.IsFinished = true; + await room.TriggerEvents(TimespaceConstEventKeys.OnTaskFail); + room.MapInstance.Broadcast(x => x.GenerateRemoveRedClock()); + + break; + case TimeSpaceTaskType.Survive: + if (task.Time == null || task.TimeLeft == null) + { + room.Task.IsFinished = true; + await room.TriggerEvents(TimespaceConstEventKeys.OnTaskFinish); + BroadcastEndDialog(room, timeSpaceParty); + await IncreaseScore(timeSpaceParty); + room.MapInstance.Broadcast(x => x.GenerateRemoveRedClock()); + return; + } + + timeLeft = task.TimeLeft.Value - DateTime.UtcNow; + if (timeLeft.TotalMilliseconds > 0) + { + return; + } + + room.Task.IsFinished = true; + await room.TriggerEvents(TimespaceConstEventKeys.OnTaskFinish); + BroadcastEndDialog(room, timeSpaceParty); + await IncreaseScore(timeSpaceParty); + room.MapInstance.Broadcast(x => x.GenerateRemoveRedClock()); + break; + } + } + + private async Task IncreaseScore(TimeSpaceParty timeSpaceParty) + { + await _asyncEventPipeline.ProcessEventAsync(new TimeSpaceIncreaseScoreEvent + { + TimeSpaceParty = timeSpaceParty, + AmountToIncrease = 50 + }); + } + + private void BroadcastEndDialog(TimeSpaceSubInstance timeSpaceSubInstance, TimeSpaceParty timeSpace) + { + foreach (IClientSession session in timeSpaceSubInstance.MapInstance.Sessions) + { + session.SendMissionTargetMessage(string.Empty); + if (timeSpaceSubInstance.Task.TaskType != TimeSpaceTaskType.None) + { + session.SendMsg(session.GetLanguage(GameDialogKey.TIMESPACE_SHOUTMESSAGE_QUICK_MISSION_DONE), MsgMessageType.BottomCard); + } + } + + TimeSpaceTask task = timeSpaceSubInstance.Task; + if (task.EndDialog.HasValue) + { + foreach (IClientSession session in timeSpaceSubInstance.MapInstance.Sessions) + { + session.SendNpcReqPacket(task.EndDialog.Value); + } + + timeSpace.Instance.StartTimeFreeze = DateTime.UtcNow; + foreach (IClientSession member in timeSpace.Members) + { + member.SendTimerFreeze(); + } + } + + if (string.IsNullOrEmpty(task.EndDialogShout)) + { + return; + } + + foreach (IClientSession session in timeSpaceSubInstance.MapInstance.Sessions) + { + session.SendMsg(session.GetLanguage(task.EndDialogShout), MsgMessageType.Middle); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceTryStartHiddenEventHandler.cs b/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceTryStartHiddenEventHandler.cs new file mode 100644 index 0000000..3f66270 --- /dev/null +++ b/srcs/_plugins/Plugin.TimeSpaces/Handlers/TimeSpaceTryStartHiddenEventHandler.cs @@ -0,0 +1,224 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsAPI.Game.Extensions.ItemExtension.Item; +using WingsAPI.Game.Extensions.PacketGeneration; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Groups; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Networking; +using WingsEmu.Game.TimeSpaces; +using WingsEmu.Game.TimeSpaces.Enums; +using WingsEmu.Game.TimeSpaces.Events; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.TimeSpaces.Handlers; + +public class TimeSpaceTryStartHiddenEventHandler : IAsyncEventProcessor +{ + private readonly IGameLanguageService _gameLanguage; + private readonly IItemsManager _itemsManager; + private readonly ITimeSpaceManager _timeSpaceManager; + + public TimeSpaceTryStartHiddenEventHandler(IItemsManager itemsManager, IGameLanguageService gameLanguage, ITimeSpaceManager timeSpaceManager) + { + _itemsManager = itemsManager; + _gameLanguage = gameLanguage; + _timeSpaceManager = timeSpaceManager; + } + + public async Task HandleAsync(TimeSpaceTryStartHiddenEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + INpcEntity timeSpacePortal = e.TimeSpacePortal; + bool isChallengeMode = e.IsChallengeMode; + bool isSoloTimeSpace = timeSpacePortal.TimeSpaceInfo.MinPlayers == 1 && timeSpacePortal.TimeSpaceInfo.MaxPlayers == 1; + + if (session.PlayerEntity.IsInRaidParty) + { + return; + } + + if (session.PlayerEntity.HasShopOpened) + { + return; + } + + if (session.IsMuted()) + { + session.SendMsg(session.GetLanguage(GameDialogKey.MUTE_SHOUTMESSAGE_YOU_ARE_MUTED), MsgMessageType.Middle); + return; + } + + if (!session.PlayerEntity.MapInstance.HasMapFlag(MapFlags.IS_BASE_MAP)) + { + session.SendMsg(session.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_MUST_BE_IN_CLASSIC_MAP), MsgMessageType.Middle); + return; + } + + TimeSpaceFileConfiguration timeSpace = timeSpacePortal.TimeSpaceInfo; + + if (timeSpace.SeedsOfPowerRequired != 0) + { + if (!session.PlayerEntity.HasItem((short)ItemVnums.SEED_OF_POWER, timeSpace.SeedsOfPowerRequired)) + { + string itemName = _itemsManager.GetItem((short)ItemVnums.SEED_OF_POWER).GetItemName(_gameLanguage, session.UserLanguage); + session.SendChatMessage(session.GetLanguageFormat(GameDialogKey.INVENTORY_SHOUTMESSAGE_NOT_ENOUGH_ITEMS, timeSpace.SeedsOfPowerRequired, itemName), ChatMessageColorType.PlayerSay); + return; + } + } + + if (isSoloTimeSpace) + { + if (session.PlayerEntity.Level < timeSpace.MinLevel) + { + session.SendChatMessage(session.GetLanguage(GameDialogKey.TIMESPACE_CHATMESSAGE_LOW_LEVEL), ChatMessageColorType.PlayerSay); + return; + } + + if (session.PlayerEntity.Level > timeSpace.MaxLevel) + { + session.SendChatMessage(session.GetLanguage(GameDialogKey.TIMESPACE_CHATMESSAGE_HIGH_LEVEL), ChatMessageColorType.PlayerSay); + return; + } + + if (e.IsChallengeMode) + { + if (!session.PlayerEntity.RemoveGold(timeSpace.MinLevel * 50)) + { + session.SendChatMessage(session.GetLanguage(GameDialogKey.INTERACTION_MESSAGE_NOT_ENOUGH_GOLD), ChatMessageColorType.PlayerSay); + return; + } + + ; + } + + if (timeSpace.SeedsOfPowerRequired != 0) + { + await session.RemoveItemFromInventory((short)ItemVnums.SEED_OF_POWER, timeSpace.SeedsOfPowerRequired); + } + + var timeSpaceParty = new TimeSpaceParty(timeSpace, false, isChallengeMode); + + timeSpaceParty.AddMember(session); + _timeSpaceManager.AddTimeSpace(timeSpaceParty); + session.PlayerEntity.TimeSpaceComponent.SetTimeSpaceParty(timeSpaceParty); + + await session.EmitEventAsync(new TimeSpaceInstanceStartEvent()); + return; + } + + if (!session.PlayerEntity.IsInGroup()) + { + return; + } + + PlayerGroup group = session.PlayerEntity.GetGroup(); + if (group.Members.Count < timeSpacePortal.TimeSpaceInfo.MinPlayers) + { + session.SendMsg(session.GetLanguage(GameDialogKey.TIMESPACE_SHOUTMESSAGE_NOT_ENOUGH_PLAYERS), MsgMessageType.Middle); + return; + } + + foreach (IPlayerEntity member in group.Members) + { + if (member.MapInstance.Id != session.CurrentMapInstance.Id) + { + session.SendMsg(session.GetLanguage(GameDialogKey.TIMESPACE_SHOUTMESSAGE_NOT_THE_SAME_MAP), MsgMessageType.Middle); + return; + } + + if (!member.MapInstance.HasMapFlag(MapFlags.IS_BASE_MAP)) + { + session.SendMsg(session.GetLanguage(GameDialogKey.TIMESPACE_SHOUTMESSAGE_MEMBER_NOT_IN_CLASSIC_MAP), MsgMessageType.Middle); + return; + } + + if (member.Level < timeSpace.MinLevel) + { + member.Session.SendMsg(member.Session.GetLanguage(GameDialogKey.TIMESPACE_CHATMESSAGE_LOW_LEVEL), MsgMessageType.Middle); + return; + } + + if (member.Level > timeSpace.MaxLevel) + { + member.Session.SendMsg(member.Session.GetLanguage(GameDialogKey.TIMESPACE_CHATMESSAGE_HIGH_LEVEL), MsgMessageType.Middle); + return; + } + } + + if (e.IsChallengeMode) + { + if (!session.PlayerEntity.RemoveGold(timeSpace.MinLevel * 50)) + { + session.SendChatMessage(session.GetLanguage(GameDialogKey.INTERACTION_MESSAGE_NOT_ENOUGH_GOLD), ChatMessageColorType.PlayerSay); + return; + } + + ; + } + + if (timeSpace.SeedsOfPowerRequired != 0) + { + await session.RemoveItemFromInventory((short)ItemVnums.SEED_OF_POWER, timeSpace.SeedsOfPowerRequired); + } + + var timeSpacePartyGroup = new TimeSpaceParty(timeSpace, false, isChallengeMode); + + IEnumerable members = group.Members.Select(x => x.Session).ToArray(); + foreach (IClientSession memberSession in members) + { + timeSpacePartyGroup.AddMember(memberSession); + } + + _timeSpaceManager.AddTimeSpace(timeSpacePartyGroup); + + foreach (IPlayerEntity member in group.Members) + { + member.TimeSpaceComponent.SetTimeSpaceParty(timeSpacePartyGroup); + } + + await session.EmitEventAsync(new TimeSpaceInstanceStartEvent()); + + foreach (IPlayerEntity member in group.Members) + { + if (member.Id == session.PlayerEntity.Id) + { + continue; + } + + IMapInstance mapStart = timeSpacePartyGroup.Instance.SpawnInstance.MapInstance; + IClientSession memberSession = member.Session; + + memberSession.ChangeMap(mapStart, timeSpacePartyGroup.Instance.SpawnPoint.X, timeSpacePartyGroup.Instance.SpawnPoint.Y); + memberSession.SendPacket(mapStart.GenerateRsfn(true, false)); + + foreach (KeyValuePair mapInstance in timeSpacePartyGroup.Instance.TimeSpaceSubInstances) + { + memberSession.SendPacket(mapInstance.Value.MapInstance.GenerateRsfn(isVisit: false)); + } + + memberSession.SendRsfpPacket(); + mapStart.MapClear(true); + mapStart.BroadcastTimeSpacePartnerInfo(); + memberSession.SendRsfmPacket(TimeSpaceAction.CAMERA_ADJUST); + memberSession.SendMinfo(); + + if (memberSession.PlayerEntity.Level > timeSpacePartyGroup.HigherLevel) + { + timeSpacePartyGroup.HigherLevel = memberSession.PlayerEntity.Level; + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.TimeSpaces/Handlers/TryStartTaskForMapEventHandler.cs b/srcs/_plugins/Plugin.TimeSpaces/Handlers/TryStartTaskForMapEventHandler.cs new file mode 100644 index 0000000..6b3c0ab --- /dev/null +++ b/srcs/_plugins/Plugin.TimeSpaces/Handlers/TryStartTaskForMapEventHandler.cs @@ -0,0 +1,43 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.TimeSpaces; +using WingsEmu.Game.TimeSpaces.Events; + +namespace Plugin.TimeSpaces.Handlers; + +public class TryStartTaskForMapEventHandler : IAsyncEventProcessor +{ + private readonly IAsyncEventPipeline _asyncEventPipeline; + + public TryStartTaskForMapEventHandler(IAsyncEventPipeline asyncEventPipeline) => _asyncEventPipeline = asyncEventPipeline; + + public async Task HandleAsync(TryStartTaskEvent e, CancellationToken cancellation) + { + TimeSpaceSubInstance map = e.Map; + if (map.Task == null) + { + return; + } + + if (map.Task.IsActivated) + { + return; + } + + if (map.Task.IsFinished) + { + return; + } + + if (map.Task.StartDialog.HasValue && map.Task.DialogStartTask) + { + return; + } + + await _asyncEventPipeline.ProcessEventAsync(new TimeSpaceStartTaskEvent + { + TimeSpaceSubInstance = map + }, cancellation); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.TimeSpaces/LuaTimeSpaceScriptManager.cs b/srcs/_plugins/Plugin.TimeSpaces/LuaTimeSpaceScriptManager.cs new file mode 100644 index 0000000..374c058 --- /dev/null +++ b/srcs/_plugins/Plugin.TimeSpaces/LuaTimeSpaceScriptManager.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.IO; +using MoonSharp.Interpreter; +using PhoenixLib.Logging; +using WingsAPI.Scripting; +using WingsAPI.Scripting.Attribute; +using WingsAPI.Scripting.LUA; +using WingsAPI.Scripting.Object.Timespace; +using WingsAPI.Scripting.ScriptManager; + +namespace Plugin.TimeSpaces; + +[ScriptObject] +public class DebuggableScriptGenerator +{ + public int TimeSpaceId { get; set; } + public Closure GenerateMission { get; set; } +} + +public class LuaTimeSpaceScriptManager : ITimeSpaceScriptManager +{ + private readonly Dictionary _cache = new(); + private readonly IScriptFactory _scriptFactory; + private readonly ScriptFactoryConfiguration _scriptFactoryConfiguration; + private readonly STimeSpaceValidator _validator; + + public LuaTimeSpaceScriptManager(IScriptFactory scriptFactory, ScriptFactoryConfiguration scriptFactoryConfiguration, STimeSpaceValidator validator) + { + _scriptFactory = scriptFactory; + _scriptFactoryConfiguration = scriptFactoryConfiguration; + _validator = validator; + } + + public void Load() + { + IEnumerable files = Directory.GetFiles(_scriptFactoryConfiguration.TimeSpacesDirectory, "*.lua"); + foreach (string file in files) + { + try + { + ScriptTimeSpace raid = _scriptFactory.LoadScript(file); + if (raid == null) + { + Log.Warn($"Failed to load timespace script {file}"); + continue; + } + + Log.Warn($"[TIMESPACE_SCRIPT_MANAGER] Loaded {Path.GetFileName(file)} for timespace: {raid.TimeSpaceId}"); + _cache[raid.TimeSpaceId] = raid; + } + catch (InvalidScriptException e) + { + Log.Error($"[TIMESPACE_SCRIPT_MANAGER] InvalidScript: {file}", e); + } + catch (ScriptRuntimeException e) + { + Log.Error($"[TIMESPACE_SCRIPT_MANAGER][SCRIPT ERROR] {file}, {e.DecoratedMessage}", e); + } + catch (Exception e) + { + Log.Error($"[TIMESPACE_SCRIPT_MANAGER][ERROR] {file}", e); + } + } + + Log.Info($"Loaded {_cache.Count} timespace from scripts"); + } + + public ScriptTimeSpace GetScriptedTimeSpace(long id) + { + if (!_cache.TryGetValue(id, out ScriptTimeSpace timeSpace)) + { + return null; + } + + return timeSpace; + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.TimeSpaces/Plugin.TimeSpaces.csproj b/srcs/_plugins/Plugin.TimeSpaces/Plugin.TimeSpaces.csproj new file mode 100644 index 0000000..a5be995 --- /dev/null +++ b/srcs/_plugins/Plugin.TimeSpaces/Plugin.TimeSpaces.csproj @@ -0,0 +1,20 @@ + + + + net5.0 + latest + + + + + + + + + + + + + + + diff --git a/srcs/_plugins/Plugin.TimeSpaces/RecurrentJob/TimeSpaceSystem.cs b/srcs/_plugins/Plugin.TimeSpaces/RecurrentJob/TimeSpaceSystem.cs new file mode 100644 index 0000000..2164384 --- /dev/null +++ b/srcs/_plugins/Plugin.TimeSpaces/RecurrentJob/TimeSpaceSystem.cs @@ -0,0 +1,223 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using PhoenixLib.Events; +using PhoenixLib.Logging; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Monster.Event; +using WingsEmu.Game.Networking; +using WingsEmu.Game.TimeSpaces; +using WingsEmu.Game.TimeSpaces.Enums; +using WingsEmu.Game.TimeSpaces.Events; +using WingsEmu.Packets.Enums.Chat; + +namespace Plugin.TimeSpaces.RecurrentJob; + +public class TimeSpaceSystem : BackgroundService +{ + private static readonly TimeSpan Interval = TimeSpan.FromSeconds(1); + private readonly IAsyncEventPipeline _eventPipeline; + private readonly ITimeSpaceManager _timeSpaceManager; + + public TimeSpaceSystem(IAsyncEventPipeline eventPipeline, ITimeSpaceManager timeSpaceManager) + { + _eventPipeline = eventPipeline; + _timeSpaceManager = timeSpaceManager; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + Log.Info("[TIMESPACE_SYSTEM] Started!"); + while (!stoppingToken.IsCancellationRequested) + { + try + { + foreach (TimeSpaceParty timeSpace in _timeSpaceManager.GetTimeSpaces()) + { + await ProcessTimeSpace(timeSpace); + } + } + catch (Exception e) + { + Log.Error("[TIMESPACE_SYSTEM]", e); + } + + await Task.Delay(Interval, stoppingToken); + } + } + + private async Task ProcessTimeSpace(TimeSpaceParty timeSpaceParty) + { + if (timeSpaceParty.Instance == null) + { + return; + } + + DateTime currentDate = DateTime.UtcNow; + await TryFinish(timeSpaceParty, currentDate); + await TryRemove(timeSpaceParty, currentDate); + await ProcessObjectivesCheck(timeSpaceParty, currentDate); + await ProcessPreDialog(timeSpaceParty, currentDate); + foreach (TimeSpaceSubInstance room in timeSpaceParty.Instance.TimeSpaceSubInstances.Values) + { + await TryFinishRoomTask(timeSpaceParty, room, currentDate); + await CheckBonusMonster(room); + await ProcessMonsterWave(room, currentDate); + } + } + + private async Task ProcessMonsterWave(TimeSpaceSubInstance room, DateTime currentDate) + { + TimeSpaceWave wave = room.TimeSpaceWaves.FirstOrDefault(); + if (wave == null || room.TimeSpaceWave == null || currentDate < room.TimeSpaceWave + wave.Delay) + { + return; + } + + room.TimeSpaceWaves.Remove(wave); + foreach (IClientSession session in room.MapInstance.Sessions) + { + session.SendMsg(session.GetLanguage(GameDialogKey.TIMESPACE_SHOUTMESSAGE_ENEMIES_REINFORCEMENTS), MsgMessageType.Middle); + } + + await _eventPipeline.ProcessEventAsync(new MonsterSummonEvent(room.MapInstance, wave.Monsters, showEffect: true)); + } + + private async Task ProcessPreDialog(TimeSpaceParty timeSpaceParty, DateTime currentDate) + { + if (!timeSpaceParty.Instance.PreFinishDialog.HasValue) + { + return; + } + + if (timeSpaceParty.Instance.PreFinishDialogTime == null) + { + return; + } + + if (timeSpaceParty.Instance.PreFinishDialogTime > currentDate) + { + return; + } + + if (timeSpaceParty.Instance.PreFinishDialogShown) + { + return; + } + + timeSpaceParty.Instance.PreFinishDialogShown = true; + foreach (IClientSession member in timeSpaceParty.Members) + { + member.SendNpcReqPacket(timeSpaceParty.Instance.PreFinishDialog.Value); + } + } + + private async Task ProcessObjectivesCheck(TimeSpaceParty timeSpaceParty, DateTime dateTime) + { + if (!timeSpaceParty.Started || timeSpaceParty.Finished || timeSpaceParty.Destroy) + { + return; + } + + if (timeSpaceParty.LastObjectivesCheck > dateTime) + { + return; + } + + await _eventPipeline.ProcessEventAsync(new TimeSpaceCheckObjectivesEvent + { + TimeSpaceParty = timeSpaceParty + }); + } + + private async Task CheckBonusMonster(TimeSpaceSubInstance room) + { + if (room.MonsterBonusId.HasValue) + { + return; + } + + await _eventPipeline.ProcessEventAsync(new TimeSpaceBonusMonsterEvent + { + MonsterEntities = room.MapInstance.GetAliveMonsters(), + TimeSpaceSubInstance = room + }); + } + + private async Task TryFinishRoomTask(TimeSpaceParty timeSpaceParty, TimeSpaceSubInstance room, DateTime dateTime) + { + if (timeSpaceParty.Finished) + { + return; + } + + if (room.MapInstance.Sessions.Count < 1) + { + return; + } + + if (room.Task == null) + { + return; + } + + if (!room.Task.IsActivated) + { + return; + } + + if (room.Task.IsFinished) + { + return; + } + + if (room.LastTryFinishTime > dateTime) + { + return; + } + + await _eventPipeline.ProcessEventAsync(new TimeSpaceTryFinishTaskEvent(room, timeSpaceParty)); + } + + private async Task TryRemove(TimeSpaceParty timeSpaceParty, DateTime currentDate) + { + if (timeSpaceParty.Members.Count == 0) + { + Log.Warn("[TIMESPACE_SYSTEM] Destroying Time-Space instance"); + await _eventPipeline.ProcessEventAsync(new TimeSpaceDestroyEvent(timeSpaceParty)); + return; + } + + if (!timeSpaceParty.Finished) + { + return; + } + + if (!timeSpaceParty.Started) + { + return; + } + + if (timeSpaceParty.Instance.RemoveDate > currentDate) + { + return; + } + + Log.Warn("[TIMESPACE_SYSTEM] Destroying Time-Space instance"); + await _eventPipeline.ProcessEventAsync(new TimeSpaceDestroyEvent(timeSpaceParty)); + } + + private async Task TryFinish(TimeSpaceParty timeSpaceParty, DateTime currentTime) + { + if (!timeSpaceParty.Started || timeSpaceParty.Finished || timeSpaceParty.Instance.FinishDate > currentTime || timeSpaceParty.Instance.StartTimeFreeze.HasValue + || timeSpaceParty.Instance.InfiniteDuration) + { + return; + } + + await _eventPipeline.ProcessEventAsync(new TimeSpaceInstanceFinishEvent(timeSpaceParty, TimeSpaceFinishType.TIME_IS_UP)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.TimeSpaces/STimeSpaceValidator.cs b/srcs/_plugins/Plugin.TimeSpaces/STimeSpaceValidator.cs new file mode 100644 index 0000000..0bbfe94 --- /dev/null +++ b/srcs/_plugins/Plugin.TimeSpaces/STimeSpaceValidator.cs @@ -0,0 +1,18 @@ +using FluentValidation; +using WingsAPI.Scripting.Object.Timespace; +using WingsAPI.Scripting.Validator.Common; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Maps; + +namespace Plugin.TimeSpaces; + +public class STimeSpaceValidator : AbstractValidator +{ + public STimeSpaceValidator(IMapManager mapManager, INpcMonsterManager npcManager, IItemsManager itemsManager) + { + RuleFor(x => x.Id).NotEmpty(); + RuleFor(x => x.Maps).NotEmpty(); + RuleFor(x => x.Spawn).SetValidator(new SLocationValidator()); + RuleForEach(x => x.Maps).SetValidator(new SMapValidator(mapManager, npcManager, itemsManager)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.TimeSpaces/Scripting/SAddTimeEventConverter.cs b/srcs/_plugins/Plugin.TimeSpaces/Scripting/SAddTimeEventConverter.cs new file mode 100644 index 0000000..55d2824 --- /dev/null +++ b/srcs/_plugins/Plugin.TimeSpaces/Scripting/SAddTimeEventConverter.cs @@ -0,0 +1,22 @@ +using System; +using PhoenixLib.Events; +using WingsAPI.Scripting.Converter; +using WingsAPI.Scripting.Event.TimeSpace; +using WingsEmu.Game.TimeSpaces; +using WingsEmu.Game.TimeSpaces.Events; + +namespace Plugin.TimeSpaces.Scripting; + +public class SAddTimeEventConverter : ScriptedEventConverter +{ + private readonly TimeSpaceParty _timeSpaceParty; + + public SAddTimeEventConverter(TimeSpaceParty timeSpaceParty) => _timeSpaceParty = timeSpaceParty; + + protected override IAsyncEvent Convert(SAddTimeEvent e) => + new TimeSpaceAddTimeToTimerEvent + { + TimeSpaceParty = _timeSpaceParty, + Time = TimeSpan.FromSeconds(e.Time) + }; +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.TimeSpaces/Scripting/SCheckForTasksCompletedEventConverter.cs b/srcs/_plugins/Plugin.TimeSpaces/Scripting/SCheckForTasksCompletedEventConverter.cs new file mode 100644 index 0000000..801a15a --- /dev/null +++ b/srcs/_plugins/Plugin.TimeSpaces/Scripting/SCheckForTasksCompletedEventConverter.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using PhoenixLib.Events; +using WingsAPI.Scripting.Converter; +using WingsAPI.Scripting.Event; +using WingsAPI.Scripting.Event.TimeSpace; +using WingsEmu.Game.TimeSpaces; +using WingsEmu.Game.TimeSpaces.Events; + +namespace Plugin.TimeSpaces.Scripting; + +public class SCheckForTasksCompletedEventConverter : ScriptedEventConverter +{ + private readonly Dictionary _events; + private readonly Dictionary _maps; + + public SCheckForTasksCompletedEventConverter(Dictionary maps, Dictionary events) + { + _maps = maps; + _events = events; + } + + protected override IAsyncEvent Convert(SCheckForTasksCompletedEvent e) + { + List list = new(); + List timeSpaceSubInstances = new(); + IEnumerable maps = e.Maps; + + foreach (SEvent scriptedEvent in e.Events) + { + IAsyncEvent asyncEvent = _events.GetValueOrDefault(scriptedEvent.GetType())?.Convert(scriptedEvent); + if (asyncEvent == null) + { + continue; + } + + list.Add(asyncEvent); + } + + foreach (Guid map in maps) + { + timeSpaceSubInstances.Add(_maps[map]); + } + + return new TimeSpaceCheckForTasksCompletedEvent + { + Events = list, + TimeSpaceSubInstances = timeSpaceSubInstances + }; + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.TimeSpaces/Scripting/SClosePortalEventConverter.cs b/srcs/_plugins/Plugin.TimeSpaces/Scripting/SClosePortalEventConverter.cs new file mode 100644 index 0000000..ac77b29 --- /dev/null +++ b/srcs/_plugins/Plugin.TimeSpaces/Scripting/SClosePortalEventConverter.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using PhoenixLib.Events; +using WingsAPI.Scripting.Converter; +using WingsAPI.Scripting.Event.TimeSpace; +using WingsEmu.Game; +using WingsEmu.Game.TimeSpaces.Events; + +namespace Plugin.TimeSpaces.Scripting; + +public class SClosePortalEventConverter : ScriptedEventConverter +{ + private readonly Dictionary _portals; + + public SClosePortalEventConverter(Dictionary portals) => _portals = portals; + + protected override IAsyncEvent Convert(SClosePortal e) => + new TimeSpaceClosePortalEvent + { + PortalEntity = _portals[e.Portal.Id] + }; +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.TimeSpaces/Scripting/SDespawnAllMobsInRoomEventConverter.cs b/srcs/_plugins/Plugin.TimeSpaces/Scripting/SDespawnAllMobsInRoomEventConverter.cs new file mode 100644 index 0000000..bc540fb --- /dev/null +++ b/srcs/_plugins/Plugin.TimeSpaces/Scripting/SDespawnAllMobsInRoomEventConverter.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using PhoenixLib.Events; +using WingsAPI.Scripting.Converter; +using WingsAPI.Scripting.Event.TimeSpace; +using WingsEmu.Game.TimeSpaces; +using WingsEmu.Game.TimeSpaces.Events; + +namespace Plugin.TimeSpaces.Scripting; + +public class SDespawnAllMobsInRoomEventConverter : ScriptedEventConverter +{ + private readonly Dictionary _maps; + + public SDespawnAllMobsInRoomEventConverter(Dictionary maps) => _maps = maps; + + protected override IAsyncEvent Convert(SDespawnAllMobsInRoomEvent e) => + new TimeSpaceDespawnMonstersInRoomEvent + { + TimeSpaceSubInstance = _maps[e.Map] + }; +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.TimeSpaces/Scripting/SMonsterSummonEventConverter.cs b/srcs/_plugins/Plugin.TimeSpaces/Scripting/SMonsterSummonEventConverter.cs new file mode 100644 index 0000000..4d28bc8 --- /dev/null +++ b/srcs/_plugins/Plugin.TimeSpaces/Scripting/SMonsterSummonEventConverter.cs @@ -0,0 +1,34 @@ +using System.Linq; +using PhoenixLib.Events; +using WingsAPI.Scripting.Converter; +using WingsAPI.Scripting.Event.Common; +using WingsEmu.Game; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Monster.Event; + +namespace Plugin.TimeSpaces.Scripting; + +public class SMonsterSummonEventConverter : ScriptedEventConverter +{ + private readonly IMapInstance _mapInstance; + + public SMonsterSummonEventConverter(IMapInstance mapInstance) => _mapInstance = mapInstance; + + protected override IAsyncEvent Convert(SMonsterSummonEvent e) + { + return new MonsterSummonEvent(_mapInstance, e.Monsters.Select(x => new ToSummon + { + VNum = x.Vnum, + SpawnCell = x.IsRandomPosition ? null : new Position(x.Position.X, x.Position.Y), + IsMoving = x.CanMove, + IsTarget = x.IsTarget, + IsBossOrMate = x.IsBoss, + Direction = x.Direction, + HpMultiplier = x.HpMultiplier, + MpMultiplier = x.MpMultiplier, + Level = x.CustomLevel, + IsHostile = true + })); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.TimeSpaces/Scripting/SOpenTimeSpacePortalEventConverter.cs b/srcs/_plugins/Plugin.TimeSpaces/Scripting/SOpenTimeSpacePortalEventConverter.cs new file mode 100644 index 0000000..a1c2feb --- /dev/null +++ b/srcs/_plugins/Plugin.TimeSpaces/Scripting/SOpenTimeSpacePortalEventConverter.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using PhoenixLib.Events; +using WingsAPI.Scripting.Converter; +using WingsAPI.Scripting.Event.Common; +using WingsEmu.Game; +using WingsEmu.Game.TimeSpaces.Events; + +namespace Plugin.TimeSpaces.Scripting; + +public class SOpenTimeSpacePortalEventConverter : ScriptedEventConverter +{ + private readonly Dictionary _portals; + + public SOpenTimeSpacePortalEventConverter(Dictionary portals) => _portals = portals; + + protected override IAsyncEvent Convert(SOpenPortalEvent e) + { + IPortalEntity portal = _portals[e.Portal.Id]; + return new TimeSpacePortalOpenEvent + { + PortalEntity = portal + }; + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.TimeSpaces/Scripting/SRemoveItemsEventConverter.cs b/srcs/_plugins/Plugin.TimeSpaces/Scripting/SRemoveItemsEventConverter.cs new file mode 100644 index 0000000..28b8afb --- /dev/null +++ b/srcs/_plugins/Plugin.TimeSpaces/Scripting/SRemoveItemsEventConverter.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using PhoenixLib.Events; +using WingsAPI.Scripting.Converter; +using WingsAPI.Scripting.Event.TimeSpace; +using WingsEmu.Game.Maps; +using WingsEmu.Game.TimeSpaces.Events; + +namespace Plugin.TimeSpaces.Scripting; + +public class SRemoveItemsEventConverter : ScriptedEventConverter +{ + private readonly Dictionary _items; + + public SRemoveItemsEventConverter(Dictionary items) => _items = items; + + protected override IAsyncEvent Convert(SRemoveItemsEvent e) + { + List itemsToRemove = new(); + foreach (Guid item in e.Items) + { + itemsToRemove.Add(_items[item]); + } + + return new TimeSpaceRemoveItemsEvent + { + ItemsToRemove = itemsToRemove + }; + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.TimeSpaces/Scripting/STimeSpaceFinishEventConverter.cs b/srcs/_plugins/Plugin.TimeSpaces/Scripting/STimeSpaceFinishEventConverter.cs new file mode 100644 index 0000000..84566e1 --- /dev/null +++ b/srcs/_plugins/Plugin.TimeSpaces/Scripting/STimeSpaceFinishEventConverter.cs @@ -0,0 +1,17 @@ +using PhoenixLib.Events; +using WingsAPI.Scripting.Converter; +using WingsAPI.Scripting.Event.TimeSpace; +using WingsEmu.Game.TimeSpaces; +using WingsEmu.Game.TimeSpaces.Enums; +using WingsEmu.Game.TimeSpaces.Events; + +namespace Plugin.TimeSpaces.Scripting; + +public class STimeSpaceFinishEventConverter : ScriptedEventConverter +{ + private readonly TimeSpaceParty _timeSpaceParty; + + public STimeSpaceFinishEventConverter(TimeSpaceParty timeSpaceParty) => _timeSpaceParty = timeSpaceParty; + + protected override IAsyncEvent Convert(STimeSpaceFinishEvent e) => new TimeSpaceInstanceFinishEvent(_timeSpaceParty, (TimeSpaceFinishType)e.TimeSpaceFinishType); +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.TimeSpaces/Scripting/STogglePortalEventConverter.cs b/srcs/_plugins/Plugin.TimeSpaces/Scripting/STogglePortalEventConverter.cs new file mode 100644 index 0000000..680a114 --- /dev/null +++ b/srcs/_plugins/Plugin.TimeSpaces/Scripting/STogglePortalEventConverter.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using PhoenixLib.Events; +using WingsAPI.Scripting.Converter; +using WingsAPI.Scripting.Event.TimeSpace; +using WingsEmu.Game; +using WingsEmu.Game.TimeSpaces.Events; + +namespace Plugin.TimeSpaces.Scripting; + +public class STogglePortalEventConverter : ScriptedEventConverter +{ + private readonly Dictionary _portals; + + public STogglePortalEventConverter(Dictionary portals) => _portals = portals; + + protected override IAsyncEvent Convert(STogglePortalEvent e) => + new TimeSpaceTogglePortalEvent + { + PortalEntity = _portals[e.Portal.Id] + }; +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.TimeSpaces/Scripting/STryStartTaskEventConverter.cs b/srcs/_plugins/Plugin.TimeSpaces/Scripting/STryStartTaskEventConverter.cs new file mode 100644 index 0000000..f505ed8 --- /dev/null +++ b/srcs/_plugins/Plugin.TimeSpaces/Scripting/STryStartTaskEventConverter.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using PhoenixLib.Events; +using WingsAPI.Scripting.Converter; +using WingsAPI.Scripting.Event.TimeSpace; +using WingsEmu.Game.TimeSpaces; +using WingsEmu.Game.TimeSpaces.Events; + +namespace Plugin.TimeSpaces.Scripting; + +public class STryStartTaskEventConverter : ScriptedEventConverter +{ + private readonly Dictionary _maps; + + public STryStartTaskEventConverter(Dictionary maps) => _maps = maps; + + protected override IAsyncEvent Convert(STryStartTaskEvent e) => + new TryStartTaskEvent + { + Map = _maps[e.MapId] + }; +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.TimeSpaces/Scripting/ScriptSetTimeEventConverter.cs b/srcs/_plugins/Plugin.TimeSpaces/Scripting/ScriptSetTimeEventConverter.cs new file mode 100644 index 0000000..96e1518 --- /dev/null +++ b/srcs/_plugins/Plugin.TimeSpaces/Scripting/ScriptSetTimeEventConverter.cs @@ -0,0 +1,22 @@ +using System; +using PhoenixLib.Events; +using WingsAPI.Scripting.Converter; +using WingsAPI.Scripting.Event.TimeSpace; +using WingsEmu.Game.TimeSpaces; +using WingsEmu.Game.TimeSpaces.Events; + +namespace Plugin.TimeSpaces.Scripting; + +public class ScriptSetTimeEventConverter : ScriptedEventConverter +{ + private readonly TimeSpaceParty _timeSpaceParty; + + public ScriptSetTimeEventConverter(TimeSpaceParty timeSpaceParty) => _timeSpaceParty = timeSpaceParty; + + protected override IAsyncEvent Convert(ScriptSetTimeEvent e) => + new TimeSpaceSetTimeEvent + { + TimeSpaceParty = _timeSpaceParty, + Time = TimeSpan.FromSeconds(e.Time) + }; +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.TimeSpaces/TimeSpaceFactory.cs b/srcs/_plugins/Plugin.TimeSpaces/TimeSpaceFactory.cs new file mode 100644 index 0000000..f5a33a7 --- /dev/null +++ b/srcs/_plugins/Plugin.TimeSpaces/TimeSpaceFactory.cs @@ -0,0 +1,510 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Microsoft.Extensions.DependencyInjection; +using PhoenixLib.Events; +using PhoenixLib.Logging; +using WingsAPI.Scripting.Attribute; +using WingsAPI.Scripting.Converter; +using WingsAPI.Scripting.Enum; +using WingsAPI.Scripting.Event; +using WingsAPI.Scripting.Object.Common; +using WingsAPI.Scripting.Object.Common.Map; +using WingsAPI.Scripting.Object.Timespace; +using WingsAPI.Scripting.ScriptManager; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Entities.Event; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Portals; +using WingsEmu.Game.Raids; +using WingsEmu.Game.TimeSpaces; +using WingsEmu.Game.TimeSpaces.Enums; +using WingsEmu.Packets.Enums; + +namespace Plugin.TimeSpaces; + +public class TimeSpaceFactory : ITimeSpaceFactory +{ + private static readonly IEnumerable Converters; + private readonly IAsyncEventPipeline _eventPipeline; + private readonly IGameItemInstanceFactory _gameItemInstance; + private readonly IMapManager _mapManager; + private readonly IMonsterEntityFactory _monsterEntityFactory; + private readonly INpcEntityFactory _npcEntityFactory; + private readonly INpcMonsterManager _npcMonsterManager; + private readonly IPortalFactory _portalFactory; + private readonly ITimeSpaceScriptManager _timeSpaceScriptManager; + + static TimeSpaceFactory() + { + Converters = typeof(IScriptedEventConverter).Assembly.GetTypes() + .Concat(typeof(TimeSpaceFactory).Assembly.GetTypes()) + .Where(x => typeof(IScriptedEventConverter).IsAssignableFrom(x)) + .Where(x => !x.IsAbstract && !x.IsInterface); + } + + public TimeSpaceFactory(ITimeSpaceScriptManager timeSpaceScriptManager, IMapManager mapManager, IAsyncEventPipeline eventPipeline, IMonsterEntityFactory monsterEntityFactory, + INpcMonsterManager npcMonsterManager, IPortalFactory portalFactory, INpcEntityFactory npcEntityFactory, IGameItemInstanceFactory gameItemInstance) + { + _timeSpaceScriptManager = timeSpaceScriptManager; + _mapManager = mapManager; + _eventPipeline = eventPipeline; + _monsterEntityFactory = monsterEntityFactory; + _npcMonsterManager = npcMonsterManager; + _portalFactory = portalFactory; + _npcEntityFactory = npcEntityFactory; + _gameItemInstance = gameItemInstance; + } + + public TimeSpaceInstance Create(TimeSpaceParty timeSpaceParty) + { + try + { + ScriptTimeSpace scriptedTimeSpace = _timeSpaceScriptManager.GetScriptedTimeSpace(timeSpaceParty.TimeSpaceId); + if (scriptedTimeSpace == null) + { + Log.Warn($"Can't create TimeSpace {timeSpaceParty.TimeSpaceId}, Couldn't find it in {nameof(LuaTimeSpaceScriptManager)}"); + return null; + } + + IServiceCollection serviceLocator = new ServiceCollection(); + + foreach (Type converter in Converters) + { + serviceLocator.AddTransient(typeof(IScriptedEventConverter), converter); + } + + var timeSpaceSubInstances = new Dictionary(); + var tasks = new Dictionary(); + var protectedNpcs = new HashSet(); + + int mapId = 20000; + foreach (SMap scriptedMap in scriptedTimeSpace.Maps) + { + IMapInstance map; + if (scriptedMap.MapType == SMapType.MapId) + { + map = _mapManager.GenerateMapInstanceByMapId(scriptedMap.MapIdVnum, MapInstanceType.TimeSpaceInstance); + } + else + { + map = _mapManager.GenerateMapInstanceByMapVNum(new ServerMapDto + { + Flags = scriptedMap.Flags.Select(mapFlag => (MapFlags)(int)mapFlag).Where(x => x != MapFlags.IS_BASE_MAP).ToList(), + Id = mapId, + MapVnum = scriptedMap.MapIdVnum, + NameId = scriptedMap.NameId, + MusicId = scriptedMap.MusicId + }, MapInstanceType.TimeSpaceInstance); + + mapId++; + } + + map.MapIndexX = scriptedMap.MapIndexX; + map.MapIndexY = scriptedMap.MapIndexY; + map.AIDisabled = true; + timeSpaceSubInstances[scriptedMap.Id] = new TimeSpaceSubInstance(map, _eventPipeline); + } + + var objective = new TimeSpaceObjective + { + KillAllMonsters = scriptedTimeSpace.Objectives.KillAllMonsters, + GoToExit = scriptedTimeSpace.Objectives.GoToExit, + ProtectNPC = scriptedTimeSpace.Objectives.ProtectNPC, + KillMonsterVnum = scriptedTimeSpace.Objectives.KillMonsterVnum, + KillMonsterAmount = scriptedTimeSpace.Objectives.KillMonsterAmount, + CollectItemVnum = scriptedTimeSpace.Objectives.CollectItemVnum, + CollectItemAmount = scriptedTimeSpace.Objectives.CollectItemAmount, + Conversation = scriptedTimeSpace.Objectives.Conversation, + InteractObjectsVnum = scriptedTimeSpace.Objectives.InteractObjectsVnum, + InteractObjectsAmount = scriptedTimeSpace.Objectives.InteractObjectsAmount + }; + + serviceLocator.AddSingleton(timeSpaceSubInstances); + serviceLocator.AddSingleton(tasks); + + var portals = new Dictionary(); + var monsters = new Dictionary(); + var npcs = new Dictionary(); + var buttons = new Dictionary(); + var items = new Dictionary(); + var eventConverters = new Dictionary(); + + // Register portals, monsters and buttons to service locator + serviceLocator.AddSingleton(portals); + serviceLocator.AddSingleton(monsters); + serviceLocator.AddSingleton(buttons); + serviceLocator.AddSingleton(npcs); + serviceLocator.AddSingleton(items); + serviceLocator.AddSingleton(timeSpaceParty); + serviceLocator.AddSingleton(eventConverters); + + // Load first portals and monster waves for later event converter usages + foreach (SMap map in scriptedTimeSpace.Maps) + { + TimeSpaceSubInstance timeSpaceSubInstance = timeSpaceSubInstances[map.Id]; + IMapInstance mapInstance = timeSpaceSubInstance.MapInstance; + + AddPortals(map, timeSpaceSubInstances, portals, mapInstance); + AddMonsterWaves(map, timeSpaceSubInstance); + } + + int npcId = 100000; + int monsterId = 100000; + foreach (SMap scriptedMap in scriptedTimeSpace.Maps) + { + TimeSpaceSubInstance timeSpaceSubInstance = timeSpaceSubInstances[scriptedMap.Id]; + IMapInstance mapInstance = timeSpaceSubInstance.MapInstance; + + serviceLocator.AddSingleton(timeSpaceSubInstance); + serviceLocator.AddSingleton(mapInstance); + + // This dictionary will contains all events to attach later + var events = new Dictionary>> + { + [timeSpaceSubInstance] = scriptedMap.Events + }; + + if (scriptedMap.TimeSpaceTask != null) + { + var task = new TimeSpaceTask + { + TaskType = (TimeSpaceTaskType)scriptedMap.TimeSpaceTask.TimeSpaceTaskType, + Time = scriptedMap.TimeSpaceTask.DurationInSeconds == null ? null : TimeSpan.FromSeconds(scriptedMap.TimeSpaceTask.DurationInSeconds.Value), + GameDialogKey = scriptedMap.TimeSpaceTask.GameDialogKey, + StartDialog = scriptedMap.TimeSpaceTask.StartDialog, + EndDialog = scriptedMap.TimeSpaceTask.EndDialog, + StartDialogShout = scriptedMap.TimeSpaceTask.StartDialogShout, + EndDialogShout = scriptedMap.TimeSpaceTask.EndDialogShout, + DialogStartTask = scriptedMap.TimeSpaceTask.DialogStartTask, + StartDialogIsObjective = scriptedMap.TimeSpaceTask.StartDialogIsObjective, + EndDialogIsObjective = scriptedMap.TimeSpaceTask.EndDialogIsObjective + }; + + timeSpaceSubInstance.Task = task; + tasks[scriptedMap.Id] = task; + } + + AddMonsters(scriptedMap, mapInstance, monsters, events, timeSpaceSubInstance, ref monsterId); + AddNpcs(scriptedMap, mapInstance, npcs, events, protectedNpcs, ref npcId); + AddMapObjects(scriptedMap, mapInstance, events, items, buttons); + + AddEventConverters(serviceLocator, events, eventConverters); + } + + return new TimeSpaceInstance(timeSpaceSubInstances.Values, timeSpaceSubInstances[scriptedTimeSpace.Spawn.MapId], + new Position(scriptedTimeSpace.Spawn.Position.X, scriptedTimeSpace.Spawn.Position.Y), + TimeSpan.FromSeconds(timeSpaceParty.IsEasyMode ? scriptedTimeSpace.DurationInSeconds * 2 : scriptedTimeSpace.DurationInSeconds), + scriptedTimeSpace.Lives, objective, scriptedTimeSpace.BonusPointItemDropChance, protectedNpcs, + scriptedTimeSpace.ObtainablePartnerVnum, + scriptedTimeSpace.InfiniteDuration, scriptedTimeSpace.PreFinishDialog, scriptedTimeSpace.PreFinishDialogIsObjective); + } + catch (Exception e) + { + Log.Error("TimeSpaceFactory", e); + return null; + } + } + + private void AddMonsterWaves(SMap scriptedMap, TimeSpaceSubInstance timeSpaceSubInstance) + { + var timeSpaceWaves = new List(); + + // Add Monster Waves + foreach (SMonsterWave monsterWave in scriptedMap.MonsterWaves) + { + var summons = new List(); + + foreach (SMonster mobWave in monsterWave.Monsters) + { + summons.Add(new ToSummon + { + VNum = mobWave.Vnum, + IsHostile = true, + IsMoving = true, + HpMultiplier = mobWave.HpMultiplier, + MpMultiplier = mobWave.MpMultiplier, + SpawnCell = mobWave.IsRandomPosition ? null : new Position(mobWave.Position.X, mobWave.Position.Y), + Direction = mobWave.Direction, + Level = mobWave.CustomLevel, + SummonType = SummonType.MONSTER_WAVE + }); + } + + timeSpaceWaves.Add(new TimeSpaceWave + { + Monsters = summons, + Delay = TimeSpan.FromSeconds(monsterWave.TimeInSeconds) + }); + } + + timeSpaceSubInstance.TimeSpaceWaves = timeSpaceWaves; + } + + private void AddNpcs(SMap scriptedMap, IMapInstance mapInstance, Dictionary npcs, Dictionary>> events, + HashSet protectedNpcs, ref int npcId) + { + foreach (SMapNpc scriptedNpc in scriptedMap.Npcs) + { + npcId++; + INpcEntity npc = _npcEntityFactory.CreateNpc(scriptedNpc.Vnum, mapInstance, npcId, new NpcAdditionalData + { + IsProtected = scriptedNpc.IsProtectedNpc, + IsTimeSpaceMate = scriptedNpc.FollowPlayer, + CanMove = scriptedNpc.CanMove, + NpcDirection = scriptedNpc.Direction, + CanAttack = true, + IsHostile = true, + HpMultiplier = scriptedNpc.HpMultiplier, + MpMultiplier = scriptedNpc.MpMultiplier, + CustomLevel = scriptedNpc.CustomLevel + }); + + if (scriptedNpc.IsProtectedNpc) + { + protectedNpcs.Add(npc.Id); + } + + npc.EmitEventAsync(new MapJoinNpcEntityEvent(npc, scriptedNpc.Position.X, scriptedNpc.Position.Y)); + + npcs[scriptedNpc.Id] = npc; + events[npc] = scriptedNpc.Events; + } + } + + private static void AddEventConverters(IServiceCollection serviceLocator, Dictionary>> events, + Dictionary eventConverters) + { + // Get event converters using service locator + var converters = serviceLocator.BuildServiceProvider() + .GetServices() + .ToDictionary(x => x.EventType, x => x); + + foreach ((Type key, IScriptedEventConverter value) in converters) + { + eventConverters[key] = value; + } + + // Add all events using event converters + foreach ((IEventTriggerContainer eventContainer, IDictionary> containerEvents) in events) + { + foreach ((string trigger, IEnumerable scriptedEvents) in containerEvents) + { + foreach (SEvent scriptedEvent in scriptedEvents) + { + IAsyncEvent e = converters.GetValueOrDefault(scriptedEvent.GetType())?.Convert(scriptedEvent); + if (e == null) + { + throw new InvalidOperationException($"Failed to convert {scriptedEvent.GetType().Name} to async event"); + } + + ScriptEventAttribute attribute = scriptedEvent.GetType().GetCustomAttribute(); + if (attribute == null) + { + throw new InvalidOperationException($"Failed to find attribute for: {scriptedEvent.GetType().Name}"); + } + + eventContainer.AddEvent(trigger, e, attribute.IsRemovedOnTrigger); + } + } + } + } + + private void AddMapObjects(SMap scriptedMap, IMapInstance mapInstance, Dictionary>> events, + Dictionary items, Dictionary buttons) + { + // Add map objects + foreach (SMapObject scriptedObject in scriptedMap.Objects) + { + switch (scriptedObject) + { + case SButton scriptedButton: + + short positionX = scriptedButton.Position.X; + short positionY = scriptedButton.Position.Y; + + if (scriptedButton.IsRandomPosition) + { + Position getPosition = mapInstance.GetRandomPosition(); + positionX = getPosition.X; + positionY = getPosition.Y; + } + + var button = new ButtonMapItem(positionX, positionY, scriptedButton.DeactivatedVnum, scriptedButton.ActivatedVnum, + false, mapInstance, _eventPipeline, scriptedButton.OnlyOnce == false ? null : false, scriptedButton.IsObjective, false, + scriptedButton.CustomDanceDuration); + + events[button] = scriptedButton.Events; + buttons[scriptedButton.Id] = button; + mapInstance.AddDrop(button); + break; + case SItem scriptedItem: + GameItemInstance newItem = _gameItemInstance.CreateItem(scriptedItem.Vnum); + positionX = scriptedItem.Position.X; + positionY = scriptedItem.Position.Y; + + if (scriptedItem.IsRandomPosition) + { + Position getPosition = mapInstance.GetRandomPosition(); + positionX = getPosition.X; + positionY = getPosition.Y; + } + + if (scriptedItem.IsRandomUniquePosition) + { + Position getPosition; + while (true) + { + getPosition = mapInstance.GetRandomPosition(); + MapItem findOtherItemInPosition = mapInstance.Drops.FirstOrDefault(x => x.PositionX == getPosition.X && x.PositionY == getPosition.Y); + if (findOtherItemInPosition == null) + { + break; + } + } + + positionX = getPosition.X; + positionY = getPosition.Y; + } + + var item = new TimeSpaceMapItem(positionX, positionY, false, newItem, + _eventPipeline, mapInstance, scriptedItem.DanceDuration, scriptedItem.IsObjective); + + events[item] = scriptedItem.Events; + items[scriptedItem.Id] = item; + mapInstance.AddDrop(item); + break; + } + } + } + + private void AddMonsters(SMap scriptedMap, IMapInstance mapInstance, IDictionary monsters, + IDictionary>> events, TimeSpaceSubInstance timeSpaceSubInstance, ref int monsterId) + { + // Add monsters + foreach (SMonster scriptedMonster in scriptedMap.Monsters) + { + monsterId++; + short positionX = scriptedMonster.Position.X; + short positionY = scriptedMonster.Position.Y; + + if (scriptedMonster.SpawnAfterTask && timeSpaceSubInstance.Task != null) + { + if (scriptedMonster.IsRandomPosition) + { + Position position = mapInstance.GetRandomPosition(); + positionX = position.X; + positionY = position.Y; + } + + IMonsterEntity monsterAfterTask = _monsterEntityFactory.CreateMonster(monsterId, scriptedMonster.Vnum, mapInstance, + new MonsterEntityBuilder + { + IsRespawningOnDeath = false, + IsBoss = scriptedMonster.IsBoss, + IsTarget = scriptedMonster.IsTarget, + IsWalkingAround = scriptedMonster.CanMove, + IsHostile = true, + HpMultiplier = scriptedMonster.HpMultiplier, + MpMultiplier = scriptedMonster.MpMultiplier, + PositionX = positionX, + PositionY = positionY, + Direction = scriptedMonster.Direction, + Level = scriptedMonster.CustomLevel + }); + + monsters[scriptedMonster.Id] = monsterAfterTask; + events[monsterAfterTask] = scriptedMonster.Events; + timeSpaceSubInstance.Task.MonstersAfterTaskStart.Add((scriptedMonster.SpawnAfterMobs == 0 ? null : scriptedMonster.SpawnAfterMobs, monsterAfterTask)); + continue; + } + + if (scriptedMonster.SpawnAfterMobs != 0) + { + if (scriptedMonster.IsRandomPosition) + { + Position position = mapInstance.GetRandomPosition(); + positionX = position.X; + positionY = position.Y; + } + + IMonsterEntity spawnAfterMobs = _monsterEntityFactory.CreateMonster(monsterId, scriptedMonster.Vnum, mapInstance, + new MonsterEntityBuilder + { + IsRespawningOnDeath = false, + IsBoss = scriptedMonster.IsBoss, + IsTarget = scriptedMonster.IsTarget, + IsWalkingAround = scriptedMonster.CanMove, + IsHostile = true, + HpMultiplier = scriptedMonster.HpMultiplier, + MpMultiplier = scriptedMonster.MpMultiplier, + PositionX = positionX, + PositionY = positionY, + Direction = scriptedMonster.Direction, + Level = scriptedMonster.CustomLevel + }); + + monsters[scriptedMonster.Id] = spawnAfterMobs; + events[spawnAfterMobs] = scriptedMonster.Events; + + if (!timeSpaceSubInstance.SpawnAfterMobsKilled.TryGetValue(scriptedMonster.SpawnAfterMobs, out List list)) + { + list = new List(); + timeSpaceSubInstance.SpawnAfterMobsKilled[scriptedMonster.SpawnAfterMobs] = list; + } + + list.Add(spawnAfterMobs); + continue; + } + + if (scriptedMonster.IsRandomPosition) + { + Position position = mapInstance.GetRandomPosition(); + positionX = position.X; + positionY = position.Y; + } + + IMonsterEntity monster = _monsterEntityFactory.CreateMonster(monsterId, scriptedMonster.Vnum, mapInstance, + new MonsterEntityBuilder + { + IsRespawningOnDeath = false, + IsBoss = scriptedMonster.IsBoss, + IsTarget = scriptedMonster.IsTarget, + IsWalkingAround = scriptedMonster.CanMove, + IsHostile = true, + HpMultiplier = scriptedMonster.HpMultiplier, + MpMultiplier = scriptedMonster.MpMultiplier, + Direction = scriptedMonster.Direction, + Level = scriptedMonster.CustomLevel + }); + + monster.EmitEventAsync(new MapJoinMonsterEntityEvent(monster, positionX, positionY)); + + monsters[scriptedMonster.Id] = monster; + events[monster] = scriptedMonster.Events; + } + } + + private void AddPortals(SMap scriptedMap, IReadOnlyDictionary raidSubInstances, Dictionary portals, IMapInstance mapInstance) + { + // Add portals + foreach (SPortal scriptedPortal in scriptedMap.Portals) + { + var sourcePosition = new Position(scriptedPortal.SourcePosition.X, scriptedPortal.SourcePosition.Y); + var destinationPos = new Position(scriptedPortal.DestinationPosition.X, scriptedPortal.DestinationPosition.Y); + var portalOrient = (PortalMinimapOrientation)scriptedPortal.PortalMiniMapOrientation; + + IPortalEntity portal = _portalFactory.CreatePortal((PortalType)scriptedPortal.Type, mapInstance, sourcePosition, raidSubInstances[scriptedPortal.DestinationId].MapInstance, + destinationPos, portalOrient); + + portals[scriptedPortal.Id] = portal; + mapInstance.Portals.Add(portal); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.TimeSpaces/TimeSpaceManager.cs b/srcs/_plugins/Plugin.TimeSpaces/TimeSpaceManager.cs new file mode 100644 index 0000000..bf50041 --- /dev/null +++ b/srcs/_plugins/Plugin.TimeSpaces/TimeSpaceManager.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using WingsEmu.Core.Extensions; +using WingsEmu.Game.TimeSpaces; + +namespace Plugin.TimeSpaces; + +public class TimeSpaceManager : ITimeSpaceManager +{ + private readonly IDictionary _timeSpaceParties = new Dictionary(); + private readonly IDictionary _timeSpacePartiesByMapInstanceId = new Dictionary(); + private readonly IDictionary _timeSpaceSubInstances = new Dictionary(); + + public IReadOnlyCollection GetTimeSpaces() => _timeSpaceParties.Values.ToArray(); + + public TimeSpaceParty GetTimeSpace(Guid id) => _timeSpaceParties.GetOrDefault(id); + public TimeSpaceParty GetTimeSpaceByMapInstanceId(Guid id) => _timeSpacePartiesByMapInstanceId.GetOrDefault(id); + + public TimeSpaceSubInstance GetSubInstance(Guid mapInstanceId) => _timeSpaceSubInstances.GetOrDefault(mapInstanceId); + + public void AddTimeSpace(TimeSpaceParty timeSpaceParty) + { + _timeSpaceParties.Add(timeSpaceParty.Id, timeSpaceParty); + } + + public void AddTimeSpaceSubInstance(Guid mapInstanceId, TimeSpaceSubInstance subInstance) + { + _timeSpaceSubInstances[mapInstanceId] = subInstance; + } + + public void AddTimeSpaceByMapInstanceId(Guid mapInstanceId, TimeSpaceParty timeSpaceParty) + { + _timeSpacePartiesByMapInstanceId[mapInstanceId] = timeSpaceParty; + } + + public void RemoveTimeSpaceSubInstance(Guid mapInstanceId) + { + _timeSpaceSubInstances.Remove(mapInstanceId); + } + + public void RemoveTimeSpacePartyByMapInstanceId(Guid mapInstanceId) + { + _timeSpacePartiesByMapInstanceId.Remove(mapInstanceId); + } + + public void RemoveTimeSpace(TimeSpaceParty timeSpaceParty) => _timeSpaceParties.Remove(timeSpaceParty.Id); +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.TimeSpaces/TimeSpacesPlugin.cs b/srcs/_plugins/Plugin.TimeSpaces/TimeSpacesPlugin.cs new file mode 100644 index 0000000..79d384b --- /dev/null +++ b/srcs/_plugins/Plugin.TimeSpaces/TimeSpacesPlugin.cs @@ -0,0 +1,27 @@ +using Plugin.TimeSpaces.Commands; +using WingsAPI.Plugins; +using WingsAPI.Scripting; +using WingsEmu.Commands.Interfaces; + +namespace Plugin.TimeSpaces; + +public class TimeSpacesPlugin : IGamePlugin +{ + private readonly ICommandContainer _commands; + private readonly IScriptFactory _scriptFactory; + + public TimeSpacesPlugin(ICommandContainer commands, IScriptFactory scriptFactory) + { + _commands = commands; + _scriptFactory = scriptFactory; + } + + public string Name { get; } = nameof(TimeSpacesPlugin); + + public void OnLoad() + { + _commands.AddModule(); + + _scriptFactory.RegisterAllScriptingObjectsInAssembly(typeof(TimeSpacesPlugin).Assembly); + } +} \ No newline at end of file diff --git a/srcs/_plugins/Plugin.TimeSpaces/TimeSpacesPluginCore.cs b/srcs/_plugins/Plugin.TimeSpaces/TimeSpacesPluginCore.cs new file mode 100644 index 0000000..ac53359 --- /dev/null +++ b/srcs/_plugins/Plugin.TimeSpaces/TimeSpacesPluginCore.cs @@ -0,0 +1,23 @@ +using Microsoft.Extensions.DependencyInjection; +using PhoenixLib.Events; +using Plugin.TimeSpaces.RecurrentJob; +using WingsAPI.Plugins; +using WingsAPI.Scripting.ScriptManager; +using WingsEmu.Game.TimeSpaces; + +namespace Plugin.TimeSpaces; + +public class TimeSpacesPluginCore : IGameServerPlugin +{ + public string Name => nameof(TimeSpacesPluginCore); + + public void AddDependencies(IServiceCollection services, GameServerLoader gameServer) + { + services.AddEventHandlersInAssembly(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddHostedService(); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Act5/Act5OpenNpcRunEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Act5/Act5OpenNpcRunEventHandler.cs new file mode 100644 index 0000000..2c3695b --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Act5/Act5OpenNpcRunEventHandler.cs @@ -0,0 +1,103 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsAPI.Game.Extensions.ItemExtension.Item; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Act5; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Character; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.Act5; + +public class Act5OpenNpcRunEventHandler : IAsyncEventProcessor +{ + private readonly IAct5NpcRunCraftItemConfiguration _act5NpcRunCraftItemConfiguration; + private readonly IGameItemInstanceFactory _gameItemInstanceFactory; + private readonly IGameLanguageService _gameLanguage; + private readonly IItemsManager _itemsManager; + + public Act5OpenNpcRunEventHandler(IAct5NpcRunCraftItemConfiguration act5NpcRunCraftItemConfiguration, + IGameItemInstanceFactory gameItemInstanceFactory, IItemsManager itemsManager, IGameLanguageService gameLanguage) + { + _act5NpcRunCraftItemConfiguration = act5NpcRunCraftItemConfiguration; + _gameItemInstanceFactory = gameItemInstanceFactory; + _itemsManager = itemsManager; + _gameLanguage = gameLanguage; + } + + public async Task HandleAsync(Act5OpenNpcRunEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + NpcRunType npcRunType = e.NpcRunType; + bool isConfirm = e.IsConfirm; + + if (session?.CurrentMapInstance == null) + { + return; + } + + if (!session.IsInAct5()) + { + return; + } + + if (!isConfirm) + { + session.SendQnaPacket($"n_run {(short)npcRunType} 0 0 0 1", session.GetLanguage(GameDialogKey.ITEM_DIALOG_ASK_EXCHANGE)); + return; + } + + Act5NpcRunCraftItemConfig config = _act5NpcRunCraftItemConfiguration.GetConfigByNpcRun(npcRunType); + if (config == null) + { + session.SendInfo(session.GetLanguage(GameDialogKey.ACT5_CHATMESSAGE_NO_RECIPE)); + return; + } + + if (config.ItemByClass is true && session.PlayerEntity.Class is ClassType.Adventurer or ClassType.Wrestler) + { + return; + } + + foreach (Act5NpcRunCraftItemConfigItem neededItem in config.NeededItems) + { + if (session.PlayerEntity.HasItem(neededItem.Item, (short)neededItem.Amount)) + { + continue; + } + + string getItemName = _itemsManager.GetItem(neededItem.Item).GetItemName(_gameLanguage, session.UserLanguage); + int missingItems = session.CountMissingItems(neededItem.Item, (short)neededItem.Amount); + session.SendMsg(session.GetLanguageFormat(GameDialogKey.INVENTORY_SHOUTMESSAGE_NOT_ENOUGH_ITEMS, missingItems, getItemName), MsgMessageType.Middle); + return; + } + + int craftedItem = config.CraftedItem; + + if (config.ItemByClass is true) + { + craftedItem += (int)session.PlayerEntity.Class; + } + + if (!session.PlayerEntity.HasSpaceFor(craftedItem, (short)config.Amount)) + { + session.SendChatMessage(session.GetLanguage(GameDialogKey.INTERACTION_MESSAGE_NOT_ENOUGH_PLACE), ChatMessageColorType.Yellow); + return; + } + + foreach (Act5NpcRunCraftItemConfigItem item in config.NeededItems) + { + await session.RemoveItemFromInventory(item.Item, (short)item.Amount); + } + + GameItemInstance newItem = _gameItemInstanceFactory.CreateItem(craftedItem, config.Amount); + await session.AddNewItemToInventory(newItem, true, ChatMessageColorType.Yellow); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Algorithms/AlgorithmPluginCore.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Algorithms/AlgorithmPluginCore.cs new file mode 100644 index 0000000..8af582c --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Algorithms/AlgorithmPluginCore.cs @@ -0,0 +1,15 @@ +using Microsoft.Extensions.DependencyInjection; +using WingsAPI.Plugins; +using WingsEmu.Game.Algorithm; + +namespace WingsEmu.Plugins.BasicImplementations.Algorithms; + +public class AlgorithmPluginCore : IGameServerPlugin +{ + public string Name => nameof(AlgorithmPluginCore); + + public void AddDependencies(IServiceCollection services, GameServerLoader gameServer) + { + services.AddSingleton(); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Algorithms/CellonGenerationAlgorithm.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Algorithms/CellonGenerationAlgorithm.cs new file mode 100644 index 0000000..b60d7a6 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Algorithms/CellonGenerationAlgorithm.cs @@ -0,0 +1,45 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using System.Linq; +using WingsEmu.DTOs.Items; +using WingsEmu.Game; +using WingsEmu.Game.Algorithm; +using WingsEmu.Game.Cellons; + +namespace WingsEmu.Plugins.BasicImplementations.Algorithms; + +public class CellonGenerationAlgorithm : ICellonGenerationAlgorithm +{ + private readonly CellonSystemConfiguration _configuration; + private readonly IRandomGenerator _randomGenerator; + + public CellonGenerationAlgorithm(IRandomGenerator randomGenerator, CellonSystemConfiguration configuration) + { + _randomGenerator = randomGenerator; + _configuration = configuration; + } + + public EquipmentOptionDTO GenerateOption(int cellonLevel) + { + CellonPossibilities dictionary = _configuration.Options.FirstOrDefault(s => s.CellonLevel == cellonLevel); + if (dictionary == null) + { + return null; + } + + HashSet list = dictionary.Options; + int rand = _randomGenerator.RandomNumber(list.Count); + + CellonOption options = list.ElementAt(rand); + return new EquipmentOptionDTO + { + EquipmentOptionType = EquipmentOptionType.JEWELS, + Value = _randomGenerator.RandomNumber(options.Range.Minimum, options.Range.Maximum), + Level = (byte)cellonLevel, + Type = (byte)options.Type + }; + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Algorithms/CharacterAlgorithm/CharacterAlgorithm.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Algorithms/CharacterAlgorithm/CharacterAlgorithm.cs new file mode 100644 index 0000000..278ceb7 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Algorithms/CharacterAlgorithm/CharacterAlgorithm.cs @@ -0,0 +1,772 @@ +using System.Collections.Generic; +using WingsAPI.Packets.Enums.Shells; +using WingsEmu.Game._enum; +using WingsEmu.Game.Algorithm; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.EntityStatistics; +using WingsEmu.Game.Extensions; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Character; + +namespace WingsEmu.Plugins.BasicImplementations.Algorithms.CharacterAlgorithm; + +public class CharacterAlgorithm : ICharacterAlgorithm +{ + public long GetLevelXp(short level, bool isMate = false, MateType mateType = 0) + { + long xp = level switch + { + 1 => 300, + 2 => 840, + 3 => 1800, + 4 => 3300, + 5 => 5460, + 6 => 8400, + 7 => 12240, + 8 => 17100, + 9 => 23100, + 10 => 30360, + 11 => 39000, + 12 => 49140, + 13 => 60900, + 14 => 74400, + 15 => 105120, + 16 => 139800, + 17 => 178680, + 18 => 222000, + 19 => 270000, + 20 => 322920, + 21 => 381000, + 22 => 444480, + 23 => 513600, + 24 => 588600, + 25 => 669720, + 26 => 757200, + 27 => 851280, + 28 => 952200, + 29 => 1060200, + 30 => 1175520, + 31 => 1298400, + 32 => 1429080, + 33 => 1567800, + 34 => 1714800, + 35 => 1870320, + 36 => 2034600, + 37 => 2207880, + 38 => 2390400, + 39 => 2582400, + 40 => 3221180, + 41 => 3891500, + 42 => 4594120, + 43 => 5329800, + 44 => 6099300, + 45 => 6903380, + 46 => 7742800, + 47 => 8618320, + 48 => 9530700, + 49 => 10480700, + 50 => 11469080, + 51 => 12496600, + 52 => 13564020, + 53 => 14672100, + 54 => 15821600, + 55 => 17013280, + 56 => 18247900, + 57 => 19526220, + 58 => 20849000, + 59 => 22217000, + 60 => 27426400, + 61 => 32808000, + 62 => 38364600, + 63 => 44099000, + 64 => 50014000, + 65 => 56112400, + 66 => 62397000, + 67 => 68870600, + 68 => 75536000, + 69 => 82396000, + 70 => 89453400, + 71 => 96711000, + 72 => 104171600, + 73 => 111838000, + 74 => 119713000, + 75 => 127799400, + 76 => 136100000, + 77 => 144617600, + 78 => 153355000, + 79 => 162315000, + 80 => 195120000, + 81 => 228740000, + 82 => 263185000, + 83 => 326689000, + 84 => 391714000, + 85 => 487862000, + 86 => 586259000, + 87 => 755624100, + 88 => 947884800, + 89 => 1163765200, + 90 => 1403999800, + 91 => 1669333500, + 92 => 1960521600, + 93 => 2278329800, + 94 => 2623534200, + 95 => 3154654000, + 96 => 3739216800, + 97 => 4793630400, + 98 => 5972157000, + 99 => 7286681500, + 100 => 2214522000, + 101 => 2349774000, + 102 => 2487691000, + 103 => 2628299000, + 104 => 2771624000, + 105 => 2917692000, + 106 => 3066529000, + 107 => 3218161000, + 108 => 3372614000, + 109 => 3529914000, + 110 => 3690087000, + 111 => 3853159000, + 112 => 4019156000, + 113 => 4188104000, + 114 => 4360029000, + 115 => 4534957000, + 116 => 4712914000, + 117 => 4893926000, + 118 => 5078019000, + 119 => 5265219000, + 120 => 5455552000, + 121 => 5649044000, + 122 => 5845721000, + 123 => 6045609000, + 124 => 6248734000, + 125 => 6455122000, + 126 => 6664799000, + 127 => 6877791000, + 128 => 7094124000, + 129 => 7313824000, + 130 => 7536917000, + 131 => 7763429000, + 132 => 7993386000, + 133 => 8226814000, + 134 => 8463739000, + 135 => 8704187000, + 136 => 8948184000, + 137 => 9195756000, + 138 => 9446929000, + 139 => 9701729000, + 140 => 9960182000, + 141 => 10222314000, + 142 => 10488151000, + 143 => 10757719000, + 144 => 11031044000, + 145 => 11308152000, + 146 => 11589069000, + 147 => 11873821000, + 148 => 12162434000, + 149 => 12454934000, + 150 => 12751347000, + _ => CalculateMaxXpWithMultipliers(level) + }; + + if (!isMate) + { + return xp; + } + + return mateType == MateType.Partner ? (long)(xp * 0.2) : (long)(xp * 0.05); + } + + public int GetHeroLevelXp(short level) + { + #region heroXpLabels + + int heroXp = level switch + { + 0 => 1068540, + 1 => 1068540, + 2 => 1187520, + 3 => 1306500, + 4 => 1425480, + 5 => 1544460, + 6 => 1663440, + 7 => 1782420, + 8 => 1901400, + 9 => 2020380, + 10 => 2139360, + 11 => 2139360, + 12 => 2377320, + 13 => 2615280, + 14 => 2853240, + 15 => 3091200, + 16 => 3329160, + 17 => 3567120, + 18 => 3805080, + 19 => 4043040, + 20 => 4281000, + 21 => 4518960, + 22 => 5113860, + 23 => 5708760, + 24 => 6303660, + 25 => 6898560, + 26 => 7493460, + 27 => 8088360, + 28 => 8683260, + 29 => 9278160, + 30 => 9873060, + 31 => 10467960, + 32 => 11538780, + 33 => 12609600, + 34 => 13680420, + 35 => 14751240, + 36 => 15822060, + 37 => 16892880, + 38 => 17963700, + 39 => 19034520, + 40 => 20105340, + 41 => 21176160, + 42 => 23079840, + 43 => 24983520, + 44 => 26887200, + 45 => 28790880, + 46 => 30694560, + 47 => 32598240, + 48 => 34501920, + 49 => 36405600, + 50 => 38309280, + 51 => 40212960, + 52 => 42116640, + 53 => 44020320, + 54 => 45924000, + 55 => 47827680, + 56 => 49731360, + 57 => 51635040, + 58 => 53538720, + 59 => 55442400, + 60 => 56422420, + _ => 0 + }; + + #endregion + + return heroXp; + } + + public int GetSpecialistJobXp(short level, bool isFunSpecialist = false) + { + #region spJobXpLabels + + if (isFunSpecialist) + { + return level switch + { + 1 => 10000, + 2 => 10000, + 3 => 10000, + 4 => 10000, + 5 => 10000, + 6 => 10000, + 7 => 10000, + 8 => 10000, + 9 => 10000, + 10 => 10000, + 11 => 10000, + 12 => 10000, + 13 => 10000, + 14 => 10000, + 15 => 10000, + 16 => 10000, + 17 => 10000, + 18 => 10000, + 19 => 10000, + 20 => 100000, + 21 => 105000, + 22 => 110000, + 23 => 115000, + 24 => 120000, + 25 => 125000, + 26 => 130000, + 27 => 135000, + 28 => 140000, + 29 => 145000, + 30 => 150000, + 31 => 155000, + 32 => 160000, + 33 => 165000, + 34 => 170000, + 35 => 175000, + 36 => 180000, + 37 => 185000, + 38 => 304000, + 39 => 312000, + 40 => 320000, + 41 => 328000, + 42 => 336000, + 43 => 344000, + 44 => 352000, + 45 => 360000, + 46 => 368000, + 47 => 376000, + 48 => 672000, + 49 => 686000, + 50 => 700000, + 51 => 714000, + 52 => 728000, + 53 => 742000, + 54 => 756000, + 55 => 770000, + 56 => 784000, + 57 => 798000, + 58 => 812000, + 59 => 826000, + 60 => 840000, + 61 => 854000, + 62 => 868000, + 63 => 882000, + 64 => 896000, + 65 => 910000, + 66 => 924000, + 67 => 938000, + 68 => 952000, + 69 => 966000, + 70 => 980000, + 71 => 994000, + 72 => 1008000, + 73 => 1022000, + 74 => 1036000, + 75 => 1050000, + 76 => 1064000, + 77 => 1078000, + 78 => 1092000, + 79 => 1106000, + 80 => 1120000, + 81 => 1134000, + 82 => 1148000, + 83 => 1162000, + 84 => 1176000, + 85 => 1190000, + 86 => 1204000, + 87 => 1218000, + 88 => 1232000, + 89 => 1246000, + 90 => 1260000, + 91 => 1274000, + 92 => 1288000, + 93 => 1302000, + 94 => 1316000, + 95 => 1330000, + 96 => 1344000, + 97 => 1358000, + 98 => 1372000, + 99 => 1386000, + _ => 1400000 + }; + } + + int spJobXp = level switch + { + 1 => 15000, + 2 => 25000, + 3 => 35000, + 4 => 45000, + 5 => 55000, + 6 => 65000, + 7 => 75000, + 8 => 85000, + 9 => 95000, + 10 => 105000, + 11 => 115000, + 12 => 125000, + 13 => 135000, + 14 => 145000, + 15 => 155000, + 16 => 165000, + 17 => 175000, + 18 => 185000, + 19 => 195000, + 20 => 23000, + 21 => 38132, + 22 => 53264, + 23 => 68396, + 24 => 83528, + 25 => 98660, + 26 => 113792, + 27 => 128924, + 28 => 144056, + 29 => 159188, + 30 => 174320, + 31 => 189452, + 32 => 204584, + 33 => 219716, + 34 => 234848, + 35 => 249980, + 36 => 265112, + 37 => 280244, + 38 => 295376, + 39 => 310508, + 40 => 325640, + 41 => 363470, + 42 => 401300, + 43 => 439130, + 44 => 476960, + 45 => 514790, + 46 => 552620, + 47 => 590450, + 48 => 628280, + 49 => 666110, + 50 => 703940, + 51 => 741770, + 52 => 779600, + 53 => 817430, + 54 => 855260, + 55 => 893090, + 56 => 930920, + 57 => 968750, + 58 => 1006580, + 59 => 1044410, + 60 => 1082240, + 61 => 1150334, + 62 => 1218428, + 63 => 1286522, + 64 => 1354616, + 65 => 1422710, + 66 => 1490804, + 67 => 1558898, + 68 => 1626992, + 69 => 1695086, + 70 => 1763180, + 71 => 1831274, + 72 => 1899368, + 73 => 1967462, + 74 => 2035556, + 75 => 2103650, + 76 => 2171744, + 77 => 2239838, + 78 => 2307932, + 79 => 2376026, + 80 => 2444120, + 81 => 2572742, + 82 => 2701364, + 83 => 2829986, + 84 => 2958608, + 85 => 3087230, + 86 => 3215852, + 87 => 3344474, + 88 => 3473096, + 89 => 3601718, + 90 => 3730340, + 91 => 3858962, + 92 => 3987584, + 93 => 4116206, + 94 => 4244828, + 95 => 4373450, + 96 => 4502072, + 97 => 4630694, + 98 => 4759316, + 99 => 4887938, + _ => 0 + }; + + #endregion + + return spJobXp; + } + + public int GetJobXp(short level, bool isAdventurer = false) + { + #region adventurerJobXpLabels + + if (isAdventurer) + { + int adventurerJobXp = level switch + { + 1 => 2200, + 2 => 2900, + 3 => 3600, + 4 => 4300, + 5 => 5000, + 6 => 5700, + 7 => 6400, + 8 => 7100, + 9 => 7800, + 10 => 8500, + 11 => 9200, + 12 => 9900, + 13 => 10600, + 14 => 11300, + 15 => 12000, + 16 => 12700, + 17 => 13400, + 18 => 14100, + 19 => 14800, + 20 => 15500 + }; + + return adventurerJobXp; + } + + #endregion + + #region jobXpLabels + + int jobXp = level switch + { + 1 => 14500, + 2 => 19000, + 3 => 23500, + 4 => 28000, + 5 => 32500, + 6 => 37000, + 7 => 41500, + 8 => 46000, + 9 => 50500, + 10 => 55000, + 11 => 59500, + 12 => 64000, + 13 => 68500, + 14 => 73000, + 15 => 77500, + 16 => 82000, + 17 => 86500, + 18 => 91000, + 19 => 95500, + 20 => 100000, + 21 => 104500, + 22 => 109000, + 23 => 113500, + 24 => 118000, + 25 => 122500, + 26 => 127000, + 27 => 131500, + 28 => 136000, + 29 => 140500, + 30 => 145000, + 31 => 149500, + 32 => 154000, + 33 => 158500, + 34 => 163000, + 35 => 167500, + 36 => 172000, + 37 => 176500, + 38 => 181000, + 39 => 185500, + 40 => 190000, + 41 => 205000, + 42 => 220000, + 43 => 235000, + 44 => 250000, + 45 => 265000, + 46 => 280000, + 47 => 295000, + 48 => 310000, + 49 => 325000, + 50 => 340000, + 51 => 355000, + 52 => 370000, + 53 => 385000, + 54 => 400000, + 55 => 415000, + 56 => 430000, + 57 => 445000, + 58 => 460000, + 59 => 475000, + 60 => 490000, + 61 => 505000, + 62 => 520000, + 63 => 535000, + 64 => 550000, + 65 => 565000, + 66 => 580000, + 67 => 595000, + 68 => 610000, + 69 => 625000, + 70 => 640000, + 71 => 655000, + 72 => 670000, + 73 => 685000, + 74 => 700000, + 75 => 715000, + 76 => 730000, + 77 => 745000, + 78 => 760000, + 79 => 775000, + 80 => 790000, + 81 => 805000, + 82 => 820000, + 83 => 835000, + 84 => 850000, + 85 => 865000, + 86 => 880000, + 87 => 895000, + 88 => 910000, + 89 => 925000, + 90 => 940000, + 91 => 955000, + 92 => 970000, + 93 => 985000, + 94 => 1000000, + 95 => 1015000, + 96 => 1030000, + 97 => 1045000, + 98 => 1060000, + 99 => 1075000, + 100 => 1090000 + }; + + #endregion + + return jobXp; + } + + public int GetFairyXp(short level) + { + if (level < 40) + { + return level * level + 50; + } + + return level * level * 3 + 50; + } + + public int GetRegenHp(IPlayerEntity character, ClassType type, bool isSiting) + { + int regenHp = type switch + { + ClassType.Adventurer => 30, + ClassType.Swordman => 80, + ClassType.Archer => 60, + ClassType.Magician => 30, + ClassType.Wrestler => 70, + _ => 0 + }; + + regenHp += character.StatisticsComponent.Passives.GetValueOrDefault(PassiveType.REGEN_HP); + regenHp += character.GetJewelsCellonsValue(CellonType.HpRecovery); + regenHp = (int)(regenHp * (1 + character.GetMaxArmorShellValue(ShellEffectType.RecovryHPOnRest) * 0.01)); + (int firstData, int secondData) hpBCards = character.BCardComponent.GetAllBCardsInformation(BCardType.Recovery, (byte)AdditionalTypes.Recovery.HPRecoveryIncreased, character.Level); + regenHp += hpBCards.firstData; + + if (isSiting) + { + return regenHp; + } + + double levelPenalty = character.Level switch + { + < 21 => 0.5, + < 41 => 0.4, + < 61 => 0.3, + _ => 0.2 + }; + + int regen = character.StatisticsComponent.Passives.GetValueOrDefault(PassiveType.PASSIVE_REGEN); + regenHp = (int)(regenHp * levelPenalty * regen + regenHp * character.GetMaxArmorShellValue(ShellEffectType.RevoryHP) * 0.01); + + return regenHp; + } + + public int GetRegenMp(IPlayerEntity character, ClassType type, bool isSiting) + { + int regenMp = type switch + { + ClassType.Adventurer => 10, + ClassType.Swordman => 30, + ClassType.Archer => 50, + ClassType.Magician => 80, + ClassType.Wrestler => 40, + _ => 0 + }; + + regenMp += character.StatisticsComponent.Passives.GetValueOrDefault(PassiveType.REGEN_MP); + regenMp += character.GetJewelsCellonsValue(CellonType.MpRecovery); + regenMp = (int)(regenMp * (1 + character.GetMaxArmorShellValue(ShellEffectType.RecoveryMPOnRest) * 0.01)); + (int firstData, int secondData) hpBCards = character.BCardComponent.GetAllBCardsInformation(BCardType.Recovery, (byte)AdditionalTypes.Recovery.MPRecoveryIncreased, character.Level); + regenMp += hpBCards.firstData; + + if (isSiting) + { + return regenMp; + } + + double levelPenalty = character.Level switch + { + < 21 => 0.5, + < 41 => 0.4, + < 61 => 0.3, + _ => 0.2 + }; + + int regen = character.StatisticsComponent.Passives.GetValueOrDefault(PassiveType.PASSIVE_REGEN); + regenMp = (int)(regenMp * levelPenalty * regen + regenMp * character.GetMaxArmorShellValue(ShellEffectType.RecoveryMP) * 0.01); + + return regenMp; + } + + public long CalculateMaxXp(short level) + { + if (level <= 1) + { + return 300; + } + + long xp = 120 * (1 + level) * (1 + level) / 2; + + if (level >= 15) + { + xp *= 2; + } + + if (level >= 85) + { + xp *= 650; + xp /= 6; + } + else if (level >= 83) + { + xp *= 450; + xp /= 6; + } + else if (level >= 80) + { + xp *= 250; + xp /= 6; + } + else if (level >= 60) + { + xp *= 70; + xp /= 6; + } + else if (level >= 40) + { + xp *= 19; + xp /= 6; + } + + return xp + CalculateMaxXp((short)(level - 1)); + } + + private long CalculateMaxXpWithMultipliers(short level) + { + double multiplier = level switch + { + 87 => 1.1, + 88 => 1.2, + 89 => 1.3, + 90 => 1.4, + 91 => 1.5, + 92 => 1.6, + 93 => 1.7, + 94 => 1.8, + 95 => 2.0, + 96 => 2.2, + 97 => 2.6, + 98 => 3.0, + 99 => 3.5, + > 99 => 3.5, + _ => 1 + }; + + return (long)(multiplier * CalculateMaxXp(level)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Algorithms/DamageAlgorithm.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Algorithms/DamageAlgorithm.cs new file mode 100644 index 0000000..9bd9f67 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Algorithms/DamageAlgorithm.cs @@ -0,0 +1,98 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using WingsEmu.DTOs.Skills; +using WingsEmu.Game._enum; +using WingsEmu.Game.Algorithm; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Helpers.Damages.Calculation; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.Algorithms; + +public class DamageAlgorithm : IDamageAlgorithm +{ + private readonly IBuffFactory _buffFactory; + + private readonly HashSet _specialSkills = new() + { + (int)SkillsVnums.GIANT_SWIRL, + (int)SkillsVnums.FOG_ARROW, + (int)SkillsVnums.FIRE_MINE, + (int)SkillsVnums.BOMB + }; + + public DamageAlgorithm(IBuffFactory buffFactory) => _buffFactory = buffFactory; + + public DamageAlgorithmResult GenerateDamage(IBattleEntityDump attacker, IBattleEntityDump defender, SkillInfo skill) + { + int damages = 0; + HitType hitMode = HitType.Normal; + bool onyxEffect = false; + + if (defender == null) + { + return new DamageAlgorithmResult(damages, hitMode, false, false); + } + + if (skill != null) + { + if (skill.TargetAffectedEntities != TargetAffectedEntities.Enemies || _specialSkills.Contains(skill.Vnum)) + { + return new DamageAlgorithmResult(damages, hitMode, false, false); + } + } + + CalculationBasicStatistics basicCalculation = attacker.CalculateBasicStatistics(defender, skill); + + if (attacker.IsMiss(defender, basicCalculation)) + { + hitMode = HitType.Miss; + return new DamageAlgorithmResult(damages, hitMode, false, false); + } + + CalculationDefense defense = attacker.CalculationDefense(defender); + CalculationPhysicalDamage physicalDamage = attacker.CalculatePhysicalDamage(defender, skill); + CalculationElementDamage elementDamage = attacker.CalculateElementDamage(defender, skill); + CalculationResult damageResult = attacker.DamageResult(defender, basicCalculation, defense, physicalDamage, elementDamage, skill); + + if (damageResult.IsCritical) + { + hitMode = HitType.Critical; + } + + damages = damageResult.Damage; + + (int firstData, int secondData, int count) onyxBuff = attacker.GetBCardInformation(BCardType.StealBuff, (byte)AdditionalTypes.StealBuff.ChanceSummonOnyxDragon); + if (attacker.IsSucceededChance(onyxBuff.firstData)) + { + onyxEffect = true; + } + + if (defender.GetBCardInformation(BCardType.NoDefeatAndNoDamage, (byte)AdditionalTypes.NoDefeatAndNoDamage.TransferAttackPower).count == 0) + { + return new DamageAlgorithmResult(damages, hitMode, onyxEffect, damageResult.IsSoftDamage); + } + + IBattleEntity targetEntity = attacker.MapInstance.GetBattleEntity(defender.Type, defender.Id); + if (targetEntity == null) + { + return new DamageAlgorithmResult(damages, hitMode, onyxEffect, damageResult.IsSoftDamage); + } + + if (targetEntity.ChargeComponent.GetCharge() != 0) + { + return new DamageAlgorithmResult(0, HitType.Miss, false, false); + } + + targetEntity.ChargeComponent.SetCharge(damages); + targetEntity.AddBuffAsync(_buffFactory.CreateBuff((short)BuffVnums.CHARGE, targetEntity)); + return new DamageAlgorithmResult(0, HitType.Miss, false, false); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Algorithms/ExperienceExtension.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Algorithms/ExperienceExtension.cs new file mode 100644 index 0000000..44e27d9 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Algorithms/ExperienceExtension.cs @@ -0,0 +1,130 @@ +using WingsAPI.Packets.Enums.Shells; +using WingsEmu.Game._enum; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Mates; +using WingsEmu.Packets.Enums; +using WingsEmu.Plugins.BasicImplementations.Event.Algorithm; + +namespace WingsEmu.Plugins.BasicImplementations.Algorithms; + +public static class ExperienceExtension +{ + public static ExcessExperience GetMoreExperience(this IPlayerEntity character, IServerManager serverManager) + { + double lowLevel = 1; + double lowJob = 1; + double lowJobSp = 1; + double mates = 1; + + lowLevel = character.Level switch + { + <= 5 => 3, + <= 18 => 2, + _ => lowLevel + }; + + lowJob = character.Level switch + { + <= 12 => 3, + <= 20 => 2, + _ => lowJob + }; + + double additionalLevel = 1; + double additionalJob = 1; + double additionalHeroLevel = 1; + double additionalMateLevel = 1; + double additionalPartnerLevel = 1; + double additionalSp = 0; + + int increaseExperienceBuff = character.BCardComponent.GetAllBCardsInformation(BCardType.Item, (byte)AdditionalTypes.Item.EXPIncreased, character.Level).firstData; + + additionalLevel += increaseExperienceBuff * 0.01; + additionalJob += increaseExperienceBuff * 0.01; + + int increaseHeroExperienceBuff = character.BCardComponent.GetAllBCardsInformation(BCardType.ReputHeroLevel, (byte)AdditionalTypes.ReputHeroLevel.ReceivedHeroExpIncrease, character.Level) + .firstData; + + additionalHeroLevel += increaseHeroExperienceBuff * 0.01; + + if (character.HasBuff(BuffVnums.GUARDIAN_BLESS)) + { + additionalMateLevel += 0.5; + additionalPartnerLevel += 0.5; + } + + if (character.HasBuff(BuffVnums.SOULSTONE_BLESSING)) + { + additionalSp += 0.5; + } + + if (character.HasBuff(BuffVnums.FAMILY_BUFF_XP)) + { + additionalLevel += 0.1; //TODO: check, if buff.familyId == character.Family.Id + } + + additionalLevel += character.GetMaxWeaponShellValue(ShellEffectType.GainMoreXP, true) * 0.01; + additionalJob += character.GetMaxWeaponShellValue(ShellEffectType.GainMoreCXP, true) * 0.01; + double additionalJobSp = additionalJob + additionalSp; + + IMateEntity mate = character.MateComponent.GetMate(x => x.MateType == MateType.Pet && x.IsTeamMember); + IMateEntity partner = character.MateComponent.GetMate(x => x.MateType == MateType.Partner && x.IsTeamMember); + + if (mate != null && partner == null) + { + mates = mate.IsAlive() ? 1.045 : 0.95; + } + + if (mate == null && partner != null) + { + mates = partner.IsAlive() ? 1.0625 : 0.85; + } + + if (mate != null && partner != null) + { + if (mate.IsAlive() && partner.IsAlive()) + { + mates = 1.08; + } + else if (mate.IsAlive() && !partner.IsAlive()) + { + mates = 0.88; + } + else if (!mate.IsAlive() && partner.IsAlive()) + { + mates = 1; + } + else + { + mates = 0.8; + } + } + + additionalLevel *= serverManager.MobXpRate; + additionalJob *= serverManager.JobXpRate; + additionalJobSp *= serverManager.JobXpRate; + additionalMateLevel *= serverManager.MateXpRate; + additionalPartnerLevel *= serverManager.PartnerXpRate; + + if (character.Specialist != null && character.UseSp) + { + additionalJob = character.Specialist.SpLevel < 20 ? 0 : additionalJobSp / 2.0; + + lowJobSp = character.Specialist.SpLevel switch + { + <= 9 => 10, + <= 17 => 5, + _ => lowJobSp + }; + } + else + { + additionalJobSp = 0; + } + + return new ExcessExperience(additionalLevel, additionalJob, additionalJobSp, additionalHeroLevel, additionalMateLevel, additionalPartnerLevel, lowLevel, lowJob, lowJobSp, mates); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Algorithms/FamilyAlgorithms/FamilyLevelBasedAlgorithm.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Algorithms/FamilyAlgorithms/FamilyLevelBasedAlgorithm.cs new file mode 100644 index 0000000..03bb5e9 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Algorithms/FamilyAlgorithms/FamilyLevelBasedAlgorithm.cs @@ -0,0 +1,17 @@ +namespace WingsEmu.Plugins.BasicImplementations.Algorithms.FamilyAlgorithms; + +public class FamilyLevelBasedAlgorithm : ILevelBasedDataAlgorithm +{ + public void Initialize() + { + Data = new long[] + { + 100000, 250000, 370000, 560000, 840000, + 1260000, 1900000, 2850000, 3570000, 3830000, + 4150000, 4750000, 5500000, 6500000, 7000000, + 8500000, 9500000, 10000000, 17000000, 999999999 + }; + } + + public long[] Data { get; set; } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Algorithms/ILevelBasedDataAlgorithm.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Algorithms/ILevelBasedDataAlgorithm.cs new file mode 100644 index 0000000..fceb38e --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Algorithms/ILevelBasedDataAlgorithm.cs @@ -0,0 +1,7 @@ +namespace WingsEmu.Plugins.BasicImplementations.Algorithms; + +public interface ILevelBasedDataAlgorithm +{ + long[] Data { get; set; } + void Initialize(); +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Algorithms/ShellGenerationAlgorithm.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Algorithms/ShellGenerationAlgorithm.cs new file mode 100644 index 0000000..0275385 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Algorithms/ShellGenerationAlgorithm.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using WingsAPI.Packets.Enums.Shells; +using WingsEmu.DTOs.Items; +using WingsEmu.Game; +using WingsEmu.Game.Algorithm; +using WingsEmu.Plugins.BasicImplementations.Algorithms.Shells; + +namespace WingsEmu.Plugins.BasicImplementations.Algorithms; + +public class ShellGenerationAlgorithm : IShellGenerationAlgorithm +{ + private readonly IRandomGenerator _randomGenerator; + private readonly ShellCategoryConfiguration _shellCategoryConfiguration; + private readonly IShellLevelEffectConfiguration _shellLevelEffect; + private readonly IShellOptionTypeConfiguration _shellOptionType; + + public ShellGenerationAlgorithm(IRandomGenerator randomGenerator, IShellOptionTypeConfiguration shellOptionType, IShellLevelEffectConfiguration shellLevelEffect, + ShellCategoryConfiguration shellCategoryConfiguration) + { + _randomGenerator = randomGenerator; + _shellOptionType = shellOptionType; + _shellLevelEffect = shellLevelEffect; + _shellCategoryConfiguration = shellCategoryConfiguration; + } + + public IEnumerable GenerateShell(byte shellType, int shellRarity, int shellLevel) + { + var shellOptions = new List(); + var optionsAlreadyOn = new List(); + + IReadOnlyCollection possibleCategories = _shellLevelEffect.GetEffects(shellType, (byte)shellRarity); + if (possibleCategories == null) + { + return shellOptions; + } + + foreach (ShellPossibleCategory possibleCategory in possibleCategories) + { + IReadOnlyCollection possibleEffects = _shellCategoryConfiguration.FirstOrDefault(s => s.EffectCategory == possibleCategory.EffectCategory)?.PossibleEffects; + if (possibleEffects == null) + { + continue; + } + + var possibleOptions = possibleEffects.Where(s => !optionsAlreadyOn.Contains((byte)s)).ToList(); + if (!possibleOptions.Any()) + { + continue; + } + + if (possibleCategory.IsRandom && _randomGenerator.RandomNumber(2) != 0) + { + continue; + } + + byte generatedOption = (byte)possibleOptions[_randomGenerator.RandomNumber(possibleOptions.Count)]; + int? optionValue = GenerateOptionValue(generatedOption, (byte)possibleCategory.EffectCategory, shellLevel); + if (optionValue == null) + { + continue; + } + + optionsAlreadyOn.Add(generatedOption); + shellOptions.Add(new EquipmentOptionDTO + { + EquipmentOptionType = generatedOption > 50 ? EquipmentOptionType.ARMOR_SHELL : EquipmentOptionType.WEAPON_SHELL, // TODO: seperate it after closed-beta gameplay + Level = (byte)possibleCategory.EffectCategory, + Type = generatedOption, + Value = (int)optionValue + }); + } + + return shellOptions; + } + + private int? GenerateOptionValue(byte randomShellEffectType, byte shellEffectCategory, int shellLevel) + { + try + { + int[] shellOptionValues = _shellOptionType.GetByTypeAndEffect(randomShellEffectType, shellEffectCategory); + if (shellOptionValues == null) + { + return null; + } + + int m = _randomGenerator.RandomNumber(shellOptionValues[0] * shellLevel, shellOptionValues[1] * shellLevel); + int value = (int)Math.Floor((double)m / 100); + + return value == 0 ? 1 : value; + } + catch (Exception e) + { + return null; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Algorithms/Shells/ShellCategoryConfiguration.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Algorithms/Shells/ShellCategoryConfiguration.cs new file mode 100644 index 0000000..4357731 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Algorithms/Shells/ShellCategoryConfiguration.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using WingsAPI.Packets.Enums.Shells; + +namespace WingsEmu.Plugins.BasicImplementations.Algorithms.Shells; + +public class ShellCategoryConfiguration : List +{ +} + +public class ShellCategoryInfo +{ + public ShellEffectCategory EffectCategory { get; set; } + public List PossibleEffects { get; set; } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Algorithms/Shells/ShellLevelEffectConfiguration.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Algorithms/Shells/ShellLevelEffectConfiguration.cs new file mode 100644 index 0000000..c753edc --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Algorithms/Shells/ShellLevelEffectConfiguration.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using PhoenixLib.Logging; +using WingsAPI.Packets.Enums.Shells; + +namespace WingsEmu.Plugins.BasicImplementations.Algorithms.Shells; + +public interface IShellLevelEffectConfiguration +{ + IReadOnlyCollection GetEffects(byte shellType, byte rarity); +} + +public class ShellLevelEffectConfiguration : IShellLevelEffectConfiguration +{ + private readonly ImmutableDictionary<(byte, byte), List> _shellEffects; + + public ShellLevelEffectConfiguration(IEnumerable shellEffects) + { + var dict = new Dictionary<(byte, byte), List>(); + foreach (ShellLevelEffect shellEffect in shellEffects) + { + foreach (ShellRarityEffect shellRarityEffect in shellEffect.Rarities) + { + dict.Add(((byte)shellEffect.ShellType, shellRarityEffect.Rarity), shellRarityEffect.PossibleEffects); + } + } + + _shellEffects = dict.ToImmutableDictionary(); + } + + public IReadOnlyCollection GetEffects(byte shellType, byte rarity) + { + if (!_shellEffects.ContainsKey((shellType, rarity))) + { + Log.Debug($"[ERROR] A configuration for {shellType.ToString()} has not been found."); + return null; + } + + return _shellEffects[(shellType, rarity)]; + } +} + +public class ShellLevelEffect +{ + public ShellType ShellType { get; set; } + public List Rarities { get; set; } +} + +public class ShellRarityEffect +{ + public byte Rarity { get; set; } + public List PossibleEffects { get; set; } +} + +public class ShellPossibleCategory +{ + public ShellEffectCategory EffectCategory { get; set; } + public bool IsRandom { get; set; } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Algorithms/Shells/ShellOptionTypeConfiguration.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Algorithms/Shells/ShellOptionTypeConfiguration.cs new file mode 100644 index 0000000..389e712 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Algorithms/Shells/ShellOptionTypeConfiguration.cs @@ -0,0 +1,66 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using WingsAPI.Packets.Enums.Shells; + +namespace WingsEmu.Plugins.BasicImplementations.Algorithms.Shells; + +public interface IShellOptionTypeConfiguration +{ + int[] GetByTypeAndEffect(byte optionType, byte effect); +} + +public class ShellOptionTypeConfiguration : IShellOptionTypeConfiguration +{ + private readonly ImmutableDictionary _optionValues; + + public ShellOptionTypeConfiguration(IEnumerable optionValues) + { + _optionValues = optionValues.ToImmutableDictionary(s => (byte)s.Id); + } + + public int[] GetByTypeAndEffect(byte optionType, byte effect) + { + switch (effect) + { + case (byte)ShellEffectCategory.CNormalWeapon: + case (byte)ShellEffectCategory.CBonusWeapon: + case (byte)ShellEffectCategory.CPVPWeapon: + case (byte)ShellEffectCategory.CNormalArmor: + case (byte)ShellEffectCategory.CBonusArmor: + case (byte)ShellEffectCategory.CPVPArmor: + return _optionValues.GetValueOrDefault(optionType)?.CRangeValues; + case (byte)ShellEffectCategory.BNormalWeapon: + case (byte)ShellEffectCategory.BBonusWeapon: + case (byte)ShellEffectCategory.BPVPWeapon: + case (byte)ShellEffectCategory.BNormalArmor: + case (byte)ShellEffectCategory.BBonusArmor: + case (byte)ShellEffectCategory.BPVPArmor: + return _optionValues.GetValueOrDefault(optionType)?.BRangeValues; + case (byte)ShellEffectCategory.ANormalWeapon: + case (byte)ShellEffectCategory.ABonusWeapon: + case (byte)ShellEffectCategory.APVPWeapon: + case (byte)ShellEffectCategory.ANormalArmor: + case (byte)ShellEffectCategory.ABonusArmor: + case (byte)ShellEffectCategory.APVPArmor: + return _optionValues.GetValueOrDefault(optionType)?.ARangeValues; + case (byte)ShellEffectCategory.SNormalWeapon: + case (byte)ShellEffectCategory.SBonusWeapon: + case (byte)ShellEffectCategory.SPVPWeapon: + case (byte)ShellEffectCategory.SNormalArmor: + case (byte)ShellEffectCategory.SBonusArmor: + case (byte)ShellEffectCategory.SPVPArmor: + return _optionValues.GetValueOrDefault(optionType)?.SRangeValues; + } + + return null; + } +} + +public class ShellOptionValues +{ + public ShellEffectType Id { get; set; } + public int[] CRangeValues { get; set; } + public int[] BRangeValues { get; set; } + public int[] ARangeValues { get; set; } + public int[] SRangeValues { get; set; } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Algorithms/Shells/ShellPerfumeConfiguration.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Algorithms/Shells/ShellPerfumeConfiguration.cs new file mode 100644 index 0000000..3ab9c64 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Algorithms/Shells/ShellPerfumeConfiguration.cs @@ -0,0 +1,52 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; + +namespace WingsEmu.Plugins.BasicImplementations.Algorithms.Shells; + +public interface IShellPerfumeConfiguration +{ + int? GetPerfumesByLevelAndRarity(short level, byte rarity, bool isHeroic); + int? GetGoldByLevel(short level, bool isHero); +} + +public class ShellPerfumeConfiguration : IShellPerfumeConfiguration +{ + private readonly ImmutableList _perfumes; + + public ShellPerfumeConfiguration(IEnumerable perfumeConfigurations) => _perfumes = perfumeConfigurations.ToImmutableList(); + + public int? GetPerfumesByLevelAndRarity(short level, byte rarity, bool isHeroic) + { + PerfumeConfiguration perfumeConfiguration = _perfumes.FirstOrDefault(s => s.IsHero == isHeroic && s.MinLevel <= level && s.MaxLevel >= level); + if (perfumeConfiguration == null) + { + return null; + } + + int? perfumes = perfumeConfiguration.Perfumes.FirstOrDefault(s => s.Rarity == rarity)?.Perfumes; + return perfumes ?? 0; + } + + public int? GetGoldByLevel(short level, bool isHeroic) => _perfumes + .FirstOrDefault(s => s.IsHero == isHeroic && s.MinLevel <= level && s.MaxLevel >= level)?.Gold; +} + +public class PerfumeConfiguration +{ + public int MinLevel { get; set; } + public int MaxLevel { get; set; } + public int Gold { get; set; } + public bool IsHero { get; set; } + public List Perfumes { get; set; } +} + +public class PerfumesByRarity +{ + public int Rarity { get; set; } + public int Perfumes { get; set; } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Arena/ArenaManager.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Arena/ArenaManager.cs new file mode 100644 index 0000000..2648b03 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Arena/ArenaManager.cs @@ -0,0 +1,38 @@ +using System.Threading.Tasks; +using PhoenixLib.Logging; +using WingsAPI.Communication.ServerApi.Protocol; +using WingsEmu.Game._enum; +using WingsEmu.Game.Arena; +using WingsEmu.Game.Maps; + +namespace WingsEmu.Plugins.BasicImplementations.Arena; + +public class ArenaManager : IArenaManager +{ + private readonly SerializableGameServer _gameServer; + private readonly IMapManager _mapManager; + + public ArenaManager(IMapManager mapManager, SerializableGameServer gameServer) + { + _mapManager = mapManager; + _gameServer = gameServer; + } + + public IMapInstance ArenaInstance { get; private set; } + public IMapInstance FamilyArenaInstance { get; private set; } + + public async Task Initialize() + { + if (_gameServer.ChannelType == GameChannelType.ACT_4) + { + Log.Warn("[ARENA_MANAGER] Not loading Arena because the channel is act4"); + return; + } + + ArenaInstance = _mapManager.GenerateMapInstanceByMapId((int)MapIds.ARENA_INDIVIDUAL, MapInstanceType.ArenaInstance); + Log.Info(ArenaInstance == null ? "[ARENA_MANAGER] Failed to load Arena Map" : "[ARENA_MANAGER] Arena Map Loaded"); + + FamilyArenaInstance = _mapManager.GenerateMapInstanceByMapId((int)MapIds.ARENA_FAMILY, MapInstanceType.ArenaInstance); + Log.Info(FamilyArenaInstance == null ? "[ARENA_MANAGER] Failed to load Family Arena Map" : "[ARENA_MANAGER] Family Arena Map Loaded"); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/BCardEffectContextFactory.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/BCardEffectContextFactory.cs new file mode 100644 index 0000000..ab64323 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/BCardEffectContextFactory.cs @@ -0,0 +1,13 @@ +using WingsEmu.DTOs.BCards; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Helpers.Damages; + +namespace WingsEmu.Plugins.BasicImplementations.BCards; + +public class BCardEffectContextFactory : IBCardEventContextFactory +{ + public IBCardEffectContext NewContext(IBattleEntity sender, IBattleEntity target, BCardDTO bcard, SkillInfo skill = null, Position position = default) + => new BcardEffectContext(sender, target, bcard, skill, position); +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/BCardGamePlugin.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/BCardGamePlugin.cs new file mode 100644 index 0000000..3b1b1f4 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/BCardGamePlugin.cs @@ -0,0 +1,43 @@ +using System; +using PhoenixLib.Extensions; +using PhoenixLib.Logging; +using WingsAPI.Plugins; +using WingsEmu.Game.Buffs; + +namespace WingsEmu.Plugins.BasicImplementations.BCards; + +public class BCardGamePlugin : IGamePlugin +{ + private readonly IServiceProvider _container; + private readonly IBCardEffectHandlerContainer _handlers; + + public BCardGamePlugin(IBCardEffectHandlerContainer handlers, IServiceProvider container) + { + _handlers = handlers; + _container = container; + } + + public string Name => nameof(BCardGamePlugin); + + public void OnLoad() + { + foreach (Type handlerType in typeof(BCardGamePlugin).Assembly.GetTypesImplementingInterface()) + { + try + { + object tmp = _container.GetService(handlerType); + if (!(tmp is IBCardEffectAsyncHandler real)) + { + continue; + } + + Log.Debug($"[BCARD][ADD_HANDLER] {handlerType}"); + _handlers.Register(real); + } + catch (Exception e) + { + Log.Error("[BCARD][FAIL_ADD]", e); + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/BCardHandlerContainer.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/BCardHandlerContainer.cs new file mode 100644 index 0000000..4419580 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/BCardHandlerContainer.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using PhoenixLib.Logging; +using WingsEmu.DTOs.BCards; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Helpers.Damages; + +namespace WingsEmu.Plugins.BasicImplementations.BCards; + +public class BCardHandlerContainer : IBCardEffectHandlerContainer +{ + private readonly IBCardEventContextFactory _contextFactory; + private readonly Dictionary _handlers; + + public BCardHandlerContainer(IBCardEventContextFactory contextFactory) + { + _contextFactory = contextFactory; + _handlers = new Dictionary(); + } + + public void Register(IBCardEffectAsyncHandler handler) + { + _handlers.Add(handler.HandledType, handler); + Log.Debug($"[BCARD][REGISTER_HANDLER] BCARDTYPE: {handler.HandledType} REGISTERED!"); + } + + public void Unregister(IBCardEffectAsyncHandler handler) + { + _handlers.Remove(handler.HandledType); + } + + public void Execute(IBattleEntity target, IBattleEntity sender, BCardDTO bCard, SkillInfo skill = null, Position position = default, + BCardNpcMonsterTriggerType triggerType = BCardNpcMonsterTriggerType.NONE) + { + if (target == null) + { + return; + } + + if (!_handlers.TryGetValue((BCardType)bCard.Type, out IBCardEffectAsyncHandler handler)) + { + return; + } + + IBCardEffectContext context = _contextFactory.NewContext(sender, target, bCard, skill, position); + handler.Execute(context); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/BCardHandlersServicesExtensions.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/BCardHandlersServicesExtensions.cs new file mode 100644 index 0000000..be31fff --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/BCardHandlersServicesExtensions.cs @@ -0,0 +1,30 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using PhoenixLib.Extensions; +using WingsEmu.Game.Buffs; + +namespace WingsEmu.Plugins.BasicImplementations.BCards; + +internal static class BCardHandlersServicesExtensions +{ + internal static void AddBcardHandlers(this IServiceCollection services) + { + Type[] tmp = typeof(BCardGamePlugin).Assembly.GetTypesImplementingInterface(); + foreach (Type handlerType in tmp) + { + if (handlerType.IsAbstract) + { + continue; + } + + services.AddTransient(handlerType); + } + + services.AddSingleton(); + } + + internal static void AddBCardContextFactory(this IServiceCollection services) + { + services.AddTransient(); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/BCardPluginCore.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/BCardPluginCore.cs new file mode 100644 index 0000000..b0084a5 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/BCardPluginCore.cs @@ -0,0 +1,15 @@ +using Microsoft.Extensions.DependencyInjection; +using WingsAPI.Plugins; + +namespace WingsEmu.Plugins.BasicImplementations.BCards; + +public class BCardPluginCore : IGameServerPlugin +{ + public string Name => nameof(BCardPluginCore); + + public void AddDependencies(IServiceCollection services, GameServerLoader gameServer) + { + services.AddBcardHandlers(); + services.AddBCardContextFactory(); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/BcardEffectContext.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/BcardEffectContext.cs new file mode 100644 index 0000000..78d876a --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/BcardEffectContext.cs @@ -0,0 +1,25 @@ +using WingsEmu.DTOs.BCards; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Helpers.Damages; + +namespace WingsEmu.Plugins.BasicImplementations.BCards; + +public class BcardEffectContext : IBCardEffectContext +{ + public BcardEffectContext(IBattleEntity sender, IBattleEntity target, BCardDTO bCard, SkillInfo skill = null, Position position = default) + { + Sender = sender; + Target = target; + BCard = bCard; + Skill = skill; + Position = position; + } + + public IBattleEntity Sender { get; } + public IBattleEntity Target { get; } + public BCardDTO BCard { get; } + public SkillInfo Skill { get; } + public Position Position { get; } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardBeriosHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardBeriosHandler.cs new file mode 100644 index 0000000..b8b293c --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardBeriosHandler.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using PhoenixLib.Events; +using WingsEmu.DTOs.BCards; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Entities.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Extensions.Mates; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Mates.Events; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.BCards.Handlers; + +public class BCardBeriosHandler : IBCardEffectAsyncHandler +{ + private readonly IBuffFactory _buffFactory; + private readonly IAsyncEventPipeline _eventPipeline; + private readonly GameRevivalConfiguration _gameRevivalConfiguration; + + public BCardBeriosHandler(IAsyncEventPipeline eventPipeline, GameRevivalConfiguration gameRevivalConfiguration, IBuffFactory buffFactory) + { + _eventPipeline = eventPipeline; + _gameRevivalConfiguration = gameRevivalConfiguration; + _buffFactory = buffFactory; + } + + public BCardType HandledType => BCardType.LordBerios; + + public void Execute(IBCardEffectContext ctx) + { + IBattleEntity sender = ctx.Sender; + BCardDTO bCard = ctx.BCard; + + int firstDataValue = bCard.FirstDataValue(sender.Level); + int secondDataValue = bCard.SecondDataValue(sender.Level); + + switch ((AdditionalTypes.LordBerios)ctx.BCard.SubType) + { + case AdditionalTypes.LordBerios.CauseDamage: + + IEnumerable toDamage = sender.GetEnemiesInRange(sender, (byte)firstDataValue).Take(50); + + foreach (IBattleEntity entity in toDamage) + { + if (!entity.IsAlive()) + { + continue; + } + + int damage = (int)(entity.MaxHp * (secondDataValue * 0.01)); + + if (sender.ShouldSaveDefender(entity, damage, _gameRevivalConfiguration, _buffFactory).ConfigureAwait(false).GetAwaiter().GetResult()) + { + continue; + } + + if (entity.Hp - damage <= 0) + { + entity.Hp = 0; + entity.EmitEvent(new GenerateEntityDeathEvent + { + Entity = entity, + Attacker = sender + }); + + sender.BroadcastCleanSuPacket(entity, damage); + continue; + } + + entity.Hp -= damage; + + switch (entity) + { + case IPlayerEntity character: + character.LastDefence = DateTime.UtcNow; + character.Session.RefreshStat(); + + if (character.IsSitting) + { + character.Session.RestAsync(force: true); + } + + break; + case IMateEntity mate: + mate.LastDefence = DateTime.UtcNow; + mate.Owner.Session.SendMateLife(mate); + + if (mate.IsSitting) + { + mate.Owner.Session.EmitEvent(new MateRestEvent + { + MateEntity = mate, + Force = true + }); + } + + break; + } + + sender.BroadcastCleanSuPacket(entity, damage); + } + + break; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardBuffHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardBuffHandler.cs new file mode 100644 index 0000000..b7dd28e --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardBuffHandler.cs @@ -0,0 +1,216 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Linq; +using WingsEmu.DTOs.Skills; +using WingsEmu.Game; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Skills; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.BCards.Handlers; + +public class BCardBuffHandler : IBCardEffectAsyncHandler +{ + private readonly IBuffFactory _buffFactory; + private readonly ICardsManager _cards; + private readonly IGameLanguageService _gameLanguage; + private readonly IRandomGenerator _randomGenerator; + + public BCardBuffHandler(IRandomGenerator randomGenerator, IBuffFactory buffFactory, IGameLanguageService gameLanguage, ICardsManager cards) + { + _randomGenerator = randomGenerator; + _buffFactory = buffFactory; + _gameLanguage = gameLanguage; + _cards = cards; + } + + public BCardType HandledType => BCardType.Buff; + + public void Execute(IBCardEffectContext ctx) + { + IBattleEntity sender = ctx.Sender; + IBattleEntity target = ctx.Target; + + switch ((AdditionalTypes.Buff)ctx.BCard.SubType) + { + case AdditionalTypes.Buff.ChanceCausing: + if (sender == null) + { + return; + } + + Buff b = _buffFactory.CreateBuff(ctx.BCard.SecondData, sender); + if (b == null) + { + return; + } + + double debuffCounter = target.CheckForResistance(b, _cards, out double buffCounter, out double specializedResistance); + + int randomNumber = _randomGenerator.RandomNumber(); + int debuffRandomNumber = _randomGenerator.RandomNumber(); + int buffRandomNumber = _randomGenerator.RandomNumber(); + int specializedRandomNumber = _randomGenerator.RandomNumber(); + if (b.CardId == (short)BuffVnums.MEMORIAL && sender.BuffComponent.HasBuff((short)BuffVnums.MEMORIAL)) + { + return; + } + + if (randomNumber > ctx.BCard.FirstData) + { + return; + } + + if (specializedRandomNumber >= (int)(specializedResistance * 100)) + { + if (target is not IPlayerEntity c) + { + return; + } + + string message = _gameLanguage.GetLanguage(GameDialogKey.BUFF_CHATMESSAGE_EFFECT_RESISTANCE, c.Session.UserLanguage); + c.Session.SendChatMessage(message, ChatMessageColorType.Buff); + return; + } + + if (ctx.Skill?.Vnum is (short)SkillsVnums.FIRE_MINE or (short)SkillsVnums.BOMB) + { + if (sender.IsSameEntity(target)) + { + return; + } + } + + switch (b.BuffGroup) + { + case BuffGroup.Bad when debuffRandomNumber >= (int)(debuffCounter * 100): + { + if (target is not IPlayerEntity c) + { + return; + } + + string message = _gameLanguage.GetLanguage(GameDialogKey.BUFF_CHATMESSAGE_EFFECT_RESISTANCE, c.Session.UserLanguage); + c.Session.SendChatMessage(message, ChatMessageColorType.Buff); + return; + } + case BuffGroup.Good when buffRandomNumber >= (int)(buffCounter * 100): + { + if (target is not IPlayerEntity c) + { + return; + } + + string message = _gameLanguage.GetLanguage(GameDialogKey.BUFF_CHATMESSAGE_EFFECT_RESISTANCE, c.Session.UserLanguage); + c.Session.SendChatMessage(message, ChatMessageColorType.Buff); + return; + } + case BuffGroup.Bad when target is IMonsterEntity monsterEntity: + monsterEntity.MapInstance.AddEntityToTargets(monsterEntity, sender); + break; + } + + switch (sender) + { + case IMonsterEntity monster when monster.SummonerId != null && monster.SummonerId == target.Id && monster.SummonerType != null && monster.SummonerType == target.Type: + return; + case IMateEntity { IsUsingSp: true } mateEntity: + { + IBattleEntitySkill skill = mateEntity.LastUsedPartnerSkill; + if (skill is not PartnerSkill partnerSkill) + { + return; + } + + int buffVnum = ctx.BCard.SecondData; + + Buff partnerBuff = _buffFactory.CreateBuff(buffVnum + (buffVnum.IsPartnerRankBuff() ? partnerSkill.Rank - 1 : 0), sender); + target.AddBuffAsync(partnerBuff).ConfigureAwait(false).GetAwaiter().GetResult(); + return; + } + } + + if (target is IMateEntity { IsUsingSp: true } mate) + { + IBattleEntitySkill skill = mate.LastUsedPartnerSkill; + + int buffVnum = ctx.BCard.SecondData; + if (skill != null && skill.Skill.TargetType == TargetType.Self && sender.Id == target.Id && skill is PartnerSkill partnerSkill) + { + Buff partnerBuff = _buffFactory.CreateBuff(buffVnum + (buffVnum.IsPartnerRankBuff() ? partnerSkill.Rank - 1 : 0), sender); + target.AddBuffAsync(partnerBuff).ConfigureAwait(false).GetAwaiter().GetResult(); + return; + } + } + + if (b.CardId == (int)BuffVnums.SONG_OF_THE_SIRENS && target is IPlayerEntity) + { + Buff sirensBuff = _buffFactory.CreateBuff((int)BuffVnums.SONG_OF_THE_SIRENS_PVP, sender); + target.AddBuffAsync(sirensBuff).ConfigureAwait(false).GetAwaiter().GetResult(); + return; + } + + Buff buff = _buffFactory.CreateBuff(ctx.BCard.SecondData, sender); + int firstRandomNumber = _randomGenerator.RandomNumber(); + int secondRandomNumber = _randomGenerator.RandomNumber(); + if (target.BCardComponent.HasBCard(BCardType.TauntSkill, (byte)AdditionalTypes.TauntSkill.ReflectBadEffect) && firstRandomNumber <= secondRandomNumber && + buff.BuffGroup == BuffGroup.Bad) + { + sender.AddBuffAsync(buff).ConfigureAwait(false).GetAwaiter().GetResult(); + return; + } + + target.AddBuffAsync(buff).ConfigureAwait(false).GetAwaiter().GetResult(); + break; + case AdditionalTypes.Buff.ChanceRemoving: + if (!target.BuffComponent.HasBuff(ctx.BCard.SecondData)) + { + return; + } + + if (_randomGenerator.RandomNumber() > ctx.BCard.FirstData) + { + return; + } + + Buff chanceRemoving = target.BuffComponent.GetBuff(ctx.BCard.SecondData); + target.RemoveBuffAsync(false, chanceRemoving).ConfigureAwait(false).GetAwaiter().GetResult(); + break; + case AdditionalTypes.Buff.CancelGroupOfEffects: + + int firstDataValue = ctx.BCard.FirstDataValue(target.Level); + int secondDataValue = ctx.BCard.SecondDataValue(target.Level); + + target.RemoveBuffAsync(false, + target.BuffComponent.GetAllBuffs().Where(x => x.GroupId == firstDataValue && x.Level <= secondDataValue).ToArray()).ConfigureAwait(false).GetAwaiter().GetResult(); + + break; + case AdditionalTypes.Buff.CounteractPoison: + + firstDataValue = ctx.BCard.FirstDataValue(target.Level); + secondDataValue = ctx.BCard.SecondDataValue(target.Level); + + if (!Enum.TryParse(firstDataValue.ToString(), out BuffCategory buffCategory)) + { + return; + } + + target.RemoveBuffAsync(false, + target.BuffComponent.GetAllBuffs().Where(x => x.BuffCategory == buffCategory && x.Level <= secondDataValue).ToArray()).ConfigureAwait(false).GetAwaiter().GetResult(); + + break; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardCalvinasHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardCalvinasHandler.cs new file mode 100644 index 0000000..80c1c68 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardCalvinasHandler.cs @@ -0,0 +1,86 @@ +using System.Collections.Generic; +using System.Text; +using WingsEmu.DTOs.BCards; +using WingsEmu.Game; +using WingsEmu.Game.Act4; +using WingsEmu.Game.Act4.Configuration; +using WingsEmu.Game.Act4.Entities; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.BCards.Handlers; + +public class BCardCalvinasHandler : IBCardEffectAsyncHandler +{ + private readonly Act4DungeonsConfiguration _act4DungeonsConfiguration; + private readonly IDungeonManager _dungeonManager; + private readonly IRandomGenerator _randomGenerator; + + public BCardCalvinasHandler(IDungeonManager dungeonManager, Act4DungeonsConfiguration act4DungeonsConfiguration, IRandomGenerator randomGenerator) + { + _dungeonManager = dungeonManager; + _act4DungeonsConfiguration = act4DungeonsConfiguration; + _randomGenerator = randomGenerator; + } + + public BCardType HandledType => BCardType.LordCalvinas; + + public void Execute(IBCardEffectContext ctx) + { + IBattleEntity sender = ctx.Sender; + BCardDTO bCard = ctx.BCard; + + switch ((AdditionalTypes.LordCalvinas)bCard.SubType) + { + case AdditionalTypes.LordCalvinas.InflictDamageAtLocation: + var dragonCord = new StringBuilder(); + List calvinasDragons = new(); + + int amountOfDragons = _randomGenerator.RandomNumber(1, 3); + + for (int i = 0; i < amountOfDragons; i++) + { + int at = _randomGenerator.RandomNumber(0, 11); + int axis = _randomGenerator.RandomNumber(0, 2); + + var newDragon = new CalvinasDragon + { + Axis = axis == 0 ? CoordType.X : CoordType.Y, + Size = 3, + At = (short)(at * 5), + Start = -50, + End = 400 + }; + + calvinasDragons.Add(newDragon); + } + + foreach (CalvinasDragon dragon in calvinasDragons) + { + if (dragon.Axis == CoordType.X) + { + dragonCord.Append($"{dragon.Start} {dragon.At} {dragon.End} {dragon.At} "); + } + else + { + dragonCord.Append($"{dragon.At} {dragon.Start} {dragon.At} {dragon.End} "); + } + } + + if (calvinasDragons.Count == 1) + { + dragonCord.Append("0 0 0 0"); + } + + sender.MapInstance.Broadcast(sender.GenerateDragonPacket((byte)calvinasDragons.Count) + dragonCord); + _dungeonManager.AddCalvinasDragons(sender.MapInstance.Id, new CalvinasState + { + CalvinasDragonsList = calvinasDragons, + CastTime = sender.GenerateSkillCastTime(ctx.Skill) + }); + break; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardCaptureHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardCaptureHandler.cs new file mode 100644 index 0000000..57a2aa8 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardCaptureHandler.cs @@ -0,0 +1,86 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsEmu.Game._i18n; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Battle; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.BCards.Handlers; + +public class BCardCaptureHandler : IBCardEffectAsyncHandler +{ + private readonly IGameLanguageService _gameLanguage; + + public BCardCaptureHandler(IGameLanguageService gameLanguage) => _gameLanguage = gameLanguage; + public BCardType HandledType => BCardType.Capture; + + public async void Execute(IBCardEffectContext ctx) + { + if (ctx.Sender is not IPlayerEntity playerEntity) + { + return; + } + + IClientSession session = playerEntity.Session; + + if (playerEntity.MapInstance.MapInstanceType == MapInstanceType.RaidInstance) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.SKILL_SHOUTMESSAGE_CAPTURE_IN_RAID, session.UserLanguage), MsgMessageType.Middle); + session.SendCancelPacket(CancelType.NotInCombatMode); + return; + } + + if (ctx.Target is not IMonsterEntity monsterToCapture) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.SKILL_SHOUTMESSAGE_CAPTURE_IMPOSSIBLE, session.UserLanguage), MsgMessageType.Middle); + session.SendCancelPacket(CancelType.NotInCombatMode); + return; + } + + if (monsterToCapture.Level > playerEntity.Level) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.SKILL_SHOUTMESSAGE_MONSTER_LEVEL_MUST_BE_LOWER_THAN_YOURS, session.UserLanguage), MsgMessageType.Middle); + session.SendCancelPacket(CancelType.NotInCombatMode); + return; + } + + if (monsterToCapture.GetHpPercentage() >= 50) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.SKILL_SHOUTMESSAGE_MONSTER_MUST_BE_LOW_HP, session.UserLanguage), MsgMessageType.Middle); + session.SendCancelPacket(CancelType.NotInCombatMode); + return; + } + + if (playerEntity.MaxPetCount <= playerEntity.MateComponent.GetMates(x => x.MateType == MateType.Pet).Count) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_MAX_PET_COUNT, session.UserLanguage), MsgMessageType.Middle); + session.SendCancelPacket(CancelType.NotInCombatMode); + return; + } + + if (!monsterToCapture.CanBeCaught) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.SKILL_SHOUTMESSAGE_CAPTURE_IMPOSSIBLE, session.UserLanguage), MsgMessageType.Middle); + session.SendCancelPacket(CancelType.NotInCombatMode); + return; + } + + if (playerEntity.GetDignityIco() > 3) + { + session.SendMsg(session.GetLanguage(GameDialogKey.SKILL_SHOUTMESSAGE_CAPTURE_DIGNITY_LOW), MsgMessageType.Middle); + session.SendCancelPacket(CancelType.NotInCombatMode); + return; + } + + await session.EmitEventAsync(new MonsterCaptureEvent(monsterToCapture, true, ctx.Skill)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardCountHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardCountHandler.cs new file mode 100644 index 0000000..031c702 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardCountHandler.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using PhoenixLib.Events; +using WingsEmu.DTOs.BCards; +using WingsEmu.Game; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Monster.Event; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.BCards.Handlers; + +public class BCardCountHandler : IBCardEffectAsyncHandler +{ + private readonly IAsyncEventPipeline _eventPipeline; + + public BCardCountHandler(IAsyncEventPipeline eventPipeline) => _eventPipeline = eventPipeline; + + public BCardType HandledType => BCardType.Count; + + public void Execute(IBCardEffectContext ctx) + { + IBattleEntity sender = ctx.Sender; + BCardDTO bCard = ctx.BCard; + + if (sender is not IMonsterEntity monsterEntity) + { + return; + } + + int firstData = bCard.FirstDataValue(monsterEntity.Level); + int secondData = bCard.SecondDataValue(monsterEntity.Level); + + switch ((AdditionalTypes.Count)bCard.SubType) + { + case AdditionalTypes.Count.Summon: + + if (monsterEntity.Mp > 0) + { + return; + } + + var summons = new List(); + + for (int i = 0; i < firstData; i++) + { + summons.Add(new ToSummon + { + VNum = (short)secondData, + SpawnCell = monsterEntity.Position, + IsMoving = true, + IsHostile = true + }); + } + + _eventPipeline.ProcessEventAsync(new + MonsterSummonEvent(sender.MapInstance, summons, showEffect: true, summoner: monsterEntity)).ConfigureAwait(false).GetAwaiter().GetResult(); + + break; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardDarkCloneSummonHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardDarkCloneSummonHandler.cs new file mode 100644 index 0000000..f366dbb --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardDarkCloneSummonHandler.cs @@ -0,0 +1,77 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using PhoenixLib.Events; +using WingsEmu.Game; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Monster.Event; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.BCards.Handlers; + +public class BCardDarkCloneSummonHandler : IBCardEffectAsyncHandler +{ + private readonly IAsyncEventPipeline _eventPipeline; + private readonly INpcMonsterManager _npcMonsterManager; + private readonly IRandomGenerator _random; + + public BCardDarkCloneSummonHandler(IRandomGenerator random, IAsyncEventPipeline eventPipeline, INpcMonsterManager npcMonsterManager) + { + _random = random; + _eventPipeline = eventPipeline; + _npcMonsterManager = npcMonsterManager; + } + + public BCardType HandledType => BCardType.DarkCloneSummon; + + public async void Execute(IBCardEffectContext ctx) + { + IBattleEntity sender = ctx.Sender; + IBattleEntity target = ctx.Target; + int firstData = ctx.BCard.FirstData; + int secondData = ctx.BCard.SecondData; + + switch (ctx.BCard.SubType) + { + case (byte)AdditionalTypes.DarkCloneSummon.SummonDarkCloneChance: + if (_random.RandomNumber() > firstData) + { + return; + } + + var monsters = new List(); + for (int i = 0; i < secondData; i++) + { + int vnum = 2112 + i; + + IMonsterData monsterData = _npcMonsterManager.GetNpc(vnum); + if (monsterData == null) + { + continue; + } + + short x = sender.Position.X; + short y = sender.Position.Y; + + x += (short)_random.RandomNumber(-1, 2); + y += (short)_random.RandomNumber(-1, 2); + monsters.Add(new ToSummon + { + VNum = monsterData.MonsterVNum, + SpawnCell = new Position(x, y), + IsMoving = monsterData.CanWalk, + IsHostile = monsterData.RawHostility != (int)HostilityType.NOT_HOSTILE + }); + } + + await _eventPipeline.ProcessEventAsync(new MonsterSummonEvent(sender.MapInstance, monsters, sender)); + break; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardDestroyerHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardDestroyerHandler.cs new file mode 100644 index 0000000..9ff6938 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardDestroyerHandler.cs @@ -0,0 +1,85 @@ +using System.Collections.Generic; +using PhoenixLib.Events; +using WingsEmu.Game; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Monster.Event; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.BCards.Handlers; + +public class BCardDestroyerHandler : IBCardEffectAsyncHandler +{ + private readonly IAsyncEventPipeline _eventPipeline; + private readonly IRandomGenerator _randomGenerator; + + public BCardDestroyerHandler(IRandomGenerator randomGenerator, IAsyncEventPipeline eventPipeline) + { + _randomGenerator = randomGenerator; + _eventPipeline = eventPipeline; + } + + public BCardType HandledType => BCardType.SecondSPCard; + + public async void Execute(IBCardEffectContext ctx) + { + IBattleEntity sender = ctx.Sender; + + if (!(ctx.Sender is IPlayerEntity character)) + { + return; + } + + byte subType = ctx.BCard.SubType; + + int firstData = ctx.BCard.FirstData; + int secondData = ctx.BCard.SecondData; + + var summons = new List(); + + Position entityPosition = sender.Position; + + switch ((AdditionalTypes.SecondSPCard)subType) + { + case AdditionalTypes.SecondSPCard.PlantBomb: + summons.Add(new ToSummon + { + VNum = (short)secondData, + SpawnCell = character.Position, + IsMoving = false, + IsHostile = false + }); + _eventPipeline.ProcessEventAsync(new MonsterSummonEvent(character.MapInstance, summons, character)).ConfigureAwait(false).GetAwaiter().GetResult(); + break; + case AdditionalTypes.SecondSPCard.PlantSelfDestructionBomb: + for (int i = 0; i < firstData; i++) + { + short x = entityPosition.X; + short y = entityPosition.Y; + + x += (short)_randomGenerator.RandomNumber(-3, 3); + y += (short)_randomGenerator.RandomNumber(-3, 3); + if (sender.MapInstance.IsBlockedZone(x, y)) + { + x = entityPosition.X; + y = entityPosition.Y; + } + + var position = new Position(x, y); + summons.Add(new ToSummon + { + VNum = (short)secondData, + SpawnCell = position, + IsHostile = true, + IsMoving = true + }); + } + + await _eventPipeline.ProcessEventAsync(new MonsterSummonEvent(character.MapInstance, summons, character)); + break; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardDrainAndStealHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardDrainAndStealHandler.cs new file mode 100644 index 0000000..9380b1e --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardDrainAndStealHandler.cs @@ -0,0 +1,185 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsEmu.DTOs.BCards; +using WingsEmu.Game; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.BCards.Handlers; + +public class BCardDrainAndStealHandler : IBCardEffectAsyncHandler +{ + private readonly IRandomGenerator _randomGenerator; + + public BCardDrainAndStealHandler(IRandomGenerator randomGenerator) => _randomGenerator = randomGenerator; + + public BCardType HandledType => BCardType.DrainAndSteal; + + public void Execute(IBCardEffectContext ctx) + { + IBattleEntity target = ctx.Target; + IBattleEntity sender = ctx.Sender; + BCardDTO bCard = ctx.BCard; + byte subType = ctx.BCard.SubType; + + int firstDataValue = bCard.FirstDataValue(sender.Level); + int secondDataValue = bCard.SecondDataValue(sender.Level); + + if (!target.IsAlive() || !sender.IsAlive()) + { + return; + } + + if (target.IsMateTrainer()) + { + return; + } + + switch ((AdditionalTypes.DrainAndSteal)subType) + { + case AdditionalTypes.DrainAndSteal.GiveEnemyHP: + break; + case AdditionalTypes.DrainAndSteal.LeechEnemyHP: + if (_randomGenerator.RandomNumber() > firstDataValue) + { + return; + } + + if (target.Hp - secondDataValue > 0) + { + target.BroadcastDamage(secondDataValue); + target.Hp -= secondDataValue; + } + + if (sender.Hp + secondDataValue < sender.MaxHp) + { + if (target.Hp - secondDataValue <= 0) + { + sender.EmitEvent(new BattleEntityHealEvent + { + Entity = sender, + HpHeal = target.Hp + }); + + target.Hp = 1; + } + else + { + sender.EmitEvent(new BattleEntityHealEvent + { + Entity = sender, + HpHeal = secondDataValue + }); + } + } + else + { + if (target.Hp - secondDataValue <= 0) + { + target.Hp = 1; + } + + sender.EmitEvent(new BattleEntityHealEvent + { + Entity = sender, + HpHeal = sender.MaxHp + }); + } + + (target as IPlayerEntity)?.Session.RefreshStat(); + (sender as IPlayerEntity)?.Session.RefreshStat(); + break; + case AdditionalTypes.DrainAndSteal.GiveEnemyMP: + break; + case AdditionalTypes.DrainAndSteal.LeechEnemyMP: + if (_randomGenerator.RandomNumber() > firstDataValue) + { + return; + } + + if (target.Mp - secondDataValue > 0) + { + target.Mp -= secondDataValue; + } + + if (sender.Mp + secondDataValue < sender.MaxMp) + { + if (target.Mp - secondDataValue <= 0) + { + target.Mp = 1; + } + + sender.Mp += secondDataValue; + } + else + { + if (target.Mp - secondDataValue <= 0) + { + target.Mp = 1; + } + + sender.Mp = sender.MaxMp; + } + + (target as IPlayerEntity)?.Session.RefreshStat(); + (sender as IPlayerEntity)?.Session.RefreshStat(); + break; + case AdditionalTypes.DrainAndSteal.ConvertEnemyMPToHP: + break; + case AdditionalTypes.DrainAndSteal.ConvertEnemyHPToMP: + + int toRemoveAndAdd = firstDataValue; + + if (sender.Mp + toRemoveAndAdd > sender.MaxMp) + { + if (target.Hp - toRemoveAndAdd <= 0) + { + target.BroadcastDamage(target.Hp - 1); + target.Hp = 1; + } + else + { + target.BroadcastDamage(toRemoveAndAdd); + target.Hp -= toRemoveAndAdd; + sender.EmitEvent(new BattleEntityHealEvent + { + Entity = sender, + MpHeal = sender.MaxMp + }); + } + } + else + { + if (target.Hp - toRemoveAndAdd <= 0) + { + toRemoveAndAdd = target.Hp - 1; + target.BroadcastDamage(toRemoveAndAdd); + target.Hp = 1; + } + else + { + target.BroadcastDamage(toRemoveAndAdd); + target.Hp -= toRemoveAndAdd; + } + + sender.EmitEvent(new BattleEntityHealEvent + { + Entity = sender, + MpHeal = toRemoveAndAdd + }); + } + + (target as IPlayerEntity)?.Session.RefreshStat(); + (sender as IPlayerEntity)?.Session.RefreshStat(); + + break; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardDrainHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardDrainHandler.cs new file mode 100644 index 0000000..0341ac1 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardDrainHandler.cs @@ -0,0 +1,59 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsEmu.DTOs.BCards; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.BCards.Handlers; + +public class BCardDrainHandler : IBCardEffectAsyncHandler +{ + public BCardType HandledType => BCardType.Drain; + + public void Execute(IBCardEffectContext ctx) + { + IBattleEntity target = ctx.Target; + IBattleEntity sender = ctx.Sender; + byte subType = ctx.BCard.SubType; + BCardDTO bCard = ctx.BCard; + + int firstDataValue = bCard.FirstDataValue(sender.Level); + + switch ((AdditionalTypes.Drain)subType) + { + case AdditionalTypes.Drain.TransferEnemyHPNegated: + case AdditionalTypes.Drain.TransferEnemyHP: + if (target.Hp - firstDataValue <= 0) + { + if (target.Hp != 1) + { + target.BroadcastDamage(target.Hp); + } + + target.Hp = 1; + } + else + { + target.BroadcastDamage(firstDataValue); + target.Hp -= firstDataValue; + } + + if (sender.MapInstance?.Id != target.MapInstance?.Id) + { + return; + } + + sender.EmitEvent(new BattleEntityHealEvent + { + Entity = sender, + HpHeal = firstDataValue + }); + break; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardFearSkillHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardFearSkillHandler.cs new file mode 100644 index 0000000..e7dfada --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardFearSkillHandler.cs @@ -0,0 +1,73 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Skills; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.BCards.Handlers; + +public class BCardFearSkillHandler : IBCardEffectAsyncHandler +{ + private readonly IBuffFactory _buffFactory; + + public BCardFearSkillHandler(IBuffFactory buffFactory) => _buffFactory = buffFactory; + + public BCardType HandledType => BCardType.FearSkill; + + public void Execute(IBCardEffectContext ctx) + { + if (!(ctx.Target is IPlayerEntity character)) + { + return; + } + + switch (ctx.BCard.SubType) + { + case (byte)AdditionalTypes.FearSkill.MoveAgainstWill: + character.Session.SendOppositeMove(true); + break; + case (byte)AdditionalTypes.FearSkill.TimesUsed: + int buffVnum = ctx.BCard.SecondData; + switch (ctx.BCard.FirstData) + { + case 1: + if (character.ScoutStateType != ScoutStateType.None) + { + return; + } + + Buff firstBuff = _buffFactory.CreateBuff(buffVnum, character); + character.AddBuffAsync(firstBuff); + break; + case 2: + if (character.BuffComponent.HasBuff(buffVnum)) + { + return; + } + + if (character.ScoutStateType != ScoutStateType.FirstState) + { + return; + } + + if (!character.BuffComponent.HasBuff(buffVnum - 1)) + { + return; + } + + Buff secondBuff = _buffFactory.CreateBuff(buffVnum, character); + character.AddBuffAsync(secondBuff); + break; + default: + return; + } + + break; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardHatusHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardHatusHandler.cs new file mode 100644 index 0000000..9bbaecd --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardHatusHandler.cs @@ -0,0 +1,87 @@ +using System; +using WingsEmu.Core; +using WingsEmu.DTOs.BCards; +using WingsEmu.Game; +using WingsEmu.Game.Act4; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Entities; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.BCards.Handlers; + +public class BCardHatusHandler : IBCardEffectAsyncHandler +{ + private readonly Range _blue = new() + { + Minimum = 18, + Maximum = 28 + }; + + private readonly IDungeonManager _dungeonManager; + + private readonly Range _green = new() + { + Minimum = 44, + Maximum = 54 + }; + + private readonly IRandomGenerator _randomGenerator; + + private readonly Range _red = new() + { + Minimum = 31, + Maximum = 41 + }; + + public BCardHatusHandler(IRandomGenerator randomGenerator, IDungeonManager dungeonManager) + { + _randomGenerator = randomGenerator; + _dungeonManager = dungeonManager; + } + + public BCardType HandledType => BCardType.LordHatus; + + public void Execute(IBCardEffectContext ctx) + { + IBattleEntity sender = ctx.Sender; + BCardDTO bCard = ctx.BCard; + + int firstDataValue = bCard.FirstDataValue(sender.Level); + + switch ((AdditionalTypes.LordHatus)bCard.SubType) + { + case AdditionalTypes.LordHatus.InflictDamageAtLocation: + + int randomNumber = _randomGenerator.RandomNumber(1, 8); + HatusState newHatusState = new() + { + CastTime = TimeSpan.FromMilliseconds(2 * ctx.Skill.CastTime * 100), + DealtDamage = firstDataValue * 0.01 + }; + + // for blue + if ((randomNumber & 1) == 1) + { + newHatusState.BlueAttack = true; + newHatusState.BlueX = (short)_randomGenerator.RandomNumber(_blue.Minimum, _blue.Maximum + 1); + } + + // for red + if ((randomNumber & 2) == 2) + { + newHatusState.RedAttack = true; + newHatusState.RedX = (short)_randomGenerator.RandomNumber(_red.Minimum, _red.Maximum + 1); + } + + // for green + if ((randomNumber & 4) == 4) + { + newHatusState.GreenAttack = true; + newHatusState.GreenX = (short)_randomGenerator.RandomNumber(_green.Minimum, _green.Maximum + 1); + } + + _dungeonManager.AddNewHatusState(sender.MapInstance.Id, newHatusState); + break; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardHealingBurningAndCastingHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardHealingBurningAndCastingHandler.cs new file mode 100644 index 0000000..30830d9 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardHealingBurningAndCastingHandler.cs @@ -0,0 +1,109 @@ +// WingsEmu +// +// Developed by NosWings Team + +using PhoenixLib.Events; +using WingsEmu.DTOs.BCards; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.BCards.Handlers; + +public class BCardHealingBurningAndCastingHandler : IBCardEffectAsyncHandler +{ + private readonly IAsyncEventPipeline _asyncEventPipeline; + + public BCardHealingBurningAndCastingHandler(IAsyncEventPipeline asyncEventPipeline) => _asyncEventPipeline = asyncEventPipeline; + + public BCardType HandledType => BCardType.HealingBurningAndCasting; + + public void Execute(IBCardEffectContext ctx) + { + IBattleEntity target = ctx.Target; + IBattleEntity sender = ctx.Sender; + byte subType = ctx.BCard.SubType; + + if (sender?.MapInstance == null) + { + return; + } + + if (target?.MapInstance == null) + { + return; + } + + BCardDTO bCard = ctx.BCard; + int firstDataValue = bCard.FirstDataValue(sender.Level); + + if (!target.IsAlive()) + { + return; + } + + if (target.IsMateTrainer()) + { + return; + } + + switch ((AdditionalTypes.HealingBurningAndCasting)subType) + { + case AdditionalTypes.HealingBurningAndCasting.RestoreHP: + case AdditionalTypes.HealingBurningAndCasting.RestoreHPWhenCasting: + target.EmitEvent(new BattleEntityHealEvent + { + Entity = target, + HpHeal = firstDataValue + }); + + break; + case AdditionalTypes.HealingBurningAndCasting.RestoreMP: + if (target.Mp + firstDataValue < target.MaxMp) + { + target.Mp += firstDataValue; + } + else + { + target.Mp = target.MaxMp; + } + + break; + case AdditionalTypes.HealingBurningAndCasting.DecreaseHP: + int damage = firstDataValue; + + if (target.Hp - damage <= 0) + { + if (target.Hp != 1) + { + target.BroadcastDamage(target.Hp - 1); + } + + target.Hp = 1; + } + else + { + target.BroadcastDamage(damage); + target.Hp -= damage; + } + + break; + case AdditionalTypes.HealingBurningAndCasting.DecreaseMP: + + int mpDecrease = firstDataValue; + target.Mp = target.Mp - mpDecrease <= 0 ? 1 : target.Mp - mpDecrease; + break; + } + + if (target is not IPlayerEntity targetPlayer) + { + return; + } + + targetPlayer.Session?.RefreshStat(); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardHideHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardHideHandler.cs new file mode 100644 index 0000000..16f6aa5 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardHideHandler.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using PhoenixLib.Events; +using WingsEmu.Game; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Monster.Event; +using WingsEmu.Game.Skills; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.BCards.Handlers; + +public class BCardHideHandler : IBCardEffectAsyncHandler +{ + private readonly IAsyncEventPipeline _eventPipeline; + private readonly INpcMonsterManager _npcMonsterManager; + private readonly ISpyOutManager _spyOutManager; + + public BCardHideHandler(IAsyncEventPipeline eventPipeline, ISpyOutManager spyOutManager, INpcMonsterManager npcMonsterManager) + { + _eventPipeline = eventPipeline; + _spyOutManager = spyOutManager; + _npcMonsterManager = npcMonsterManager; + } + + public BCardType HandledType => BCardType.FalconSkill; + + public void Execute(IBCardEffectContext ctx) + { + IBattleEntity sender = ctx.Sender; + IBattleEntity target = ctx.Target; + byte subType = ctx.BCard.SubType; + + switch (subType) + { + case (byte)AdditionalTypes.FalconSkill.Hide: + if (target is not IPlayerEntity character) + { + return; + } + + character.CharacterInvisible(true); + break; + case (byte)AdditionalTypes.FalconSkill.Ambush: + if (target is not IPlayerEntity characterAmbush) + { + return; + } + + characterAmbush.CharacterInvisible(); + + break; + case (byte)AdditionalTypes.FalconSkill.CausingChanceLocation: + var summons = new List(); + short x = ctx.Position.X; + short y = ctx.Position.Y; + + if (x == 0 || y == 0) + { + x = sender.PositionX; + y = sender.PositionY; + } + + IMonsterData monsterData = _npcMonsterManager.GetNpc(ctx.BCard.SecondData); + if (monsterData == null) + { + return; + } + + summons.Add(new ToSummon + { + VNum = monsterData.MonsterVNum, + SpawnCell = new Position(x, y), + IsMoving = monsterData.CanWalk, + SummonChance = (byte)ctx.BCard.FirstData, + IsHostile = monsterData.RawHostility != (int)HostilityType.NOT_HOSTILE, + RemoveTick = true + }); + + _eventPipeline.ProcessEventAsync(new MonsterSummonEvent(sender.MapInstance, summons, sender)).ConfigureAwait(false).GetAwaiter().GetResult(); + break; + case (byte)AdditionalTypes.FalconSkill.FalconFollowing: + if (sender is not IPlayerEntity spyOutCharacter) + { + return; + } + + _spyOutManager.AddSpyOutSkill(spyOutCharacter.Id, spyOutCharacter.LastEntity.Item2, spyOutCharacter.LastEntity.Item1); + spyOutCharacter.SpyOutStart = DateTime.UtcNow; + break; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardJumpBackPushHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardJumpBackPushHandler.cs new file mode 100644 index 0000000..e42b25f --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardJumpBackPushHandler.cs @@ -0,0 +1,195 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Maps; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.BCards.Handlers; + +public class BCardJumpBackPushHandler : IBCardEffectAsyncHandler +{ + private readonly IRandomGenerator _random; + + public BCardJumpBackPushHandler(IRandomGenerator random) => _random = random; + + public BCardType HandledType => BCardType.JumpBackPush; + + public void Execute(IBCardEffectContext ctx) + { + IBattleEntity sender = ctx.Sender; + IBattleEntity target = ctx.Target; + int firstData = ctx.BCard.FirstData; + + if (_random.RandomNumber() > firstData) + { + return; + } + + int secondData = ctx.BCard.SecondData; + switch (ctx.BCard.SubType) + { + case (byte)AdditionalTypes.JumpBackPush.JumpBackChance: + short sX = target.PositionX; + short sY = target.PositionY; + short tX = sender.PositionX; + short tY = sender.PositionY; + + int distance = sender.GetDistance(target); + if (distance <= 0) + { + distance = 1; + } + + int d = distance + secondData; + + short maxX = (short)(sX - d * (sX - tX) / distance); + short maxY = (short)(sY - d * (sY - tY) / distance); + + int dx = Math.Abs(maxX - tX); + int sx = tX < maxX ? 1 : -1; + int dy = -Math.Abs(maxY - tY); + int sy = tY < maxY ? 1 : -1; + int err = dx + dy; + + short lastX = tX; + short lastY = tY; + + while (true) + { + if (sender.MapInstance.IsBlockedZone(tX, tY)) + { + tX = lastX; + tY = lastY; + break; + } + + lastX = tX; + lastY = tY; + + if (tX == maxX && tY == maxY) + { + break; + } + + int e2 = 2 * err; + if (e2 >= dy) + { + err += dy; + tX += (short)sx; + } + + if (e2 > dx) + { + continue; + } + + err += dx; + tY += (short)sy; + } + + if (sender is IPlayerEntity playerEntity && playerEntity.MapInstance != null && playerEntity.MapInstance.HasMapFlag(MapFlags.IS_BASE_MAP)) + { + playerEntity.MapX = tX; + playerEntity.MapY = tY; + } + + sender.MapInstance?.Broadcast(sender.GeneratePushPacket(tX, tY, 2)); + sender.ChangePosition(new Position(tX, tY)); + break; + case (byte)AdditionalTypes.JumpBackPush.PushBackChance: + if (!target.IsAlive()) + { + return; + } + + if (target is IMonsterEntity mapMonster && !mapMonster.CanBePushed) + { + return; + } + + (int resistance, _) = target.BCardComponent.GetAllBCardsInformation(BCardType.AbsorbedSpirit, (byte)AdditionalTypes.AbsorbedSpirit.ResistForcedMovement, target.Level); + if (resistance != 0 && _random.RandomNumber() <= resistance) + { + return; + } + + sX = target.PositionX; + sY = target.PositionY; + tX = sender.PositionX; + tY = sender.PositionY; + + distance = sender.GetDistance(target); + if (distance <= 0) + { + distance = 1; + } + + d = distance + secondData; + + maxX = (short)(tX + d * (sX - tX) / distance); + maxY = (short)(tY + d * (sY - tY) / distance); + + dx = Math.Abs(maxX - tX); + sx = tX < maxX ? 1 : -1; + dy = -Math.Abs(maxY - tY); + sy = tY < maxY ? 1 : -1; + err = dx + dy; + + lastX = tX; + lastY = tY; + + while (true) + { + if (sender.MapInstance.IsBlockedZone(tX, tY)) + { + tX = lastX; + tY = lastY; + break; + } + + lastX = tX; + lastY = tY; + + if (tX == maxX && tY == maxY) + { + break; + } + + int e2 = 2 * err; + if (e2 >= dy) + { + err += dy; + tX += (short)sx; + } + + if (e2 > dx) + { + continue; + } + + err += dx; + tY += (short)sy; + } + + if (target is IPlayerEntity player && player.MapInstance != null && player.MapInstance.HasMapFlag(MapFlags.IS_BASE_MAP)) + { + player.MapX = tX; + player.MapY = tY; + } + + sender.MapInstance?.Broadcast(target.GeneratePushPacket(tX, tY, 2)); + target.ChangePosition(new Position(tX, tY)); + break; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardKnockdownHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardKnockdownHandler.cs new file mode 100644 index 0000000..8c54b3e --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardKnockdownHandler.cs @@ -0,0 +1,177 @@ +using WingsEmu.Game; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.BCards.Handlers; + +public class BCardKnockdownHandler : IBCardEffectAsyncHandler +{ + private readonly IBuffFactory _buffFactory; + private readonly ICardsManager _cards; + private readonly IGameLanguageService _gameLanguage; + private readonly IRandomGenerator _randomGenerator; + + public BCardKnockdownHandler(IRandomGenerator randomGenerator, IBuffFactory buffFactory, ICardsManager cards, IGameLanguageService gameLanguage) + { + _randomGenerator = randomGenerator; + _buffFactory = buffFactory; + _cards = cards; + _gameLanguage = gameLanguage; + } + + public BCardType HandledType => BCardType.TauntSkill; + + public void Execute(IBCardEffectContext ctx) + { + IBattleEntity sender = ctx.Sender; + IBattleEntity target = ctx.Target; + byte subType = ctx.BCard.SubType; + int firstData = ctx.BCard.FirstData; + int secondData = ctx.BCard.SecondData; + + if (_randomGenerator.RandomNumber() > firstData) + { + return; + } + + switch (subType) + { + case (byte)AdditionalTypes.TauntSkill.TauntWhenKnockdown: + if (!target.BuffComponent.HasBuff((short)BuffVnums.KNOCKDOWN)) + { + return; + } + + Buff b = _buffFactory.CreateBuff(secondData, target); + + double debuffCounter = target.CheckForResistance(b, _cards, out double buffCounter, out double specializedResistance); + + int debuffRandomNumber = _randomGenerator.RandomNumber(); + int buffRandomNumber = _randomGenerator.RandomNumber(); + int specializedRandomNumber = _randomGenerator.RandomNumber(); + switch (target) + { + case IMonsterEntity monster: + if (!monster.CanBeDebuffed) + { + return; + } + + break; + } + + if (specializedRandomNumber >= (int)(specializedResistance * 100)) + { + if (!(target is IPlayerEntity c)) + { + return; + } + + string message = _gameLanguage.GetLanguage(GameDialogKey.BUFF_CHATMESSAGE_EFFECT_RESISTANCE, c.Session.UserLanguage); + c.Session.SendChatMessage(message, ChatMessageColorType.Buff); + return; + } + + switch (b.BuffGroup) + { + case BuffGroup.Bad when debuffRandomNumber >= (int)(debuffCounter * 100): + { + if (!(target is IPlayerEntity c)) + { + return; + } + + string message = _gameLanguage.GetLanguage(GameDialogKey.BUFF_CHATMESSAGE_EFFECT_RESISTANCE, c.Session.UserLanguage); + c.Session.SendChatMessage(message, ChatMessageColorType.Buff); + return; + } + case BuffGroup.Good when buffRandomNumber >= (int)(buffCounter * 100): + { + if (!(target is IPlayerEntity c)) + { + return; + } + + string message = _gameLanguage.GetLanguage(GameDialogKey.BUFF_CHATMESSAGE_EFFECT_RESISTANCE, c.Session.UserLanguage); + c.Session.SendChatMessage(message, ChatMessageColorType.Buff); + return; + } + } + + target.AddBuffAsync().ConfigureAwait(false).GetAwaiter().GetResult(); + break; + + case (byte)AdditionalTypes.TauntSkill.TauntWhenNormal: + if (target.BuffComponent.HasBuff((short)BuffVnums.KNOCKDOWN)) + { + return; + } + + b = _buffFactory.CreateBuff(secondData, target); + + debuffCounter = target.CheckForResistance(b, _cards, out buffCounter, out specializedResistance); + debuffRandomNumber = _randomGenerator.RandomNumber(); + buffRandomNumber = _randomGenerator.RandomNumber(); + specializedRandomNumber = _randomGenerator.RandomNumber(); + switch (target) + { + case IMonsterEntity monster: + if (!monster.CanBeDebuffed) + { + return; + } + + break; + } + + if (specializedRandomNumber >= (int)(specializedResistance * 100)) + { + if (!(target is IPlayerEntity c)) + { + return; + } + + string message = _gameLanguage.GetLanguage(GameDialogKey.BUFF_CHATMESSAGE_EFFECT_RESISTANCE, c.Session.UserLanguage); + c.Session.SendChatMessage(message, ChatMessageColorType.Buff); + return; + } + + switch (b.BuffGroup) + { + case BuffGroup.Bad when debuffRandomNumber >= (int)(debuffCounter * 100): + { + if (!(target is IPlayerEntity c)) + { + return; + } + + string message = _gameLanguage.GetLanguage(GameDialogKey.BUFF_CHATMESSAGE_EFFECT_RESISTANCE, c.Session.UserLanguage); + c.Session.SendChatMessage(message, ChatMessageColorType.Buff); + return; + } + case BuffGroup.Good when buffRandomNumber >= (int)(buffCounter * 100): + { + if (!(target is IPlayerEntity c)) + { + return; + } + + string message = _gameLanguage.GetLanguage(GameDialogKey.BUFF_CHATMESSAGE_EFFECT_RESISTANCE, c.Session.UserLanguage); + c.Session.SendChatMessage(message, ChatMessageColorType.Buff); + return; + } + } + + target.AddBuffAsync().ConfigureAwait(false).GetAwaiter().GetResult(); + break; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardLightAndShadowHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardLightAndShadowHandler.cs new file mode 100644 index 0000000..38f14bb --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardLightAndShadowHandler.cs @@ -0,0 +1,80 @@ +// WingsEmu +// +// Developed by NosWings Team[403] + +using WingsEmu.DTOs.BCards; +using WingsEmu.Game._enum; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Monster; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.BCards.Handlers; + +public class BCardLightAndShadowHandler : IBCardEffectAsyncHandler +{ + public BCardType HandledType => BCardType.LightAndShadow; + + public void Execute(IBCardEffectContext ctx) + { + IBattleEntity sender = ctx.Target; + IBattleEntity target = ctx.Target; + BCardDTO bCard = ctx.BCard; + + int firstData = bCard.FirstDataValue(target.Level); + + switch ((AdditionalTypes.LightAndShadow)bCard.SubType) + { + case AdditionalTypes.LightAndShadow.RemoveBadEffects: + ctx.Target.RemoveNegativeBuffs(firstData); + break; + case AdditionalTypes.LightAndShadow.RemoveGoodEffects: + ctx.Target.RemovePositiveBuffs(firstData); + break; + case AdditionalTypes.LightAndShadow.InflictDamageOnUndead: + + if (target is not IMonsterEntity monsterEntity) + { + return; + } + + if (!monsterEntity.IsAlive()) + { + return; + } + + if (sender.Level < monsterEntity.Level) + { + return; + } + + if (monsterEntity.MonsterRaceType != MonsterRaceType.Undead) + { + return; + } + + if (!Equals(monsterEntity.GetMonsterRaceSubType(), MonsterSubRace.Undead.LowLevelUndead)) + { + return; + } + + monsterEntity.Hp /= 2; + if (monsterEntity.Hp <= 0) + { + monsterEntity.Hp = 1; + } + + if (sender is not IPlayerEntity playerEntity) + { + return; + } + + playerEntity.Session.SendStPacket(monsterEntity); + + break; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardMateSummonHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardMateSummonHandler.cs new file mode 100644 index 0000000..000ac66 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardMateSummonHandler.cs @@ -0,0 +1,134 @@ +using System.Collections.Generic; +using PhoenixLib.Events; +using WingsEmu.DTOs.BCards; +using WingsEmu.Game; +using WingsEmu.Game._enum; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Monster.Event; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.BCards.Handlers; + +public class BCardMateSummonHandler : IBCardEffectAsyncHandler +{ + private readonly IAsyncEventPipeline _asyncEvent; + private readonly IRandomGenerator _randomGenerator; + + public BCardMateSummonHandler(IRandomGenerator randomGenerator, IAsyncEventPipeline asyncEvent) + { + _randomGenerator = randomGenerator; + _asyncEvent = asyncEvent; + } + + public BCardType HandledType => BCardType.SummonAndRecoverHP; + + public void Execute(IBCardEffectContext ctx) + { + IBattleEntity sender = ctx.Sender; + BCardDTO bCard = ctx.BCard; + + int firstData = bCard.FirstDataValue(sender.Level); + int secondData = bCard.SecondDataValue(sender.Level); + + switch ((AdditionalTypes.SummonAndRecoverHP)bCard.SubType) + { + case AdditionalTypes.SummonAndRecoverHP.ChanceSummon: + + if (_randomGenerator.RandomNumber() > firstData) + { + return; + } + + if (sender is not IMateEntity mateEntity) + { + return; + } + + IPlayerEntity owner = mateEntity.Owner; + + if (owner == null) + { + return; + } + + if (owner.MapInstance.Id != mateEntity.MapInstance.Id) + { + return; + } + + int amount = _randomGenerator.RandomNumber(1, 4); + + var monsters = new List(); + Position position = mateEntity.Position; + + for (int i = 0; i < amount; i++) + { + int x = position.X + _randomGenerator.RandomNumber(-2, 2); + int y = position.Y + _randomGenerator.RandomNumber(-2, 2); + + if (mateEntity.MapInstance.IsBlockedZone(x, y)) + { + x = position.X; + y = position.Y; + } + + var toSummon = new ToSummon + { + VNum = (short)secondData, + SpawnCell = new Position((short)x, (short)y), + IsMoving = true, + IsHostile = true + }; + + monsters.Add(toSummon); + } + + _asyncEvent.ProcessEventAsync(new MonsterSummonEvent(owner.MapInstance, monsters, owner)).ConfigureAwait(false).GetAwaiter().GetResult(); + break; + case AdditionalTypes.SummonAndRecoverHP.RestoreHP: + + if (sender is not IMonsterEntity monsterEntity) + { + return; + } + + if (!monsterEntity.SummonerId.HasValue) + { + return; + } + + IMateEntity mate = monsterEntity.MapInstance.GetCharacterById(monsterEntity.SummonerId.Value)?.MateComponent.GetTeamMember(x => x.MateType == MateType.Pet); + if (mate == null) + { + return; + } + + if (monsterEntity.MapInstance.Id != mate.MapInstance.Id) + { + return; + } + + if (mate.Owner.IsOnVehicle) + { + return; + } + + int toHeal = (int)(mate.MaxHp * (firstData * 0.01)); + mate.EmitEvent(new BattleEntityHealEvent + { + Entity = mate, + HpHeal = toHeal + }); + + monsterEntity.BroadcastEffectTarget(mate, EffectType.MateHealByMonster); + break; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardMeditationSkillHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardMeditationSkillHandler.cs new file mode 100644 index 0000000..2ee77e4 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardMeditationSkillHandler.cs @@ -0,0 +1,167 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Generic; +using System.Linq; +using WingsAPI.Game.Extensions.Quicklist; +using WingsEmu.DTOs.Quicklist; +using WingsEmu.DTOs.Skills; +using WingsEmu.Game; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Skills; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.BCards.Handlers; + +public class BCardMeditationSkillHandler : IBCardEffectAsyncHandler +{ + private readonly IBuffFactory _buffFactory; + private readonly IGameLanguageService _gameLanguage; + private readonly IMeditationManager _meditationManager; + private readonly IRandomGenerator _randomGenerator; + private readonly ISacrificeManager _sacrificeManager; + + public BCardMeditationSkillHandler(IRandomGenerator randomGenerator, IBuffFactory buffFactory, IGameLanguageService gameLanguage, ISacrificeManager sacrificeManager, + IMeditationManager meditationManager) + { + _randomGenerator = randomGenerator; + _buffFactory = buffFactory; + _gameLanguage = gameLanguage; + _sacrificeManager = sacrificeManager; + _meditationManager = meditationManager; + } + + public BCardType HandledType => BCardType.MeditationSkill; + + public async void Execute(IBCardEffectContext ctx) + { + IBattleEntity sender = ctx.Sender; + byte subType = ctx.BCard.SubType; + int firstData = ctx.BCard.FirstData; + if (sender is not IPlayerEntity character) + { + return; + } + + IClientSession session = character.Session; + + if (subType != (byte)AdditionalTypes.MeditationSkill.CausingChance) + { + switch (subType) + { + case (byte)AdditionalTypes.MeditationSkill.ShortMeditation: + _meditationManager.SaveMeditation(session.PlayerEntity, (short)ctx.BCard.SecondData, DateTime.UtcNow.AddSeconds(4)); + break; + case (byte)AdditionalTypes.MeditationSkill.RegularMeditation: + _meditationManager.SaveMeditation(session.PlayerEntity, (short)ctx.BCard.SecondData, DateTime.UtcNow.AddSeconds(8)); + break; + case (byte)AdditionalTypes.MeditationSkill.LongMeditation: + _meditationManager.SaveMeditation(session.PlayerEntity, (short)ctx.BCard.SecondData, DateTime.UtcNow.AddSeconds(12)); + break; + case (byte)AdditionalTypes.MeditationSkill.Sacrifice: + // check if target has sacrifice + IBattleEntity caster = _sacrificeManager.GetCaster(ctx.Target); + IBattleEntity target = null; + if (caster != null) + { + target = _sacrificeManager.GetTarget(caster); + } + + if (caster != null && target != null) + { + await caster.RemoveSacrifice(target, _sacrificeManager, _gameLanguage); + Buff targetBuff = _buffFactory.CreateBuff((short)BuffVnums.NOBLE_GESTURE, sender); + await ctx.Target.AddBuffAsync(targetBuff); + } + + Buff buff = _buffFactory.CreateBuff(ctx.BCard.SecondData, sender); + await sender.AddBuffAsync(buff); + if (target is IPlayerEntity characterTarget) + { + characterTarget.Session.SendMsg( + _gameLanguage.GetLanguageFormat(GameDialogKey.SKILL_SHOUTMESSAGE_SACRIFICE, characterTarget.Session.UserLanguage, session.PlayerEntity.Name, characterTarget.Name), + MsgMessageType.SmallMiddle); + session.SendMsg(_gameLanguage.GetLanguageFormat(GameDialogKey.SKILL_SHOUTMESSAGE_SACRIFICE, session.UserLanguage, session.PlayerEntity.Name, characterTarget.Name), + MsgMessageType.SmallMiddle); + } + + _sacrificeManager.SaveSacrifice(character, ctx.Target); + break; + } + + return; + } + + if (_randomGenerator.RandomNumber() > firstData) + { + return; + } + + if (!ctx.BCard.SkillVNum.HasValue) + { + return; + } + + if (!character.UseSp) + { + return; + } + + if (character.Specialist == null) + { + return; + } + + if (character.Specialist.SpLevel < 20) + { + return; + } + + SkillInfo skill = ctx.Skill; + + ComboSkillState comboSkillState = session.PlayerEntity.GetComboState(); + if (comboSkillState == null) + { + return; + } + + SkillDTO newSkill = character.GetSkills().FirstOrDefault(x => x.Skill.Id == ctx.BCard.SecondData)?.Skill; + if (newSkill == null) + { + return; + } + + int usedSkillVnum = skill.Vnum; + ComboSkillState combo = character.GetComboState(); + + // Check if current skill is combo and find the original one + if (combo != null) + { + usedSkillVnum = character.GetSkills().FirstOrDefault(x => + x.Skill.SkillType == SkillType.NormalPlayerSkill && x.Skill.CastId == combo.OriginalSkillCastId && x.Skill.Id > 200)?.Skill.Id ?? skill.Vnum; + } + + int characterMorph = character.Specialist != null && character.UseSp ? character.Specialist.GameItem.Morph : 0; + + List quick = character.QuicklistComponent.GetQuicklist(); + IEnumerable skillShot = quick.Where(s => s.SkillVnum.HasValue && s.SkillVnum.Value == usedSkillVnum && s.Morph == characterMorph && s.Type == QuicklistType.SKILLS); + character.Session.SendMSlotPacket((byte)newSkill.CastId); + foreach (CharacterQuicklistEntryDto quickListEntry in skillShot) + { + character.Session.SendQuicklistSlot(quickListEntry, newSkill.CastId); + } + + session.PlayerEntity.IncreaseComboState((byte)newSkill.CastId); + character.LastSkillCombo = DateTime.UtcNow; + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardMeteoriteTeleportHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardMeteoriteTeleportHandler.cs new file mode 100644 index 0000000..350f6d3 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardMeteoriteTeleportHandler.cs @@ -0,0 +1,206 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Generic; +using PhoenixLib.Events; +using WingsEmu.DTOs.BCards; +using WingsEmu.Game; +using WingsEmu.Game._enum; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Families; +using WingsEmu.Game.Groups; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Monster.Event; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.BCards.Handlers; + +public class BCardMeteoriteTeleportHandler : IBCardEffectAsyncHandler +{ + private readonly IAsyncEventPipeline _asyncEventPipeline; + private readonly IRandomGenerator _randomGenerator; + private readonly ITeleportManager _teleportManager; + + public BCardMeteoriteTeleportHandler(ITeleportManager teleportManager, IRandomGenerator randomGenerator, IAsyncEventPipeline asyncEventPipeline) + { + _teleportManager = teleportManager; + _randomGenerator = randomGenerator; + _asyncEventPipeline = asyncEventPipeline; + } + + public BCardType HandledType => BCardType.MeteoriteTeleport; + + public void Execute(IBCardEffectContext ctx) + { + IBattleEntity sender = ctx.Sender; + IBattleEntity target = ctx.Target; + int firstData = ctx.BCard.FirstData; + BCardDTO bCard = ctx.BCard; + + int firstDataValue = bCard.FirstDataValue(sender.Level); + + switch (ctx.BCard.SubType) + { + case (byte)AdditionalTypes.MeteoriteTeleport.CauseMeteoriteFall: + if (!(sender is IPlayerEntity senderCharacter)) + { + return; + } + + int meteoritesAmount = firstDataValue + 10; + var meteorites = new List(); + Position positionNonTarget = ctx.Position; + + for (int i = 0; i < meteoritesAmount; i++) + { + int x = positionNonTarget.X + _randomGenerator.RandomNumber(-4, 4); + int y = positionNonTarget.Y + _randomGenerator.RandomNumber(-4, 4); + + if (senderCharacter.MapInstance.IsBlockedZone(x, y)) + { + x = positionNonTarget.X; + y = positionNonTarget.Y; + } + + int vnum = _randomGenerator.RandomNumber((short)MonsterVnum.FIRST_METEORITE, (short)MonsterVnum.SECOND_METEORITE + 1); + var toSummon = new ToSummon + { + VNum = (short)vnum, + SpawnCell = new Position((short)x, (short)y), + Target = senderCharacter, + IsMoving = true, + IsHostile = true + }; + meteorites.Add(toSummon); + } + + _asyncEventPipeline.ProcessEventAsync(new MonsterSummonEvent(senderCharacter.MapInstance, meteorites, senderCharacter)).ConfigureAwait(false).GetAwaiter().GetResult(); + + break; + case (byte)AdditionalTypes.MeteoriteTeleport.TeleportYouAndGroupToSavedLocation: + if (!(sender is IPlayerEntity character)) + { + return; + } + + IFamily family = character.Family; + + if (!character.HasBuff(BuffVnums.MEMORIAL)) + { + _teleportManager.RemovePosition(character.Id); + return; + } + + Position position = _teleportManager.GetPosition(character.Id); + if (position.X == 0 && position.Y == 0) + { + _teleportManager.SavePosition(character.Id, character.Position); + character.BroadcastEffectGround(EffectType.ArchmageTeleportSet, character.PositionX, character.PositionY, false); + character.SetSkillCooldown(ctx.Skill); + character.RemoveCastingSkill(); + return; + } + + short savedX = _teleportManager.GetPosition(character.Id).X; + short savedY = _teleportManager.GetPosition(character.Id).Y; + + IEnumerable allies = sender.GetAlliesInRange(target, ctx.Skill.AoERange); + int counter = 0; + foreach (IBattleEntity entity in allies) + { + if (counter == firstData) + { + break; + } + + if (entity.IsNpc() && !entity.IsMate()) + { + continue; + } + + if (entity.IsMate()) + { + var mateEntity = (IMateEntity)entity; + IPlayerEntity mateOwner = mateEntity.Owner; + if (character.Id == mateOwner?.Id) + { + continue; + } + + if (mateOwner == null) + { + continue; + } + + PlayerGroup mateOwnerGroup = mateOwner.GetGroup(); + PlayerGroup sessionGroup = character.GetGroup(); + + bool isInGroupMate = mateOwnerGroup?.GroupId == sessionGroup?.GroupId; + if (!isInGroupMate) + { + if (mateOwner.Family?.Id != family?.Id) + { + continue; + } + } + + mateEntity.BroadcastEffectGround(EffectType.ArchmageTeleport, mateEntity.PositionX, mateEntity.PositionY, false); + mateEntity.MapInstance.Broadcast(mateEntity.GenerateEffectPacket(EffectType.ArchmageTeleportAfter)); + mateEntity.TeleportOnMap(savedX, savedY); + counter++; + continue; + } + + var anotherCharacter = (IPlayerEntity)entity; + if (character.Id == anotherCharacter.Id) + { + continue; + } + + if (!anotherCharacter.IsInGroup() && !anotherCharacter.IsInFamily()) + { + continue; + } + + PlayerGroup anotherCharacterGroup = anotherCharacter.GetGroup(); + PlayerGroup characterGroup = character.GetGroup(); + + bool isInGroup = anotherCharacterGroup?.GroupId == characterGroup?.GroupId; + if (!isInGroup) + { + if (anotherCharacter.Family?.Id != family?.Id) + { + continue; + } + } + + anotherCharacter.BroadcastEffectGround(EffectType.ArchmageTeleport, anotherCharacter.PositionX, anotherCharacter.PositionY, false); + anotherCharacter.MapInstance.Broadcast(anotherCharacter.GenerateEffectPacket(EffectType.ArchmageTeleportAfter)); + anotherCharacter.TeleportOnMap(savedX, savedY); + counter++; + } + + character.BroadcastEffectGround(EffectType.ArchmageTeleportWhiteEffect, character.PositionX, character.PositionY, false); + character.TeleportOnMap(savedX, savedY, true); + _teleportManager.RemovePosition(character.Id); + character.BroadcastEffectGround(EffectType.ArchmageTeleportSet, savedX, savedY, true); + + SkillInfo fakeTeleport = character.GetFakeTeleportSkill(); + + Buff memorialBuff = character.BuffComponent.GetBuff((short)BuffVnums.MEMORIAL); + character.RemoveBuffAsync(false, memorialBuff); + character.SetSkillCooldown(fakeTeleport); + character.RemoveCastingSkill(); + character.SkillComponent.SendTeleportPacket = DateTime.UtcNow; + break; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardModeHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardModeHandler.cs new file mode 100644 index 0000000..d89eeb4 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardModeHandler.cs @@ -0,0 +1,48 @@ +using WingsEmu.DTOs.BCards; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Entities; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.BCards.Handlers; + +public class BCardModeHandler : IBCardEffectAsyncHandler +{ + public BCardType HandledType => BCardType.Mode; + + public void Execute(IBCardEffectContext ctx) + { + IBattleEntity sender = ctx.Sender; + + if (!(sender is IMonsterEntity monsterEntity)) + { + return; + } + + BCardDTO bCardDto = ctx.BCard; + int firstData = bCardDto.FirstDataValue(sender.Level); + + switch ((AdditionalTypes.Mode)bCardDto.SubType) + { + case AdditionalTypes.Mode.Range: + + monsterEntity.BasicSkill.Range = (byte)firstData; + + break; + case AdditionalTypes.Mode.ReturnRange: + + monsterEntity.BasicSkill.Range = monsterEntity.BasicRange; + + break; + case AdditionalTypes.Mode.AttackTimeIncreased: + + monsterEntity.BasicSkill.Cooldown = (short)(monsterEntity.BasicSkill.Cooldown + firstData); + + break; + case AdditionalTypes.Mode.AttackTimeDecreased: + + monsterEntity.BasicSkill.Cooldown = (short)(monsterEntity.BasicSkill.Cooldown - firstData); + + break; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardMorcosHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardMorcosHandler.cs new file mode 100644 index 0000000..b9a3f78 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardMorcosHandler.cs @@ -0,0 +1,115 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using PhoenixLib.Events; +using WingsEmu.DTOs.BCards; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Entities.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Extensions.Mates; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Mates.Events; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.BCards.Handlers; + +public class BCardMorcosHandler : IBCardEffectAsyncHandler +{ + private readonly IBuffFactory _buffFactory; + private readonly IAsyncEventPipeline _eventPipeline; + private readonly GameRevivalConfiguration _gameRevivalConfiguration; + + public BCardMorcosHandler(IAsyncEventPipeline eventPipeline, GameRevivalConfiguration gameRevivalConfiguration, IBuffFactory buffFactory) + { + _eventPipeline = eventPipeline; + _gameRevivalConfiguration = gameRevivalConfiguration; + _buffFactory = buffFactory; + } + + public BCardType HandledType => BCardType.LordMorcos; + + public void Execute(IBCardEffectContext ctx) + { + IBattleEntity sender = ctx.Sender; + BCardDTO bCard = ctx.BCard; + + int firstDataValue = bCard.FirstDataValue(sender.Level); + int secondDataValue = bCard.SecondDataValue(sender.Level); + + switch ((AdditionalTypes.LordMorcos)ctx.BCard.SubType) + { + case AdditionalTypes.LordMorcos.InflictDamageAfter: + + IEnumerable toDamage = sender.MapInstance.GetNonMonsterBattleEntities(x => !sender.Position.IsInAoeZone(x.Position, firstDataValue)).Take(50); + + foreach (IBattleEntity entity in toDamage) + { + if (!entity.IsAlive()) + { + continue; + } + + int damage = (int)(entity.MaxHp * (secondDataValue * 0.01)); + + if (sender.ShouldSaveDefender(entity, damage, _gameRevivalConfiguration, _buffFactory).ConfigureAwait(false).GetAwaiter().GetResult()) + { + continue; + } + + if (entity.Hp - damage <= 0) + { + entity.Hp = 0; + entity.EmitEvent(new GenerateEntityDeathEvent + { + Entity = entity, + Attacker = sender + }); + + sender.BroadcastCleanSuPacket(entity, damage); + continue; + } + + entity.Hp -= damage; + + + switch (entity) + { + case IPlayerEntity character: + character.LastDefence = DateTime.UtcNow; + character.Session.RefreshStat(); + + if (character.IsSitting) + { + character.Session.RestAsync(force: true); + } + + break; + case IMateEntity mate: + mate.LastDefence = DateTime.UtcNow; + mate.Owner.Session.SendMateLife(mate); + + if (mate.IsSitting) + { + mate.Owner.Session.EmitEvent(new MateRestEvent + { + MateEntity = mate, + Force = true + }); + } + + break; + } + + sender.BroadcastCleanSuPacket(entity, damage); + } + + break; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardMoveHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardMoveHandler.cs new file mode 100644 index 0000000..78eee15 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardMoveHandler.cs @@ -0,0 +1,35 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Plugins.BasicImplementations.BCards.Handlers; + +public class BCardMoveHandler : IBCardEffectAsyncHandler +{ + public BCardType HandledType => BCardType.Move; + + public void Execute(IBCardEffectContext ctx) + { + if (ctx.Sender is IMonsterEntity monsterEntity) + { + monsterEntity.RefreshStats(); + } + + IClientSession session = (ctx.Sender as IPlayerEntity)?.Session; + if (session?.PlayerEntity == null) + { + return; + } + + session.PlayerEntity.LastSpeedChange = DateTime.UtcNow; + session.RefreshStat(); + session.SendCondPacket(); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardQuestHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardQuestHandler.cs new file mode 100644 index 0000000..c28b475 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardQuestHandler.cs @@ -0,0 +1,49 @@ +using WingsEmu.Game; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Entities.Event; +using WingsEmu.Game.Managers.StaticData; + +namespace WingsEmu.Plugins.BasicImplementations.BCards.Handlers; + +public class BCardQuestHandler : IBCardEffectAsyncHandler +{ + private readonly IMonsterEntityFactory _monsterEntityFactory; + private readonly INpcMonsterManager _monsterManager; + private readonly IRandomGenerator _randomGenerator; + + public BCardQuestHandler(INpcMonsterManager monsterManager, IRandomGenerator randomGenerator, IMonsterEntityFactory monsterEntityFactory) + { + _monsterManager = monsterManager; + _randomGenerator = randomGenerator; + _monsterEntityFactory = monsterEntityFactory; + } + + public BCardType HandledType => BCardType.Quest; + + public void Execute(IBCardEffectContext ctx) + { + IMonsterData sender; + if (ctx.Sender is IMonsterEntity monsterEntity) + { + sender = monsterEntity; + } + else + { + return; + } + + for (int i = 0; i < sender.VNumRequired; i++) + { + int posX = ctx.Sender.PositionX + _randomGenerator.RandomNumber(-1, 1); + int posY = ctx.Sender.PositionY + _randomGenerator.RandomNumber(-1, 1); + + IMonsterEntity mapMonster = _monsterEntityFactory.CreateMonster(sender.SpawnMobOrColor, ctx.Sender.MapInstance, new MonsterEntityBuilder + { + IsHostile = true, + IsWalkingAround = true + }); + mapMonster.EmitEventAsync(new MapJoinMonsterEntityEvent(mapMonster, (short)posX, (short)posY, true)); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardRecoveryAndDamagePercentHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardRecoveryAndDamagePercentHandler.cs new file mode 100644 index 0000000..a16f2ac --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardRecoveryAndDamagePercentHandler.cs @@ -0,0 +1,85 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using WingsEmu.DTOs.BCards; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.BCards.Handlers; + +public class BCardRecoveryAndDamagePercentHandler : IBCardEffectAsyncHandler +{ + public BCardType HandledType => BCardType.RecoveryAndDamagePercent; + + public void Execute(IBCardEffectContext ctx) + { + IBattleEntity sender = ctx.Sender; + IBattleEntity target = ctx.Target; + sender ??= target; + + BCardDTO bCard = ctx.BCard; + int firstDataValue = bCard.FirstDataValue(sender.Level); + switch (ctx.BCard.SubType) + { + case (byte)AdditionalTypes.RecoveryAndDamagePercent.HPRecovered: + int heal = (int)(target.MaxHp * (firstDataValue * 0.01)); + target.EmitEvent(new BattleEntityHealEvent + { + Entity = target, + HpHeal = heal + }); + + break; + case (byte)AdditionalTypes.RecoveryAndDamagePercent.HPReduced: + int damage = (int)(target.MaxHp * (firstDataValue * 0.01)); + if (target.Hp - damage <= 1) + { + damage = Math.Abs(target.Hp - 1); + } + + target.Hp -= damage; + if (damage == 0) + { + return; + } + + target.BroadcastDamage(damage); + break; + case (byte)AdditionalTypes.RecoveryAndDamagePercent.MPRecovered: + int mpRegen = (int)(target.MaxMp * (firstDataValue * 0.01)); + + if (target.Mp + mpRegen > target.MaxMp) + { + target.Mp = target.MaxMp; + } + else + { + target.Mp += mpRegen; + } + + break; + case (byte)AdditionalTypes.RecoveryAndDamagePercent.MPReduced: + int mpDamage = (int)(target.MaxMp * (firstDataValue * 0.01)); + if (target.Mp - mpDamage <= 1) + { + mpDamage = Math.Abs(target.Mp - 1); + } + + target.Mp -= mpDamage; + break; + } + + if (target is not IPlayerEntity playerEntity) + { + return; + } + + playerEntity.Session.RefreshStat(); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardReflectionHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardReflectionHandler.cs new file mode 100644 index 0000000..e3890fe --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardReflectionHandler.cs @@ -0,0 +1,37 @@ +using WingsEmu.Game; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Entities; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.BCards.Handlers; + +public class BCardReflectionHandler : IBCardEffectAsyncHandler +{ + private readonly IRandomGenerator _random; + + public BCardReflectionHandler(IRandomGenerator random) => _random = random; + + public BCardType HandledType => BCardType.Reflection; + + public void Execute(IBCardEffectContext ctx) + { + IBattleEntity target = ctx.Target; + byte subType = ctx.BCard.SubType; + int firstData = ctx.BCard.FirstData; + int secondData = ctx.BCard.SecondData; + + int randomNumber = _random.RandomNumber(); + if (randomNumber > firstData) + { + return; + } + + switch (subType) + { + case (byte)AdditionalTypes.Reflection.ChanceMpLost: + int loss = (int)(target.Mp * (secondData * 0.01)); + target.Mp = target.Mp - loss <= 0 ? 0 : target.Mp - loss; + break; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardSESpecialistHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardSESpecialistHandler.cs new file mode 100644 index 0000000..c4953d4 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardSESpecialistHandler.cs @@ -0,0 +1,38 @@ +using System.Linq; +using WingsEmu.DTOs.BCards; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Entities; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.BCards.Handlers; + +public class BCardSESpecialistHandler : IBCardEffectAsyncHandler +{ + private readonly IBuffFactory _buffFactory; + + public BCardSESpecialistHandler(IBuffFactory buffFactory) => _buffFactory = buffFactory; + + public BCardType HandledType => BCardType.SESpecialist; + + public void Execute(IBCardEffectContext ctx) + { + IBattleEntity sender = ctx.Sender; + BCardDTO bCard = ctx.BCard; + + int firstDataValue = bCard.FirstDataValue(sender.Level); + int secondDataValue = bCard.SecondDataValue(sender.Level); + + switch (ctx.BCard.SubType) + { + case (byte)AdditionalTypes.SESpecialist.EnterNumberOfBuffsAndDamage: + bool alreadyHaveBuffDamage = sender.EndBuffDamages.Any(x => x.Key == firstDataValue); + if (alreadyHaveBuffDamage) + { + sender.RemoveEndBuffDamage((short)firstDataValue); + } + + sender.AddEndBuff((short)firstDataValue, secondDataValue * 1000); + break; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardSpecialActionsHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardSpecialActionsHandler.cs new file mode 100644 index 0000000..a349cab --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardSpecialActionsHandler.cs @@ -0,0 +1,381 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using WingsEmu.Game; +using WingsEmu.Game._enum; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Entities.Extensions; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Mates.Events; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.BCards.Handlers; + +public class BCardSpecialActionsHandler : IBCardEffectAsyncHandler +{ + private readonly IRandomGenerator _randomGenerator; + + public BCardSpecialActionsHandler(IRandomGenerator randomGenerator) => _randomGenerator = randomGenerator; + + public BCardType HandledType => BCardType.SpecialActions; + + public void Execute(IBCardEffectContext ctx) + { + IBattleEntity target = ctx.Target; + IBattleEntity sender = ctx.Sender; + byte subType = ctx.BCard.SubType; + int firstData = ctx.BCard.FirstDataValue(sender.Level); + int secondData = ctx.BCard.SecondDataValue(sender.Level); + switch (subType) + { + case (byte)AdditionalTypes.SpecialActions.RunAway: + + if (target is IMonsterEntity monsterEntity) + { + if (sender.Level < monsterEntity.Level) + { + return; + } + + monsterEntity.BroadcastChatBubble("!!!", ChatMessageColorType.PlayerSay); + monsterEntity.IsRunningAway = true; + } + + if (target is not INpcEntity npcEntity) + { + return; + } + + if (sender.Level < npcEntity.Level) + { + return; + } + + npcEntity.BroadcastChatBubble("!!!", ChatMessageColorType.PlayerSay); + npcEntity.IsRunningAway = true; + break; + case (byte)AdditionalTypes.SpecialActions.Hide: + if (target is not IPlayerEntity character) + { + return; + } + + character.CharacterInvisible(); + break; + case (byte)AdditionalTypes.SpecialActions.FocusEnemies: + if (!target.IsAlive()) + { + return; + } + + switch (target) + { + case INpcEntity { CanAttack: false }: + return; + case IMonsterEntity { CanBePushed: false }: + return; + } + + (int resistance, _) = target.BCardComponent.GetAllBCardsInformation(BCardType.AbsorbedSpirit, (byte)AdditionalTypes.AbsorbedSpirit.ResistForcedMovement, target.Level); + if (resistance != 0 && _randomGenerator.RandomNumber() <= resistance) + { + return; + } + + short sX = sender.PositionX; + short sY = sender.PositionY; + short tX = target.PositionX; + short tY = target.PositionY; + + int distance = sender.GetDistance(target); + if (distance <= 0) + { + distance = 1; + } + + int d = firstData; + + short maxX = (short)(sX - d * (sX - tX) / distance); + short maxY = (short)(sY - d * (sY - tY) / distance); + + int dx = Math.Abs(maxX - tX); + int sx = tX < maxX ? 1 : -1; + int dy = -Math.Abs(maxY - tY); + int sy = tY < maxY ? 1 : -1; + int err = dx + dy; + + bool isLineOfSight = true; + + while (true) + { + if (sender.MapInstance.IsBlockedZone(tX, tY)) + { + isLineOfSight = false; + break; + } + + if (tX == maxX && tY == maxY) + { + break; + } + + int e2 = 2 * err; + if (e2 >= dy) + { + err += dy; + tX += (short)sx; + } + + if (e2 > dx) + { + continue; + } + + err += dx; + tY += (short)sy; + } + + if (!isLineOfSight) + { + return; + } + + var newPosition = new Position(tX, tY); + switch (target) + { + case IPlayerEntity playerEntity: + if (playerEntity.IsSitting) + { + playerEntity.Session.RestAsync(force: true); + } + + break; + case IMateEntity mate: + if (mate.IsSitting) + { + mate.Owner.Session.EmitEvent(new MateRestEvent + { + MateEntity = mate, + Rest = false, + Force = true + }); + } + + break; + } + + sender.MapInstance?.Broadcast(target.GeneratePushPacket(maxX, maxY, secondData)); + target.BroadcastEffectInRange(EffectType.PushSmoke); + target.ChangePosition(newPosition); + + break; + case (byte)AdditionalTypes.SpecialActions.PushBack: + { + if (!target.IsAlive()) + { + return; + } + + switch (target) + { + case INpcEntity { CanAttack: false }: + return; + case IMonsterEntity { CanBePushed: false }: + return; + } + + (int pushResistance, _) = target.BCardComponent.GetAllBCardsInformation(BCardType.AbsorbedSpirit, (byte)AdditionalTypes.AbsorbedSpirit.ResistForcedMovement, target.Level); + if (pushResistance != 0 && _randomGenerator.RandomNumber() <= pushResistance) + { + return; + } + + sX = sender.PositionX; + sY = sender.PositionY; + tX = target.PositionX; + tY = target.PositionY; + + + distance = sender.GetDistance(target); + if (distance <= 0) + { + distance = 1; + } + + d = distance + firstData; + + maxX = (short)(sX - d * (sX - tX) / distance); + maxY = (short)(sY - d * (sY - tY) / distance); + + dx = Math.Abs(maxX - tX); + sx = tX < maxX ? 1 : -1; + dy = -Math.Abs(maxY - tY); + sy = tY < maxY ? 1 : -1; + err = dx + dy; + + short lastX = tX; + short lastY = tY; + + while (true) + { + if (sender.MapInstance.IsBlockedZone(tX, tY)) + { + tX = lastX; + tY = lastY; + break; + } + + lastX = tX; + lastY = tY; + + if (tX == maxX && tY == maxY) + { + break; + } + + int e2 = 2 * err; + if (e2 >= dy) + { + err += dy; + tX += (short)sx; + } + + if (e2 > dx) + { + continue; + } + + err += dx; + tY += (short)sy; + } + + newPosition = new Position(tX, tY); + switch (target) + { + case IPlayerEntity playerEntity: + if (playerEntity.IsSitting) + { + playerEntity.Session.RestAsync(force: true); + } + + break; + case IMateEntity mate: + + if (mate.IsSitting) + { + mate.Owner.Session.EmitEvent(new MateRestEvent + { + MateEntity = mate, + Rest = false, + Force = true + }); + } + + break; + } + + sender.MapInstance?.Broadcast(target.GeneratePushPacket(tX, tY, secondData)); + target.ChangePosition(newPosition); + target.BroadcastEffectInRange(EffectType.PushSmoke); + + break; + } + case (byte)AdditionalTypes.SpecialActions.Charge: + if (target == null) + { + return; + } + + if (!target.IsAlive()) + { + return; + } + + newPosition = target.Position; + sender.MapInstance?.Broadcast(sender.GenerateDashGuriPacket(newPosition.X, newPosition.Y, secondData)); + sender.ChangePosition(newPosition); + + break; + case (byte)AdditionalTypes.SpecialActions.ChargeNegated: + + if (!target.IsAlive()) + { + return; + } + + sX = target.PositionX; + sY = target.PositionY; + tX = sender.PositionX; + tY = sender.PositionY; + + distance = sender.GetDistance(target); + if (distance <= 0) + { + distance = 1; + } + + d = firstData; + + maxX = (short)(sX - d * (sX - tX) / distance); + maxY = (short)(sY - d * (sY - tY) / distance); + + dx = Math.Abs(maxX - tX); + sx = tX < maxX ? 1 : -1; + dy = -Math.Abs(maxY - tY); + sy = tY < maxY ? 1 : -1; + err = dx + dy; + + isLineOfSight = true; + + while (true) + { + if (sender.MapInstance.IsBlockedZone(tX, tY)) + { + isLineOfSight = false; + break; + } + + if (tX == maxX && tY == maxY) + { + break; + } + + int e2 = 2 * err; + if (e2 >= dy) + { + err += dy; + tX += (short)sx; + } + + if (e2 > dx) + { + continue; + } + + err += dx; + tY += (short)sy; + } + + if (!isLineOfSight) + { + return; + } + + newPosition = new Position(tX, tY); + sender.MapInstance?.Broadcast(sender.GenerateDashGuriPacket(tX, tY, secondData)); + sender.ChangePosition(newPosition); + + break; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardSpecialBehaviorHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardSpecialBehaviorHandler.cs new file mode 100644 index 0000000..e8d7af4 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardSpecialBehaviorHandler.cs @@ -0,0 +1,111 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using WingsEmu.Game._enum; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Mates; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.BCards.Handlers; + +public class BCardSpecialBehaviorHandler : IBCardEffectAsyncHandler +{ + private readonly IBuffFactory _buffFactory; + + public BCardSpecialBehaviorHandler(IBuffFactory buffFactory) => _buffFactory = buffFactory; + + public BCardType HandledType => BCardType.SpecialBehaviour; + + public void Execute(IBCardEffectContext ctx) + { + IBattleEntity target = ctx.Target; + IBattleEntity sender = ctx.Sender; + byte subType = ctx.BCard.SubType; + int firstData = ctx.BCard.FirstData; + int secondData = ctx.BCard.SecondData; + + switch ((AdditionalTypes.SpecialBehaviour)subType) + { + case AdditionalTypes.SpecialBehaviour.InflictOnTeam: + IEnumerable allies = target.Position.GetAlliesInRange(target, (byte)firstData); + foreach (IBattleEntity entity in allies) + { + if (entity.BuffComponent.HasBuff(secondData)) + { + continue; + } + + if (!ShouldGiveDebuff(target, entity)) + { + continue; + } + + Buff buff = _buffFactory.CreateBuff(secondData, sender); + entity.AddBuffAsync(buff); + } + + break; + case AdditionalTypes.SpecialBehaviour.TeleportRandom: + + Position newPosition = sender.MapInstance.GetRandomPosition(); + sender.TeleportOnMap(newPosition.X, newPosition.Y); + + break; + case AdditionalTypes.SpecialBehaviour.JumpToEveryObject: + + if (sender is not IMonsterEntity monsterEntity) + { + return; + } + + if (monsterEntity.Target == null) + { + return; + } + + IBattleEntity monsterTarget = monsterEntity.Target; + IEnumerable monsters = monsterEntity.MapInstance.GetAliveMonstersInRange(monsterEntity.Position, (byte)firstData); + foreach (IMonsterEntity monster in monsters) + { + if (monster.Id == monsterEntity.Id) + { + continue; + } + + monster.MapInstance.AddEntityToTargets(monster, monsterTarget); + if (monsterTarget is not IPlayerEntity playerEntity) + { + continue; + } + + playerEntity.Session.SendEffectEntity(monster, EffectType.TargetedByOthers); + } + + break; + } + } + + private bool ShouldGiveDebuff(IBattleEntity debuffer, IBattleEntity receiver) + { + bool give = debuffer switch + { + IPlayerEntity player when receiver is IPlayerEntity receiverPlayer => player.IsInGroupOf(receiverPlayer) + || player.Family != null && receiverPlayer.Family != null && player.Family.Id == receiverPlayer.Family.Id, + IPlayerEntity player when receiver is IMateEntity mateEntity => player.IsInGroupOf(mateEntity.Owner) + || player.Family != null && mateEntity.Owner.Family != null && player.Family.Id == mateEntity.Owner.Family.Id, + IMateEntity mateEntity when receiver is IPlayerEntity receiverPlayer => mateEntity.Owner.Id == receiverPlayer.Id, + IMateEntity mateEntity when receiver is IMateEntity mate => mateEntity.Owner.Id == mate.Owner.Id, + INpcEntity => false, + _ => true + }; + + return give; + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardSpecialDamageHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardSpecialDamageHandler.cs new file mode 100644 index 0000000..66a8a9d --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardSpecialDamageHandler.cs @@ -0,0 +1,130 @@ +using System.Collections.Generic; +using WingsEmu.Game; +using WingsEmu.Game._enum; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.BCards.Handlers; + +public class BCardSpecialDamageHandler : IBCardEffectAsyncHandler +{ + private readonly IRandomGenerator _randomGenerator; + + public BCardSpecialDamageHandler(IRandomGenerator randomGenerator) => _randomGenerator = randomGenerator; + + public BCardType HandledType => BCardType.SpecialDamageAndExplosions; + + public void Execute(IBCardEffectContext ctx) + { + IBattleEntity sender = ctx.Sender; + IBattleEntity target = ctx.Target; + + if (sender == null) + { + return; + } + + if (target == null) + { + return; + } + + if (sender.MapInstance?.Id != target.MapInstance?.Id) + { + return; + } + + int firstData = ctx.BCard.FirstDataValue(sender.Level); + int secondData = ctx.BCard.SecondDataValue(sender.Level); + + switch ((AdditionalTypes.SpecialDamageAndExplosions)ctx.BCard.SubType) + { + case AdditionalTypes.SpecialDamageAndExplosions.ChanceExplosion: + if (!target.IsAlive()) + { + return; + } + + if (_randomGenerator.RandomNumber() > firstData) + { + return; + } + + if (target.Hp - secondData <= 0) + { + target.Hp = 1; + } + else + { + target.Hp -= secondData; + } + + target.BroadcastEffectInRange(EffectType.FireExplosion); + if (target is not IPlayerEntity playerEntity) + { + return; + } + + playerEntity.Session.RefreshStat(); + break; + case AdditionalTypes.SpecialDamageAndExplosions.ExplosionCauses: + + IEnumerable targets = sender.GetEnemiesInRange(sender, (byte)firstData); + + foreach (IBattleEntity skillTarget in targets) + { + skillTarget.BroadcastEffectInRange(EffectType.SmokePuff); + if (skillTarget.Hp - secondData <= 0) + { + skillTarget.BroadcastDamage(skillTarget.Hp - 1); + skillTarget.Hp = 1; + } + else + { + skillTarget.Hp -= secondData; + skillTarget.BroadcastDamage(secondData); + } + + if (skillTarget is not IPlayerEntity player) + { + continue; + } + + player.Session.RefreshStat(); + } + + break; + case AdditionalTypes.SpecialDamageAndExplosions.ExplosionCausesNegated: + + targets = target.GetAlliesInRange(target, (byte)firstData); + + foreach (IBattleEntity skillTarget in targets) + { + skillTarget.BroadcastEffectInRange(EffectType.FireExplosion); + if (skillTarget.Hp - secondData <= 0) + { + skillTarget.BroadcastDamage(skillTarget.Hp - 1); + skillTarget.Hp = 1; + } + else + { + skillTarget.Hp -= secondData; + skillTarget.BroadcastDamage(secondData); + } + + if (skillTarget is not IPlayerEntity player) + { + continue; + } + + player.Session.RefreshStat(); + } + + break; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardSpecialEffect2Handler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardSpecialEffect2Handler.cs new file mode 100644 index 0000000..5145bbb --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardSpecialEffect2Handler.cs @@ -0,0 +1,144 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsEmu.Game; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.BCards.Handlers; + +public class BCardSpecialEffect2Handler : IBCardEffectAsyncHandler +{ + private readonly IBuffFactory _buffFactory; + private readonly ICardsManager _cardsManager; + private readonly IGameLanguageService _gameLanguage; + private readonly IRandomGenerator _randomGenerator; + + public BCardSpecialEffect2Handler(IRandomGenerator randomGenerator, ICardsManager cardsManager, IBuffFactory buffFactory, IGameLanguageService gameLanguage) + { + _randomGenerator = randomGenerator; + _cardsManager = cardsManager; + _buffFactory = buffFactory; + _gameLanguage = gameLanguage; + } + + public BCardType HandledType => BCardType.SpecialEffects2; + + public void Execute(IBCardEffectContext ctx) + { + if (!(ctx.Sender is IPlayerEntity character)) + { + return; + } + + IBattleEntity sender = ctx.Sender; + IBattleEntity target = ctx.Target; + + switch ((AdditionalTypes.SpecialEffects2)ctx.BCard.SubType) + { + case AdditionalTypes.SpecialEffects2.FocusEnemy: + if (target is not IMonsterEntity monsterEntity) + { + return; + } + + sender.MapInstance.AddEntityToTargets(monsterEntity, sender); + + break; + case AdditionalTypes.SpecialEffects2.TeleportInRadius: + character.Session.SendGuriPacket(1, (byte)ctx.BCard.FirstData); + break; + case AdditionalTypes.SpecialEffects2.MainWeaponCausingChance: + case AdditionalTypes.SpecialEffects2.SecondaryWeaponCausingChance: + + if (sender.IsSameEntity(target)) + { + return; + } + + Buff b = _buffFactory.CreateBuff(ctx.BCard.SecondData, sender); + if (b == null) + { + return; + } + + double debuffCounter = target.CheckForResistance(b, _cardsManager, out double buffCounter, out double specializedResistance); + + int randomNumber = _randomGenerator.RandomNumber(); + int debuffRandomNumber = _randomGenerator.RandomNumber(); + int buffRandomNumber = _randomGenerator.RandomNumber(); + int specializedRandomNumber = _randomGenerator.RandomNumber(); + switch (target) + { + case IMonsterEntity monster: + if (!monster.CanBeDebuffed) + { + return; + } + + break; + } + + if (b.CardId == (short)BuffVnums.MEMORIAL && sender.BuffComponent.HasBuff((short)BuffVnums.MEMORIAL)) + { + return; + } + + if (randomNumber > ctx.BCard.FirstData) + { + return; + } + + if (specializedRandomNumber >= (int)(specializedResistance * 100)) + { + if (!(target is IPlayerEntity c)) + { + return; + } + + string message = _gameLanguage.GetLanguage(GameDialogKey.BUFF_CHATMESSAGE_EFFECT_RESISTANCE, c.Session.UserLanguage); + c.Session.SendChatMessage(message, ChatMessageColorType.Buff); + return; + } + + switch (b.BuffGroup) + { + case BuffGroup.Bad when debuffRandomNumber >= (int)(debuffCounter * 100): + { + if (!(target is IPlayerEntity c)) + { + return; + } + + string message = _gameLanguage.GetLanguage(GameDialogKey.BUFF_CHATMESSAGE_EFFECT_RESISTANCE, c.Session.UserLanguage); + c.Session.SendChatMessage(message, ChatMessageColorType.Buff); + return; + } + case BuffGroup.Good when buffRandomNumber >= (int)(buffCounter * 100): + { + if (!(target is IPlayerEntity c)) + { + return; + } + + string message = _gameLanguage.GetLanguage(GameDialogKey.BUFF_CHATMESSAGE_EFFECT_RESISTANCE, c.Session.UserLanguage); + c.Session.SendChatMessage(message, ChatMessageColorType.Buff); + return; + } + } + + Buff buff = _buffFactory.CreateBuff(ctx.BCard.SecondData, sender); + target.AddBuffAsync(buff).ConfigureAwait(false).GetAwaiter().GetResult(); + break; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardSpecialEffectHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardSpecialEffectHandler.cs new file mode 100644 index 0000000..d4b043b --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardSpecialEffectHandler.cs @@ -0,0 +1,103 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using WingsEmu.Game; +using WingsEmu.Game._enum; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Entities.Extensions; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking.Broadcasting; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.BCards.Handlers; + +public class BCardSpecialEffectHandler : IBCardEffectAsyncHandler +{ + private readonly IGibberishConfig _gibberishConfig; + private readonly IRandomGenerator _randomGenerator; + + public BCardSpecialEffectHandler(IRandomGenerator randomGenerator, IGibberishConfig gibberishConfig) + { + _randomGenerator = randomGenerator; + _gibberishConfig = gibberishConfig; + } + + public BCardType HandledType => BCardType.SpecialEffects; + + public void Execute(IBCardEffectContext ctx) + { + IBattleEntity target = ctx.Target; + IBattleEntity sender = ctx.Sender; + byte subType = ctx.BCard.SubType; + int firstData = ctx.BCard.FirstDataValue(sender.Level); + int secondData = ctx.BCard.SecondDataValue(sender.Level); + + switch ((AdditionalTypes.SpecialEffects)subType) + { + case AdditionalTypes.SpecialEffects.DecreaseKillerHP: + if (target.Killer == null) + { + return; + } + + IBattleEntity killer = target.Killer; + + if (killer.MapInstance.Id != target.MapInstance.Id) + { + return; + } + + if (killer.BCardComponent.HasBCard(BCardType.Buff, (byte)AdditionalTypes.Buff.EffectResistance) && sender.IsMandra()) + { + return; + } + + int removeHp = (int)(killer.Hp * (firstData * 0.01)); + killer.Hp = killer.Hp - removeHp <= 0 ? 1 : killer.Hp - removeHp; + killer.BroadcastEffectInRange(EffectType.DecreaseHp); + if (killer is IPlayerEntity characterKiller) + { + characterKiller.Session.RefreshStat(); + } + + break; + case AdditionalTypes.SpecialEffects.ToNonPrefferedAttack: + int randomNumber = _randomGenerator.RandomNumber(); + if (randomNumber > firstData) + { + return; + } + + if (sender is IMonsterEntity { Target: { } } monsterEntity) + { + monsterEntity.MapInstance.RemoveTarget(monsterEntity, monsterEntity.Target); + } + + if (target is IMonsterEntity { Target: { } } monsterTarget) + { + monsterTarget.MapInstance.RemoveTarget(monsterTarget, monsterTarget.Target); + } + + break; + case AdditionalTypes.SpecialEffects.Gibberish: + + IReadOnlyList getConfig = _gibberishConfig.GetKeysById(firstData); + if (getConfig.Count == 0) + { + return; + } + + string message = getConfig[_randomGenerator.RandomNumber(getConfig.Count)]; + + target.MapInstance.Broadcast(x => target.GenerateSayPacket(x.GetLanguage(message), ChatMessageColorType.PlayerSay), + new RangeBroadcast(target.PositionX, target.PositionY, 30)); + break; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardSpecialisationBuffResistance.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardSpecialisationBuffResistance.cs new file mode 100644 index 0000000..ae20514 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardSpecialisationBuffResistance.cs @@ -0,0 +1,49 @@ +// WingsEmu +// +// Developed by NosWings Team + +using WingsEmu.Game; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.BCards.Handlers; + +public class BCardSpecialisationBuffResistance : IBCardEffectAsyncHandler +{ + private readonly IRandomGenerator _randomGenerator; + + public BCardSpecialisationBuffResistance(IRandomGenerator randomGenerator) => _randomGenerator = randomGenerator; + + public BCardType HandledType => BCardType.SpecialisationBuffResistance; + + public void Execute(IBCardEffectContext ctx) + { + IBattleEntity target = ctx.Target; + byte subType = ctx.BCard.SubType; + int firstData = ctx.BCard.FirstData; + int secondData = ctx.BCard.SecondData; + int randomNumber = _randomGenerator.RandomNumber(); + + if (randomNumber > firstData) + { + return; + } + + switch (subType) + { + case (byte)AdditionalTypes.SpecialisationBuffResistance.RemoveBadEffects: + { + target.RemoveNegativeBuffs(secondData); + break; + } + + case (byte)AdditionalTypes.SpecialisationBuffResistance.RemoveGoodEffects: + { + target.RemovePositiveBuffs(secondData); + break; + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardSummonHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardSummonHandler.cs new file mode 100644 index 0000000..6213a82 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardSummonHandler.cs @@ -0,0 +1,170 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Generic; +using PhoenixLib.Events; +using WingsEmu.Game; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Monster.Event; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.BCards.Handlers; + +public class BCardSummonHandler : IBCardEffectAsyncHandler +{ + private readonly IAsyncEventPipeline _eventPipeline; + private readonly INpcMonsterManager _manager; + private readonly IRandomGenerator _randomGenerator; + + public BCardSummonHandler(IRandomGenerator randomGenerator, IAsyncEventPipeline eventPipeline, INpcMonsterManager manager) + { + _randomGenerator = randomGenerator; + _eventPipeline = eventPipeline; + _manager = manager; + } + + public BCardType HandledType => BCardType.Summons; + + public void Execute(IBCardEffectContext ctx) + { + if (ctx.Sender == null) + { + return; + } + + if (ctx.Target == null) + { + return; + } + + IBattleEntity sender = ctx.Sender; + byte subType = ctx.BCard.SubType; + + int firstData = ctx.BCard.FirstData; + int secondData = ctx.BCard.SecondData; + int procChance = ctx.BCard.ProcChance; + + var summons = new List(); + + Position entityPosition = sender.Position; + + switch ((AdditionalTypes.Summons)subType) + { + case AdditionalTypes.Summons.Summons: + for (int i = 0; i < firstData; i++) + { + IMonsterData monsterToSummon = _manager.GetNpc(secondData); + if (monsterToSummon == null) + { + continue; + } + + short x = entityPosition.X; + short y = entityPosition.Y; + + x += (short)_randomGenerator.RandomNumber(-3, 3); + y += (short)_randomGenerator.RandomNumber(-3, 3); + if (sender.MapInstance.IsBlockedZone(x, y)) + { + x = entityPosition.X; + y = entityPosition.Y; + } + + var position = new Position(x, y); + summons.Add(new ToSummon + { + VNum = monsterToSummon.MonsterVNum, + SpawnCell = position, + IsMoving = monsterToSummon.CanWalk, + IsHostile = true + }); + } + + _eventPipeline.ProcessEventAsync(new MonsterSummonEvent(sender.MapInstance, summons, sender, showEffect: true)).ConfigureAwait(false).GetAwaiter().GetResult(); + break; + case AdditionalTypes.Summons.SummonningChance: + + short posX = sender.Position.X; + short posY = sender.Position.Y; + + posX += (short)_randomGenerator.RandomNumber(-3, 3); + posY += (short)_randomGenerator.RandomNumber(-3, 3); + + if (sender.MapInstance.IsBlockedZone(posX, posY)) + { + posX = entityPosition.X; + posY = entityPosition.Y; + } + + var newPosition = new Position(posX, posY); + + summons.Add(new ToSummon + { + VNum = (short)secondData, + SpawnCell = newPosition, + IsMoving = true, + IsHostile = true, + SummonChance = (byte)Math.Abs(firstData) + }); + _eventPipeline.ProcessEventAsync(new MonsterSummonEvent(sender.MapInstance, summons, showEffect: true)).ConfigureAwait(false).GetAwaiter().GetResult(); + break; + case AdditionalTypes.Summons.SummonTrainingDummy: + { + summons.Add(new ToSummon + { + VNum = (short)secondData, + SpawnCell = entityPosition, + IsMoving = true, + IsHostile = true, + SummonChance = (byte)Math.Abs(procChance) + }); + + _eventPipeline.ProcessEventAsync(new MonsterSummonEvent(sender.MapInstance, summons, showEffect: true)).ConfigureAwait(false).GetAwaiter().GetResult(); + break; + } + case AdditionalTypes.Summons.SummonUponDeathChance: + summons.Add(new ToSummon + { + VNum = (short)secondData, + SpawnCell = new Position(sender.PositionX, sender.PositionY), + IsMoving = true, + IsHostile = true, + SummonChance = (byte)Math.Abs(firstData) + }); + _eventPipeline.ProcessEventAsync(new MonsterSummonEvent(sender.MapInstance, summons)).ConfigureAwait(false).GetAwaiter().GetResult(); + break; + case AdditionalTypes.Summons.SummonUponDeath: + for (short i = 0; i < firstData; i++) + { + short senderPositionX = sender.Position.X; + short senderPositionY = sender.Position.Y; + + senderPositionX += (short)_randomGenerator.RandomNumber(-2, 2); + senderPositionY += (short)_randomGenerator.RandomNumber(-2, 2); + + if (sender.MapInstance.IsBlockedZone(senderPositionX, senderPositionY)) + { + senderPositionX = entityPosition.X; + senderPositionY = entityPosition.Y; + } + + summons.Add(new ToSummon + { + VNum = (short)secondData, + SpawnCell = new Position(senderPositionX, senderPositionY), + IsMoving = true, + IsHostile = true + }); + } + + _eventPipeline.ProcessEventAsync(new MonsterSummonEvent(sender.MapInstance, summons, sender)).ConfigureAwait(false).GetAwaiter().GetResult(); + break; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardTeleportToLocation.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardTeleportToLocation.cs new file mode 100644 index 0000000..93fc128 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardTeleportToLocation.cs @@ -0,0 +1,27 @@ +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.BCards.Handlers; + +public class BCardTeleportToLocation : IBCardEffectAsyncHandler +{ + public BCardType HandledType => BCardType.FairyXPIncrease; + + public void Execute(IBCardEffectContext ctx) + { + IBattleEntity sender = ctx.Sender; + IBattleEntity target = ctx.Target; + Position position = ctx.Position; + + switch (ctx.BCard.SubType) + { + case (byte)AdditionalTypes.FairyXPIncrease.TeleportToLocation: + sender.ChangePosition(position); + sender.TeleportOnMap(sender.PositionX, sender.PositionY); + break; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardTimeTwisterHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardTimeTwisterHandler.cs new file mode 100644 index 0000000..7f2faea --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/BCardTimeTwisterHandler.cs @@ -0,0 +1,56 @@ +using WingsEmu.Game; +using WingsEmu.Game._enum; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Entities; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.BCards.Handlers; + +public class BCardTimeTwisterHandler : IBCardEffectAsyncHandler +{ + private readonly IBuffFactory _buffFactory; + private readonly IRandomGenerator _randomGenerator; + + public BCardTimeTwisterHandler(IRandomGenerator randomGenerator, IBuffFactory buffFactory) + { + _randomGenerator = randomGenerator; + _buffFactory = buffFactory; + } + + public BCardType HandledType => BCardType.AbsorbedSpirit; + + public void Execute(IBCardEffectContext ctx) + { + IBattleEntity sender = ctx.Sender; + byte subType = ctx.BCard.SubType; + int firstData = ctx.BCard.FirstData; + int secondData = ctx.BCard.SecondData; + + if (_randomGenerator.RandomNumber() > firstData) + { + return; + } + + switch (subType) + { + case (byte)AdditionalTypes.AbsorbedSpirit.ApplyEffectIfPresent: + if (!sender.BuffComponent.HasBuff((short)BuffVnums.SPIRIT_ABSORPTION)) + { + return; + } + + sender.AddBuffAsync(_buffFactory.CreateBuff(secondData, sender)).ConfigureAwait(false).GetAwaiter().GetResult(); + break; + + case (byte)AdditionalTypes.AbsorbedSpirit.ApplyEffectIfNotPresent: + if (sender.BuffComponent.HasBuff((short)BuffVnums.SPIRIT_ABSORPTION)) + { + return; + } + + sender.AddBuffAsync(_buffFactory.CreateBuff(secondData, sender)).ConfigureAwait(false).GetAwaiter().GetResult(); + break; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/TimeCircleSkillsHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/TimeCircleSkillsHandler.cs new file mode 100644 index 0000000..7cf7550 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BCards/Handlers/TimeCircleSkillsHandler.cs @@ -0,0 +1,33 @@ +using WingsEmu.DTOs.BCards; +using WingsEmu.Game._enum; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Entities; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.BCards.Handlers; + +public class TimeCircleSkillsHandler : IBCardEffectAsyncHandler +{ + private readonly IBuffFactory _buffFactory; + + public TimeCircleSkillsHandler(IBuffFactory buffFactory) => _buffFactory = buffFactory; + + public BCardType HandledType => BCardType.TimeCircleSkills; + + public void Execute(IBCardEffectContext ctx) + { + IBattleEntity sender = ctx.Sender; + IBattleEntity target = ctx.Target; + BCardDTO bCard = ctx.BCard; + + switch ((AdditionalTypes.TimeCircleSkills)bCard.SubType) + { + case AdditionalTypes.TimeCircleSkills.GatherEnergy: + sender.ChargeComponent.SetCharge(bCard.FirstDataValue(sender.Level)); + sender.AddBuffAsync(_buffFactory.CreateBuff((short)BuffVnums.CHARGE, sender)); + + break; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BaseGuriHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BaseGuriHandler.cs new file mode 100644 index 0000000..5f7b851 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/BaseGuriHandler.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using PhoenixLib.Logging; +using WingsEmu.Game._Guri; +using WingsEmu.Game._Guri.Event; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Plugins.BasicImplementations; + +public class BaseGuriHandler : IGuriHandlerContainer +{ + private readonly Dictionary _handlersByDialogId; + + public BaseGuriHandler() => _handlersByDialogId = new Dictionary(); + + public void Register(IGuriHandler handler) + { + if (_handlersByDialogId.ContainsKey(handler.GuriEffectId)) + { + return; + } + + Log.Debug($"[GURI_HANDLER][REGISTER] GURI_EFFECT : {handler.GuriEffectId} REGISTERED !"); + _handlersByDialogId.Add(handler.GuriEffectId, handler); + } + + public void Unregister(long guriEffectId) + { + Log.Debug($"[GURI_HANDLER][UNREGISTER] GURI_EFFECT : {guriEffectId} UNREGISTERED !"); + _handlersByDialogId.Remove(guriEffectId); + } + + public void Handle(IClientSession player, GuriEvent args) + { + HandleAsync(player, args).ConfigureAwait(false).GetAwaiter().GetResult(); + } + + public async Task HandleAsync(IClientSession player, GuriEvent args) + { + if (!_handlersByDialogId.TryGetValue(args.EffectId, out IGuriHandler handler)) + { + Log.Debug($"[GURI_HANDLER] GURI_EFFECT : {args.EffectId} "); + return; + } + + Log.Debug($"[GURI_HANDLER][HANDLING] : {args.EffectId} "); + await handler.ExecuteAsync(player, args); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Bazaar/BazaarGetListedItemsEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Bazaar/BazaarGetListedItemsEventHandler.cs new file mode 100644 index 0000000..ed50f42 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Bazaar/BazaarGetListedItemsEventHandler.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.PacketGeneration; +using WingsEmu.Game.Algorithm; +using WingsEmu.Game.Bazaar; +using WingsEmu.Game.Bazaar.Configuration; +using WingsEmu.Game.Bazaar.Events; +using WingsEmu.Game.Managers.StaticData; + +namespace WingsEmu.Plugins.BasicImplementations.Bazaar; + +public class BazaarGetListedItemsEventHandler : IAsyncEventProcessor +{ + private readonly BazaarConfiguration _bazaarConfiguration; + private readonly IBazaarManager _bazaarManager; + private readonly ICharacterAlgorithm _characterAlgorithm; + private readonly IItemsManager _itemsManager; + + public BazaarGetListedItemsEventHandler(IItemsManager itemsManager, ICharacterAlgorithm characterAlgorithm, IBazaarManager bazaarManager, BazaarConfiguration bazaarConfiguration) + { + _itemsManager = itemsManager; + _characterAlgorithm = characterAlgorithm; + _bazaarManager = bazaarManager; + _bazaarConfiguration = bazaarConfiguration; + } + + public async Task HandleAsync(BazaarGetListedItemsEvent e, CancellationToken cancellation) + { + IReadOnlyCollection items = await _bazaarManager.GetListedItemsByCharacterId(e.Sender.PlayerEntity.Id); + + e.Sender.SendCharacterListedBazaarItems(e.Index, items, e.Filter, _itemsManager, _characterAlgorithm, _bazaarConfiguration); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Bazaar/BazaarItemAddEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Bazaar/BazaarItemAddEventHandler.cs new file mode 100644 index 0000000..01ddafc --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Bazaar/BazaarItemAddEventHandler.cs @@ -0,0 +1,138 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using PhoenixLib.Logging; +using WingsAPI.Communication; +using WingsAPI.Communication.Bazaar; +using WingsAPI.Data.Bazaar; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsAPI.Game.Extensions.ItemExtension.Item; +using WingsEmu.DTOs.Items; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Bazaar; +using WingsEmu.Game.Bazaar.Configuration; +using WingsEmu.Game.Bazaar.Events; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.Bazaar; + +public class BazaarItemAddEventHandler : IAsyncEventProcessor +{ + private readonly BazaarConfiguration _bazaarConfiguration; + private readonly IBazaarManager _bazaarManager; + private readonly IBazaarService _bazaarService; + private readonly IGameItemInstanceFactory _itemInstanceFactory; + private readonly IGameLanguageService _languageService; + private readonly IServerManager _serverManager; + + public BazaarItemAddEventHandler(IBazaarService bazaarService, IBazaarManager bazaarManager, IGameLanguageService languageService, IServerManager serverManager, + BazaarConfiguration bazaarConfiguration, IGameItemInstanceFactory itemInstanceFactory) + { + _bazaarService = bazaarService; + _bazaarManager = bazaarManager; + _languageService = languageService; + _serverManager = serverManager; + _bazaarConfiguration = bazaarConfiguration; + _itemInstanceFactory = itemInstanceFactory; + } + + public async Task HandleAsync(BazaarItemAddEvent e, CancellationToken cancellation) + { + if (e.InventoryItem == null || e.InventoryItem.ItemInstance.Amount < e.Amount) + { + await e.Sender.NotifyStrangeBehavior(StrangeBehaviorSeverity.ABUSING, "[BAZAAR] The item defined is null or doesn't meet the expectations."); + return; + } + + if (!e.InventoryItem.ItemInstance.GameItem.IsSoldable || e.InventoryItem.ItemInstance.IsBound + || e.InventoryItem.ItemInstance.GameItem.ItemType is ItemType.Specialist or ItemType.Quest1 or ItemType.Quest2) + { + await e.Sender.NotifyStrangeBehavior(StrangeBehaviorSeverity.ABUSING, "[BAZAAR] Item that can't be sold is being tried to be sold."); + return; + } + + int maximumListedItems = e.UsedMedal ? _bazaarConfiguration.MaximumListedItemsMedal : _bazaarConfiguration.MaximumListedItems; + + ItemInstanceDTO itemCopy = _itemInstanceFactory.CreateDto(e.InventoryItem.ItemInstance); + itemCopy.Amount = e.Amount; + + await e.Sender.RemoveItemFromInventory(amount: e.Amount, item: e.InventoryItem); + e.Sender.PlayerEntity.RemoveGold(e.Tax); + + BazaarItemResponse response = null; + + try + { + response = await _bazaarService.AddItemToBazaar(new BazaarAddItemRequest + { + ChannelId = _serverManager.ChannelId, + BazaarItemDto = new BazaarItemDTO + { + Amount = itemCopy.Amount, + CharacterId = e.Sender.PlayerEntity.Id, + ItemInstance = itemCopy, + SaleFee = e.UsedMedal ? 0 : (long)(e.PricePerItem * 0.05), + ExpiryDate = e.ExpiryDate, + DayExpiryAmount = e.DayExpiryAmount, + IsPackage = e.IsPackage, + PricePerItem = e.PricePerItem, + UsedMedal = e.UsedMedal + }, + OwnerName = e.Sender.PlayerEntity.Name, + SunkGold = e.Tax, + MaximumListedItems = maximumListedItems + }); + } + catch (Exception ex) + { + Log.Error("[BAZAAR_LISTING]", ex); + } + + if (response == null || response.ResponseType == RpcResponseType.MAINTENANCE_MODE) + { + e.Sender.SendInfo(_languageService.GetLanguage(GameDialogKey.BAZAAR_INFO_MAINTENANCE_MODE, e.Sender.UserLanguage)); + await e.Sender.AddNewItemToInventory(_itemInstanceFactory.CreateItem(itemCopy), sendGiftIsFull: true); + await e.Sender.EmitEventAsync(new GenerateGoldEvent(e.Tax, sendMessage: false)); + return; + } + + if (response.ResponseType == RpcResponseType.SUCCESS) + { + string itemName = e.InventoryItem.ItemInstance.GameItem.GetItemName(_languageService, e.Sender.UserLanguage); + string message = _languageService.GetLanguageFormat(GameDialogKey.BAZAAR_CHATMESSAGE_ITEM_ADDED, e.Sender.UserLanguage, itemName, e.Amount); + e.Sender.SendChatMessage(message, ChatMessageColorType.Yellow); + e.Sender.SendMsg(_languageService.GetLanguage(GameDialogKey.BAZAAR_SHOUTMESSAGE_ITEM_ADDED, e.Sender.UserLanguage), MsgMessageType.Middle); + e.Sender.SendPacket("rc_reg 1"); + await e.Sender.EmitEventAsync(new BazaarItemAddedEvent + { + ItemVnum = e.InventoryItem.ItemInstance.ItemVNum, + Amount = e.Amount, + PricePerItem = e.PricePerItem, + ExpiryDate = e.ExpiryDate, + UsedMedal = e.UsedMedal, + IsPackage = e.IsPackage, + Tax = e.Tax + }); + + await e.Sender.EmitEventAsync(new BazaarItemInsertedEvent + { + ItemInstance = e.InventoryItem.ItemInstance, + BazaarItemId = response.BazaarItemDto.Id, + Price = e.PricePerItem, + Quantity = e.Amount, + Taxes = e.Tax + }); + return; + } + + e.Sender.SendInfo(_languageService.GetLanguage(GameDialogKey.BAZAAR_INFO_MAXIMUM_ITEMS_REACHED, e.Sender.UserLanguage)); + await e.Sender.AddNewItemToInventory(_itemInstanceFactory.CreateItem(itemCopy), sendGiftIsFull: true); + await e.Sender.EmitEventAsync(new GenerateGoldEvent(e.Tax, sendMessage: false)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Bazaar/BazaarItemBuyEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Bazaar/BazaarItemBuyEventHandler.cs new file mode 100644 index 0000000..4dd6afa --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Bazaar/BazaarItemBuyEventHandler.cs @@ -0,0 +1,128 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using PhoenixLib.Logging; +using PhoenixLib.ServiceBus; +using WingsAPI.Communication; +using WingsAPI.Communication.Bazaar; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsAPI.Game.Extensions.ItemExtension.Item; +using WingsAPI.Game.Extensions.PacketGeneration; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Bazaar; +using WingsEmu.Game.Bazaar.Events; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums.Chat; +using WingsEmu.Plugins.DistributedGameEvents.InterChannel; + +namespace WingsEmu.Plugins.BasicImplementations.Bazaar; + +public class BazaarItemBuyEventHandler : IAsyncEventProcessor +{ + private readonly IBazaarManager _bazaarManager; + private readonly IBazaarService _bazaarService; + private readonly IGameItemInstanceFactory _itemInstanceFactory; + private readonly IGameLanguageService _languageService; + private readonly IMessagePublisher _messagePublisher; + private readonly IServerManager _serverManager; + private readonly ISessionManager _sessionManager; + + public BazaarItemBuyEventHandler(IBazaarManager bazaarManager, IBazaarService bazaarService, + IGameLanguageService languageService, IServerManager serverManager, IGameItemInstanceFactory itemInstanceFactory, + ISessionManager sessionManager, IMessagePublisher messagePublisher) + { + _bazaarManager = bazaarManager; + _bazaarService = bazaarService; + _languageService = languageService; + _serverManager = serverManager; + _itemInstanceFactory = itemInstanceFactory; + _sessionManager = sessionManager; + _messagePublisher = messagePublisher; + } + + public async Task HandleAsync(BazaarItemBuyEvent e, CancellationToken cancellation) + { + long cost = e.PricePerItem * e.Amount; + + if (!e.Sender.HasEnoughGold(cost)) + { + e.Sender.SendInfo(_languageService.GetLanguage(GameDialogKey.INTERACTION_MESSAGE_NOT_ENOUGH_GOLD, e.Sender.UserLanguage)); + return; + } + + e.Sender.PlayerEntity.RemoveGold(cost); + + BazaarItemResponse response = null; + try + { + response = await _bazaarService.BuyItemFromBazaar(new BazaarBuyItemRequest + { + ChannelId = _serverManager.ChannelId, + BazaarItemId = e.BazaarItemId, + BuyerCharacterId = e.Sender.PlayerEntity.Id, + Amount = e.Amount, + PricePerItem = e.PricePerItem + }); + } + catch (Exception ex) + { + Log.Error("", ex); + } + + switch (response?.ResponseType) + { + case RpcResponseType.MAINTENANCE_MODE: + e.Sender.SendInfo(_languageService.GetLanguage(GameDialogKey.BAZAAR_INFO_MAINTENANCE_MODE, e.Sender.UserLanguage)); + break; + case RpcResponseType.SUCCESS: + GameItemInstance itemInstance = _itemInstanceFactory.CreateItem(response.BazaarItemDto.ItemInstance); + itemInstance.Amount = e.Amount; + await e.Sender.AddNewItemToInventory(itemInstance, sendGiftIsFull: true); + string ownerName = await _bazaarManager.GetOwnerName(response.BazaarItemDto.CharacterId); + await e.Sender.EmitEventAsync(new BazaarItemBoughtEvent + { + BoughtItem = itemInstance, + BazaarItemId = response.BazaarItemDto.Id, + Amount = e.Amount, + PricePerItem = e.PricePerItem, + SellerId = response.BazaarItemDto.CharacterId, + SellerName = ownerName + }); + e.Sender.SendBazaarResponseItemBuy(true, itemInstance.ItemVNum, ownerName, e.Amount, e.PricePerItem, + itemInstance.Upgrade, itemInstance.Rarity); + await SendNotificationToOwner(ownerName, itemInstance); + return; + default: + e.Sender.SendInfo(_languageService.GetLanguage(GameDialogKey.BAZAAR_INFO_ITEM_CHANGED, e.Sender.UserLanguage)); + break; + } + + await e.Sender.EmitEventAsync(new GenerateGoldEvent(cost, sendMessage: false)); + e.Sender.SendBazaarResponseItemBuy(false, 0, "", 0, 0, 0, 0); + } + + private async Task SendNotificationToOwner(string ownerName, GameItemInstance itemInstance) + { + IClientSession owner = _sessionManager.GetSessionByCharacterName(ownerName); + if (owner != null) + { + string itemName = itemInstance.GameItem.GetItemName(_languageService, owner.UserLanguage); + int amount = itemInstance.Amount; + + owner.SendChatMessage(owner.GetLanguageFormat(GameDialogKey.BAZAAR_CHATMESSAGE_ITEM_SOLD, amount, itemName), ChatMessageColorType.Green); + return; + } + + await _messagePublisher.PublishAsync(new BazaarNotificationMessage + { + OwnerName = ownerName, + ItemVnum = itemInstance.ItemVNum, + Amount = itemInstance.Amount + }); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Bazaar/BazaarItemChangePriceEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Bazaar/BazaarItemChangePriceEventHandler.cs new file mode 100644 index 0000000..01751c8 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Bazaar/BazaarItemChangePriceEventHandler.cs @@ -0,0 +1,133 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using PhoenixLib.Logging; +using WingsAPI.Communication; +using WingsAPI.Communication.Bazaar; +using WingsAPI.Game.Extensions.Bazaar; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsAPI.Packets.Enums.Bazaar; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Bazaar; +using WingsEmu.Game.Bazaar.Configuration; +using WingsEmu.Game.Bazaar.Events; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.Bazaar; + +public class BazaarItemChangePriceEventHandler : IAsyncEventProcessor +{ + private readonly BazaarConfiguration _bazaarConfiguration; + private readonly IBazaarManager _bazaarManager; + private readonly IBazaarService _bazaarService; + private readonly IGameLanguageService _languageService; + private readonly IServerManager _serverManager; + + public BazaarItemChangePriceEventHandler(IBazaarManager bazaarManager, IBazaarService bazaarService, IServerManager serverManager, IGameLanguageService languageService, + BazaarConfiguration bazaarConfiguration) + { + _bazaarManager = bazaarManager; + _bazaarService = bazaarService; + _serverManager = serverManager; + _languageService = languageService; + _bazaarConfiguration = bazaarConfiguration; + } + + public async Task HandleAsync(BazaarItemChangePriceEvent e, CancellationToken cancellation) + { + await MainMethod(e); + + //TODO: Check official implementation + await e.Sender.EmitEventAsync(new BazaarGetListedItemsEvent(0, BazaarListedItemType.All)); + } + + private async Task MainMethod(BazaarItemChangePriceEvent e) + { + BazaarItem cachedItem = await _bazaarManager.GetBazaarItemById(e.BazaarItemId); + if (cachedItem == null) + { + return; + } + + if (cachedItem.BazaarItemDto.CharacterId != e.Sender.PlayerEntity.Id) + { + await e.Sender.NotifyStrangeBehavior(StrangeBehaviorSeverity.ABUSING, + $"Tried to change the price of an item that the character doesn't own. BazaarItemId: {cachedItem.BazaarItemDto.Id.ToString()}"); + return; + } + + if (cachedItem.BazaarItemDto.SoldAmount > 0 || cachedItem.BazaarItemDto.GetBazaarItemStatus() != BazaarListedItemType.ForSale) + { + e.Sender.SendInfo(_languageService.GetLanguage(GameDialogKey.BAZAAR_INFO_ITEM_CHANGED, e.Sender.UserLanguage)); + return; + } + + if (BazaarExtensions.PriceOrAmountExceeds(cachedItem.BazaarItemDto.UsedMedal, e.NewPricePerItem, cachedItem.BazaarItemDto.Amount)) + { + e.Sender.SendInfo(_languageService.GetLanguage(GameDialogKey.BAZAAR_INFO_PRICE_EXCEEDS_LIMITS, e.Sender.UserLanguage)); + return; + } + + long tax = cachedItem.BazaarItemDto.UsedMedal + ? BazaarExtensions.MedalTax(e.NewPricePerItem * cachedItem.BazaarItemDto.Amount, cachedItem.BazaarItemDto.DayExpiryAmount) + : BazaarExtensions.NormalTax(e.NewPricePerItem * cachedItem.BazaarItemDto.Amount); + + if (!e.Confirmed) + { + e.Sender.SendQnaPacket($"c_mod {e.BazaarItemId.ToString()} 0 0 {e.NewPricePerItem.ToString()} 1", + _languageService.GetLanguageFormat(GameDialogKey.BAZAAR_QNA_CHANGE_PRICE_FEE, e.Sender.UserLanguage, tax.ToString())); + return; + } + + DateTime currentDate = DateTime.UtcNow; + if (e.Sender.PlayerEntity.LastAdministrationBazaarRefresh > currentDate) + { + return; + } + + e.Sender.PlayerEntity.LastAdministrationBazaarRefresh = currentDate.AddSeconds(_bazaarConfiguration.DelayServerBetweenRequestsInSecs); + + if (!e.Sender.HasEnoughGold(tax)) + { + e.Sender.SendInfo(_languageService.GetLanguage(GameDialogKey.INTERACTION_MESSAGE_NOT_ENOUGH_GOLD, e.Sender.UserLanguage)); + return; + } + + e.Sender.PlayerEntity.RemoveGold(tax); + + BazaarItemResponse response = null; + try + { + response = await _bazaarService.ChangeItemPriceFromBazaar(new BazaarChangeItemPriceRequest + { + ChannelId = _serverManager.ChannelId, + BazaarItemDto = cachedItem.BazaarItemDto, + ChangerCharacterId = e.Sender.PlayerEntity.Id, + NewPrice = e.NewPricePerItem, + NewSaleFee = cachedItem.BazaarItemDto.UsedMedal ? 0 : (long)(e.NewPricePerItem * 0.05), + SunkGold = tax + }); + } + catch (Exception ex) + { + Log.Error(nameof(BazaarItemChangePriceEventHandler), ex); + } + + if (response?.ResponseType != RpcResponseType.SUCCESS) + { + e.Sender.SendInfo( + response?.ResponseType == RpcResponseType.MAINTENANCE_MODE + ? _languageService.GetLanguage(GameDialogKey.BAZAAR_INFO_MAINTENANCE_MODE, e.Sender.UserLanguage) + : _languageService.GetLanguage(GameDialogKey.BAZAAR_INFO_RESYNC, e.Sender.UserLanguage)); + + await e.Sender.EmitEventAsync(new GenerateGoldEvent(tax, sendMessage: false)); + return; + } + + e.Sender.SendMsg(_languageService.GetLanguage(GameDialogKey.BAZAAR_SHOUTMESSAGE_ITEM_PRICE_CHANGED, e.Sender.UserLanguage), MsgMessageType.Middle); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Bazaar/BazaarItemRemoveEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Bazaar/BazaarItemRemoveEventHandler.cs new file mode 100644 index 0000000..7bbe3d5 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Bazaar/BazaarItemRemoveEventHandler.cs @@ -0,0 +1,116 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using PhoenixLib.Logging; +using WingsAPI.Communication; +using WingsAPI.Communication.Bazaar; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsAPI.Game.Extensions.PacketGeneration; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Bazaar; +using WingsEmu.Game.Bazaar.Events; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.Bazaar; + +public class BazaarItemRemoveEventHandler : IAsyncEventProcessor +{ + private readonly IBazaarManager _bazaarManager; + private readonly IBazaarService _bazaarService; + private readonly IGameItemInstanceFactory _itemInstanceFactory; + private readonly IGameLanguageService _languageService; + private readonly IServerManager _serverManager; + + public BazaarItemRemoveEventHandler(IBazaarManager bazaarManager, IBazaarService bazaarService, IServerManager serverManager, IGameLanguageService languageService, + IGameItemInstanceFactory itemInstanceFactory) + { + _bazaarManager = bazaarManager; + _bazaarService = bazaarService; + _serverManager = serverManager; + _languageService = languageService; + _itemInstanceFactory = itemInstanceFactory; + } + + public async Task HandleAsync(BazaarItemRemoveEvent e, CancellationToken cancellation) + { + BazaarItem item = await _bazaarManager.GetBazaarItemById(e.BazaarItemId); + if (item == null) + { + return; + } + + if (item.BazaarItemDto.CharacterId != e.Sender.PlayerEntity.Id) + { + await e.Sender.NotifyStrangeBehavior(StrangeBehaviorSeverity.ABUSING, $"Tried to remove an item that is not listed by this character. BazaarItemId: {e.BazaarItemId.ToString()}"); + return; + } + + int amount = item.BazaarItemDto.Amount - item.BazaarItemDto.SoldAmount; + + if (amount > 0 && !e.Sender.PlayerEntity.HasSpaceFor(item.Item.ItemVNum, (short)amount)) + { + e.Sender.SendMsg(_languageService.GetLanguage(GameDialogKey.INTERACTION_MESSAGE_NOT_ENOUGH_PLACE, e.Sender.UserLanguage), MsgMessageType.Middle); + return; + } + + BazaarItemResponse response = null; + try + { + response = await _bazaarService.RemoveItemFromBazaar(new BazaarRemoveItemRequest + { + ChannelId = _serverManager.ChannelId, + BazaarItemDto = item.BazaarItemDto, + RequesterCharacterId = e.Sender.PlayerEntity.Id + }); + } + catch (Exception ex) + { + Log.Error("", ex); + } + + if (response?.ResponseType != RpcResponseType.SUCCESS) + { + e.Sender.SendInfo(_languageService.GetLanguage( + response?.ResponseType == RpcResponseType.MAINTENANCE_MODE ? GameDialogKey.BAZAAR_INFO_MAINTENANCE_MODE : GameDialogKey.BAZAAR_INFO_ITEM_CHANGED, e.Sender.UserLanguage)); + return; + } + + long pricePerItem = response.BazaarItemDto.PricePerItem; + long totalProfit = 0; + long taxes = 0; + if (response.BazaarItemDto.SoldAmount >= 1) + { + taxes = response.BazaarItemDto.SaleFee * response.BazaarItemDto.SoldAmount / response.BazaarItemDto.Amount; + totalProfit = pricePerItem * response.BazaarItemDto.SoldAmount - taxes; + await e.Sender.EmitEventAsync(new GenerateGoldEvent(totalProfit, sendMessage: false, fallBackToBank: true)); + await e.Sender.EmitEventAsync(new BazaarItemRemovedEvent + { + ItemVnum = response.BazaarItemDto.ItemInstance.ItemVNum, + SoldAmount = (short)response.BazaarItemDto.SoldAmount, + Amount = (short)response.BazaarItemDto.Amount, + TotalProfit = totalProfit + }); + await e.Sender.EmitEventAsync(new BazaarItemWithdrawnEvent + { + ItemInstance = response.BazaarItemDto.ItemInstance, + Quantity = response.BazaarItemDto.Amount, + Price = response.BazaarItemDto.PricePerItem, + BazaarItemId = response.BazaarItemDto.Id + }); + } + + GameItemInstance itemInstance = _itemInstanceFactory.CreateItem(response.BazaarItemDto.ItemInstance); + if (response.BazaarItemDto.SoldAmount < response.BazaarItemDto.Amount) + { + itemInstance.Amount = response.BazaarItemDto.Amount - response.BazaarItemDto.SoldAmount; + await e.Sender.AddNewItemToInventory(itemInstance, sendGiftIsFull: true); + } + + e.Sender.SendBazaarResponseItemRemove(true, pricePerItem, response.BazaarItemDto.SoldAmount, response.BazaarItemDto.Amount, taxes, totalProfit, itemInstance.ItemVNum); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Bazaar/BazaarManager.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Bazaar/BazaarManager.cs new file mode 100644 index 0000000..218191a --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Bazaar/BazaarManager.cs @@ -0,0 +1,162 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using PhoenixLib.Caching; +using PhoenixLib.Logging; +using WingsAPI.Communication; +using WingsAPI.Communication.Bazaar; +using WingsAPI.Communication.DbServer.CharacterService; +using WingsAPI.Data.Bazaar; +using WingsEmu.Game.Bazaar; +using WingsEmu.Game.Items; + +namespace WingsEmu.Plugins.BasicImplementations.Bazaar; + +public class BazaarManager : IBazaarManager +{ + private static readonly TimeSpan CacheLifeTime = TimeSpan.FromMinutes(Convert.ToInt32(Environment.GetEnvironmentVariable("BAZAAR_MANAGER_TTL_MINUTES") ?? "30")); + + private readonly IBazaarService _bazaarService; + private readonly ICharacterService _characterService; + private readonly IGameItemInstanceFactory _itemInstanceFactory; + + private readonly IKeyValueCache _nameByKey; + + public BazaarManager(IBazaarService bazaarService, ICharacterService characterService, IGameItemInstanceFactory itemInstanceFactory, IKeyValueCache nameByKey) + { + _bazaarService = bazaarService; + _characterService = characterService; + _itemInstanceFactory = itemInstanceFactory; + _nameByKey = nameByKey; + } + + public async Task GetOwnerName(long characterId) + { + string name = _nameByKey.Get(GetKey(characterId)); + if (name != null) + { + SetName(characterId, name); + return name; + } + + DbServerGetCharacterResponse response = null; + try + { + response = await _characterService.GetCharacterById(new DbServerGetCharacterByIdRequest + { + CharacterId = characterId + }); + } + catch (Exception e) + { + Log.Error("[BAZAAR_MANAGER][GetCharacterName] Unexpected error:", e); + } + + if (response?.RpcResponseType != RpcResponseType.SUCCESS) + { + return string.Empty; + } + + name = response.CharacterDto.Name; + SetName(characterId, name); + return name; + } + + public async Task GetBazaarItemById(long bazaarItemId) + { + BazaarItemResponse response = null; + try + { + response = await _bazaarService.GetBazaarItemById(new BazaarGetItemByIdRequest + { + BazaarItemId = bazaarItemId + }); + } + catch (Exception e) + { + Log.Error("[BAZAAR_MANAGER][GetBazaarItemById] Unexpected error:", e); + } + + if (response?.ResponseType != RpcResponseType.SUCCESS) + { + return null; + } + + BazaarItemDTO bazaarItemDto = response.BazaarItemDto; + return new BazaarItem(bazaarItemDto, _itemInstanceFactory.CreateItem(bazaarItemDto.ItemInstance), await GetOwnerName(bazaarItemDto.CharacterId)); + } + + public async Task> GetListedItemsByCharacterId(long characterId) + { + BazaarGetItemsByCharIdResponse response = null; + try + { + response = await _bazaarService.GetItemsByCharacterIdFromBazaar(new BazaarGetItemsByCharIdRequest + { + CharacterId = characterId + }); + } + catch (Exception e) + { + Log.Error("[BAZAAR_MANAGER][GetListedItemsByCharacterId]", e); + } + + if (response?.ResponseType != RpcResponseType.SUCCESS) + { + return null; + } + + return await GetInstance(response.BazaarItems); + } + + public async Task<(IReadOnlyCollection, RpcResponseType)> SearchBazaarItems(BazaarSearchContext bazaarSearchContext) + { + BazaarSearchBazaarItemsResponse response = null; + try + { + response = await _bazaarService.SearchBazaarItems(new BazaarSearchBazaarItemsRequest + { + BazaarSearchContext = bazaarSearchContext + }); + } + catch (Exception e) + { + Log.Error("[BAZAAR_MANAGER][GetListedItemsByCharacterId]", e); + } + + if (response == null) + { + return (null, RpcResponseType.UNKNOWN_ERROR); + } + + return (await GetInstance(response.BazaarItemDtos), response.ResponseType); + } + + private static string GetKey(long characterId) => $"bazaar:name:character-id:{characterId.ToString()}"; + + private void SetName(long characterId, string name) + { + _nameByKey.Set(GetKey(characterId), name, CacheLifeTime); + } + + private void RemoveName(long characterId) + { + _nameByKey.Remove(GetKey(characterId)); + } + + private async Task> GetInstance(IEnumerable bazaarItems) + { + if (bazaarItems == null) + { + return null; + } + + List bazaarInstances = new(); + foreach (BazaarItemDTO bazaarItemDto in bazaarItems) + { + bazaarInstances.Add(new BazaarItem(bazaarItemDto, _itemInstanceFactory.CreateItem(bazaarItemDto.ItemInstance), await GetOwnerName(bazaarItemDto.CharacterId))); + } + + return bazaarInstances; + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Bazaar/BazaarModuleExtensions.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Bazaar/BazaarModuleExtensions.cs new file mode 100644 index 0000000..44d85f9 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Bazaar/BazaarModuleExtensions.cs @@ -0,0 +1,18 @@ +using Microsoft.Extensions.DependencyInjection; +using PhoenixLib.Configuration; +using WingsEmu.Communication.gRPC.Extensions; +using WingsEmu.Game.Bazaar; +using WingsEmu.Game.Bazaar.Configuration; + +namespace WingsEmu.Plugins.BasicImplementations.Bazaar; + +public static class BazaarModuleExtensions +{ + public static void AddBazaarModule(this IServiceCollection services) + { + services.AddGrpcBazaarServiceClient(); + + services.AddFileConfiguration(); + services.AddSingleton(); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Bazaar/BazaarOpenUiEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Bazaar/BazaarOpenUiEventHandler.cs new file mode 100644 index 0000000..52a85b0 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Bazaar/BazaarOpenUiEventHandler.cs @@ -0,0 +1,44 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.DTOs.Bonus; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Bazaar.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Maps; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.Bazaar; + +public class BazaarOpenUiEventHandler : IAsyncEventProcessor +{ + private readonly IGameLanguageService _languageService; + + public BazaarOpenUiEventHandler(IGameLanguageService languageService) => _languageService = languageService; + + public async Task HandleAsync(BazaarOpenUiEvent e, CancellationToken cancellation) + { + if (e.Sender.CurrentMapInstance?.MapInstanceType == MapInstanceType.TimeSpaceInstance) + { + return; + } + + CharacterStaticBonusDto medalBonus = + e.Sender.PlayerEntity.GetStaticBonus(x => x.StaticBonusType == StaticBonusType.BazaarMedalGold || x.StaticBonusType == StaticBonusType.BazaarMedalSilver); + + if (e.ThroughMedal && medalBonus == null) + { + e.Sender.SendInfo(_languageService.GetLanguage(GameDialogKey.BAZAAR_INFO_NEEDS_MEDAL, e.Sender.UserLanguage)); + return; + } + + MedalType medal = medalBonus == null ? MedalType.None : medalBonus.StaticBonusType == StaticBonusType.BazaarMedalGold ? MedalType.Gold : MedalType.Silver; + + int time = medalBonus == null ? 0 : medalBonus.DateEnd == null ? 999 : (int)(medalBonus.DateEnd.Value - DateTime.UtcNow).TotalHours; + + e.Sender.SendMsg(_languageService.GetLanguage(GameDialogKey.BAZAAR_SHOUTMESSAGE_NOTICE_BAZAAR, e.Sender.UserLanguage), MsgMessageType.Middle); + e.Sender.OpenNosBazaarUi(medal, time); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Bazaar/BazaarSearchItemsEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Bazaar/BazaarSearchItemsEventHandler.cs new file mode 100644 index 0000000..b56f6f8 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Bazaar/BazaarSearchItemsEventHandler.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Communication; +using WingsAPI.Communication.Bazaar; +using WingsAPI.Game.Extensions.PacketGeneration; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Algorithm; +using WingsEmu.Game.Bazaar; +using WingsEmu.Game.Bazaar.Configuration; +using WingsEmu.Game.Bazaar.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers.StaticData; + +namespace WingsEmu.Plugins.BasicImplementations.Bazaar; + +public class BazaarSearchItemsEventHandler : IAsyncEventProcessor +{ + private readonly BazaarConfiguration _bazaarConfiguration; + private readonly IBazaarManager _bazaarManager; + private readonly ICharacterAlgorithm _characterAlgorithm; + private readonly IItemsManager _itemsManager; + + public BazaarSearchItemsEventHandler(BazaarConfiguration bazaarConfiguration, IBazaarManager bazaarManager, IItemsManager itemsManager, ICharacterAlgorithm characterAlgorithm) + { + _bazaarConfiguration = bazaarConfiguration; + _bazaarManager = bazaarManager; + _itemsManager = itemsManager; + _characterAlgorithm = characterAlgorithm; + } + + public async Task HandleAsync(BazaarSearchItemsEvent e, CancellationToken cancellation) + { + (IReadOnlyCollection items, RpcResponseType rpcResponseType) = await _bazaarManager.SearchBazaarItems(new BazaarSearchContext + { + Index = e.Index, + AmountOfItemsPerIndex = _bazaarConfiguration.MaximumListedItems, + CategoryFilterType = e.CategoryFilterType, + ItemVNumFilter = e.ItemVNumFilter, + LevelFilter = e.LevelFilter, + OrderFilter = e.OrderFilter, + RareFilter = e.RareFilter, + SubTypeFilter = e.SubTypeFilter, + UpgradeFilter = e.UpgradeFilter + }); + + if (rpcResponseType != RpcResponseType.SUCCESS) + { + if (rpcResponseType != RpcResponseType.MAINTENANCE_MODE) + { + return; + } + + e.Sender.SendInfo(e.Sender.GetLanguage(GameDialogKey.BAZAAR_INFO_MAINTENANCE_MODE)); + return; + } + + e.Sender.SendSearchResponseBazaarItems(e.Index, items, _itemsManager, _characterAlgorithm, _bazaarConfiguration); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/CharacterLifetimeStats/TotalDamageDealtEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/CharacterLifetimeStats/TotalDamageDealtEventHandler.cs new file mode 100644 index 0000000..5c365ef --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/CharacterLifetimeStats/TotalDamageDealtEventHandler.cs @@ -0,0 +1,36 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Characters; + +namespace WingsEmu.Plugins.BasicImplementations.CharacterLifetimeStats; + +public class TotalDamageDealtEventHandler : IAsyncEventProcessor +{ + private readonly ISkillUsageManager _skillUsageManager; + + public TotalDamageDealtEventHandler(ISkillUsageManager skillUsageManager) => _skillUsageManager = skillUsageManager; + + public async Task HandleAsync(ApplyHitEvent e, CancellationToken cancellation) + { + if (e.HitInformation.Caster is not IPlayerEntity playerEntity) + { + return; + } + + SkillInfo skill = e.HitInformation.Skill; + long totalDamage = e.ProcessResults.Damages; + + if (skill.Combos.Any()) + { + ComboState comboState = _skillUsageManager.GetComboState(e.HitInformation.Caster.Id, e.Target.Id); + double increaseDamageByComboState = 0.05 + 0.1 * comboState.Hit; + + totalDamage += (int)(totalDamage * increaseDamageByComboState); + } + + playerEntity.LifetimeStats.TotalDamageDealt += totalDamage; + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/CharacterLifetimeStats/TotalDeathsEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/CharacterLifetimeStats/TotalDeathsEventHandler.cs new file mode 100644 index 0000000..6fb83b7 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/CharacterLifetimeStats/TotalDeathsEventHandler.cs @@ -0,0 +1,38 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Mates; + +namespace WingsEmu.Plugins.BasicImplementations.CharacterLifetimeStats; + +public class TotalDeathsEventHandler : IAsyncEventProcessor +{ + public async Task HandleAsync(PlayerDeathEvent e, CancellationToken cancellation) + { + IPlayerEntity character = e.Sender.PlayerEntity; + + if (character == null || character.IsAlive()) + { + return; + } + + switch (e.Killer) + { + case IPlayerEntity killerCharacter: + killerCharacter.LifetimeStats.TotalPlayersKilled++; + character.LifetimeStats.TotalDeathsByPlayer++; + break; + case IMonsterEntity: + character.LifetimeStats.TotalDeathsByMonster++; + break; + case IMateEntity mate: + mate.Owner.LifetimeStats.TotalPlayersKilled++; + character.LifetimeStats.TotalDeathsByPlayer++; + break; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/CharacterLifetimeStats/TotalGoldDroppedEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/CharacterLifetimeStats/TotalGoldDroppedEventHandler.cs new file mode 100644 index 0000000..0ddeaa5 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/CharacterLifetimeStats/TotalGoldDroppedEventHandler.cs @@ -0,0 +1,22 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game._enum; +using WingsEmu.Game.Inventory.Event; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Plugins.BasicImplementations.CharacterLifetimeStats; + +public class TotalGoldDroppedEventHandler : IAsyncEventProcessor +{ + public async Task HandleAsync(InventoryPickedUpItemEvent e, CancellationToken cancellation) + { + if (e.ItemVnum != (int)ItemVnums.GOLD) + { + return; + } + + IClientSession session = e.Sender; + session.PlayerEntity.LifetimeStats.TotalGoldDropped += e.Amount; + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/CharacterLifetimeStats/TotalGoldEarnedInBazaarItemsEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/CharacterLifetimeStats/TotalGoldEarnedInBazaarItemsEventHandler.cs new file mode 100644 index 0000000..f8f847c --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/CharacterLifetimeStats/TotalGoldEarnedInBazaarItemsEventHandler.cs @@ -0,0 +1,16 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Bazaar.Events; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Plugins.BasicImplementations.CharacterLifetimeStats; + +public class TotalGoldEarnedInBazaarItemsEventHandler : IAsyncEventProcessor +{ + public async Task HandleAsync(BazaarItemWithdrawnEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + session.PlayerEntity.LifetimeStats.TotalGoldEarnedInBazaarItems += e.Price * e.Quantity; + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/CharacterLifetimeStats/TotalGoldSpentInBazaarFeesEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/CharacterLifetimeStats/TotalGoldSpentInBazaarFeesEventHandler.cs new file mode 100644 index 0000000..b9efe3f --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/CharacterLifetimeStats/TotalGoldSpentInBazaarFeesEventHandler.cs @@ -0,0 +1,17 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Bazaar.Events; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Plugins.BasicImplementations.CharacterLifetimeStats; + +public class TotalGoldSpentInBazaarFeesEventHandler : IAsyncEventProcessor +{ + public async Task HandleAsync(BazaarItemAddedEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + session.PlayerEntity.LifetimeStats.TotalGoldSpentInBazaarFees += e.Tax; + session.PlayerEntity.LifetimeStats.TotalGoldSpent += e.Tax; + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/CharacterLifetimeStats/TotalGoldSpentInBazaarItemsEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/CharacterLifetimeStats/TotalGoldSpentInBazaarItemsEventHandler.cs new file mode 100644 index 0000000..ae268ca --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/CharacterLifetimeStats/TotalGoldSpentInBazaarItemsEventHandler.cs @@ -0,0 +1,17 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Bazaar.Events; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Plugins.BasicImplementations.CharacterLifetimeStats; + +public class TotalGoldSpentInBazaarItemsEventHandler : IAsyncEventProcessor +{ + public async Task HandleAsync(BazaarItemBoughtEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + session.PlayerEntity.LifetimeStats.TotalGoldSpentInBazaarItems += e.Amount * e.PricePerItem; + session.PlayerEntity.LifetimeStats.TotalGoldSpent += e.Amount * e.PricePerItem; + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/CharacterLifetimeStats/TotalGoldSpentInNpcShopEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/CharacterLifetimeStats/TotalGoldSpentInNpcShopEventHandler.cs new file mode 100644 index 0000000..6fe916f --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/CharacterLifetimeStats/TotalGoldSpentInNpcShopEventHandler.cs @@ -0,0 +1,17 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Shops.Event; + +namespace WingsEmu.Plugins.BasicImplementations.CharacterLifetimeStats; + +public class TotalGoldSpentInNpcShopEventHandler : IAsyncEventProcessor +{ + public async Task HandleAsync(ShopNpcBoughtItemEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + session.PlayerEntity.LifetimeStats.TotalGoldSpentInNpcShop += e.TotalPrice; + session.PlayerEntity.LifetimeStats.TotalGoldSpent += e.TotalPrice; + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/CharacterLifetimeStats/TotalInstantBattleWonEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/CharacterLifetimeStats/TotalInstantBattleWonEventHandler.cs new file mode 100644 index 0000000..89da612 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/CharacterLifetimeStats/TotalInstantBattleWonEventHandler.cs @@ -0,0 +1,16 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.GameEvent.InstantBattle; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Plugins.BasicImplementations.CharacterLifetimeStats; + +public class TotalInstantBattleWonEventHandler : IAsyncEventProcessor +{ + public async Task HandleAsync(InstantBattleWonEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + session.PlayerEntity.LifetimeStats.TotalInstantBattleWon++; + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/CharacterLifetimeStats/TotalItemsUsedEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/CharacterLifetimeStats/TotalItemsUsedEventHandler.cs new file mode 100644 index 0000000..1712d6d --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/CharacterLifetimeStats/TotalItemsUsedEventHandler.cs @@ -0,0 +1,31 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Core.ItemHandling.Event; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.CharacterLifetimeStats; + +public class TotalItemsUsedEventHandler : IAsyncEventProcessor +{ + public async Task HandleAsync(InventoryUsedItemEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + + switch (e.Item.ItemInstance.GameItem.ItemType) + { + case ItemType.Potion: + session.PlayerEntity.LifetimeStats.TotalPotionsUsed++; + break; + case ItemType.Food: + session.PlayerEntity.LifetimeStats.TotalFoodUsed++; + break; + case ItemType.Snack: + session.PlayerEntity.LifetimeStats.TotalSnacksUsed++; + break; + } + + session.PlayerEntity.LifetimeStats.TotalItemsUsed++; + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/CharacterLifetimeStats/TotalMonstersKilledEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/CharacterLifetimeStats/TotalMonstersKilledEventHandler.cs new file mode 100644 index 0000000..56413f1 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/CharacterLifetimeStats/TotalMonstersKilledEventHandler.cs @@ -0,0 +1,47 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Monster.Event; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.CharacterLifetimeStats; + +public class TotalMonstersKilledEventHandler : IAsyncEventProcessor +{ + public async Task HandleAsync(MonsterDeathEvent e, CancellationToken cancellation) + { + switch (e.Killer) + { + case IPlayerEntity player: + player.LifetimeStats.TotalMonstersKilled++; + break; + + case IMateEntity mate: + mate.Owner.LifetimeStats.TotalMonstersKilled++; + break; + case IMonsterEntity monster: + if (!monster.SummonerId.HasValue || monster.IsMateTrainer) + { + break; + } + + if (monster.SummonerType != VisualType.Player) + { + break; + } + + IClientSession summoner = monster.MapInstance.GetCharacterById(monster.SummonerId.Value)?.Session; + if (summoner == null) + { + break; + } + + summoner.PlayerEntity.LifetimeStats.TotalMonstersKilled++; + break; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/CharacterLifetimeStats/TotalRaidsLostEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/CharacterLifetimeStats/TotalRaidsLostEventHandler.cs new file mode 100644 index 0000000..7f2b9de --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/CharacterLifetimeStats/TotalRaidsLostEventHandler.cs @@ -0,0 +1,16 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Raids.Events; + +namespace WingsEmu.Plugins.BasicImplementations.CharacterLifetimeStats; + +public class TotalRaidsLostEventHandler : IAsyncEventProcessor +{ + public async Task HandleAsync(RaidLostEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + session.PlayerEntity.LifetimeStats.TotalRaidsLost++; + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/CharacterLifetimeStats/TotalRaidsWonEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/CharacterLifetimeStats/TotalRaidsWonEventHandler.cs new file mode 100644 index 0000000..7afea81 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/CharacterLifetimeStats/TotalRaidsWonEventHandler.cs @@ -0,0 +1,16 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Raids.Events; + +namespace WingsEmu.Plugins.BasicImplementations.CharacterLifetimeStats; + +public class TotalRaidsWonEventHandler : IAsyncEventProcessor +{ + public async Task HandleAsync(RaidWonEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + session.PlayerEntity.LifetimeStats.TotalRaidsWon++; + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/CharacterLifetimeStats/TotalSkillsCastedEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/CharacterLifetimeStats/TotalSkillsCastedEventHandler.cs new file mode 100644 index 0000000..06d4d2a --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/CharacterLifetimeStats/TotalSkillsCastedEventHandler.cs @@ -0,0 +1,20 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Characters; + +namespace WingsEmu.Plugins.BasicImplementations.CharacterLifetimeStats; + +public class TotalSkillsCastedEventHandler : IAsyncEventProcessor +{ + public async Task HandleAsync(BattleExecuteSkillEvent e, CancellationToken cancellation) + { + if (e.Entity is not IPlayerEntity playerEntity) + { + return; + } + + playerEntity.LifetimeStats.TotalSkillsCasted++; + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Chat/ChatSendFriendMessageEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Chat/ChatSendFriendMessageEventHandler.cs new file mode 100644 index 0000000..19954ac --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Chat/ChatSendFriendMessageEventHandler.cs @@ -0,0 +1,102 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using PhoenixLib.ServiceBus; +using WingsAPI.Game.Extensions.RelationsExtensions; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Chat; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums.Chat; +using WingsEmu.Plugins.DistributedGameEvents.Relation; +using ChatType = WingsEmu.Game._playerActionLogs.ChatType; + +namespace WingsEmu.Plugins.BasicImplementations.Chat; + +public class ChatSendFriendMessageEventHandler : IAsyncEventProcessor +{ + private readonly IGameLanguageService _gameLanguage; + private readonly IMessagePublisher _messagePublisher; + private readonly ISessionManager _sessionManager; + + public ChatSendFriendMessageEventHandler(ISessionManager sessionManager, IGameLanguageService gameLanguage, IMessagePublisher messagePublisher) + { + _sessionManager = sessionManager; + _gameLanguage = gameLanguage; + _messagePublisher = messagePublisher; + } + + public async Task HandleAsync(ChatSendFriendMessageEvent e, CancellationToken cancellation) + { + long targetId = e.TargetId; + string message = e.Message; + IClientSession session = e.Sender; + + if (string.IsNullOrEmpty(message)) + { + return; + } + + if (session.PlayerEntity.Id == targetId) + { + return; + } + + if (message.Length > 60) + { + message = message.Substring(0, 60); + } + + message = message.Trim(); + + if (!session.PlayerEntity.IsFriend(targetId) && !session.PlayerEntity.IsMarried(targetId)) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.FRIEND_MESSAGE_NOT_FRIEND, session.UserLanguage), MsgMessageType.Middle); + return; + } + + IClientSession target = _sessionManager.GetSessionByCharacterId(targetId); + if (target == null) + { + if (!_sessionManager.IsOnline(targetId)) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.INFORMATION_MESSAGE_USER_NOT_CONNECTED, session.UserLanguage), MsgMessageType.Middle); + return; + } + + await _messagePublisher.PublishAsync(new RelationSendTalkMessage + { + Message = message, + TargetId = targetId, + SenderId = session.PlayerEntity.Id + }); + await session.EmitEventAsync(new ChatGenericEvent + { + Message = e.Message, + ChatType = ChatType.FriendChat, + TargetCharacterId = targetId + }); + return; + } + + if (session.CurrentMapInstance.HasMapFlag(MapFlags.ACT_4) && target.CurrentMapInstance.HasMapFlag(MapFlags.ACT_4)) + { + if (session.PlayerEntity.Faction != target.PlayerEntity.Faction) + { + message = _gameLanguage.GetLanguage(GameDialogKey.FRIEND_TALKMESSAGE_DIFFRENT_FACTION, session.UserLanguage); + session.SendFriendMessage(targetId, message); + return; + } + } + + await session.EmitEventAsync(new ChatGenericEvent + { + Message = e.Message, + ChatType = ChatType.FriendChat, + TargetCharacterId = targetId + }); + target.SendFriendMessage(session.PlayerEntity.Id, message); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Chat/ChatSpeakerEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Chat/ChatSpeakerEventHandler.cs new file mode 100644 index 0000000..f199171 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Chat/ChatSpeakerEventHandler.cs @@ -0,0 +1,101 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Algorithm; +using WingsEmu.Game.Chat; +using WingsEmu.Game.Entities.Extensions; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Networking.Broadcasting; +using WingsEmu.Packets.Enums.Chat; +using ChatType = WingsEmu.Game._playerActionLogs.ChatType; + +namespace WingsEmu.Plugins.BasicImplementations.Chat; + +public class ChatSpeakerEventHandler : IAsyncEventProcessor +{ + private readonly ICharacterAlgorithm _algorithm; + private readonly IItemsManager _itemsManager; + private readonly IGameLanguageService _languageService; + private readonly ISessionManager _sessionManager; + + public ChatSpeakerEventHandler(IGameLanguageService languageService, ISessionManager sessionManager, IItemsManager itemsManager, ICharacterAlgorithm algorithm) + { + _languageService = languageService; + _sessionManager = sessionManager; + _itemsManager = itemsManager; + _algorithm = algorithm; + } + + public async Task HandleAsync(ChatSpeakerEvent e, CancellationToken cancellation) + { + IClientSession sender = e.Sender; + GameItemInstance item = e.Item; + SpeakerType chatSpeakerType = e.ChatSpeakerType; + + switch (chatSpeakerType) + { + case SpeakerType.Normal_Speaker: + if (sender.CurrentMapInstance.HasMapFlag(MapFlags.ACT_4)) + { + _sessionManager.Broadcast(s => s.PlayerEntity.GenerateSayPacket(GenerateLanguageMessage(e, s), ChatMessageColorType.LightPurple), + new SpeakerHeroBroadcast(), new FactionBroadcast(sender.PlayerEntity.Faction), new ExpectBlockedPlayerBroadcast(sender.PlayerEntity.Id)); + } + else + { + _sessionManager.Broadcast(s => s.PlayerEntity.GenerateSayPacket(GenerateLanguageMessage(e, s), ChatMessageColorType.LightPurple), + new SpeakerHeroBroadcast(), new ExpectBlockedPlayerBroadcast(sender.PlayerEntity.Id)); + } + + break; + case SpeakerType.Items_Speaker: + + string speakerName = sender.GenerateItemSpeaker(item, e.Message, _itemsManager, _algorithm); + + if (sender.CurrentMapInstance.HasMapFlag(MapFlags.ACT_4)) + { + _sessionManager.Broadcast(s => speakerName, new SpeakerHeroBroadcast(), + new FactionBroadcast(sender.PlayerEntity.Faction), new ExpectBlockedPlayerBroadcast(sender.PlayerEntity.Id)); + } + else + { + _sessionManager.Broadcast(s => speakerName, + new SpeakerHeroBroadcast(), new ExpectBlockedPlayerBroadcast(sender.PlayerEntity.Id)); + } + + break; + default: + return; + } + + await sender.EmitEventAsync(new ChatGenericEvent + { + Message = (e.Message.Length > 120 ? e.Message.Substring(0, 120) : e.Message).Trim(), + ChatType = ChatType.Shout + }); + } + + private string GenerateLanguageMessage(ChatSpeakerEvent e, IClientSession recv) + { + IClientSession sender = e.Sender; + GameItemInstance item = e.Item; + SpeakerType chatSpeakerType = e.ChatSpeakerType; + string message = e.Message; + message = message.Trim(); + + string messageHeader = $"<{_languageService.GetLanguage(GameDialogKey.SPEAKER_NAME, recv.UserLanguage)}>"; + messageHeader += chatSpeakerType == SpeakerType.Normal_Speaker ? $" [{sender.PlayerEntity.Name}]: " : $"|[{sender.PlayerEntity.Name}]:|"; // Weird packet handling + message = messageHeader + message; + if (message.Length > 120) + { + message = message.Substring(0, 120); + } + + return message; + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Compliments/ComplimentsManager.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Compliments/ComplimentsManager.cs new file mode 100644 index 0000000..c363d5e --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Compliments/ComplimentsManager.cs @@ -0,0 +1,21 @@ +using System; +using System.Threading.Tasks; +using PhoenixLib.DAL.Redis.Locks; +using WingsEmu.Game.Compliments; + +namespace WingsEmu.Plugins.BasicImplementations.Compliments; + +public class ComplimentsManager : IComplimentsManager +{ + private readonly IExpirableLockService _expirableLock; + + public ComplimentsManager(IExpirableLockService expirableLock) => _expirableLock = expirableLock; + + public async Task CanRefresh(long characterId) + { + DateTime nextMonth = DateTime.UtcNow.Date.AddMonths(1).AddDays(-DateTime.UtcNow.Date.Day + 1); + return await _expirableLock.TryAddTemporaryLockAsync($"game:locks:compliments-monthly-refresh:{characterId}", nextMonth); + } + + public async Task CanCompliment(long accountId) => await _expirableLock.TryAddTemporaryLockAsync($"game:locks:compliments-usage:{accountId}", DateTime.UtcNow.Date.AddDays(1)); +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Compliments/ComplimentsMonthlyRefreshEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Compliments/ComplimentsMonthlyRefreshEventHandler.cs new file mode 100644 index 0000000..436e7ad --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Compliments/ComplimentsMonthlyRefreshEventHandler.cs @@ -0,0 +1,28 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Compliments; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Plugins.BasicImplementations.General; + +public class ComplimentsMonthlyRefreshEventHandler : IAsyncEventProcessor +{ + private readonly IComplimentsManager _complimentsManager; + + public ComplimentsMonthlyRefreshEventHandler(IComplimentsManager complimentsManager) => _complimentsManager = complimentsManager; + + public async Task HandleAsync(ComplimentsMonthlyRefreshEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + + bool canRefresh = await _complimentsManager.CanRefresh(session.PlayerEntity.Id); + if (!canRefresh && !e.Force) + { + return; + } + + session.PlayerEntity.Compliment = 0; + // TODO: Compliment rewards + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/DbServer/CharacterSaveSystem.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/DbServer/CharacterSaveSystem.cs new file mode 100644 index 0000000..c35bb8b --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/DbServer/CharacterSaveSystem.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using PhoenixLib.Logging; +using Polly; +using Polly.Retry; +using WingsAPI.Communication; +using WingsAPI.Communication.DbServer.CharacterService; +using WingsAPI.Data.Character; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Plugins.BasicImplementations.DbServer; + +public class CharacterSaveSystem : BackgroundService +{ + private static readonly TimeSpan Interval = TimeSpan.FromSeconds(Convert.ToUInt32(Environment.GetEnvironmentVariable("CHARACTER_SAVE_SYSTEM_INTERVAL_SECONDS") ?? "15")); + private readonly ICharacterService _characterService; + private readonly IPlayerEntityFactory _playerEntityFactory; + private readonly ISessionManager _sessionManager; + + public CharacterSaveSystem(ISessionManager sessionManager, IPlayerEntityFactory playerEntityFactory, ICharacterService characterService) + { + _sessionManager = sessionManager; + _playerEntityFactory = playerEntityFactory; + _characterService = characterService; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + Log.Debug("[CHARACTER_SAVE_SYSTEM] Started"); + while (!stoppingToken.IsCancellationRequested) + { + try + { + await ProcessSaves(); + } + catch (Exception e) + { + Log.Error("[CHARACTER_SAVE_SYSTEM]", e); + } + + await Task.Delay(Interval, stoppingToken); + } + } + + private static IEnumerable> SplitList(List locations, int nSize = 30) + { + for (int i = 0; i < locations.Count; i += nSize) + { + yield return locations.GetRange(i, Math.Min(nSize, locations.Count - i)); + } + } + + private async Task ProcessSaves() + { + IReadOnlyList sessions = _sessionManager.Sessions; + if (sessions.Count < 1) + { + Log.Debug("[CHARACTER_SAVE_SYSTEM] No characters to save"); + return; + } + + var charactersToSave = new List(); + foreach (IClientSession session in sessions) + { + if (session.PlayerEntity == null) + { + continue; + } + + charactersToSave.Add(_playerEntityFactory.CreateCharacterDto(session.PlayerEntity)); + } + + IEnumerable> toSave = SplitList(charactersToSave, 50); + + AsyncRetryPolicy policy = Policy.Handle().RetryAsync(3, (exception, i1) => Log.Error($"[CHARACTER_SAVE_SYSTEM] Failed to save characters, try {i1.ToString()}. ", exception)); + + int i = 0; + foreach (List chunkToSave in toSave) + { + i += chunkToSave.Count; + Log.Warn($"[CHARACTER_SAVE_SYSTEM] Saving chunk of {i}/{charactersToSave.Count.ToString()}"); + try + { + await policy.ExecuteAsync(() => TrySave(chunkToSave)); + } + catch (Exception e) + { + Log.Error($"[CHARACTER_SAVE_SYSTEM] Failed to save chunk of {i}/{charactersToSave.Count.ToString()}", e); + } + } + } + + private async Task TrySave(List chunkToSave) + { + DbServerSaveCharactersResponse response = await _characterService.SaveCharacters(new DbServerSaveCharactersRequest + { + Characters = chunkToSave + }); + + if (response.RpcResponseType == RpcResponseType.SUCCESS) + { + Log.Info($"[CHARACTER_SAVE_SYSTEM] Saved {chunkToSave.Count.ToString()} characters"); + return; + } + + Log.Warn("[CHARACTER_SAVE_SYSTEM] The saves couldn't be saved, will be saved on next loop"); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/DbServer/DbServerModuleExtensions.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/DbServer/DbServerModuleExtensions.cs new file mode 100644 index 0000000..81c8194 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/DbServer/DbServerModuleExtensions.cs @@ -0,0 +1,14 @@ +using Microsoft.Extensions.DependencyInjection; +using WingsEmu.Communication.gRPC.Extensions; + +namespace WingsEmu.Plugins.BasicImplementations.DbServer; + +public static class DbServerModuleExtensions +{ + public static void AddDbServerModule(this IServiceCollection services) + { + services.AddGrpcDbServerServiceClient(); + + services.AddHostedService(); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Entities/GenerateEntityDeathEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Entities/GenerateEntityDeathEventHandler.cs new file mode 100644 index 0000000..10a76ac --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Entities/GenerateEntityDeathEventHandler.cs @@ -0,0 +1,104 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Packets.Enums.Shells; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Entities.Event; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Mates.Events; +using WingsEmu.Game.Monster.Event; +using WingsEmu.Game.Npcs.Event; + +namespace WingsEmu.Plugins.BasicImplementations.Entities; + +public class GenerateEntityDeathEventHandler : IAsyncEventProcessor +{ + private readonly IAsyncEventPipeline _eventPipeline; + + public GenerateEntityDeathEventHandler(IAsyncEventPipeline eventPipeline) => _eventPipeline = eventPipeline; + + public async Task HandleAsync(GenerateEntityDeathEvent e, CancellationToken cancellation) + { + IBattleEntity defender = e.Entity; + IBattleEntity attacker = e.Attacker; + + defender.Hp = 0; + switch (defender) + { + case IPlayerEntity character: + await character.Session.EmitEventAsync(new PlayerDeathEvent + { + Killer = attacker + }); + break; + case IMateEntity mate: + await mate.Owner.Session.EmitEventAsync(new MateDeathEvent(attacker, mate)); + break; + case INpcEntity npc: + await _eventPipeline.ProcessEventAsync(new MapNpcGenerateDeathEvent(npc, attacker)); + break; + case IMonsterEntity monster: + await _eventPipeline.ProcessEventAsync(new MonsterDeathEvent(monster) + { + Killer = attacker + }); + break; + } + + if (e.IsByMainWeapon is null) + { + return; + } + + if (attacker is not IPlayerEntity playerEntity) + { + return; + } + + if (!playerEntity.IsAlive()) + { + return; + } + + if (HasLevelPenalty(attacker, defender)) + { + return; + } + + int hpHeal = playerEntity.GetMaxWeaponShellValue(ShellEffectType.HPRecoveryForKilling, e.IsByMainWeapon.Value); + + if (hpHeal != 0) + { + await playerEntity.EmitEventAsync(new BattleEntityHealEvent + { + Entity = playerEntity, + HpHeal = hpHeal + }); + } + + int mpHeal = playerEntity.GetMaxWeaponShellValue(ShellEffectType.MPRecoveryForKilling, e.IsByMainWeapon.Value); + if (mpHeal == 0) + { + return; + } + + await playerEntity.EmitEventAsync(new BattleEntityHealEvent + { + Entity = playerEntity, + MpHeal = mpHeal + }); + } + + private bool HasLevelPenalty(IBattleEntity attacker, IBattleEntity target) + { + if (target.Level >= 75) + { + return false; + } + + return attacker.Level - target.Level > 15; + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Entities/MapJoinMonsterEntityEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Entities/MapJoinMonsterEntityEventHandler.cs new file mode 100644 index 0000000..6f2fe1a --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Entities/MapJoinMonsterEntityEventHandler.cs @@ -0,0 +1,76 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Entities.Event; +using WingsEmu.Game.Entities.Extensions; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Monster.Event; + +namespace WingsEmu.Plugins.BasicImplementations.Entities; + +public class MapJoinMonsterEntityEventHandler : IAsyncEventProcessor +{ + private readonly IAsyncEventPipeline _asyncEventPipeline; + private readonly IRandomGenerator _randomGenerator; + + public MapJoinMonsterEntityEventHandler(IRandomGenerator randomGenerator, IAsyncEventPipeline asyncEventPipeline) + { + _randomGenerator = randomGenerator; + _asyncEventPipeline = asyncEventPipeline; + } + + public async Task HandleAsync(MapJoinMonsterEntityEvent e, CancellationToken cancellation) + { + IMonsterEntity monsterEntity = e.MonsterEntity; + + short x = e.MapX ?? monsterEntity.PositionX; + short y = e.MapY ?? monsterEntity.PositionY; + + if (monsterEntity.MapInstance.IsBlockedZone(x, y)) + { + Position randomPosition = monsterEntity.MapInstance.GetRandomPosition(); + x = randomPosition.X; + y = randomPosition.Y; + } + + monsterEntity.ChangePosition(new Position(x, y)); + monsterEntity.FirstX = x; + monsterEntity.FirstY = y; + monsterEntity.NextTick = DateTime.UtcNow; + monsterEntity.NextAttackReady = DateTime.UtcNow; + monsterEntity.OnFirstDamageReceive = true; + + monsterEntity.NextTick += TimeSpan.FromMilliseconds(_randomGenerator.RandomNumber(1000)); + + monsterEntity.MapInstance.AddMonster(monsterEntity); + monsterEntity.ModeDeathsSinceRespawn = monsterEntity.MapInstance.MonsterDeathsOnMap(); + + if (monsterEntity.ModeIsHpTriggered) + { + monsterEntity.ModeIsActive = false; + } + else + { + monsterEntity.MapInstance.ActivateMode(monsterEntity); + } + + if (!monsterEntity.IsStillAlive || !monsterEntity.IsAlive()) + { + monsterEntity.IsStillAlive = true; + monsterEntity.Hp = monsterEntity.MaxHp; + monsterEntity.Mp = monsterEntity.MaxMp; + } + + monsterEntity.MapInstance.Broadcast(monsterEntity.GenerateIn(e.ShowEffect)); + + await _asyncEventPipeline.ProcessEventAsync(new MonsterRespawnedEvent + { + Monster = monsterEntity + }); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Entities/MapJoinNpcEntityEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Entities/MapJoinNpcEntityEventHandler.cs new file mode 100644 index 0000000..663bc3e --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Entities/MapJoinNpcEntityEventHandler.cs @@ -0,0 +1,40 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Entities.Event; +using WingsEmu.Game.Entities.Extensions; +using WingsEmu.Game.Helpers.Damages; + +namespace WingsEmu.Plugins.BasicImplementations.Entities; + +public class MapJoinNpcEntityEventHandler : IAsyncEventProcessor +{ + private readonly IRandomGenerator _randomGenerator; + + public MapJoinNpcEntityEventHandler(IRandomGenerator randomGenerator) => _randomGenerator = randomGenerator; + + public async Task HandleAsync(MapJoinNpcEntityEvent e, CancellationToken cancellation) + { + INpcEntity npcEntity = e.NpcEntity; + if (npcEntity.MapInstance != null) + { + // await npcEntity.EmitEventAsync(new MapLeaveNpcEntityEvent(npcEntity)); + } + + short x = e.MapX ?? npcEntity.PositionX; + short y = e.MapY ?? npcEntity.PositionY; + npcEntity.ChangePosition(new Position(x, y)); + npcEntity.FirstX = x; + npcEntity.FirstY = y; + npcEntity.NextTick = DateTime.UtcNow; + npcEntity.NextTick += TimeSpan.FromMilliseconds(_randomGenerator.RandomNumber(1000)); + npcEntity.NextAttackReady = DateTime.UtcNow; + + npcEntity.MapInstance?.AddNpc(npcEntity); + string inPacket = npcEntity.GenerateIn(); + npcEntity.MapInstance?.Broadcast(inPacket); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Entities/MapLeaveMonsterEntityEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Entities/MapLeaveMonsterEntityEventHandler.cs new file mode 100644 index 0000000..55542d6 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Entities/MapLeaveMonsterEntityEventHandler.cs @@ -0,0 +1,23 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Entities.Event; +using WingsEmu.Game.Entities.Extensions; +using WingsEmu.Game.Maps; + +namespace WingsEmu.Plugins.BasicImplementations.Entities; + +public class MapLeaveMonsterEntityEventHandler : IAsyncEventProcessor +{ + public async Task HandleAsync(MapLeaveMonsterEntityEvent e, CancellationToken cancellation) + { + IMapInstance mapInstance = e.MonsterEntity.MapInstance; + if (mapInstance == null) + { + return; + } + + mapInstance.Broadcast(e.MonsterEntity.GenerateOut()); + mapInstance.RemoveMonster(e.MonsterEntity); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Entities/MapLeaveNpcEntityEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Entities/MapLeaveNpcEntityEventHandler.cs new file mode 100644 index 0000000..999f020 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Entities/MapLeaveNpcEntityEventHandler.cs @@ -0,0 +1,23 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Entities.Event; +using WingsEmu.Game.Entities.Extensions; +using WingsEmu.Game.Maps; + +namespace WingsEmu.Plugins.BasicImplementations.Entities; + +public class MapLeaveNpcEntityEventHandler : IAsyncEventProcessor +{ + public async Task HandleAsync(MapLeaveNpcEntityEvent e, CancellationToken cancellation) + { + IMapInstance mapInstance = e.NpcEntity.MapInstance; + if (mapInstance == null) + { + return; + } + + mapInstance.Broadcast(e.NpcEntity.GenerateOut()); + mapInstance.RemoveNpc(e.NpcEntity); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Entities/ShopFactory.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Entities/ShopFactory.cs new file mode 100644 index 0000000..2613fa3 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Entities/ShopFactory.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using WingsAPI.Data.Shops; +using WingsEmu.DTOs.Shops; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Shops; + +namespace WingsEmu.Plugins.BasicImplementations.Entities; + +public class ShopFactory : IShopFactory +{ + private readonly List _emptyItemList = new(); + private readonly List _emptySkillList = new(); + + public ShopNpc CreateShop(ShopDTO shopDto) => new ShopNpc(shopDto.MapNpcId, (ShopNpcMenuType)shopDto.MenuType, shopDto.Name, shopDto.ShopType, shopDto.Items ?? _emptyItemList, + shopDto.Skills ?? _emptySkillList); +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Algorithm/GenerateExperienceEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Algorithm/GenerateExperienceEventHandler.cs new file mode 100644 index 0000000..248baba --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Algorithm/GenerateExperienceEventHandler.cs @@ -0,0 +1,514 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Algorithm; +using WingsEmu.Game.Algorithm.Events; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Groups; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Mates.Events; +using WingsEmu.Game.TimeSpaces; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Character; +using WingsEmu.Packets.Enums.Chat; +using WingsEmu.Plugins.BasicImplementations.Algorithms; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Algorithm; + +public class GenerateExperienceEventHandler : IAsyncEventProcessor +{ + private readonly ICharacterAlgorithm _characterAlgorithm; + private readonly IGameLanguageService _gameLanguageService; + private readonly IServerManager _serverManager; + private readonly ITimeSpaceManager _timeSpaceManager; + + public GenerateExperienceEventHandler(ICharacterAlgorithm characterAlgorithm, IServerManager serverManager, IGameLanguageService gameLanguageService, ITimeSpaceManager timeSpaceManager) + { + _characterAlgorithm = characterAlgorithm; + _serverManager = serverManager; + _gameLanguageService = gameLanguageService; + _timeSpaceManager = timeSpaceManager; + } + + public async Task HandleAsync(GenerateExperienceEvent e, CancellationToken cancellation) + { + IPlayerEntity character = e.Character; + IMonsterEntity monsterEntity = e.MonsterEntity; + long? monsterOwnerId = e.MonsterOwnerId; + + if (monsterEntity.IsAlive()) + { + return; + } + + if (monsterEntity.MapInstance.MapInstanceType == MapInstanceType.TimeSpaceInstance) + { + TimeSpaceParty timeSpace = _timeSpaceManager.GetTimeSpaceByMapInstanceId(monsterEntity.MapInstance.Id); + if (timeSpace == null) + { + return; + } + + if (timeSpace.IsEasyMode) + { + return; + } + } + + if (!character.IsInGroup()) + { + if (!character.IsAlive()) + { + return; + } + + ExperienceInfo singleExpInfo = new() + { + BeforeCalculationPlayerLevel = character.Level + }; + + ExcessExperience getExtraExp = character.GetMoreExperience(_serverManager); + getExtraExp.ExperienceInfo = singleExpInfo; + bool decreaseXp = monsterOwnerId.HasValue && monsterOwnerId.Value != character.Id; + + await ProcessExperience(character, getExtraExp, monsterEntity, decreaseXp, decreaseXp); + return; + } + + bool decrease = true; + PlayerGroup group = character.GetGroup(); + if (monsterOwnerId.HasValue) + { + if (group.Members.Any(entity => entity.Id == monsterOwnerId.Value)) + { + decrease = false; + } + } + + ExperienceInfo experienceInfo = new() + { + MembersLevel = (short)group.Members.Where(x => x.MapInstance?.Id == character.MapInstance?.Id).Sum(x => x.Level), + MembersOnMap = (byte)group.Members.Count(x => x.MapInstance?.Id == character.MapInstance?.Id) + }; + + foreach (IPlayerEntity member in character.GetGroup().Members) + { + if (!member.IsAlive()) + { + continue; + } + + if (member.MapInstance?.Id != character.MapInstance?.Id) + { + continue; + } + + ExcessExperience memberExtraXp = member.GetMoreExperience(_serverManager); + experienceInfo.BeforeCalculationPlayerLevel = member.Level; + memberExtraXp.ExperienceInfo = experienceInfo; + + if (monsterOwnerId.HasValue) + { + await ProcessExperience(member, memberExtraXp, monsterEntity, decrease, member.Id == character.Id); + continue; + } + + await ProcessExperience(member, memberExtraXp, monsterEntity); + } + } + + private async Task ProcessExperience(IPlayerEntity character, ExcessExperience getExtraExp, IMonsterEntity monsterEntity, bool decreaseXp = false, bool showMessage = false) + { + await ProcessExp(character, LevelType.Level, getExtraExp, monsterEntity, decreaseXp); + await ProcessExp(character, LevelType.JobLevel, getExtraExp, monsterEntity, decreaseXp); + await ProcessExp(character, LevelType.SpJobLevel, getExtraExp, monsterEntity, decreaseXp); + await ProcessExp(character, LevelType.Heroic, getExtraExp, monsterEntity, decreaseXp); + await ProcessExp(character, LevelType.LevelMate, getExtraExp, monsterEntity, decreaseXp); + await ProcessExp(character, LevelType.Fairy, getExtraExp, monsterEntity, decreaseXp); + + if (decreaseXp && showMessage && character.MapInstance.MapInstanceType == MapInstanceType.BaseMapInstance && + !character.MapInstance.HasMapFlag(MapFlags.ACT_4)) // yes, MapInstanceType, not MapFlag + { + character.Session.SendChatMessage(_gameLanguageService.GetLanguage(GameDialogKey.INTERACTION_CHATMESSAGE_XP_NOT_FIRST_HIT, character.Session.UserLanguage), ChatMessageColorType.Yellow); + } + } + + private async Task ProcessExp(IPlayerEntity character, LevelType level, ExcessExperience getExtraExp, IMonsterEntity monsterEntity, bool decreaseXp = false) + { + long experience = 0; + + int monsterXp = monsterEntity.Xp; + int monsterJobXp = monsterEntity.JobXp; + + switch (level) + { + case LevelType.Level: + experience = (long)(Math.Floor(Math.Floor(Math.Ceiling(monsterXp * getExtraExp.Mates) * getExtraExp.Level) * GetPenalty(character, false, monsterEntity, getExtraExp.ExperienceInfo)) * + getExtraExp.LowLevel); + break; + case LevelType.JobLevel: + experience = (long)(Math.Floor(Math.Floor(monsterJobXp * getExtraExp.JobLevel) * GetPenalty(character, true, monsterEntity, getExtraExp.ExperienceInfo)) * getExtraExp.LowJob); + break; + case LevelType.SpJobLevel: + experience = (long)(Math.Floor(Math.Floor(monsterJobXp * getExtraExp.JobSpLevel) * GetPenalty(character, true, monsterEntity, getExtraExp.ExperienceInfo)) * getExtraExp.LowJobSp); + break; + case LevelType.Heroic: + experience = (long)(Math.Floor( + Math.Floor(Math.Ceiling(monsterXp * getExtraExp.Mates) * getExtraExp.HeroLevel) * GetPenalty(character, false, monsterEntity, getExtraExp.ExperienceInfo)) * getExtraExp.LowLevel); + break; + case LevelType.LevelMate: + if (!character.MateComponent.GetMates().Any(x => x.IsTeamMember)) + { + break; + } + + foreach (IMateEntity mate in character.MateComponent.TeamMembers()) + { + if (!mate.IsAlive()) + { + continue; + } + + IMateEntity anotherMate = character.MateComponent.GetTeamMember(x => x.MateType != mate.MateType); + + switch (mate.MateType) + { + case MateType.Partner: + if (anotherMate != null && anotherMate.IsAlive()) + { + experience = (long)Math.Floor(Math.Floor(monsterXp * 0.2025 * getExtraExp.PartnerLevel) * GetPenalty(character, false, monsterEntity, getExtraExp.ExperienceInfo)); + } + else + { + experience = (long)Math.Floor(Math.Floor(monsterXp * 0.1875 * getExtraExp.PartnerLevel) * GetPenalty(character, false, monsterEntity, getExtraExp.ExperienceInfo)); + } + + break; + case MateType.Pet: + if (anotherMate != null && anotherMate.IsAlive()) + { + experience = (long)Math.Floor(Math.Floor(monsterXp * 0.0675 * getExtraExp.MatesLevel) * GetPenalty(character, false, monsterEntity, getExtraExp.ExperienceInfo)); + } + else + { + experience = (long)Math.Floor(Math.Floor(monsterXp * 0.055 * getExtraExp.MatesLevel) * GetPenalty(character, false, monsterEntity, getExtraExp.ExperienceInfo)); + } + + break; + } + + await character.Session.EmitEventAsync(new MateProcessExperienceEvent(mate, experience)); + } + + break; + } + + if (decreaseXp) + { + experience /= 3; + } + + if (monsterEntity.MapInstance.MapInstanceType == MapInstanceType.EventGameInstance && level != LevelType.Fairy) + { + experience /= 10; + } + + if (experience <= 0 && level != LevelType.Fairy) + { + return; + } + + await ProcessFinalExperience(character, level, experience, monsterEntity); + } + + private async Task ProcessFinalExperience(IPlayerEntity character, LevelType level, long experience, IMonsterEntity monsterEntity) + { + if (monsterEntity.SummonerId != 0 && monsterEntity.SummonerType == VisualType.Player) + { + return; + } + + long neededExperienceToLevel; + switch (level) + { + case LevelType.Level: + if (character.Level >= _serverManager.MaxLevel) + { + return; + } + + neededExperienceToLevel = _characterAlgorithm.GetLevelXp(character.Level); + + if (character.Level <= 20) + { + // every 10% generate full HP/MP + if ((int)(character.LevelXp / (neededExperienceToLevel / 10)) < (int)((character.LevelXp + experience) / (neededExperienceToLevel / 10))) + { + character.Hp = character.MaxHp; + character.Mp = character.MaxMp; + character.Session.RefreshStat(); + character.Session.SendEffect(EffectType.ShinyStars); + } + } + + character.LevelXp += experience; + if (character.LevelXp < neededExperienceToLevel) + { + break; + } + + await character.Session.EmitEventAsync(new LevelUpEvent + { + LevelType = LevelType.Level + }); + break; + case LevelType.JobLevel: + if (character.Class == ClassType.Adventurer) + { + if (character.JobLevel > 19) + { + break; + } + + neededExperienceToLevel = _characterAlgorithm.GetJobXp(character.JobLevel, true); + character.JobLevelXp += experience; + if (character.JobLevelXp < neededExperienceToLevel) + { + break; + } + + await character.Session.EmitEventAsync(new LevelUpEvent + { + LevelType = LevelType.JobLevel + }); + break; + } + + if (character.JobLevel >= _serverManager.MaxJobLevel) + { + break; + } + + neededExperienceToLevel = _characterAlgorithm.GetJobXp(character.JobLevel); + character.JobLevelXp += experience; + if (character.JobLevelXp < neededExperienceToLevel) + { + break; + } + + await character.Session.EmitEventAsync(new LevelUpEvent + { + LevelType = LevelType.JobLevel + }); + break; + case LevelType.SpJobLevel: + if (character.Specialist == null || !character.UseSp) + { + break; + } + + if (character.Specialist.SpLevel >= _serverManager.MaxSpLevel) + { + break; + } + + neededExperienceToLevel = _characterAlgorithm.GetSpecialistJobXp(character.Specialist.SpLevel, character.Specialist.IsFunSpecialist()); + character.Specialist.Xp += experience; + if (character.Specialist.Xp < neededExperienceToLevel) + { + break; + } + + await character.Session.EmitEventAsync(new LevelUpEvent + { + LevelType = LevelType.SpJobLevel, + ItemVnum = character.Specialist.ItemVNum + }); + break; + case LevelType.Heroic: + if (monsterEntity.MapInstance != null && (!monsterEntity.MapInstance.HasMapFlag(MapFlags.ACT_6_1) || monsterEntity.MapInstance.HasMapFlag(MapFlags.ACT_6_2))) + { + break; + } + + if (monsterEntity.VesselMonster) + { + break; + } + + if (character.HeroLevel == 0) + { + break; + } + + if (character.HeroLevel >= _serverManager.MaxHeroLevel) + { + break; + } + + neededExperienceToLevel = _characterAlgorithm.GetHeroLevelXp(character.HeroLevel); + character.HeroXp += experience; + if (character.HeroXp < neededExperienceToLevel) + { + break; + } + + await character.Session.EmitEventAsync(new LevelUpEvent + { + LevelType = LevelType.Heroic + }); + break; + case LevelType.Fairy: + GameItemInstance fairy = character.Fairy; + if (fairy == null) + { + return; + } + + if (fairy.ElementRate + fairy.GameItem.ElementRate >= fairy.GameItem.MaxElementRate || character.Level > monsterEntity.Level + 15) + { + return; + } + + fairy.Xp += (int)(_serverManager.FairyXpRate * + (1 + character.BCardComponent.GetAllBCardsInformation(BCardType.FairyXPIncrease, (byte)AdditionalTypes.FairyXPIncrease.IncreaseFairyXPPoints, character.Level).firstData * 0.01)); + int fairyXp = _characterAlgorithm.GetFairyXp((short)(fairy.ElementRate + fairy.GameItem.ElementRate)); + + if (fairy.Xp < fairyXp) + { + return; + } + + await character.Session.EmitEventAsync(new LevelUpEvent + { + LevelType = LevelType.Fairy, + ItemVnum = fairy.ItemVNum + }); + + + break; + } + + character.Session.RefreshLevel(_characterAlgorithm); + } + + private double GetPenalty(IPlayerEntity character, bool isJob, IMonsterEntity monsterEntity, ExperienceInfo experienceInfo) + { + int actHeroPenalty = character.MapInstance.HasMapFlag(MapFlags.ACT_6_1) ? 5 : 0; + bool isInGroup = character.IsInGroup(); + + double levelPenalty = 1; + double jobPenalty = 1; + double monsterPenalty; + int difference = character.Level - monsterEntity.Level; + + if (difference <= 5 + actHeroPenalty) + { + monsterPenalty = 1; + } + else if (difference == (6 + actHeroPenalty)) + { + monsterPenalty = 0.9; + } + else if (difference == (7 + actHeroPenalty)) + { + monsterPenalty = 0.7; + } + else if (difference == (8 + actHeroPenalty)) + { + monsterPenalty = 0.5; + } + else if (difference == (9 + actHeroPenalty)) + { + monsterPenalty = 0.3; + } + else + { + monsterPenalty = 0.1; + } + + int groupMembers = experienceInfo.MembersOnMap ?? 1; + + if (isInGroup && experienceInfo.MembersOnMap != null) + { + byte membersOnTheSameMap = experienceInfo.MembersOnMap.Value; + switch (membersOnTheSameMap) + { + case 2: + levelPenalty = 69.0 / 120.0; + jobPenalty = 75.0 / 120.0; + break; + case 3: + levelPenalty = 52.0 / 120.0; + jobPenalty = 60.0 / 120.0; + break; + } + } + + double groupLevel = experienceInfo.MembersLevel ?? experienceInfo.BeforeCalculationPlayerLevel; + + if (!isJob) + { + return levelPenalty / (groupLevel / (experienceInfo.BeforeCalculationPlayerLevel * groupMembers)) * monsterPenalty; + } + + if (monsterEntity.Level >= 70) + { + monsterPenalty = 1; + } + + return jobPenalty / (groupLevel / (experienceInfo.BeforeCalculationPlayerLevel * groupMembers)) * monsterPenalty; + } +} + +public class ExcessExperience +{ + public ExcessExperience(double level, double jobLevel, double jobSpLevel, double heroLevel, double matesLevel, double partnerLevel, double lowLevel, double lowJob, double lowJobSp, double mates) + { + Level = level; + JobLevel = jobLevel; + JobSpLevel = jobSpLevel; + HeroLevel = heroLevel; + MatesLevel = matesLevel; + PartnerLevel = partnerLevel; + LowLevel = lowLevel; + LowJob = lowJob; + LowJobSp = lowJobSp; + Mates = mates; + } + + public double Level { get; } + public double JobLevel { get; } + public double JobSpLevel { get; } + public double HeroLevel { get; } + public double MatesLevel { get; } + public double PartnerLevel { get; } + + public double LowLevel { get; } + public double LowJob { get; } + public double LowJobSp { get; } + public double Mates { get; } + + public ExperienceInfo ExperienceInfo { get; set; } +} + +public struct ExperienceInfo +{ + public byte BeforeCalculationPlayerLevel { get; set; } + + public short? MembersLevel { get; init; } + public byte? MembersOnMap { get; init; } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/AntiCheat/GameMasterNotifierStrangeBehaviorEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/AntiCheat/GameMasterNotifierStrangeBehaviorEventHandler.cs new file mode 100644 index 0000000..2da33de --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/AntiCheat/GameMasterNotifierStrangeBehaviorEventHandler.cs @@ -0,0 +1,22 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers; + +namespace WingsEmu.Plugins.BasicImplementations.Event.AntiCheat; + +public class GameMasterNotifierStrangeBehaviorEventHandler : IAsyncEventProcessor +{ + private readonly ISessionManager _sessionManager; + + public GameMasterNotifierStrangeBehaviorEventHandler(ISessionManager sessionManager) => _sessionManager = sessionManager; + + public async Task HandleAsync(StrangeBehaviorEvent e, CancellationToken cancellation) => + _sessionManager.BroadcastToGameMaster($"[STRANGE_BEHAVIOR][{e.Severity}] {e.Sender?.PlayerEntity?.Name ?? $"[NOT_INITIALIZED Account: {e.Sender?.Account?.Id}]"} => {e.Reason}"); +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Battle/ApplyProcessedHitEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Battle/ApplyProcessedHitEventHandler.cs new file mode 100644 index 0000000..8068b96 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Battle/ApplyProcessedHitEventHandler.cs @@ -0,0 +1,439 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.DTOs.BCards; +using WingsEmu.DTOs.Skills; +using WingsEmu.Game; +using WingsEmu.Game._enum; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Entities.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Mates; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Battle; +using WingsEmu.Packets.ServerPackets.Battle; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Battle; + +public class ApplyProcessedHitEventHandler : IAsyncEventProcessor +{ + private readonly IBCardEffectHandlerContainer _bCardHandlerContainer; + private readonly IAsyncEventPipeline _eventPipeline; + private readonly IMonsterEntityFactory _monsterEntityFactory; + private readonly IRandomGenerator _randomGenerator; + private readonly ISkillUsageManager _skillUsageManager; + + public ApplyProcessedHitEventHandler(IAsyncEventPipeline eventPipeline, + IBCardEffectHandlerContainer bCardHandlerContainer, ISkillUsageManager skillUsageManager, IRandomGenerator randomGenerator, IMonsterEntityFactory monsterEntityFactory) + { + _eventPipeline = eventPipeline; + _bCardHandlerContainer = bCardHandlerContainer; + _skillUsageManager = skillUsageManager; + _randomGenerator = randomGenerator; + _monsterEntityFactory = monsterEntityFactory; + } + + public async Task HandleAsync(ApplyHitEvent e, CancellationToken cancellation) + { + HitInformation hit = e.HitInformation; + + IBattleEntity caster = hit.Caster; + IBattleEntity target = e.Target; + DamageAlgorithmResult algorithmResult = e.ProcessResults; + HitType hitType = algorithmResult.HitType; + int totalDamage = algorithmResult.Damages; + + SkillInfo skill = hit.Skill; + + BCardDTO[] afterAttackAllTargets = skill.BCardsType.TryGetValue(SkillCastType.AFTER_ATTACK_ALL_TARGETS, out HashSet bCards) ? bCards.ToArray() : Array.Empty(); + + if (hit.IsFirst) + { + switch (skill.TargetType) + { + case TargetType.Self when skill.HitType == TargetHitType.EnemiesInAffectedAoE: + caster.BroadcastSuPacket(caster, skill, 0, SuPacketHitMode.NoDamageFail, isFirst: hit.IsFirst); + break; + case TargetType.NonTarget: + caster.BroadcastNonTargetSkill(e.HitInformation.Position, skill); + break; + } + } + + bool onyx = false; + if (caster.BCardComponent.HasBCard(BCardType.StealBuff, (byte)AdditionalTypes.StealBuff.ChanceSummonOnyxDragon)) + { + onyx = e.ProcessResults.OnyxEffect; + } + + if (totalDamage <= 0 && caster is not IMonsterEntity { IsMateTrainer: true }) + { + totalDamage = 1; + } + + IMonsterEntity onyxMonsterEntity = null; + if (onyx && skill.CastId != 0) + { + short x = caster.PositionX; + short y = caster.PositionY; + + x += (short)_randomGenerator.RandomNumber(-3, 3); + y += (short)_randomGenerator.RandomNumber(-3, 3); + if (caster.MapInstance.IsBlockedZone(x, y)) + { + x = caster.PositionX; + y = caster.PositionY; + } + + onyxMonsterEntity = _monsterEntityFactory.CreateMonster((int)MonsterVnum.ONYX_MONSTER, caster.MapInstance, new MonsterEntityBuilder + { + IsRespawningOnDeath = false, + IsWalkingAround = false + }); + + await onyxMonsterEntity.EmitEventAsync(new MapJoinMonsterEntityEvent(onyxMonsterEntity, x, y)); + caster.MapInstance.Broadcast(caster.GenerateOnyxGuriPacket(x, y)); + } + + if (caster.IsPlayer()) + { + var player = (IPlayerEntity)caster; + player.SkillComponent.IsSkillInterrupted = false; + player.SkillComponent.CanBeInterrupted = false; + } + + SuPacketHitMode hitMode = GetHitMode(skill, hitType, hit.IsFirst); + + if (!caster.IsAlive()) + { + hitMode = SuPacketHitMode.OutOfRange; + caster.BroadcastSuPacket(caster, skill, 0, hitMode, isFirst: hit.IsFirst); + Skip(caster, skill); + return; + } + + switch (target) + { + case IMonsterEntity monsterEntity: + monsterEntity.MapInstance.MonsterRefreshTarget(monsterEntity, caster, DateTime.UtcNow, true); + break; + case INpcEntity npcEntity: + npcEntity.MapInstance.NpcRefreshTarget(npcEntity, caster); + break; + } + + int monsterSize = target switch + { + IMonsterEntity monsterEntity => monsterEntity.CellSize, + INpcEntity npcEntity => npcEntity.CellSize, + IMateEntity mateEntity => mateEntity.CellSize, + _ => 0 + }; + + int cellSizeBonus = target switch + { + IPlayerEntity => 7, + _ => 3 + }; + + if (skill.Vnum != -1 && skill.CastId != -1 && + skill.HitType == TargetHitType.TargetOnly && !caster.Position.IsInRange(target.Position, skill.Range + monsterSize + cellSizeBonus) && skill.AttackType != AttackType.Dash) + { + hitMode = SuPacketHitMode.OutOfRange; + caster.BroadcastSuPacket(target, skill, 0, hitMode, isFirst: hit.IsFirst); + Skip(caster, skill); + return; + } + + if (target.BCardComponent.HasBCard(BCardType.AbsorptionAndPowerSkill, (byte)AdditionalTypes.AbsorptionAndPowerSkill.AddDamageToHP)) + { + (int firstData, int secondData) bCard = + target.BCardComponent.GetAllBCardsInformation(BCardType.AbsorptionAndPowerSkill, (byte)AdditionalTypes.AbsorptionAndPowerSkill.AddDamageToHP, target.Level); + if (_randomGenerator.RandomNumber() <= bCard.firstData) + { + double toHealPercentage = bCard.secondData * 0.01; + int toHeal = (int)(totalDamage * toHealPercentage); + await target.EmitEventAsync(new BattleEntityHealEvent + { + Entity = target, + HpHeal = toHeal + }); + + caster.BroadcastSuPacket(target, skill, 0, GetHitMode(skill, HitType.Miss, hit.IsFirst), isFirst: hit.IsFirst); + Skip(caster, skill); + return; + } + } + + if (hitType == HitType.Miss) + { + caster.BroadcastSuPacket(target, skill, 0, hitMode, isFirst: hit.IsFirst); + Skip(caster, skill); + return; + } + + if (caster.IsPlayer()) + { + var character = (IPlayerEntity)caster; + if (algorithmResult.SoftDamageEffect && hit.IsFirst) + { + caster.MapInstance.Broadcast(character.GenerateEffectPacket(EffectType.BoostedAttack)); + } + + if (skill.Combos.Any()) + { + double increaseDamageByComboState = 0; + ComboState comboState = _skillUsageManager.GetComboState(caster.Id, target.Id); + increaseDamageByComboState = 0.05 + 0.1 * comboState.Hit; + + totalDamage += (int)(totalDamage * increaseDamageByComboState); + + comboState.Hit++; + ComboDTO combo = skill.Combos.FirstOrDefault(s => s.Hit == comboState.Hit); + if (combo != null) + { + skill.HitAnimation = combo.Animation; + skill.HitEffect = combo.Effect; + } + + if (skill.Combos.Max(s => s.Hit) <= comboState.Hit) + { + _skillUsageManager.ResetComboState(caster.Id); + } + } + } + + if (caster.BCardComponent.HasBCard(BCardType.NoDefeatAndNoDamage, (byte)AdditionalTypes.NoDefeatAndNoDamage.NeverCauseDamage)) + { + totalDamage = 0; + } + + if (target.IsPlayer()) + { + var character = (IPlayerEntity)target; + if (character.SkillComponent.CanBeInterrupted && character.IsCastingSkill) + { + character.SkillComponent.CanBeInterrupted = false; + character.SkillComponent.IsSkillInterrupted = true; + } + } + + // REFLECTION + if (IsReflectionNoDamage(target)) + { + await ReflectDamage(caster, target, algorithmResult, skill, hit); + Skip(caster, skill); + return; + } + + if (IsReflectionWithDamage(target)) + { + await ReflectDamage(caster, target, algorithmResult, skill, hit, true); + } + + await _eventPipeline.ProcessEventAsync(new EntityDamageEvent + { + Damaged = target, + Damager = caster, + Damage = totalDamage, + CanKill = true, + SkillInfo = skill + }); + + caster.BroadcastSuPacket(target, skill, totalDamage, hitMode, isFirst: hit.IsFirst); + + if (hit.IsFirst) + { + hit.IsFirst = false; + } + + if (target.IsAlive()) + { + foreach (BCardDTO bCard in afterAttackAllTargets) + { + _bCardHandlerContainer.Execute(target, caster, bCard, skill); + } + } + + if (!target.IsAlive() && caster.IsPlayer()) + { + (caster as IPlayerEntity).Session.SendCancelPacket(CancelType.NotInCombatMode); + } + + switch (skill.Vnum) + { + case (short)SkillsVnums.HOLY_EXPLOSION when target.BuffComponent.HasBuff((short)BuffVnums.ILLUMINATING_POWDER): + { + await _eventPipeline.ProcessEventAsync(new EntityDamageEvent + { + Damaged = target, + Damager = caster, + Damage = totalDamage, + CanKill = true, + SkillInfo = skill + }); + + caster.BroadcastSuPacket(target, skill, totalDamage, hitMode, true); + Buff buff = target.BuffComponent.GetBuff((short)BuffVnums.ILLUMINATING_POWDER); + await target.RemoveBuffAsync(false, buff); + break; + } + case (short)SkillsVnums.CONVERT when target.BuffComponent.HasBuff((short)BuffVnums.CORRUPTION): + { + int convertDamage = totalDamage / 2; + await _eventPipeline.ProcessEventAsync(new EntityDamageEvent + { + Damaged = target, + Damager = caster, + Damage = convertDamage, + CanKill = true, + SkillInfo = skill + }); + + caster.BroadcastSuPacket(target, skill, convertDamage, hitMode, true); + Buff buff = target.BuffComponent.GetBuff((short)BuffVnums.CORRUPTION); + await target.RemoveBuffAsync(false, buff); + break; + } + } + + if (skill.CastId != 0 && onyx) + { + int onyxDamage = totalDamage / 2; + await _eventPipeline.ProcessEventAsync(new EntityDamageEvent + { + Damaged = target, + Damager = onyxMonsterEntity, + Damage = onyxDamage, + CanKill = false, + SkillInfo = skill + }); + + onyxMonsterEntity.BroadcastSuPacket(target, skill, onyxDamage, hitMode); + } + } + + private void Skip(IBattleEntity entity, SkillInfo skillInfo) + { + if (!entity.IsPlayer()) + { + return; + } + + if (!skillInfo.Combos.Any()) + { + return; + } + + _skillUsageManager.ResetComboState(entity.Id); + } + + private static bool IsReflectionNoDamage(IBattleEntity target) => + target.BCardComponent.HasBCard(BCardType.TauntSkill, (byte)AdditionalTypes.TauntSkill.ReflectsMaximumDamageFromNegated) || + target.BCardComponent.HasBCard(BCardType.TauntSkill, (byte)AdditionalTypes.TauntSkill.ReflectsMaximumDamageFrom); + + private static bool IsReflectionWithDamage(IBattleEntity target) => + target.BCardComponent.HasBCard(BCardType.DamageConvertingSkill, (byte)AdditionalTypes.DamageConvertingSkill.ReflectMaximumReceivedDamage); + + private async Task ReflectDamage(IBattleEntity caster, IBattleEntity target, DamageAlgorithmResult damageAlgorithmResult, SkillInfo skill, HitInformation hitInformation, + bool shouldDamageCaster = false) + { + int totalDamage = damageAlgorithmResult.Damages; + HitType hitType = damageAlgorithmResult.HitType; + + (int firstData, int secondData) reflection = + target.BCardComponent.GetAllBCardsInformation(BCardType.TauntSkill, (byte)AdditionalTypes.TauntSkill.ReflectsMaximumDamageFromNegated, target.Level); + if (reflection.firstData == 0) + { + reflection = target.BCardComponent.GetAllBCardsInformation(BCardType.TauntSkill, (byte)AdditionalTypes.TauntSkill.ReflectsMaximumDamageFrom, target.Level); + } + + if (shouldDamageCaster) + { + reflection = target.BCardComponent.GetAllBCardsInformation(BCardType.DamageConvertingSkill, (byte)AdditionalTypes.DamageConvertingSkill.ReflectMaximumReceivedDamage, target.Level); + } + + if (totalDamage > reflection.firstData) + { + totalDamage = reflection.firstData; + } + + await _eventPipeline.ProcessEventAsync(new EntityDamageEvent + { + Damaged = caster, + Damager = target, + Damage = totalDamage, + CanKill = false, + SkillInfo = skill + }); + + SuPacketHitMode hitMode = GetHitMode(skill, hitType, hitInformation.IsFirst); + target.BroadcastSuPacket(caster, skill, totalDamage, hitMode, true, hitInformation.IsFirst); + + if (!shouldDamageCaster) + { + if (skill.Vnum != (short)SkillsVnums.DOUBLE_RIPPER) + { + SuPacketHitMode reflectHitMode = skill.AoERange != 0 ? SuPacketHitMode.ReflectionAoeMiss : SuPacketHitMode.Miss; + caster.BroadcastSuPacket(caster, skill, totalDamage, reflectHitMode, false, hitInformation.IsFirst); // Yes, it should be false for reflect. + } + + if (hitInformation.IsFirst) + { + hitInformation.IsFirst = false; + } + } + } + + private SuPacketHitMode GetHitMode(SkillInfo skill, HitType hitType, bool isFirst) + { + if (skill.TargetType == TargetType.Self && (skill.HitType == TargetHitType.EnemiesInAffectedAoE || skill.HitType == TargetHitType.AlliesInAffectedAoE)) + { + switch (hitType) + { + case HitType.Miss: + return SuPacketHitMode.MissAoe; + case HitType.Normal: + return SuPacketHitMode.AttackedInAoe; + case HitType.Critical: + return SuPacketHitMode.AttackedInAoeCrit; + } + } + + if (isFirst) + { + switch (hitType) + { + case HitType.Miss: + return SuPacketHitMode.Miss; + case HitType.Normal: + return skill.TargetType == TargetType.NonTarget ? SuPacketHitMode.AttackedInAoe : SuPacketHitMode.SuccessAttack; + case HitType.Critical: + return SuPacketHitMode.CriticalAttack; + } + } + else + { + switch (hitType) + { + case HitType.Miss: + return SuPacketHitMode.MissAoe; + case HitType.Normal: + return SuPacketHitMode.AttackedInAoe; + case HitType.Critical: + return SuPacketHitMode.AttackedInAoeCrit; + } + } + + return SuPacketHitMode.SuccessAttack; + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Battle/BattleExecuteSkillEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Battle/BattleExecuteSkillEventHandler.cs new file mode 100644 index 0000000..e18674c --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Battle/BattleExecuteSkillEventHandler.cs @@ -0,0 +1,74 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.DTOs.Skills; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Helpers.Damages; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Battle; + +public class BattleExecuteSkillEventHandler : IAsyncEventProcessor +{ + private readonly ISkillExecutor _skillExecutor; + + public BattleExecuteSkillEventHandler(ISkillExecutor skillExecutor) => _skillExecutor = skillExecutor; + + public async Task HandleAsync(BattleExecuteSkillEvent e, CancellationToken cancellation) + { + IBattleEntity caster = e.Entity; + IBattleEntity target = e.Target; + SkillInfo skillInfo = e.SkillInfo; + DateTime endSkillCastTime = e.EndSkillCastTime; + Position position = e.Position; + + caster.SetCastingSkill(skillInfo, endSkillCastTime); + skillInfo.Cooldown = (short)caster.ApplyCooldownReduction(skillInfo); + + if (skillInfo.TargetType == TargetType.NonTarget) + { + switch (skillInfo.TargetAffectedEntities) + { + case TargetAffectedEntities.Enemies: + _skillExecutor.ExecuteDamageZoneHitSkill(caster, caster.SkillCast, position); + break; + case TargetAffectedEntities.DebuffForEnemies: + _skillExecutor.ExecuteDebuffZoneHitSkill(caster, caster.SkillCast, position); + break; + case TargetAffectedEntities.BuffForAllies: + _skillExecutor.ExecuteBuffZoneHitSkill(caster, caster.SkillCast, position); + break; + } + + return; + } + + switch (skillInfo.TargetAffectedEntities) + { + case TargetAffectedEntities.Enemies: + _skillExecutor.ExecuteDamageSkill(caster, target, caster.SkillCast, position); + break; + case TargetAffectedEntities.BuffForAllies: + _skillExecutor.ExecuteBuffSkill(caster, target, caster.SkillCast); + break; + case TargetAffectedEntities.DebuffForEnemies: + _skillExecutor.ExecuteDebuffSkill(caster, target, caster.SkillCast); + break; + case TargetAffectedEntities.None: + if (caster is not IPlayerEntity character) + { + caster.CancelCastingSkill(); + return; + } + + character.Session.SendSpecialistGuri(skillInfo.CastId); + character.CancelCastingSkill(); + break; + default: + return; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Battle/EntityDamageEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Battle/EntityDamageEventHandler.cs new file mode 100644 index 0000000..3f1e5ac --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Battle/EntityDamageEventHandler.cs @@ -0,0 +1,753 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Packets.Enums.Shells; +using WingsEmu.DTOs.BCards; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Buffs.Events; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Entities.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Extensions.Mates; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Mates.Events; +using WingsEmu.Game.Triggers; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Battle; + +public class EntityDamageEventHandler : IAsyncEventProcessor +{ + private static readonly HashSet _meditationBuffs = new() { BuffVnums.SPIRIT_OF_STRENGTH, BuffVnums.SPIRIT_OF_TEMPERANCE, BuffVnums.SPIRIT_OF_ENLIGHTENMENT }; + private readonly IBCardEffectHandlerContainer _bCardEffectHandlerContainer; + private readonly IBuffFactory _buff; + private readonly IBuffFactory _buffFactory; + private readonly IBuffsToRemoveConfig _buffsToRemoveConfig; + private readonly IGameLanguageService _gameLanguage; + private readonly GameRevivalConfiguration _gameRevivalConfiguration; + private readonly IMeditationManager _meditationManager; + private readonly GameMinMaxConfiguration _minMaxConfiguration; + private readonly IRandomGenerator _randomGenerator; + + public EntityDamageEventHandler(IBuffFactory buff, IMeditationManager meditationManager, IRandomGenerator randomGenerator, IBuffFactory buffFactory, + GameRevivalConfiguration gameRevivalConfiguration, IBuffsToRemoveConfig buffsToRemoveConfig, IBCardEffectHandlerContainer bCardEffectHandlerContainer, + GameMinMaxConfiguration minMaxConfiguration, IGameLanguageService gameLanguage) + { + _buff = buff; + _meditationManager = meditationManager; + _randomGenerator = randomGenerator; + _buffFactory = buffFactory; + _gameRevivalConfiguration = gameRevivalConfiguration; + _buffsToRemoveConfig = buffsToRemoveConfig; + _bCardEffectHandlerContainer = bCardEffectHandlerContainer; + _minMaxConfiguration = minMaxConfiguration; + _gameLanguage = gameLanguage; + } + + public async Task HandleAsync(EntityDamageEvent e, CancellationToken cancellation) + { + IBattleEntity defender = e.Damaged; + IBattleEntity attacker = e.Damager; + int damage = e.Damage; + int initialDefenderHp = defender.Hp; + IMapInstance map = defender.MapInstance; + SkillInfo skillInfo = e.SkillInfo; + + bool shouldDamageNamaju = defender is IMonsterEntity { DamagedOnlyLastJajamaruSkill: true } && skillInfo.Vnum == (short)SkillsVnums.JAJAMARU_LAST_SKILL; + + if (!defender.IsAlive()) + { + return; + } + + if (map == null) + { + return; + } + + /* Sorry, it need to be hardcoded :c */ + await RemoveDamagedHardcodedBuff(defender, skillInfo); + await RemoveDamagerHardcodedBuff(attacker); + await RemovePvPHardcodedBuff(attacker, defender); + await AttackerBuffChance(attacker, defender); + await HeadShot(attacker, defender, skillInfo); + TryLoseLoyalty(attacker, defender); + + if (attacker is IPlayerEntity player && damage != 0) + { + await player.RemoveInvisibility(); + + if (player.TriggerAmbush) + { + Buff buff = player.BuffComponent.GetBuff((int)BuffVnums.AMBUSH); + await player.RemoveBuffAsync(false, buff); + Buff newBuff = _buff.CreateBuff((int)BuffVnums.AMBUSH_RAID, player); + await player.AddBuffAsync(newBuff); + player.TriggerAmbush = false; + } + } + + switch (defender) + { + case IPlayerEntity c: + if (c.IsSeal) + { + return; + } + + if (c.TriggerAmbush) + { + break; + } + + await c.RemoveInvisibility(); + break; + case INpcEntity { HasGodMode: true }: + return; + case IMonsterEntity monster: + if (monster.BCardComponent.HasBCard(BCardType.NoDefeatAndNoDamage, (byte)AdditionalTypes.NoDefeatAndNoDamage.DecreaseHPNoDeath)) + { + e.CanKill = false; + } + + if (monster.BCardComponent.HasBCard(BCardType.TimeCircleSkills, (byte)AdditionalTypes.TimeCircleSkills.DisableHPConsumption)) + { + return; + } + + if (monster.DamagedOnlyLastJajamaruSkill && !shouldDamageNamaju) + { + return; + } + + if (monster.OnFirstDamageReceive && monster.BCards.Any()) + { + foreach (BCardDTO bCard in monster.BCards.Where(x => x.TriggerType is BCardNpcMonsterTriggerType.ON_FIRST_ATTACK)) + { + _bCardEffectHandlerContainer.Execute(monster, monster, bCard); + } + + monster.OnFirstDamageReceive = false; + } + + break; + } + + if (defender.HasGodMode()) + { + return; + } + + switch (attacker) + { + case IMonsterEntity act4Monster when act4Monster.BCardComponent.HasBCard(BCardType.NoDefeatAndNoDamage, (byte)AdditionalTypes.NoDefeatAndNoDamage.DecreaseHPNoKill): + e.CanKill = false; + break; + } + + ProcessBuffDamage(defender, damage); + + damage /= map.MapInstanceType switch + { + MapInstanceType.IceBreakerInstance => 3, + MapInstanceType.RainbowBattle => 3, + MapInstanceType.ArenaInstance => 2, + _ => 1 + }; + + if (shouldDamageNamaju) + { + damage = GenerateNamajuDamage(defender as IMonsterEntity); + } + + // HP is increased by x% of damage given. + await HealByGivenDamage(attacker, damage); + + // Heal x% of inflicted damage by reducing MP. + ReduceDamageByMp(defender, ref damage); + + // MP is increased by x% of damage given. + HealMpByGivenDamage(attacker, damage); + + await HealDefenderByGivenDamage(defender, damage); + + if (defender is IPlayerEntity { AdditionalHp: > 0 } characterDamaged) + { + int removedAdditionalHp; + if (characterDamaged.AdditionalHp > damage) + { + removedAdditionalHp = damage; + } + else + { + removedAdditionalHp = characterDamaged.AdditionalHp; + + int overflow = Math.Abs(characterDamaged.AdditionalHp - damage); + + if (e.CanKill) + { + if (!await attacker.ShouldSaveDefender(defender, overflow, _gameRevivalConfiguration, _buffFactory)) + { + defender.Hp = overflow >= defender.Hp ? 0 : defender.Hp - overflow; + } + } + else + { + defender.Hp = overflow >= defender.Hp ? 1 : defender.Hp - overflow; + } + } + + await characterDamaged.Session.EmitEventAsync(new RemoveAdditionalHpMpEvent + { + Hp = removedAdditionalHp + }); + } + else + { + if (e.CanKill) + { + if (!await attacker.ShouldSaveDefender(defender, damage, _gameRevivalConfiguration, _buffFactory)) + { + defender.Hp = damage >= defender.Hp ? 0 : defender.Hp - damage; + } + } + else + { + defender.Hp = damage >= defender.Hp ? 1 : defender.Hp - damage; + } + } + + AddPlayerDamageToMonster(attacker, defender, damage); + AddMonsterHitsToPlayer(attacker, defender); + attacker.ApplyAttackBCard(defender, e.SkillInfo, _bCardEffectHandlerContainer); + defender.ApplyDefenderBCard(attacker, e.SkillInfo, _bCardEffectHandlerContainer); + + BCardDTO bCardOnDeath = skillInfo.BCards.FirstOrDefault(x => x.Type == (short)BCardType.TauntSkill && x.SubType == (byte)AdditionalTypes.TauntSkill.EffectOnKill); + if (defender.Hp <= 0 && bCardOnDeath != null && attacker.IsAlive()) + { + Buff buffForWinner = _buff.CreateBuff(bCardOnDeath.SecondData, attacker); + await attacker.AddBuffAsync(buffForWinner); + } + + bool onHpExecuted = false; + float quarterHp = defender.MaxHp * 0.25f; + if (defender.Hp <= quarterHp && quarterHp <= initialDefenderHp) + { + onHpExecuted = true; + await defender.TriggerEvents(BattleTriggers.OnQuarterHp); + } + + float halfHp = defender.MaxHp * 0.5f; + if (!onHpExecuted && defender.Hp <= halfHp && halfHp <= initialDefenderHp) + { + onHpExecuted = true; + await defender.TriggerEvents(BattleTriggers.OnHalfHp); + } + + float threeFourthsHp = defender.MaxHp * 0.75f; + if (!onHpExecuted && defender.Hp <= threeFourthsHp && threeFourthsHp <= initialDefenderHp) + { + await defender.TriggerEvents(BattleTriggers.OnThreeFourthsHp); + } + + switch (attacker) + { + case IPlayerEntity playerEntity: + playerEntity.LastAttack = DateTime.UtcNow; + break; + } + + switch (defender) + { + case IPlayerEntity character: + { + character.LastDefence = DateTime.UtcNow; + + character.Session.RefreshStat(); + + if (character.IsSitting) + { + await character.Session.RestAsync(force: true); + } + + break; + } + case IMateEntity mate: + { + mate.LastDefence = DateTime.UtcNow; + + mate.Owner.Session.SendMateLife(mate); + + if (mate.IsSitting) + { + await mate.Owner.Session.EmitEventAsync(new MateRestEvent + { + MateEntity = mate, + Force = true + }); + } + + break; + } + case IMonsterEntity { Hp: <= 0, IsStillAlive: false }: + return; + } + + if (!defender.IsAlive()) + { + await defender.EmitEventAsync(new GenerateEntityDeathEvent + { + Entity = defender, + Attacker = attacker, + IsByMainWeapon = !skillInfo.IsUsingSecondWeapon + }); + } + } + + private void TryLoseLoyalty(IBattleEntity attacker, IBattleEntity defender) + { + if (defender is not IMateEntity mateEntity) + { + return; + } + + if (attacker is IMonsterEntity { IsMateTrainer: true }) + { + return; + } + + if (_randomGenerator.RandomNumber() > 2) + { + return; + } + + mateEntity.RemoveLoyalty((short)_randomGenerator.RandomNumber(1, 6), _minMaxConfiguration, _gameLanguage); + } + + private int GenerateNamajuDamage(IMonsterEntity namaju) + { + BCardDTO bCard = namaju.BCards.FirstOrDefault(x => + x.Type == (short)BCardType.RecoveryAndDamagePercent && x.SubType == (byte)AdditionalTypes.RecoveryAndDamagePercent.DecreaseSelfHP); + if (bCard == null) + { + return default; + } + + return (int)(namaju.MaxHp * (bCard.FirstData * 0.01)); + } + + private async Task RemovePvPHardcodedBuff(IBattleEntity attacker, IBattleEntity defender) + { + if (attacker is not IPlayerEntity playerAttacker) + { + return; + } + + if (defender is not IPlayerEntity playerDefender) + { + return; + } + + if (attacker.IsSameEntity(defender)) + { + return; + } + + DateTime now = DateTime.UtcNow; + playerAttacker.LastPvPAttack = now; + playerDefender.LastPvPAttack = now; + + short[] pvpBuffs = _buffsToRemoveConfig.GetBuffsToRemove(BuffsToRemoveType.PVP); + + if (playerAttacker.BuffComponent.HasAnyBuff()) + { + foreach (short buff in pvpBuffs) + { + foreach (IMateEntity teamMember in playerAttacker.MateComponent.TeamMembers()) + { + Buff toRemoveMate = teamMember.BuffComponent.GetBuff(buff); + await teamMember.RemoveBuffAsync(false, toRemoveMate); + } + + Buff toRemove = playerAttacker.BuffComponent.GetBuff(buff); + await playerAttacker.RemoveBuffAsync(false, toRemove); + } + + foreach (Buff buff in playerAttacker.BuffComponent.GetAllBuffs(x => x.IsDisappearOnPvp())) + { + await playerAttacker.RemoveBuffAsync(false, buff); + } + } + + if (!playerDefender.BuffComponent.HasAnyBuff()) + { + return; + } + + foreach (short buff in pvpBuffs) + { + foreach (IMateEntity teamMember in playerDefender.MateComponent.TeamMembers()) + { + Buff toRemoveMate = teamMember.BuffComponent.GetBuff(buff); + await teamMember.RemoveBuffAsync(false, toRemoveMate); + } + + Buff toRemove = playerDefender.BuffComponent.GetBuff(buff); + await playerDefender.RemoveBuffAsync(false, toRemove); + } + + foreach (Buff buff in playerDefender.BuffComponent.GetAllBuffs(x => x.IsDisappearOnPvp())) + { + await playerDefender.RemoveBuffAsync(false, buff); + } + } + + private async Task HealDefenderByGivenDamage(IBattleEntity defender, int damage) + { + if (defender is not IPlayerEntity playerEntity) + { + return; + } + + int toHeal = playerEntity.GetMaxArmorShellValue(ShellEffectType.RecoveryHPInDefence); + if (toHeal == 0) + { + return; + } + + if (!playerEntity.IsAlive()) + { + return; + } + + int heal = (int)(damage * (toHeal * 0.01 / 5)); + await playerEntity.EmitEventAsync(new BattleEntityHealEvent + { + Entity = playerEntity, + HpHeal = heal + }); + } + + private void AddMonsterHitsToPlayer(IBattleEntity attacker, IBattleEntity defender) + { + if (attacker is not IMonsterEntity monsterEntity) + { + return; + } + + if (!monsterEntity.DropToInventory && !monsterEntity.MapInstance.HasMapFlag(MapFlags.HAS_DROP_DIRECTLY_IN_INVENTORY_ENABLED) && !monsterEntity.MapInstance.HasMapFlag(MapFlags.ACT_4)) + { + return; + } + + IPlayerEntity playerEntity = defender switch + { + IMateEntity mateEntity => mateEntity.Owner, + IPlayerEntity player => player, + _ => null + }; + + if (playerEntity == null) + { + return; + } + + if (!playerEntity.HitsByMonsters.TryGetValue(monsterEntity.Id, out int hits)) + { + playerEntity.HitsByMonsters.TryAdd(monsterEntity.Id, 1); + return; + } + + hits++; + playerEntity.HitsByMonsters[monsterEntity.Id] = hits; + } + + private void AddPlayerDamageToMonster(IBattleEntity attacker, IBattleEntity defender, int damage) + { + if (defender is not IMonsterEntity monsterEntity) + { + return; + } + + if (!monsterEntity.DropToInventory && !monsterEntity.MapInstance.HasMapFlag(MapFlags.HAS_DROP_DIRECTLY_IN_INVENTORY_ENABLED) && !monsterEntity.MapInstance.HasMapFlag(MapFlags.ACT_4)) + { + return; + } + + IPlayerEntity playerEntity = attacker switch + { + IMateEntity mateEntity => mateEntity.Owner, + IMonsterEntity monster => monster.SummonerType is VisualType.Player && monster.SummonerId.HasValue ? attacker.MapInstance.GetCharacterById(monster.SummonerId.Value) : null, + IPlayerEntity player => player, + _ => null + }; + + if (playerEntity == null) + { + return; + } + + if (!monsterEntity.PlayersDamage.TryGetValue(playerEntity.Id, out int playerDamage)) + { + monsterEntity.PlayersDamage.TryAdd(playerEntity.Id, damage); + return; + } + + playerDamage += damage; + monsterEntity.PlayersDamage[playerEntity.Id] = playerDamage; + } + + private void HealMpByGivenDamage(IBattleEntity attacker, int damage) + { + if (!attacker.IsAlive()) + { + return; + } + + if (!attacker.BCardComponent.HasBCard(BCardType.Reflection, (byte)AdditionalTypes.Reflection.MPIncreased)) + { + return; + } + + int firstData = attacker.BCardComponent.GetAllBCardsInformation(BCardType.Reflection, (byte)AdditionalTypes.Reflection.MPIncreased, attacker.Level).firstData; + int mpToIncrease = (int)(damage * (firstData * 0.01)); + if (attacker.Mp + mpToIncrease < attacker.MaxMp) + { + attacker.Mp += mpToIncrease; + } + else + { + attacker.Mp = attacker.MaxMp; + } + } + + private void ReduceDamageByMp(IBattleEntity defender, ref int damage) + { + (int firstData, _) = defender.BCardComponent.GetAllBCardsInformation(BCardType.LightAndShadow, (byte)AdditionalTypes.LightAndShadow.InflictDamageToMP, defender.Level); + if (firstData == 0) + { + return; + } + + (int firstDataPositive, int _) = defender.BCardComponent.GetAllBCardsInformation(BCardType.HealingBurningAndCasting, + (byte)AdditionalTypes.HealingBurningAndCasting.HPIncreasedByConsumingMP, defender.Level); + + int defenderMp = defender.Mp; + int defenderMpToRemove = defender.CalculateManaUsage((int)(damage * (firstData * 0.01))); + + if (defenderMp - defenderMpToRemove <= 0) + { + damage -= defenderMp; + defender.Mp = 0; + + int hpToAdd = (int)(firstDataPositive / 100.0 * defenderMp); + defender.EmitEvent(new BattleEntityHealEvent + { + Entity = defender, + HpHeal = hpToAdd + }); + } + else + { + damage -= defenderMpToRemove; + defender.Mp -= defenderMpToRemove; + + int hpToAdd = (int)(firstDataPositive / 100.0 * defenderMpToRemove); + defender.EmitEvent(new BattleEntityHealEvent + { + Entity = defender, + HpHeal = hpToAdd + }); + } + } + + private async Task HealByGivenDamage(IBattleEntity attacker, int damage) + { + if (!attacker.IsAlive()) + { + return; + } + + if (!attacker.BCardComponent.HasBCard(BCardType.Reflection, (byte)AdditionalTypes.Reflection.HPIncreased)) + { + return; + } + + int firstData = attacker.BCardComponent.GetAllBCardsInformation(BCardType.Reflection, (byte)AdditionalTypes.Reflection.HPIncreased, attacker.Level).firstData; + int hpToIncrease = (int)(damage * (firstData * 0.01)); + await attacker.EmitEventAsync(new BattleEntityHealEvent + { + Entity = attacker, + HpHeal = hpToIncrease + }); + } + + private async Task HeadShot(IBattleEntity attacker, IBattleEntity defender, SkillInfo skillInfo) + { + if (!attacker.IsPlayer()) + { + return; + } + + if (skillInfo.Vnum != (short)SkillsVnums.SNIPER) + { + return; + } + + if (!attacker.BuffComponent.HasBuff((short)BuffVnums.SNIPER_POSITION_1) && !attacker.BuffComponent.HasBuff((short)BuffVnums.SNIPER_POSITION_2)) + { + return; + } + + (int firstData, int secondData) = attacker.BCardComponent.GetAllBCardsInformation(BCardType.SniperAttack, (byte)AdditionalTypes.SniperAttack.ChanceCausing, attacker.Level); + if (_randomGenerator.RandomNumber() > firstData) + { + return; + } + + Buff headShot = _buffFactory.CreateBuff(secondData, attacker); + await defender.AddBuffAsync(headShot); + } + + private async Task AttackerBuffChance(IBattleEntity attacker, IBattleEntity defender) + { + if (!defender.BuffComponent.HasAnyBuff()) + { + return; + } + + if (!defender.BCardComponent.HasBCard(BCardType.SecondSPCard, (byte)AdditionalTypes.SecondSPCard.HitAttacker)) + { + return; + } + + (int firstData, int secondData) = defender.BCardComponent.GetAllBCardsInformation(BCardType.SecondSPCard, (byte)AdditionalTypes.SecondSPCard.HitAttacker, defender.Level); + if (_randomGenerator.RandomNumber() > firstData) + { + return; + } + + Buff newBuff = _buffFactory.CreateBuff(secondData, defender); + await attacker.AddBuffAsync(newBuff); + } + + private void ProcessBuffDamage(IBattleEntity defender, int damage) + { + if (!defender.BuffComponent.HasAnyBuff()) + { + return; + } + + if (!defender.EndBuffDamages.Any()) + { + return; + } + + var listToRemove = new ConcurrentQueue(); + + foreach (short buffVnum in defender.EndBuffDamages.Keys) + { + if (!defender.BuffComponent.HasBuff(buffVnum)) + { + defender.RemoveEndBuffDamage(buffVnum); + continue; + } + + int damageAfter = defender.DecreaseDamageEndBuff(buffVnum, damage); + if (damageAfter > 0) + { + continue; + } + + Buff buffToRemove = defender.BuffComponent.GetBuff(buffVnum); + defender.RemoveBuffAsync(false, buffToRemove).ConfigureAwait(false).GetAwaiter().GetResult(); + listToRemove.Enqueue(buffVnum); + } + + while (listToRemove.TryDequeue(out short toRemoveBuff)) + { + defender.RemoveEndBuffDamage(toRemoveBuff); + } + } + + private async Task RemoveDamagerHardcodedBuff(IBattleEntity damager) + { + var listToRemove = new List(); + foreach (short buffVnum in _buffsToRemoveConfig.GetBuffsToRemove(BuffsToRemoveType.ATTACKER)) + { + if (!damager.BuffComponent.HasBuff(buffVnum)) + { + continue; + } + + listToRemove.Add(damager.BuffComponent.GetBuff(buffVnum)); + } + + await damager.EmitEventAsync(new BuffRemoveEvent + { + Entity = damager, + Buffs = listToRemove.AsReadOnly(), + RemovePermanentBuff = false + }); + } + + private async Task RemoveDamagedHardcodedBuff(IBattleEntity damaged, SkillInfo skillInfo) + { + if (damaged.BuffComponent.HasBuff((short)BuffVnums.MAGICAL_FETTERS) && damaged.IsPlayer()) + { + var characterMagical = (IPlayerEntity)damaged; + await characterMagical.Session.EmitEventAsync(new AngelSpecialistElementalBuffEvent + { + Skill = skillInfo + }); + } + + var listToRemove = new List(); + foreach (short buffVnum in _buffsToRemoveConfig.GetBuffsToRemove(BuffsToRemoveType.DEFENDER)) + { + if (!damaged.BuffComponent.HasBuff(buffVnum)) + { + continue; + } + + listToRemove.Add(damaged.BuffComponent.GetBuff(buffVnum)); + } + + await damaged.EmitEventAsync(new BuffRemoveEvent + { + Entity = damaged, + Buffs = listToRemove.AsReadOnly(), + RemovePermanentBuff = false + }); + + if (damaged is IPlayerEntity character) + { + if (!_meditationManager.HasMeditation(character)) + { + return; + } + + _meditationManager.RemoveAllMeditation(character); + foreach (BuffVnums buffVnum in _meditationBuffs) + { + Buff buff = character.BuffComponent.GetBuff((short)buffVnum); + await damaged.RemoveBuffAsync(false, buff); + } + + await damaged.AddBuffAsync(_buff.CreateBuff((short)BuffVnums.KUNDALINI_SYNDROME, damaged)); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Battle/ProcessBuffEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Battle/ProcessBuffEventHandler.cs new file mode 100644 index 0000000..5224db0 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Battle/ProcessBuffEventHandler.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Core.Extensions; +using WingsEmu.DTOs.BCards; +using WingsEmu.DTOs.Skills; +using WingsEmu.Game._enum; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Mates; +using WingsEmu.Packets.Enums.Battle; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Battle; + +public class ProcessBuffEventHandler : IAsyncEventProcessor +{ + private static readonly byte MAX_TARGETS = 50; + + private readonly IBCardEffectHandlerContainer _bCardEffectHandler; + + public ProcessBuffEventHandler(IBCardEffectHandlerContainer bCardEffectHandler) => _bCardEffectHandler = bCardEffectHandler; + + public async Task HandleAsync(ProcessBuffEvent e, CancellationToken cancellation) + { + IBattleEntity caster = e.Caster; + IBattleEntity target = e.Target; + SkillCast skillCast = e.SkillCast; + SkillInfo skill = skillCast.Skill; + Position position = e.Position; + + BCardDTO[] beforeAttackOnMainTarget = skill.BCardsType.TryGetValue(SkillCastType.BEFORE_ATTACK_ON_MAIN_TARGET, out HashSet beforeOnMain) + ? beforeOnMain.ToArray() + : Array.Empty(); + BCardDTO[] beforeAttackSelf = skill.BCardsType.TryGetValue(SkillCastType.BEFORE_ATTACK_SELF, out HashSet beforeSelf) ? beforeSelf.ToArray() : Array.Empty(); + BCardDTO[] beforeAttackAllTargets = skill.BCardsType.TryGetValue(SkillCastType.BEFORE_ATTACK_ALL_TARGETS, out HashSet beforeAttackAll) + ? beforeAttackAll.ToArray() + : Array.Empty(); + + IEnumerable entities = null; + + if (skill.TargetType == TargetType.NonTarget) + { + switch (skill.TargetAffectedEntities) + { + case TargetAffectedEntities.DebuffForEnemies: + entities = position.GetEnemiesInRange(caster, skill.AoERange); + break; + case TargetAffectedEntities.BuffForAllies: + entities = skill.HitType switch + { + TargetHitType.TargetOnly => Lists.Create(caster), + TargetHitType.AlliesInAffectedAoE => position.GetAlliesInRange(caster, skill.AoERange) + }; + break; + } + } + else + { + switch (skill.TargetAffectedEntities) + { + case TargetAffectedEntities.DebuffForEnemies: + entities = skill.HitType switch + { + TargetHitType.TargetOnly => Lists.Create(target), + TargetHitType.EnemiesInAffectedAoE => target.GetEnemiesInRange(caster, skill.AoERange), + TargetHitType.PlayerAndHisMates => target.GetEnemiesInRange(caster, skill.AoERange).Where(x => x is IMateEntity mate && mate.Owner?.Id == caster.Id && mate.IsTeamMember), + _ => null + }; + break; + case TargetAffectedEntities.BuffForAllies: + entities = skill.HitType switch + { + TargetHitType.TargetOnly => Lists.Create(target), + TargetHitType.AlliesInAffectedAoE => target.GetAlliesInRange(caster, skill.AoERange), + TargetHitType.PlayerAndHisMates => target.GetAlliesInRange(caster, skill.AoERange).Where(x => x is IMateEntity mate && mate.Owner?.Id == caster.Id && mate.IsTeamMember), + _ => null + }; + break; + } + } + + if (entities == null) + { + caster.CancelCastingSkill(); + return; + } + + var entitiesToReturn = entities.Take(MAX_TARGETS).ToList(); + if (skill.TargetType == TargetType.Self && skill.TargetAffectedEntities == TargetAffectedEntities.BuffForAllies && !entitiesToReturn.Contains(caster)) + { + entitiesToReturn.Add(caster); + } + + foreach (BCardDTO bCard in beforeAttackSelf) + { + _bCardEffectHandler.Execute(caster, caster, bCard, skill, position); + } + + if (target != null && skill.TargetType != TargetType.NonTarget) + { + foreach (BCardDTO bCard in beforeAttackOnMainTarget) + { + _bCardEffectHandler.Execute(target, caster, bCard, skill, position); + } + } + + foreach (BCardDTO bCard in beforeAttackAllTargets) + { + foreach (IBattleEntity entity in entitiesToReturn) + { + _bCardEffectHandler.Execute(entity, caster, bCard, skill, position); + } + } + + caster.MapInstance.AddBuffRequest(new BuffRequest(caster, entitiesToReturn, skillCast, position, target)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Battle/ProcessHitEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Battle/ProcessHitEventHandler.cs new file mode 100644 index 0000000..673589a --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Battle/ProcessHitEventHandler.cs @@ -0,0 +1,216 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Core.Extensions; +using WingsEmu.DTOs.BCards; +using WingsEmu.DTOs.Skills; +using WingsEmu.Game; +using WingsEmu.Game._enum; +using WingsEmu.Game.Algorithm; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Mates; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Battle; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Battle; + +public class ProcessHitEventHandler : IAsyncEventProcessor +{ + private static readonly byte MAX_TARGETS = 50; + private readonly IBattleEntityDumpFactory _battleEntityDumpFactory; + private readonly IBCardEffectHandlerContainer _bCardEffectHandler; + private readonly IDamageAlgorithm _damageAlgorithm; + private readonly IRandomGenerator _randomGenerator; + private readonly ISkillUsageManager _skillUsageManager; + + public ProcessHitEventHandler(IBattleEntityDumpFactory battleEntityDumpFactory, IDamageAlgorithm damageAlgorithm, + ISkillUsageManager skillUsageManager, IBCardEffectHandlerContainer bCardEffectHandler, IRandomGenerator randomGenerator) + { + _battleEntityDumpFactory = battleEntityDumpFactory; + _damageAlgorithm = damageAlgorithm; + _skillUsageManager = skillUsageManager; + _bCardEffectHandler = bCardEffectHandler; + _randomGenerator = randomGenerator; + } + + public async Task HandleAsync(ProcessHitEvent e, CancellationToken cancellation) + { + IBattleEntity caster = e.Caster; + + if (!caster.IsAlive()) + { + caster.CancelCastingSkill(); + return; + } + + IBattleEntity target = e.Target; + SkillInfo skill = e.HitInformation.Skill; + Position position = e.HitInformation.Position; + + BCardDTO[] afterAttackAllAllies = skill.BCardsType.TryGetValue(SkillCastType.AFTER_ATTACK_ALL_ALLIES, out HashSet allAllies) ? allAllies.ToArray() : Array.Empty(); + BCardDTO[] beforeAttackOnMainTarget = skill.BCardsType.TryGetValue(SkillCastType.BEFORE_ATTACK_ON_MAIN_TARGET, out HashSet beforeAttackMain) + ? beforeAttackMain.ToArray() + : Array.Empty(); + BCardDTO[] beforeAttackSelf = skill.BCardsType.TryGetValue(SkillCastType.BEFORE_ATTACK_SELF, out HashSet beforeAttack) ? beforeAttack.ToArray() : Array.Empty(); + BCardDTO[] beforeAttackAllTargets = skill.BCardsType.TryGetValue(SkillCastType.BEFORE_ATTACK_ALL_TARGETS, out HashSet beforeAttackAll) + ? beforeAttackAll.ToArray() + : Array.Empty(); + + IBattleEntityDump attacker = caster switch + { + IPlayerEntity character => _battleEntityDumpFactory.Dump(character, skill), + IMonsterEntity monster => _battleEntityDumpFactory.Dump(monster, skill), + INpcEntity mapNpc => _battleEntityDumpFactory.Dump(mapNpc, skill), + IMateEntity mate => _battleEntityDumpFactory.Dump(mate, skill), + _ => null + }; + + if (attacker == null) + { + caster.CancelCastingSkill(); + return; + } + + List entities; + + TargetHitType hitType = skill.HitType; + + (int randomChance, int range) = + caster.BCardComponent.GetAllBCardsInformation(BCardType.SpecialDamageAndExplosions, (byte)AdditionalTypes.SpecialDamageAndExplosions.SurroundingDamage, caster.Level); + + if (randomChance != 0 && _randomGenerator.RandomNumber() <= randomChance && hitType is not TargetHitType.SpecialArea) + { + hitType = TargetHitType.EnemiesInAffectedAoE; + skill.AoERange += (byte)range; + } + + if (skill.TargetType == TargetType.NonTarget) + { + entities = position.GetEnemiesInRange(caster, skill.AoERange).ToList(); + if (skill.BCards.Any(x => (BCardType)x.Type == BCardType.FalconSkill && x.SubType == (byte)AdditionalTypes.FalconSkill.FalconFocusLowestHP)) + { + if (entities.Count != 0) + { + entities = Lists.Create(entities.OrderBy(x => x.HpPercentage).First()); + } + } + } + else + { + switch (hitType) + { + case TargetHitType.EnemiesInAffectedAoE: + entities = skill.Vnum == (short)SkillsVnums.DOUBLE_RIPPER ? position.GetEnemiesInRange(caster, skill.AoERange).ToList() : target.GetEnemiesInRange(caster, skill.AoERange).ToList(); + + break; + case TargetHitType.SpecialArea: + entities = _skillUsageManager.GetMultiTargets(caster.Id) + .Select(x => caster.MapInstance.GetBattleEntity(x.Item1, x.Item2)) + .Where(x => x != null && caster.Position.IsInAoeZone(x.Position, 10) && caster.IsEnemyWith(x)) + .ToList(); + _skillUsageManager.ResetMultiTargets(caster.Id); + break; + case TargetHitType.TargetOnly: + entities = Lists.Create(target); + break; + default: + caster.CancelCastingSkill(); + return; + } + + if (!caster.Equals(target)) + { + entities.SetFirst(target); + } + } + + if (!caster.IsPlayer() || caster is IPlayerEntity playerEntity && playerEntity.CheatComponent.HasNoTargetLimit == false) + { + entities = entities.Take(MAX_TARGETS).ToList(); + } + + foreach (BCardDTO bCard in beforeAttackOnMainTarget) + { + _bCardEffectHandler.Execute(target, caster, bCard, skill, position); + } + + foreach (BCardDTO bCard in beforeAttackSelf) + { + _bCardEffectHandler.Execute(caster, caster, bCard, skill, position); + } + + var targets = new List<(IBattleEntity, DamageAlgorithmResult)>(); + foreach (IBattleEntity entity in entities) + { + if (caster is IPlayerEntity c) + { + if (c.CheatComponent.IsInvisible) + { + continue; + } + + if (entity.IsMonster()) + { + var monster = entity as IMonsterEntity; + sbyte monsterMinRange = monster.MinimumAttackRange; + if (monsterMinRange != 0 && skill.Range < monsterMinRange) + { + continue; + } + } + } + + foreach (BCardDTO bCard in beforeAttackAllTargets) + { + _bCardEffectHandler.Execute(entity, caster, bCard, skill, position); + } + + IBattleEntityDump defender = entity switch + { + IPlayerEntity character => _battleEntityDumpFactory.Dump(character, skill, true, entity.IsSameEntity(target)), + IMonsterEntity monster => _battleEntityDumpFactory.Dump(monster, skill, true, entity.IsSameEntity(target)), + INpcEntity mapNpc => _battleEntityDumpFactory.Dump(mapNpc, skill, true, entity.IsSameEntity(target)), + IMateEntity mate => _battleEntityDumpFactory.Dump(mate, skill, true, entity.IsSameEntity(target)), + _ => null + }; + + if (defender == null) + { + caster.CancelCastingSkill(); + continue; + } + + DamageAlgorithmResult algorithmResult = _damageAlgorithm.GenerateDamage(attacker, defender, skill); + targets.Add((entity, algorithmResult)); + + if (caster.IsEnemyWith(entity)) + { + continue; + } + + foreach (BCardDTO bCard in afterAttackAllAllies) + { + _bCardEffectHandler.Execute(entity, caster, bCard, skill, position); + } + } + + foreach (BCardDTO bCard in afterAttackAllAllies) + { + _bCardEffectHandler.Execute(caster, caster, bCard, skill, position); + } + + caster.MapInstance.AddHitRequest(new HitRequest(targets, e.HitInformation, target)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Buffs/AngelSpecialistElementalBuffEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Buffs/AngelSpecialistElementalBuffEventHandler.cs new file mode 100644 index 0000000..fd73382 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Buffs/AngelSpecialistElementalBuffEventHandler.cs @@ -0,0 +1,77 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game._enum; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Buffs.Events; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Skills; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Buffs; + +public class AngelSpecialistElementalBuffEventHandler : IAsyncEventProcessor +{ + private readonly IBuffFactory _buffFactory; + + public AngelSpecialistElementalBuffEventHandler(IBuffFactory buffFactory) => _buffFactory = buffFactory; + + public async Task HandleAsync(AngelSpecialistElementalBuffEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + IPlayerEntity character = session.PlayerEntity; + SkillInfo skillInfo = e.Skill; + + if (character.AngelElement.HasValue) + { + return; + } + + if (!character.UseSp) + { + return; + } + + if (character.Specialist == null) + { + return; + } + + if (!character.HasBuff(BuffVnums.MAGICAL_FETTERS)) + { + return; + } + + character.RemoveAngelElement(); + await character.RemoveBuffAsync(false, character.BuffComponent.GetBuff((short)BuffVnums.MAGICAL_FETTERS)); + Buff newBuff = _buffFactory.CreateBuff((short)BuffVnums.MAGIC_SPELL, character); + await character.AddBuffAsync(newBuff); + + if (character.Specialist.SpLevel < 20) + { + return; + } + + byte skillCastId = (ElementType)skillInfo.Element switch + { + ElementType.Neutral => 15, + ElementType.Fire => 11, + ElementType.Water => 12, + ElementType.Light => 13, + ElementType.Shadow => 14 + }; + + var newComboState = new ComboSkillState + { + State = byte.MinValue, + LastSkillByCastId = skillCastId, + OriginalSkillCastId = skillCastId + }; + + character.SaveComboSkill(newComboState); + session.SendMSlotPacket(skillCastId); + character.AddAngelElement((ElementType)skillInfo.Element); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Buffs/BuffAddEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Buffs/BuffAddEventHandler.cs new file mode 100644 index 0000000..b6eee6b --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Buffs/BuffAddEventHandler.cs @@ -0,0 +1,261 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Core.Extensions; +using WingsEmu.DTOs.BCards; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Buffs.Events; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Extensions.Mates; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Skills; +using WingsEmu.Packets.Enums.Chat; +using WingsEmu.Packets.ServerPackets; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Buffs; + +public class BuffAddEventHandler : IAsyncEventProcessor +{ + private readonly IBCardEffectHandlerContainer _bCardEffectHandlerContainer; + private readonly IBuffFactory _buffFactory; + private readonly IGameLanguageService _gameLanguage; + + public BuffAddEventHandler(IBuffFactory buffFactory, IGameLanguageService gameLanguage, IBCardEffectHandlerContainer bCardEffectHandlerContainer) + { + _buffFactory = buffFactory; + _gameLanguage = gameLanguage; + _bCardEffectHandlerContainer = bCardEffectHandlerContainer; + } + + public async Task HandleAsync(BuffAddEvent e, CancellationToken cancellation) + { + IBattleEntity battleEntity = e.Entity; + foreach (Buff buff in e.Buffs) + { + if (buff == null) + { + continue; + } + + if (buff.ElementType != ElementType.Neutral && (ElementType)battleEntity.Element != buff.ElementType) + { + continue; + } + + switch (battleEntity) + { + case IMonsterEntity { CanBeDebuffed: false } when !buff.IsBigBuff(): + continue; + } + + Buff soundFlowerBuff = battleEntity.BuffComponent.GetBuff((short)BuffVnums.SOUND_FLOWER_BLESSING_BETTER); + if (soundFlowerBuff != null && buff.CardId == (short)BuffVnums.SOUND_FLOWER_BLESSING) + { + // Refresh sound flower buff duration + soundFlowerBuff.SetBuffDuration(soundFlowerBuff.Duration); + + if (battleEntity is IPlayerEntity playerEntity) + { + playerEntity.Session.SendBfPacket(soundFlowerBuff, 0); + } + + continue; + } + + bool showMessage = true; + Buff existingBuffWithSameId = battleEntity.BuffComponent.GetBuff(buff.CardId); + if (existingBuffWithSameId != null && buff.IsNormal() && !buff.IsPartnerBuff()) + { + await battleEntity.EmitEventAsync(new BuffRemoveEvent + { + Entity = battleEntity, + Buffs = Lists.Create(existingBuffWithSameId), + RemovePermanentBuff = false, + ShowMessage = false + }); + + showMessage = false; + } + + Buff existingBuffByGroupId = battleEntity.BuffComponent.GetBuffByGroupId(buff.GroupId); + if (existingBuffByGroupId != null && !existingBuffByGroupId.IsBigBuff() && !buff.IsBigBuff()) + { + showMessage = false; + if (existingBuffByGroupId.Level > buff.Level) + { + continue; + } + + if (existingBuffByGroupId.Level == buff.Level) + { + existingBuffByGroupId.SetBuffDuration(buff.Duration); + + if (battleEntity is IPlayerEntity playerEntity) + { + playerEntity.Session.SendBfPacket(existingBuffByGroupId, 0); + } + + continue; + } + + await battleEntity.EmitEventAsync(new BuffRemoveEvent + { + Entity = battleEntity, + Buffs = Lists.Create(existingBuffByGroupId), + RemovePermanentBuff = false, + ShowMessage = false, + RemoveFromGroupId = false + }); + } + + foreach (BCardDTO bCard in buff.BCards.Where(x => (!x.IsSecondBCardExecution.HasValue || !x.IsSecondBCardExecution.Value) && !x.TickPeriod.HasValue)) + { + _bCardEffectHandlerContainer.Execute(battleEntity, buff.Caster, bCard); + } + + switch (battleEntity) + { + case IPlayerEntity character: + { + IClientSession session = character.Session; + + switch (buff.CardId) + { + case (short)BuffVnums.MAGICAL_FETTERS: + if (character.HasBuff(BuffVnums.MAGIC_SPELL)) + { + await character.RemoveBuffAsync(false, character.BuffComponent.GetBuff((short)BuffVnums.MAGIC_SPELL)); + } + + break; + case (short)BuffVnums.AMBUSH_PREPARATION_1: + character.ChangeScoutState(ScoutStateType.FirstState); + break; + case (short)BuffVnums.AMBUSH_PREPARATION_2: + character.ChangeScoutState(ScoutStateType.SecondState); + break; + case (short)BuffVnums.AMBUSH: + switch (character.ScoutStateType) + { + case ScoutStateType.FirstState: + Buff toAdd = _buffFactory.CreateBuff((short)BuffVnums.AMBUSH_POSITION_1, character, buff.Duration); + await character.AddBuffAsync(toAdd); + + Buff buffToRemove = character.BuffComponent.GetBuff((short)BuffVnums.AMBUSH_PREPARATION_1); + await character.RemoveBuffAsync(false, buffToRemove); + break; + case ScoutStateType.SecondState: + Buff toAddSecond = _buffFactory.CreateBuff((short)BuffVnums.AMBUSH_POSITION_2, character, buff.Duration); + await character.AddBuffAsync(toAddSecond); + + Buff secondBuffToRemove = character.BuffComponent.GetBuff((short)BuffVnums.AMBUSH_PREPARATION_2); + await character.RemoveBuffAsync(false, secondBuffToRemove); + break; + } + + break; + case (short)BuffVnums.AMBUSH_RAID: + switch (character.ScoutStateType) + { + case ScoutStateType.FirstState: + Buff toAdd = _buffFactory.CreateBuff((short)BuffVnums.SNIPER_POSITION_1, character, buff.Duration); + await character.AddBuffAsync(toAdd); + + Buff buffToRemove = character.BuffComponent.GetBuff((short)BuffVnums.AMBUSH_POSITION_1); + await character.RemoveBuffAsync(false, buffToRemove); + break; + case ScoutStateType.SecondState: + Buff toAddSecond = _buffFactory.CreateBuff((short)BuffVnums.SNIPER_POSITION_2, character, buff.Duration); + await character.AddBuffAsync(toAddSecond); + + Buff secondBuffToRemove = character.BuffComponent.GetBuff((short)BuffVnums.AMBUSH_POSITION_2); + await character.RemoveBuffAsync(false, secondBuffToRemove); + break; + } + + break; + } + + if (!buff.IsBigBuff()) + { + switch (buff.CardId) + { + case (short)BuffVnums.CHARGE when session.PlayerEntity.BCardComponent.GetChargeBCards().Any(): + int sum = session.PlayerEntity.BCardComponent.GetChargeBCards().Sum(x => x.FirstDataValue(session.PlayerEntity.Level)); + session.SendBfPacket(buff, sum, sum); + break; + case (short)BuffVnums.CHARGE: + session.SendBfPacket(buff, session.PlayerEntity.ChargeComponent.GetCharge(), session.PlayerEntity.ChargeComponent.GetCharge()); + break; + default: + session.SendBfPacket(buff, 0); + break; + } + } + else + { + session.SendStaticBuffUiPacket(buff, buff.RemainingTimeInMilliseconds()); + } + + if (showMessage) + { + string message = _gameLanguage.GetLanguage(GameDialogKey.BUFF_CHATMESSAGE_UNDER_EFFECT, session.UserLanguage); + string cardName = _gameLanguage.GetLanguage(GameDataType.Card, buff.Name, session.UserLanguage); + + session.SendChatMessage(string.Format(message, cardName), !buff.IsSavingOnDisconnect() ? ChatMessageColorType.Buff : ChatMessageColorType.Red); + } + + break; + } + } + + if (buff.IsConstEffect) + { + battleEntity.BroadcastConstBuffEffect(buff, 0); + battleEntity.BroadcastConstBuffEffect(buff, (int)buff.Duration.TotalMilliseconds); + } + + if (buff.EffectId > 0 && !buff.IsConstEffect) + { + var effect = new EffectServerPacket + { + EffectType = (byte)battleEntity.Type, + CharacterId = battleEntity.Id, + Id = buff.EffectId + }; + + battleEntity.MapInstance?.Broadcast(effect); + } + + battleEntity.BuffComponent.AddBuff(buff); + battleEntity.BCardComponent.AddBuffBCards(buff); + + battleEntity.ShadowAppears(false, buff); + switch (battleEntity) + { + case IPlayerEntity c: + await c.CheckAct52Buff(_buffFactory); + c.Session.RefreshStatChar(); + c.Session.RefreshStat(); + c.Session.SendCondPacket(); + c.Session.SendIncreaseRange(); + c.Session.UpdateVisibility(); + break; + case IMonsterEntity monsterEntity: + monsterEntity.RefreshStats(); + break; + case IMateEntity mateEntity: + mateEntity.Owner?.Session.SendPetInfo(mateEntity, _gameLanguage); + mateEntity.Owner?.Session.SendCondMate(mateEntity); + break; + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Buffs/BuffPartnerCheckEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Buffs/BuffPartnerCheckEventHandler.cs new file mode 100644 index 0000000..f9e6e68 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Buffs/BuffPartnerCheckEventHandler.cs @@ -0,0 +1,94 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game._enum; +using WingsEmu.Game._ItemUsage.Configuration; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Buffs.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Skills; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Buffs; + +public class BuffPartnerCheckEventHandler : IAsyncEventProcessor +{ + private readonly IBuffFactory _buffFactory; + private readonly IMateBuffConfigsContainer _mateBuffConfigsContainer; + private readonly ISpPartnerConfiguration _spPartner; + + public BuffPartnerCheckEventHandler(ISpPartnerConfiguration spPartner, IBuffFactory buffFactory, IMateBuffConfigsContainer mateBuffConfigsContainer) + { + _spPartner = spPartner; + _buffFactory = buffFactory; + _mateBuffConfigsContainer = mateBuffConfigsContainer; + } + + public async Task HandleAsync(BuffPartnerCheckEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + IMateEntity matePet = session.PlayerEntity.MateComponent.GetTeamMember(x => x.MateType == MateType.Pet); + if (matePet != null) + { + await session.AddPetBuff(matePet, _mateBuffConfigsContainer, _buffFactory); + } + + IMateEntity mateEntity = session.PlayerEntity.MateComponent.GetTeamMember(x => x.MateType == MateType.Partner); + if (mateEntity == null) + { + return; + } + + if (!mateEntity.IsUsingSp) + { + return; + } + + SpPartnerInfo spInfo = _spPartner.GetByMorph(mateEntity.Specialist.GameItem.Morph); + + if (spInfo == null) + { + return; + } + + if (spInfo.BuffId == 0) + { + return; + } + + if (session.PlayerEntity.BuffComponent.HasBuff(spInfo.BuffId)) + { + return; + } + + int sum = 0; + foreach (IBattleEntitySkill skill in mateEntity.Skills) + { + if (!(skill is PartnerSkill partnerSkill)) + { + continue; + } + + sum += partnerSkill.Rank; + } + + if (sum == 0) + { + sum = 1; + } + else + { + sum /= 3; + } + + if (sum < 1) + { + sum = 1; + } + + await session.PlayerEntity.AddBuffAsync(_buffFactory.CreateBuff(spInfo.BuffId + (sum - 1), session.PlayerEntity, BuffFlag.PARTNER)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Buffs/BuffRemoveEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Buffs/BuffRemoveEventHandler.cs new file mode 100644 index 0000000..aab414c --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Buffs/BuffRemoveEventHandler.cs @@ -0,0 +1,314 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.Groups; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage.Configuration; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Buffs.Events; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Extensions.Mates; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Skills; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; +using WingsEmu.Plugins.BasicImplementations.Vehicles; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Buffs; + +public class BuffRemoveEventHandler : IAsyncEventProcessor +{ + private readonly IBuffFactory _buffFactory; + private readonly IGameLanguageService _gameLanguage; + private readonly ISacrificeManager _sacrificeManager; + private readonly ISpPartnerConfiguration _spPartner; + private readonly ITeleportManager _teleportManager; + private readonly IVehicleConfigurationProvider _vehicle; + + public BuffRemoveEventHandler(ISpPartnerConfiguration spPartner, IGameLanguageService gameLanguage, IBuffFactory buffFactory, ITeleportManager teleportManager, + ISacrificeManager sacrificeManager, IVehicleConfigurationProvider vehicle) + { + _spPartner = spPartner; + _gameLanguage = gameLanguage; + _buffFactory = buffFactory; + _teleportManager = teleportManager; + _sacrificeManager = sacrificeManager; + _vehicle = vehicle; + } + + public async Task HandleAsync(BuffRemoveEvent e, CancellationToken cancellation) + { + if (e.Buffs == null) + { + return; + } + + IBattleEntity battleEntity = e.Entity; + + foreach (Buff buff in e.Buffs) + { + if (buff == null) + { + continue; + } + + if (!battleEntity.BuffComponent.HasBuff(buff.BuffId)) + { + continue; + } + + if (buff.IsSavingOnDisconnect() && buff.RemainingTimeInMilliseconds() > 0) + { + continue; + } + + switch (buff.CardId) + { + case (short)BuffVnums.SPIRIT_OF_SACRIFICE: + { + IBattleEntity target = _sacrificeManager.GetTarget(battleEntity); + if (target != null) + { + _sacrificeManager.RemoveSacrifice(battleEntity, target); + } + + break; + } + case (short)BuffVnums.NOBLE_GESTURE: + { + IBattleEntity caster = _sacrificeManager.GetCaster(battleEntity); + if (caster != null) + { + _sacrificeManager.RemoveSacrifice(caster, battleEntity); + } + + break; + } + } + + battleEntity.BuffComponent.RemoveBuff(buff.BuffId); + battleEntity.BCardComponent.RemoveBuffBCards(buff); + battleEntity.ShadowAppears(true, buff); + ProcessEndBuffDamage(battleEntity, buff); + + if (buff.IsConstEffect) + { + battleEntity.BroadcastConstBuffEffect(buff, 0); + } + + if ((buff.IsPartnerBuff() || buff.IsBigBuff() && !buff.IsPartnerBuff() || buff.IsNoDuration() || buff.IsRefreshAtExpiration()) && !e.RemovePermanentBuff) + { + Buff newBuff = _buffFactory.CreateBuff(buff.CardId, buff.Caster, buff.Duration, buff.BuffFlags); + await battleEntity.AddBuffAsync(newBuff); + continue; + } + + if (battleEntity is not IPlayerEntity character) + { + bool buffRunAway = buff.BCards.Any(x => x.Type == (short)BCardType.SpecialActions && x.SubType == (byte)AdditionalTypes.SpecialActions.RunAway); + + switch (battleEntity) + { + case INpcEntity npcEntity: + + if (buffRunAway) + { + npcEntity.IsRunningAway = false; + } + + break; + case IMonsterEntity monsterEntity: + monsterEntity.RefreshStats(); + + if (buffRunAway) + { + monsterEntity.IsRunningAway = false; + } + + break; + case IMateEntity mateEntity: + mateEntity.RefreshStatistics(); + mateEntity.Owner?.Session.SendPetInfo(mateEntity, _gameLanguage); + mateEntity.Owner?.Session.SendCondMate(mateEntity); + break; + } + + continue; + } + + IClientSession session = character.Session; + bool refreshHpMp = true; + + switch (buff.CardId) + { + case (short)BuffVnums.PRAYER_OF_DEFENCE: + case (short)BuffVnums.ENERGY_ENHANCEMENT: + case (short)BuffVnums.BEAR_SPIRIT: + if (e.ShowMessage) + { + break; + } + + refreshHpMp = false; + break; + case (short)BuffVnums.FAIRY_BOOSTER: + session.RefreshFairy(); + break; + case (short)BuffVnums.SPEED_BOOSTER when session.PlayerEntity.IsOnVehicle: + session.PlayerEntity.VehicleSpeed -= (byte)BuffVehicle(session.PlayerEntity); + break; + case (short)BuffVnums.MAGIC_SPELL: + session.SendMsCPacket(0); + session.PlayerEntity.RemoveAngelElement(); + character.CleanComboState(); + for (int i = (int)BuffVnums.FLAME; i < (int)BuffVnums.DARKNESS; i++) + { + if (!session.PlayerEntity.BuffComponent.HasBuff(i)) + { + continue; + } + + session.PlayerEntity.RemoveBuffAsync(false, session.PlayerEntity.BuffComponent.GetBuff(i)).ConfigureAwait(false).GetAwaiter().GetResult(); + } + + break; + case (short)BuffVnums.AMBUSH_PREPARATION_1: + case (short)BuffVnums.AMBUSH_PREPARATION_2: + if (buff.RemainingTimeInMilliseconds() <= 0) + { + character.ChangeScoutState(ScoutStateType.None); + } + + break; + case (short)BuffVnums.AMBUSH_RAID: + character.ChangeScoutState(ScoutStateType.None); + break; + case (short)BuffVnums.AMBUSH: + character.TriggerAmbush = false; + + if (buff.RemainingTimeInMilliseconds() <= 0) + { + character.ChangeScoutState(ScoutStateType.None); + } + + break; + } + + if (!buff.IsBigBuff()) + { + session.SendBfPacket(buff); + } + else + { + session.SendEmptyStaticBuffUiPacket(buff); + } + + if (e.ShowMessage) + { + string message = _gameLanguage.GetLanguage(GameDialogKey.BUFF_CHATMESSAGE_EFFECT_TERMINATED, session.UserLanguage); + string cardName = _gameLanguage.GetLanguage(GameDataType.Card, buff.Name, session.UserLanguage); + + session.SendChatMessage(string.Format(message, cardName), ChatMessageColorType.Buff); + } + + session.RefreshStatChar(refreshHpMp); + session.RefreshStat(); + session.SendCondPacket(); + + if (buff.BCards.Any(x => x.Type == (byte)BCardType.FearSkill && x.SubType == (byte)AdditionalTypes.FearSkill.AttackRangedIncreased)) + { + session.SendIncreaseRange(); + } + + Position position = _teleportManager.GetPosition(session.PlayerEntity.Id); + if (position.X != 0 && position.Y != 0 && buff.CardId == (short)BuffVnums.MEMORIAL) + { + short savedX = _teleportManager.GetPosition(character.Id).X; + short savedY = _teleportManager.GetPosition(character.Id).Y; + _teleportManager.RemovePosition(session.PlayerEntity.Id); + character.BroadcastEffectGround(EffectType.ArchmageTeleportSet, savedX, savedY, true); + } + + await character.CheckAct52Buff(_buffFactory); + + if (buff.BCards.Any(x => x.Type == (short)BCardType.FearSkill && x.SubType == (byte)AdditionalTypes.FearSkill.MoveAgainstWill)) + { + session.SendOppositeMove(false); + } + + if (!buff.BCards.Any(s => + s.Type == (short)BCardType.SpecialActions && s.SubType == (byte)AdditionalTypes.SpecialActions.Hide) + && buff.CardId != (short)BuffVnums.AMBUSH && buff.CardId != (short)BuffVnums.AMBUSH_RAID) + { + continue; + } + + if (buff.CardId == (short)BuffVnums.AMBUSH && character.TriggerAmbush) + { + continue; + } + + if (!session.PlayerEntity.IsOnVehicle) + { + session.BroadcastInTeamMembers(_gameLanguage, _spPartner); + session.RefreshParty(_spPartner); + } + + session.UpdateVisibility(); + } + } + + private void ProcessEndBuffDamage(IBattleEntity battleEntity, Buff buff) + { + if (!battleEntity.EndBuffDamages.Any()) + { + return; + } + + battleEntity.RemoveEndBuffDamage((short)buff.CardId); + } + + private int BuffVehicle(IPlayerEntity c) + { + VehicleConfiguration vehicle = _vehicle.GetByMorph(c.Morph, c.Gender); + + if (vehicle?.VehicleBoostType == null) + { + return 0; + } + + int speedToRemove = 0; + + foreach (VehicleBoost boost in vehicle.VehicleBoostType) + { + switch (boost.BoostType) + { + case BoostType.INCREASE_SPEED: + if (!boost.FirstValue.HasValue) + { + break; + } + + speedToRemove = boost.FirstValue.Value; + break; + case BoostType.CREATE_BUFF_ON_END: + if (!boost.FirstValue.HasValue) + { + break; + } + + c.AddBuffAsync(_buffFactory.CreateBuff(boost.FirstValue.Value, c)).ConfigureAwait(false).GetAwaiter().GetResult(); + break; + } + } + + return speedToRemove; + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/AddAdditionalHpMpEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/AddAdditionalHpMpEventHandler.cs new file mode 100644 index 0000000..b9b37a6 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/AddAdditionalHpMpEventHandler.cs @@ -0,0 +1,59 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Game.SnackFood.Events; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Characters; + +public class AddAdditionalHpMpEventHandler : IAsyncEventProcessor +{ + public async Task HandleAsync(AddAdditionalHpMpEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + IPlayerEntity character = session.PlayerEntity; + + if (!character.IsAlive()) + { + return; + } + + if (e.Hp != 0) + { + int maxHpOverflow = character.MaxHp * e.MaxHpPercentage / 100; + if (character.AdditionalHp + e.Hp > maxHpOverflow) + { + if (character.AdditionalHp < maxHpOverflow) + { + character.AdditionalHp = maxHpOverflow; + } + } + else + { + character.AdditionalHp += e.Hp; + } + } + + if (e.Mp != 0) + { + int maxMpOverflow = character.MaxMp * e.MaxMpPercentage / 100; + + if (character.AdditionalMp + e.Mp > maxMpOverflow) + { + if (character.AdditionalMp < maxMpOverflow) + { + character.AdditionalMp = maxMpOverflow; + } + } + else + { + character.AdditionalMp += e.Mp; + } + } + + session.SendGuriPacket(4); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/AddExpEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/AddExpEventHandler.cs new file mode 100644 index 0000000..7cc8ed6 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/AddExpEventHandler.cs @@ -0,0 +1,125 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Algorithm; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Extensions; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Character; + +namespace WingsEmu.Plugins.BasicImplementations.Quests; + +public class AddExpEventHandler : IAsyncEventProcessor +{ + private readonly ICharacterAlgorithm _characterAlgorithm; + private readonly GameMinMaxConfiguration _gameMinMaxConfiguration; + + public AddExpEventHandler(ICharacterAlgorithm characterAlgorithm, GameMinMaxConfiguration gameMinMaxConfiguration) + { + _characterAlgorithm = characterAlgorithm; + _gameMinMaxConfiguration = gameMinMaxConfiguration; + } + + public async Task HandleAsync(AddExpEvent e, CancellationToken cancellation) + { + IPlayerEntity character = e.Sender.PlayerEntity; + long exp = e.Exp; + LevelType levelType = e.LevelType; + + switch (levelType) + { + case LevelType.Level: + HandleXp(character, exp); + break; + case LevelType.JobLevel: + HandleJobXp(character, exp); + break; + case LevelType.SpJobLevel: + HandleSpJobXp(character, exp); + break; + case LevelType.Heroic: + HandleHeroicXp(character, exp); + break; + } + + character.Session.RefreshStatChar(); + character.Session.RefreshStat(); + character.Session.RefreshLevel(_characterAlgorithm); + } + + private void HandleXp(IPlayerEntity character, long exp) + { + if (character.Level >= _gameMinMaxConfiguration.MaxLevel) + { + return; + } + + character.LevelXp += exp; + if (character.LevelXp < _characterAlgorithm.GetLevelXp(character.Level)) + { + return; + } + + character.Session.EmitEventAsync(new LevelUpEvent { LevelType = LevelType.Level }); + } + + private void HandleJobXp(IPlayerEntity character, long exp) + { + if (character.Class == ClassType.Adventurer && character.JobLevel > 19) + { + return; + } + + if (character.JobLevel >= _gameMinMaxConfiguration.MaxJobLevel) + { + return; + } + + character.JobLevelXp += exp; + if (character.JobLevelXp < _characterAlgorithm.GetJobXp(character.JobLevel)) + { + return; + } + + character.Session.EmitEventAsync(new LevelUpEvent { LevelType = LevelType.JobLevel }); + } + + private void HandleSpJobXp(IPlayerEntity character, long exp) + { + if (character.Specialist == null) + { + return; + } + + if (character.Specialist.SpLevel >= _gameMinMaxConfiguration.MaxSpLevel) + { + return; + } + + character.Specialist.Xp += exp; + if (character.Specialist.Xp < _characterAlgorithm.GetSpecialistJobXp(character.Specialist.SpLevel, character.Specialist.IsFunSpecialist())) + { + return; + } + + character.Session.EmitEventAsync(new LevelUpEvent { LevelType = LevelType.SpJobLevel }); + } + + private void HandleHeroicXp(IPlayerEntity character, long exp) + { + if (character.HeroLevel >= _gameMinMaxConfiguration.MaxHeroLevel) + { + return; + } + + character.HeroXp += exp; + if (character.LevelXp < _characterAlgorithm.GetHeroLevelXp(character.HeroLevel)) + { + return; + } + + character.Session.EmitEventAsync(new LevelUpEvent { LevelType = LevelType.Heroic }); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/AddStaticBonusEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/AddStaticBonusEventHandler.cs new file mode 100644 index 0000000..2acff73 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/AddStaticBonusEventHandler.cs @@ -0,0 +1,16 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Characters; + +public class AddStaticBonusEventHandler : IAsyncEventProcessor +{ + public async Task HandleAsync(AddStaticBonusEvent e, CancellationToken cancellation) + { + e.Sender.PlayerEntity.AddStaticBonus(e.StaticBonusDto); + e.Sender.SendStaticBonuses(); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/BankOpenEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/BankOpenEventHandler.cs new file mode 100644 index 0000000..3ef43b2 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/BankOpenEventHandler.cs @@ -0,0 +1,88 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Characters; + +public class BankOpenEventHandler : IAsyncEventProcessor +{ + private readonly IBankReputationConfiguration _bankReputationConfiguration; + private readonly IGameLanguageService _gameLanguage; + private readonly IRankingManager _rankingManager; + private readonly IReputationConfiguration _reputationConfiguration; + + public BankOpenEventHandler(IGameLanguageService gameLanguage, IReputationConfiguration reputationConfiguration, IBankReputationConfiguration bankReputationConfiguration, + IRankingManager rankingManager) + { + _gameLanguage = gameLanguage; + _reputationConfiguration = reputationConfiguration; + _bankReputationConfiguration = bankReputationConfiguration; + _rankingManager = rankingManager; + } + + public async Task HandleAsync(BankOpenEvent e, CancellationToken cancellation) + { + long? npcId = e.NpcId; + InventoryItem bankCard = e.BankCard; + IClientSession session = e.Sender; + + if (session.PlayerEntity.IsInExchange()) + { + return; + } + + if (session.PlayerEntity.HasShopOpened) + { + return; + } + + if (session.CantPerformActionOnAct4()) + { + return; + } + + if (!session.CurrentMapInstance.HasMapFlag(MapFlags.IS_BASE_MAP)) + { + return; + } + + if (npcId.HasValue) + { + INpcEntity npcEntity = session.CurrentMapInstance.GetNpcById(npcId.Value); + if (npcEntity?.ShopNpc == null) + { + return; + } + + if (npcEntity.ShopNpc.ShopType != (byte)NpcShopType.BANK) + { + return; + } + } + else + { + if (bankCard == null) + { + return; + } + + await session.RemoveItemFromInventory(item: bankCard); + } + + session.SendGbPacket(BankType.OpenBank, _reputationConfiguration, _bankReputationConfiguration, _rankingManager.TopReputation); + session.SendSMemo(SmemoType.BankInfo, session.GetLanguage(GameDialogKey.BANK_LOG_OPEN_BANK)); + session.PlayerEntity.IsBankOpen = true; + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/BattleEntityHealEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/BattleEntityHealEventHandler.cs new file mode 100644 index 0000000..a70a202 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/BattleEntityHealEventHandler.cs @@ -0,0 +1,82 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Mates.Events; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Characters; + +public class BattleEntityHealEventHandler : IAsyncEventProcessor +{ + public async Task HandleAsync(BattleEntityHealEvent e, CancellationToken cancellation) + { + IBattleEntity battleEntity = e.Entity; + + if (!battleEntity.IsAlive()) + { + return; + } + + int maxHp = battleEntity.MaxHp; + int maxMp = battleEntity.MaxMp; + + int hpHeal = battleEntity.Hp + e.HpHeal > maxHp ? maxHp - battleEntity.Hp : e.HpHeal; + int mpHeal = battleEntity.Mp + e.MpHeal > maxMp ? maxMp - battleEntity.Mp : e.MpHeal; + + (int hpToDecrease, int _) = + battleEntity.BCardComponent.GetAllBCardsInformation(BCardType.DamageConvertingSkill, (byte)AdditionalTypes.DamageConvertingSkill.HPRecoveryDecreased, battleEntity.Level); + + (int hpToIncrease, int _) = + battleEntity.BCardComponent.GetAllBCardsInformation(BCardType.DamageConvertingSkill, (byte)AdditionalTypes.DamageConvertingSkill.HPRecoveryIncreased, battleEntity.Level); + + int hpToChange = hpToIncrease - hpToDecrease; + hpHeal = (int)(hpHeal * (1 + hpToChange / 100.0)); + + battleEntity.Hp += hpHeal; + battleEntity.Mp += mpHeal; + + battleEntity.BroadcastHeal(hpHeal); + + if (battleEntity is not IPlayerEntity playerEntity) + { + return; + } + + IClientSession session = playerEntity.Session; + + session.RefreshStat(); + session.RefreshStatInfo(); + + if (!e.HealMates) + { + return; + } + + foreach (IMateEntity mate in session.PlayerEntity.MateComponent.TeamMembers()) + { + (int mateHpToDecrease, int _) = + mate.BCardComponent.GetAllBCardsInformation(BCardType.DamageConvertingSkill, (byte)AdditionalTypes.DamageConvertingSkill.HPRecoveryDecreased, mate.Level); + + (int mateHpToIncrease, int _) = + mate.BCardComponent.GetAllBCardsInformation(BCardType.DamageConvertingSkill, (byte)AdditionalTypes.DamageConvertingSkill.HPRecoveryIncreased, mate.Level); + + int mateHpToChange = mateHpToDecrease - mateHpToIncrease; + int mateHpHeal = (int)(e.HpHeal * (1 + mateHpToChange / 100.0)); + + await session.EmitEventAsync(new MateHealEvent + { + MateEntity = mate, + HpHeal = mateHpHeal, + MpHeal = e.MpHeal + }); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/ChangeClassEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/ChangeClassEventHandler.cs new file mode 100644 index 0000000..6539ec6 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/ChangeClassEventHandler.cs @@ -0,0 +1,269 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.Families; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsAPI.Game.Extensions.Quicklist; +using WingsEmu.DTOs.Quicklist; +using WingsEmu.Game; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Algorithm; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Inventory.Event; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Networking.Broadcasting; +using WingsEmu.Game.Skills; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Character; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Characters; + +public class ChangeClassEventHandler : IAsyncEventProcessor +{ + private readonly IBattleEntityAlgorithmService _algorithm; + private readonly ICharacterAlgorithm _characterAlgorithm; + private readonly IGameItemInstanceFactory _gameItemInstance; + private readonly IGameLanguageService _languageService; + private readonly IRandomGenerator _randomNumberGenerator; + private readonly IRankingManager _rankingManager; + private readonly IReputationConfiguration _reputationConfiguration; + + public ChangeClassEventHandler(IGameLanguageService languageService, IRandomGenerator randomNumberGenerator, + IBattleEntityAlgorithmService algorithm, ICharacterAlgorithm characterAlgorithm, IGameItemInstanceFactory gameItemInstance, IReputationConfiguration reputationConfiguration, + IRankingManager rankingManager) + { + _languageService = languageService; + _randomNumberGenerator = randomNumberGenerator; + _algorithm = algorithm; + _characterAlgorithm = characterAlgorithm; + _gameItemInstance = gameItemInstance; + _reputationConfiguration = reputationConfiguration; + _rankingManager = rankingManager; + } + + public async Task HandleAsync(ChangeClassEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + if (e.ShouldResetJobLevel) + { + session.PlayerEntity.JobLevel = 1; + } + + session.PlayerEntity.JobLevelXp = 0; + session.SendPacket("npinfo 0"); + session.SendPClearPacket(); + + if (e.NewClass == (byte)ClassType.Adventurer) + { + session.PlayerEntity.HairStyle = + (byte)session.PlayerEntity.HairStyle > 1 ? 0 : session.PlayerEntity.HairStyle; + + if (session.PlayerEntity.JobLevel > 20) + { + session.PlayerEntity.JobLevel = 20; + } + } + + session.SendCondPacket(); + session.PlayerEntity.Class = e.NewClass; + session.PlayerEntity.RefreshMaxHpMp(_algorithm); + session.PlayerEntity.Hp = session.PlayerEntity.MaxHp; + session.PlayerEntity.Mp = session.PlayerEntity.MaxMp; + session.BroadcastTitleInfo(); + session.RefreshStat(); + session.RefreshLevel(_characterAlgorithm); + session.SendEqPacket(); + session.BroadcastEffectInRange(EffectType.JobLevelUp); + session.SendMsg(_languageService.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_CLASS_CHANGED, session.UserLanguage), MsgMessageType.Middle); + session.BroadcastEffectInRange(EffectType.Transform); + if (e.ShouldObtainNewFaction && !session.PlayerEntity.IsInFamily()) + { + await session.EmitEventAsync(new ChangeFactionEvent + { + NewFaction = (FactionType)_randomNumberGenerator.RandomNumber(1, 3) + }); + } + + session.BroadcastCMode(); + session.BroadcastIn(_reputationConfiguration, _rankingManager.TopReputation, new ExceptSessionBroadcast(session)); + session.BroadcastGidx(session.PlayerEntity.Family, _languageService); + session.BroadcastEffectInRange(EffectType.NormalLevelUp); + session.BroadcastEffectInRange(EffectType.NormalLevelUpSubEffect); + session.PlayerEntity.Skills.Clear(); + List passivesToRemove = new(); + foreach (CharacterSkill skill in session.PlayerEntity.CharacterSkills.Values) + { + if (skill.Skill.IsPassiveSkill()) + { + int skillMinimumLevel = 0; + if (skill.Skill.MinimumSwordmanLevel == 0 && skill.Skill.MinimumArcherLevel == 0 && skill.Skill.MinimumMagicianLevel == 0) + { + skillMinimumLevel = skill.Skill.MinimumAdventurerLevel; + } + else + { + skillMinimumLevel = session.PlayerEntity.Class switch + { + ClassType.Adventurer => skill.Skill.MinimumAdventurerLevel, + ClassType.Swordman => skill.Skill.MinimumSwordmanLevel, + ClassType.Archer => skill.Skill.MinimumArcherLevel, + ClassType.Magician => skill.Skill.MinimumMagicianLevel, + _ => skillMinimumLevel + }; + } + + if (skillMinimumLevel == 0) + { + passivesToRemove.Add(skill); + } + + continue; + } + + session.PlayerEntity.CharacterSkills.TryRemove(skill.SkillVNum, out CharacterSkill value); + if (session.PlayerEntity.SkillComponent.SkillUpgrades.TryGetValue((short)skill.SkillVNum, out HashSet upgrades)) + { + upgrades.Clear(); + } + } + + foreach (CharacterSkill passive in passivesToRemove) + { + session.PlayerEntity.CharacterSkills.TryRemove(passive.Skill.Id, out _); + } + + CharacterSkill newSkill; + var skillsToAdd = new List(); + + if (session.PlayerEntity.Class != ClassType.Wrestler) + { + int skillCatch = session.PlayerEntity.Class switch + { + ClassType.Adventurer => 209, + ClassType.Swordman => 235, + ClassType.Archer => 236, + ClassType.Magician => 237 + }; + + newSkill = new CharacterSkill + { + SkillVNum = (short)(200 + 20 * (byte)session.PlayerEntity.Class) + }; + + skillsToAdd.Add(newSkill); + + session.PlayerEntity.CharacterSkills[(short)(200 + 20 * (byte)session.PlayerEntity.Class)] = newSkill; + + newSkill = new CharacterSkill + { + SkillVNum = (short)(201 + 20 * (byte)session.PlayerEntity.Class) + }; + + skillsToAdd.Add(newSkill); + + session.PlayerEntity.CharacterSkills[(short)(201 + 20 * (byte)session.PlayerEntity.Class)] = newSkill; + + newSkill = new CharacterSkill + { + SkillVNum = skillCatch + }; + + skillsToAdd.Add(newSkill); + + session.PlayerEntity.CharacterSkills[skillCatch] = newSkill; + } + else + { + newSkill = new CharacterSkill + { + SkillVNum = 1525 + }; + + skillsToAdd.Add(newSkill); + + session.PlayerEntity.CharacterSkills[1525] = newSkill; + + newSkill = new CharacterSkill + { + SkillVNum = 1529 + }; + + skillsToAdd.Add(newSkill); + + session.PlayerEntity.CharacterSkills[1529] = newSkill; + + newSkill = new CharacterSkill + { + SkillVNum = 1565 + }; + + skillsToAdd.Add(newSkill); + + session.PlayerEntity.CharacterSkills[1565] = newSkill; + } + + session.PlayerEntity.ClearSkillCooldowns(); + + foreach (CharacterQuicklistEntryDto remove in session.PlayerEntity.QuicklistComponent.GetQuicklist().Where(x => x.Morph == 0).ToList()) + { + session.PlayerEntity.QuicklistComponent.RemoveQuicklist(remove.QuicklistTab, remove.QuicklistSlot, 0); + } + + session.PlayerEntity.Skills.AddRange(skillsToAdd); + session.RefreshPassiveBCards(); + session.RefreshSkillList(); + session.RefreshQuicklist(); + + if (!e.ShouldObtainBasicItems) + { + return; + } + + GameItemInstance mainWeapon = _gameItemInstance.CreateItem((short)(4 + (byte)e.NewClass * 14)); + InventoryItem item = await session.AddNewItemToInventory(mainWeapon); + await session.EmitEventAsync(new InventoryEquipItemEvent(item.Slot)); + + GameItemInstance secondWeapon; + GameItemInstance otherItem; + + switch (e.NewClass) + { + case ClassType.Swordman: + secondWeapon = _gameItemInstance.CreateItem(68); + item = await session.AddNewItemToInventory(secondWeapon); + await session.EmitEventAsync(new InventoryEquipItemEvent(item.Slot)); + + otherItem = _gameItemInstance.CreateItem(2082, 10); + await session.AddNewItemToInventory(otherItem); + break; + + case ClassType.Archer: + secondWeapon = _gameItemInstance.CreateItem(78); + item = await session.AddNewItemToInventory(secondWeapon); + await session.EmitEventAsync(new InventoryEquipItemEvent(item.Slot)); + + otherItem = _gameItemInstance.CreateItem(2083, 10); + await session.AddNewItemToInventory(otherItem); + break; + + case ClassType.Magician: + secondWeapon = _gameItemInstance.CreateItem(86); + item = await session.AddNewItemToInventory(secondWeapon); + await session.EmitEventAsync(new InventoryEquipItemEvent(item.Slot)); + break; + } + + GameItemInstance armor = _gameItemInstance.CreateItem((short)(81 + (byte)e.NewClass * 13)); + item = await session.AddNewItemToInventory(armor); + await session.EmitEventAsync(new InventoryEquipItemEvent(item.Slot)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/ChangeFactionEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/ChangeFactionEventHandler.cs new file mode 100644 index 0000000..e6ed9a7 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/ChangeFactionEventHandler.cs @@ -0,0 +1,44 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Algorithm; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Characters; + +public class ChangeFactionEventHandler : IAsyncEventProcessor +{ + private readonly ICharacterAlgorithm _characterAlgorithm; + private readonly IGameLanguageService _gameLanguage; + + public ChangeFactionEventHandler(IGameLanguageService gameLanguage, ICharacterAlgorithm characterAlgorithm) + { + _gameLanguage = gameLanguage; + _characterAlgorithm = characterAlgorithm; + } + + public async Task HandleAsync(ChangeFactionEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + session.PlayerEntity.SetFaction(e.NewFaction); + GameDialogKey factionMessageKey = session.PlayerEntity.Faction == 0 + ? GameDialogKey.INTERACTION_SHOUTMESSAGE_GET_PROTECTION_POWER_NEUTRAL + : session.PlayerEntity.Faction == FactionType.Angel + ? GameDialogKey.INTERACTION_SHOUTMESSAGE_GET_PROTECTION_POWER_ANGEL + : GameDialogKey.INTERACTION_SHOUTMESSAGE_GET_PROTECTION_POWER_DEMON; + session.SendMsg(_gameLanguage.GetLanguage(factionMessageKey, session.UserLanguage), + MsgMessageType.Middle); + session.SendPacket("scr 0 0 0 0 0 0"); + session.RefreshFaction(); + session.RefreshStatChar(); + session.SendEffect(session.PlayerEntity.Faction == FactionType.Demon ? EffectType.DemonProtection : EffectType.AngelProtection); + session.SendCondPacket(); + session.RefreshLevel(_characterAlgorithm); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/CharacterBonusExpiredEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/CharacterBonusExpiredEventHandler.cs new file mode 100644 index 0000000..d044198 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/CharacterBonusExpiredEventHandler.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.DTOs.Bonus; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Characters; + +public class CharacterBonusExpiredEventHandler : IAsyncEventProcessor +{ + public async Task HandleAsync(CharacterBonusExpiredEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + + DateTime currentTime = DateTime.UtcNow; + + Queue toRemove = new(); + foreach (CharacterStaticBonusDto staticBonus in session.PlayerEntity.Bonus.ToArray()) + { + if (staticBonus.DateEnd == null) + { + continue; + } + + if (staticBonus.DateEnd.Value >= currentTime) + { + continue; + } + + toRemove.Enqueue(staticBonus); + } + + while (toRemove.TryDequeue(out CharacterStaticBonusDto bonus)) + { + session.PlayerEntity.Bonus.Remove(bonus); + + switch (bonus.StaticBonusType) + { + case StaticBonusType.InventoryExpansion: + case StaticBonusType.Backpack: + session.ShowInventoryExtensions(); + break; + case StaticBonusType.PetBasket: + session.SendPetBasketPacket(false); + break; + } + } + + session.SendStaticBonuses(); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/CharacterLoadEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/CharacterLoadEventHandler.cs new file mode 100644 index 0000000..76ecb72 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/CharacterLoadEventHandler.cs @@ -0,0 +1,958 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.DAL.Redis.Locks; +using PhoenixLib.Events; +using PhoenixLib.Logging; +using PhoenixLib.ServiceBus; +using WingsAPI.Communication; +using WingsAPI.Communication.DbServer.AccountService; +using WingsAPI.Communication.DbServer.CharacterService; +using WingsAPI.Communication.Player; +using WingsAPI.Communication.ServerApi; +using WingsAPI.Communication.ServerApi.Protocol; +using WingsAPI.Communication.Sessions; +using WingsAPI.Communication.Sessions.Model; +using WingsAPI.Communication.Sessions.Request; +using WingsAPI.Communication.Sessions.Response; +using WingsAPI.Data.Account; +using WingsAPI.Data.Character; +using WingsAPI.Game.Extensions.Families; +using WingsAPI.Game.Extensions.MinilandExtensions; +using WingsAPI.Game.Extensions.Quests; +using WingsAPI.Game.Extensions.Quicklist; +using WingsAPI.Packets.Enums; +using WingsEmu.DTOs.Bonus; +using WingsEmu.DTOs.Buffs; +using WingsEmu.DTOs.Inventory; +using WingsEmu.DTOs.Items; +using WingsEmu.DTOs.Maps; +using WingsEmu.DTOs.Mates; +using WingsEmu.DTOs.Quests; +using WingsEmu.DTOs.Quicklist; +using WingsEmu.DTOs.Skills; +using WingsEmu.DTOs.Titles; +using WingsEmu.Game; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Algorithm; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Compliments; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Extensions.Mates; +using WingsEmu.Game.Families; +using WingsEmu.Game.Families.Configuration; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Mates.Events; +using WingsEmu.Game.Miniland; +using WingsEmu.Game.Miniland.Minigames; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Quests; +using WingsEmu.Game.Quests.Event; +using WingsEmu.Game.Raids.Events; +using WingsEmu.Game.RainbowBattle.Event; +using WingsEmu.Game.Revival; +using WingsEmu.Game.Skills; +using WingsEmu.Game.TimeSpaces; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; +using WingsEmu.Plugins.DistributedGameEvents.PlayerEvents; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Characters; + +public class CharacterLoadEventHandler : IAsyncEventProcessor +{ + private static readonly SemaphoreSlim _semaphoreSlim = new(1, 1); + private readonly IAccountService _accountService; + private readonly IBCardEffectHandlerContainer _bcardHandlerContainer; + private readonly IBuffFactory _buffFactory; + private readonly ICharacterAlgorithm _characterAlgorithm; + private readonly ICharacterService _characterService; + private readonly FamilyConfiguration _familyConfiguration; + private readonly IFamilyManager _familyManager; + private readonly SerializableGameServer _gameServer; + private readonly IGameItemInstanceFactory _itemInstanceFactory; + private readonly IItemsManager _itemsManager; + private readonly IGameLanguageService _language; + private readonly IExpirableLockService _lock; + private readonly IMapManager _mapManager; + private readonly IMateEntityFactory _mateEntityFactory; + private readonly IMessagePublisher _messagePublisher; + private readonly IMinilandManager _minilandManager; + private readonly IPlayerEntityFactory _playerEntityFactory; + private readonly IQuestFactory _questFactory; + private readonly IQuestManager _questManager; + private readonly IRandomGenerator _randomGenerator; + private readonly IRankingManager _rankingManager; + private readonly IReputationConfiguration _reputationConfiguration; + private readonly IRevivalManager _revivalManager; + private readonly IServerApiService _serverApiService; + + + private readonly IServerManager _serverManager; + private readonly ISessionManager _sessionManager; + private readonly ISessionService _sessionService; + private readonly ISubActConfiguration _subActConfiguration; + private readonly ITimeSpaceConfiguration _timeSpaceConfiguration; + + public CharacterLoadEventHandler(IServerManager serverManager, IGameLanguageService language, SerializableGameServer gameServer, IBuffFactory buffFactory, ISessionManager sessionManager, + ICharacterAlgorithm characterAlgorithm, IMinilandManager minilandManager, IMessagePublisher messagePublisher, IRevivalManager revivalManager, + IQuestManager questManager, FamilyConfiguration familyConfiguration, IServerApiService serverApiService, IRandomGenerator randomGenerator, IMapManager mapManager, + IReputationConfiguration reputationConfiguration, IItemsManager itemsManager, ISubActConfiguration subActConfiguration, IRankingManager rankingManager, + IExpirableLockService @lock, ITimeSpaceConfiguration timeSpaceConfiguration, ISessionService sessionService, IGameItemInstanceFactory itemInstanceFactory, + ICharacterService characterService, IPlayerEntityFactory playerEntityFactory, IMateEntityFactory mateEntityFactory, IFamilyManager familyManager, + IBCardEffectHandlerContainer bcardHandlerContainer, IQuestFactory questFactory, IAccountService accountService) + { + _serverManager = serverManager; + _language = language; + _gameServer = gameServer; + _buffFactory = buffFactory; + _sessionManager = sessionManager; + _characterAlgorithm = characterAlgorithm; + _minilandManager = minilandManager; + _messagePublisher = messagePublisher; + _revivalManager = revivalManager; + _questManager = questManager; + _familyConfiguration = familyConfiguration; + _serverApiService = serverApiService; + _randomGenerator = randomGenerator; + _mapManager = mapManager; + _reputationConfiguration = reputationConfiguration; + _itemsManager = itemsManager; + _subActConfiguration = subActConfiguration; + _rankingManager = rankingManager; + _lock = @lock; + _timeSpaceConfiguration = timeSpaceConfiguration; + _sessionService = sessionService; + _itemInstanceFactory = itemInstanceFactory; + _characterService = characterService; + _playerEntityFactory = playerEntityFactory; + _mateEntityFactory = mateEntityFactory; + _familyManager = familyManager; + _bcardHandlerContainer = bcardHandlerContainer; + _questFactory = questFactory; + _accountService = accountService; + } + + public async Task HandleAsync(CharacterLoadEvent e, CancellationToken cancellation) + { + await _semaphoreSlim.WaitAsync(); + try + { + IClientSession session = e.Sender; + + byte? characterSlot = session.SelectedCharacterSlot; + + if (characterSlot is null) + { + session.ForceDisconnect(); + return; + } + + if (session.HasCurrentMapInstance || session.HasSelectedCharacter) + { + return; + } + + session.SelectedCharacterSlot = null; + DbServerGetCharacterResponse response = await _characterService.GetCharacterBySlot(new DbServerGetCharacterFromSlotRequest + { + AccountId = session.Account.Id, + Slot = characterSlot.Value + }); + + if (response is not { RpcResponseType: RpcResponseType.SUCCESS } || response.CharacterDto is null) + { + session.ForceDisconnect(); + return; + } + + SessionResponse sessionResponse = await _sessionService.GetSessionByAccountId(new GetSessionByAccountIdRequest { AccountId = session.Account.Id }); + if (sessionResponse is not { ResponseType: RpcResponseType.SUCCESS }) + { + await session.NotifyStrangeBehavior(StrangeBehaviorSeverity.DANGER, + $"[POTENTIAL_DUPE][LOAD] sessionResponse not success => characterName: {response.CharacterDto.Name} | master: {session.Account.MasterAccountId} | hardwareId: {session.HardwareId} on channel {_serverManager.ChannelId}"); + session.ForceDisconnect(); + return; + } + + Session storedSession = sessionResponse.Session; + if (storedSession.ChannelId != _serverManager.ChannelId) + { + await session.NotifyStrangeBehavior(StrangeBehaviorSeverity.DANGER, + $"[POTENTIAL_DUPE][LOAD] Wrong session ChannelId => characterName: {response.CharacterDto.Name} | master: {session.Account.MasterAccountId} | hardwareId: {session.HardwareId} on channel {_serverManager.ChannelId}"); + session.ForceDisconnect(); + return; + } + + if (storedSession.EncryptionKey != session.SessionId) + { + await session.NotifyStrangeBehavior(StrangeBehaviorSeverity.DANGER, + $"[POTENTIAL_DUPE][LOAD] Wrong encryption key => characterName: {response.CharacterDto.Name} | master: {session.Account.MasterAccountId} | hardwareId: {session.HardwareId} on channel {_serverManager.ChannelId}"); + session.ForceDisconnect(); + return; + } + + + AccountPenaltyGetAllResponse penaltiesResponse = null; + try + { + penaltiesResponse = await _accountService.GetAccountPenalties(new AccountPenaltyGetRequest + { + AccountId = session.Account.Id + }); + } + catch (Exception ex) + { + Log.Error("[CHARACTER_PRELOAD] Unexpected error: ", ex); + } + + if (penaltiesResponse?.ResponseType != RpcResponseType.SUCCESS) + { + Log.Error("penaltiesResponse.AccountPenaltyDtos", new Exception()); + session.ForceDisconnect(); + return; + } + + session.Account.Logs = penaltiesResponse.AccountPenaltyDtos == null ? new List() : penaltiesResponse.AccountPenaltyDtos.ToList(); + + IPlayerEntity playerEntity = _playerEntityFactory.CreatePlayerEntity(response.CharacterDto); + playerEntity.GameStartDate = DateTime.UtcNow; + playerEntity.MapInstanceId = _mapManager.GetBaseMapInstanceIdByMapId(playerEntity.MapId); + IMapInstance currentMap = _mapManager.GetMapInstance(playerEntity.MapInstanceId); + + if (currentMap != null && currentMap.IsBlockedZone(playerEntity.MapX, playerEntity.MapY)) + { + Position pos = currentMap.GetRandomPosition(); + playerEntity.ChangePosition(pos); + } + else + { + playerEntity.ChangePosition(new Position(playerEntity.MapX, playerEntity.MapY)); + } + + playerEntity.Authority = session.Account.Authority; + + + if (response.CharacterDto.ReturnPoint != null) + { + if (response.CharacterDto.ReturnPoint.MapId == 10000 && !session.IsGameMaster()) + { + response.CharacterDto.ReturnPoint.MapId = 1; + response.CharacterDto.ReturnPoint.MapX = 1; + response.CharacterDto.ReturnPoint.MapY = 1; + } + } + + playerEntity.HomeComponent.ChangeReturn(response.CharacterDto.ReturnPoint); + playerEntity.HomeComponent.ChangeAct5Respawn(response.CharacterDto.Act5RespawnType); + playerEntity.HomeComponent.ChangeRespawn(response.CharacterDto.RespawnType); + + LoadQuicklist(playerEntity); + LoadInventory(playerEntity); + LoadSkills(playerEntity); + LoadScripts(playerEntity); + LoadQuests(playerEntity); + LoadTitleBCards(playerEntity); + + sessionResponse = await _sessionService.GetSessionByAccountId(new GetSessionByAccountIdRequest { AccountId = session.Account.Id }); + if (sessionResponse is not { ResponseType: RpcResponseType.SUCCESS }) + { + await session.NotifyStrangeBehavior(StrangeBehaviorSeverity.DANGER, + $"[POTENTIAL_DUPE][LOAD] 2nd check sessionResponse not success => characterName: {response.CharacterDto.Name} | master: {session.Account.MasterAccountId} | hardwareId: {session.HardwareId} on channel {_serverManager.ChannelId}"); + session.ForceDisconnect(); + return; + } + + storedSession = sessionResponse.Session; + if (storedSession.ChannelId != _serverManager.ChannelId) + { + await session.NotifyStrangeBehavior(StrangeBehaviorSeverity.DANGER, + $"[POTENTIAL_DUPE][LOAD] 2nd check Wrong session ChannelId => characterName: {response.CharacterDto.Name} | master: {session.Account.MasterAccountId} | hardwareId: {session.HardwareId} on channel {_serverManager.ChannelId}"); + session.ForceDisconnect(); + return; + } + + if (storedSession.EncryptionKey != session.SessionId) + { + await session.NotifyStrangeBehavior(StrangeBehaviorSeverity.DANGER, + $"[POTENTIAL_DUPE][LOAD] 2nd check Wrong encryption key => characterName: {response.CharacterDto.Name} | master: {session.Account.MasterAccountId} | hardwareId: {session.HardwareId} on channel {_serverManager.ChannelId}"); + session.ForceDisconnect(); + return; + } + + IClientSession tmp = _sessionManager.GetSessionByCharacterId(playerEntity.Id); + if (tmp != null) + { + await session.NotifyStrangeBehavior(StrangeBehaviorSeverity.DANGER, $"[POTENTIAL_DUPE][LOAD] {playerEntity.Name} tried to connect twice on {_serverManager.ChannelId}!"); + tmp.ForceDisconnect(); + session.ForceDisconnect(); + return; + } + + // here everything starts + session.InitializePlayerEntity(playerEntity); + + playerEntity.Miniland ??= _minilandManager.CreateMinilandByCharacterSession(session); + + FamilyMembership familyMember = _familyManager.GetFamilyMembershipByCharacterId(session.PlayerEntity.Id); + playerEntity.SetFamilyMembership(familyMember); + + foreach (MateDTO mateDto in session.PlayerEntity.NosMates) + { + IMateEntity mate = _mateEntityFactory.CreateMateEntity(session.PlayerEntity, mateDto); + + await session.EmitEventAsync(new MateInitializeEvent + { + MateEntity = mate, + IsOnCharacterEnter = true + }); + } + + LoadPartnerWarehouse(playerEntity); + LoadPartnerInventory(playerEntity); + LoadPartnerEqBCards(playerEntity); + + await session.EmitEventAsync(new MinigameRefreshProductionEvent(false)); + await session.EmitEventAsync(new QuestDailyRefreshEvent { Force = false }); + await session.EmitEventAsync(new ComplimentsMonthlyRefreshEvent { Force = false }); + await session.EmitEventAsync(new RainbowBattleLeaverBusterRefreshEvent { Force = false }); + await session.EmitEventAsync(new SpecialistRefreshEvent { Force = false }); + await session.EmitEventAsync(new RaidResetRestrictionEvent()); + + session.CurrentMapInstance = session.PlayerEntity.MapInstance; + + // need to improve this logic + if (await PreLoadLogicAndCheckChannelChange(session)) + { + Log.Warn($"[CHARACTER_LOAD] {session.PlayerEntity.Name} changed to another channel"); + return; + } + + IFamily family = session.PlayerEntity.Family; + + await ExecuteInitialMapJoin(session, family); + + await TryToSendSomeWarningMessages(session); + + // todo dump + _sessionManager.AddOnline(new ClusterCharacterInfo + { + ChannelId = (byte?)_gameServer.ChannelId, + Id = session.PlayerEntity.Id, + Name = session.PlayerEntity.Name, + Class = session.PlayerEntity.Class, + Gender = session.PlayerEntity.Gender, + HeroLevel = session.PlayerEntity.HeroLevel, + Level = session.PlayerEntity.Level, + HardwareId = session.HardwareId + }); + + if (family != null) + { + FamilyPacketExtensions.SendFamilyMembersInfoToMembers(family, _sessionManager, _familyConfiguration); + } + + await _messagePublisher.PublishAsync(new PlayerConnectedOnChannelMessage + { + ChannelId = _gameServer.ChannelId, + CharacterId = session.PlayerEntity.Id, + CharacterName = session.PlayerEntity.Name, + Class = session.PlayerEntity.Class, + Gender = session.PlayerEntity.Gender, + HeroLevel = session.PlayerEntity.HeroLevel, + Level = session.PlayerEntity.Level, + FamilyId = family?.Id, + HardwareId = session.HardwareId + }, cancellation); + } + finally + { + _semaphoreSlim.Release(); + } + } + + private async Task PreLoadLogicAndCheckChannelChange(IClientSession session) + { + await RefreshRevivalInfo(session); + + await LoadBuffs(session); + + if (session.CurrentMapInstance != null && !session.CurrentMapInstance.HasMapFlag(MapFlags.ACT_4)) + { + return false; + } + + if (session.PlayerEntity.Faction == FactionType.Neutral) + { + return false; + } + + GetChannelInfoResponse response = null; + try + { + response = await _serverApiService.GetAct4ChannelInfo(new GetAct4ChannelInfoRequest + { + WorldGroup = _serverManager.ServerGroup + }); + } + catch (Exception e) + { + Log.Error("[CHARACTER_LOAD] Unexpected error while trying to obtain channel's Act4 information: ", e); + } + + SerializableGameServer gameServer = response?.GameServer; + + if (response?.ResponseType != RpcResponseType.SUCCESS || gameServer == null) + { + session.ChangeMap(1, 149, 60); + return false; + } + + if (gameServer.ChannelId == _gameServer.ChannelId) + { + return false; + } + + short mapId = session.PlayerEntity.Faction == FactionType.Angel ? (short)MapIds.ACT4_ANGEL_CITADEL : (short)MapIds.ACT4_DEMON_CITADEL; + short mapX = (short)(12 + _randomGenerator.RandomNumber(-2, 3)); + short mapY = (short)(40 + _randomGenerator.RandomNumber(-2, 3)); + + if (_mapManager.HasMapFlagByMapId(session.PlayerEntity.MapId, MapFlags.ACT_4)) + { + mapId = (short)session.PlayerEntity.MapId; + mapX = session.PlayerEntity.MapX; + mapY = session.PlayerEntity.MapY; + } + + await session.EmitEventAsync(new PlayerChangeChannelEvent(gameServer, ItModeType.ToAct4, mapId, mapX, mapY)); + return true; + } + + private async Task LoadBuffs(IClientSession session) + { + var toAdd = new List(); + var toRemove = new List(); + foreach (CharacterStaticBuffDto buff in session.PlayerEntity.StaticBuffs) + { + if (buff.RemainingTime <= 0) + { + toRemove.Add(buff); + continue; + } + + toAdd.Add(_buffFactory.CreateBuff(buff.CardId, session.PlayerEntity, TimeSpan.FromMilliseconds(buff.RemainingTime), BuffFlag.BIG_AND_KEEP_ON_LOGOUT)); + } + + await session.PlayerEntity.AddBuffsAsync(toAdd); + foreach (CharacterStaticBuffDto removeBuff in toRemove) + { + session.PlayerEntity.StaticBuffs.Remove(removeBuff); + } + } + + private async Task RefreshRevivalInfo(IClientSession session) + { + foreach (IMateEntity mate in session.PlayerEntity.MateComponent.GetMates()) + { + _revivalManager.TryUnregisterRevival(mate.Id); + } + + if (session.PlayerEntity.IsAlive()) + { + return; + } + + await session.EmitEventAsync(new RevivalReviveEvent(RevivalType.DontPayRevival) + { + Forced = ForcedType.Reconnect + }); + } + + + private async Task TryToSendSomeWarningMessages(IClientSession session) + { + await session.EmitEventAsync(new InventoryExpiredItemsEvent()); + + await session.EmitEventAsync(new CharacterBonusExpiredEvent()); + + if (session.PlayerEntity.HaveStaticBonus(StaticBonusType.BazaarMedalGold) || session.PlayerEntity.HaveStaticBonus(StaticBonusType.BazaarMedalSilver)) + { + session.SendChatMessage(_language.GetLanguage(GameDialogKey.INFORMATION_CHATMESSAGE_ON_LOGIN_BAZAAR_MEDAL, session.UserLanguage), ChatMessageColorType.Green); + } + + if (!string.IsNullOrWhiteSpace(session.PlayerEntity.Family?.Message)) + { + session.SendInfo("--- Family Message ---\n" + session.PlayerEntity.Family.Message); + } + + foreach (IMateEntity mate in session.PlayerEntity.MateComponent.GetMates()) + { + session.SendCondMate(mate); + if (mate.IsTeamMember) + { + await session.EmitEventAsync(new MateJoinTeamEvent { MateEntity = mate, IsOnCharacterEnter = true }); + continue; + } + + await session.EmitEventAsync(new MateStayInsideMinilandEvent { MateEntity = mate, IsOnCharacterEnter = true }); + } + + if (session.PlayerEntity.HaveStaticBonus(StaticBonusType.PetBasket)) + { + session.SendPetBasketPacket(true); + } + + if (_gameServer.ChannelType == GameChannelType.ACT_4) + { + bool isLosingRep = await _lock.ExistsTemporaryLock($"game:locks:character:{session.PlayerEntity.Id}:act-4-less-rep"); + if (isLosingRep) + { + session.PlayerEntity.IsGettingLosingReputation = true; + session.SendChatMessage(session.GetLanguage(GameDialogKey.ACT4_CHATMESSAGE_LESS_REPUTATION), ChatMessageColorType.Red); + } + } + + AccountPenaltyDto mute = session.Account.Logs.FirstOrDefault(x => x.PenaltyType == PenaltyType.Muted && x.RemainingTime.HasValue); + if (mute == null) + { + return; + } + + DateTime now = DateTime.UtcNow; + session.PlayerEntity.MuteRemainingTime = TimeSpan.FromSeconds(mute.RemainingTime ?? 0); + session.PlayerEntity.LastChatMuteMessage = DateTime.MinValue; + session.PlayerEntity.LastMuteTick = now; + } + + private async Task ExecuteInitialMapJoin(IClientSession session, IFamily family) + { + session.RefreshSkillList(); + + session.SendRsfiPacket(_subActConfiguration, _timeSpaceConfiguration); + + IReadOnlyList topReputation = _rankingManager.TopReputation; + session.RefreshReputation(_reputationConfiguration, topReputation); + session.RefreshPassiveBCards(); + session.SendTitlePacket(); + session.SendIncreaseRange(); + + session.ShowInventoryExtensions(); + session.SendStaticBonuses(); + session.SendMinilandPrivateInformation(_minilandManager, _language); + + session.SendTitlePacket(); + + session.SendZzimPacket(); + session.SendTwkPacket(); + + session.RefreshFaction(); + session.SendMessageUnderChat(); + + session.SendScpStcPacket(); + session.SendScpPackets(); + session.SendScnPackets(); + + session.SendStartStartupInventory(); + + session.RefreshGold(); + + session.SendClinitPacket(_rankingManager.TopCompliment); + session.SendFlinitPacket(topReputation); + session.SendKdlinitPacket(_rankingManager.TopPoints); + + session.SendPacket("skyinit 0"); + session.SendPacket("skyinit 1"); + session.SendPacket("skyinit 2"); + session.SendPacket("skyinit 3"); + session.SendPacket("tbf 1.0 2.0 3.0 4.0 5.0"); + + session.SendScpPackets(); + session.SendScnPackets(); + + session.RefreshSpPoint(); + session.SendPacket("rage 0 250000"); + session.SendPacket("rank_cool 0 0 18000"); + + bool changeMap = false; + if (family != null) + { + if ((byte)session.PlayerEntity.Faction != family.Faction) + { + await session.EmitEventAsync(new ChangeFactionEvent + { + NewFaction = family.Faction == (byte)FactionType.Angel ? FactionType.Angel : FactionType.Demon + }); + + if (session.CurrentMapInstance.HasMapFlag(MapFlags.ACT_4)) + { + changeMap = true; + } + } + + session.BroadcastGidx(family, _language); + session.RefreshFamilyInfo(family, _familyConfiguration); + session.SendFamilyLogsToMember(family); + session.RefreshFamilyMembers(_sessionManager, family); + session.RefreshFamilyMembersMessages(family); + session.RefreshFamilyMembersExp(family); + session.SendFmiPacket(); + session.SendFmpPacket(_itemsManager); + } + + session.RefreshQuicklist(); + + session.ChangeToLastBaseMap(); + + session.SendPacket("scr 0 0 0 0 0 0"); + + await ExecuteQuestLogic(session); + + if (!changeMap) + { + return; + } + + await session.Respawn(); + } + + private async Task ExecuteQuestLogic(IClientSession session) + { + CompletedScriptsDto lastCompletedScriptDto = session.PlayerEntity.GetLastCompletedScript(); + if (lastCompletedScriptDto == null) + { + session.SendScriptPacket(1, 10); + return; + } + + + TutorialDto lastCompletedTutorial = _questManager.GetScriptTutorialByIndex(lastCompletedScriptDto.ScriptId, lastCompletedScriptDto.ScriptIndex); + + + if (lastCompletedTutorial == null) + { + return; + } + + TutorialDto nextScript = _questManager.GetScriptTutorialById(lastCompletedTutorial.Id + 1); + + // We check if it was the last script from that sub-questline, for sending a QnpcPacket instead of a new script + if (lastCompletedTutorial.ScriptIndex == _questManager.GetScriptsTutorialByScriptId(lastCompletedTutorial.ScriptId).Max(s => s.ScriptIndex)) + { + QuestNpcDto questNpc = _questManager.GetQuestNpcByScriptId(lastCompletedTutorial.ScriptId + 1); + session.SendQnpcPacket(questNpc.Level, questNpc.NpcVnum, questNpc.MapId); + await CheckCompletedQuests(session); + return; + } + + // We check if we have an active quest-script + CompletedScriptsDto lastCompletedQuestStartScript = session.PlayerEntity.GetLastCompletedScriptByType(TutorialActionType.START_QUEST); + if (lastCompletedQuestStartScript != null) + { + TutorialDto lastCompletedQuestStartTutorial = _questManager.GetScriptTutorialByIndex(lastCompletedQuestStartScript.ScriptId, lastCompletedQuestStartScript.ScriptIndex); + TutorialDto waitQuestCompletionScript = _questManager.GetScriptsTutorialByType(TutorialActionType.WAIT_FOR_QUEST_COMPLETION) + .First(s => s.Data == lastCompletedQuestStartTutorial.Data && s.Id > lastCompletedQuestStartTutorial.Id); + if (!session.PlayerEntity.HasCompletedScriptByIndex(waitQuestCompletionScript.ScriptId, waitQuestCompletionScript.ScriptIndex)) + { + if (!session.PlayerEntity.HasQuestWithId(lastCompletedQuestStartTutorial.Data)) + { + session.PlayerEntity.RemoveCompletedScript(lastCompletedQuestStartTutorial.ScriptId, lastCompletedQuestStartTutorial.ScriptIndex); + var completedScriptsToRemove = session.PlayerEntity.GetCompletedScripts().Where(s => s.ScriptId > lastCompletedQuestStartTutorial.ScriptId || + s.ScriptId == lastCompletedQuestStartTutorial.ScriptId && s.ScriptIndex > lastCompletedQuestStartTutorial.ScriptIndex).ToList(); + foreach (CompletedScriptsDto completedScript in completedScriptsToRemove) + { + session.PlayerEntity.RemoveCompletedScript(completedScript.ScriptId, completedScript.ScriptIndex); + } + + nextScript = lastCompletedQuestStartTutorial; + } + else + { + nextScript = waitQuestCompletionScript; + } + } + } + + session.SendScriptPacket(nextScript.ScriptId, nextScript.ScriptIndex); + await CheckCompletedQuests(session); + } + + private async Task CheckCompletedQuests(IClientSession session) + { + foreach (CharacterQuest characterQuest in session.PlayerEntity.GetCurrentQuests()) + { + if (session.PlayerEntity.IsQuestCompleted(characterQuest)) + { + await session.EmitEventAsync(new QuestCompletedEvent(characterQuest, true)); + } + } + + session.RefreshQuestList(_questManager, null); + session.SendQuestsTargets(); + session.SendSqstPackets(_questManager); + } + + private void LoadSkills(IPlayerEntity session) + { + foreach (CharacterSkillDTO skillDto in session.LearnedSkills) + { + if (session.CharacterSkills.ContainsKey(skillDto.SkillVNum)) + { + continue; + } + + var skill = new CharacterSkill + { + SkillVNum = skillDto.SkillVNum + }; + + session.CharacterSkills[skill.SkillVNum] = skill; + session.Skills.Add(skill); + + short upgradeSkill = skill.Skill?.UpgradeSkill ?? 0; + + if (upgradeSkill == 0) + { + continue; + } + + if (!session.SkillComponent.SkillUpgrades.TryGetValue(upgradeSkill, out HashSet hashSet)) + { + hashSet = new HashSet(); + session.SkillComponent.SkillUpgrades[upgradeSkill] = hashSet; + } + + if (hashSet.Contains(skill)) + { + continue; + } + + hashSet.Add(skill); + } + } + + private void LoadScripts(IPlayerEntity session) + { + IEnumerable completedScriptsDtos = session.CompletedScripts; + foreach (CompletedScriptsDto completedScriptDto in completedScriptsDtos) + { + TutorialDto completedScript = _questManager.GetScriptTutorialByIndex(completedScriptDto.ScriptId, completedScriptDto.ScriptIndex); + + if (completedScript == null) + { + continue; + } + + session.SaveScript(completedScript.ScriptId, completedScript.ScriptIndex, completedScript.Type, completedScriptDto.CompletedDate); + } + } + + + private void LoadQuests(IPlayerEntity session) + { + IEnumerable characterQuests = session.ActiveQuests; + IEnumerable completedPeriodicQuests = session.CompletedPeriodicQuests; + IEnumerable completedQuests = session.CompletedQuests; + + foreach (CharacterQuestDto characterQuestDto in completedPeriodicQuests) + { + CharacterQuest characterQuest = _questFactory.NewQuest(session.Id, characterQuestDto.QuestId, characterQuestDto.SlotType); + characterQuest.ObjectiveAmount = characterQuestDto.ObjectiveAmount; + session.AddCompletedPeriodicQuest(characterQuest); + } + + // This can be removed when the wipe of the server happens. + // It's gonna contain only the MAIN quests, since we have a "backup" (the scripts), but + // with new characters will also contain GENERAL and SECONDARY ones + if (!completedQuests.Any()) + { + foreach (CompletedScriptsDto completedScript in session.GetCompletedScriptsByType(TutorialActionType.START_QUEST)) + { + TutorialDto script = _questManager.GetScriptTutorialByIndex(completedScript.ScriptId, completedScript.ScriptIndex); + + if (script == null) + { + continue; + } + + if (characterQuests.Any(s => s.QuestId == script.Data)) // It's an active one + { + continue; + } + + CharacterQuest characterQuest = _questFactory.NewQuest(session.Id, script.Data, QuestSlotType.MAIN); + session.AddCompletedQuest(characterQuest); + } + } + + else + { + foreach (CharacterQuestDto characterQuestDto in completedQuests) + { + CharacterQuest characterQuest = _questFactory.NewQuest(session.Id, characterQuestDto.QuestId, characterQuestDto.SlotType); + characterQuest.ObjectiveAmount = characterQuestDto.ObjectiveAmount; + session.AddCompletedQuest(characterQuest); + } + } + + foreach (CharacterQuestDto characterQuestDto in characterQuests) + { + CharacterQuest characterQuest = _questFactory.NewQuest(session.Id, characterQuestDto.QuestId, characterQuestDto.SlotType); + characterQuest.ObjectiveAmount = characterQuestDto.ObjectiveAmount; + session.AddActiveQuest(characterQuest); + } + } + + private void LoadPartnerEqBCards(IPlayerEntity playerEntity) + { + foreach (IMateEntity mate in playerEntity.MateComponent.GetMates(x => x.MateType == MateType.Partner)) + { + foreach (PartnerInventoryItem partnerInventory in playerEntity.PartnerGetEquippedItems(mate.PetSlot)) + { + if (partnerInventory?.ItemInstance == null) + { + continue; + } + + mate.RefreshEquipmentValues(partnerInventory.ItemInstance, false); + } + } + } + + private void LoadTitleBCards(IPlayerEntity playerEntity) + { + CharacterTitleDto equippedTitle = playerEntity.Titles.FirstOrDefault(x => x.IsEquipped); + if (equippedTitle == null) + { + return; + } + + playerEntity.RefreshTitleBCards(_itemsManager, equippedTitle, _bcardHandlerContainer); + } + + private void LoadPartnerInventory(IPlayerEntity playerEntity) + { + IEnumerable partnerItems = playerEntity.PartnerInventory; + foreach (CharacterPartnerInventoryItemDto partnerItem in partnerItems) + { + if (partnerItem == null) + { + continue; + } + + GameItemInstance itemInstance = _itemInstanceFactory.CreateItem(partnerItem.ItemInstance); + playerEntity.PartnerEquipItem(itemInstance, partnerItem.PartnerSlot); + } + } + + private void LoadQuicklist(IPlayerEntity playerEntity) + { + List inventory = playerEntity.Quicklist; + foreach (CharacterQuicklistEntryDto item in inventory) + { + if (item == null) + { + continue; + } + + playerEntity.QuicklistComponent.AddQuicklist(item); + } + } + + private void LoadInventory(IPlayerEntity playerEntity) + { + foreach (CharacterInventoryItemDto item in playerEntity.Inventory) + { + if (item == null) + { + continue; + } + + CharacterInventoryItemDto inventoryItem = item; + + GameItemInstance itemInstance = _itemInstanceFactory.CreateItem(inventoryItem.ItemInstance); + var newItem = new InventoryItem + { + CharacterId = inventoryItem.CharacterId, + InventoryType = inventoryItem.InventoryType, + IsEquipped = inventoryItem.IsEquipped, + Slot = inventoryItem.Slot, + ItemInstance = itemInstance + }; + + if (newItem.InventoryType == InventoryType.EquippedItems && newItem.ItemInstance != null) + { + playerEntity.EquipItem(newItem, newItem.ItemInstance.GameItem.EquipmentSlot, true); + if (itemInstance.Type == ItemInstanceType.WearableInstance) + { + playerEntity.RefreshEquipmentValues(itemInstance); + } + + continue; + } + + playerEntity.AddItemToInventory(newItem); + } + + // equipped items + DateTime date = DateTime.UtcNow; + foreach (CharacterInventoryItemDto item in playerEntity.EquippedStuffs) + { + if (item == null) + { + continue; + } + + GameItemInstance itemInstance = _itemInstanceFactory.CreateItem(item.ItemInstance); + var newItem = new InventoryItem + { + CharacterId = item.CharacterId, + InventoryType = item.InventoryType, + IsEquipped = item.IsEquipped, + Slot = item.Slot, + ItemInstance = itemInstance + }; + if (newItem.InventoryType != InventoryType.EquippedItems || newItem.ItemInstance == null) + { + continue; + } + + if (newItem.ItemInstance.ItemDeleteTime <= date) + { + continue; + } + + playerEntity.EquipItem(newItem, newItem.ItemInstance.GameItem.EquipmentSlot, true); + if (itemInstance.Type == ItemInstanceType.WearableInstance) + { + playerEntity.RefreshEquipmentValues(itemInstance); + } + } + } + + private void LoadPartnerWarehouse(IPlayerEntity playerEntity) + { + IEnumerable items = playerEntity.PartnerWarehouse; + foreach (PartnerWarehouseItemDto item in items) + { + if (item == null) + { + continue; + } + + playerEntity.AddPartnerWarehouseItem(_itemInstanceFactory.CreateItem(item.ItemInstance), item.Slot); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/CharacterPreLoadEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/CharacterPreLoadEventHandler.cs new file mode 100644 index 0000000..39bcd21 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/CharacterPreLoadEventHandler.cs @@ -0,0 +1,146 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using PhoenixLib.Logging; +using WingsAPI.Communication; +using WingsAPI.Communication.DbServer.CharacterService; +using WingsAPI.Communication.Sessions; +using WingsAPI.Communication.Sessions.Model; +using WingsAPI.Communication.Sessions.Request; +using WingsAPI.Communication.Sessions.Response; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Characters; + +public class CharacterPreLoadEventHandler : IAsyncEventProcessor +{ + private static readonly SemaphoreSlim _semaphoreSlim = new(1, 1); + private readonly ICharacterService _characterService; + private readonly IServerManager _serverManager; + private readonly ISessionManager _sessionManager; + private readonly ISessionService _sessionService; + + + public CharacterPreLoadEventHandler(ISessionService sessionService, ICharacterService characterService, IServerManager serverManager, ISessionManager sessionManager) + { + _sessionService = sessionService; + _characterService = characterService; + _serverManager = serverManager; + _sessionManager = sessionManager; + } + + public async Task HandleAsync(CharacterPreLoadEvent e, CancellationToken cancellation) + { + await _semaphoreSlim.WaitAsync(cancellation); + try + { + IClientSession session = e.Sender; + if (session?.Account == null || session.HasSelectedCharacter) + { + return; + } + + SessionResponse sessionResponse = await _sessionService.GetSessionByAccountId(new GetSessionByAccountIdRequest { AccountId = session.Account.Id }); + if (sessionResponse is not { ResponseType: RpcResponseType.SUCCESS }) + { + session.ForceDisconnect(); + return; + } + + Session sharedSession = sessionResponse.Session; + + if (sharedSession.State != SessionState.CharacterSelection) + { + session.ForceDisconnect(); + return; + } + + if (sharedSession.ChannelId != _serverManager.ChannelId) + { + await session.NotifyStrangeBehavior(StrangeBehaviorSeverity.DANGER, + $"[POTENTIAL_DUPE][PRE_LOAD] Wrong Channel: {sharedSession.AccountId} - {session.Account.MasterAccountId} - {sharedSession.HardwareId}"); + session.ForceDisconnect(); + return; + } + + if (session.SessionId != sharedSession.EncryptionKey) + { + await session.NotifyStrangeBehavior(StrangeBehaviorSeverity.DANGER, + $"[POTENTIAL_DUPE][PRE_LOAD] Wrong Encryption Key: {sharedSession.AccountId} - {session.Account.MasterAccountId} - {sharedSession.HardwareId}"); + session.ForceDisconnect(); + return; + } + + DbServerGetCharacterResponse response = await _characterService.GetCharacterBySlot(new DbServerGetCharacterFromSlotRequest + { + AccountId = session.Account.Id, + Slot = e.Slot + }); + + if (response.RpcResponseType != RpcResponseType.SUCCESS) + { + Log.Warn("[CHARACTER_SCREEN] Selected a character that does not exist"); + session.ForceDisconnect(); + return; + } + + SessionResponse tmp = await _sessionService.ConnectCharacter(new ConnectCharacterRequest + { + AccountId = session.Account.Id, + ChannelId = _serverManager.ChannelId, + CharacterId = response.CharacterDto.Id + }); + + if (tmp.ResponseType != RpcResponseType.SUCCESS) + { + await session.NotifyStrangeBehavior(StrangeBehaviorSeverity.DANGER, + $"[POTENTIAL_DUPE][PRE_LOAD] Looks like {response.CharacterDto.Name} - {session.Account.MasterAccountId} - {sharedSession.HardwareId} was already connected and tried to connect to {_serverManager.ChannelId}"); + session.ForceDisconnect(); + return; + } + + if (sharedSession.State == SessionState.CrossChannelAuthentication && _serverManager.ChannelId != sharedSession.AllowedCrossChannelId) + { + session.ForceDisconnect(); + return; + } + + if (sharedSession.State != SessionState.CharacterSelection) + { + await session.NotifyStrangeBehavior(StrangeBehaviorSeverity.DANGER, + $"[POTENTIAL_DUPE][PRE_LOAD] Looks like accountId: {sharedSession.AccountId} - {response.CharacterDto.Name} - {sharedSession.HardwareId} was already connected on the channel while being on {_serverManager.ChannelId}"); + session.ForceDisconnect(); + return; + } + + if (_sessionManager.IsOnline(response.CharacterDto.Id)) + { + session.ForceDisconnect(); + return; + } + + IClientSession alreadyConnectedSession = _sessionManager.GetSessionByCharacterId(response.CharacterDto.Id); + if (alreadyConnectedSession != null) + { + await alreadyConnectedSession.NotifyStrangeBehavior(StrangeBehaviorSeverity.DANGER, + $"[POTENTIAL_DUPE][PRE_LOAD] Looks like {alreadyConnectedSession.PlayerEntity.Name} was already connected on the channel while being on {_serverManager.ChannelId}"); + alreadyConnectedSession.ForceDisconnect(); + + await session.NotifyStrangeBehavior(StrangeBehaviorSeverity.DANGER, + $"[POTENTIAL_DUPE][PRE_LOAD] Looks like {response.CharacterDto.Name} - {sharedSession.HardwareId} was already connected on the channel while being on {_serverManager.ChannelId}"); + session.ForceDisconnect(); + return; + } + + session.SelectedCharacterSlot = e.Slot; + + session.SendPacket("OK"); + } + finally + { + _semaphoreSlim.Release(); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/CharacterRemoveManagersEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/CharacterRemoveManagersEventHandler.cs new file mode 100644 index 0000000..fd272b3 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/CharacterRemoveManagersEventHandler.cs @@ -0,0 +1,58 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Relations; +using WingsEmu.Game.Skills; +using WingsEmu.Game.Warehouse; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Characters; + +public class CharacterRemoveManagersEventHandler : IAsyncEventProcessor +{ + private readonly IAccountWarehouseManager _accountWarehouseManager; + private readonly IInvitationManager _invitationManager; + private readonly IMeditationManager _meditationManager; + private readonly ISacrificeManager _sacrificeManager; + private readonly ISpyOutManager _spyOutManager; + private readonly ITeleportManager _teleportManager; + + public CharacterRemoveManagersEventHandler(IMeditationManager meditationManager, ITeleportManager teleportManager, ISpyOutManager spyOutManager, + ISacrificeManager sacrificeManager, IInvitationManager invitationManager, IAccountWarehouseManager accountWarehouseManager) + { + _meditationManager = meditationManager; + _teleportManager = teleportManager; + _spyOutManager = spyOutManager; + _sacrificeManager = sacrificeManager; + _invitationManager = invitationManager; + _accountWarehouseManager = accountWarehouseManager; + } + + public async Task HandleAsync(CharacterRemoveManagersEvent e, CancellationToken cancellation) + { + long id = e.Sender.PlayerEntity.Id; + long accountId = e.Sender.Account.Id; + IPlayerEntity character = e.Sender.PlayerEntity; + + _meditationManager.RemoveAllMeditation(character); + _teleportManager.RemovePosition(id); + _spyOutManager.RemoveSpyOutSkill(id); + + IBattleEntity target = _sacrificeManager.GetTarget(character); + if (target != null) + { + IBattleEntity caster = _sacrificeManager.GetCaster(target); + _sacrificeManager.RemoveSacrifice(character, target); + if (caster != null) + { + _sacrificeManager.RemoveSacrifice(caster, character); + } + } + + _invitationManager.RemoveAllPendingInvitations(id); + _accountWarehouseManager.CleanCache(accountId); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/CharacterSaveOnDisconnectEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/CharacterSaveOnDisconnectEventHandler.cs new file mode 100644 index 0000000..c925707 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/CharacterSaveOnDisconnectEventHandler.cs @@ -0,0 +1,113 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using PhoenixLib.Logging; +using PhoenixLib.ServiceBus; +using WingsAPI.Communication.ServerApi.Protocol; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.Game._enum; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Groups.Events; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Maps.Event; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Raids.Events; +using WingsEmu.Game.RainbowBattle.Event; +using WingsEmu.Game.TimeSpaces.Events; +using WingsEmu.Packets.Enums; +using WingsEmu.Plugins.DistributedGameEvents.PlayerEvents; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Characters; + +public class CharacterSaveOnDisconnectEventHandler : IAsyncEventProcessor +{ + private readonly IMessagePublisher _disconnectedPublisher; + private readonly SerializableGameServer _gameServer; + private readonly IRespawnDefaultConfiguration _respawnDefaultConfiguration; + private readonly ISessionManager _sessionManager; + private readonly IMateTransportFactory _transportFactory; + + public CharacterSaveOnDisconnectEventHandler(ISessionManager sessionManager, IMessagePublisher disconnectedPublisher, SerializableGameServer gameServer, + IRespawnDefaultConfiguration respawnDefaultConfiguration, IMateTransportFactory transportFactory) + { + _sessionManager = sessionManager; + _disconnectedPublisher = disconnectedPublisher; + _gameServer = gameServer; + _respawnDefaultConfiguration = respawnDefaultConfiguration; + _transportFactory = transportFactory; + } + + public async Task HandleAsync(CharacterDisconnectedEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + + try + { + session.PlayerEntity.LifetimeStats.TotalTimeOnline += DateTime.UtcNow - session.PlayerEntity.GameStartDate; + await session.CloseExchange(); + await session.EmitEventAsync(new LeaveGroupEvent()); + Log.Info("Group left"); + await session.EmitEventAsync(new RaidPartyLeaveEvent(false)); + Log.Info("Raid party left"); + await session.EmitEventAsync(new TimeSpaceLeavePartyEvent + { + RemoveLive = true, + CheckFinished = false + }); + Log.Info("Timespace left"); + await session.EmitEventAsync(new LeaveMapEvent()); + Log.Info("Rainbow left"); + await session.EmitEventAsync(new RainbowBattleLeaveEvent + { + SendMessage = true + }); + Log.Info("Map left"); + await session.EmitEventAsync(new CharacterRemoveManagersEvent()); + Log.Info("Remove character from managers"); + } + catch (Exception exc) + { + Log.Error("[DISCONNECT_HANDLER] Leave maps", exc); + } + + try + { + _sessionManager.RemoveOnline(session.PlayerEntity.Name, session.PlayerEntity.Id); + + if (session.PlayerEntity.IsSeal && _gameServer.ChannelType == GameChannelType.ACT_4) + { + RespawnDefault respawn = _respawnDefaultConfiguration.GetReturn(session.PlayerEntity.Faction == FactionType.Angel ? RespawnType.ACT4_ANGEL_SPAWN : RespawnType.ACT4_DEMON_SPAWN); + session.PlayerEntity.MapId = respawn.MapId; + session.PlayerEntity.MapX = respawn.MapX; + session.PlayerEntity.MapY = respawn.MapY; + } + } + catch (Exception exc) + { + Log.Error("[DISCONNECT_HANDLER]", exc); + } + + try + { + await session.EmitEventAsync(new SessionSaveEvent()); + } + catch (Exception exc) + { + Log.Error("[DISCONNECT_HANDLER]", exc); + } + + await _disconnectedPublisher.PublishAsync(new PlayerDisconnectedChannelMessage + { + ChannelId = _gameServer.ChannelId, + CharacterId = session.PlayerEntity.Id, + CharacterName = session.PlayerEntity.Name, + DisconnectionTime = e.DisconnectionTime, + FamilyId = session.PlayerEntity.Family?.Id + }); + + Log.Warn($"Character disconnected: {session.PlayerEntity.Name}:{session.PlayerEntity.Id}"); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/GenerateGoldEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/GenerateGoldEventHandler.cs new file mode 100644 index 0000000..6459358 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/GenerateGoldEventHandler.cs @@ -0,0 +1,62 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Characters; + +public class GenerateGoldEventHandler : IAsyncEventProcessor +{ + private readonly IGameLanguageService _gameLanguage; + private readonly IItemsManager _itemsManager; + private readonly GameMinMaxConfiguration _minMaxConfiguration; + + public GenerateGoldEventHandler(IGameLanguageService gameLanguage, IItemsManager itemsManager, GameMinMaxConfiguration minMaxConfiguration) + { + _gameLanguage = gameLanguage; + _itemsManager = itemsManager; + _minMaxConfiguration = minMaxConfiguration; + } + + public async Task HandleAsync(GenerateGoldEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + long val = e.Amount; + + if (session.PlayerEntity.Gold + val > _minMaxConfiguration.MaxGold) + { + long difference = session.PlayerEntity.Gold + val - _minMaxConfiguration.MaxGold; + session.PlayerEntity.Gold = _minMaxConfiguration.MaxGold; + if (e.FallBackToBank) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.BANK_SHOUTMESSAGE_EXCEED_GOLD_SENT_TO_BANK, session.UserLanguage), MsgMessageType.Middle); + session.Account.BankMoney += difference; + } + else + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.INFORMATION_MESSAGE_MAX_GOLD, session.UserLanguage), MsgMessageType.Middle); + } + + session.RefreshGold(); + return; + } + + session.PlayerEntity.Gold += val; + + if (e.SendMessage) + { + string gold = _gameLanguage.GetLanguage(GameDataType.Item, _itemsManager.GetItem((short)ItemVnums.GOLD).Name, session.UserLanguage); + + session.SendChatMessage(_gameLanguage.GetLanguageFormat(GameDialogKey.INVENTORY_CHATMESSAGE_X_ITEM_ACQUIRED, session.UserLanguage, val, gold), ChatMessageColorType.Yellow); + } + + session.RefreshGold(); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/GenerateReputationEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/GenerateReputationEventHandler.cs new file mode 100644 index 0000000..9e57735 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/GenerateReputationEventHandler.cs @@ -0,0 +1,69 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Characters; + +public class GenerateReputationEventHandler : IAsyncEventProcessor +{ + private readonly IGameLanguageService _languageService; + private readonly GameMinMaxConfiguration _minMaxConfiguration; + private readonly IRankingManager _rankingManager; + private readonly IReputationConfiguration _reputationConfiguration; + private readonly IServerManager _serverManager; + + public GenerateReputationEventHandler(IServerManager serverManager, IGameLanguageService languageService, GameMinMaxConfiguration minMaxConfiguration, + IReputationConfiguration reputationConfiguration, IRankingManager rankingManager) + { + _serverManager = serverManager; + _languageService = languageService; + _minMaxConfiguration = minMaxConfiguration; + _reputationConfiguration = reputationConfiguration; + _rankingManager = rankingManager; + } + + public async Task HandleAsync(GenerateReputationEvent e, CancellationToken cancellation) + { + IPlayerEntity character = e.Sender.PlayerEntity; + long amount = e.Amount * _serverManager.ReputRate; + + if (character.Reput <= 0 && amount <= 0) + { + return; + } + + long oldReput = character.Reput; + character.Reput += amount; + + if (character.Reput < _minMaxConfiguration.MinReputation) + { + character.Reput = _minMaxConfiguration.MinReputation; + } + + if (_minMaxConfiguration.MaxReputation < character.Reput) + { + character.Reput = _minMaxConfiguration.MaxReputation; + } + + character.Session.RefreshReputation(_reputationConfiguration, _rankingManager.TopReputation); + bool decrease = amount < 0; + + if (!e.SendMessage) + { + return; + } + + character.Session.SendChatMessage( + _languageService.GetLanguageFormat(decrease ? GameDialogKey.INFORMATION_CHATMESSAGE_REPUT_DECREASE : GameDialogKey.INFORMATION_CHATMESSAGE_REPUT_INCREASE, character.Session.UserLanguage, + Math.Abs(oldReput - character.Reput)), + decrease ? ChatMessageColorType.Red : ChatMessageColorType.Green); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/GetDefaultMorphEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/GetDefaultMorphEventHandler.cs new file mode 100644 index 0000000..10c87da --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/GetDefaultMorphEventHandler.cs @@ -0,0 +1,44 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Items; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Characters; + +public class GetDefaultMorphEventHandler : IAsyncEventProcessor +{ + public async Task HandleAsync(GetDefaultMorphEvent e, CancellationToken cancellation) + { + IPlayerEntity character = e.Sender.PlayerEntity; + + if (character.IsMorphed) + { + character.IsMorphed = false; + character.Morph = 0; + + e.Sender.BroadcastCMode(); + return; + } + + if (!character.UseSp && character.Morph != 0) + { + character.Morph = 0; + e.Sender.BroadcastCMode(); + return; + } + + GameItemInstance sp = character.Specialist; + if (sp == null) + { + return; + } + + character.Morph = sp.GameItem.Morph; + character.MorphUpgrade = sp.Upgrade; + character.MorphUpgrade2 = sp.Design; + e.Sender.BroadcastCMode(); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/InviteJoinMinilandEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/InviteJoinMinilandEventHandler.cs new file mode 100644 index 0000000..559b510 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/InviteJoinMinilandEventHandler.cs @@ -0,0 +1,169 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Communication.ServerApi.Protocol; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsAPI.Game.Extensions.ItemExtension.Item; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Miniland; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Characters; + +public class InviteJoinMinilandEventHandler : IAsyncEventProcessor +{ + private readonly IGameLanguageService _gameLanguageService; + private readonly SerializableGameServer _gameServer; + private readonly IItemsManager _itemsManager; + private readonly IMinilandManager _miniland; + private readonly ISessionManager _sessionManager; + + public InviteJoinMinilandEventHandler(ISessionManager sessionManager, IMinilandManager miniland, IItemsManager itemsManager, SerializableGameServer gameServer, + IGameLanguageService gameLanguageService) + { + _sessionManager = sessionManager; + _miniland = miniland; + _itemsManager = itemsManager; + _gameServer = gameServer; + _gameLanguageService = gameLanguageService; + } + + public async Task HandleAsync(InviteJoinMinilandEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + IClientSession target = _sessionManager.GetSessionByCharacterName(e.Target); + bool isFirstStep = e.IsFirstStep; + bool isByFriend = e.IsByFriend; + bool isOnAct4 = _gameServer.ChannelType == GameChannelType.ACT_4; + + if (isOnAct4) + { + session.SendMsg(session.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_MUST_BE_IN_CLASSIC_MAP), MsgMessageType.Middle); + return; + } + + if (session.CurrentMapInstance.Id != session.PlayerEntity.Miniland.Id && !isByFriend) + { + return; + } + + if (target == null) + { + session.SendInfo(session.GetLanguage(GameDialogKey.INFORMATION_INFO_PLAYER_OFFLINE)); + return; + } + + if (session.PlayerEntity.Id == target.PlayerEntity.Id) + { + return; + } + + /* + * IsFirstStep -> session is who sent the invitation + * !IsFirstStep -> target is who sent the invitation, so session is who accepted it + */ + + if (target.PlayerEntity.MinilandInviteBlocked) + { + session.SendInfo(session.GetLanguage(GameDialogKey.MINILAND_INFO_INVITE_LOCK)); + return; + } + + if (!isFirstStep) + { + switch (target.PlayerEntity.MinilandState) + { + case MinilandState.PRIVATE when !session.PlayerEntity.IsFriend(target.PlayerEntity.Id) && !session.PlayerEntity.IsMarried(target.PlayerEntity.Id) && !session.IsGameMaster(): + session.SendInfo(session.GetLanguage(GameDialogKey.MINILAND_SHOUTMESSAGE_CLOSED)); + return; + case MinilandState.LOCK: + if (session.IsGameMaster()) + { + break; + } + + session.SendInfo(session.GetLanguage(GameDialogKey.MINILAND_SHOUTMESSAGE_CLOSED)); + return; + } + } + + if (isByFriend) + { + if (!session.CurrentMapInstance.HasMapFlag(MapFlags.IS_BASE_MAP)) + { + session.SendMsg(session.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_MUST_BE_IN_CLASSIC_MAP), MsgMessageType.Middle); + return; + } + + if (!session.PlayerEntity.IsFriend(target.PlayerEntity.Id) && !session.PlayerEntity.IsMarried(target.PlayerEntity.Id)) + { + return; + } + + if (!session.PlayerEntity.IsAlive()) + { + return; + } + + if (target.PlayerEntity.MinilandState == MinilandState.LOCK && !session.IsGameMaster()) + { + session.SendInfo(session.GetLanguage(GameDialogKey.MINILAND_SHOUTMESSAGE_CLOSED)); + return; + } + + session.ChangeMap(target.PlayerEntity.Miniland); + return; + } + + if (!session.PlayerEntity.HasItem((short)ItemVnums.SEED_OF_POWER)) + { + string itemName = _itemsManager.GetItem((short)ItemVnums.SEED_OF_POWER).GetItemName(_gameLanguageService, session.UserLanguage); + session.SendInfo(session.GetLanguageFormat(GameDialogKey.INVENTORY_SHOUTMESSAGE_NOT_ENOUGH_ITEMS, 1, itemName)); + return; + } + + if (isFirstStep) + { + _miniland.SaveMinilandInvite(session.PlayerEntity.Id, target.PlayerEntity.Id); + target.SendDialog($"mjoin 0 {session.PlayerEntity.Id} 1", $"mjoin 0 {session.PlayerEntity.Id} 0", + target.GetLanguageFormat(GameDialogKey.MINILAND_DIALOG_ASK_INVITE, session.PlayerEntity.Name)); + return; + } + + if (!_miniland.ContainsMinilandInvite(session.PlayerEntity.Id)) + { + return; + } + + if (!_miniland.ContainsTargetInvite(session.PlayerEntity.Id, target.PlayerEntity.Id)) + { + return; + } + + _miniland.RemoveMinilandInvite(session.PlayerEntity.Id, target.PlayerEntity.Id); + + if (!target.CurrentMapInstance.HasMapFlag(MapFlags.IS_BASE_MAP)) + { + target.SendMsg(target.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_MUST_BE_IN_CLASSIC_MAP), MsgMessageType.Middle); + return; + } + + if (session.PlayerEntity.MinilandState == MinilandState.LOCK && !target.IsGameMaster()) + { + target.SendInfo(target.GetLanguage(GameDialogKey.MINILAND_SHOUTMESSAGE_CLOSED)); + return; + } + + await session.RemoveItemFromInventory((short)ItemVnums.SEED_OF_POWER); + target.ChangeMap(session.PlayerEntity.Miniland); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/KillBonusEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/KillBonusEventHandler.cs new file mode 100644 index 0000000..6687513 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/KillBonusEventHandler.cs @@ -0,0 +1,805 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Data.Drops; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.DTOs.Maps; +using WingsEmu.DTOs.Quests; +using WingsEmu.Game; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Algorithm.Events; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Groups; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Inventory.Event; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Managers.ServerData; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Quests; +using WingsEmu.Game.Quests.Event; +using WingsEmu.Game.Raids; +using WingsEmu.Game.TimeSpaces; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Characters; + +public sealed class KillBonusEventHandler : IAsyncEventProcessor +{ + private readonly IDropManager _dropManager; + private readonly IDropRarityConfigurationProvider _dropRarityConfigurationProvider; + private readonly IAsyncEventPipeline _eventPipeline; + private readonly IGameItemInstanceFactory _gameItemInstance; + private readonly IGameLanguageService _gameLanguage; + private readonly IItemsManager _itemsManager; + private readonly IRandomGenerator _randomGenerator; + private readonly IRankingManager _rankingManager; + private readonly IReputationConfiguration _reputationConfiguration; + private readonly IServerManager _serverManager; + private readonly ISessionManager _sessionManager; + + private readonly HashSet NormalDropQuestTypes = new() { QuestType.DROP_CHANCE, QuestType.DROP_CHANCE_2, QuestType.DROP_IN_TIMESPACE }; + + public KillBonusEventHandler(IRandomGenerator randomGenerator, + IDropManager dropManager, IServerManager serverManager, IGameLanguageService gameLanguage, ISessionManager sessionManager, + IItemsManager itemsManager, IAsyncEventPipeline eventPipeline, IGameItemInstanceFactory gameItemInstance, + IDropRarityConfigurationProvider dropRarityConfigurationProvider, IReputationConfiguration reputationConfiguration, IRankingManager rankingManager) + { + _randomGenerator = randomGenerator; + _dropManager = dropManager; + _serverManager = serverManager; + _gameLanguage = gameLanguage; + _sessionManager = sessionManager; + _itemsManager = itemsManager; + _eventPipeline = eventPipeline; + _gameItemInstance = gameItemInstance; + _dropRarityConfigurationProvider = dropRarityConfigurationProvider; + _reputationConfiguration = reputationConfiguration; + _rankingManager = rankingManager; + } + + public async Task HandleAsync(KillBonusEvent e, CancellationToken cancellation) + { + IMonsterEntity monsterEntityToAttack = e.MonsterEntity; + IPlayerEntity character = e.Sender.PlayerEntity; + IClientSession session = e.Sender; + + if (monsterEntityToAttack == null || monsterEntityToAttack.IsStillAlive || monsterEntityToAttack.SummonerType is VisualType.Player) + { + return; + } + + if (!ShouldMonsterDrop(monsterEntityToAttack)) + { + return; + } + + // owner set + IPlayerEntity dropOwner = null; + + if (monsterEntityToAttack.Damagers.Count > 0) + { + IBattleEntity entityDropOwner = monsterEntityToAttack.Damagers.FirstOrDefault(); + if (entityDropOwner != null) + { + dropOwner = entityDropOwner switch + { + IMonsterEntity monsterEntity + => monsterEntity.SummonerType != null && monsterEntity.SummonerId != null && monsterEntity.SummonerType == VisualType.Player + ? monsterEntity.MapInstance.GetCharacterById(monsterEntity.SummonerId.Value) + : null, + IPlayerEntity playerEntity => playerEntity, + IMateEntity mateEntity => mateEntity.Owner, + _ => null + }; + } + } + + // Check if owner is online and it's at the same map + IClientSession firstAttacker = dropOwner != null ? _sessionManager.GetSessionByCharacterId(dropOwner.Id) : null; + if (firstAttacker == null) + { + dropOwner = character; + } + else + { + dropOwner = firstAttacker.CurrentMapInstance?.Id == character.MapInstance.Id ? firstAttacker.PlayerEntity : character; + } + + PlayerGroup playerGroup = null; + if (dropOwner != null) + { + playerGroup = dropOwner.GetGroup(); + } + + // end owner set + if (!session.HasCurrentMapInstance) + { + return; + } + + await HandleExp(session, character, monsterEntityToAttack, dropOwner?.Id); + await HandleGoldDrops(monsterEntityToAttack, playerGroup, dropOwner); + await HandleDrops(monsterEntityToAttack, session.PlayerEntity, playerGroup, dropOwner); + } + + private bool ShouldMonsterDrop(IMonsterEntity monsterEntityToAttack) + { + switch ((MonsterVnum)monsterEntityToAttack.MonsterVNum) + { + case MonsterVnum.TRAINING_STAKE: + case MonsterVnum.DEMON_CAMP: + case MonsterVnum.ANGEL_CAMP: + return false; + } + + return true; + } + + private async Task HandleExp(IClientSession session, IPlayerEntity character, IMonsterEntity monsterEntityToAttack, long? dropOwner) + { + if (!character.IsAlive()) + { + return; + } + + await _eventPipeline.ProcessEventAsync(new GenerateExperienceEvent(character, monsterEntityToAttack, dropOwner)); + + if (character.Level >= monsterEntityToAttack.Level || character.Dignity >= 100) + { + return; + } + + character.Dignity += 1; + session.RefreshReputation(_reputationConfiguration, _rankingManager.TopReputation); + session.SendSuccessChatMessage(_gameLanguage.GetLanguageFormat(GameDialogKey.DIGNITY_CHATMESSAGE_RESTORE, session.UserLanguage, 1)); + } + + private async Task HandleDrops(IMonsterEntity monsterEntityToAttack, IPlayerEntity mainKiller, PlayerGroup playerGroup, IPlayerEntity firstAttacker) + { + IClientSession session = firstAttacker.Session; + + IReadOnlyList monsterDrops = monsterEntityToAttack.Drops; + var additionalDrop = new List(); + IReadOnlyList mapDrop = _dropManager.GetDropsByMapId(monsterEntityToAttack.MapInstance.MapId); + IEnumerable generalDrop = _dropManager.GetGeneralDrops(); + additionalDrop.AddRange(mapDrop); + additionalDrop.AddRange(generalDrop); + + int secondChanceDropBCard = session.PlayerEntity.BCardComponent + .GetAllBCardsInformation(BCardType.DropItemTwice, (byte)AdditionalTypes.DropItemTwice.DoubleDropChance, session.PlayerEntity.Level).firstData; + bool secondChanceDrop = secondChanceDropBCard != 0 && _randomGenerator.RandomNumber() <= secondChanceDropBCard; + + #region Quests + + // Normal quest drops + IEnumerable characterQuests = session.PlayerEntity.GetCurrentQuestsByTypes(NormalDropQuestTypes); + foreach (CharacterQuest characterQuest in characterQuests) + { + foreach (QuestObjectiveDto objective in characterQuest.Quest.Objectives) + { + if (monsterEntityToAttack.MonsterVNum != objective.Data0 && characterQuest.Quest.QuestType != QuestType.DROP_IN_TIMESPACE) + { + continue; + } + + if (characterQuest.Quest.QuestType == QuestType.DROP_IN_TIMESPACE) + { + TimeSpaceParty timeSpace = session.PlayerEntity.TimeSpaceComponent.TimeSpace; + if (timeSpace == null || timeSpace.TimeSpaceId != objective.Data0) + { + continue; + } + } + + float rndChance = _randomGenerator.RandomNumber(); + float chance = characterQuest.Quest.QuestType == QuestType.DROP_CHANCE ? objective.Data3 : objective.Data3 * 0.1f; + if (rndChance > chance) + { + continue; + } + + await DropQuestItem(session, monsterEntityToAttack, playerGroup, objective.Data1); + + if (!secondChanceDrop) + { + continue; + } + + await DropQuestItem(session, monsterEntityToAttack, playerGroup, objective.Data1); + } + } + + // It has to be hardcoded, sorry T-T + if (session.PlayerEntity.HasQuestWithId((int)QuestsVnums.LILIES_SP2)) + { + if (monsterEntityToAttack.Level >= session.PlayerEntity.Level - 15 && monsterEntityToAttack.Level <= session.PlayerEntity.Level + 15 || monsterEntityToAttack.Level > 75) + { + float rndChance = _randomGenerator.RandomNumber(); + float chance = 25; // It has to be like this for now + if (rndChance < chance) + { + await DropQuestItem(session, monsterEntityToAttack, playerGroup, (int)ItemVnums.LILY_OF_PURITY); + + if (secondChanceDrop) + { + await DropQuestItem(session, monsterEntityToAttack, playerGroup, (int)ItemVnums.LILY_OF_PURITY); + } + } + } + } + + #endregion + + bool hasPenalty = HasLevelPenalty(mainKiller, monsterEntityToAttack); + + int rate = session.CurrentMapInstance.HasMapFlag(MapFlags.ACT_4) || session.CurrentMapInstance.HasMapFlag(MapFlags.HAS_DROP_DIRECTLY_IN_INVENTORY_ENABLED) || + monsterEntityToAttack.DropToInventory + ? 1 + : _serverManager.MobDropRate; + + if (secondChanceDrop) + { + session.PlayerEntity.BroadcastEffectInRange(EffectType.DoubleChanceDrop); + } + + for (int i = 0; i < rate; i++) + { + foreach (DropDTO drop in monsterDrops) + { + float rndChance = _randomGenerator.RandomNumber(0, 100000); + float chance = drop.DropChance + drop.DropChance * _serverManager.MobDropChance * (hasPenalty ? 0.1f : 1.0f); + if (rndChance > chance) + { + continue; + } + + await DropItem(session, monsterEntityToAttack, drop.ItemVNum, drop.Amount, playerGroup); + + if (!secondChanceDrop) + { + continue; + } + + await DropItem(session, monsterEntityToAttack, drop.ItemVNum, drop.Amount, playerGroup); + } + } + + if (monsterEntityToAttack.RaidDrop != null) + { + foreach (DropChance drop in monsterEntityToAttack.RaidDrop) + { + float rndChance = _randomGenerator.RandomNumber(0, 100000); + float chance = drop.Chance + drop.Chance * _serverManager.MobDropChance; + if (rndChance > chance) + { + continue; + } + + await DropItem(session, monsterEntityToAttack, drop.ItemVnum, drop.Amount, playerGroup); + + if (!secondChanceDrop) + { + continue; + } + + await DropItem(session, monsterEntityToAttack, drop.ItemVnum, drop.Amount, playerGroup); + } + } + + if (session.PlayerEntity.TimeSpaceComponent.TimeSpace != null && monsterEntityToAttack.MapInstance.MapInstanceType == MapInstanceType.TimeSpaceInstance) + { + TimeSpaceParty timeSpace = session.PlayerEntity.TimeSpaceComponent.TimeSpace; + float rndChance = _randomGenerator.RandomNumber(0, 100000); + int itemChance = timeSpace.Instance.BonusPointItemDropChance; + float chance = itemChance + itemChance * _serverManager.MobDropChance * (hasPenalty ? 0.1f : 1.0f); + if (rndChance <= chance) + { + await DropItem(session, monsterEntityToAttack, (short)ItemVnums.BONUS_POINTS, 1, playerGroup); + + if (secondChanceDrop) + { + await DropItem(session, monsterEntityToAttack, (short)ItemVnums.BONUS_POINTS, 1, playerGroup); + } + } + } + + IReadOnlyList raceDrop = _dropManager.GetDropsByMonsterRace(monsterEntityToAttack.MonsterRaceType, monsterEntityToAttack.MonsterRaceSubType); + for (int i = 0; i < rate; i++) + { + foreach (DropDTO drop in raceDrop) + { + float rndChance = _randomGenerator.RandomNumber(0, 10000); + float chance = drop.DropChance + drop.DropChance * _serverManager.MobDropChance * (hasPenalty ? 0.1f : 1.0f); + if (rndChance > chance) + { + continue; + } + + await DropItem(session, monsterEntityToAttack, drop.ItemVNum, drop.Amount, playerGroup); + + if (!secondChanceDrop) + { + continue; + } + + await DropItem(session, monsterEntityToAttack, drop.ItemVNum, drop.Amount, playerGroup); + } + } + + if (monsterEntityToAttack.MonsterRaceType is MonsterRaceType.Fixed or MonsterRaceType.Other) + { + return; + } + + int genericRate = session.CurrentMapInstance.HasMapFlag(MapFlags.ACT_4) ? 1 : _serverManager.GenericDropRate; + + for (int i = 0; i < genericRate; i++) + { + foreach (DropDTO drop in additionalDrop) + { + float rndChance = _randomGenerator.RandomNumber(0, 10000); + float chance = drop.DropChance + drop.DropChance * _serverManager.GenericDropChance * (hasPenalty ? 0.1f : 1.0f); + if (rndChance > chance) + { + continue; + } + + await DropItem(session, monsterEntityToAttack, drop.ItemVNum, drop.Amount, playerGroup); + + if (!secondChanceDrop) + { + continue; + } + + await DropItem(session, monsterEntityToAttack, drop.ItemVNum, drop.Amount, playerGroup); + } + } + } + + private async Task DropQuestItem(IClientSession session, IMonsterEntity monsterEntityToAttack, PlayerGroup playerGroup, int itemVnum) + { + if (session.CurrentMapInstance.HasMapFlag(MapFlags.ACT_4) || monsterEntityToAttack.DropToInventory + || session.CurrentMapInstance.HasMapFlag(MapFlags.HAS_DROP_DIRECTLY_IN_INVENTORY_ENABLED)) + { + var alreadyGifted = new List(); + foreach (IBattleEntity entity in monsterEntityToAttack.Damagers) + { + long charId = entity.Id; + if (alreadyGifted.Contains(charId)) + { + continue; + } + + IClientSession giftSession = _sessionManager.GetSessionByCharacterId(charId); + if (giftSession == null) + { + continue; + } + + if (giftSession.PlayerEntity.MapInstance?.Id != monsterEntityToAttack.MapInstance?.Id) + { + continue; + } + + bool shouldReceiveDrop = ShouldReceiveDrop(giftSession, monsterEntityToAttack); + if (!shouldReceiveDrop) + { + continue; + } + + if (giftSession.PlayerEntity.IsInGroup()) + { + foreach (IPlayerEntity member in giftSession.PlayerEntity.GetGroup().Members) + { + await member.Session.EmitEventAsync(new QuestItemPickUpEvent + { + ItemVnum = itemVnum, + Amount = 1, + SendMessage = true + }); + alreadyGifted.Add(member.Id); + } + } + else + { + await giftSession.EmitEventAsync(new QuestItemPickUpEvent + { + ItemVnum = itemVnum, + Amount = 1, + SendMessage = true + }); + alreadyGifted.Add(giftSession.PlayerEntity.Id); + } + } + + return; + } + + short newX = (short)(monsterEntityToAttack.PositionX + _randomGenerator.RandomNumber(-1, 2)); + short newY = (short)(monsterEntityToAttack.PositionY + _randomGenerator.RandomNumber(-1, 2)); + + if (monsterEntityToAttack.MapInstance.IsBlockedZone(newX, newY)) + { + newX = monsterEntityToAttack.PositionX; + newY = monsterEntityToAttack.PositionY; + } + + var newItemPosition = new Position(newX, newY); + + if (playerGroup == null) + { + var dropItem = new DropMapItemEvent(session.PlayerEntity.MapInstance, newItemPosition, (short)itemVnum, 1, ownerId: session.PlayerEntity.Id, isQuest: true); + await _eventPipeline.ProcessEventAsync(dropItem); + return; + } + + string itemName; + + if (playerGroup.SharingMode == (byte)GroupSharingType.ByOrder) + { + long? dropOwner = playerGroup.GetNextOrderedCharacterId(session.PlayerEntity); + + if (!dropOwner.HasValue) + { + return; + } + + foreach (IPlayerEntity s in playerGroup.Members) + { + itemName = _gameLanguage.GetItemName(_itemsManager.GetItem(itemVnum), s.Session); + s.Session.SendChatMessage(_gameLanguage.GetLanguageFormat(GameDialogKey.GROUP_CHATMESSAGE_DROP_ORDERED, session.UserLanguage, 1, + itemName, playerGroup.Members.FirstOrDefault(c => c.Id == (long)dropOwner)?.Name), ChatMessageColorType.Yellow); + } + + var dropItem = new DropMapItemEvent(session.PlayerEntity.MapInstance, newItemPosition, (short)itemVnum, 1, ownerId: dropOwner.Value, isQuest: true); + await _eventPipeline.ProcessEventAsync(dropItem); + } + else + { + foreach (IPlayerEntity s in playerGroup.Members) + { + itemName = _gameLanguage.GetItemName(_itemsManager.GetItem(itemVnum), s.Session); + s.Session.SendChatMessage(_gameLanguage.GetLanguageFormat(GameDialogKey.GROUP_CHATMESSAGE_DROP_SHARED, session.UserLanguage, 1, itemName), ChatMessageColorType.Yellow); + } + + var dropItem = new DropMapItemEvent(session.PlayerEntity.MapInstance, newItemPosition, (short)itemVnum, 1, ownerId: session.PlayerEntity.Id, isQuest: true); + await _eventPipeline.ProcessEventAsync(dropItem); + } + } + + private async Task DropItem(IClientSession session, IMonsterEntity monsterEntityToAttack, int itemVnum, int amount, PlayerGroup playerGroup) + { + if (session.CurrentMapInstance.HasMapFlag(MapFlags.ACT_4) || session.CurrentMapInstance.HasMapFlag(MapFlags.HAS_DROP_DIRECTLY_IN_INVENTORY_ENABLED) + || monsterEntityToAttack.DropToInventory) + { + var alreadyGifted = new HashSet(); + foreach (IBattleEntity entity in monsterEntityToAttack.Damagers) + { + long charId = entity.Id; + if (alreadyGifted.Contains(charId)) + { + continue; + } + + IClientSession giftSession = _sessionManager.GetSessionByCharacterId(charId); + + if (giftSession == null) + { + continue; + } + + if (giftSession.PlayerEntity.MapInstance?.Id != monsterEntityToAttack.MapInstance?.Id) + { + continue; + } + + bool shouldReceiveDrop = ShouldReceiveDrop(giftSession, monsterEntityToAttack); + if (!shouldReceiveDrop) + { + continue; + } + + IGameItem item = _itemsManager.GetItem(itemVnum); + sbyte randomRarity = _dropRarityConfigurationProvider.GetRandomRarity(item.ItemType); + + GameItemInstance itemInstance = _gameItemInstance.CreateItem(itemVnum, amount, 0, randomRarity); + + if (item.ItemType == ItemType.Map) + { + continue; + } + + await giftSession.AddNewItemToInventory(itemInstance, true, ChatMessageColorType.Yellow, true); + alreadyGifted.Add(charId); + } + + return; + } + + short newX = (short)(monsterEntityToAttack.PositionX + _randomGenerator.RandomNumber(-1, 2)); + short newY = (short)(monsterEntityToAttack.PositionY + _randomGenerator.RandomNumber(-1, 2)); + + if (monsterEntityToAttack.MapInstance.IsBlockedZone(newX, newY)) + { + newX = monsterEntityToAttack.PositionX; + newY = monsterEntityToAttack.PositionY; + } + + var newItemPosition = new Position(newX, newY); + + if (playerGroup == null) + { + var dropItem = new DropMapItemEvent(session.PlayerEntity.MapInstance, newItemPosition, (short)itemVnum, amount, ownerId: session.PlayerEntity.Id); + await _eventPipeline.ProcessEventAsync(dropItem); + return; + } + + if (playerGroup.SharingMode == (byte)GroupSharingType.ByOrder) + { + long? dropOwner = playerGroup.GetNextOrderedCharacterId(session.PlayerEntity); + + if (!dropOwner.HasValue) + { + return; + } + + foreach (IPlayerEntity s in playerGroup.Members) + { + string itemName = _gameLanguage.GetItemName(_itemsManager.GetItem(itemVnum), s.Session); + s.Session.SendChatMessage(_gameLanguage.GetLanguageFormat(GameDialogKey.GROUP_CHATMESSAGE_DROP_ORDERED, s.Session.UserLanguage, amount, + itemName, playerGroup.Members.FirstOrDefault(c => c.Id == (long)dropOwner)?.Name), ChatMessageColorType.Yellow); + } + + var dropItem = new DropMapItemEvent(session.PlayerEntity.MapInstance, newItemPosition, (short)itemVnum, amount, ownerId: dropOwner.Value); + await _eventPipeline.ProcessEventAsync(dropItem); + } + else + { + foreach (IPlayerEntity s in playerGroup.Members) + { + string itemName = _gameLanguage.GetItemName(_itemsManager.GetItem(itemVnum), s.Session); + s.Session.SendChatMessage(_gameLanguage.GetLanguageFormat(GameDialogKey.GROUP_CHATMESSAGE_DROP_SHARED, s.Session.UserLanguage, amount, itemName), ChatMessageColorType.Yellow); + } + + var dropItem = new DropMapItemEvent(session.PlayerEntity.MapInstance, newItemPosition, (short)itemVnum, amount, ownerId: session.PlayerEntity.Id); + await _eventPipeline.ProcessEventAsync(dropItem); + } + } + + private bool ShouldReceiveDrop(IClientSession giftSession, IMonsterEntity monsterEntityToAttack) + { + long? tankPlayer = null; + int highestHits = 0; + foreach (IBattleEntity damager in monsterEntityToAttack.Damagers) + { + if (damager is not IPlayerEntity playerEntity) + { + continue; + } + + if (!playerEntity.HitsByMonsters.TryGetValue(monsterEntityToAttack.Id, out int hits)) + { + continue; + } + + if (highestHits > hits) + { + continue; + } + + tankPlayer = playerEntity.Id; + highestHits = hits; + } + + if (tankPlayer != null && giftSession.PlayerEntity.Id == tankPlayer.Value) + { + return true; + } + + IPlayerEntity player = giftSession.PlayerEntity; + if (!monsterEntityToAttack.PlayersDamage.TryGetValue(player.Id, out int damage)) + { + return false; + } + + int damageToDealt = (int)(monsterEntityToAttack.MaxHp * 0.05); + return damageToDealt <= damage; + } + + private async Task HandleGoldDrops(IMonsterEntity monsterEntityToAttack, PlayerGroup playerGroup, IPlayerEntity firstAttacker) + { + if (monsterEntityToAttack.MonsterRaceType is MonsterRaceType.Fixed or MonsterRaceType.Other) + { + return; + } + + if (monsterEntityToAttack.MapInstance.MapInstanceType == MapInstanceType.TimeSpaceInstance) + { + return; + } + + if (HasLevelPenalty(firstAttacker, monsterEntityToAttack)) + { + return; + } + + int gold = GetGold(firstAttacker, monsterEntityToAttack); + long maxGold = _serverManager.MaxGold; + gold = gold > maxGold ? (int)maxGold : gold; + + int randomNumber = 0; + + int rate = _serverManager.GoldDropRate; + + for (int i = 0; i < rate; i++) + { + randomNumber += _randomGenerator.RandomNumber(); + } + + if (randomNumber >= 50 * _serverManager.GoldDropChance) + { + return; + } + + if (gold <= 0) + { + return; + } + + IClientSession session = firstAttacker.Session; + + if (session.CurrentMapInstance == null) + { + return; + } + + int secondChanceDropBCard = session.PlayerEntity.BCardComponent + .GetAllBCardsInformation(BCardType.DropItemTwice, (byte)AdditionalTypes.DropItemTwice.DoubleDropChance, session.PlayerEntity.Level).firstData; + bool secondChanceDrop = secondChanceDropBCard != 0 && _randomGenerator.RandomNumber() <= secondChanceDropBCard; + + if (secondChanceDrop) + { + session.BroadcastEffectInRange(EffectType.DoubleChanceDrop); + } + + if (session.CurrentMapInstance.HasMapFlag(MapFlags.ACT_4) || session.CurrentMapInstance.HasMapFlag(MapFlags.HAS_DROP_DIRECTLY_IN_INVENTORY_ENABLED) + || monsterEntityToAttack.DropToInventory) + { + var alreadyGifted = new HashSet(); + foreach (IBattleEntity entity in monsterEntityToAttack.Damagers) + { + long charId = entity.Id; + if (alreadyGifted.Contains(charId)) + { + continue; + } + + IClientSession giftSession = _sessionManager.GetSessionByCharacterId(charId); + + if (giftSession == null) + { + continue; + } + + if (giftSession.PlayerEntity.MapInstance?.Id != monsterEntityToAttack.MapInstance?.Id) + { + continue; + } + + bool shouldReceiveDrop = ShouldReceiveDrop(giftSession, monsterEntityToAttack); + if (!shouldReceiveDrop) + { + continue; + } + + await giftSession.EmitEventAsync(new GenerateGoldEvent + ( + (long)(gold * (1 + giftSession.PlayerEntity.BCardComponent.GetAllBCardsInformation(BCardType.Item, + (byte)AdditionalTypes.Item.IncreaseEarnedGold, giftSession.PlayerEntity.Level).firstData * 0.01)) + )); + + if (secondChanceDrop) + { + await giftSession.EmitEventAsync(new GenerateGoldEvent + ( + (long)(gold * (1 + giftSession.PlayerEntity.BCardComponent.GetAllBCardsInformation(BCardType.Item, + (byte)AdditionalTypes.Item.IncreaseEarnedGold, giftSession.PlayerEntity.Level).firstData * 0.01)) + )); + } + + alreadyGifted.Add(charId); + } + + return; + } + + string itemName = _itemsManager.GetItem((short)ItemVnums.GOLD).Name; + if (playerGroup == null) + { + var dropGold = new DropMapItemEvent(session.PlayerEntity.MapInstance, monsterEntityToAttack.Position, (short)ItemVnums.GOLD, gold, ownerId: firstAttacker.Id); + await _eventPipeline.ProcessEventAsync(dropGold); + + if (secondChanceDrop) + { + await _eventPipeline.ProcessEventAsync(dropGold); + } + + return; + } + + if (playerGroup.SharingMode == (byte)GroupSharingType.ByOrder) + { + long? dropOwner = playerGroup.GetNextOrderedCharacterId(firstAttacker); + + if (!dropOwner.HasValue) + { + return; + } + + foreach (IPlayerEntity s in playerGroup.Members) + { + string itemNameTranslated = _gameLanguage.GetLanguage(GameDataType.Item, itemName, s.Session.UserLanguage); + s.Session.SendChatMessage( + s.Session.GetLanguageFormat(GameDialogKey.GROUP_CHATMESSAGE_DROP_ORDERED, gold, itemNameTranslated, playerGroup.Members.FirstOrDefault(c => c.Id == (long)dropOwner)?.Name), + ChatMessageColorType.Yellow); + } + + var dropGold = new DropMapItemEvent(session.PlayerEntity.MapInstance, monsterEntityToAttack.Position, (short)ItemVnums.GOLD, gold, ownerId: dropOwner.Value); + await _eventPipeline.ProcessEventAsync(dropGold); + if (secondChanceDrop) + { + await _eventPipeline.ProcessEventAsync(dropGold); + } + } + else + { + foreach (IPlayerEntity s in playerGroup.Members) + { + string itemNameTranslated = _gameLanguage.GetLanguage(GameDataType.Item, itemName, s.Session.UserLanguage); + s.Session.SendChatMessage(s.Session.GetLanguageFormat(GameDialogKey.GROUP_CHATMESSAGE_DROP_SHARED, gold, itemNameTranslated), ChatMessageColorType.Yellow); + } + + var dropGold = new DropMapItemEvent(session.PlayerEntity.MapInstance, monsterEntityToAttack.Position, (short)ItemVnums.GOLD, gold, ownerId: session.PlayerEntity.Id); + await _eventPipeline.ProcessEventAsync(dropGold); + if (secondChanceDrop) + { + await _eventPipeline.ProcessEventAsync(dropGold); + } + } + } + + private int GetGold(IPlayerEntity playerEntity, IMonsterEntity monsterEntity) + { + if (!playerEntity.MapInstance.HasMapFlag(MapFlags.IS_BASE_MAP) && playerEntity.MapInstance.MapInstanceType != MapInstanceType.TimeSpaceInstance) + { + return 0; + } + + int lowBaseGold = _randomGenerator.RandomNumber(6 * monsterEntity.Level, 12 * monsterEntity.Level); + return lowBaseGold * _serverManager.GoldRate; + } + + private bool HasLevelPenalty(IPlayerEntity playerEntity, IMonsterEntity monsterEntity) + { + if (monsterEntity.Level >= 70) + { + return false; + } + + return playerEntity.Level - monsterEntity.Level > 10; + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/LevelUpEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/LevelUpEventHandler.cs new file mode 100644 index 0000000..ee87e64 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/LevelUpEventHandler.cs @@ -0,0 +1,208 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.Families; +using WingsAPI.Game.Extensions.Groups; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage.Configuration; +using WingsEmu.Game.Algorithm; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Families.Enum; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Character; +using WingsEmu.Packets.Enums.Chat; +using WingsEmu.Packets.Enums.Families; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Characters; + +public class LevelUpEventHandler : IAsyncEventProcessor +{ + private readonly ICharacterAlgorithm _characterAlgorithm; + private readonly IGameLanguageService _gameLanguage; + private readonly IServerManager _serverManager; + private readonly ISkillsManager _skillsManager; + private readonly ISpPartnerConfiguration _spPartnerConfiguration; + + public LevelUpEventHandler(IServerManager serverManager, + IGameLanguageService gameLanguage, ICharacterAlgorithm characterAlgorithm, ISkillsManager skillsManager, ISpPartnerConfiguration spPartnerConfiguration) + { + _serverManager = serverManager; + _gameLanguage = gameLanguage; + _characterAlgorithm = characterAlgorithm; + _skillsManager = skillsManager; + _spPartnerConfiguration = spPartnerConfiguration; + } + + public async Task HandleAsync(LevelUpEvent e, CancellationToken cancellation) + { + IPlayerEntity character = e.Sender.PlayerEntity; + + switch (e.LevelType) + { + case LevelType.Level: + await HandleLevelUp(character, e.Sender); + break; + case LevelType.JobLevel: + await HandleJobLevelUp(character, e.Sender); + break; + case LevelType.SpJobLevel: + await HandleSpJobLevelUp(character, e.Sender); + break; + case LevelType.Heroic: + await HandleHeroicLevelUp(character, e.Sender); + break; + case LevelType.Fairy: + await HandleFairyLevelUp(character, e.Sender); + break; + } + + e.Sender.RefreshLevel(_characterAlgorithm); + } + + private async Task HandleLevelUp(IPlayerEntity character, IClientSession session) + { + character.LevelXp -= _characterAlgorithm.GetLevelXp(character.Level); + character.Level++; + + if (character.Level >= _serverManager.MaxLevel) + { + character.Level = (byte)_serverManager.MaxLevel; + character.LevelXp = 0; + } + + if (character.Level == _serverManager.HeroicStartLevel && character.HeroLevel == 0) + { + character.HeroLevel = 1; + character.HeroXp = 0; + } + + character.Session.RefreshStatChar(); + + character.Hp = character.MaxHp; + character.Mp = character.MaxMp; + + character.Session.RefreshStat(); + + if (character.Level > 20 && (character.Level % 10) == 0) + { + await session.FamilyAddLogAsync(FamilyLogType.LevelUp, character.Name, character.Level.ToString()); + await session.FamilyAddExperience(character.Level * 20, FamXpObtainedFromType.LevelUp); + } + else if (character.Level > 80) + { + await session.FamilyAddLogAsync(FamilyLogType.LevelUp, character.Name, character.Level.ToString()); + } + + session.SendLevelUp(); + session.RefreshGroupLevelUi(_spPartnerConfiguration); + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_LEVELUP, session.UserLanguage), MsgMessageType.Middle); + session.BroadcastEffectInRange(EffectType.NormalLevelUp); + session.BroadcastEffectInRange(EffectType.NormalLevelUpSubEffect); + } + + private async Task HandleJobLevelUp(IPlayerEntity character, IClientSession session) + { + bool isAdventurer = character.Class == ClassType.Adventurer; + character.JobLevelXp -= _characterAlgorithm.GetJobXp(character.JobLevel, isAdventurer); + character.JobLevel++; + + if (character.JobLevel >= 20 && character.Class == ClassType.Adventurer) + { + character.JobLevel = 20; + character.JobLevelXp = 0; + } + else if (character.JobLevel >= _serverManager.MaxJobLevel) + { + character.JobLevel = (byte)_serverManager.MaxJobLevel; + character.JobLevelXp = 0; + } + + session.SendLevelUp(); + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_JOB_LEVELUP, session.UserLanguage), MsgMessageType.Middle); + session.BroadcastEffectInRange(EffectType.JobLevelUp); + character.SkillComponent.ResetSkillCooldowns = DateTime.UtcNow; + } + + private async Task HandleHeroicLevelUp(IPlayerEntity character, IClientSession session) + { + character.HeroXp -= _characterAlgorithm.GetHeroLevelXp(character.HeroLevel); + character.HeroLevel++; + + if (character.HeroLevel >= _serverManager.MaxHeroLevel) + { + character.HeroLevel = (byte)_serverManager.MaxHeroLevel; + character.HeroXp = 0; + } + + character.Hp = character.MaxHp; + character.Mp = character.MaxMp; + + character.Session.RefreshStat(); + + if (character.HeroLevel > 1 && (character.HeroLevel % 10) == 0) + { + await session.FamilyAddLogAsync(FamilyLogType.HeroLevelUp, character.Name, character.HeroLevel.ToString()); + await session.FamilyAddExperience(character.HeroLevel * 20, FamXpObtainedFromType.LevelUp); + } + else if (character.HeroLevel > 50) + { + await session.FamilyAddLogAsync(FamilyLogType.HeroLevelUp, character.Name, character.HeroLevel.ToString()); + } + + session.SendLevelUp(); + session.RefreshGroupLevelUi(_spPartnerConfiguration); + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.INFORMATION_HERO_LEVELUP, session.UserLanguage), MsgMessageType.Middle); + session.BroadcastEffectInRange(EffectType.NormalLevelUp); + session.BroadcastEffectInRange(EffectType.NormalLevelUpSubEffect); + } + + private async Task HandleSpJobLevelUp(IPlayerEntity character, IClientSession session) + { + character.Specialist.Xp -= _characterAlgorithm.GetSpecialistJobXp(character.Specialist.SpLevel, character.Specialist.IsFunSpecialist()); + character.Specialist.SpLevel++; + + if (character.Specialist.SpLevel >= _serverManager.MaxSpLevel) + { + character.Specialist.SpLevel = (byte)_serverManager.MaxSpLevel; + character.Specialist.Xp = 0; + } + + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.SPECIALIST_SHOUTMESSAGE_LEVELUP, session.UserLanguage), MsgMessageType.Middle); + session.BroadcastEffectInRange(EffectType.JobLevelUp); + character.SkillComponent.ResetSpSkillCooldowns = DateTime.UtcNow; + } + + private async Task HandleFairyLevelUp(IPlayerEntity character, IClientSession session) + { + GameItemInstance fairy = character.Fairy; + if (fairy == null) + { + return; + } + + int fairyXp = _characterAlgorithm.GetFairyXp((short)(fairy.ElementRate + fairy.GameItem.ElementRate)); + fairy.Xp -= fairyXp; + fairy.ElementRate++; + + string fairyName = _gameLanguage.GetLanguage(GameDataType.Item, fairy.GameItem.Name, session.UserLanguage); + if ((fairy.ElementRate + fairy.GameItem.ElementRate) == fairy.GameItem.MaxElementRate) + { + fairy.Xp = 0; + session.SendMsg(_gameLanguage.GetLanguageFormat(GameDialogKey.INFORMATION_SHOUTMESSAGE_FAIRYMAX, session.UserLanguage, fairyName), MsgMessageType.Middle); + } + else + { + session.SendMsg(_gameLanguage.GetLanguageFormat(GameDialogKey.INFORMATION_SHOUTMESSAGE_FAIRY_LEVELUP, session.UserLanguage, fairyName), MsgMessageType.Middle); + } + + session.RefreshFairy(); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/MonsterCaptureEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/MonsterCaptureEventHandler.cs new file mode 100644 index 0000000..11f209e --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/MonsterCaptureEventHandler.cs @@ -0,0 +1,196 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.Quests; +using WingsEmu.DTOs.Quests; +using WingsEmu.Game; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Entities.Extensions; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Mates.Events; +using WingsEmu.Game.Monster.Event; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Npcs; +using WingsEmu.Game.Quests; +using WingsEmu.Game.Quests.Event; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Characters; + +public class MonsterCaptureEventHandler : IAsyncEventProcessor +{ + private const int CAPTURE_RATE = 50; + private readonly IAsyncEventPipeline _asyncEventPipeline; + private readonly IGameLanguageService _gameLanguage; + private readonly IMateEntityFactory _mateEntityFactory; + private readonly INpcMonsterManager _npcMonsterManager; + private readonly IQuestManager _questManager; + private readonly IRandomGenerator _randomGenerator; + + public MonsterCaptureEventHandler(IGameLanguageService gameLanguage, IRandomGenerator randomGenerator, + INpcMonsterManager npcMonsterManager, IMateEntityFactory mateEntityFactory, IAsyncEventPipeline asyncEventPipeline, IQuestManager questManager) + { + _gameLanguage = gameLanguage; + _randomGenerator = randomGenerator; + _npcMonsterManager = npcMonsterManager; + _mateEntityFactory = mateEntityFactory; + _asyncEventPipeline = asyncEventPipeline; + _questManager = questManager; + } + + public async Task HandleAsync(MonsterCaptureEvent e, CancellationToken cancellation) + { + IMonsterEntity monsterEntityToCapture = e.Target; + IClientSession session = e.Sender; + SkillInfo skill = e.Skill; + bool isSkill = e.IsSkill; + + int captureRate = session.PlayerEntity.Level < 20 ? 90 : CAPTURE_RATE; + + if (_randomGenerator.RandomNumber() > captureRate) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.SKILL_SHOUTMESSAGE_CAPTURE_FAILED, session.UserLanguage), MsgMessageType.Middle); + if (!isSkill) + { + return; + } + + e.Sender.CurrentMapInstance.Broadcast(session.PlayerEntity.GenerateSuCapturePacket(monsterEntityToCapture, skill, true)); + return; + } + + session.BroadcastEffect(EffectType.CatchSuccess); + if (session.PlayerEntity.HasQuestWithQuestType(QuestType.CAPTURE_AND_KEEP) || session.PlayerEntity.HasQuestWithQuestType(QuestType.CAPTURE_WITHOUT_KEEPING)) + { + await HandleQuestCapture(session, monsterEntityToCapture, isSkill, skill); + } + else + { + await HandleNormalCapture(session, monsterEntityToCapture, isSkill, skill); + } + } + + private async Task HandleNormalCapture(IClientSession session, IMonsterEntity monsterEntityToCapture, bool isSkill, SkillInfo skill) + { + int monsterVnum = monsterEntityToCapture.MonsterVNum; + int level = monsterEntityToCapture.Level - 15 < 1 ? 1 : monsterEntityToCapture.Level - 15; + IMateEntity currentMateEntity = session.PlayerEntity.MateComponent.GetTeamMember(m => m.MateType == MateType.Pet); + + if (isSkill) + { + session.CurrentMapInstance.Broadcast(session.PlayerEntity.GenerateSuCapturePacket(monsterEntityToCapture, skill, false)); + } + + monsterEntityToCapture.MapInstance.Broadcast(monsterEntityToCapture.GenerateOut()); + await _asyncEventPipeline.ProcessEventAsync(new MonsterDeathEvent(monsterEntityToCapture)); + + var mateNpc = new MonsterData(_npcMonsterManager.GetNpc(monsterVnum)); + IMateEntity newMate = _mateEntityFactory.CreateMateEntity(session.PlayerEntity, mateNpc, MateType.Pet, (byte)level); + + await session.EmitEventAsync(new MateInitializeEvent + { + MateEntity = newMate + }); + + if (currentMateEntity == null) + { + await session.EmitEventAsync(new MateJoinTeamEvent + { + MateEntity = newMate, + IsNewCreated = true + }); + } + else + { + await session.EmitEventAsync(new MateLeaveTeamEvent { MateEntity = newMate }); + } + + session.SendMsg(_gameLanguage.GetLanguageFormat(GameDialogKey.SKILL_SHOUTMESSAGE_CAUGHT_PET, session.UserLanguage, + _gameLanguage.GetLanguage(GameDataType.NpcMonster, mateNpc.Name, session.UserLanguage)), MsgMessageType.Middle); + } + + private async Task HandleQuestCapture(IClientSession session, IMonsterEntity monsterEntityToCapture, bool isSkill, SkillInfo skill) + { + IEnumerable characterQuests = session.PlayerEntity.GetCurrentQuests() + .Where(s => s.Quest.QuestType == QuestType.CAPTURE_AND_KEEP || s.Quest.QuestType == QuestType.CAPTURE_WITHOUT_KEEPING); + + foreach (CharacterQuest characterQuest in characterQuests) + { + IEnumerable objectives = characterQuest.Quest.Objectives; + foreach (QuestObjectiveDto objective in objectives) + { + if (monsterEntityToCapture.MonsterVNum != objective.Data0) + { + continue; + } + + CharacterQuestObjectiveDto questObjectiveDto = characterQuest.ObjectiveAmount[objective.ObjectiveIndex]; + switch (characterQuest.Quest.QuestType) + { + case QuestType.CAPTURE_WITHOUT_KEEPING: + monsterEntityToCapture.MapInstance?.DespawnMonster(monsterEntityToCapture); + + if (isSkill) + { + session.CurrentMapInstance.Broadcast(session.PlayerEntity.GenerateSuCapturePacket(monsterEntityToCapture, skill, false)); + } + + if (questObjectiveDto.CurrentAmount >= questObjectiveDto.RequiredAmount) + { + continue; + } + + questObjectiveDto.CurrentAmount++; + await session.EmitEventAsync(new QuestObjectiveUpdatedEvent + { + CharacterQuest = characterQuest + }); + + session.RefreshQuestProgress(_questManager, characterQuest.QuestId); + if (session.PlayerEntity.IsQuestCompleted(characterQuest)) + { + await session.EmitEventAsync(new QuestCompletedEvent(characterQuest)); + } + + break; + + case QuestType.CAPTURE_AND_KEEP: + await HandleNormalCapture(session, monsterEntityToCapture, isSkill, skill); + + if (questObjectiveDto.CurrentAmount >= questObjectiveDto.RequiredAmount) + { + continue; + } + + questObjectiveDto.CurrentAmount++; + await session.EmitEventAsync(new QuestObjectiveUpdatedEvent + { + CharacterQuest = characterQuest + }); + + session.RefreshQuestProgress(_questManager, characterQuest.QuestId); + if (session.PlayerEntity.IsQuestCompleted(characterQuest)) + { + await session.EmitEventAsync(new QuestCompletedEvent(characterQuest)); + } + + break; + } + + // If it has already been captured, it ends the loop + return; + } + } + + await HandleNormalCapture(session, monsterEntityToCapture, isSkill, skill); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/NormalChatEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/NormalChatEventHandler.cs new file mode 100644 index 0000000..063ea3a --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/NormalChatEventHandler.cs @@ -0,0 +1,55 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Entities.Extensions; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Networking.Broadcasting; +using WingsEmu.Game.TimeSpaces; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Characters; + +public class NormalChatEventHandler : IAsyncEventProcessor +{ + public async Task HandleAsync(NormalChatEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + + if (session.CurrentMapInstance.HasMapFlag(MapFlags.ACT_4)) + { + session.CurrentMapInstance.Broadcast(session.PlayerEntity.GenerateSayPacket(e.Message.Trim(), ChatMessageColorType.White), + new FactionBroadcast(session.PlayerEntity.Faction), new ExceptSessionBroadcast(session)); + + FactionType enemyFaction = session.PlayerEntity.Faction == FactionType.Angel ? FactionType.Demon : FactionType.Angel; + session.CurrentMapInstance.Broadcast(session.PlayerEntity.GenerateSayPacket("^$#%#&^%$@#", ChatMessageColorType.PlayerSay), + new FactionBroadcast(enemyFaction, true), new ExceptSessionBroadcast(session)); + return; + } + + session.CurrentMapInstance.Broadcast(session.PlayerEntity.GenerateSayPacket(e.Message.Trim(), ChatMessageColorType.White), new ExceptSessionBroadcast(session), + new RainbowTeamBroadcast(session)); + + TimeSpaceParty timeSpace = session.PlayerEntity.TimeSpaceComponent.TimeSpace; + if (timeSpace?.Instance == null) + { + return; + } + + foreach (TimeSpaceSubInstance timeSpaceSubInstance in timeSpace.Instance.TimeSpaceSubInstances.Values) + { + if (session.CurrentMapInstance.Id == timeSpaceSubInstance.MapInstance.Id) + { + continue; + } + + foreach (IClientSession member in timeSpaceSubInstance.MapInstance.Sessions) + { + session.SendSpeakToTarget(member, e.Message.Trim(), SpeakType.Normal); + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/PartnerKillBonusEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/PartnerKillBonusEventHandler.cs new file mode 100644 index 0000000..57fd5d4 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/PartnerKillBonusEventHandler.cs @@ -0,0 +1,77 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Extensions.Mates; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Characters; + +public sealed class PartnerKillBonusEventHandler : IAsyncEventProcessor +{ + private readonly IGameLanguageService _gameLanguage; + + public PartnerKillBonusEventHandler(IGameLanguageService gameLanguage) => _gameLanguage = gameLanguage; + + public async Task HandleAsync(KillBonusEvent e, CancellationToken cancellation) + { + IMonsterEntity monsterEntityToAttack = e.MonsterEntity; + IPlayerEntity character = e.Sender.PlayerEntity; + IClientSession session = e.Sender; + + if (monsterEntityToAttack == null || monsterEntityToAttack.IsAlive()) + { + return; + } + + IMateEntity partnerInTeam = character.MateComponent.GetTeamMember(s => s.MateType == MateType.Partner); + + if (character.Level < monsterEntityToAttack.Level + 15 && !monsterEntityToAttack.IsMateTrainer) + { + if (partnerInTeam?.Specialist == null || partnerInTeam.Specialist.Agility == 100) + { + return; + } + + partnerInTeam.Specialist.Agility = (byte)(partnerInTeam.Specialist.Agility + 2 > 100 ? 100 : partnerInTeam.Specialist.Agility + 2); + session.SendPetInfo(partnerInTeam, _gameLanguage); + + if (partnerInTeam.Specialist.Agility != 100) + { + return; + } + + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.PARTNER_MESSAGE_100_AGILITY, session.UserLanguage), ChatMessageColorType.Yellow); + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.PARTNER_MESSAGE_100_AGILITY, session.UserLanguage), MsgMessageType.Middle); + + return; + } + + if (character.Level < monsterEntityToAttack.Level + 30 && !monsterEntityToAttack.IsMateTrainer) + { + if (partnerInTeam?.Specialist == null || partnerInTeam.Specialist.Agility == 100) + { + return; + } + + partnerInTeam.Specialist.Agility += 1; + session.SendPetInfo(partnerInTeam, _gameLanguage); + + if (partnerInTeam.Specialist.Agility != 100) + { + return; + } + + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.PARTNER_MESSAGE_100_AGILITY, session.UserLanguage), ChatMessageColorType.Yellow); + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.PARTNER_MESSAGE_100_AGILITY, session.UserLanguage), MsgMessageType.Middle); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/PlayerChangeChannelAct4EventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/PlayerChangeChannelAct4EventHandler.cs new file mode 100644 index 0000000..841e3aa --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/PlayerChangeChannelAct4EventHandler.cs @@ -0,0 +1,90 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using PhoenixLib.Logging; +using WingsAPI.Communication; +using WingsAPI.Communication.ServerApi; +using WingsAPI.Communication.ServerApi.Protocol; +using WingsAPI.Packets.Enums; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game; +using WingsEmu.Game._enum; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Maps; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Characters; + +public class PlayerChangeChannelAct4EventHandler : IAsyncEventProcessor +{ + private readonly SerializableGameServer _gameServer; + private readonly IMapManager _mapManager; + private readonly IRandomGenerator _randomGenerator; + private readonly IServerApiService _serverApiService; + private readonly IServerManager _serverManager; + + public PlayerChangeChannelAct4EventHandler(IServerApiService serverApiService, IServerManager serverManager, SerializableGameServer gameServer, IRandomGenerator randomGenerator, + IMapManager mapManager) + { + _serverApiService = serverApiService; + _serverManager = serverManager; + _gameServer = gameServer; + _randomGenerator = randomGenerator; + _mapManager = mapManager; + } + + public async Task HandleAsync(PlayerChangeChannelAct4Event e, CancellationToken cancellation) + { + if (e.Sender.PlayerEntity.Faction == FactionType.Neutral) + { + return; + } + + GetChannelInfoResponse response = null; + try + { + response = await _serverApiService.GetAct4ChannelInfo(new GetAct4ChannelInfoRequest + { + WorldGroup = _serverManager.ServerGroup + }); + } + catch (Exception ex) + { + Log.Error("[PLAYER_CHANGE_CHANNEL_ACT4] Unexpected error while trying to obtain channel's Act4 information: ", ex); + } + + SerializableGameServer gameServer = response?.GameServer; + if (response?.ResponseType != RpcResponseType.SUCCESS) + { + Log.Error("[GetAct4ChannelInfoRequest] Response type is not success.", new Exception()); + return; + } + + if (gameServer == null) + { + Log.Error("[GetAct4ChannelInfoRequest] Game server is null.", new Exception()); + return; + } + + if (gameServer.ChannelId == _gameServer.ChannelId) + { + Log.Error("[GetAct4ChannelInfoRequest] It's the same channel id.", new Exception()); + return; + } + + short mapId = e.Sender.PlayerEntity.Faction == FactionType.Angel ? (short)MapIds.ACT4_ANGEL_CITADEL : (short)MapIds.ACT4_DEMON_CITADEL; + short mapX = (short)(12 + _randomGenerator.RandomNumber(-2, 3)); + short mapY = (short)(40 + _randomGenerator.RandomNumber(-2, 3)); + + if (_mapManager.HasMapFlagByMapId(e.Sender.PlayerEntity.MapId, MapFlags.ACT_4)) + { + mapId = (short)e.Sender.PlayerEntity.MapId; + mapX = e.Sender.PlayerEntity.MapX; + mapY = e.Sender.PlayerEntity.MapY; + } + + await e.Sender.EmitEventAsync(new PlayerChangeChannelEvent(gameServer, ItModeType.ToAct4, mapId, mapX, mapY)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/PlayerChangeChannelEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/PlayerChangeChannelEventHandler.cs new file mode 100644 index 0000000..7b989c9 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/PlayerChangeChannelEventHandler.cs @@ -0,0 +1,43 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using PhoenixLib.Logging; +using WingsAPI.Communication; +using WingsAPI.Communication.Sessions; +using WingsAPI.Communication.Sessions.Request; +using WingsAPI.Communication.Sessions.Response; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Characters; + +public class PlayerChangeChannelEventHandler : IAsyncEventProcessor +{ + private readonly ISessionService _sessionService; + + public PlayerChangeChannelEventHandler(ISessionService sessionService) => _sessionService = sessionService; + + public async Task HandleAsync(PlayerChangeChannelEvent e, CancellationToken cancellation) + { + SessionResponse result = await _sessionService.ActivateCrossChannelAuthentication(new ActivateCrossChannelAuthenticationRequest + { + AccountId = e.Sender.Account.Id, + ChannelId = e.GameServer.ChannelId + }); + + if (result.ResponseType != RpcResponseType.SUCCESS) + { + Log.Warn("[CROSS_AUTH] Failure"); + return; + } + + e.Sender.PlayerEntity.MapId = e.MapId; + e.Sender.PlayerEntity.MapX = e.MapX; + e.Sender.PlayerEntity.MapY = e.MapY; + + e.Sender.SendMzPacket(e.GameServer.EndPointIp, (short)e.GameServer.EndPointPort); + e.Sender.SendItPacket(e.ModeType); + + e.Sender.ForceDisconnect(); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/PlayerDeathEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/PlayerDeathEventHandler.cs new file mode 100644 index 0000000..2554222 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/PlayerDeathEventHandler.cs @@ -0,0 +1,75 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.CharacterExtensions; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Raids.Events; +using WingsEmu.Game.Revival; +using WingsEmu.Game.Skills; +using WingsEmu.Game.TimeSpaces.Events; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Characters; + +public class PlayerDeathEventHandler : IAsyncEventProcessor +{ + private readonly IMeditationManager _meditation; + private readonly ISpyOutManager _spyOutManager; + + public PlayerDeathEventHandler(IMeditationManager meditation, ISpyOutManager spyOutManager) + { + _meditation = meditation; + _spyOutManager = spyOutManager; + } + + public async Task HandleAsync(PlayerDeathEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + IPlayerEntity character = session.PlayerEntity; + + if (character == null || character.IsAlive()) + { + return; + } + + if (_spyOutManager.ContainsSpyOut(character.Id)) + { + _spyOutManager.RemoveSpyOutSkill(character.Id); + session.SendObArPacket(); + } + + await character.Session.EmitEventAsync(new RemoveVehicleEvent()); + await character.Session.EmitEventAsync(new GetDefaultMorphEvent()); + await character.Session.EmitEventAsync(new RemoveAdditionalHpMpEvent + { + Hp = character.AdditionalHp, + Mp = character.AdditionalMp + }); + + character.ClearFoodBuffer(); + character.ClearSnackBuffer(); + character.BCardComponent.ClearChargeBCard(); + character.ChargeComponent.ResetCharge(); + character.HitsByMonsters.Clear(); + character.Killer = e.Killer; + character.LastDeath = DateTime.UtcNow; + character.RemoveMeditation(_meditation); + + switch (session.CurrentMapInstance.MapInstanceType) + { + case MapInstanceType.RaidInstance: + await session.EmitEventAsync(new RaidDiedEvent()); + break; + case MapInstanceType.TimeSpaceInstance: + await session.EmitEventAsync(new TimeSpaceDeathEvent()); + break; + } + + await session.EmitEventAsync(new RevivalStartProcedureEvent(e.Killer)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/PlayerRestEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/PlayerRestEventHandler.cs new file mode 100644 index 0000000..1dd15ff --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/PlayerRestEventHandler.cs @@ -0,0 +1,73 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using PhoenixLib.Logging; +using WingsAPI.Game.Extensions.CharacterExtensions; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Mates.Events; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Characters; + +public class PlayerRestEventHandler : IAsyncEventProcessor +{ + private readonly IGameLanguageService _gameLanguage; + private readonly IMeditationManager _meditation; + + public PlayerRestEventHandler(IGameLanguageService gameLanguage, IMeditationManager meditation) + { + _gameLanguage = gameLanguage; + _meditation = meditation; + } + + public async Task HandleAsync(PlayerRestEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + IPlayerEntity character = e.Sender.PlayerEntity; + bool force = e.Force; + + DateTime now = DateTime.UtcNow; + if (!character.IsSitting && + (character.LastSkillUse.AddSeconds(4) > now || character.LastDefence.AddSeconds(4) > now) && !force) + { + Log.Debug($"{nameof(PlayerRestEventHandler)} Can't rest (probably in combat)."); + return; + } + + if (character.IsOnVehicle || character.IsMorphed) + { + string message = _gameLanguage.GetLanguage(GameDialogKey.INFORMATION_CHATMESSAGE_IMPOSSIBLE_TO_USE, session.UserLanguage); + session.SendChatMessage(message, ChatMessageColorType.Yellow); + return; + } + + character.LastSitting = now; + character.IsSitting = !character.IsSitting; + session.BroadcastRest(); + session.PlayerEntity.RemoveMeditation(_meditation); + + if (e.RestTeamMemberMates) + { + foreach (IMateEntity mate in character.MateComponent.TeamMembers()) + { + await session.EmitEventAsync(new MateRestEvent + { + MateEntity = mate, + Rest = character.IsSitting + }); + } + } + + if (!character.IsSitting) + { + character.ClearFoodBuffer(); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/PlayerReturnFromAct4EventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/PlayerReturnFromAct4EventHandler.cs new file mode 100644 index 0000000..ab02f44 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/PlayerReturnFromAct4EventHandler.cs @@ -0,0 +1,65 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Communication; +using WingsAPI.Communication.ServerApi; +using WingsAPI.Communication.ServerApi.Protocol; +using WingsAPI.Communication.Sessions; +using WingsAPI.Communication.Sessions.Request; +using WingsAPI.Communication.Sessions.Response; +using WingsAPI.Packets.Enums; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Characters; + +public class PlayerReturnFromAct4EventHandler : IAsyncEventProcessor +{ + private readonly IGameLanguageService _language; + private readonly IServerApiService _serverApiService; + private readonly IServerManager _serverManager; + private readonly ISessionService _sessionService; + + public PlayerReturnFromAct4EventHandler(ISessionService sessionService, IGameLanguageService language, IServerApiService serverApiService, IServerManager serverManager) + { + _sessionService = sessionService; + _language = language; + _serverApiService = serverApiService; + _serverManager = serverManager; + } + + + public async Task HandleAsync(PlayerReturnFromAct4Event e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + SessionResponse sessionResponse = await _sessionService.GetSessionByAccountId(new GetSessionByAccountIdRequest + { + AccountId = session.Account.Id + }); + + if (sessionResponse?.ResponseType != RpcResponseType.SUCCESS) + { + return; + } + + if (sessionResponse.Session.LastChannelId == 0) + { + return; + } + + GetChannelInfoResponse response = await _serverApiService.GetChannelInfo(new GetChannelInfoRequest + { + WorldGroup = _serverManager.ServerGroup, + ChannelId = sessionResponse.Session.LastChannelId + }); + + if (response?.ResponseType != RpcResponseType.SUCCESS) + { + return; + } + + await session.EmitEventAsync(new PlayerChangeChannelEvent(response.GameServer, ItModeType.ToPortAlveus, 145, 51, 41)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/RemoveAdditionalHpMpEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/RemoveAdditionalHpMpEventHandler.cs new file mode 100644 index 0000000..77d39b1 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/RemoveAdditionalHpMpEventHandler.cs @@ -0,0 +1,30 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Characters; + +public class RemoveAdditionalHpMpEventHandler : IAsyncEventProcessor +{ + public async Task HandleAsync(RemoveAdditionalHpMpEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + IPlayerEntity character = session.PlayerEntity; + + if (e.Hp > 0) + { + character.AdditionalHp = character.AdditionalHp - e.Hp > 0 ? character.AdditionalHp - e.Hp : 0; + } + + if (e.Mp > 0) + { + character.AdditionalMp = character.AdditionalMp - e.Mp > 0 ? character.AdditionalMp - e.Mp : 0; + } + + session.SendGuriPacket(4); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/RemoveItemTimeEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/RemoveItemTimeEventHandler.cs new file mode 100644 index 0000000..ec96ce0 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/RemoveItemTimeEventHandler.cs @@ -0,0 +1,65 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsAPI.Game.Extensions.ItemExtension.Item; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Inventory; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Characters; + +public class RemoveItemTimeEventHandler : IAsyncEventProcessor +{ + private readonly IGameLanguageService _gameLanguage; + + public RemoveItemTimeEventHandler(IGameLanguageService gameLanguage) => _gameLanguage = gameLanguage; + + public async Task HandleAsync(InventoryExpiredItemsEvent e, CancellationToken cancellation) + { + if (!e.Sender.PlayerEntity.GetAllPlayerInventoryItems().Any()) + { + return; + } + + DateTime date = DateTime.UtcNow; + + foreach (InventoryItem item in e.Sender.PlayerEntity.GetAllPlayerInventoryItems()) + { + if (item == null) + { + continue; + } + + if (!item.ItemInstance.IsBound) + { + continue; + } + + if (item.ItemInstance.ItemDeleteTime == null) + { + continue; + } + + if (item.ItemInstance.ItemDeleteTime >= date) + { + continue; + } + + string itemName = item.ItemInstance.GameItem.GetItemName(_gameLanguage, e.Sender.UserLanguage); + + e.Sender.SendChatMessage(_gameLanguage.GetLanguageFormat(GameDialogKey.ITEM_CHATMESSAGE_TIMEOUT, e.Sender.UserLanguage, itemName), ChatMessageColorType.Red); + + await e.Sender.RemoveItemFromInventory(item: item, isEquiped: item.IsEquipped); + + if (e.Sender.ShouldSendAmuletPacket(item.ItemInstance.GameItem.EquipmentSlot)) + { + e.Sender.SendEmptyAmuletBuffPacket(); + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/RollItemBoxEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/RollItemBoxEventHandler.cs new file mode 100644 index 0000000..250a5ad --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/RollItemBoxEventHandler.cs @@ -0,0 +1,143 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.DTOs.Items; +using WingsEmu.DTOs.ServerDatas; +using WingsEmu.Game; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Characters; + +public class RollItemBoxEventHandler : IAsyncEventProcessor +{ + private readonly IGameItemInstanceFactory _gameItemInstanceFactory; + private readonly IGameLanguageService _gameLanguage; + private readonly IItemBoxManager _itemBoxManager; + private readonly IItemsManager _itemManager; + private readonly IRandomGenerator _random; + private readonly ISessionManager _sessionManager; + + public RollItemBoxEventHandler(IRandomGenerator random, IGameLanguageService gameLanguage, IItemsManager itemManager, ISessionManager sessionManager, + IGameItemInstanceFactory gameItemInstanceFactory, IItemBoxManager itemBoxManager) + { + _random = random; + _gameLanguage = gameLanguage; + _itemManager = itemManager; + _sessionManager = sessionManager; + _gameItemInstanceFactory = gameItemInstanceFactory; + _itemBoxManager = itemBoxManager; + } + + public async Task HandleAsync(RollItemBoxEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + InventoryItem item = e.Item; + + // ItemBoxManager + ItemBoxDto itemBox = _itemBoxManager.GetItemBoxByItemVnumAndDesign(item.ItemInstance.ItemVNum); + if (itemBox == null) + { + return; + } + + if (itemBox.ItemBoxType == ItemBoxType.BUNDLE) + { + foreach (ItemBoxItemDto rollItem in itemBox.Items) + { + GameItemInstance newItem = + _gameItemInstanceFactory.CreateItem(rollItem.ItemGeneratedVNum, rollItem.ItemGeneratedAmount, rollItem.ItemGeneratedUpgrade, (sbyte)item.ItemInstance.Rarity); + await session.AddNewItemToInventory(newItem, true, sendGiftIsFull: true); + if (itemBox.ShowsRaidBoxPanelOnOpen) + { + session.SendRdiPacket(rollItem.ItemGeneratedVNum, rollItem.ItemGeneratedAmount); + } + } + + + await session.RemoveItemFromInventory(item: item); + return; + } + + IReadOnlyCollection rewards = GetRandomRewards(session, itemBox); + List obtainedItems = new(); + foreach (ItemBoxItemDto itemBoxItem in rewards) + { + byte upgrade = itemBoxItem.ItemGeneratedUpgrade; + IGameItem createdGameItem = _itemManager.GetItem(itemBoxItem.ItemGeneratedVNum); + + if (createdGameItem == null) + { + continue; + } + + sbyte rarity = 0; + if (createdGameItem.ItemType != ItemType.Box) + { + rarity = (sbyte)item.ItemInstance.Rarity; + } + + GameItemInstance newItem = _gameItemInstanceFactory.CreateItem(itemBoxItem.ItemGeneratedVNum, itemBoxItem.ItemGeneratedAmount, upgrade, rarity); + obtainedItems.Add(newItem); + await session.AddNewItemToInventory(newItem, true, sendGiftIsFull: true); + + if (itemBox.ShowsRaidBoxPanelOnOpen) + { + session.SendRdiPacket(itemBoxItem.ItemGeneratedVNum, itemBoxItem.ItemGeneratedAmount); + } + } + + await session.RemoveItemFromInventory(item: item); + await session.EmitEventAsync(new BoxOpenedEvent + { + Box = e.Item.ItemInstance, + Rewards = obtainedItems + }); + } + + private IReadOnlyCollection GetRandomRewards(IClientSession session, ItemBoxDto box) + { + var rewards = new List(); + + int minimumRoll = box.MinimumRewards ?? 1; + int maximumRoll = box.MaximumRewards ?? minimumRoll; + int rolls = _random.RandomNumber(minimumRoll, maximumRoll + 1); + + // We group them by category (probability) + var possibleRewards = new Dictionary>(); + foreach (ItemBoxItemDto item in box.Items) + { + if (!possibleRewards.ContainsKey(item.Probability)) + { + possibleRewards[item.Probability] = new List(); + } + + possibleRewards[item.Probability].Add(item); + } + + var randomBag = new RandomBag>(_random); + foreach ((short categoryChance, List items) in possibleRewards) + { + randomBag.AddEntry(items, categoryChance); + } + + for (int i = 0; i < rolls; i++) + { + List randomCategory = randomBag.GetRandom(); + ItemBoxItemDto rolledItem = randomCategory.ElementAt(_random.RandomNumber(randomCategory.Count)); + rewards.Add(rolledItem); + } + + return rewards; + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/SessionSaveEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/SessionSaveEventHandler.cs new file mode 100644 index 0000000..6999efb --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/SessionSaveEventHandler.cs @@ -0,0 +1,130 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.DAL; +using PhoenixLib.Events; +using PhoenixLib.Logging; +using Polly; +using Polly.Retry; +using WingsAPI.Communication; +using WingsAPI.Communication.DbServer.AccountService; +using WingsAPI.Communication.DbServer.CharacterService; +using WingsAPI.Data.Account; +using WingsAPI.Data.Character; +using WingsEmu.Game; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Characters; + +public class SessionSaveEventHandler : IAsyncEventProcessor +{ + private readonly IAccountService _accountService; + private readonly ICharacterService _characterService; + private readonly IMapper _mapper; + private readonly IPlayerEntityFactory _playerEntityFactory; + + public SessionSaveEventHandler(IPlayerEntityFactory playerEntityFactory, ICharacterService characterService, IAccountService accountService, IMapper mapper) + { + _playerEntityFactory = playerEntityFactory; + _characterService = characterService; + _accountService = accountService; + _mapper = mapper; + } + + public async Task HandleAsync(SessionSaveEvent e, CancellationToken cancellation) + { + IPlayerEntity playerEntity = e.Sender.PlayerEntity; + AccountDTO account = _mapper.Map(e.Sender.Account); + long accountId = account.Id; + AsyncRetryPolicy policy = Policy.Handle().RetryAsync(3, + (exception, i1) => Log.Error($"[CHARACTER_SAVE][ACCOUNT_ID: '{accountId.ToString()}'] Failed to save characters, try {i1.ToString()}. ", exception)); + + AccountSaveResponse response = null; + try + { + response = await policy.ExecuteAsync(() => _accountService.SaveAccount(new AccountSaveRequest + { + AccountDto = account + })); + } + catch (Exception ex) + { + Log.Error($"[CHARACTER_SAVE][ACCOUNT_ID: '{account.Id.ToString()}'] Unexpected error: ", ex); + } + + if (response?.ResponseType != RpcResponseType.SUCCESS) + { + Log.Warn($"[CHARACTER_SAVE][ACCOUNT_ID: '{account.Id.ToString()}'] Failed to save account"); + } + + CharacterDTO characterDto = _playerEntityFactory.CreateCharacterDto(playerEntity); + DbServerSaveCharacterResponse response2 = null; + try + { + response2 = await _characterService.SaveCharacter(new DbServerSaveCharacterRequest + { + Character = characterDto + }); + } + catch (Exception ex) + { + Log.Error($"[CHARACTER_SAVE][CHARACTER_ID: '{characterDto.Id.ToString()}'] Unexpected error: ", ex); + } + + if (response2?.RpcResponseType != RpcResponseType.SUCCESS) + { + Log.Warn($"[CHARACTER_SAVE][CHARACTER_ID: '{characterDto.Id.ToString()}'] Failed to save character"); + } + + Log.Warn($"{characterDto.Name} was saved"); + + CheckPlayerMute(playerEntity); + AccountPenaltyMultiSaveResponse response3 = null; + try + { + response3 = await _accountService.SaveAccountPenalties(new AccountPenaltyMultiSaveRequest + { + AccountPenaltyDtos = e.Sender.Account.Logs + }); + } + catch (Exception ex) + { + Log.Error($"[CHARACTER_SAVE][ACCOUNT_ID: '{account.Id.ToString()}'] Unexpected error: ", ex); + } + + if (response3?.ResponseType != RpcResponseType.SUCCESS) + { + Log.Warn($"[CHARACTER_SAVE][ACCOUNT_ID: '{account.Id.ToString()}'] Failed to save account's penalties"); + } + } + + private void CheckPlayerMute(IPlayerEntity playerEntity) + { + if (!playerEntity.MuteRemainingTime.HasValue) + { + return; + } + + AccountPenaltyDto penalty = playerEntity.Session.Account.Logs.FirstOrDefault(x => x.PenaltyType == PenaltyType.Muted && x.RemainingTime.HasValue); + if (penalty == null) + { + return; + } + + if (playerEntity.MuteRemainingTime.Value.TotalMilliseconds > 0) + { + penalty.RemainingTime = (int?)playerEntity.MuteRemainingTime.Value.TotalSeconds; + } + else + { + penalty.RemainingTime = null; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/SpPerfectEvent.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/SpPerfectEvent.cs new file mode 100644 index 0000000..8cc4682 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/SpPerfectEvent.cs @@ -0,0 +1,11 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Game.Inventory; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Characters; + +public class SpPerfectEvent : PlayerEvent +{ + public SpPerfectEvent(InventoryItem inventoryItem) => InventoryItem = inventoryItem; + + public InventoryItem InventoryItem { get; } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/SpPerfectEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/SpPerfectEventHandler.cs new file mode 100644 index 0000000..36014a7 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/SpPerfectEventHandler.cs @@ -0,0 +1,223 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.Core; +using WingsEmu.DTOs.Items; +using WingsEmu.Game; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Items; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Characters; + +public class SpPerfectEventHandler : IAsyncEventProcessor +{ + private readonly IGameLanguageService _gameLanguage; + private readonly IRandomGenerator _randomGenerator; + private readonly SpPerfectionConfiguration _spConfiguration; + + public SpPerfectEventHandler(IRandomGenerator randomGenerator, IGameLanguageService gameLanguageService, SpPerfectionConfiguration spConfiguration) + { + _randomGenerator = randomGenerator; + _gameLanguage = gameLanguageService; + _spConfiguration = spConfiguration; + } + + public async Task HandleAsync(SpPerfectEvent e, CancellationToken cancellation) + { + if (e.InventoryItem.ItemInstance.Type != ItemInstanceType.SpecialistInstance) + { + return; + } + + GameItemInstance sp = e.InventoryItem.ItemInstance; + + if (sp.GameItem.IsPartnerSpecialist) + { + return; + } + + if (sp.Rarity == -2) + { + return; + } + + if (sp.SpStoneUpgrade >= 100) + { + return; + } + + IClientSession session = e.Sender; + + PerfUpgradeConfiguration configuration = _spConfiguration.PerfUpgradeConfigurations + .FirstOrDefault(upgradeConfiguration => + upgradeConfiguration.SpPerfUpgradeRange.Minimum <= sp.SpStoneUpgrade + && sp.SpStoneUpgrade <= upgradeConfiguration.SpPerfUpgradeRange.Maximum + ); + + if (configuration == null) + { + return; + } + + int stoneVNum = GetStoneVnum(sp); + + if (!session.HasEnoughGold(configuration.GoldNeeded)) + { + return; + } + + if (!session.PlayerEntity.HasItem(stoneVNum, (short)configuration.StonesNeeded)) + { + return; + } + + session.PlayerEntity.RemoveGold(configuration.GoldNeeded); + await session.RemoveItemFromInventory(stoneVNum, (short)configuration.StonesNeeded); + + int rnd = _randomGenerator.RandomNumber(); + + var randomBag = new RandomBag(_randomGenerator); + + randomBag.AddEntry(true, configuration.SuccessChance); + randomBag.AddEntry(false, 100 - configuration.SuccessChance); + + bool isSuccess = randomBag.GetRandom(); + + + if (isSuccess) + { + await PerfectSp(sp, session, configuration); + } + else + { + SendBothMessages(session, _gameLanguage.GetLanguage(GameDialogKey.PERFECTSP_MESSAGE_FAILURE, session.UserLanguage), true); + await session.EmitEventAsync(new SpPerfectedEvent + { + Success = false, + Sp = sp, + SpPerfectionLevel = sp.SpStoneUpgrade + }); + } + + session.SendInventoryAddPacket(e.InventoryItem); + session.SendShopEndPacket(ShopEndType.Npc); + } + + private async Task PerfectSp(ItemInstanceDTO spInstance, IClientSession session, PerfUpgradeConfiguration configuration) + { + Range countRange = configuration.StatAmountRange; + byte count = (byte)_randomGenerator.RandomNumber(countRange.Minimum, countRange.Maximum + 1); + + session.SendEffect(EffectType.UpgradeSuccess); + + var randomBag = new RandomBag(_randomGenerator); + foreach ((SpPerfStats stat, short chance) in _spConfiguration.StatProbabilityConfiguration) + { + byte statValue = stat switch + { + SpPerfStats.Attack => spInstance.SpDamage, + SpPerfStats.Defense => spInstance.SpDefence, + SpPerfStats.Element => spInstance.SpElement, + SpPerfStats.HpMp => spInstance.SpHP, + SpPerfStats.ResistanceFire => spInstance.SpFire, + SpPerfStats.ResistanceWater => spInstance.SpWater, + SpPerfStats.ResistanceLight => spInstance.SpLight, + SpPerfStats.ResistanceDark => spInstance.SpDark + }; + + if (statValue >= 50) + { + continue; + } + + randomBag.AddEntry(stat, chance); + } + + SpPerfStats selectedStat = randomBag.GetRandom(); + GameDialogKey dialogSpUpgrade; + + switch (selectedStat) + { + case SpPerfStats.Attack: + spInstance.SpDamage += count; + dialogSpUpgrade = GameDialogKey.PERFECTSP_ATTACK; + break; + case SpPerfStats.Defense: + spInstance.SpDefence += count; + dialogSpUpgrade = GameDialogKey.PERFECTSP_DEFENSE; + break; + case SpPerfStats.Element: + spInstance.SpElement += count; + dialogSpUpgrade = GameDialogKey.PERFECTSP_ELEMENT; + break; + case SpPerfStats.HpMp: + spInstance.SpHP += count; + dialogSpUpgrade = GameDialogKey.PERFECTSP_HPMP; + break; + case SpPerfStats.ResistanceFire: + spInstance.SpFire += count; + dialogSpUpgrade = GameDialogKey.PERFECTSP_FIRE; + break; + case SpPerfStats.ResistanceWater: + spInstance.SpWater += count; + dialogSpUpgrade = GameDialogKey.PERFECTSP_WATER; + break; + case SpPerfStats.ResistanceLight: + spInstance.SpLight += count; + dialogSpUpgrade = GameDialogKey.PERFECTSP_LIGHT; + break; + case SpPerfStats.ResistanceDark: + spInstance.SpDark += count; + dialogSpUpgrade = GameDialogKey.PERFECTSP_SHADOW; + break; + default: + throw new ArgumentOutOfRangeException(); + } + + SuccessMessageGenerator(session, GameDialogKey.PERFECTSP_MESSAGE_SUCCESS, dialogSpUpgrade, count); + spInstance.SpStoneUpgrade++; + await session.EmitEventAsync(new SpPerfectedEvent + { + Success = true, + Sp = spInstance, + SpPerfectionLevel = spInstance.SpStoneUpgrade + }); + } + + private void SuccessMessageGenerator(IClientSession session, GameDialogKey baseDialog, GameDialogKey successType, byte count) + { + SendBothMessages(session, _gameLanguage.GetLanguageFormat(baseDialog, session.UserLanguage, + _gameLanguage.GetLanguage(successType, session.UserLanguage), count.ToString()), false); + } + + public void SendBothMessages(IClientSession session, string message, bool bad) + { + ChatMessageColorType color = bad ? ChatMessageColorType.Red : ChatMessageColorType.Green; + + session.SendChatMessage(message, color); + session.SendMsg(message, MsgMessageType.Middle); + } + + private int GetStoneVnum(GameItemInstance sp) + { + foreach (SpStoneLink spStonesLink in _spConfiguration.SpStonesLinks) + { + if (spStonesLink.SpVnums.Contains(sp.ItemVNum)) + { + return spStonesLink.StoneVnum; + } + } + + return 0; + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/SpTransformEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/SpTransformEventHandler.cs new file mode 100644 index 0000000..302ae9c --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/SpTransformEventHandler.cs @@ -0,0 +1,168 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.Quicklist; +using WingsEmu.DTOs.Skills; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage.Configuration; +using WingsEmu.Game.Algorithm; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Networking.Broadcasting; +using WingsEmu.Game.Raids; +using WingsEmu.Game.Skills; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Characters; + +public class SpTransformEventHandler : IAsyncEventProcessor +{ + private readonly IBuffFactory _buffFactory; + private readonly ICharacterAlgorithm _characterAlgorithm; + private readonly IGameLanguageService _languageService; + private readonly IRankingManager _rankingManager; + private readonly IReputationConfiguration _reputationConfiguration; + private readonly ISkillsManager _skillsManager; + private readonly ISpWingConfiguration _spWingConfiguration; + + public SpTransformEventHandler(IGameLanguageService languageService, ISkillsManager skillsManager, ISpWingConfiguration spWingConfiguration, IBuffFactory buffFactory, + ICharacterAlgorithm characterAlgorithm, IReputationConfiguration reputationConfiguration, IRankingManager rankingManager) + { + _languageService = languageService; + _skillsManager = skillsManager; + _spWingConfiguration = spWingConfiguration; + _buffFactory = buffFactory; + _characterAlgorithm = characterAlgorithm; + _reputationConfiguration = reputationConfiguration; + _rankingManager = rankingManager; + } + + public async Task HandleAsync(SpTransformEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + GameItemInstance specialist = e.Specialist; + GameItemInstance fairy = session.PlayerEntity.Fairy; + + if (session.PlayerEntity.IsCastingSkill) + { + return; + } + + if (specialist == null) + { + session.SendMsg(_languageService.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_NO_SPECIALIST_CARD, session.UserLanguage), MsgMessageType.Middle); + session.PlayerEntity.BroadcastEndDancingGuriPacket(); + return; + } + + if (specialist.GameItem.IsPartnerSpecialist) + { + session.PlayerEntity.BroadcastEndDancingGuriPacket(); + return; + } + + if (session.PlayerEntity.CharacterSkills.Any(s => !session.PlayerEntity.SkillCanBeUsed(s.Value))) + { + session.SendMsg(_languageService.GetLanguage(GameDialogKey.SKILL_SHOUTMESSAGE_SKILLS_IN_LOADING, session.UserLanguage), MsgMessageType.Middle); + session.PlayerEntity.BroadcastEndDancingGuriPacket(); + return; + } + + if ((byte)session.PlayerEntity.GetReputationIcon(_reputationConfiguration, _rankingManager.TopReputation) < specialist.GameItem.ReputationMinimum) + { + session.SendMsg(_languageService.GetLanguage(GameDialogKey.INFORMATION_CHATMESSAGE_NOT_ENOUGH_REPUT, session.UserLanguage), MsgMessageType.Middle); + session.PlayerEntity.BroadcastEndDancingGuriPacket(); + return; + } + + if (fairy != null && specialist.GameItem.Element != 0 && fairy.GameItem.Element != specialist.GameItem.Element && fairy.GameItem.Element != (byte)ElementType.All) + { + session.SendMsg(_languageService.GetLanguage(GameDialogKey.ITEM_SHOUTMESSAGE_FAIRY_WRONG_ELEMENT, session.UserLanguage), MsgMessageType.Middle); + session.PlayerEntity.BroadcastEndDancingGuriPacket(); + return; + } + + if (!session.PlayerEntity.IsSpCooldownElapsed()) + { + session.SendMsg(_languageService.GetLanguageFormat(GameDialogKey.SPECIALIST_SHOUTMESSAGE_IN_COOLDOWN, session.UserLanguage, session.PlayerEntity.GetSpCooldown()), MsgMessageType.Middle); + session.PlayerEntity.BroadcastEndDancingGuriPacket(); + return; + } + + await session.PlayerEntity.RemoveBuffsOnSpTransformAsync(); + + session.PlayerEntity.BCardComponent.AddEquipmentBCards(EquipmentType.Sp, specialist.GameItem.BCards); + session.RefreshSpPoint(); + session.PlayerEntity.LastTransform = DateTime.UtcNow; + session.PlayerEntity.LastSkillCombo = null; + session.PlayerEntity.UseSp = true; + session.PlayerEntity.Morph = specialist.GameItem.Morph; + session.PlayerEntity.MorphUpgrade = specialist.Upgrade; + session.PlayerEntity.MorphUpgrade2 = specialist.Design; + + session.BroadcastCMode(); + session.PlayerEntity.BroadcastEndDancingGuriPacket(); + session.RefreshLevel(_characterAlgorithm); + session.BroadcastEffect(EffectType.Transform, new RangeBroadcast(session.PlayerEntity.PositionX, session.PlayerEntity.PositionY)); + session.PlayerEntity.SpecialistComponent.RefreshSlStats(); + session.RefreshSpPoint(); + session.RefreshStatChar(); + session.RefreshStat(true); + session.SendCondPacket(); + session.SendIncreaseRange(); + session.PlayerEntity.ChargeComponent.ResetCharge(); + session.PlayerEntity.BCardComponent.ClearChargeBCard(); + + if (session.PlayerEntity.IsInRaidParty) + { + foreach (IClientSession s in session.PlayerEntity.Raid.Members) + { + s.RefreshRaidMemberList(); + } + } + + session.PlayerEntity.SkillsSp = new ConcurrentDictionary(); + session.PlayerEntity.Skills.Clear(); + foreach (SkillDTO skill in _skillsManager.GetSkills()) + { + if (!session.PlayerEntity.Specialist.IsSpSkill(skill)) + { + continue; + } + + var newSkill = new CharacterSkill + { + SkillVNum = skill.Id + }; + + session.PlayerEntity.SkillsSp[skill.Id] = newSkill; + session.PlayerEntity.Skills.Add(newSkill); + } + + session.RefreshSkillList(); + session.PlayerEntity.ClearSkillCooldowns(); + session.RefreshQuicklist(); + + SpWingInfo wingInfo = _spWingConfiguration.GetSpWingInfo(specialist.Design); + if (wingInfo == null) + { + return; + } + + IEnumerable buffs = wingInfo.Buffs.Select(buff => _buffFactory.CreateBuff(buff.BuffId, session.PlayerEntity, buff.IsPermanent ? BuffFlag.NO_DURATION : BuffFlag.NORMAL)); + await session.PlayerEntity.AddBuffsAsync(buffs); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/SpUntransformEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/SpUntransformEventHandler.cs new file mode 100644 index 0000000..b346019 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/SpUntransformEventHandler.cs @@ -0,0 +1,130 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.Quicklist; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage.Configuration; +using WingsEmu.Game.Algorithm; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Networking.Broadcasting; +using WingsEmu.Game.Raids; +using WingsEmu.Game.Skills; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Characters; + +public class SpUntransformEventHandler : IAsyncEventProcessor +{ + private readonly ICharacterAlgorithm _characterAlgorithm; + private readonly IGameLanguageService _languageService; + private readonly IMeditationManager _meditationManager; + private readonly ISpWingConfiguration _spWingConfiguration; + private readonly ISpyOutManager _spyOutManager; + private readonly ITeleportManager _teleportManager; + + public SpUntransformEventHandler(IGameLanguageService languageService, ISpWingConfiguration spWingConfiguration, + ICharacterAlgorithm characterAlgorithm, ISpyOutManager spyOutManager, IMeditationManager meditationManager, ITeleportManager teleportManager) + { + _languageService = languageService; + _spWingConfiguration = spWingConfiguration; + _characterAlgorithm = characterAlgorithm; + _meditationManager = meditationManager; + _teleportManager = teleportManager; + _spyOutManager = spyOutManager; + } + + public async Task HandleAsync(SpUntransformEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + + SpWingInfo wingInfo = _spWingConfiguration.GetSpWingInfo(session.PlayerEntity.MorphUpgrade2); + if (wingInfo != null) + { + foreach (WingBuff buff in wingInfo.Buffs) + { + Buff wingBuff = session.PlayerEntity.BuffComponent.GetBuff(buff.BuffId); + await session.PlayerEntity.RemoveBuffAsync(buff.IsPermanent, wingBuff); + } + } + + await session.PlayerEntity.RemoveBuffsOnSpTransformAsync(); + + session.PlayerEntity.BCardComponent.ClearEquipmentBCards(EquipmentType.Sp); + + session.PlayerEntity.RemoveAngelElement(); + session.PlayerEntity.ChangeScoutState(ScoutStateType.None); + session.PlayerEntity.CleanComboState(); + session.SendMsCPacket(1); + session.PlayerEntity.UseSp = false; + session.PlayerEntity.LastSkillCombo = null; + session.RefreshLevel(_characterAlgorithm); + + int cooldown = 30; + if (session.PlayerEntity.SkillsSp != null) + { + foreach ((int skillVnum, CharacterSkill skill) in session.PlayerEntity.SkillsSp) + { + if (session.PlayerEntity.SkillCanBeUsed(skill)) + { + continue; + } + + short time = skill.Skill.Cooldown; + double temp = (skill.LastUse - DateTime.UtcNow).TotalMilliseconds + time * 100; + temp /= 2000; + cooldown = temp > cooldown ? (int)temp : cooldown; + } + } + + if (_spyOutManager.ContainsSpyOut(session.PlayerEntity.Id)) + { + session.SendObArPacket(); + _spyOutManager.RemoveSpyOutSkill(session.PlayerEntity.Id); + } + + session.SendIncreaseRange(); + session.PlayerEntity.ChargeComponent.ResetCharge(); + session.PlayerEntity.BCardComponent.ClearChargeBCard(); + await session.EmitEventAsync(new GetDefaultMorphEvent()); + session.PlayerEntity.SpCooldownEnd = DateTime.UtcNow.AddSeconds(cooldown); + session.SendChatMessage(_languageService.GetLanguageFormat(GameDialogKey.INFORMATION_CHATMESSAGE_SP_STAY_TIME, session.UserLanguage, cooldown), ChatMessageColorType.Red); + session.SendSpCooldownUi(cooldown); + session.BroadcastCMode(); + session.BroadcastGuri(6, 0, rules: new RangeBroadcast(session.PlayerEntity.PositionX, session.PlayerEntity.PositionY)); + _meditationManager.RemoveAllMeditation(session.PlayerEntity); + _teleportManager.RemovePosition(session.PlayerEntity.Id); + session.PlayerEntity.SkillComponent.SendTeleportPacket = null; + + session.PlayerEntity.IsRemovingSpecialistPoints = false; + session.PlayerEntity.InitialScpPacketSent = false; + session.PlayerEntity.Session.SendScpPacket(0); + + if (session.PlayerEntity.IsInRaidParty) + { + foreach (IClientSession s in session.PlayerEntity.Raid.Members) + { + s.RefreshRaidMemberList(); + } + } + + session.RefreshSkillList(); + session.PlayerEntity.ClearSkillCooldowns(); + session.PlayerEntity.Skills.Clear(); + foreach (IBattleEntitySkill skill in session.PlayerEntity.GetSkills()) + { + session.PlayerEntity.Skills.Add(skill); + } + + session.RefreshQuicklist(); + session.RefreshStatChar(); + session.RefreshEquipment(); + session.RefreshStat(true); + session.SendCondPacket(); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/SpUpgradeEvent.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/SpUpgradeEvent.cs new file mode 100644 index 0000000..abcaff9 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/SpUpgradeEvent.cs @@ -0,0 +1,19 @@ +using WingsEmu.Game._packetHandling; +using WingsEmu.Game.Inventory; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Characters; + +public class SpUpgradeEvent : PlayerEvent +{ + public SpUpgradeEvent(UpgradeProtection upgradeProtection, InventoryItem inventoryItem, bool isFree = false) + { + UpgradeProtection = upgradeProtection; + InventoryItem = inventoryItem; + IsFree = isFree; + } + + public UpgradeProtection UpgradeProtection { get; } + public InventoryItem InventoryItem { get; } + public bool IsFree { get; } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/SpUpgradeEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/SpUpgradeEventHandler.cs new file mode 100644 index 0000000..4745df2 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/SpUpgradeEventHandler.cs @@ -0,0 +1,240 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.Families; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsAPI.Game.Extensions.ItemExtension.Item; +using WingsAPI.Packets.Enums; +using WingsEmu.DTOs.Items; +using WingsEmu.Game; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; +using WingsEmu.Packets.Enums.Families; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Characters; + +public class SpUpgradeEventHandler : IAsyncEventProcessor +{ + private readonly IItemsManager _itemsManager; + private readonly IGameLanguageService _languageService; + private readonly IRandomGenerator _randomGenerator; + private readonly SpUpgradeConfiguration _spConfiguration; + private readonly SpPerfectEventHandler _spPerfectHandler; + + public SpUpgradeEventHandler(IRandomGenerator randomGenerator, SpUpgradeConfiguration spConfiguration, SpPerfectEventHandler spPerfectHandler, IGameLanguageService languageService, + IItemsManager itemsManager) + { + _randomGenerator = randomGenerator; + _spConfiguration = spConfiguration; + _spPerfectHandler = spPerfectHandler; + _languageService = languageService; + _itemsManager = itemsManager; + } + + public async Task HandleAsync(SpUpgradeEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + + if (e.InventoryItem.ItemInstance.Type != ItemInstanceType.SpecialistInstance) + { + return; + } + + GameItemInstance sp = e.InventoryItem.ItemInstance; + + if (sp.GameItem.IsPartnerSpecialist) + { + return; + } + + if (sp.Rarity == -2) + { + return; + } + + UpgradeConfiguration configuration = _spConfiguration.FirstOrDefault(upgradeConfiguration => + upgradeConfiguration.SpUpgradeRange.Minimum <= sp.Upgrade + && sp.Upgrade < upgradeConfiguration.SpUpgradeRange.Maximum); + + if (configuration == null) + { + return; + } + + if (e.IsFree) + { + await SpUpgrade(configuration, session, e.InventoryItem, sp, configuration.SpecialItemsNeeded, true, true); + switch (sp.ItemVNum) + { + case (short)ItemVnums.CHICKEN_SP: + await session.RemoveItemFromInventory((short)ItemVnums.SCROLL_CHICKEN); + break; + case (short)ItemVnums.PYJAMA_SP: + await session.RemoveItemFromInventory((short)ItemVnums.SCROLL_PYJAMA); + break; + case (short)ItemVnums.PIRATE_SP: + await session.RemoveItemFromInventory((short)ItemVnums.SCROLL_PIRATE); + break; + } + + return; + } + + if (sp.SpLevel < configuration.SpLevelNeeded) + { + session.SendMsg(_languageService.GetLanguageFormat(GameDialogKey.INFORMATION_SHOUTMESSAGE_SP_LVL_LOW, session.UserLanguage, configuration.SpLevelNeeded.ToString()), MsgMessageType.Middle); + return; + } + + if (session.PlayerEntity.Gold < configuration.GoldNeeded) + { + return; + } + + if (!session.PlayerEntity.HasItem((int)ItemVnums.ANGEL_FEATHER, (short)configuration.FeatherNeeded)) + { + string itemName = _itemsManager.GetItem((int)ItemVnums.ANGEL_FEATHER).GetItemName(_languageService, session.UserLanguage); + session.SendMsg(_languageService.GetLanguageFormat(GameDialogKey.INVENTORY_SHOUTMESSAGE_NOT_ENOUGH_ITEMS, session.UserLanguage, configuration.FeatherNeeded, itemName), + MsgMessageType.Middle); + return; + } + + if (!session.PlayerEntity.HasItem((int)ItemVnums.FULL_MOON_CRYSTAL, (short)configuration.FullMoonsNeeded)) + { + string itemName = _itemsManager.GetItem((int)ItemVnums.FULL_MOON_CRYSTAL).GetItemName(_languageService, session.UserLanguage); + session.SendMsg(_languageService.GetLanguageFormat(GameDialogKey.INVENTORY_SHOUTMESSAGE_NOT_ENOUGH_ITEMS, session.UserLanguage, configuration.FullMoonsNeeded, itemName), + MsgMessageType.Middle); + return; + } + + var specialItems = new List(); + + foreach (SpecialItem specialItem in configuration.SpecialItemsNeeded) + { + if (specialItem.SpVnums.Count > 0 && !specialItem.SpVnums.Contains(sp.ItemVNum)) + { + continue; + } + + if (!session.PlayerEntity.HasItem(specialItem.ItemVnum, (short)specialItem.Amount)) + { + session.SendMsg(_languageService.GetLanguageFormat(GameDialogKey.INVENTORY_SHOUTMESSAGE_NOT_ENOUGH_ITEMS, session.UserLanguage, configuration.SpLevelNeeded.ToString()), + MsgMessageType.Middle); + return; + } + + specialItems.Add(specialItem); + } + + bool isProtected = e.UpgradeProtection == UpgradeProtection.Protected; + + if (isProtected && !session.PlayerEntity.HasItem(configuration.ScrollVnum)) + { + return; + } + + session.PlayerEntity.RemoveGold(configuration.GoldNeeded); + await session.RemoveItemFromInventory((int)ItemVnums.ANGEL_FEATHER, (short)configuration.FeatherNeeded); + await session.RemoveItemFromInventory((int)ItemVnums.FULL_MOON_CRYSTAL, (short)configuration.FullMoonsNeeded); + + if (isProtected) + { + await session.RemoveItemFromInventory(configuration.ScrollVnum); + } + + await SpUpgrade(configuration, session, e.InventoryItem, sp, specialItems, isProtected, false); + } + + private async Task SpUpgrade(UpgradeConfiguration configuration, IClientSession session, InventoryItem spItem, ItemInstanceDTO sp, List specialItems, bool isProtected, bool isFree) + { + byte originalUpgrade = sp.Upgrade; + + var randomBag = new RandomBag(_randomGenerator); + + randomBag.AddEntry(SpUpgradeResult.Succeed, configuration.SuccessChance); + randomBag.AddEntry(SpUpgradeResult.Fail, 100 - configuration.SuccessChance - configuration.DestroyChance); + if (configuration.DestroyChance > 0) + { + randomBag.AddEntry(SpUpgradeResult.Break, configuration.DestroyChance); + } + + SpUpgradeResult upgradeResult = randomBag.GetRandom(); + + switch (upgradeResult) + { + case SpUpgradeResult.Break when isProtected: + session.SendEffect(EffectType.UpgradeFail); + _spPerfectHandler.SendBothMessages(session, _languageService.GetLanguage(GameDialogKey.UPGRADE_MESSAGE_SP_FAILED_SAVED, session.UserLanguage), true); + break; + case SpUpgradeResult.Break: + sp.Rarity = -2; + session.SendInventoryAddPacket(spItem); + session.SendShopEndPacket(ShopEndType.Npc); + _spPerfectHandler.SendBothMessages(session, _languageService.GetLanguage(GameDialogKey.UPGRADE_MESSAGE_SP_DESTROYED, session.UserLanguage), true); + await RemoveSpecialItems(session, specialItems); + break; + case SpUpgradeResult.Succeed: + { + session.SendEffect(EffectType.UpgradeSuccess); + sp.Upgrade++; + session.SendInventoryAddPacket(spItem); + _spPerfectHandler.SendBothMessages(session, _languageService.GetLanguage(GameDialogKey.UPGRADE_MESSAGE_SP_SUCCESS, session.UserLanguage), false); + + if (sp.Upgrade > 7) + { + await session.FamilyAddLogAsync(FamilyLogType.ItemUpgraded, session.PlayerEntity.Name, sp.ItemVNum.ToString(), sp.Upgrade.ToString()); + } + + await RemoveSpecialItems(session, specialItems); + break; + } + + case SpUpgradeResult.Fail: + { + _spPerfectHandler.SendBothMessages(session, _languageService.GetLanguage(GameDialogKey.UPGRADE_MESSAGE_SP_FAILED, session.UserLanguage), true); + if (!isProtected) + { + await RemoveSpecialItems(session, specialItems); + } + + break; + } + } + + await session.EmitEventAsync(new SpUpgradedEvent + { + IsProtected = isProtected, + Sp = sp, + OriginalUpgrade = originalUpgrade, + UpgradeMode = isFree ? UpgradeMode.Free : UpgradeMode.Normal, + UpgradeResult = upgradeResult + }); + + if (isProtected) + { + session.SendShopEndPacket(ShopEndType.SpecialistHolder); + return; + } + + session.SendShopEndPacket(ShopEndType.Npc); + } + + private async Task RemoveSpecialItems(IClientSession session, List specialItems) + { + foreach (SpecialItem specialItem in specialItems) + { + await session.RemoveItemFromInventory(specialItem.ItemVnum, (short)specialItem.Amount); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/SpecialistRefreshEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/SpecialistRefreshEventHandler.cs new file mode 100644 index 0000000..432ed47 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/SpecialistRefreshEventHandler.cs @@ -0,0 +1,38 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.DAL.Redis.Locks; +using PhoenixLib.Events; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Characters; + +public class SpecialistRefreshEventHandler : IAsyncEventProcessor +{ + private readonly GameMinMaxConfiguration _gameMinMaxConfiguration; + private readonly IExpirableLockService _lockService; + + public SpecialistRefreshEventHandler(IExpirableLockService lockService, GameMinMaxConfiguration gameMinMaxConfiguration) + { + _lockService = lockService; + _gameMinMaxConfiguration = gameMinMaxConfiguration; + } + + public async Task HandleAsync(SpecialistRefreshEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + bool canRefresh = await _lockService.TryAddTemporaryLockAsync($"game:locks:specialist-points-refresh:{session.PlayerEntity.Id}", DateTime.UtcNow.Date.AddDays(1)); + + if (canRefresh == false && e.Force == false) + { + session.SendDebugMessage("Specialist Points already refreshed today."); + return; + } + + session.PlayerEntity.SpPointsBasic = _gameMinMaxConfiguration.MaxSpBasePoints; + session.RefreshSpPoint(); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/UpgradeItemEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/UpgradeItemEventHandler.cs new file mode 100644 index 0000000..2f642f0 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/UpgradeItemEventHandler.cs @@ -0,0 +1,264 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.Families; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsAPI.Game.Extensions.ItemExtension.Item; +using WingsAPI.Game.Extensions.PacketGeneration; +using WingsAPI.Packets.Enums; +using WingsEmu.DTOs.Items; +using WingsEmu.Game; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; +using WingsEmu.Packets.Enums.Families; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Characters; + +public class UpgradeItemEventHandler : IAsyncEventProcessor +{ + private readonly IGameLanguageService _gameLanguage; + private readonly IItemsManager _itemManager; + private readonly IRandomGenerator _randomGenerator; + private readonly UpgradeNormalItemConfiguration _upgradeNormalItemConfiguration; + private readonly UpgradePhenomenalItemConfiguration _upgradePhenomenalItemConfiguration; + + public UpgradeItemEventHandler(IGameLanguageService gameLanguage, IItemsManager itemManager, IRandomGenerator randomGenerator, UpgradeNormalItemConfiguration upgradeNormalItemConfiguration, + UpgradePhenomenalItemConfiguration upgradePhenomenalItemConfiguration) + { + _gameLanguage = gameLanguage; + _itemManager = itemManager; + _randomGenerator = randomGenerator; + _upgradeNormalItemConfiguration = upgradeNormalItemConfiguration; + _upgradePhenomenalItemConfiguration = upgradePhenomenalItemConfiguration; + } + + public async Task HandleAsync(UpgradeItemEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + GameItemInstance item = e.Inv.ItemInstance; + if (item.Type != ItemInstanceType.WearableInstance) + { + return; + } + + UpgradeItemConfiguration upgradeItemConfiguration; + + FixedUpMode hasAmulet = e.HasAmulet; + UpgradeMode mode = e.Mode; + UpgradeProtection protection = e.Protection; + + ItemVnums cella = ItemVnums.CELLA; + ItemVnums gem = item.Upgrade < 5 ? ItemVnums.SOUL_GEM : ItemVnums.COMPLETE_SOUL_GEM; + ItemVnums usedScroll = mode == UpgradeMode.Reduced ? ItemVnums.EQ_GOLD_SCROLL : ItemVnums.EQ_NORMAL_SCROLL; + double priceFactor = mode == UpgradeMode.Reduced ? 0.5 : 1.0; + + if (!session.HasCurrentMapInstance) + { + return; + } + + if (item.Upgrade >= 10) + { + session.PlayerEntity.BroadcastEndDancingGuriPacket(); + return; + } + + if (item.Rarity > 8) + { + session.PlayerEntity.BroadcastEndDancingGuriPacket(); + return; + } + + // The item has different configurations depending if it's r8 or not + if (item.Rarity == 8) + { + upgradeItemConfiguration = _upgradePhenomenalItemConfiguration; + } + else + { + upgradeItemConfiguration = _upgradeNormalItemConfiguration; + } + + UpgradeItemStats upgradeItemStats = upgradeItemConfiguration.FirstOrDefault(s => s.Upgrade == item.Upgrade); + if (upgradeItemStats == null) + { + session.PlayerEntity.BroadcastEndDancingGuriPacket(); + return; + } + + if (item.IsFixed) + { + if (session.PlayerEntity.Amulet != null && session.PlayerEntity.Amulet.GameItem.Id == (short)ItemVnums.AMULET_OF_REINFORCEMENT) + { + hasAmulet = FixedUpMode.HasAmulet; + } + else + { + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.GAMBLING_CHATMESSAGE_ITEM_IS_FIXED, session.UserLanguage), ChatMessageColorType.Yellow); + session.SendShopEndPacket(ShopEndType.Npc); + session.PlayerEntity.BroadcastEndDancingGuriPacket(); + return; + } + } + + long totalPrice = (long)(upgradeItemStats.Gold * priceFactor); + if (session.PlayerEntity.Gold < totalPrice) + { + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.INTERACTION_MESSAGE_NOT_ENOUGH_GOLD, session.UserLanguage), ChatMessageColorType.Yellow); + session.PlayerEntity.BroadcastEndDancingGuriPacket(); + return; + } + + if (session.PlayerEntity.CountItemWithVnum((short)cella) < upgradeItemStats.Cella * priceFactor) + { + session.SendChatMessage( + _gameLanguage.GetLanguageFormat(GameDialogKey.INVENTORY_SHOUTMESSAGE_NOT_ENOUGH_ITEMS, session.UserLanguage, upgradeItemStats.Cella * priceFactor, + _itemManager.GetItem((short)cella).GetItemName(_gameLanguage, session.UserLanguage)), + ChatMessageColorType.Yellow); + session.PlayerEntity.BroadcastEndDancingGuriPacket(); + return; + } + + if (protection == UpgradeProtection.Protected && session.PlayerEntity.CountItemWithVnum((short)usedScroll) < 1) + { + session.SendChatMessage( + _gameLanguage.GetLanguageFormat(GameDialogKey.INVENTORY_SHOUTMESSAGE_NOT_ENOUGH_ITEMS, session.UserLanguage, upgradeItemStats.Cella * priceFactor, + _itemManager.GetItem((short)usedScroll).Name), ChatMessageColorType.Yellow); + session.PlayerEntity.BroadcastEndDancingGuriPacket(); + return; + } + + if (session.PlayerEntity.CountItemWithVnum((short)gem) < upgradeItemStats.Gem) + { + session.SendChatMessage( + _gameLanguage.GetLanguageFormat(GameDialogKey.INVENTORY_SHOUTMESSAGE_NOT_ENOUGH_ITEMS, session.UserLanguage, upgradeItemStats.Gem, + _itemManager.GetItem((short)gem).GetItemName(_gameLanguage, session.UserLanguage)), ChatMessageColorType.Yellow); + session.PlayerEntity.BroadcastEndDancingGuriPacket(); + return; + } + + await session.RemoveItemFromInventory((short)gem, upgradeItemStats.Gem); + + if (protection == UpgradeProtection.Protected) + { + await session.RemoveItemFromInventory((short)usedScroll); + session.SendShopEndPacket(session.PlayerEntity.CountItemWithVnum((short)usedScroll) < 1 ? ShopEndType.SpecialistHolder : ShopEndType.Player); + } + + if (hasAmulet == FixedUpMode.HasAmulet && item.IsFixed) + { + GameItemInstance amulet = session.PlayerEntity.Amulet; + InventoryItem amuletInEq = session.PlayerEntity.GetInventoryItemFromEquipmentSlot(EquipmentType.Amulet); + amulet.DurabilityPoint -= 1; + session.SendAmuletBuffPacket(amulet); + if (amulet.DurabilityPoint <= 0) + { + await session.RemoveItemFromInventory(item: amuletInEq, isEquiped: true); + session.RefreshEquipment(); + session.SendModal(_gameLanguage.GetLanguage(GameDialogKey.GAMBLING_INFO_AMULET_DESTROYED, session.UserLanguage), ModalType.Confirm); + } + } + + session.PlayerEntity.Gold -= totalPrice; + await session.RemoveItemFromInventory((short)cella, (short)(upgradeItemStats.Cella * priceFactor)); + session.RefreshGold(); + + var randomBag = new RandomBag(_randomGenerator); + + randomBag.AddEntry(UpgradeResult.Succeed, 1000 - upgradeItemStats.UpFail - upgradeItemStats.UpFix); + randomBag.AddEntry(UpgradeResult.Fail, item.IsFixed ? upgradeItemStats.UpFail + upgradeItemStats.UpFix : upgradeItemStats.UpFail); + if (!item.IsFixed) + { + randomBag.AddEntry(UpgradeResult.Fixed, upgradeItemStats.UpFix); + } + + UpgradeResult upgradeResult = randomBag.GetRandom(); + + switch (upgradeResult) + { + case UpgradeResult.Fixed: + session.BroadcastEffectInRange(EffectType.UpgradeFail); + item.IsFixed = true; + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.UPGRADE_MESSAGE_FIXED, session.UserLanguage), ChatMessageColorType.Red); + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.UPGRADE_MESSAGE_FIXED, session.UserLanguage), MsgMessageType.Middle); + + await session.EmitEventAsync(new ItemUpgradedEvent + { + Item = item, + Mode = e.Mode, + Protection = e.Protection, + HasAmulet = e.HasAmulet == FixedUpMode.HasAmulet, + OriginalUpgrade = item.Upgrade, + Result = UpgradeResult.Fixed, + TotalPrice = totalPrice + }); + break; + case UpgradeResult.Fail: + if (protection == UpgradeProtection.None) + { + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.UPGRADE_MESSAGE_FAILED, session.UserLanguage), ChatMessageColorType.Red); + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.UPGRADE_MESSAGE_FAILED, session.UserLanguage), MsgMessageType.Middle); + await session.RemoveItemFromInventory(item: e.Inv); + } + else + { + session.BroadcastEffectInRange(EffectType.UpgradeFail); + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.INFORMATION_CHATMESSAGE_SCROLL_PROTECT_USED, session.UserLanguage), ChatMessageColorType.Red); + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.UPGRADE_MESSAGE_FAILED_ITEM_SAVED, session.UserLanguage), MsgMessageType.Middle); + } + + await session.EmitEventAsync(new ItemUpgradedEvent + { + Item = item, + Mode = e.Mode, + Protection = e.Protection, + HasAmulet = e.HasAmulet == FixedUpMode.HasAmulet, + OriginalUpgrade = item.Upgrade, + Result = UpgradeResult.Fail, + TotalPrice = totalPrice + }); + break; + default: + { + session.BroadcastEffectInRange(EffectType.UpgradeSuccess); + + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.UPGRADE_MESSAGE_SUCCESS, session.UserLanguage), ChatMessageColorType.Green); + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.UPGRADE_MESSAGE_SUCCESS, session.UserLanguage), MsgMessageType.Middle); + item.Upgrade++; + if (item.Upgrade > 4) + { + await session.FamilyAddLogAsync(FamilyLogType.ItemUpgraded, session.PlayerEntity.Name, item.ItemVNum.ToString(), item.Upgrade.ToString()); + } + + InventoryItem itemInstance = session.PlayerEntity.GetItemBySlotAndType(e.Inv.Slot, InventoryType.Equipment); + + session.SendInventoryAddPacket(itemInstance); + + await session.EmitEventAsync(new ItemUpgradedEvent + { + Item = item, + Mode = e.Mode, + Protection = e.Protection, + HasAmulet = e.HasAmulet == FixedUpMode.HasAmulet, + OriginalUpgrade = (short)(item.Upgrade - 1), + Result = UpgradeResult.Succeed, + TotalPrice = totalPrice + }); + break; + } + } + + session.PlayerEntity.BroadcastEndDancingGuriPacket(); + session.SendShopEndPacket(ShopEndType.Npc); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/VehicleCheckMapSpeedEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/VehicleCheckMapSpeedEventHandler.cs new file mode 100644 index 0000000..f85645a --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/VehicleCheckMapSpeedEventHandler.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Networking; +using WingsEmu.Plugins.BasicImplementations.Vehicles; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Characters; + +public class VehicleCheckMapSpeedEventHandler : IAsyncEventProcessor +{ + private readonly IMapManager _mapManager; + private readonly IVehicleConfigurationProvider _vehicleConfiguration; + + public VehicleCheckMapSpeedEventHandler(IVehicleConfigurationProvider vehicleConfiguration, IMapManager mapManager) + { + _vehicleConfiguration = vehicleConfiguration; + _mapManager = mapManager; + } + + public async Task HandleAsync(VehicleCheckMapSpeedEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + + if (!session.PlayerEntity.IsOnVehicle) + { + return; + } + + VehicleConfiguration vehicle = _vehicleConfiguration.GetByMorph(session.PlayerEntity.Morph, session.PlayerEntity.Gender); + if (vehicle?.VehicleMapSpeeds == null) + { + RefreshSpeed(session, 0); + return; + } + + IReadOnlyList flags = _mapManager.GetMapFlagByMapId(session.CurrentMapInstance.MapId); + VehicleMapSpeed vehicleFlags = vehicle.VehicleMapSpeeds.FirstOrDefault(x => flags.Contains(x.MapFlag)); + + if (vehicleFlags == null) + { + RefreshSpeed(session, 0); + return; + } + + RefreshSpeed(session, (byte)vehicleFlags.SpeedBonus); + } + + private void RefreshSpeed(IClientSession session, byte vehicleSpeed) + { + session.PlayerEntity.VehicleMapSpeed = vehicleSpeed; + session.PlayerEntity.RefreshCharacterStats(); + session.SendCondPacket(); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/VehicleRemoveEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/VehicleRemoveEventHandler.cs new file mode 100644 index 0000000..f6c8cbc --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Characters/VehicleRemoveEventHandler.cs @@ -0,0 +1,56 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.Groups; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage.Configuration; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Characters; + +public class VehicleRemoveEventHandler : IAsyncEventProcessor +{ + private readonly IGameLanguageService _gameLanguage; + private readonly ISpPartnerConfiguration _spPartner; + + public VehicleRemoveEventHandler(IGameLanguageService gameLanguage, ISpPartnerConfiguration spPartner) + { + _gameLanguage = gameLanguage; + _spPartner = spPartner; + } + + public async Task HandleAsync(RemoveVehicleEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + + if (!session.PlayerEntity.IsOnVehicle) + { + return; + } + + if (e.ShowMates && !session.PlayerEntity.IsInvisible()) + { + session.BroadcastInTeamMembers(_gameLanguage, _spPartner); + } + + session.PlayerEntity.RandomMapTeleport = null; + session.RefreshParty(_spPartner); + + Buff speedBooster = session.PlayerEntity.BuffComponent.GetBuff((short)BuffVnums.SPEED_BOOSTER); + await session.PlayerEntity.RemoveBuffAsync(false, speedBooster); + + session.PlayerEntity.IsOnVehicle = false; + await session.EmitEventAsync(new GetDefaultMorphEvent()); + session.RefreshStatChar(); + session.BroadcastEq(); + session.RefreshStat(); + session.SendCondPacket(); + session.PlayerEntity.LastSpeedChange = DateTime.UtcNow; + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Exchange/ExchangeCloseEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Exchange/ExchangeCloseEventHandler.cs new file mode 100644 index 0000000..59eb9cf --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Exchange/ExchangeCloseEventHandler.cs @@ -0,0 +1,40 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Exchange; +using WingsEmu.Game.Exchange.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Exchange; + +public class ExchangeCloseEventHandler : IAsyncEventProcessor +{ + private readonly ISessionManager _sessionManager; + + public ExchangeCloseEventHandler(ISessionManager sessionManager) => _sessionManager = sessionManager; + + public async Task HandleAsync(ExchangeCloseEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + + if (!session.PlayerEntity.IsInExchange()) + { + return; + } + + PlayerExchange exchange = session.PlayerEntity.GetExchange(); + session.PlayerEntity.RemoveExchange(); + session.SendExcClosePacket(e.Type); + + IClientSession target = _sessionManager.GetSessionByCharacterId(exchange.TargetId); + if (target == null) + { + return; + } + + target.PlayerEntity.RemoveExchange(); + target.SendExcClosePacket(e.Type); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Exchange/ExchangeJoinEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Exchange/ExchangeJoinEventHandler.cs new file mode 100644 index 0000000..5e24711 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Exchange/ExchangeJoinEventHandler.cs @@ -0,0 +1,54 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Exchange; +using WingsEmu.Game.Exchange.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Exchange; + +public class ExchangeJoinEventHandler : IAsyncEventProcessor +{ + private readonly IBankReputationConfiguration _bankReputationConfiguration; + private readonly IRankingManager _rankingManager; + private readonly IReputationConfiguration _reputationConfiguration; + + public ExchangeJoinEventHandler(IReputationConfiguration reputationConfiguration, IBankReputationConfiguration bankReputationConfiguration, IRankingManager rankingManager) + { + _reputationConfiguration = reputationConfiguration; + _bankReputationConfiguration = bankReputationConfiguration; + _rankingManager = rankingManager; + } + + public async Task HandleAsync(ExchangeJoinEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + IClientSession target = e.Target; + + if (session.PlayerEntity.IsInExchange()) + { + return; + } + + if (target.PlayerEntity.IsInExchange()) + { + return; + } + + var senderExchange = new PlayerExchange(session.PlayerEntity.Id, target.PlayerEntity.Id); + var targetExchange = new PlayerExchange(target.PlayerEntity.Id, session.PlayerEntity.Id); + + session.PlayerEntity.SetExchange(senderExchange); + target.PlayerEntity.SetExchange(targetExchange); + + session.SendEmptyExchangeWindow(target.PlayerEntity.Id); + target.SendEmptyExchangeWindow(session.PlayerEntity.Id); + + session.SendGbexPacket(_reputationConfiguration, _bankReputationConfiguration, _rankingManager.TopReputation); + target.SendGbexPacket(_reputationConfiguration, _bankReputationConfiguration, _rankingManager.TopReputation); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Exchange/ExchangeRegisterEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Exchange/ExchangeRegisterEventHandler.cs new file mode 100644 index 0000000..2d226ab --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Exchange/ExchangeRegisterEventHandler.cs @@ -0,0 +1,50 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.Game.Exchange; +using WingsEmu.Game.Exchange.Event; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Exchange; + +public class ExchangeRegisterEventHandler : IAsyncEventProcessor +{ + private readonly ISessionManager _sessionManager; + + public ExchangeRegisterEventHandler(ISessionManager sessionManager) => _sessionManager = sessionManager; + + public async Task HandleAsync(ExchangeRegisterEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + + if (!session.PlayerEntity.IsInExchange()) + { + return; + } + + PlayerExchange exchange = session.PlayerEntity.GetExchange(); + if (exchange == null) + { + return; + } + + if (exchange.RegisteredItems) + { + return; + } + + IClientSession target = _sessionManager.GetSessionByCharacterId(exchange.TargetId); + if (target == null) + { + return; + } + + exchange.Items = e.InventoryItems; + exchange.Gold = e.Gold; + exchange.BankGold = e.BankGold; + exchange.RegisteredItems = true; + target.SendExchangeWindow(session.PlayerEntity.Id, e.Gold, e.BankGold, e.Packets); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Exchange/ExchangeTransferItemsEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Exchange/ExchangeTransferItemsEventHandler.cs new file mode 100644 index 0000000..e548cb3 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Exchange/ExchangeTransferItemsEventHandler.cs @@ -0,0 +1,275 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.DTOs.Items; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Exchange.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Exchange; + +public class ExchangeTransferItemsEventHandler : IAsyncEventProcessor +{ + private readonly IBankReputationConfiguration _bankReputationConfiguration; + private readonly IGameItemInstanceFactory _gameItemInstance; + private readonly IGameLanguageService _gameLanguage; + private readonly IRankingManager _rankingManager; + private readonly IReputationConfiguration _reputationConfiguration; + private readonly IServerManager _serverManager; + + public ExchangeTransferItemsEventHandler(IServerManager serverManager, IGameLanguageService gameLanguage, IGameItemInstanceFactory gameItemInstance, + IReputationConfiguration reputationConfiguration, IBankReputationConfiguration bankReputationConfiguration, IRankingManager rankingManager) + { + _serverManager = serverManager; + _gameLanguage = gameLanguage; + _gameItemInstance = gameItemInstance; + _reputationConfiguration = reputationConfiguration; + _bankReputationConfiguration = bankReputationConfiguration; + _rankingManager = rankingManager; + } + + public async Task HandleAsync(ExchangeTransferItemsEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + IClientSession target = e.Target; + bool transformGold = true; + + long maxGold = _serverManager.MaxGold; + long maxBankGold = _serverManager.MaxBankGold; + + int senderGold = e.SenderGold; + long senderBankGold = e.SenderBankGold; + + int targetGold = e.TargetGold; + long targetBankGold = e.TargetBankGold; + + if (senderGold + target.PlayerEntity.Gold > maxGold) + { + transformGold = false; + } + + if (targetGold + session.PlayerEntity.Gold > maxGold) + { + transformGold = false; + } + + if (senderBankGold + target.Account.BankMoney > maxBankGold) + { + transformGold = false; + } + + if (targetBankGold + session.Account.BankMoney > maxBankGold) + { + transformGold = false; + } + + if (senderBankGold != 0) + { + if (!session.HasEnoughGold(session.PlayerEntity.GetBankPenalty(_reputationConfiguration, _bankReputationConfiguration, _rankingManager.TopReputation))) + { + await session.CloseExchange(); + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.INTERACTION_MESSAGE_NOT_ENOUGH_GOLD, session.UserLanguage), MsgMessageType.Middle); + return; + } + } + + if (targetBankGold != 0) + { + if (!target.HasEnoughGold(target.PlayerEntity.GetBankPenalty(_reputationConfiguration, _bankReputationConfiguration, _rankingManager.TopReputation))) + { + await target.CloseExchange(); + target.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.INTERACTION_MESSAGE_NOT_ENOUGH_GOLD, target.UserLanguage), MsgMessageType.Middle); + return; + } + } + + if (!transformGold) + { + await session.CloseExchange(); + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.INFORMATION_MESSAGE_MAX_GOLD, session.UserLanguage), MsgMessageType.Middle); + target.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.INFORMATION_MESSAGE_MAX_GOLD, target.UserLanguage), MsgMessageType.Middle); + return; + } + + List<(InventoryItem, short)> senderItems = e.SenderItems; + List<(InventoryItem, short)> targetItems = e.TargetItems; + + bool targetCanReceiveSenderItems = CanReceive(target.PlayerEntity, senderItems); + + if (!targetCanReceiveSenderItems) + { + await session.CloseExchange(); + target.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.INTERACTION_MESSAGE_NOT_ENOUGH_PLACE, target.UserLanguage), MsgMessageType.Middle); + return; + } + + bool senderCanReceiveTargetItems = CanReceive(session.PlayerEntity, targetItems); + + if (!senderCanReceiveTargetItems) + { + await session.CloseExchange(); + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.INTERACTION_MESSAGE_NOT_ENOUGH_PLACE, target.UserLanguage), MsgMessageType.Middle); + return; + } + + if (senderBankGold != 0) + { + session.PlayerEntity.Gold -= session.PlayerEntity.GetBankPenalty(_reputationConfiguration, _bankReputationConfiguration, _rankingManager.TopReputation); + } + + if (targetBankGold != 0) + { + target.PlayerEntity.Gold -= target.PlayerEntity.GetBankPenalty(_reputationConfiguration, _bankReputationConfiguration, _rankingManager.TopReputation); + } + + session.PlayerEntity.Gold -= senderGold; + target.PlayerEntity.Gold += senderGold; + + target.PlayerEntity.Gold -= targetGold; + session.PlayerEntity.Gold += targetGold; + + session.Account.BankMoney -= senderBankGold; + target.Account.BankMoney += senderBankGold; + + target.Account.BankMoney -= targetBankGold; + session.Account.BankMoney += targetBankGold; + + session.RefreshGold(); + target.RefreshGold(); + + foreach ((InventoryItem inventoryItem, short amount) in senderItems) + { + bool asGift = false; + if (inventoryItem.ItemInstance.Type != ItemInstanceType.NORMAL_ITEM) + { + GameItemInstance deepCopy = _gameItemInstance.DuplicateItem(inventoryItem.ItemInstance); + if (deepCopy.Amount != 1) + { + deepCopy.Amount = 1; + } + + if (!target.PlayerEntity.HasSpaceFor(deepCopy.ItemVNum)) + { + asGift = true; + } + + await target.AddNewItemToInventory(deepCopy, sendGiftIsFull: asGift); + } + else + { + GameItemInstance newItem = _gameItemInstance.CreateItem(inventoryItem.ItemInstance.ItemVNum, amount); + + if (!target.PlayerEntity.HasSpaceFor(newItem.ItemVNum, amount)) + { + asGift = true; + } + + await target.AddNewItemToInventory(newItem, sendGiftIsFull: asGift); + } + + await session.RemoveItemFromInventory(item: inventoryItem, amount: amount); + } + + foreach ((InventoryItem inventoryItem, short amount) in targetItems) + { + bool asGift = false; + if (inventoryItem.ItemInstance.Type != ItemInstanceType.NORMAL_ITEM) + { + GameItemInstance deepCopy = _gameItemInstance.DuplicateItem(inventoryItem.ItemInstance); + if (deepCopy.Amount != 1) + { + deepCopy.Amount = 1; + } + + if (!session.PlayerEntity.HasSpaceFor(deepCopy.ItemVNum)) + { + asGift = true; + } + + await session.AddNewItemToInventory(deepCopy, sendGiftIsFull: asGift); + } + else + { + GameItemInstance newItem = _gameItemInstance.CreateItem(inventoryItem.ItemInstance.ItemVNum, amount); + + if (!session.PlayerEntity.HasSpaceFor(newItem.ItemVNum, amount)) + { + asGift = true; + } + + await session.AddNewItemToInventory(newItem, sendGiftIsFull: asGift); + } + + await target.RemoveItemFromInventory(item: inventoryItem, amount: amount); + } + + await session.EmitEventAsync(new ExchangeCompletedEvent + { + Target = target, + SenderGold = senderGold, + SenderBankGold = senderBankGold, + SenderItems = senderItems.Select(s => (_gameItemInstance.CreateDto(s.Item1.ItemInstance), s.Item2)).ToList(), + TargetItems = targetItems.Select(s => (_gameItemInstance.CreateDto(s.Item1.ItemInstance), s.Item2)).ToList(), + TargetGold = targetGold, + TargetBankGold = targetBankGold + }); + + await session.CloseExchange(ExcCloseType.Successful); + } + + private bool CanReceive(IPlayerEntity playerEntity, List<(InventoryItem, short)> items) + { + var dictionary = new Dictionary(); + int counter = 0; + + if (!items.Any()) + { + return true; + } + + foreach ((InventoryItem item, short amount) in items) + { + InventoryType type = item.ItemInstance.GameItem.Type; + short slots = playerEntity.GetInventorySlots(false, type); + + for (short i = 0; i < slots; i++) + { + if (type != InventoryType.Etc && type != InventoryType.Main && type != InventoryType.Equipment) + { + return false; + } + + if (dictionary.TryGetValue(type, out short slot)) + { + if (i == slot) + { + continue; + } + } + + InventoryItem freeSlot = playerEntity.GetItemBySlotAndType(i, type); + if (freeSlot?.ItemInstance != null) + { + continue; + } + + counter++; + dictionary[item.InventoryType] = i; + break; + } + } + + return counter != 0 && counter == items.Count; + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Groups/GroupActionEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Groups/GroupActionEventHandler.cs new file mode 100644 index 0000000..d2f7ab8 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Groups/GroupActionEventHandler.cs @@ -0,0 +1,387 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Groups; +using WingsEmu.Game.Groups.Events; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Relations; +using WingsEmu.Game.RespawnReturn.Event; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Groups; + +public class GroupActionEventHandler : IAsyncEventProcessor +{ + private readonly IGroupFactory _groupFactory; + private readonly IInvitationManager _invitation; + private readonly IGameLanguageService _language; + private readonly ISessionManager _sessionManager; + + public GroupActionEventHandler(IGameLanguageService language, ISessionManager sessionManager, IInvitationManager invitation, IGroupFactory groupFactory) + { + _sessionManager = sessionManager; + _invitation = invitation; + _groupFactory = groupFactory; + _language = language; + } + + public async Task HandleAsync(GroupActionEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + + if (session.PlayerEntity.RainbowBattleComponent.IsInRainbowBattle) + { + return; + } + + IClientSession sender; + switch (e.RequestType) + { + case GroupRequestType.Requested: + case GroupRequestType.Invited: + if (session.PlayerEntity.IsInGroup() && session.PlayerEntity.IsGroupFull()) + { + session.SendInfo(_language.GetLanguage(GameDialogKey.GROUP_INFO_FULL, session.UserLanguage)); + return; + } + + if (session.PlayerEntity.Id == e.CharacterId) + { + return; + } + + if (session.PlayerEntity.IsInRaidParty) + { + return; + } + + if (session.PlayerEntity.HasRaidStarted) + { + return; + } + + IClientSession target = _sessionManager.GetSessionByCharacterId(e.CharacterId); + if (target == null) + { + return; + } + + if (session.PlayerEntity.IsBlocking(e.CharacterId)) + { + session.SendInfo(_language.GetLanguage(GameDialogKey.BLACKLIST_INFO_BLOCKING, session.UserLanguage)); + return; + } + + if (target.PlayerEntity.IsBlocking(session.PlayerEntity.Id)) + { + session.SendInfo(_language.GetLanguage(GameDialogKey.BLACKLIST_INFO_BLOCKED, session.UserLanguage)); + return; + } + + if (session.CurrentMapInstance.HasMapFlag(MapFlags.ACT_4)) + { + if (session.PlayerEntity.IsSeal) + { + return; + } + + if (target.PlayerEntity.Faction != session.PlayerEntity.Faction) + { + return; + } + } + + if (target.PlayerEntity.GroupRequestBlocked) + { + session.SendInfo(_language.GetLanguage(GameDialogKey.GROUP_INFO_BLOCKED, session.UserLanguage)); + return; + } + + if (session.PlayerEntity.IsInGroup() && target.PlayerEntity.IsInGroup()) + { + session.SendInfo(_language.GetLanguage(GameDialogKey.GROUP_INFO_ALREADY_IN_GROUP, session.UserLanguage)); + return; + } + + if (target.PlayerEntity.IsInRaidParty) + { + return; + } + + if (target.PlayerEntity.HasRaidStarted) + { + return; + } + + if (_invitation.ContainsPendingInvitation(session.PlayerEntity.Id, target.PlayerEntity.Id, InvitationType.Group)) + { + _invitation.RemovePendingInvitation(session.PlayerEntity.Id, target.PlayerEntity.Id, InvitationType.Group); + } + + await session.EmitEventAsync(new InvitationEvent(target.PlayerEntity.Id, InvitationType.Group)); + session.SendInfo(_language.GetLanguageFormat(GameDialogKey.GROUP_INFO_REQUEST, session.UserLanguage, target.PlayerEntity.Name)); + break; + case GroupRequestType.Accepted: + if (session.PlayerEntity.Id == e.CharacterId) + { + return; + } + + sender = _sessionManager.GetSessionByCharacterId(e.CharacterId); + if (sender == null) + { + return; + } + + if (!_invitation.ContainsPendingInvitation(sender.PlayerEntity.Id, session.PlayerEntity.Id, InvitationType.Group)) + { + return; + } + + _invitation.RemovePendingInvitation(sender.PlayerEntity.Id, session.PlayerEntity.Id, InvitationType.Group); + + if (session.PlayerEntity.IsBlocking(e.CharacterId)) + { + session.SendInfo(_language.GetLanguage(GameDialogKey.BLACKLIST_INFO_BLOCKING, session.UserLanguage)); + return; + } + + if (sender.PlayerEntity.IsBlocking(session.PlayerEntity.Id)) + { + session.SendInfo(_language.GetLanguage(GameDialogKey.BLACKLIST_INFO_BLOCKED, session.UserLanguage)); + return; + } + + if (session.PlayerEntity.IsInGroup() && sender.PlayerEntity.IsInGroup()) + { + session.SendInfo(_language.GetLanguage(GameDialogKey.GROUP_INFO_ALREADY_IN_GROUP, session.UserLanguage)); + return; + } + + if (sender.PlayerEntity.IsInRaidParty) + { + return; + } + + if (sender.PlayerEntity.HasRaidStarted) + { + return; + } + + if (session.PlayerEntity.IsInRaidParty) + { + return; + } + + if (session.PlayerEntity.HasRaidStarted) + { + return; + } + + if (session.PlayerEntity.IsInGroup()) + { + if (session.PlayerEntity.IsGroupFull()) + { + session.SendInfo(_language.GetLanguage(GameDialogKey.GROUP_INFO_FULL, session.UserLanguage)); + return; + } + + PlayerGroup grp = session.PlayerEntity.GetGroup(); + await sender.EmitEventAsync(new JoinToGroupEvent(grp)); + await session.EmitEventAsync(new GroupAddMemberEvent(sender.PlayerEntity)); + sender.SendInfo(session.GetLanguageFormat(GameDialogKey.GROUP_INFO_JOIN, session.PlayerEntity.Name)); + + foreach (IPlayerEntity player in grp.Members) + { + if (player.MapInstance is not { MapInstanceType: MapInstanceType.ArenaInstance }) + { + continue; + } + + player.Session.SendArenaStatistics(false, grp); + } + + return; + } + + if (!sender.PlayerEntity.IsInGroup()) + { + var members = new List { sender.PlayerEntity, session.PlayerEntity }; + PlayerGroup newPlayerGroup = _groupFactory.CreateGroup(3, members, sender.PlayerEntity.Id); + await sender.EmitEventAsync(new JoinToGroupEvent(newPlayerGroup)); + await session.EmitEventAsync(new JoinToGroupEvent(newPlayerGroup)); + + sender.SendInfo(_language.GetLanguage(GameDialogKey.GROUP_INFO_ADMIN, sender.UserLanguage)); + session.SendInfo(session.GetLanguageFormat(GameDialogKey.GROUP_INFO_JOIN, sender.PlayerEntity.Name)); + + foreach (IPlayerEntity player in members) + { + if (player.MapInstance is not { MapInstanceType: MapInstanceType.ArenaInstance }) + { + continue; + } + + player.Session.SendArenaStatistics(false, newPlayerGroup); + } + + return; + } + + if (sender.PlayerEntity.IsGroupFull()) + { + session.SendInfo(session.GetLanguage(GameDialogKey.GROUP_INFO_FULL)); + return; + } + + PlayerGroup getPlayerGroup = sender.PlayerEntity.GetGroup(); + await session.EmitEventAsync(new JoinToGroupEvent(getPlayerGroup)); + await sender.EmitEventAsync(new GroupAddMemberEvent(session.PlayerEntity)); + session.SendInfo(session.GetLanguageFormat(GameDialogKey.GROUP_INFO_JOIN, sender.PlayerEntity.Name)); + + foreach (IPlayerEntity player in getPlayerGroup.Members) + { + if (player.MapInstance is not { MapInstanceType: MapInstanceType.ArenaInstance }) + { + continue; + } + + player.Session.SendArenaStatistics(false, getPlayerGroup); + } + + break; + case GroupRequestType.Declined: + if (session.PlayerEntity.IsInGroup()) + { + return; + } + + if (session.PlayerEntity.Id == e.CharacterId) + { + return; + } + + sender = _sessionManager.GetSessionByCharacterId(e.CharacterId); + if (sender == null) + { + return; + } + + if (!_invitation.ContainsPendingInvitation(sender.PlayerEntity.Id, session.PlayerEntity.Id, InvitationType.Group)) + { + return; + } + + sender.SendChatMessage(string.Format( + _language.GetLanguage(GameDialogKey.GROUP_CHATMESSAGE_REQUEST_REFUSED, sender.UserLanguage), session.PlayerEntity.Name), ChatMessageColorType.Yellow); + + _invitation.RemovePendingInvitation(sender.PlayerEntity.Id, session.PlayerEntity.Id, InvitationType.Group); + break; + case GroupRequestType.Sharing: + + if (!session.PlayerEntity.IsInGroup()) + { + return; + } + + if (session.PlayerEntity.HomeComponent.Return == null || session.PlayerEntity.HomeComponent.Return.MapId == 0) + { + return; + } + + foreach (IPlayerEntity member in session.PlayerEntity.GetGroup().Members) + { + if (member.Id == session.PlayerEntity.Id) + { + continue; + } + + await session.EmitEventAsync(new InvitationEvent(member.Id, InvitationType.GroupPointShare)); + } + + session.SendInfo(_language.GetLanguage(GameDialogKey.GROUP_INFO_SHARE_POINT_TO_MEMBERS, session.UserLanguage)); + break; + case GroupRequestType.AcceptedShare: + + if (!session.PlayerEntity.IsInGroup()) + { + return; + } + + sender = _sessionManager.GetSessionByCharacterId(e.CharacterId); + if (sender == null) + { + return; + } + + if (!sender.PlayerEntity.IsInGroup()) + { + return; + } + + if (session.PlayerEntity.GetGroupId() != sender.PlayerEntity.GetGroupId()) + { + return; + } + + if (!_invitation.ContainsPendingInvitation(sender.PlayerEntity.Id, session.PlayerEntity.Id, InvitationType.GroupPointShare)) + { + return; + } + + await session.EmitEventAsync(new ReturnChangeEvent + { + MapId = sender.PlayerEntity.HomeComponent.Return.MapId, + MapX = sender.PlayerEntity.HomeComponent.Return.MapX, + MapY = sender.PlayerEntity.HomeComponent.Return.MapY, + IsByGroup = true + }); + + _invitation.RemovePendingInvitation(sender.PlayerEntity.Id, session.PlayerEntity.Id, InvitationType.GroupPointShare); + sender.SendMsg(_language.GetLanguageFormat(GameDialogKey.GROUP_SHOUTMESSAGE_SHARE_POINT_ACCEPTED, sender.UserLanguage, session.PlayerEntity.Name), MsgMessageType.Middle); + session.SendMsg(_language.GetLanguage(GameDialogKey.GROUP_SHOUTMESSAGE_SHARE_POINT_YOU_ACCEPTED, session.UserLanguage), MsgMessageType.Middle); + break; + case GroupRequestType.DeclinedShare: + + if (!session.PlayerEntity.IsInGroup()) + { + return; + } + + sender = _sessionManager.GetSessionByCharacterId(e.CharacterId); + if (sender == null) + { + return; + } + + if (!sender.PlayerEntity.IsInGroup()) + { + return; + } + + if (session.PlayerEntity.GetGroupId() != sender.PlayerEntity.GetGroupId()) + { + return; + } + + if (!_invitation.ContainsPendingInvitation(sender.PlayerEntity.Id, session.PlayerEntity.Id, InvitationType.GroupPointShare)) + { + return; + } + + _invitation.RemovePendingInvitation(sender.PlayerEntity.Id, session.PlayerEntity.Id, InvitationType.GroupPointShare); + + sender.SendMsg(_language.GetLanguageFormat(GameDialogKey.GROUP_SHOUTMESSAGE_SHARE_POINT_DECLINED, sender.UserLanguage, session.PlayerEntity.Name), MsgMessageType.Middle); + session.SendMsg(_language.GetLanguage(GameDialogKey.GROUP_SHOUTMESSAGE_SHARE_POINT_YOU_DECLINED, session.UserLanguage), MsgMessageType.Middle); + break; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Groups/GroupAddMemberEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Groups/GroupAddMemberEventHandler.cs new file mode 100644 index 0000000..6f2b6be --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Groups/GroupAddMemberEventHandler.cs @@ -0,0 +1,27 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Groups; +using WingsEmu.Game.Groups.Events; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Groups; + +public class AddMemberToGroupEventHandler : IAsyncEventProcessor +{ + private readonly IGroupManager _group; + + public AddMemberToGroupEventHandler(IGroupManager group) => _group = group; + + public async Task HandleAsync(GroupAddMemberEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + if (!session.PlayerEntity.IsInGroup()) + { + return; + } + + PlayerGroup playerGroup = session.PlayerEntity.GetGroup(); + _group.AddMemberGroup(playerGroup, e.NewMember); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Groups/GroupJoinEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Groups/GroupJoinEventHandler.cs new file mode 100644 index 0000000..a372191 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Groups/GroupJoinEventHandler.cs @@ -0,0 +1,22 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Groups; +using WingsEmu.Game.Groups.Events; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Groups; + +public class JoinToGroupEventHandler : IAsyncEventProcessor +{ + private readonly IGroupManager _group; + public JoinToGroupEventHandler(IGroupManager group) => _group = group; + + public async Task HandleAsync(JoinToGroupEvent e, CancellationToken cancellation) + { + IPlayerEntity playerEntity = e.Sender.PlayerEntity; + _group.JoinGroup(e.PlayerGroup, playerEntity); + playerEntity.CheckWeedingBuff = DateTime.UtcNow; + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Groups/GroupLeaveEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Groups/GroupLeaveEventHandler.cs new file mode 100644 index 0000000..6d9f5b7 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Groups/GroupLeaveEventHandler.cs @@ -0,0 +1,120 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Groups; +using WingsEmu.Game.Groups.Events; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Groups; + +public class LeaveGroupEventHandler : IAsyncEventProcessor +{ + private readonly IGroupManager _groupManager; + + public LeaveGroupEventHandler(IGroupManager groupManager) => _groupManager = groupManager; + + public async Task HandleAsync(LeaveGroupEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + if (!session.PlayerEntity.IsInGroup()) + { + return; + } + + PlayerGroup playerGroup = session.PlayerEntity.GetGroup(); + IReadOnlyList otherMembers = playerGroup.Members; + + bool closed = false; + bool isLeader = session.PlayerEntity.IsLeaderOfGroup(session.PlayerEntity.Id); + var toRemove = new List<(PlayerGroup, IPlayerEntity)>(); + + foreach (IPlayerEntity member in otherMembers) + { + if (member.Id == session.PlayerEntity.Id) + { + continue; + } + + if (otherMembers.Count - 1 > 1) + { + break; + } + + toRemove.Add((playerGroup, member)); + closed = true; + } + + _groupManager.RemoveMemberGroup(playerGroup, session.PlayerEntity); + session.SendMsg(session.GetLanguage(GameDialogKey.GROUP_INFO_LEFT), MsgMessageType.Middle); + await session.EmitEventAsync(new GroupWeedingEvent + { + RemoveBuff = true + }); + + if (session.CurrentMapInstance is { MapInstanceType: MapInstanceType.ArenaInstance }) + { + session.SendArenaStatistics(false, playerGroup); + } + + foreach ((PlayerGroup group, IPlayerEntity character) in toRemove) + { + _groupManager.RemoveMemberGroup(group, character); + character.Session.SendMsg(character.Session.GetLanguage(GameDialogKey.GROUP_SHOUTMESSAGE_CLOSED), MsgMessageType.Middle); + if (character.MapInstance is { MapInstanceType: MapInstanceType.ArenaInstance }) + { + character.Session.SendArenaStatistics(false); + } + } + + if (closed) + { + return; + } + + if (!isLeader) + { + return; + } + + var members = otherMembers.ToList(); + if (!members.Any()) + { + return; + } + + if (members.Count <= 1) + { + foreach (IPlayerEntity member in members) + { + _groupManager.RemoveMemberGroup(member.GetGroup(), member); + member.Session.SendMsg(member.Session.GetLanguage(GameDialogKey.GROUP_SHOUTMESSAGE_CLOSED), MsgMessageType.Middle); + if (member.MapInstance is { MapInstanceType: MapInstanceType.ArenaInstance }) + { + member.Session.SendArenaStatistics(false); + } + } + + return; + } + + IClientSession newLeader = members.ElementAt(1).Session; + if (newLeader == null) + { + return; + } + + _groupManager.ChangeLeader(playerGroup, newLeader.PlayerEntity.Id); + newLeader.SendInfo(newLeader.GetLanguage(GameDialogKey.GROUP_INFO_NEW_LEADER)); + if (newLeader.CurrentMapInstance is { MapInstanceType: MapInstanceType.ArenaInstance }) + { + newLeader.SendArenaStatistics(false, playerGroup); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Groups/GroupRemoveMemberEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Groups/GroupRemoveMemberEventHandler.cs new file mode 100644 index 0000000..ebbf9a0 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Groups/GroupRemoveMemberEventHandler.cs @@ -0,0 +1,27 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Groups; +using WingsEmu.Game.Groups.Events; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Groups; + +public class RemoveMemberFromGroupEventHandler : IAsyncEventProcessor +{ + private readonly IGroupManager _group; + + public RemoveMemberFromGroupEventHandler(IGroupManager group) => _group = group; + + public async Task HandleAsync(RemoveMemberFromGroupEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + if (!session.PlayerEntity.IsInGroup()) + { + return; + } + + PlayerGroup playerGroup = session.PlayerEntity.GetGroup(); + _group.RemoveMemberGroup(playerGroup, e.MemberToRemove); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Groups/GroupWeedingEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Groups/GroupWeedingEventHandler.cs new file mode 100644 index 0000000..f78305d --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Groups/GroupWeedingEventHandler.cs @@ -0,0 +1,75 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game._enum; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Groups.Events; +using WingsEmu.Game.Managers; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Groups; + +public class GroupWeedingEventHandler : IAsyncEventProcessor +{ + private readonly IBuffFactory _buffFactory; + private readonly ISessionManager _sessionManager; + + public GroupWeedingEventHandler(IBuffFactory buffFactory, ISessionManager sessionManager) + { + _buffFactory = buffFactory; + _sessionManager = sessionManager; + } + + public async Task HandleAsync(GroupWeedingEvent e, CancellationToken cancellation) + { + IPlayerEntity playerEntity = e.Sender.PlayerEntity; + bool removeBuff = e.RemoveBuff; + short buff = (short)BuffVnums.WEDDING; + + if (!playerEntity.IsInGroup()) + { + return; + } + + IPlayerEntity lover = playerEntity.GetGroup().Members.FirstOrDefault(x => x.IsMarried(playerEntity.Id)); + if (lover == null) + { + if (!e.RelatedId.HasValue) + { + return; + } + + lover = _sessionManager.GetSessionByCharacterId(e.RelatedId.Value)?.PlayerEntity; + if (lover == null) + { + return; + } + } + + if (!removeBuff) + { + if (!playerEntity.BuffComponent.HasBuff(buff)) + { + Buff playerBuff = _buffFactory.CreateBuff(buff, playerEntity, BuffFlag.BIG | BuffFlag.NO_DURATION); + await playerEntity.AddBuffAsync(playerBuff); + } + + if (lover.BuffComponent.HasBuff(buff)) + { + return; + } + + Buff marriageBuff = _buffFactory.CreateBuff(buff, lover, BuffFlag.BIG | BuffFlag.NO_DURATION); + await lover.AddBuffAsync(marriageBuff); + return; + } + + Buff weedingPlayerBuff = playerEntity.BuffComponent.GetBuff(buff); + await playerEntity.RemoveBuffAsync(true, weedingPlayerBuff); + + Buff weedingLoverBuff = lover.BuffComponent.GetBuff(buff); + await lover.RemoveBuffAsync(true, weedingLoverBuff); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Guri/GuriEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Guri/GuriEventHandler.cs new file mode 100644 index 0000000..bbc5e86 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Guri/GuriEventHandler.cs @@ -0,0 +1,19 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game._Guri; +using WingsEmu.Game._Guri.Event; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Guri; + +public class GuriEventHandler : IAsyncEventProcessor +{ + private readonly IGuriHandlerContainer _guriHandler; + + public GuriEventHandler(IGuriHandlerContainer guriHandler) => _guriHandler = guriHandler; + + public async Task HandleAsync(GuriEvent e, CancellationToken cancellation) + { + await Task.Run(() => _guriHandler.Handle(e.Sender, e), cancellation); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Items/CellonUpgradeEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Items/CellonUpgradeEventHandler.cs new file mode 100644 index 0000000..aec86f3 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Items/CellonUpgradeEventHandler.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.DTOs.Items; +using WingsEmu.Game; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Algorithm; +using WingsEmu.Game.Cellons; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Items; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Items; + +public class CellonUpgradeEventHandler : IAsyncEventProcessor +{ + private readonly ICellonGenerationAlgorithm _cellonGenerationAlgorithm; + private readonly CellonSystemConfiguration _cellonUpgradeConfiguration; + private readonly IGameLanguageService _language; + private readonly IRandomGenerator _randomGenerator; + + public CellonUpgradeEventHandler(IGameLanguageService language, ICellonGenerationAlgorithm cellonGenerationAlgorithm, CellonSystemConfiguration cellonUpgradeConfiguration, + IRandomGenerator randomGenerator) + { + _language = language; + _cellonGenerationAlgorithm = cellonGenerationAlgorithm; + _cellonUpgradeConfiguration = cellonUpgradeConfiguration; + _randomGenerator = randomGenerator; + } + + public async Task HandleAsync(CellonUpgradeEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + InventoryItem cellonItem = e.Cellon; + GameItemInstance cellon = cellonItem.ItemInstance; + GameItemInstance upgradableItem = e.UpgradableItem; + + if (cellon.GameItem.EffectValue > upgradableItem.GameItem.MaxCellonLvl) + { + session.SendMsg(_language.GetLanguage(GameDialogKey.CELLON_SHOUTMESSAGE_LEVEL_TOO_HIGH, session.UserLanguage), MsgMessageType.Middle); + session.SendShopEndPacket(ShopEndType.Npc); + return; + } + + if (upgradableItem.GameItem.MaxCellon <= upgradableItem.EquipmentOptions?.Count) + { + session.SendMsg(_language.GetLanguage(GameDialogKey.CELLON_SHOUTMESSAGE_OPTIONS_FULL, session.UserLanguage), MsgMessageType.Middle); + session.SendShopEndPacket(ShopEndType.Npc); + return; + } + + CellonPossibilities mats = _cellonUpgradeConfiguration.Options.FirstOrDefault(s => s.CellonLevel == cellon.GameItem.EffectValue); + if (mats == default) + { + return; + } + + int gold = mats.Price; + if (!session.HasEnoughGold(gold)) + { + session.SendMsg(_language.GetLanguage(GameDialogKey.INTERACTION_MESSAGE_NOT_ENOUGH_GOLD, session.UserLanguage), MsgMessageType.Middle); + session.SendShopEndPacket(ShopEndType.Npc); + return; + } + + // REMOVE ITEMS TO USE + session.PlayerEntity.RemoveGold(gold); + await session.RemoveItemFromInventory(item: cellonItem); + + // roll chance + CellonChances tmp = _cellonUpgradeConfiguration.ChancesToSuccess.FirstOrDefault(s => s.CellonAmount == upgradableItem.Cellon); + + if (tmp == null) + { + return; + } + + int roll = _randomGenerator.RandomNumber((int)Math.Floor(tmp.SuccessChance)); + if (roll >= tmp.SuccessChance) + { + session.SendMsg(_language.GetLanguage(GameDialogKey.CELLON_SHOUTMESSAGE_FAIL, session.UserLanguage), MsgMessageType.Middle); + session.SendShopEndPacket(ShopEndType.Npc); + return; + } + + // GENERATE OPTION + EquipmentOptionDTO option = _cellonGenerationAlgorithm.GenerateOption(cellon.GameItem.EffectValue); + + upgradableItem.EquipmentOptions ??= new List(); + + // FAIL + if (option == null || upgradableItem.EquipmentOptions.Any(s => s.Type == option.Type)) + { + session.SendMsg(_language.GetLanguage(GameDialogKey.CELLON_SHOUTMESSAGE_FAIL, session.UserLanguage), MsgMessageType.Middle); + session.SendShopEndPacket(ShopEndType.Npc); + await session.EmitEventAsync(new CellonUpgradedEvent + { + Item = upgradableItem, + CellonVnum = cellon.ItemVNum, + Succeed = false + }); + return; + } + + // SUCCESS + upgradableItem.EquipmentOptions.Add(option); + upgradableItem.Cellon++; + session.SendMsg(_language.GetLanguage(GameDialogKey.CELLON_SHOUTMESSAGE_SUCCESS, session.UserLanguage), MsgMessageType.Middle); + session.SendShopEndPacket(ShopEndType.Npc); + + await session.EmitEventAsync(new CellonUpgradedEvent + { + Item = upgradableItem, + CellonVnum = cellon.ItemVNum, + Succeed = true + }); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Items/DropItemEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Items/DropItemEventHandler.cs new file mode 100644 index 0000000..346ed43 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Items/DropItemEventHandler.cs @@ -0,0 +1,56 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.PacketGeneration; +using WingsEmu.Game; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Inventory.Event; +using WingsEmu.Game.Items; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Raids; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Items; + +public class ThrowItemEventHandler : IAsyncEventProcessor +{ + private readonly IGameItemInstanceFactory _gameItem; + private readonly IRandomGenerator _randomGenerator; + + public ThrowItemEventHandler(IGameItemInstanceFactory gameItem, IRandomGenerator randomGenerator) + { + _gameItem = gameItem; + _randomGenerator = randomGenerator; + } + + public async Task HandleAsync(ThrowItemEvent e, CancellationToken cancellation) + { + GameItemInstance newItem = _gameItem.CreateItem(e.ItemVnum, e.Quantity); + + int rndX = e.BattleEntity.PositionX + _randomGenerator.RandomNumber(e.MinimumDistance, e.MaximumDistance + 1) * (_randomGenerator.RandomNumber(0, 2) * 2 - 1); + int rndY = e.BattleEntity.PositionY + _randomGenerator.RandomNumber(e.MinimumDistance, e.MaximumDistance + 1) * (_randomGenerator.RandomNumber(0, 2) * 2 - 1); + + var position = new Position((short)rndX, (short)rndY); + + var item = new MonsterMapItem(position.X, position.Y, newItem, e.BattleEntity.MapInstance); + + e.BattleEntity.MapInstance.AddDrop(item); + e.BattleEntity.BroadcastThrow(item); + } +} + +public class DropItemEventHandler : IAsyncEventProcessor +{ + private readonly IGameItemInstanceFactory _gameItem; + + public DropItemEventHandler(IGameItemInstanceFactory gameItem) => _gameItem = gameItem; + + public async Task HandleAsync(DropMapItemEvent e, CancellationToken cancellation) + { + IMapInstance map = e.Map; + GameItemInstance newItem = _gameItem.CreateItem(e.Vnum, e.Amount, (byte)e.Upgrade, (sbyte)e.Rarity, (byte)e.Design); + var item = new MonsterMapItem(e.Position.X, e.Position.Y, newItem, e.Map, e.OwnerId, e.IsQuest); + + map.AddDrop(item); + item.BroadcastDrop(); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Items/DropRarityConfiguration.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Items/DropRarityConfiguration.cs new file mode 100644 index 0000000..07d909a --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Items/DropRarityConfiguration.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Items; + +public class DropRarityConfiguration +{ + public List Equipment { get; set; } + public List Shells { get; set; } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Items/DropRarityConfigurationProvider.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Items/DropRarityConfigurationProvider.cs new file mode 100644 index 0000000..25c6356 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Items/DropRarityConfigurationProvider.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using System.Linq; +using WingsEmu.Game; +using WingsEmu.Game.Items; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Items; + +public class DropRarityConfigurationProvider : IDropRarityConfigurationProvider +{ + private readonly List _equipment; + private readonly IRandomGenerator _randomGenerator; + private readonly List _shells; + + public DropRarityConfigurationProvider(DropRarityConfiguration dropRarityConfiguration, IRandomGenerator randomGenerator) + { + _randomGenerator = randomGenerator; + _equipment = dropRarityConfiguration.Equipment.OrderBy(s => s.Chance).ToList(); + _shells = dropRarityConfiguration.Shells.OrderBy(s => s.Chance).ToList(); + } + + public sbyte GetRandomRarity(ItemType itemType) + { + if (itemType != ItemType.Weapon && itemType != ItemType.Armor && itemType != ItemType.Shell) + { + return 0; + } + + List rarities = itemType == ItemType.Shell ? _shells : _equipment; + var randomBag = new RandomBag(_randomGenerator); + + foreach (RarityChance rarity in rarities) + { + randomBag.AddEntry(rarity, rarity.Chance); + } + + return randomBag.GetRandom().Rarity; + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Items/GamblingEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Items/GamblingEventHandler.cs new file mode 100644 index 0000000..77fca00 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Items/GamblingEventHandler.cs @@ -0,0 +1,300 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsAPI.Game.Extensions.PacketGeneration; +using WingsAPI.Packets.Enums; +using WingsEmu.DTOs.Items; +using WingsEmu.Game; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Items; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Networking.Broadcasting; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Items; + +public class GamblingEventHandler : IAsyncEventProcessor +{ + private readonly IGamblingRarityConfiguration _gamblingRarityConfiguration; + private readonly GamblingRarityInfo _gamblingRarityInfo; + private readonly IGameLanguageService _gameLanguage; + private readonly IRandomGenerator _randomGenerator; + + public GamblingEventHandler(IGameLanguageService gameLanguage, IRandomGenerator randomGenerator, IGamblingRarityConfiguration gamblingRarityConfiguration, GamblingRarityInfo gamblingRarityInfo) + { + _gameLanguage = gameLanguage; + _randomGenerator = randomGenerator; + _gamblingRarityConfiguration = gamblingRarityConfiguration; + _gamblingRarityInfo = gamblingRarityInfo; + } + + public async Task HandleAsync(GamblingEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + RarifyProtection protection = e.Protection; + + if (!session.HasCurrentMapInstance || e.Item.ItemInstance.Type != ItemInstanceType.WearableInstance) + { + return; + } + + + GameItemInstance item = e.Item.ItemInstance; + GameItemInstance amulet = e.Amulet?.ItemInstance; + + const int cellaVnum = (int)ItemVnums.CELLA; + const int scrollVnum = (int)ItemVnums.EQ_NORMAL_SCROLL; + + short originalRarity = item.Rarity; + switch (e.Mode) + { + case RarifyMode.Increase: + if (item.Rarity >= 8) + { + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.GAMBLING_MESSAGE_ALREADY_MAX_RARE, session.UserLanguage), ChatMessageColorType.Yellow); + return; + } + + if (IsChampion(amulet) && !item.GameItem.IsHeroic) + { + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.GAMBLING_CHATMESSAGE_ITEM_IS_NOT_HEROIC, session.UserLanguage), ChatMessageColorType.Yellow); + return; + } + + if (!IsChampion(amulet) && item.GameItem.IsHeroic) + { + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.GAMBLING_MESSAGE_ITEM_IS_HEROIC, session.UserLanguage), ChatMessageColorType.Yellow); + return; + } + + if (amulet == null) + { + return; + } + + item.Rarity += 1; + item.SetRarityPoint(_randomGenerator); + session.SendInventoryAddPacket(e.Item); + session.NotifyRarifyResult(_gameLanguage, item.Rarity); + await session.RemoveItemFromInventory(item: e.Amulet, isEquiped: true); + session.RefreshEquipment(); + session.SendModal(_gameLanguage.GetLanguage(GameDialogKey.GAMBLING_INFO_AMULET_DESTROYED, session.UserLanguage), ModalType.Confirm); + + await session.EmitEventAsync(new ItemGambledEvent + { + ItemVnum = item.ItemVNum, + Mode = e.Mode, + Protection = e.Protection, + Amulet = e.Amulet?.ItemInstance.ItemVNum, + Succeed = true, + OriginalRarity = originalRarity, + FinalRarity = item.Rarity + }); + return; + + case RarifyMode.Normal: + if (session.PlayerEntity.Gold < _gamblingRarityInfo.GoldPrice) + { + return; + } + + if (!session.PlayerEntity.HasItem(cellaVnum, _gamblingRarityInfo.CellaUsed)) + { + return; + } + + + switch (protection) + { + case RarifyProtection.Scroll when !session.PlayerEntity.HasItem(scrollVnum): + return; + // Using normal amulet/scroll on heroic item + case RarifyProtection.Scroll or RarifyProtection.ProtectionAmulet or RarifyProtection.BlessingAmulet when item.GameItem.IsHeroic: + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.GAMBLING_MESSAGE_ITEM_IS_HEROIC, session.UserLanguage), MsgMessageType.Middle); + return; + // Using heroic amulet on normal item + case RarifyProtection.HeroicAmulet or RarifyProtection.RandomHeroicAmulet when !item.GameItem.IsHeroic: + session.SendMsg( + _gameLanguage.GetLanguage(GameDialogKey.GAMBLING_CHATMESSAGE_ITEM_IS_NOT_HEROIC, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (item.GameItem.IsHeroic && item.Rarity == 8) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.GAMBLING_MESSAGE_ALREADY_MAX_RARE, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (protection == RarifyProtection.Scroll) + { + if (!session.PlayerEntity.HasItem(scrollVnum)) + { + session.SendShopEndPacket(ShopEndType.SpecialistHolder); + return; + } + + await session.RemoveItemFromInventory(scrollVnum); + session.SendShopEndPacket(ShopEndType.Player); + } + + session.PlayerEntity.Gold -= _gamblingRarityInfo.GoldPrice; + await session.RemoveItemFromInventory(cellaVnum, _gamblingRarityInfo.CellaUsed); + session.RefreshGold(); + break; + + + default: + throw new ArgumentOutOfRangeException(nameof(e.Mode), e.Mode, "The selected RarifyMode is not handled"); + } + + if (GamblingSuccess(item, amulet)) + { + short rarity = _gamblingRarityConfiguration.GetRandomRarity(); + if (protection == RarifyProtection.Scroll && rarity > item.Rarity || protection != RarifyProtection.Scroll) + { + session.NotifyRarifyResult(_gameLanguage, rarity); + session.BroadcastEffectInRange(EffectType.UpgradeSuccess); + item.Rarity = rarity; + } + + else if (rarity <= item.Rarity) + { + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.GAMBLING_MESSAGE_FAILED_ITEM_SAVED, session.UserLanguage), ChatMessageColorType.Red); + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.GAMBLING_MESSAGE_FAILED_ITEM_SAVED, session.UserLanguage), MsgMessageType.Middle); + session.BroadcastEffectInRange(EffectType.UpgradeFail); + + await session.EmitEventAsync(new ItemGambledEvent + { + ItemVnum = item.ItemVNum, + Mode = e.Mode, + Protection = e.Protection, + Amulet = e.Amulet?.ItemInstance.ItemVNum, + Succeed = false, + OriginalRarity = originalRarity, + FinalRarity = item.Rarity + }); + return; + } + + item.SetRarityPoint(_randomGenerator); + session.SendInventoryAddPacket(e.Item); + + await session.EmitEventAsync(new ItemGambledEvent + { + ItemVnum = item.ItemVNum, + Mode = e.Mode, + Protection = e.Protection, + Amulet = e.Amulet?.ItemInstance.ItemVNum, + Succeed = true, + OriginalRarity = originalRarity, + FinalRarity = item.Rarity + }); + } + else + { + switch (protection) + { + case RarifyProtection.ProtectionAmulet: + case RarifyProtection.BlessingAmulet: + case RarifyProtection.HeroicAmulet: + case RarifyProtection.RandomHeroicAmulet: + if (amulet == null) + { + return; + } + + amulet.DurabilityPoint -= 1; + session.SendAmuletBuffPacket(amulet); + if (amulet.DurabilityPoint <= 0) + { + await session.RemoveItemFromInventory(item: e.Amulet, isEquiped: true); + session.RefreshEquipment(); + session.SendModal(_gameLanguage.GetLanguage(GameDialogKey.GAMBLING_INFO_AMULET_DESTROYED, session.UserLanguage), ModalType.Confirm); + } + + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.GAMBLING_MESSAGE_AMULET_FAIL_SAVED, session.UserLanguage), ChatMessageColorType.Red); + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.GAMBLING_MESSAGE_AMULET_FAIL_SAVED, session.UserLanguage), MsgMessageType.Middle); + + await session.EmitEventAsync(new ItemGambledEvent + { + ItemVnum = item.ItemVNum, + Mode = e.Mode, + Protection = e.Protection, + Amulet = e.Amulet.ItemInstance.ItemVNum, + Succeed = false, + OriginalRarity = originalRarity, + FinalRarity = item.Rarity + }); + return; + + case RarifyProtection.Scroll: + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.GAMBLING_MESSAGE_FAILED_ITEM_SAVED, session.UserLanguage), ChatMessageColorType.Red); + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.GAMBLING_MESSAGE_FAILED_ITEM_SAVED, session.UserLanguage), MsgMessageType.Middle); + session.BroadcastEffect(EffectType.UpgradeFail, new RangeBroadcast(session.PlayerEntity.PositionX, session.PlayerEntity.PositionY)); + + if (!session.PlayerEntity.HasItem(scrollVnum)) + { + session.SendShopEndPacket(ShopEndType.SpecialistHolder); + } + + await session.EmitEventAsync(new ItemGambledEvent + { + ItemVnum = item.ItemVNum, + Mode = e.Mode, + Protection = e.Protection, + Amulet = e.Amulet?.ItemInstance.ItemVNum, + Succeed = false, + OriginalRarity = originalRarity, + FinalRarity = item.Rarity + }); + return; + + case RarifyProtection.None: + await session.RemoveItemFromInventory(item: e.Item); + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.GAMBLING_MESSAGE_FAILED, session.UserLanguage), ChatMessageColorType.Red); + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.GAMBLING_MESSAGE_FAILED, session.UserLanguage), MsgMessageType.Middle); + + await session.EmitEventAsync(new ItemGambledEvent + { + ItemVnum = item.ItemVNum, + Mode = e.Mode, + Protection = e.Protection, + Amulet = e.Amulet?.ItemInstance.ItemVNum, + Succeed = false, + OriginalRarity = originalRarity + }); + return; + } + } + } + + private bool GamblingSuccess(GameItemInstance item, GameItemInstance amulet) + { + if (item.Rarity < 0) + { + return true; + } + + RaritySuccess raritySuccess = _gamblingRarityConfiguration.GetRaritySuccess((byte)item.Rarity); + if (raritySuccess == null) + { + return false; + } + + int rnd = _randomGenerator.RandomNumber(10000); + return rnd < (IsEnhanced(amulet) ? raritySuccess.SuccessChance + 1000 : raritySuccess.SuccessChance); + } + + private bool IsChampion(GameItemInstance amulet) => + amulet.ItemVNum is (short)ItemVnums.CHAMPION_AMULET or (short)ItemVnums.CHAMPION_AMULET_RANDOM; + + private bool IsEnhanced(GameItemInstance amulet) => + amulet != null && amulet.ItemVNum is (short)ItemVnums.BLESSING_AMULET or (short)ItemVnums.BLESSING_AMULET_DOUBLE or (short)ItemVnums.CHAMPION_AMULET or (short)ItemVnums.CHAMPION_AMULET_RANDOM + or (short)ItemVnums.PROTECTION_AMULET; +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Items/GamblingRarityConfiguration.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Items/GamblingRarityConfiguration.cs new file mode 100644 index 0000000..cfb62fa --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Items/GamblingRarityConfiguration.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Linq; +using WingsEmu.Game; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Items; + +public interface IGamblingRarityConfiguration +{ + RaritySuccess GetRaritySuccess(byte fromRarity); + short GetRandomRarity(); +} + +public class GamblingRarityConfiguration : IGamblingRarityConfiguration +{ + private readonly List _gamblingSuccess; + private readonly RandomBag _randomRarities; + + public GamblingRarityConfiguration(GamblingRarityInfo gamblingRarityInfo, IRandomGenerator randomGenerator) + { + _gamblingSuccess = gamblingRarityInfo.GamblingSuccess; + var gamblingRarities = gamblingRarityInfo.GamblingRarities.OrderBy(s => s.Chance).ToList(); + _randomRarities = new RandomBag(randomGenerator); + + foreach (RarityChance rarity in gamblingRarities) + { + _randomRarities.AddEntry(rarity, rarity.Chance); + } + } + + public RaritySuccess GetRaritySuccess(byte fromRarity) => _gamblingSuccess.FirstOrDefault(s => s.FromRarity == fromRarity); + + public short GetRandomRarity() => _randomRarities.GetRandom().Rarity; +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Items/GamblingRarityInfo.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Items/GamblingRarityInfo.cs new file mode 100644 index 0000000..3aaa646 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Items/GamblingRarityInfo.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Items; + +public class GamblingRarityInfo +{ + public List GamblingSuccess { get; set; } + public List GamblingRarities { get; set; } + public short GoldPrice { get; set; } + public byte CellaUsed { get; set; } +} + +public class RarityChance +{ + public sbyte Rarity { get; set; } + public int Chance { get; set; } +} + +public class RaritySuccess +{ + public short FromRarity { get; set; } + public int SuccessChance { get; set; } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Items/ItemSumConfiguration.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Items/ItemSumConfiguration.cs new file mode 100644 index 0000000..2d3fa23 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Items/ItemSumConfiguration.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Items; + +[DataContract] +public class ItemSumConfiguration : List +{ +} + +public class ItemSumMats +{ + public int SumUpgrade { get; set; } + public int Gold { get; set; } + public int RiverSand { get; set; } + public int SuccessChance { get; set; } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Items/ItemSumEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Items/ItemSumEventHandler.cs new file mode 100644 index 0000000..3b95173 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Items/ItemSumEventHandler.cs @@ -0,0 +1,160 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using PhoenixLib.Logging; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.DTOs.Items; +using WingsEmu.Game; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Inventory.Event; +using WingsEmu.Game.Items; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Items; + +public class ItemSumEventHandler : IAsyncEventProcessor +{ + private readonly IGameLanguageService _gameLanguage; + private readonly ItemSumConfiguration _itemSumConfiguration; + private readonly IRandomGenerator _randomGenerator; + + public ItemSumEventHandler(IRandomGenerator randomGenerator, IGameLanguageService gameLanguage, ItemSumConfiguration itemSumConfiguration) + { + _randomGenerator = randomGenerator; + _gameLanguage = gameLanguage; + _itemSumConfiguration = itemSumConfiguration; + } + + public async Task HandleAsync(ItemSumEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + + if (e.LeftItem.ItemInstance.Type != ItemInstanceType.WearableInstance) + { + return; + } + + if (e.RightItem.ItemInstance.Type != ItemInstanceType.WearableInstance) + { + return; + } + + GameItemInstance leftItem = e.LeftItem.ItemInstance; + GameItemInstance rightItem = e.RightItem.ItemInstance; + + const int sandVnum = (int)ItemVnums.DONA_RIVER_SAND; + int totalUpgrade = leftItem.Upgrade + rightItem.Upgrade; + + // They are the same item + if (leftItem == rightItem) + { + return; + } + + // Items aren't compatible to sum + if (totalUpgrade >= 6) + { + Log.Debug($"[ITEM_SUM] Sum total of {totalUpgrade.ToString()}"); + return; + } + + // Neither gloves or boots + if ((rightItem.GameItem.EquipmentSlot != EquipmentType.Gloves || leftItem.GameItem.EquipmentSlot != EquipmentType.Gloves) + && (leftItem.GameItem.EquipmentSlot != EquipmentType.Boots || rightItem.GameItem.EquipmentSlot != EquipmentType.Boots)) + { + Log.Debug("[ITEM_SUM] At least one of the items is neither boots or gloves."); + return; + } + + // They aren't the same type of equipment + if (rightItem.GameItem.EquipmentSlot != leftItem.GameItem.EquipmentSlot) + { + Log.Debug("[ITEM_SUM] They are not the same type of equipment."); + return; + } + + ItemSumMats itemSumMats = _itemSumConfiguration.FirstOrDefault(i => i.SumUpgrade == totalUpgrade); + if (itemSumMats == null) + { + return; + } + + // No money + if (session.PlayerEntity.Gold < itemSumMats.Gold) + { + Log.Debug($"[ITEM_SUM] Not enough gold. Required: {itemSumMats.Gold.ToString()}"); + return; + } + + // No sand + if (!session.PlayerEntity.HasItem(sandVnum, (short)itemSumMats.RiverSand)) + { + Log.Debug($"[ITEM_SUM] Not enough sand. Required: {itemSumMats.RiverSand.ToString()}"); + return; + } + + await session.RemoveItemFromInventory(sandVnum, (short)itemSumMats.RiverSand); + session.PlayerEntity.Gold -= itemSumMats.Gold; + + InventoryItem rightItemToRemove = session.PlayerEntity.GetItemBySlotAndType(e.RightItem.Slot, e.RightItem.InventoryType); + InventoryItem leftItemInstance = session.PlayerEntity.GetItemBySlotAndType(e.LeftItem.Slot, e.LeftItem.InventoryType); + ; + + int rnd = _randomGenerator.RandomNumber(); + if (rnd < itemSumMats.SuccessChance) + { + await session.EmitEventAsync(new ItemSummedEvent + { + LeftItem = leftItem, + RightItem = rightItem, + Succeed = true, + SumLevel = itemSumMats.SumUpgrade + }); + + leftItem.Upgrade += (byte)(rightItem.Upgrade + 1); + leftItem.DarkResistance += (short)(rightItem.DarkResistance + rightItem.GameItem.DarkResistance); + leftItem.LightResistance += (short)(rightItem.LightResistance + rightItem.GameItem.LightResistance); + leftItem.WaterResistance += (short)(rightItem.WaterResistance + rightItem.GameItem.WaterResistance); + leftItem.FireResistance += (short)(rightItem.FireResistance + rightItem.GameItem.FireResistance); + + await session.RemoveItemFromInventory(item: rightItemToRemove); + + session.SendPdtiPacket(PdtiType.ResistancesAreFused, leftItem.ItemVNum, 1, e.RightItem.Slot, leftItem.Upgrade, 0); + + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.SUM_MESSAGE_SUCCESS, session.UserLanguage), MsgMessageType.Middle); + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.SUM_MESSAGE_SUCCESS, session.UserLanguage), ChatMessageColorType.Green); + session.BroadcastEffectInRange(EffectType.UpgradeSuccess); + session.SendSound(SoundType.CRAFTING_SUCCESS); + + session.SendInventoryAddPacket(leftItemInstance); + } + else + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.SUM_MESSAGE_FAILED, session.UserLanguage), MsgMessageType.Middle); + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.SUM_MESSAGE_FAILED, session.UserLanguage), ChatMessageColorType.Red); + session.SendSound(SoundType.CRAFTING_FAILED); + + await session.RemoveItemFromInventory(item: rightItemToRemove); + await session.RemoveItemFromInventory(item: leftItemInstance); + + await session.EmitEventAsync(new ItemSummedEvent + { + LeftItem = leftItem, + RightItem = rightItem, + Succeed = false, + SumLevel = itemSumMats.SumUpgrade + }); + } + + session.PlayerEntity.BroadcastEndDancingGuriPacket(); + session.RefreshGold(); + session.SendShopEndPacket(ShopEndType.Npc); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Items/PartnerSpecialistRollConfiguration.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Items/PartnerSpecialistRollConfiguration.cs new file mode 100644 index 0000000..f088ed9 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Items/PartnerSpecialistRollConfiguration.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using System.Linq; +using WingsEmu.Game; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Items; + +public interface IPartnerSpecialistSkillRoll +{ + byte GetRandomSkillRank(); +} + +public class PartnerSpecialistSkillRoll : IPartnerSpecialistSkillRoll +{ + private readonly RandomBag _randomRanks; + + public PartnerSpecialistSkillRoll(PartnerSpecialistSkillRollConfiguration randomRarities, IRandomGenerator randomGenerator) + { + var skillRanks = randomRarities.OrderBy(s => s.Chance).ToList(); + _randomRanks = new RandomBag(randomGenerator); + + foreach (PartnerSpecialistSkillChances skill in skillRanks) + { + _randomRanks.AddEntry(skill, skill.Chance); + } + } + + public byte GetRandomSkillRank() => _randomRanks.GetRandom().SkillRank; +} + +public class PartnerSpecialistSkillRollConfiguration : List +{ +} + +public class PartnerSpecialistSkillChances +{ + public int Chance { get; set; } + public byte SkillRank { get; set; } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Items/PartnerSpecialistSkillEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Items/PartnerSpecialistSkillEventHandler.cs new file mode 100644 index 0000000..b8d4982 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Items/PartnerSpecialistSkillEventHandler.cs @@ -0,0 +1,154 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.PacketGeneration; +using WingsAPI.Packets.Enums; +using WingsEmu.DTOs.Skills; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Extensions.Mates; +using WingsEmu.Game.Inventory.Event; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Networking.Broadcasting; +using WingsEmu.Game.Skills; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Items; + +public class PartnerSpecialistSkillEventHandler : IAsyncEventProcessor +{ + private readonly IDelayManager _delayManager; + private readonly IGameLanguageService _gameLanguage; + private readonly IPartnerSpecialistSkillRoll _partnerSpecialistSkill; + private readonly ISkillsManager _skillsManager; + + public PartnerSpecialistSkillEventHandler(ISkillsManager skillsManager, IGameLanguageService languageService, IDelayManager delayManager, IPartnerSpecialistSkillRoll partnerSpecialistSkill) + { + _skillsManager = skillsManager; + _gameLanguage = languageService; + _delayManager = delayManager; + _partnerSpecialistSkill = partnerSpecialistSkill; + } + + public async Task HandleAsync(PartnerSpecialistSkillEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + byte partnerSlot = e.PartnerSlot; + byte skillSlot = e.SkillSlot; + bool roll = e.Roll; + + IMateEntity partnerInTeam = session.PlayerEntity.MateComponent.GetTeamMember(s => s.MateType == MateType.Partner && s.PetSlot == partnerSlot); + if (partnerInTeam == null) + { + session.SendModal(_gameLanguage.GetLanguage(GameDialogKey.PARTNER_INFO_NO_PARTNER_IN_TEAM, session.UserLanguage), ModalType.Confirm); + return; + } + + if (!partnerInTeam.IsAlive()) + { + return; + } + + if (session.PlayerEntity.IsOnVehicle) + { + return; + } + + if (skillSlot > 2) + { + await session.NotifyStrangeBehavior(StrangeBehaviorSeverity.ABUSING, "Tried to learn PSP skill when slot > 2"); + return; + } + + if (partnerInTeam.Specialist == null) + { + session.SendModal(_gameLanguage.GetLanguage(GameDialogKey.PARTNER_MESSAGE_NO_SP_EQUIPPED, session.UserLanguage), ModalType.Confirm); + return; + } + + if (partnerInTeam.IsUsingSp) + { + session.SendModal(_gameLanguage.GetLanguage(GameDialogKey.PARTNER_INFO_IS_WEARING_SP, session.UserLanguage), ModalType.Confirm); + return; + } + + if (partnerInTeam.Level < 30) + { + session.SendModal(_gameLanguage.GetLanguage(GameDialogKey.PARTNER_INFO_LEVEL_IS_TOO_LOW, session.UserLanguage), ModalType.Confirm); + return; + } + + if (partnerInTeam.Specialist.Agility < 100 && !session.IsGameMaster()) + { + session.SendModal(_gameLanguage.GetLanguage(GameDialogKey.PARTNER_INFO_NEED_AGILITY_POINTS, session.UserLanguage), ModalType.Confirm); + return; + } + + if (partnerInTeam.HavePartnerSkill(skillSlot)) + { + return; + } + + if (!roll) + { + DateTime waitUntil = await _delayManager.RegisterAction(partnerInTeam, DelayedActionType.PartnerLearnSkill); + session.SendMateDelay(partnerInTeam, (int)(waitUntil - DateTime.UtcNow).TotalMilliseconds, GuriType.Identifying, $"#ps_op^{partnerSlot}^{skillSlot}^1"); + session.CurrentMapInstance?.Broadcast(partnerInTeam.GenerateMateDance(), new RangeBroadcast(partnerInTeam.PositionX, partnerInTeam.PositionY)); + return; + } + + bool canLearn = await _delayManager.CanPerformAction(partnerInTeam, DelayedActionType.PartnerLearnSkill); + if (!canLearn) + { + return; + } + + await _delayManager.CompleteAction(partnerInTeam, DelayedActionType.PartnerLearnSkill); + + switch (skillSlot) + { + case 0: + partnerInTeam.Specialist.PartnerSkill1 = true; + break; + case 1: + partnerInTeam.Specialist.PartnerSkill2 = true; + break; + case 2: + partnerInTeam.Specialist.PartnerSkill3 = true; + break; + default: + return; + } + + RollChances(partnerInTeam, skillSlot); + + partnerInTeam.Specialist.Agility = 0; + session.SendMatePskiPacket(partnerInTeam); + session.SendPetInfo(partnerInTeam, _gameLanguage); + session.SendModal(_gameLanguage.GetLanguage(GameDialogKey.PARTNER_INFO_SP_NEW_SKILL, session.UserLanguage), ModalType.Confirm); + } + + private void RollChances(IMateEntity mateEntity, byte slot) + { + mateEntity.Specialist.PartnerSkills ??= new List(3); + foreach (SkillDTO ski in _skillsManager.GetSkills().Where(ski + => ski.SkillType == SkillType.PartnerSkill && ski.UpgradeType == mateEntity.Specialist.GameItem.SpMorphId && ski.CastId == slot)) + { + mateEntity.Specialist.PartnerSkills.Add(new PartnerSkill + { + LastUse = DateTime.MinValue, + Rank = _partnerSpecialistSkill.GetRandomSkillRank(), + Slot = (byte)ski.CastId, + SkillId = ski.Id + }); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Items/PlayerItemToPartnerItemEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Items/PlayerItemToPartnerItemEventHandler.cs new file mode 100644 index 0000000..302025f --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Items/PlayerItemToPartnerItemEventHandler.cs @@ -0,0 +1,153 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.DTOs.Items; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Inventory.Event; +using WingsEmu.Game.Items; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Items; + +public class PlayerItemToPartnerItemEventHandler : IAsyncEventProcessor +{ + private static readonly int[] _partnerItems = + { + (int)ItemVnums.PARTNER_WEAPON_MELEE, (int)ItemVnums.PARTNER_WEAPON_RANGED, (int)ItemVnums.PARTNER_WEAPON_MAGIC, (int)ItemVnums.PARTNER_ARMOR_MAGIC, (int)ItemVnums.PARTNER_ARMOR_RANGED, + (int)ItemVnums.PARTNER_ARMOR_MELEE + }; + + private readonly IGameLanguageService _languageService; + + public PlayerItemToPartnerItemEventHandler(IGameLanguageService languageService) => _languageService = languageService; + + public async Task HandleAsync(PlayerItemToPartnerItemEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + + InventoryItem item = session.PlayerEntity.GetItemBySlotAndType(e.Slot, e.InventoryType); + if (item == null) + { + return; + } + + if (item.ItemInstance.Type != ItemInstanceType.WearableInstance) + { + return; + } + + GameItemInstance itemToTransform = item.ItemInstance; + + const ItemVnums donaVNum = ItemVnums.DONA_RIVER_SAND; + int price = 300 * itemToTransform.GameItem.LevelMinimum + 2000; // Formula by friends111 :peepoLove: + + if (itemToTransform.GameItem.EquipmentSlot != EquipmentType.Armor + && itemToTransform.GameItem.EquipmentSlot != EquipmentType.MainWeapon + && itemToTransform.GameItem.EquipmentSlot != EquipmentType.SecondaryWeapon) + { + await session.NotifyStrangeBehavior(StrangeBehaviorSeverity.NORMAL, "Tried to transform a PlayerItem to a PartnerItem, and that PlayerItem is not the 'transformable' type" + + "(in theory there is a Client-side check for that)"); + return; + } + + if (_partnerItems.Contains(itemToTransform.ItemVNum)) + { + await session.NotifyStrangeBehavior(StrangeBehaviorSeverity.NORMAL, "Tried to transform a PartnerItem to a PartnerItem (not possible with normal client)"); + return; + } + + if (session.PlayerEntity.Gold < price) + { + await session.NotifyStrangeBehavior(StrangeBehaviorSeverity.ABUSING, "Seems like the balance the client thinks he has is superior to what the server has. (not sufficient" + + " gold to pay the PlayerItem to PartnerItem transformation)"); + return; + } + + if (!session.PlayerEntity.HasItem((short)donaVNum, itemToTransform.GameItem.LevelMinimum)) + { + await session.NotifyStrangeBehavior(StrangeBehaviorSeverity.ABUSING, "Tried to transform a PlayerItem to a PartnerItem without sufficient amount of" + + $" 'Dona River Sand' ({((short)donaVNum).ToString()}), and the Client-side check should have prevented that."); + return; + } + + if (itemToTransform.EquipmentOptions != null && itemToTransform.EquipmentOptions.Any()) + { + session.SendChatMessage(_languageService.GetLanguage(GameDialogKey.ITEM_CHATMESSAGE_CANNOT_BE_WITH_SHELL, session.UserLanguage), ChatMessageColorType.Red); + session.SendShopEndPacket(ShopEndType.Npc); + return; + } + + GameItemInstance newItem = itemToTransform; + switch (itemToTransform.GameItem.EquipmentSlot) + { + case EquipmentType.Armor: + switch (itemToTransform.GameItem.Class) + { + case (int)ItemClassType.Swordsman: + case (int)ItemClassType.MartialArtist: + newItem.ItemVNum = (int)ItemVnums.PARTNER_ARMOR_MELEE; + break; + case (int)ItemClassType.Archer: + newItem.ItemVNum = (int)ItemVnums.PARTNER_ARMOR_RANGED; + break; + case (int)ItemClassType.Mage: + newItem.ItemVNum = (int)ItemVnums.PARTNER_ARMOR_MAGIC; + break; + default: + session.SendShopEndPacket(ShopEndType.Npc); + return; + } + + break; + case EquipmentType.SecondaryWeapon: + case EquipmentType.MainWeapon: + switch (itemToTransform.GameItem.Class) + { + case (int)ItemClassType.Swordsman: + newItem.ItemVNum = itemToTransform.GameItem.EquipmentSlot == (int)EquipmentType.MainWeapon ? (int)ItemVnums.PARTNER_WEAPON_MELEE : (int)ItemVnums.PARTNER_WEAPON_RANGED; + break; + case (int)ItemClassType.Archer: + newItem.ItemVNum = itemToTransform.GameItem.EquipmentSlot == (int)EquipmentType.MainWeapon ? (int)ItemVnums.PARTNER_WEAPON_RANGED : (int)ItemVnums.PARTNER_WEAPON_MELEE; + break; + case (int)ItemClassType.Mage: + newItem.ItemVNum = itemToTransform.GameItem.EquipmentSlot == (int)EquipmentType.MainWeapon ? (int)ItemVnums.PARTNER_WEAPON_MAGIC : (int)ItemVnums.PARTNER_WEAPON_RANGED; + break; + case (int)ItemClassType.MartialArtist: + newItem.ItemVNum = (int)ItemVnums.PARTNER_WEAPON_MELEE; + break; + default: + session.SendShopEndPacket(ShopEndType.Npc); + return; + } + + break; + default: + session.SendShopEndPacket(ShopEndType.Npc); + return; + } + + if (newItem.Type == ItemInstanceType.WearableInstance) + { + newItem.EquipmentOptions?.Clear(); + newItem.OriginalItemVnum = item.ItemInstance.GameItem.Id; + newItem.BoundCharacterId = null; + } + + await session.RemoveItemFromInventory((short)donaVNum, itemToTransform.GameItem.LevelMinimum); + session.PlayerEntity.Gold -= price; + InventoryItem getItem = session.PlayerEntity.GetItemBySlotAndType(e.Slot, e.InventoryType); + await session.RemoveItemFromInventory(item: getItem); + await session.AddNewItemToInventory(newItem); + session.RefreshGold(); + session.SendShopEndPacket(ShopEndType.Npc); + session.SendMsg(_languageService.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_PARTNER_ITEM_DONE, session.UserLanguage), MsgMessageType.Middle); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Items/SpeedBoosterEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Items/SpeedBoosterEventHandler.cs new file mode 100644 index 0000000..4f841bc --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Items/SpeedBoosterEventHandler.cs @@ -0,0 +1,160 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game; +using WingsEmu.Game._enum; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Networking; +using WingsEmu.Plugins.BasicImplementations.Vehicles; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Items; + +public class SpeedBoosterEventHandler : IAsyncEventProcessor +{ + private readonly IBuffFactory _buffFactory; + private readonly IMapManager _mapManager; + private readonly IVehicleConfigurationProvider _provider; + private readonly IRandomGenerator _randomGenerator; + + public SpeedBoosterEventHandler(IBuffFactory buffFactory, IMapManager mapManager, IVehicleConfigurationProvider provider, IRandomGenerator randomGenerator) + { + _buffFactory = buffFactory; + _mapManager = mapManager; + _provider = provider; + _randomGenerator = randomGenerator; + } + + public async Task HandleAsync(SpeedBoosterEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + + if (!session.PlayerEntity.IsOnVehicle) + { + return; + } + + if (session.PlayerEntity.HasBuff(BuffVnums.SPEED_BOOSTER)) + { + return; + } + + VehicleConfiguration vehicle = _provider.GetByMorph(session.PlayerEntity.Morph, session.PlayerEntity.Gender); + + if (vehicle?.VehicleBoostType == null) + { + return; + } + + await session.PlayerEntity.AddBuffAsync(_buffFactory.CreateBuff((short)BuffVnums.SPEED_BOOSTER, session.PlayerEntity, TimeSpan.FromSeconds(BuffVehicleDuration(session.PlayerEntity)))); + session.BroadcastEffectInRange(EffectType.SpeedBoost); + + foreach (VehicleBoost vehicleBoost in vehicle.VehicleBoostType) + { + switch (vehicleBoost.BoostType) + { + case BoostType.REMOVE_BAD_EFFECTS: + await session.PlayerEntity.RemoveNegativeBuffs(vehicleBoost.FirstValue ?? 0); + break; + case BoostType.REGENERATE_HP_MP: + if (!session.PlayerEntity.IsAlive()) + { + break; + } + + if (!vehicleBoost.FirstValue.HasValue) + { + break; + } + + int toIncrease = session.PlayerEntity.Level * vehicleBoost.FirstValue.Value; + + session.PlayerEntity.Hp += toIncrease; + session.PlayerEntity.BroadcastHeal(toIncrease); + if (session.PlayerEntity.Hp > session.PlayerEntity.MaxHp) + { + session.PlayerEntity.Hp = session.PlayerEntity.MaxHp; + } + + session.PlayerEntity.Mp += toIncrease; + if (session.PlayerEntity.Mp > session.PlayerEntity.MaxMp) + { + session.PlayerEntity.Mp = session.PlayerEntity.MaxMp; + } + + session.RefreshStat(); + break; + + case BoostType.TELEPORT_FORWARD: + session.SendGuriPacket(1, 5); + break; + case BoostType.RANDOM_TELEPORT_ON_MAP: + session.PlayerEntity.RandomMapTeleport = DateTime.UtcNow; + break; + case BoostType.CREATE_BUFF: + if (!vehicleBoost.FirstValue.HasValue) + { + break; + } + + if (!vehicleBoost.SecondValue.HasValue) + { + break; + } + + short chance = vehicleBoost.FirstValue.Value; + short buffId = vehicleBoost.SecondValue.Value; + + if (_randomGenerator.RandomNumber() > chance) + { + break; + } + + if (buffId == (short)BuffVnums.DAZZLE) + { + IEnumerable enemiesInRange = session.PlayerEntity.GetEnemiesInRange(session.PlayerEntity, 3); + foreach (IBattleEntity entity in enemiesInRange) + { + if (!entity.IsAlive()) + { + continue; + } + + await entity.AddBuffAsync(_buffFactory.CreateBuff(buffId, session.PlayerEntity)); + } + } + else + { + await session.PlayerEntity.AddBuffAsync(_buffFactory.CreateBuff(buffId, session.PlayerEntity)); + } + + break; + } + } + } + + private int BuffVehicleDuration(IPlayerEntity character) + { + VehicleConfiguration vehicle = _provider.GetByMorph(character.Morph, character.Gender); + VehicleBoost boost = vehicle?.VehicleBoostType.FirstOrDefault(x => x.BoostType == BoostType.INCREASE_SPEED); + + if (boost?.FirstValue == null || !boost.SecondValue.HasValue) + { + return 3; + } + + short speed = boost.FirstValue.Value; + short duration = boost.SecondValue.Value; + + character.VehicleSpeed += (byte)speed; + return duration; + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Maps/DisposeMapEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Maps/DisposeMapEventHandler.cs new file mode 100644 index 0000000..afcfc56 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Maps/DisposeMapEventHandler.cs @@ -0,0 +1,15 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Maps.Event; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Maps; + +public class DisposeMapEventHandler : IAsyncEventProcessor +{ + public Task HandleAsync(DisposeMapEvent e, CancellationToken cancellation) + { + e.Map.Destroy(); + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Maps/JoinMapEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Maps/JoinMapEventHandler.cs new file mode 100644 index 0000000..5f801a0 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Maps/JoinMapEventHandler.cs @@ -0,0 +1,541 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using PhoenixLib.Logging; +using WingsAPI.Communication.ServerApi.Protocol; +using WingsAPI.Game.Extensions.Families; +using WingsAPI.Game.Extensions.Groups; +using WingsAPI.Game.Extensions.MinilandExtensions; +using WingsAPI.Game.Extensions.PacketGeneration; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage.Configuration; +using WingsEmu.Game._packetHandling; +using WingsEmu.Game.Algorithm; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Entities.Extensions; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Extensions.Mates; +using WingsEmu.Game.Families; +using WingsEmu.Game.Groups; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Maps.Event; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Mates.Events; +using WingsEmu.Game.Miniland; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Networking.Broadcasting; +using WingsEmu.Game.Portals; +using WingsEmu.Game.Raids; +using WingsEmu.Game.TimeSpaces; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Maps; + +public class JoinMapEventHandler : IAsyncEventProcessor +{ + private readonly IBuffFactory _buffFactory; + private readonly ICharacterAlgorithm _characterAlgorithm; + private readonly IGameLanguageService _languageService; + private readonly IMapManager _mapManager; + private readonly IMinilandManager _minilandManager; + private readonly IPortalFactory _portalFactory; + private readonly IRankingManager _rankingManager; + private readonly IReputationConfiguration _reputationConfiguration; + private readonly SerializableGameServer _serializableGameServer; + private readonly ISpPartnerConfiguration _spPartnerConfiguration; + + public JoinMapEventHandler(ICharacterAlgorithm characterAlgorithm, IGameLanguageService languageService, ISpPartnerConfiguration spPartnerConfiguration, + IBuffFactory buffFactory, IMinilandManager minilandManager, IMapManager mapManager, + IReputationConfiguration reputationConfiguration, IPortalFactory portalFactory, IRankingManager rankingManager, + SerializableGameServer serializableGameServer) + { + _characterAlgorithm = characterAlgorithm; + _languageService = languageService; + _spPartnerConfiguration = spPartnerConfiguration; + _buffFactory = buffFactory; + _minilandManager = minilandManager; + _mapManager = mapManager; + _reputationConfiguration = reputationConfiguration; + _portalFactory = portalFactory; + _rankingManager = rankingManager; + _serializableGameServer = serializableGameServer; + } + + public async Task HandleAsync(JoinMapEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + IMapInstance mapInstance; + + if (e.JoinedMapInstance != default) + { + mapInstance = e.JoinedMapInstance; + } + else if (e.JoinedMapGuid != default) + { + mapInstance = _mapManager.GetMapInstance(e.JoinedMapGuid); + } + else + { + mapInstance = _mapManager.GetBaseMapInstanceByMapId(e.JoinedMapId); + } + + if (mapInstance == default) + { + /*if (_serializableGameServer.ChannelType == GameChannelType.ACT_4 && e.JoinedMapId is 145) + { + await session.EmitEventAsync(new PlayerReturnFromAct4Event()); + return; + }*/ + + Log.Error( + "Seems like the provided information for the Map to join doesn't equal to any known instance." + + $"JoinedMapInstance: {e.JoinedMapInstance?.Id}|{e.JoinedMapInstance?.MapId}" + + $"JoinedMapGuid: {e.JoinedMapGuid}" + + $"JoinedMapId: {e.JoinedMapId}" + + $"ChannelType: {_serializableGameServer.ChannelType} | ChannelId: {_serializableGameServer.ChannelId}", new ArgumentNullException("MapInstance")); + //session.ChangeToLastBaseMap(); + return; + } + + Stopwatch watch = null; + + if (session.DebugMode) + { + watch = Stopwatch.StartNew(); + } + + if (session.CurrentMapInstance != default) + { + await session.EmitEventAsync(new LeaveMapEvent()); + } + + ServerSideJoinMap(e, mapInstance); + IFamily family = session.PlayerEntity.Family; + + await SendJoinMapInformation(e, family); + await BroadcastAndReceiveJoinInformation(e, family); + await session.EmitEventAsync(new JoinMapEndEvent(mapInstance)); + + await session.PlayerEntity.CheckAct52Buff(_buffFactory); + await session.PlayerEntity.CheckAct4Buff(_buffFactory); + await session.PlayerEntity.CheckPvPBuff(); + await session.EmitEventAsync(new VehicleCheckMapSpeedEvent()); + session.PlayerEntity.ShadowAppears(false); + + session.PlayerEntity.LastMapChange = DateTime.UtcNow; + + if (session.DebugMode && watch != null) + { + watch.Stop(); + session.SendDebugMessage($"The map change took: {watch.ElapsedMilliseconds.ToString()}ms"); + } + + if (session.PlayerEntity.IsRaidLeader(session.PlayerEntity.Id)) + { + if (session.PlayerEntity.Raid?.Instance?.RaidSubInstances == null) + { + return; + } + + if (session.PlayerEntity.Raid.Instance.RaidSubInstances.TryGetValue(session.CurrentMapInstance.Id, out RaidSubInstance subInstance) && subInstance != null) + { + subInstance.IsDiscoveredByLeader = true; + } + } + } + + private void ServerSideJoinMap(JoinMapEvent e, IMapInstance mapInstance) + { + IClientSession session = e.Sender; + + if (mapInstance.HasMapFlag(MapFlags.IS_BASE_MAP) && mapInstance.MapInstanceType != MapInstanceType.TimeSpaceInstance) + { + session.PlayerEntity.MapId = mapInstance.MapId; + } + + if (mapInstance.HasMapFlag(MapFlags.IS_BASE_MAP) && mapInstance.MapInstanceType != MapInstanceType.TimeSpaceInstance && e.X != null && e.Y != null) + { + session.PlayerEntity.MapX = e.X.Value; + session.PlayerEntity.MapY = e.Y.Value; + } + + if (e.X != null && e.Y != null) + { + session.PlayerEntity.Position = new Position(e.X.Value, e.Y.Value); + } + + switch (mapInstance.MapInstanceType) + { + case MapInstanceType.Miniland: + Game.Configurations.Miniland.Miniland minilandConfiguration = _minilandManager.GetMinilandConfiguration(mapInstance); + session.PlayerEntity.Position = new Position(minilandConfiguration.ArrivalSerializablePosition.X, minilandConfiguration.ArrivalSerializablePosition.Y); + + if (mapInstance.Id == session.PlayerEntity.Miniland.Id) + { + foreach (IMateEntity mate in session.PlayerEntity.MateComponent.GetMates(x => !x.IsTeamMember)) + { + session.SendCondMate(mate); + if (mapInstance.GetMateById(mate.Id) != null) + { + continue; + } + + mapInstance.AddMate(mate); + } + } + + break; + case MapInstanceType.ArenaInstance: + session.PlayerEntity.ArenaImmunity = DateTime.UtcNow; + session.SendArenaStatistics(false, session.PlayerEntity.GetGroup()); + break; + case MapInstanceType.TimeSpaceInstance: + + List partners = session.PlayerEntity.TimeSpaceComponent.Partners; + if (!partners.Any()) + { + break; + } + + foreach (INpcEntity partner in partners) + { + partner.ChangeMapInstance(mapInstance); + mapInstance.AddNpc(partner); + partner.Position = session.PlayerEntity.Position; + } + + break; + default: + session.PlayerEntity.ArenaDeaths = 0; + session.PlayerEntity.ArenaKills = 0; + session.PlayerEntity.TimeSpaceComponent.Partners.Clear(); + break; + } + + session.PlayerEntity.MapInstanceId = mapInstance.Id; + session.CurrentMapInstance = session.PlayerEntity.MapInstance; + + session.CurrentMapInstance.RegisterSession(session); + + HashSet toRemovePlayer = new(); + foreach (Guid id in session.PlayerEntity.AggroedEntities) + { + IMonsterEntity monster = mapInstance.GetMonsterByUniqueId(id); + if (monster != null) + { + continue; + } + + toRemovePlayer.Add(id); + } + + foreach (Guid remove in toRemovePlayer) + { + session.PlayerEntity.AggroedEntities.Remove(remove); + } + + foreach (IMateEntity mate in session.PlayerEntity.MateComponent.TeamMembers()) + { + HashSet toRemoveMate = new(); + foreach (Guid id in mate.AggroedEntities) + { + IMonsterEntity monster = mapInstance.GetMonsterByUniqueId(id); + if (monster != null) + { + continue; + } + + toRemoveMate.Add(id); + } + + foreach (Guid remove in toRemoveMate) + { + mate.AggroedEntities.Remove(remove); + } + } + } + + private async Task SendJoinMapInformation(JoinMapEvent e, IFamily family) + { + IClientSession session = e.Sender; + + session.SendCInfoPacket(family, _reputationConfiguration, _rankingManager.TopReputation); + session.SendCModePacket(); + session.RefreshEquipment(); + session.RefreshLevel(_characterAlgorithm); + session.RefreshStat(); + session.SendAtPacket(); + + session.SendGidxPacket(family, _languageService); + session.SendCondPacket(); + session.SendCondPacket(); + session.SendTitInfoPacket(); + session.SendEqPacket(); + session.SendCMapPacket(true); + + if (session.CurrentMapInstance.MapId == (int)MapIds.HATUS_BOSS_MAP) + { + session.SendEmptyHatusHeads(); + } + + session.RefreshStatChar(); + if (session.CurrentMapInstance.MapInstanceType != MapInstanceType.TimeSpaceInstance) + { + session.SendRsfpPacket(); + } + + session.RefreshFairy(); + session.SendCharConstBuffEffect(); + session.RefreshZoom(); + + await SendPartyInfo(e); + + session.SendPacket(session.GenerateAct6EmptyPacket()); + + session.SendCondPacket(); + + session.SendPacket(session.CurrentMapInstance.GenerateMapDesignObjects()); + foreach (MapDesignObject mapObject in session.CurrentMapInstance.MapDesignObjects) + { + session.SendPacket(mapObject.GenerateEffect(false)); + } + + if (session.CurrentMapInstance.MapInstanceType == MapInstanceType.TimeSpaceInstance && session.PlayerEntity.TimeSpaceComponent.IsInTimeSpaceParty) + { + IClientSession[] timeSpaceMembers = session.PlayerEntity.TimeSpaceComponent.TimeSpace.Members.ToArray(); + foreach (IClientSession member in timeSpaceMembers) + { + member.SendPacket(e.JoinedMapInstance.GenerateRsfn(isVisit: true)); + } + + if (session.PlayerEntity.TimeSpaceComponent.TimeSpace.Started) + { + session.SendRsfpPacket(); + } + } + + session.SendPackets(session.CurrentMapInstance.GetEntitiesOnMapPackets()); + + foreach (IPortalEntity p in session.CurrentMapInstance.GenerateMinilandEntryPortals(session.PlayerEntity.Miniland, _portalFactory)) + { + session.SendPacket(p.GenerateGp()); + } + + session.SendTimeSpacePortals(); + + session.TrySendScalPacket(); + session.SendDancePacket(session.CurrentMapInstance.IsDance); + + if (session.PlayerEntity.ShowRaidDeathInfo) + { + session.PlayerEntity.ShowRaidDeathInfo = false; + session.SendInfo(session.GetLanguage(GameDialogKey.RAID_INFO_NO_LIVES_LEFT)); + } + + List partners = session.PlayerEntity.TimeSpaceComponent.Partners; + if (partners.Any()) + { + foreach (INpcEntity partner in partners) + { + session.PlayerEntity.MapInstance.Broadcast(x => partner.GenerateIn()); + session.SendMateControl(partner); + session.SendCondMate(partner); + } + } + + await MinilandInfoSend(e); + } + + private async Task MinilandInfoSend(PlayerEvent e) + { + if (e.Sender.CurrentMapInstance?.MapInstanceType != MapInstanceType.Miniland) + { + return; + } + + IClientSession session = e.Sender; + IClientSession minilandOwner = _minilandManager.GetSessionByMiniland(session.CurrentMapInstance); + if (minilandOwner == default) + { + return; + } + + if (session.PlayerEntity.Miniland.Id != minilandOwner.PlayerEntity.Miniland.Id) + { + if (minilandOwner.PlayerEntity.MinilandState == MinilandState.LOCK && !session.IsGameMaster() + || minilandOwner.PlayerEntity.MinilandState == MinilandState.PRIVATE && !session.PlayerEntity.IsFriend(minilandOwner.PlayerEntity.Id) + && !session.PlayerEntity.IsMarried(minilandOwner.PlayerEntity.Id) && !session.IsGameMaster()) + { + session.ChangeToLastBaseMap(); + session.SendMsg(_languageService.GetLanguage(GameDialogKey.MINILAND_SHOUTMESSAGE_CLOSED, session.UserLanguage), MsgMessageType.Middle); + return; + } + + int count = minilandOwner.PlayerEntity.Miniland.Sessions.Count(x => x.PlayerEntity.Id != minilandOwner.PlayerEntity.Id && !x.GmMode); + int capacity = _minilandManager.GetMinilandMaximumCapacity(minilandOwner.PlayerEntity.Id); + + if (count > capacity) + { + session.ChangeToLastBaseMap(); + session.SendMsg(_languageService.GetLanguage(GameDialogKey.MINILAND_SHOUTMESSAGE_FULL, session.UserLanguage), MsgMessageType.Middle); + return; + } + + session.SendMsg(minilandOwner.GetMinilandCleanMessage(_languageService), MsgMessageType.Middle); + + if (!session.IsGameMaster()) + { + await _minilandManager.IncreaseMinilandVisitCounter(minilandOwner.PlayerEntity.Id); + } + + session.SendMinilandPublicInformation(_minilandManager, _languageService); + minilandOwner.SendMinilandPrivateInformation(_minilandManager, _languageService); + } + else + { + session.SendMinilandPrivateInformation(_minilandManager, _languageService); + } + + foreach (IMateEntity mate in minilandOwner.PlayerEntity.MateComponent.GetMates()) + { + if (!mate.IsAlive() && session.PlayerEntity.Id == minilandOwner.PlayerEntity.Id) + { + await session.EmitEventAsync(new MateReviveEvent(mate, false)); + continue; + } + + if (!mate.IsTeamMember) + { + session.SendPacket(mate.GenerateIn(_languageService, session.UserLanguage, _spPartnerConfiguration)); + } + } + + long visitCount = await _minilandManager.GetMinilandVisitCounter(session.PlayerEntity.Id); + session.SendInformationChatMessage(_languageService.GetLanguageFormat(GameDialogKey.MINILAND_MESSAGE_VISITOR, session.UserLanguage, + session.PlayerEntity.LifetimeStats.TotalMinilandVisits, visitCount)); + } + + private async Task SendPartyInfo(PlayerEvent e) + { + IClientSession session = e.Sender; + + session.SendPstPackets(); + if (!session.PlayerEntity.IsOnVehicle && !session.PlayerEntity.CheatComponent.IsInvisible) + { + foreach (IMateEntity mate in session.PlayerEntity.MateComponent.TeamMembers()) + { + if (!mate.IsAlive()) + { + continue; + } + + if (mate.IsSitting) + { + await session.EmitEventAsync(new MateRestEvent + { + MateEntity = mate, + Force = true + }); + } + + mate.TeleportNearCharacter(); + session.SendPacket(mate.GenerateIn(_languageService, session.UserLanguage, _spPartnerConfiguration)); + session.TrySendScalPacket(mate); + session.SendMateControl(mate); + session.SendTargetConstBuffEffects(mate); + session.SendCondMate(mate); + } + } + + session.RefreshParty(_spPartnerConfiguration); + + if (!session.PlayerEntity.IsInGroup()) + { + return; + } + + PlayerGroup group = session.PlayerEntity.GetGroup(); + + foreach (IPlayerEntity member in group.Members) + { + if (session.PlayerEntity.Id == member.Id) + { + continue; + } + + member.Session.RefreshParty(_spPartnerConfiguration); + } + + session.CurrentMapInstance?.Broadcast(session.GeneratePidx(), new ExceptSessionBroadcast(session)); + } + + private async Task BroadcastAndReceiveJoinInformation(JoinMapEvent e, IFamily family) + { + IClientSession session = e.Sender; + foreach (IClientSession currentPlayerOnMap in session.CurrentMapInstance.Sessions) + { + if (currentPlayerOnMap.PlayerEntity.Id == session.PlayerEntity.Id) + { + continue; + } + + if (!currentPlayerOnMap.PlayerEntity.CheatComponent.IsInvisible) + { + await TargetedSendJoinInformation(session, currentPlayerOnMap, currentPlayerOnMap.PlayerEntity.Family, true); + } + + if (!session.PlayerEntity.CheatComponent.IsInvisible) + { + await TargetedSendJoinInformation(currentPlayerOnMap, session, family, false); + } + } + } + + private async Task TargetedSendJoinInformation(IClientSession receiverSession, IClientSession targetSession, IFamily family, bool showInEffect) + { + bool isAnonymous = targetSession.CurrentMapInstance.HasMapFlag(MapFlags.ACT_4) + && receiverSession.PlayerEntity.Faction != targetSession.PlayerEntity.Faction && !receiverSession.IsGameMaster(); + + receiverSession.SendTargetInPacket(targetSession, _reputationConfiguration, _rankingManager.TopReputation, isAnonymous, showInEffect); + if (!isAnonymous) + { + receiverSession.SendTargetGidxPacket(targetSession, family, _languageService); + } + + receiverSession.SendTargetTitInfoPacket(targetSession); + receiverSession.SendTargetConstBuffEffects(targetSession.PlayerEntity); + + if (targetSession.PlayerEntity.HasShopOpened) + { + receiverSession.SendPlayerShopTitle(targetSession); + receiverSession.SendPacket(targetSession.GeneratePlayerFlag((long)DialogVnums.SHOP_PLAYER)); + } + + if (targetSession.PlayerEntity.IsOnVehicle) + { + return; + } + + foreach (IMateEntity mate in targetSession.PlayerEntity.MateComponent.TeamMembers()) + { + string inPacket = mate.GenerateIn(_languageService, receiverSession.UserLanguage, _spPartnerConfiguration, isAnonymous); + receiverSession.SendPacket(inPacket); + receiverSession.SendTargetConstBuffEffects(mate); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Maps/LeaveMapEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Maps/LeaveMapEventHandler.cs new file mode 100644 index 0000000..4563975 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Maps/LeaveMapEventHandler.cs @@ -0,0 +1,160 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Entities.Extensions; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Extensions.Mates; +using WingsEmu.Game.Families.Event; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Maps.Event; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Networking.Broadcasting; +using WingsEmu.Game.Shops.Event; +using WingsEmu.Game.Skills; +using WingsEmu.Game.TimeSpaces; +using WingsEmu.Packets.Enums.Battle; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Maps; + +public class LeaveMapEventHandler : IAsyncEventProcessor +{ + private readonly IAsyncEventPipeline _asyncEventPipeline; + private readonly IGameLanguageService _languageService; + private readonly ISacrificeManager _sacrificeManager; + private readonly ISpyOutManager _spyOutManager; + private readonly ITeleportManager _teleportManager; + + public LeaveMapEventHandler(ISacrificeManager sacrificeManager, IGameLanguageService languageService, ISpyOutManager spyOutManager, ITeleportManager teleportManager, + IAsyncEventPipeline asyncEventPipeline) + { + _sacrificeManager = sacrificeManager; + _languageService = languageService; + _spyOutManager = spyOutManager; + _teleportManager = teleportManager; + _asyncEventPipeline = asyncEventPipeline; + } + + public async Task HandleAsync(LeaveMapEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + if (session.CurrentMapInstance == null) + { + return; + } + + IMapInstance mapInstance = session.CurrentMapInstance; + + await session.PlayerEntity.TryDespawnBomb(_asyncEventPipeline); + await session.TryDespawnTimeSpacePortal(); + session.CurrentMapInstance.UnregisterSession(session); + session.PlayerEntity.HitsByMonsters.Clear(); + + if (session.PlayerEntity.HasShopOpened) + { + await session.EmitEventAsync(new ShopPlayerCloseEvent()); + } + + await session.CloseExchange(); + + if (session.PlayerEntity.IsSitting) + { + session.PlayerEntity.IsSitting = false; + } + + if (session.PlayerEntity.IsCastingSkill) + { + session.SendCancelPacket(CancelType.InCombatMode); + session.PlayerEntity.RemoveCastingSkill(); + } + + session.PlayerEntity.IsWarehouseOpen = false; + session.PlayerEntity.IsPartnerWarehouseOpen = false; + if (session.PlayerEntity.IsFamilyWarehouseOpen || session.PlayerEntity.IsFamilyWarehouseLogsOpen) + { + await session.EmitEventAsync(new FamilyWarehouseCloseEvent()); + } + + session.PlayerEntity.IsCraftingItem = false; + + //Try removing shop + await session.EmitEventAsync(new ShopPlayerCloseEvent()); + + //Remove Sacrifice + IBattleEntity target = _sacrificeManager.GetTarget(e.Sender.PlayerEntity); + IBattleEntity caster = _sacrificeManager.GetCaster(e.Sender.PlayerEntity); + if (target != null) + { + await e.Sender.PlayerEntity.RemoveSacrifice(target, _sacrificeManager, _languageService); + } + + if (caster != null) + { + await caster.RemoveSacrifice(e.Sender.PlayerEntity, _sacrificeManager, _languageService); + } + + //Remove Teleport + _teleportManager.RemovePosition(session.PlayerEntity.Id); + if (session.PlayerEntity.HasBuff(BuffVnums.MEMORIAL)) + { + await session.PlayerEntity.RemoveBuffAsync(false, session.PlayerEntity.BuffComponent.GetBuff((short)BuffVnums.MEMORIAL)); + } + + //Remove Spy + if (_spyOutManager.ContainsSpyOut(session.PlayerEntity.Id)) + { + _spyOutManager.RemoveSpyOutSkill(session.PlayerEntity.Id); + session.SendObArPacket(); + } + + if (session.CurrentMapInstance.MapInstanceType == MapInstanceType.ArenaInstance) + { + session.SendArenaStatistics(true); + } + + if (session.CurrentMapInstance.MapInstanceType == MapInstanceType.TimeSpaceInstance && session.PlayerEntity.TimeSpaceComponent.IsInTimeSpaceParty) + { + if (session.CurrentMapInstance.Id != session.PlayerEntity.TimeSpaceComponent.TimeSpace.Instance.SpawnInstance.MapInstance.Id) + { + IClientSession[] timeSpaceMembers = session.PlayerEntity.TimeSpaceComponent.TimeSpace.Members.ToArray(); + foreach (IClientSession member in timeSpaceMembers) + { + member.SendPacket(mapInstance.GenerateRsfn(isVisit: true)); + } + } + + if (session.PlayerEntity.TimeSpaceComponent.Partners.Any()) + { + foreach (INpcEntity partner in session.PlayerEntity.TimeSpaceComponent.Partners) + { + session.CurrentMapInstance.RemoveNpc(partner); + session.Broadcast(partner.GenerateOut(), new ExceptSessionBroadcast(session)); + } + } + } + + session.SendCMapPacket(false); + session.SendMapOutPacket(); + if (!session.PlayerEntity.CheatComponent.IsInvisible) + { + session.BroadcastOut(new ExceptSessionBroadcast(session)); + } + + foreach (IMateEntity mate in session.PlayerEntity.MateComponent.TeamMembers()) + { + mapInstance.RemoveMate(mate); + if (session.PlayerEntity.CheatComponent.IsInvisible) + { + continue; + } + + session.Broadcast(mate.GenerateOut(), new ExceptSessionBroadcast(session)); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Maps/MapActivatedEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Maps/MapActivatedEventHandler.cs new file mode 100644 index 0000000..605b5cc --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Maps/MapActivatedEventHandler.cs @@ -0,0 +1,29 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game._ECS; +using WingsEmu.Game.Maps.Event; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Maps; + +public class MapActivatedEventHandler : IAsyncEventProcessor +{ + private readonly ITickManager _tickManager; + + public MapActivatedEventHandler(ITickManager tickManager) => _tickManager = tickManager; + + public async Task HandleAsync(MapActivatedEvent e, CancellationToken cancellation) => _tickManager.AddProcessable(e.MapInstance); +} + +public class MapDeactivatedEventHandler : IAsyncEventProcessor +{ + private readonly ITickManager _tickManager; + + public MapDeactivatedEventHandler(ITickManager tickManager) => _tickManager = tickManager; + + public async Task HandleAsync(MapDeactivatedEvent e, CancellationToken cancellation) => _tickManager.RemoveProcessable(e.MapInstance); +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Maps/PortalTriggerEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Maps/PortalTriggerEventHandler.cs new file mode 100644 index 0000000..5e77c16 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Maps/PortalTriggerEventHandler.cs @@ -0,0 +1,125 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Maps.Event; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Raids; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Maps; + +public class PortalTriggerEventHandler : IAsyncEventProcessor +{ + private readonly IMapManager _mapManager; + + public PortalTriggerEventHandler(IMapManager mapManager) => _mapManager = mapManager; + + public async Task HandleAsync(PortalTriggerEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + + switch (e.Portal.Type) + { + case PortalType.MapPortal: + case PortalType.Open: + case PortalType.Miniland: + case PortalType.Exit: + case PortalType.Effect: + case PortalType.ShopTeleport: + case PortalType.TSNormal when e.Sender.CurrentMapInstance.MapInstanceType != MapInstanceType.TimeSpaceInstance && + e.Sender.CurrentMapInstance.MapInstanceType != MapInstanceType.RaidInstance: + break; + default: + return; + } + + switch (session.CurrentMapInstance.MapInstanceType) + { + case MapInstanceType.RaidInstance: + if (e.Portal.Type != PortalType.Open) + { + break; + } + + RaidParty raidParty = session.PlayerEntity.Raid; + if (raidParty == null || session == raidParty.Leader) + { + break; + } + + if (e.Portal.DestinationMapInstance != null && raidParty.Leader.CurrentMapInstance.Id == e.Portal.DestinationMapInstance.Id) + { + break; + } + + if (e.Portal.DestinationMapInstance != null && raidParty.Instance?.RaidSubInstances != null) + { + if (raidParty.Instance.RaidSubInstances.TryGetValue(e.Portal.DestinationMapInstance.Id, out RaidSubInstance subInstance) + && subInstance is { IsDiscoveredByLeader: true }) + // avoid reteleporting leader on already discovered maps + { + break; + } + } + + await ProcessTeleport(raidParty.Leader, e.Portal); + + break; + case MapInstanceType.Miniland: + case MapInstanceType.ArenaInstance: + case MapInstanceType.EventGameInstance: + session.ChangeToLastBaseMap(); + return; + case MapInstanceType.TimeSpaceInstance: + case MapInstanceType.Act4Instance: + return; + } + + if (session.CurrentMapInstance.HasMapFlag(MapFlags.ACT_4)) + { + return; + } + + if (e.Portal.DestinationMapInstance == null) + { + return; + } + + e.Sender.PlayerEntity.LastPortal = DateTime.UtcNow; + await ProcessTeleport(session, e.Portal); + } + + private async Task ProcessTeleport(IClientSession session, IPortalEntity portal) + { + if (!session.CurrentMapInstance.HasMapFlag(MapFlags.IS_BASE_MAP) && portal.DestinationMapInstance != null && portal.DestinationMapInstance.HasMapFlag(MapFlags.IS_BASE_MAP)) + { + session.ChangeToLastBaseMap(); + return; + } + + if (portal.DestinationMapInstance?.Id == session.PlayerEntity.Miniland.Id) + { + session.ChangeMap(session.PlayerEntity.Miniland, portal.DestinationX, portal.DestinationY); + return; + } + + if (portal.DestinationX == -1 && portal.DestinationY == -1) + { + await _mapManager.TeleportOnRandomPlaceInMapAsync(session, portal.DestinationMapInstance); + return; + } + + if (portal.DestinationMapInstance?.Id == session.PlayerEntity.MapInstanceId && portal.DestinationX.HasValue && portal.DestinationY.HasValue) + { + session.PlayerEntity.TeleportOnMap(portal.DestinationX.Value, portal.DestinationY.Value, true); + return; + } + + session.ChangeMap(portal.DestinationMapInstance, portal.DestinationX, portal.DestinationY); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Maps/RemovePortalEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Maps/RemovePortalEventHandler.cs new file mode 100644 index 0000000..7d9b255 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Maps/RemovePortalEventHandler.cs @@ -0,0 +1,12 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.PacketGeneration; +using WingsEmu.Game.Maps.Event; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Maps; + +public class RemovePortalEventHandler : IAsyncEventProcessor +{ + public async Task HandleAsync(PortalRemoveEvent e, CancellationToken cancellation) => e.Portal.MapInstance.DeletePortal(e.Portal); +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Maps/SpawnPortalEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Maps/SpawnPortalEventHandler.cs new file mode 100644 index 0000000..8789ca5 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Maps/SpawnPortalEventHandler.cs @@ -0,0 +1,12 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.PacketGeneration; +using WingsEmu.Game.Maps.Event; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Maps; + +public class SpawnPortalEventHandler : IAsyncEventProcessor +{ + public async Task HandleAsync(SpawnPortalEvent e, CancellationToken cancellation) => e.Map.AddPortalToMap(e.Portal); +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Mates/MateBackToMinilandEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Mates/MateBackToMinilandEventHandler.cs new file mode 100644 index 0000000..688d6cc --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Mates/MateBackToMinilandEventHandler.cs @@ -0,0 +1,33 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Mates.Events; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Mates; + +public class MateBackToMinilandEventHandler : IAsyncEventProcessor +{ + private readonly MateReviveEventHandler _mateReviveEventHandler; + + public MateBackToMinilandEventHandler(MateReviveEventHandler mateReviveEventHandler) => _mateReviveEventHandler = mateReviveEventHandler; + + public async Task HandleAsync(MateBackToMinilandEvent e, CancellationToken cancellation) + { + if (!e.MateEntity.IsTeamMember || e.Sender.PlayerEntity?.MapInstance == null) + { + return; + } + + if (!_mateReviveEventHandler.BasicUnregisteringForMates(e.MateEntity, true, e.ExpectedGuid)) + { + return; + } + + await e.Sender.EmitEventAsync(new MateLeaveTeamEvent + { + MateEntity = e.MateEntity + }); + e.MateEntity.Hp = 1; + e.MateEntity.Mp = 1; + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Mates/MateDeathEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Mates/MateDeathEventHandler.cs new file mode 100644 index 0000000..47eb2aa --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Mates/MateDeathEventHandler.cs @@ -0,0 +1,159 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.Game._i18n; +using WingsEmu.Game._packetHandling; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Extensions.Mates; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Mates.Events; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Mates; + +public class MateDeathEventHandler : IAsyncEventProcessor +{ + private readonly IGameLanguageService _gameLanguage; + private readonly IItemsManager _itemManager; + private readonly MateRevivalConfiguration _mateRevivalConfiguration; + private readonly GameMinMaxConfiguration _minMaxConfiguration; + private readonly IRevivalManager _revivalManager; + + public MateDeathEventHandler(IItemsManager itemManager, IGameLanguageService gameLanguage, GameRevivalConfiguration gameRevivalConfiguration, IRevivalManager revivalManager, + GameMinMaxConfiguration minMaxConfiguration) + { + _gameLanguage = gameLanguage; + _itemManager = itemManager; + _mateRevivalConfiguration = gameRevivalConfiguration.MateRevivalConfiguration; + _revivalManager = revivalManager; + _minMaxConfiguration = minMaxConfiguration; + } + + public async Task HandleAsync(MateDeathEvent e, CancellationToken cancellation) + { + if (e.MateEntity.IsAlive()) + { + return; + } + + Guid id = _revivalManager.RegisterRevival(e.MateEntity.Id); + + if (id == default) + { + return; + } + + e.MateEntity.LastDeath = DateTime.UtcNow; + e.MateEntity.Killer = e.Killer; + await e.MateEntity.RemoveAllBuffsAsync(true); + + await MateFateDictator(e, id); + + e.Sender.SendPetInfo(e.MateEntity, _gameLanguage); + e.Sender.SendMateLife(e.MateEntity); + } + + private async Task MateFateDictator(MateDeathEvent e, Guid expectedGuid) + { + if (e.MateEntity.MapInstance?.Id == e.Sender.PlayerEntity.Miniland?.Id) + { + await e.Sender.EmitEventAsync(new MateReviveEvent(e.MateEntity, false, expectedGuid)); + return; + } + + if (e.MateEntity.MateType == MateType.Pet ? e.Sender.PlayerEntity.IsPetAutoRelive : e.Sender.PlayerEntity.IsPartnerAutoRelive) + { + if (await MateTrySaveByGuardian(e.MateEntity)) + { + e.MateEntity.AddLoyalty(_mateRevivalConfiguration.LoyaltyDeathPenalizationAmount, _minMaxConfiguration, _gameLanguage); + _revivalManager.TryUnregisterRevival(e.MateEntity.Id); + e.MateEntity.SpawnMateByGuardian = DateTime.UtcNow; + return; + } + + e.MateEntity.RemoveLoyalty(_mateRevivalConfiguration.LoyaltyDeathPenalizationAmount, _minMaxConfiguration, _gameLanguage); + + if (MateTryDelayedRevival(e)) + { + e.MateEntity.UpdateRevival(DateTime.UtcNow + _mateRevivalConfiguration.DelayedRevivalDelay, true); + GameDialogKey gameDialogKey = e.MateEntity.MateType == MateType.Pet ? GameDialogKey.PET_SHOUTMESSAGE_WILL_BE_BACK : GameDialogKey.PARTNER_SHOUTMESSAGE_WILL_BE_BACK; + e.Sender.SendMsg(_gameLanguage.GetLanguageFormat(gameDialogKey, e.Sender.UserLanguage, e.MateEntity.MateType), MsgMessageType.Middle); + return; + } + + e.Sender.SendMsg(_gameLanguage.GetLanguageFormat(GameDialogKey.INVENTORY_SHOUTMESSAGE_NOT_ENOUGH_ITEMS, e.Sender.UserLanguage, + _mateRevivalConfiguration.DelayedRevivalPenalizationSaverAmount, + _gameLanguage.GetItemName(_itemManager.GetItem(_mateRevivalConfiguration.DelayedRevivalPenalizationSaver), e.Sender)), + MsgMessageType.Middle); + } + + GameDialogKey key = e.MateEntity.MateType == MateType.Pet ? GameDialogKey.PET_SHOUTMESSAGE_WENT_BACK_TO_MINILAND : GameDialogKey.PARTNER_SHOUTMESSAGE_WENT_BACK_TO_MINILAND; + await e.Sender.EmitEventAsync(new MateBackToMinilandEvent(e.MateEntity, expectedGuid)); + e.Sender.SendMsg(_gameLanguage.GetLanguage(key, e.Sender.UserLanguage), MsgMessageType.Middle); + } + + private bool MateTryDelayedRevival(PlayerEvent e) + { + if (!e.Sender.PlayerEntity.HasItem(_mateRevivalConfiguration.DelayedRevivalPenalizationSaver, (short)_mateRevivalConfiguration.DelayedRevivalPenalizationSaverAmount)) + { + return false; + } + + e.Sender.RemoveItemFromInventory(_mateRevivalConfiguration.DelayedRevivalPenalizationSaver, (short)_mateRevivalConfiguration.DelayedRevivalPenalizationSaverAmount); + return true; + } + + private async Task MateTrySaveByGuardian(IMateEntity mateEntity) + { + IPlayerEntity owner = mateEntity.Owner; + if (mateEntity.MateType == MateType.Pet ? !owner.IsPetAutoRelive : !owner.IsPartnerAutoRelive) + { + return false; + } + + if (mateEntity.MapInstance.MapInstanceType == MapInstanceType.RainbowBattle) + { + return false; + } + + bool shouldSave = false; + + List itemNeeded = mateEntity.MateType == MateType.Pet + ? _mateRevivalConfiguration.MateInstantRevivalPenalizationSaver + : _mateRevivalConfiguration.PartnerInstantRevivalPenalizationSaver; + + foreach (int item in itemNeeded) + { + InventoryItem getItem = owner.GetFirstItemByVnum(item); + if (getItem == null) + { + continue; + } + + await owner.Session.RemoveItemFromInventory(item: getItem); + shouldSave = true; + break; + } + + if (!shouldSave) + { + return false; + } + + GameDialogKey gameDialogKey = mateEntity.MateType == MateType.Pet ? GameDialogKey.PET_SHOUTMESSGE_SAVED_BY_SAVER : GameDialogKey.PARTNER_SHOUTMESSAGE_SAVED_BY_SAVER; + owner.Session.SendMsg(owner.Session.GetLanguage(gameDialogKey), MsgMessageType.Middle); + + return true; + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Mates/MateHealEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Mates/MateHealEventHandler.cs new file mode 100644 index 0000000..506580b --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Mates/MateHealEventHandler.cs @@ -0,0 +1,36 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Extensions.Mates; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Mates.Events; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Mates; + +public class MateHealEventHandler : IAsyncEventProcessor +{ + public async Task HandleAsync(MateHealEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + IMateEntity mateEntity = e.MateEntity; + + if (!mateEntity.IsAlive()) + { + return; + } + + int mateMaxHp = mateEntity.MaxHp; + int mateMaxMp = mateEntity.MaxMp; + + int mateHpHeal = mateEntity.Hp + e.HpHeal > mateMaxHp ? mateMaxHp - mateEntity.Hp : e.HpHeal; + int mateMpHeal = mateEntity.Mp + e.MpHeal > mateMaxMp ? mateMaxMp - mateEntity.Mp : e.MpHeal; + + mateEntity.Hp += mateHpHeal; + mateEntity.Mp += mateMpHeal; + + session.CurrentMapInstance?.Broadcast(mateEntity.GenerateRc(mateHpHeal)); + session.SendPacket(mateEntity.GenerateStatInfo()); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Mates/MateInitializeEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Mates/MateInitializeEventHandler.cs new file mode 100644 index 0000000..7d5a56f --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Mates/MateInitializeEventHandler.cs @@ -0,0 +1,43 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Algorithm; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Extensions.Mates; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Mates.Events; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Mates; + +public class MateInitializeEventHandler : IAsyncEventProcessor +{ + private readonly IBattleEntityAlgorithmService _algorithm; + + public MateInitializeEventHandler(IBattleEntityAlgorithmService algorithm) => _algorithm = algorithm; + + public async Task HandleAsync(MateInitializeEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + IMateEntity mateEntity = e.MateEntity; + + mateEntity.Initialize(); + mateEntity.RefreshStatistics(); + + if (e.IsOnCharacterEnter) + { + session.PlayerEntity.MateComponent.AddMate(mateEntity); + return; + } + + mateEntity.PetSlot = session.PlayerEntity.GetFreeMateSlot(mateEntity.MateType == MateType.Partner); + session.PlayerEntity.MateComponent.AddMate(mateEntity); + mateEntity.RefreshMaxHpMp(_algorithm); + mateEntity.ChangePosition(new Position(session.PlayerEntity.PositionX, session.PlayerEntity.PositionY)); + session.SendPClearPacket(); + session.SendScpPackets(); + session.SendScnPackets(); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Mates/MateJoinInMinilandEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Mates/MateJoinInMinilandEventHandler.cs new file mode 100644 index 0000000..d6d6fee --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Mates/MateJoinInMinilandEventHandler.cs @@ -0,0 +1,94 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.Groups; +using WingsAPI.Game.Extensions.Quicklist; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage.Configuration; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Mates.Events; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Mates; + +public class MateJoinInMinilandEventHandler : IAsyncEventProcessor +{ + private readonly IBuffFactory _buffFactory; + private readonly IGameLanguageService _gameLanguage; + private readonly IMateBuffConfigsContainer _mateBuffConfigsContainer; + private readonly ISpPartnerConfiguration _spPartnerConfiguration; + + public MateJoinInMinilandEventHandler(IBuffFactory buffFactory, IMateBuffConfigsContainer mateBuffConfigsContainer, IGameLanguageService gameLanguage, + ISpPartnerConfiguration spPartnerConfiguration) + { + _buffFactory = buffFactory; + _mateBuffConfigsContainer = mateBuffConfigsContainer; + _gameLanguage = gameLanguage; + _spPartnerConfiguration = spPartnerConfiguration; + } + + public async Task HandleAsync(MateJoinInMinilandEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + IMateEntity mateEntity = e.MateEntity; + + if (mateEntity == null) + { + return; + } + + if (mateEntity.IsTeamMember) + { + return; + } + + if (!session.IsGameMaster()) + { + if (mateEntity.Level > session.PlayerEntity.Level) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.PET_SHOUTMESSAGE_HIGHER_LEVEL, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (session.PlayerEntity.GetDignityIco() == 6) + { + session.SendMsg(session.GetLanguage(GameDialogKey.PET_SHOUTMESSAGE_DIGNITY_LOW), MsgMessageType.Middle); + return; + } + } + + IMateEntity teammate = session.PlayerEntity.MateComponent.GetMate(s => s.MateType == mateEntity.MateType && s.IsTeamMember); + if (teammate != null) + { + await session.EmitEventAsync(new MateStayInsideMinilandEvent { MateEntity = teammate }); + } + + mateEntity.IsTeamMember = true; + mateEntity.ChangePosition(new Position(mateEntity.MinilandX, mateEntity.MinilandY)); + + session.SendScpStcPacket(); + + switch (mateEntity.MateType) + { + case MateType.Pet: + await session.AddPetBuff(mateEntity, _mateBuffConfigsContainer, _buffFactory); + break; + case MateType.Partner when mateEntity.MonsterSkills?.Count != 0: + session.RefreshSkillList(); + session.RefreshQuicklist(); + break; + } + + session.SendScnPackets(); + session.SendScpPackets(); + session.SendPClearPacket(); + session.SendScnPackets(); + session.SendScpPackets(); + session.RefreshParty(_spPartnerConfiguration); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Mates/MateJoinTeamEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Mates/MateJoinTeamEventHandler.cs new file mode 100644 index 0000000..32cebf3 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Mates/MateJoinTeamEventHandler.cs @@ -0,0 +1,103 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.Groups; +using WingsAPI.Game.Extensions.Quicklist; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage.Configuration; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Extensions.Mates; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Mates.Events; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Mates; + +public class MateJoinTeamEventHandler : IAsyncEventProcessor +{ + private readonly IBuffFactory _buffFactory; + private readonly IGameLanguageService _gameLang; + private readonly IMateBuffConfigsContainer _mateBuffConfigsContainer; + private readonly ISpPartnerConfiguration _spPartnerConfiguration; + + public MateJoinTeamEventHandler(IGameLanguageService gameLang, IBuffFactory buffFactory, ISpPartnerConfiguration spPartnerConfiguration, IMateBuffConfigsContainer mateBuffConfigsContainer) + { + _gameLang = gameLang; + _buffFactory = buffFactory; + _spPartnerConfiguration = spPartnerConfiguration; + _mateBuffConfigsContainer = mateBuffConfigsContainer; + } + + public async Task HandleAsync(MateJoinTeamEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + IMateEntity mateEntity = e.MateEntity; + + if (mateEntity == null) + { + return; + } + + if (!e.IsOnCharacterEnter) + { + if (mateEntity.IsTeamMember) + { + return; + } + } + + if (!session.IsGameMaster()) + { + if (mateEntity.Level > session.PlayerEntity.Level) + { + session.SendMsg(_gameLang.GetLanguage(GameDialogKey.PET_SHOUTMESSAGE_HIGHER_LEVEL, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (session.PlayerEntity.GetDignityIco() == 6) + { + session.SendMsg(session.GetLanguage(GameDialogKey.PET_SHOUTMESSAGE_DIGNITY_LOW), MsgMessageType.Middle); + return; + } + } + + mateEntity.IsTeamMember = true; + if (e.IsOnCharacterEnter) + { + mateEntity.TeleportNearCharacter(); + } + else if (!e.IsNewCreated) + { + mateEntity.ChangePosition(new Position(mateEntity.MapX, mateEntity.MapY)); + } + + session.SendScpStcPacket(); + + switch (mateEntity.MateType) + { + case MateType.Pet: + await session.AddPetBuff(mateEntity, _mateBuffConfigsContainer, _buffFactory); + break; + case MateType.Partner when mateEntity.MonsterSkills?.Count != 0: + session.RefreshSkillList(); + session.RefreshQuicklist(); + break; + } + + if (!e.IsOnCharacterEnter) + { + session.PlayerEntity.MapInstance.AddMate(e.MateEntity); + e.MateEntity.MapInstance.Broadcast(s => e.MateEntity.GenerateIn(_gameLang, s.UserLanguage, _spPartnerConfiguration)); + } + + session.SendCondMate(mateEntity); + session.SendPClearPacket(); + session.SendScnPackets(); + session.SendScpPackets(); + session.RefreshParty(_spPartnerConfiguration); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Mates/MateLeaveTeamEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Mates/MateLeaveTeamEventHandler.cs new file mode 100644 index 0000000..b67f998 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Mates/MateLeaveTeamEventHandler.cs @@ -0,0 +1,103 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.Groups; +using WingsAPI.Game.Extensions.Quicklist; +using WingsEmu.Game; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage.Configuration; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Extensions.Mates; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Mates.Events; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Mates; + +public class MateLeaveTeamEventHandler : IAsyncEventProcessor +{ + private readonly IDelayManager _delayManager; + private readonly IGameLanguageService _langService; + private readonly IMateBuffConfigsContainer _mateBuffConfigsContainer; + private readonly IRandomGenerator _randomGenerator; + private readonly ISpPartnerConfiguration _spPartnerConfiguration; + + public MateLeaveTeamEventHandler(IGameLanguageService gameLanguageService, IDelayManager delayManager, + ISpPartnerConfiguration spPartnerConfiguration, IRandomGenerator randomGenerator, IMateBuffConfigsContainer mateBuffConfigsContainer) + { + _langService = gameLanguageService; + _delayManager = delayManager; + _spPartnerConfiguration = spPartnerConfiguration; + _randomGenerator = randomGenerator; + _mateBuffConfigsContainer = mateBuffConfigsContainer; + } + + public async Task HandleAsync(MateLeaveTeamEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + IMateEntity mateEntity = e.MateEntity; + + if (mateEntity == null) + { + return; + } + + if (mateEntity.Owner.Id != e.Sender.PlayerEntity.Id) + { + return; + } + + bool isInMiniland = e.MateEntity.MapInstance?.Id == e.MateEntity.Owner.Miniland.Id; + + if (isInMiniland) + { + await session.EmitEventAsync(new MateStayInsideMinilandEvent + { + MateEntity = mateEntity + }); + return; + } + + session.PlayerEntity.MapInstance?.RemoveMate(mateEntity); + mateEntity.BroadcastMateOut(); + Position cell = mateEntity.NewMinilandMapCell(_randomGenerator); + mateEntity.MinilandX = cell.X; + mateEntity.MinilandY = cell.Y; + mateEntity.ChangePosition(cell); + + mateEntity.IsTeamMember = false; + + if (session.PlayerEntity.Miniland.GetBattleEntity(VisualType.Npc, mateEntity.Id) == null) + { + session.PlayerEntity.Miniland.AddMate(mateEntity); + } + + await mateEntity.RemovePartnerSp(); + mateEntity.RefreshPartnerSkills(); + await mateEntity.RemoveAllBuffsAsync(true); + session.RemovePetBuffs(mateEntity, _mateBuffConfigsContainer); + + session.SendScpPackets(); + session.SendScnPackets(); + + session.RefreshParty(_spPartnerConfiguration); + + if (mateEntity.MateType == MateType.Partner && mateEntity.MonsterSkills?.Count != 0) + { + session.RefreshSkillList(); + session.RefreshQuicklist(); + } + + if (!session.PlayerEntity.Miniland.Sessions.Any()) + { + return; + } + + e.MateEntity.MapInstance?.Broadcast(s => e.MateEntity.GenerateIn(_langService, s.UserLanguage, _spPartnerConfiguration)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Mates/MateProcessExperienceEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Mates/MateProcessExperienceEventHandler.cs new file mode 100644 index 0000000..5416f80 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Mates/MateProcessExperienceEventHandler.cs @@ -0,0 +1,117 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.Groups; +using WingsAPI.Packets.Enums; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage.Configuration; +using WingsEmu.Game.Algorithm; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Extensions.Mates; +using WingsEmu.Game.Helpers; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Mates.Events; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Mates; + +public class MateProcessExperienceEventHandler : IAsyncEventProcessor +{ + private readonly ICharacterAlgorithm _characterAlgorithm; + private readonly IGameLanguageService _gameLanguage; + private readonly IServerManager _serverManager; + private readonly ISpPartnerConfiguration _spPartnerConfiguration; + + public MateProcessExperienceEventHandler(IServerManager serverManager, IGameLanguageService gameLanguage, ICharacterAlgorithm characterAlgorithm, ISpPartnerConfiguration spPartnerConfiguration) + { + _serverManager = serverManager; + _gameLanguage = gameLanguage; + _characterAlgorithm = characterAlgorithm; + _spPartnerConfiguration = spPartnerConfiguration; + } + + public async Task HandleAsync(MateProcessExperienceEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + IPlayerEntity character = session.PlayerEntity; + + if (character.IsOnVehicle) + { + return; + } + + IMateEntity mate = e.MateEntity; + + if (!mate.IsAlive()) + { + return; + } + + long mateXp = _characterAlgorithm.GetLevelXp(mate.Level, true, mate.MateType); + long experience = e.Experience; + + if (mate.Level >= _serverManager.MaxMateLevel) + { + mate.Level = (byte)_serverManager.MaxMateLevel; + mate.Experience = 0; + return; + } + + if (mate.Level > character.Level) + { + return; + } + + if (mate.Level == character.Level && mate.Experience >= mateXp) + { + mate.Experience = mateXp; + session.SendPetInfo(mate, _gameLanguage); + return; + } + + mate.Experience += experience; + session.SendPetInfo(mate, _gameLanguage); + + if (mate.Experience < mateXp) + { + return; + } + + if (mate.Level + 1 > character.Level) + { + mate.Experience = mateXp; + session.SendPetInfo(mate, _gameLanguage); + return; + } + + int loyalty = mate.Loyalty + 100 > 1000 ? 1000 - mate.Loyalty : 100; + mate.Loyalty += (short)loyalty; + mate.Experience -= mateXp; + mate.Level++; + mate.RefreshStatistics(); + session.RefreshParty(_spPartnerConfiguration); + mate.Hp = mate.MaxHp; + mate.Mp = mate.MaxMp; + mate.BroadcastEffectInRange(EffectType.NormalLevelUp); + mate.BroadcastEffectInRange(EffectType.NormalLevelUpSubEffect); + string name = mate.MateName == mate.Name ? _gameLanguage.GetLanguage(GameDataType.NpcMonster, mate.Name, session.UserLanguage) : mate.MateName; + GameDialogKey dialogKey = mate.MateType == MateType.Partner ? GameDialogKey.PARTNER_SHOUTMESSAGE_LEVEL_UP : GameDialogKey.PET_SHOUTMESSAGE_LEVEL_UP; + session.SendMsg(session.GetLanguageFormat(dialogKey, name), MsgMessageType.Middle); + session.SendPetInfo(mate, _gameLanguage); + + await session.EmitEventAsync(new LevelUpMateEvent + { + Level = mate.Level, + LevelUpType = MateLevelUpType.Normal, + Location = new Location(mate.MapInstance.MapId, mate.MapX, mate.MapY), + NosMateMonsterVnum = mate.NpcMonsterVNum + }); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Mates/MateRemoveEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Mates/MateRemoveEventHandler.cs new file mode 100644 index 0000000..1c84a5e --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Mates/MateRemoveEventHandler.cs @@ -0,0 +1,31 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Entities.Extensions; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Mates.Events; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Mates; + +public class MateRemoveEventHandler : IAsyncEventProcessor +{ + private readonly IMateTransportFactory _mateTransportFactory; + + public MateRemoveEventHandler(IMateTransportFactory mateTransportFactory) => _mateTransportFactory = mateTransportFactory; + + public async Task HandleAsync(MateRemoveEvent e, CancellationToken cancellation) + { + IMateEntity mateEntity = e.MateEntity; + IClientSession session = e.Sender; + + session.PlayerEntity.MateComponent.RemoveMate(mateEntity); + + session.SendPClearPacket(); + session.SendScpPackets(); + session.SendScnPackets(); + session.Broadcast(mateEntity.GenerateOut()); + session.CurrentMapInstance.RemoveMate(mateEntity); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Mates/MateRestEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Mates/MateRestEventHandler.cs new file mode 100644 index 0000000..0d78bec --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Mates/MateRestEventHandler.cs @@ -0,0 +1,41 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Extensions.Mates; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Mates.Events; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Mates; + +public class MateRestEventHandler : IAsyncEventProcessor +{ + public async Task HandleAsync(MateRestEvent e, CancellationToken cancellation) + { + IMateEntity mateEntity = e.MateEntity; + + if (mateEntity == null) + { + return; + } + + if (mateEntity.LastSkillUse.AddSeconds(4) > DateTime.UtcNow && !e.Force) + { + return; + } + + if (mateEntity.LastDefence.AddSeconds(4) > DateTime.UtcNow && !e.Force) + { + return; + } + + if (!e.Sender.HasCurrentMapInstance) + { + return; + } + + mateEntity.IsSitting = e.Rest; + e.Sender.Broadcast(mateEntity.GenerateRest()); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Mates/MateReviveEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Mates/MateReviveEventHandler.cs new file mode 100644 index 0000000..df1d125 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Mates/MateReviveEventHandler.cs @@ -0,0 +1,89 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.Families; +using WingsAPI.Game.Extensions.Groups; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage.Configuration; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Extensions.Mates; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Mates.Events; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Networking.Broadcasting; +using WingsEmu.Game.RainbowBattle; +using WingsEmu.Game.Revival; +using WingsEmu.Plugins.BasicImplementations.Revival; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Mates; + +public class MateReviveEventHandler : IAsyncEventProcessor +{ + private readonly RevivalEventBaseHandler _eventBaseHandler; + private readonly IGameLanguageService _gameLanguage; + private readonly IGameLanguageService _language; + private readonly IRankingManager _rankingManager; + private readonly IReputationConfiguration _reputationConfiguration; + private readonly ISpPartnerConfiguration _spPartner; + + public MateReviveEventHandler(ISpPartnerConfiguration spPartner, IGameLanguageService language, + RevivalEventBaseHandler eventBaseHandler, IRankingManager rankingManager, IReputationConfiguration reputationConfiguration, IGameLanguageService gameLanguage) + { + _spPartner = spPartner; + _language = language; + _eventBaseHandler = eventBaseHandler; + _rankingManager = rankingManager; + _reputationConfiguration = reputationConfiguration; + _gameLanguage = gameLanguage; + } + + public async Task HandleAsync(MateReviveEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + IMateEntity mateEntity = e.MateEntity; + + if (mateEntity.IsAlive()) + { + return; + } + + if (!BasicUnregisteringForMates(mateEntity, e.Delayed, e.ExpectedGuid)) + { + return; + } + + mateEntity.Hp = mateEntity.MaxHp / 2; + mateEntity.Mp = mateEntity.MaxMp / 2; + session.SendMateLife(mateEntity); + + if (session.PlayerEntity.IsOnVehicle) + { + return; + } + + // yes, twice + session.CurrentMapInstance.Broadcast(x => mateEntity.GenerateOut()); + session.CurrentMapInstance.Broadcast(x => mateEntity.GenerateOut()); + + mateEntity.TeleportNearCharacter(); + + if (mateEntity.MapInstance.MapInstanceType == MapInstanceType.RainbowBattle) + { + session.BroadcastIn(_reputationConfiguration, _rankingManager.TopReputation, new ExceptSessionBroadcast(session)); + session.BroadcastGidx(session.PlayerEntity.Family, _gameLanguage); + session.BroadcastRainbowTeamType(); + } + + session.CurrentMapInstance.Broadcast(s => mateEntity.GenerateIn(_language, s.UserLanguage, _spPartner)); + session.SendCondMate(mateEntity); + session.RefreshParty(_spPartner); + } + + public bool BasicUnregisteringForMates(IMateEntity mateEntity, bool delayed, Guid expectedGuid) + => mateEntity.Owner.Session.IsConnected && _eventBaseHandler.BasicUnregistering(mateEntity.Id, delayed ? ForcedType.Forced : ForcedType.NoForced, expectedGuid); +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Mates/MateSpTransformEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Mates/MateSpTransformEventHandler.cs new file mode 100644 index 0000000..abec3a2 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Mates/MateSpTransformEventHandler.cs @@ -0,0 +1,114 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.Groups; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage.Configuration; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Extensions.Mates; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Mates.Events; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Skills; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Mates; + +public class MateSpTransformEventHandler : IAsyncEventProcessor +{ + private readonly IBuffFactory _buffFactory; + private readonly IGameLanguageService _gameLanguage; + private readonly ISkillsManager _skillsManager; + private readonly ISpPartnerConfiguration _spPartner; + + public MateSpTransformEventHandler(IGameLanguageService gameLanguage, ISkillsManager skillsManager, IBuffFactory buffFactory, ISpPartnerConfiguration spPartner) + { + _gameLanguage = gameLanguage; + _skillsManager = skillsManager; + _buffFactory = buffFactory; + _spPartner = spPartner; + } + + public async Task HandleAsync(MateSpTransformEvent e, CancellationToken cancellation) + { + IMateEntity mateEntity = e.MateEntity; + IClientSession session = e.Sender; + + if (mateEntity.Specialist == null) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.PARTNER_MESSAGE_NO_SP_EQUIPPED, session.UserLanguage), MsgMessageType.Middle); + return; + } + + await mateEntity.RemoveAllBuffsAsync(false); + mateEntity.IsUsingSp = true; + mateEntity.Skills.Clear(); + session.PlayerEntity.ClearMateSkillCooldowns(); + + if (mateEntity.Specialist.PartnerSkills != null) + { + foreach (PartnerSkill partnerSkill in mateEntity.Specialist.PartnerSkills) + { + if (partnerSkill == null) + { + continue; + } + + mateEntity.Skills.Add(partnerSkill); + } + } + + session.SendCondMate(mateEntity); + session.Broadcast(mateEntity.GenerateCMode(mateEntity.Specialist.GameItem.Morph)); + session.SendMatePskiPacket(mateEntity); + session.SendPetInfo(mateEntity, _gameLanguage); + session.Broadcast(mateEntity.GenerateOut()); + session.CurrentMapInstance.Broadcast(s => mateEntity.GenerateIn(_gameLanguage, s.UserLanguage, _spPartner)); + session.RefreshParty(_spPartner); + session.SendMateEffect(mateEntity, EffectType.Transform); + session.CurrentMapInstance.Broadcast(session.GenerateGuriPacket(6, 2, mateEntity.Id, 10)); + + int sum = 0; + foreach (IBattleEntitySkill skill in mateEntity.Skills) + { + if (skill is not PartnerSkill partnerSkill) + { + continue; + } + + sum += partnerSkill.Rank; + } + + if (sum == 0) + { + sum = 1; + } + else + { + sum /= 3; + } + + if (sum < 1) + { + sum = 1; + } + + SpPartnerInfo spInfo = _spPartner.GetByMorph(mateEntity.Specialist.GameItem.Morph); + + if (spInfo == null) + { + return; + } + + if (spInfo.BuffId == 0) + { + return; + } + + await session.PlayerEntity.AddBuffAsync(_buffFactory.CreateBuff(spInfo.BuffId + (sum - 1), session.PlayerEntity, BuffFlag.PARTNER)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Mates/MateSpUntransformEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Mates/MateSpUntransformEventHandler.cs new file mode 100644 index 0000000..9825868 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Mates/MateSpUntransformEventHandler.cs @@ -0,0 +1,81 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.Groups; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage.Configuration; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Extensions.Mates; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Mates.Events; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Skills; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Mates; + +public class MateSpUntransformEventHandler : IAsyncEventProcessor +{ + private readonly IAsyncEventPipeline _eventPipeline; + private readonly IGameLanguageService _gameLanguage; + private readonly ISpPartnerConfiguration _spPartner; + + public MateSpUntransformEventHandler(IAsyncEventPipeline eventPipeline, IGameLanguageService gameLanguage, ISpPartnerConfiguration spPartner) + { + _eventPipeline = eventPipeline; + _gameLanguage = gameLanguage; + _spPartner = spPartner; + } + + public async Task HandleAsync(MateSpUntransformEvent e, CancellationToken cancellation) + { + IMateEntity mateEntity = e.MateEntity; + IClientSession session = e.Sender; + + if (mateEntity.Specialist == null) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.PARTNER_MESSAGE_NO_SP_EQUIPPED, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (mateEntity.Specialist.PartnerSkills != null) + { + foreach (PartnerSkill partnerSkill in mateEntity.Specialist.PartnerSkills) + { + partnerSkill.LastUse = DateTime.MinValue; + } + } + + await _eventPipeline.ProcessEventAsync(new BuffRemoveEvent + { + Entity = session.PlayerEntity, + Buffs = session.PlayerEntity.BuffComponent.GetAllBuffs(x => x.BuffFlags == BuffFlag.PARTNER), + RemovePermanentBuff = true + }); + + short cooldown = 30; + mateEntity.IsUsingSp = false; + mateEntity.Skills.Clear(); + foreach (INpcMonsterSkill skill in mateEntity.MonsterSkills) + { + mateEntity.Skills.Add(skill); + } + + await mateEntity.RemoveAllBuffsAsync(false); + mateEntity.SpCooldownEnd = DateTime.UtcNow.AddSeconds(cooldown); + session.PlayerEntity.ClearMateSkillCooldowns(); + session.SendChatMessage(_gameLanguage.GetLanguageFormat(GameDialogKey.INFORMATION_CHATMESSAGE_PSP_STAY_TIME, session.UserLanguage, cooldown), ChatMessageColorType.Red); + session.SendCondMate(mateEntity); + session.Broadcast(mateEntity.GenerateCMode(-1)); + session.SendRemoveMateSpSkills(mateEntity); + session.SendPetInfo(mateEntity, _gameLanguage); + session.Broadcast(mateEntity.GenerateOut()); + session.CurrentMapInstance.Broadcast(s => mateEntity.GenerateIn(_gameLanguage, s.UserLanguage, _spPartner)); + session.RefreshParty(_spPartner); + session.SendMateSpCooldown(mateEntity, cooldown); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Mates/MateStayInsideMinilandEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Mates/MateStayInsideMinilandEventHandler.cs new file mode 100644 index 0000000..1f334fa --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Mates/MateStayInsideMinilandEventHandler.cs @@ -0,0 +1,80 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.Groups; +using WingsEmu.Game; +using WingsEmu.Game._ItemUsage.Configuration; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Extensions.Mates; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Mates.Events; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Mates; + +public class MateStayInsideMinilandEventHandler : IAsyncEventProcessor +{ + private readonly IMateBuffConfigsContainer _mateBuffConfigsContainer; + private readonly IRandomGenerator _randomGenerator; + private readonly ISpPartnerConfiguration _spPartnerConfiguration; + + public MateStayInsideMinilandEventHandler(IRandomGenerator randomGenerator, IMateBuffConfigsContainer mateBuffConfigsContainer, ISpPartnerConfiguration spPartnerConfiguration) + { + _randomGenerator = randomGenerator; + _mateBuffConfigsContainer = mateBuffConfigsContainer; + _spPartnerConfiguration = spPartnerConfiguration; + } + + public async Task HandleAsync(MateStayInsideMinilandEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + IMateEntity mateEntity = e.MateEntity; + + if (mateEntity == null) + { + return; + } + + if (session.PlayerEntity.Miniland == null) + { + return; + } + + if (!mateEntity.IsAlive()) + { + mateEntity.Hp = 1; + } + + if (session.PlayerEntity.Miniland.IsBlockedZone(mateEntity.PositionX, mateEntity.PositionY)) + { + Position cell = mateEntity.NewMinilandMapCell(_randomGenerator); + mateEntity.MinilandX = cell.X; + mateEntity.MinilandY = cell.Y; + } + + if (session.PlayerEntity.Miniland.IsBlockedZone(mateEntity.MinilandX, mateEntity.MinilandY)) + { + Position cell = mateEntity.NewMinilandMapCell(_randomGenerator); + mateEntity.MinilandX = cell.X; + mateEntity.MinilandY = cell.Y; + } + + mateEntity.IsTeamMember = false; + mateEntity.ChangePosition(new Position(mateEntity.MinilandX, mateEntity.MinilandY)); + + if (!e.IsOnCharacterEnter) + { + await mateEntity.RemovePartnerSp(); + mateEntity.RefreshPartnerSkills(); + await mateEntity.RemoveAllBuffsAsync(true); + session.RemovePetBuffs(mateEntity, _mateBuffConfigsContainer); + } + + session.SendScpPackets(); + session.SendScnPackets(); + session.RefreshParty(_spPartnerConfiguration); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Mates/MateSummonEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Mates/MateSummonEventHandler.cs new file mode 100644 index 0000000..4776508 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Mates/MateSummonEventHandler.cs @@ -0,0 +1,124 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.Groups; +using WingsAPI.Game.Extensions.Quicklist; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage.Configuration; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Extensions.Mates; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Mates.Events; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Mates; + +public class MateSummonEventHandler : IAsyncEventProcessor +{ + private readonly IBuffFactory _buffFactory; + private readonly IGameLanguageService _gameLanguage; + private readonly IMateBuffConfigsContainer _mateBuffConfigsContainer; + private readonly ISpPartnerConfiguration _partnerConfiguration; + + public MateSummonEventHandler(IGameLanguageService gameLanguage, IMateBuffConfigsContainer mateBuffConfigsContainer, ISpPartnerConfiguration partnerConfiguration, IBuffFactory buffFactory) + { + _gameLanguage = gameLanguage; + _mateBuffConfigsContainer = mateBuffConfigsContainer; + _partnerConfiguration = partnerConfiguration; + _buffFactory = buffFactory; + } + + public async Task HandleAsync(MateSummonEvent e, CancellationToken cancellation) + { + IMateEntity mateEntity = e.MateEntity; + IClientSession session = e.Sender; + + if (mateEntity == null) + { + return; + } + + if (!mateEntity.IsSummonable) + { + return; + } + + if (session.PlayerEntity.MapInstance.Id == session.PlayerEntity.Miniland.Id) + { + return; + } + + if (session.PlayerEntity.RainbowBattleComponent.IsInRainbowBattle) + { + return; + } + + if (!mateEntity.IsAlive()) + { + return; + } + + if (!session.IsGameMaster()) + { + if (mateEntity.Level > session.PlayerEntity.Level) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.PET_SHOUTMESSAGE_HIGHER_LEVEL, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (session.PlayerEntity.GetDignityIco() == 6) + { + session.SendMsg(session.GetLanguage(GameDialogKey.PET_SHOUTMESSAGE_DIGNITY_LOW), MsgMessageType.Middle); + return; + } + } + + if (e.Sender.PlayerEntity.MateComponent.GetMate(s => s.IsTeamMember && s.MateType == mateEntity.MateType) != null) + { + e.Sender.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.PET_MESSAGE_ALREADY_IN_TEAM, e.Sender.UserLanguage), ChatMessageColorType.Red); + e.Sender.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.PET_MESSAGE_ALREADY_IN_TEAM, e.Sender.UserLanguage), MsgMessageType.Middle); + return; + } + + if (mateEntity.IsTeamMember) + { + return; + } + + if (session.PlayerEntity.Miniland.Sessions.Any()) + { + mateEntity.MapInstance.Broadcast(mateEntity.GenerateOut()); + } + + session.PlayerEntity.Miniland.RemoveMate(mateEntity); + mateEntity.IsTeamMember = true; + session.SendScpStcPacket(); + + switch (mateEntity.MateType) + { + case MateType.Pet: + await session.AddPetBuff(mateEntity, _mateBuffConfigsContainer, _buffFactory); + break; + case MateType.Partner when mateEntity.MonsterSkills?.Count != 0: + session.RefreshSkillList(); + session.RefreshQuicklist(); + break; + } + + session.PlayerEntity.MapInstance.AddMate(e.MateEntity); + mateEntity.TeleportNearCharacter(); + session.CurrentMapInstance.Broadcast(s => e.MateEntity.GenerateIn(_gameLanguage, s.UserLanguage, _partnerConfiguration)); + session.SendCondMate(mateEntity); + session.SendScnPackets(); + session.SendScpPackets(); + session.SendPClearPacket(); + session.SendScnPackets(); + session.SendScpPackets(); + session.RefreshParty(_partnerConfiguration); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Miniland/AddObjMinilandEndLogicEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Miniland/AddObjMinilandEndLogicEventHandler.cs new file mode 100644 index 0000000..ab7f54d --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Miniland/AddObjMinilandEndLogicEventHandler.cs @@ -0,0 +1,253 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.MinilandExtensions; +using WingsEmu.Game; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Configurations.Miniland; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Extensions.Mates; +using WingsEmu.Game.Helpers; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Items; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Miniland; +using WingsEmu.Game.Miniland.Events; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Miniland; + +public class AddObjMinilandEndLogicEventHandler : IAsyncEventProcessor +{ + private readonly IGameLanguageService _languageService; + private readonly IMinilandManager _minilandManager; + private readonly IRandomGenerator _randomGenerator; + + public AddObjMinilandEndLogicEventHandler(IMinilandManager minilandManager, IRandomGenerator randomGenerator, IGameLanguageService languageService) + { + _minilandManager = minilandManager; + _randomGenerator = randomGenerator; + _languageService = languageService; + } + + public async Task HandleAsync(AddObjMinilandEndLogicEvent e, CancellationToken cancellation) + { + IMapInstance mapInstance = e.Miniland; + + if (mapInstance.MapDesignObjects.Exists(x => x.InventorySlot == e.MapObject.InventoryItem.Slot)) + { + return; + } + + Game.Configurations.Miniland.Miniland minilandConfiguration = _minilandManager.GetMinilandConfiguration(mapInstance); + if (minilandConfiguration == default) + { + return; + } + + bool isForced = false; + + if (e.MapObject.InventoryItem.ItemInstance.GameItem.ItemType == ItemType.House) + { + ForcedPlacing forcedPlacing = minilandConfiguration.ForcedPlacings.FirstOrDefault(x => + (int)x.SubType == e.MapObject.InventoryItem.ItemInstance.GameItem.ItemSubType); + + if (forcedPlacing != default) + { + e.MapObject.MapX = forcedPlacing.ForcedLocation.X; + e.MapObject.MapY = forcedPlacing.ForcedLocation.Y; + } + + isForced = true; + } + + byte[] modifiedGrid = GetModifiedGrid(mapInstance, minilandConfiguration, e.MapObject, isForced); + if (!IsZoneConstructable(modifiedGrid, e.MapObject, mapInstance.Width, mapInstance.Height)) + { + IGameItem gameItem = e.MapObject.InventoryItem.ItemInstance.GameItem; + e.Sender.SendDebugMessage($"[MINILAND] The zone is not constructable. Object Width: {gameItem.Width.ToString()} | Object Height: {gameItem.Height.ToString()}"); + return; + } + + MoveMatesColliding(e, minilandConfiguration); + + mapInstance.MapDesignObjects.Add(e.MapObject); + + if (e.MapObject.InventoryItem.ItemInstance.GameItem.IsWarehouse) + { + e.Sender.PlayerEntity.WareHouseSize = e.MapObject.InventoryItem.ItemInstance.GameItem.MinilandObjectPoint; + } + + if (e.MapObject.InventoryItem.ItemInstance.GameItem.ItemType == ItemType.House) + { + _minilandManager.RelativeUpdateMinilandCapacity(e.Sender.PlayerEntity.Id, e.MapObject.InventoryItem.ItemInstance.GameItem.MinilandObjectPoint); + BroadcastCapacityUpdate(e); + } + + mapInstance.Broadcast(e.MapObject.GenerateEffect(false)); + mapInstance.Broadcast(e.MapObject.GenerateMinilandObject(false)); + } + + private void BroadcastCapacityUpdate(AddObjMinilandEndLogicEvent e) + { + foreach (IClientSession session in e.Miniland.Sessions) + { + if (session.PlayerEntity.Id == e.Sender.PlayerEntity.Id) + { + continue; + } + + session.SendMinilandPublicInformation(_minilandManager, _languageService); + } + } + + private void MoveMatesColliding(AddObjMinilandEndLogicEvent e, Game.Configurations.Miniland.Miniland minilandConfiguration) + { + (int x1, int y1, int x2, int y2) boundaries = GetMapObjectBoundaries(e.MapObject); + + RestrictedZone secureZoneForMate = minilandConfiguration.RestrictedZones.FirstOrDefault(x => + x.RestrictionTag == RestrictionType.OnlyMates); + + int baseX1 = default; + int baseX2 = default; + int baseY1 = default; + int baseY2 = default; + if (secureZoneForMate != default) + { + (baseX1, baseY1, baseX2, baseY2) = GetNormalizedBoundaries( + secureZoneForMate.Corner1.X, secureZoneForMate.Corner1.Y, secureZoneForMate.Corner2.X, secureZoneForMate.Corner2.Y); + } + + foreach (IMateEntity mateEntity in e.Miniland.GetAliveMates()) + { + if (!IsInsideRectangle(boundaries, mateEntity.PositionX, mateEntity.PositionY)) + { + continue; + } + + if (secureZoneForMate != default && mateEntity.Owner.Id == e.Sender.PlayerEntity.Id) + { + mateEntity.ChangePosition(new Position((short)_randomGenerator.RandomNumber(baseX1, baseX2 + 1), (short)_randomGenerator.RandomNumber(baseY1, baseY2 + 1))); + e.Sender.BroadcastMateTeleport(mateEntity); + } + else + { + mateEntity.TeleportToCharacter(); + } + } + } + + private static byte[] GetModifiedGrid(IMapInstance mapInstance, Game.Configurations.Miniland.Miniland minilandConfiguration, MapDesignObject mapObject, bool forcedPlacing = false) + { + //TODO This should be reworked, it not needed to copy the map grid + byte[] mapGrid = forcedPlacing ? new byte[mapInstance.Width * mapInstance.Height] : mapInstance.Grid.ToArray(); + + foreach (MapDesignObject mapDesignObject in mapInstance.MapDesignObjects) + { + (int x1, int y1, int x2, int y2) = GetMapObjectBoundaries(mapDesignObject); + + for (int i = x1; i <= x2; i++) + { + for (int j = y1; j <= y2; j++) + { + mapGrid[i + j * mapInstance.Width] = (byte)(mapGrid[i + j * mapInstance.Width] | 1); + } + } + } + + foreach (IClientSession session in mapInstance.Sessions) + { + IPlayerEntity character = session.PlayerEntity; + mapGrid[character.PositionX + character.PositionY * mapInstance.Width] = + (byte)(mapGrid[character.PositionX + character.PositionY * mapInstance.Width] | 1); + } + + foreach (RestrictedZone restrictedZone in minilandConfiguration.RestrictedZones) + { + bool blockZone = false; + switch (restrictedZone.RestrictionTag) + { + case RestrictionType.OnlySoilObjects when mapObject.InventoryItem.ItemInstance.GameItem.ItemType != ItemType.Minigame: + case RestrictionType.OnlyTerraceObjects when mapObject.InventoryItem.ItemInstance.GameItem.ItemType != ItemType.Terrace: + case RestrictionType.OnlyGardenObjects when mapObject.InventoryItem.ItemInstance.GameItem.ItemType != ItemType.Garden: + case RestrictionType.OnlyMates: + case RestrictionType.Unconstructable: + blockZone = true; + break; + } + + if (!blockZone) + { + continue; + } + + (int x1, int y1, int x2, int y2) = GetNormalizedBoundaries( + restrictedZone.Corner1.X, restrictedZone.Corner1.Y, restrictedZone.Corner2.X, restrictedZone.Corner2.Y); + + for (int i = x1; i <= x2; i++) + { + for (int j = y1; j <= y2; j++) + { + mapGrid[i + j * mapInstance.Width] = (byte)(mapGrid[i + j * mapInstance.Width] | 1); + } + } + } + + return mapGrid; + } + + private static bool IsZoneConstructable(IReadOnlyList array, MapDesignObject mapObject, int width, int height) + { + (int x1, int y1, int x2, int y2) = GetMapObjectBoundaries(mapObject); + + for (int i = x1; i <= x2; i++) + { + for (int j = y1; j <= y2; j++) + { + if (!array.IsWalkable(i, j, width, height)) + { + return false; + } + } + } + + return true; + } + + private static (int x1, int y1, int x2, int y2) GetMapObjectBoundaries(MapDesignObject mapObject) + { + int x1 = mapObject.MapX; + int y1 = mapObject.MapY; + int x2 = mapObject.MapX + mapObject.InventoryItem.ItemInstance.GameItem.Width - 1; + int y2 = mapObject.MapY + mapObject.InventoryItem.ItemInstance.GameItem.Height - 1; + + return GetNormalizedBoundaries(x1, y1, x2, y2); + } + + private static (int x1, int y1, int x2, int y2) GetNormalizedBoundaries(int x1, int y1, int x2, int y2) + { + bool randBoolean1 = x1 < x2; + int baseX1 = randBoolean1 ? x1 : x2; + int baseX2 = randBoolean1 ? x2 : x1; + + bool randBoolean2 = y1 < y2; + int baseY1 = randBoolean2 ? y1 : y2; + int baseY2 = randBoolean2 ? y2 : y1; + return (baseX1, baseY1, baseX2, baseY2); + } + + private static bool IsInsideRectangle((int x1, int y1, int x2, int y2) boundaries, int mX, int mY) + { + (int x1, int y1, int x2, int y2) = boundaries; + return IsInsideRectangle(mX, mY, x1, y1, x2, y2); + } + + private static bool IsInsideRectangle(int mX, int mY, int x1, int y1, int x2, int y2) => + mX >= x1 && mX <= x2 && + mY >= y1 && mY <= y2; +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Miniland/AddObjMinilandEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Miniland/AddObjMinilandEventHandler.cs new file mode 100644 index 0000000..814234d --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Miniland/AddObjMinilandEventHandler.cs @@ -0,0 +1,52 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Miniland; +using WingsEmu.Game.Miniland.Events; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Miniland; + +public class AddObjMinilandEventHandler : IAsyncEventProcessor +{ + public async Task HandleAsync(AddObjMinilandEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + IPlayerEntity character = e.Sender.PlayerEntity; + if (character.MapInstanceId != character.Miniland.Id) + { + return; + } + + if (character.MinilandState != MinilandState.LOCK) + { + session.SendMsg(session.GetLanguage(GameDialogKey.MINILAND_SHOUTMESSAGE_NEED_BE_LOCKED), MsgMessageType.Middle); + return; + } + + InventoryItem minilandItem = character.GetItemBySlotAndType(e.Slot, InventoryType.Miniland); + if (minilandItem == null) + { + return; + } + + var mapObject = new MapDesignObject + { + Id = Guid.NewGuid(), + CharacterId = e.Sender.PlayerEntity.Id, + InventoryItem = minilandItem, + InventorySlot = minilandItem.Slot, + MapX = e.X, + MapY = e.Y + }; + + await e.Sender.EmitEventAsync(new AddObjMinilandEndLogicEvent(mapObject, e.Sender.PlayerEntity.Miniland)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Miniland/MinigameDurabilityCouponEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Miniland/MinigameDurabilityCouponEventHandler.cs new file mode 100644 index 0000000..408bd41 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Miniland/MinigameDurabilityCouponEventHandler.cs @@ -0,0 +1,79 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsAPI.Game.Extensions.MinilandExtensions; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Configurations.Miniland; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Miniland; +using WingsEmu.Game.Miniland.Events; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Miniland; + +public class MinigameDurabilityCouponEventHandler : IAsyncEventProcessor +{ + private const MinigameInteraction ThisAction = MinigameInteraction.UseDurabilityCoupon; + private readonly IGameLanguageService _languageService; + private readonly MinigameConfiguration _minigameConfiguration; + private readonly IMinigameManager _minigameManager; + + public MinigameDurabilityCouponEventHandler(MinigameConfiguration minigameConfiguration, IGameLanguageService languageService, IMinigameManager minigameManager) + { + _minigameConfiguration = minigameConfiguration; + _languageService = languageService; + _minigameManager = minigameManager; + } + + public async Task HandleAsync(MinigameDurabilityCouponEvent e, CancellationToken cancellation) + { + MinilandInteractionInformationHolder minilandInteraction = _minigameManager.GetLastInteraction(e.Sender); + + if (minilandInteraction.Interaction != MinigameInteraction.GetMinigameDurability + && minilandInteraction.Interaction != ThisAction + && minilandInteraction.MapObject != e.MapObject) + { + _minigameManager.ReportInteractionIncoherence(e.Sender, minilandInteraction.Interaction, minilandInteraction.MapObject, ThisAction, e.MapObject); + return; + } + + if (e.MapObject.CharacterId != e.Sender.PlayerEntity.Id) + { + await e.Sender.NotifyStrangeBehavior(StrangeBehaviorSeverity.ABUSING, "Tried to repair with a coupon a minigame that he doesn't own." + + $" 'SuspectCharacterId': {e.Sender.PlayerEntity.Id.ToString()} | 'VictimCharacterId': {e.MapObject.CharacterId.ToString()}"); + return; + } + + if (e.MapObject.InventoryItem.ItemInstance.DurabilityPoint >= e.MapObject.InventoryItem.ItemInstance.GameItem.MinilandObjectPoint) + { + e.Sender.SendInfo(_languageService.GetLanguage(GameDialogKey.MINILAND_INFO_DURABILITY_MAXIMUM, e.Sender.UserLanguage)); + return; + } + + if (!e.Sender.PlayerEntity.HasItem(_minigameConfiguration.Configuration.RepairDurabilityCouponVnum)) + { + await e.Sender.NotifyStrangeBehavior(StrangeBehaviorSeverity.ABUSING, "Tried to 'coupon repair' without a coupon" + + $" 'SuspectCharacterId': {e.Sender.PlayerEntity.Id.ToString()} | 'VictimCharacterId': {e.MapObject.CharacterId.ToString()}"); + return; + } + + await e.Sender.RemoveItemFromInventory(_minigameConfiguration.Configuration.RepairDurabilityCouponVnum); + _minigameManager.RegisterInteraction(e.Sender, new MinilandInteractionInformationHolder(ThisAction, e.MapObject)); + + int pointsToAdd = _minigameConfiguration.Configuration.DurabilityCouponRepairingAmount; + + if (e.MapObject.InventoryItem.ItemInstance.DurabilityPoint + pointsToAdd > e.MapObject.InventoryItem.ItemInstance.GameItem.MinilandObjectPoint) + { + pointsToAdd = e.MapObject.InventoryItem.ItemInstance.GameItem.MinilandObjectPoint - e.MapObject.InventoryItem.ItemInstance.DurabilityPoint; + e.MapObject.InventoryItem.ItemInstance.DurabilityPoint = e.MapObject.InventoryItem.ItemInstance.GameItem.MinilandObjectPoint; + } + else + { + e.MapObject.InventoryItem.ItemInstance.DurabilityPoint += pointsToAdd; + } + + e.Sender.SendInfo(_languageService.GetLanguageFormat(GameDialogKey.MINIGAME_INFO_REFILL, e.Sender.UserLanguage, pointsToAdd)); + e.Sender.SendMinilandDurabilityInfo(e.MapObject, _minigameConfiguration); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Miniland/MinigameDurabilityInfoEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Miniland/MinigameDurabilityInfoEventHandler.cs new file mode 100644 index 0000000..0902925 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Miniland/MinigameDurabilityInfoEventHandler.cs @@ -0,0 +1,47 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.MinilandExtensions; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Configurations.Miniland; +using WingsEmu.Game.Miniland; +using WingsEmu.Game.Miniland.Events; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Miniland; + +public class MinigameDurabilityInfoEventHandler : IAsyncEventProcessor +{ + private const MinigameInteraction ThisAction = MinigameInteraction.GetMinigameDurability; + private readonly MinigameConfiguration _minigameConfiguration; + private readonly IMinigameManager _minigameManager; + + public MinigameDurabilityInfoEventHandler(MinigameConfiguration minigameConfiguration, IMinigameManager minigameManager) + { + _minigameConfiguration = minigameConfiguration; + _minigameManager = minigameManager; + } + + public async Task HandleAsync(MinigameDurabilityInfoEvent e, CancellationToken cancellation) + { + MinilandInteractionInformationHolder lastMinilandInteraction = _minigameManager.GetLastInteraction(e.Sender); + if (lastMinilandInteraction.Interaction != MinigameInteraction.GetMinigameInformation + && lastMinilandInteraction.Interaction != MinigameInteraction.RepairMinigameDurability + && lastMinilandInteraction.Interaction != MinigameInteraction.GetYieldInformation + && lastMinilandInteraction.Interaction != MinigameInteraction.GetYieldReward + && lastMinilandInteraction.MapObject != e.MapObject) + { + _minigameManager.ReportInteractionIncoherence(e.Sender, lastMinilandInteraction.Interaction, lastMinilandInteraction.MapObject, ThisAction, e.MapObject); + return; + } + + if (e.MapObject.CharacterId != e.Sender.PlayerEntity.Id) + { + await e.Sender.NotifyStrangeBehavior(StrangeBehaviorSeverity.ABUSING, "Tried to see durability information from a minigame that he doesn't own." + + $" 'SuspectCharacterId': {e.Sender.PlayerEntity.Id.ToString()} | 'VictimCharacterId': {e.MapObject.CharacterId.ToString()}"); + return; + } + + _minigameManager.RegisterInteraction(e.Sender, new MinilandInteractionInformationHolder(ThisAction, e.MapObject)); + e.Sender.SendMinilandDurabilityInfo(e.MapObject, _minigameConfiguration); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Miniland/MinigameGetYieldInfoEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Miniland/MinigameGetYieldInfoEventHandler.cs new file mode 100644 index 0000000..9e066fe --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Miniland/MinigameGetYieldInfoEventHandler.cs @@ -0,0 +1,47 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.MinilandExtensions; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Configurations.Miniland; +using WingsEmu.Game.Miniland; +using WingsEmu.Game.Miniland.Events; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Miniland; + +public class MinigameGetYieldInfoEventHandler : IAsyncEventProcessor +{ + private readonly MinigameConfiguration _minigameConfiguration; + private readonly IMinigameManager _minigameManager; + private readonly MinigameInteraction ThisAction = MinigameInteraction.GetYieldInformation; + + public MinigameGetYieldInfoEventHandler(IMinigameManager minigameManager, MinigameConfiguration minigameConfiguration) + { + _minigameManager = minigameManager; + _minigameConfiguration = minigameConfiguration; + } + + public async Task HandleAsync(MinigameGetYieldInfoEvent e, CancellationToken cancellation) + { + MinilandInteractionInformationHolder minilandInteraction = _minigameManager.GetLastInteraction(e.Sender); + + if (minilandInteraction.Interaction != MinigameInteraction.GetMinigameInformation + && minilandInteraction.Interaction != MinigameInteraction.GetMinigameDurability + && minilandInteraction.Interaction != MinigameInteraction.RepairMinigameDurability + && minilandInteraction.MapObject != e.MapObject) + { + _minigameManager.ReportInteractionIncoherence(e.Sender, minilandInteraction.Interaction, minilandInteraction.MapObject, ThisAction, e.MapObject); + return; + } + + if (e.MapObject.CharacterId != e.Sender.PlayerEntity.Id) + { + await e.Sender.NotifyStrangeBehavior(StrangeBehaviorSeverity.ABUSING, "Tried to see information about the yield rewards from a minigame that he doesn't own." + + $" 'SuspectCharacterId': {e.Sender.PlayerEntity.Id.ToString()} | 'VictimCharacterId': {e.MapObject.CharacterId.ToString()}"); + return; + } + + _minigameManager.RegisterInteraction(e.Sender, new MinilandInteractionInformationHolder(ThisAction, e.MapObject)); + e.Sender.SendMinilandYieldInfo(e.MapObject, e.MapObject.GetYieldRewardEnumerable(), _minigameConfiguration); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Miniland/MinigameGetYieldRewardEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Miniland/MinigameGetYieldRewardEventHandler.cs new file mode 100644 index 0000000..93edcf1 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Miniland/MinigameGetYieldRewardEventHandler.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsAPI.Game.Extensions.MinilandExtensions; +using WingsEmu.Game; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Configurations.Miniland; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Items; +using WingsEmu.Game.Miniland; +using WingsEmu.Game.Miniland.Events; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Miniland; + +public class MinigameGetYieldRewardEventHandler : IAsyncEventProcessor +{ + private const MinigameInteraction ThisAction = MinigameInteraction.GetYieldReward; + private readonly IGameItemInstanceFactory _gameItemInstanceFactory; + private readonly IGameLanguageService _languageService; + private readonly MinigameConfiguration _minigameConfiguration; + private readonly IMinigameManager _minigameManager; + private readonly IRandomGenerator _randomGenerator; + + public MinigameGetYieldRewardEventHandler(MinigameConfiguration minigameConfiguration, IGameLanguageService languageService, IMinigameManager minigameManager, IRandomGenerator randomGenerator, + IGameItemInstanceFactory gameItemInstanceFactory) + { + _minigameConfiguration = minigameConfiguration; + _languageService = languageService; + _minigameManager = minigameManager; + _randomGenerator = randomGenerator; + _gameItemInstanceFactory = gameItemInstanceFactory; + } + + public async Task HandleAsync(MinigameGetYieldRewardEvent e, CancellationToken cancellation) + { + MinilandInteractionInformationHolder minilandInteraction = _minigameManager.GetLastInteraction(e.Sender); + + if (minilandInteraction.Interaction != MinigameInteraction.GetYieldInformation + && minilandInteraction.Interaction != ThisAction + && minilandInteraction.MapObject != e.MapObject) + { + _minigameManager.ReportInteractionIncoherence(e.Sender, minilandInteraction.Interaction, minilandInteraction.MapObject, ThisAction, e.MapObject); + return; + } + + if (e.MapObject.CharacterId != e.Sender.PlayerEntity.Id) + { + await e.Sender.NotifyStrangeBehavior(StrangeBehaviorSeverity.ABUSING, "Tried to steal the yield rewards from a minigame that he doesn't own." + + $" 'SuspectCharacterId': {e.Sender.PlayerEntity.Id.ToString()} | 'VictimCharacterId': {e.MapObject.CharacterId.ToString()}"); + return; + } + + MinigameRewards rewardsToGive = + _minigameManager.GetSpecificMinigameConfiguration(e.MapObject.InventoryItem.ItemInstance.ItemVNum).Rewards.FirstOrDefault(x => x.RewardLevel == e.RewardLevel); + + if (rewardsToGive == default || rewardsToGive.Rewards.Count < 1) + { + e.Sender.SendInfo(_languageService.GetLanguage(GameDialogKey.MINIGAME_INFO_NO_REWARD, e.Sender.UserLanguage)); + return; + } + + int amount = GetAmount(e); + if (amount < 1) + { + await e.Sender.NotifyStrangeBehavior(StrangeBehaviorSeverity.NORMAL, $"The quantity of the minigame requested in GetYieldReward is 0 -> 'RewardLevel': {e.RewardLevel}"); + return; + } + + _minigameManager.RegisterInteraction(e.Sender, new MinilandInteractionInformationHolder(ThisAction, e.MapObject)); + var dictionary = new Dictionary(); + + for (int i = 0; i < amount; i++) + { + MinigameReward reward = rewardsToGive.Rewards[_randomGenerator.RandomNumber(0, rewardsToGive.Rewards.Count)]; + if (dictionary.ContainsKey(reward.Vnum)) + { + dictionary[reward.Vnum] += reward.Amount; + continue; + } + + dictionary.TryAdd(reward.Vnum, reward.Amount); + } + + var list = e.MapObject.GetYieldRewardEnumerable().ToList(); + + foreach ((int vnum, int amount1) in dictionary) + { + GameItemInstance item = _gameItemInstanceFactory.CreateItem(vnum, amount1); + await e.Sender.AddNewItemToInventory(item, sendGiftIsFull: true); + list.Add(new MinigameReward + { + Amount = amount1, + Vnum = vnum + }); + } + + e.Sender.SendMinilandYieldInfo(e.MapObject, list, _minigameConfiguration); + } + + public ushort GetAmount(MinigameGetYieldRewardEvent e) + { + ushort amount; + switch (e.RewardLevel) + { + case RewardLevel.FirstReward: + amount = e.MapObject.Level1BoxAmount; + e.MapObject.Level1BoxAmount = 0; + break; + case RewardLevel.SecondReward: + amount = e.MapObject.Level2BoxAmount; + e.MapObject.Level2BoxAmount = 0; + break; + case RewardLevel.ThirdReward: + amount = e.MapObject.Level3BoxAmount; + e.MapObject.Level3BoxAmount = 0; + break; + case RewardLevel.FourthReward: + amount = e.MapObject.Level4BoxAmount; + e.MapObject.Level4BoxAmount = 0; + break; + case RewardLevel.FifthReward: + amount = e.MapObject.Level5BoxAmount; + e.MapObject.Level5BoxAmount = 0; + break; + default: + e.Sender.NotifyStrangeBehavior(StrangeBehaviorSeverity.NORMAL, $"RewardLevel requested in GetYieldReward is incoherent -> 'RewardLevel': {e.RewardLevel}"); + throw new ArgumentOutOfRangeException(); + } + + return amount; + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Miniland/MinigamePlayEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Miniland/MinigamePlayEventHandler.cs new file mode 100644 index 0000000..6086b2e --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Miniland/MinigamePlayEventHandler.cs @@ -0,0 +1,76 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Configurations.Miniland; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Miniland; +using WingsEmu.Game.Miniland.Events; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Miniland; + +public class MinigamePlayEventHandler : IAsyncEventProcessor +{ + private const MinigameInteraction ThisAction = MinigameInteraction.DeclaratePlay; + private readonly IGameLanguageService _languageService; + private readonly MinigameConfiguration _minigameConfiguration; + private readonly IMinigameManager _minigameManager; + + public MinigamePlayEventHandler(IMinigameManager minigameManager, IGameLanguageService languageService, + MinigameConfiguration minigameConfiguration) + { + _minigameManager = minigameManager; + _languageService = languageService; + _minigameConfiguration = minigameConfiguration; + } + + public Task HandleAsync(MinigamePlayEvent e, CancellationToken cancellation) + { + MinilandInteractionInformationHolder lastMinilandInteraction = _minigameManager.GetLastInteraction(e.Sender); + + IPlayerEntity character = e.Sender.PlayerEntity; + + if (lastMinilandInteraction.Interaction != MinigameInteraction.GetReward + && lastMinilandInteraction.Interaction != MinigameInteraction.GetMinigameInformation + && lastMinilandInteraction.Interaction != ThisAction + && lastMinilandInteraction.MapObject != e.MinigameObject) + { + _minigameManager.ReportInteractionIncoherence(e.Sender, lastMinilandInteraction.Interaction, lastMinilandInteraction.MapObject, ThisAction, e.MinigameObject); + return Task.CompletedTask; + } + + if (!e.IsForFun && character.MinilandPoint < _minigameConfiguration.Configuration.MinigamePointsCostPerMinigame) + { + e.Sender.SendQnaPacket($"mg 10 {e.MinigameObject.InventoryItem.Slot.ToString()} {e.MinigameObject.InventoryItem.ItemInstance.ItemVNum.ToString()}", + _languageService.GetLanguage(GameDialogKey.MINILAND_DIALOG_ASK_MINIGAME_FOR_FUN, e.Sender.UserLanguage)); + return Task.CompletedTask; + } + + Minigame minigameConfiguration = _minigameManager.GetSpecificMinigameConfiguration(e.MinigameObject.InventoryItem.ItemInstance.ItemVNum); + if (minigameConfiguration == default) + { + return Task.CompletedTask; + } + + EffectType minigameEffect = minigameConfiguration.Type switch + { + MinigameType.Quarry => EffectType.MinigameQuarry, + MinigameType.Sawmill => EffectType.MinigameSawmill, + MinigameType.Shooting => EffectType.MinigameShooting, + MinigameType.Fishing => EffectType.MinigameFishing, + MinigameType.Typewriter => EffectType.MinigameTypewritter, + MinigameType.Memory => EffectType.MinigameMemory, + _ => throw new ArgumentOutOfRangeException() + }; + + character.CurrentMinigame = (int)minigameEffect; + e.Sender.BroadcastGuri(2, 1); + e.Sender.SendMinigameStart(minigameConfiguration.Type); + + _minigameManager.RegisterInteraction(e.Sender, new MinilandInteractionInformationHolder(ThisAction, e.MinigameObject)); + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Miniland/MinigameRepairDurabilityEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Miniland/MinigameRepairDurabilityEventHandler.cs new file mode 100644 index 0000000..d0be67d --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Miniland/MinigameRepairDurabilityEventHandler.cs @@ -0,0 +1,74 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.MinilandExtensions; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Configurations.Miniland; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Miniland; +using WingsEmu.Game.Miniland.Events; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Miniland; + +public class MinigameRepairDurabilityEventHandler : IAsyncEventProcessor +{ + private const MinigameInteraction ThisAction = MinigameInteraction.RepairMinigameDurability; + private readonly IGameLanguageService _languageService; + private readonly MinigameConfiguration _minigameConfiguration; + private readonly IMinigameManager _minigameManager; + + public MinigameRepairDurabilityEventHandler(MinigameConfiguration minigameConfiguration, IGameLanguageService languageService, IMinigameManager minigameManager) + { + _minigameConfiguration = minigameConfiguration; + _languageService = languageService; + _minigameManager = minigameManager; + } + + public async Task HandleAsync(MinigameRepairDurabilityEvent e, CancellationToken cancellation) + { + MinilandInteractionInformationHolder minilandInteraction = _minigameManager.GetLastInteraction(e.Sender); + + if (minilandInteraction.Interaction != MinigameInteraction.GetMinigameDurability + && minilandInteraction.Interaction != ThisAction + && minilandInteraction.MapObject != e.MapObject) + { + _minigameManager.ReportInteractionIncoherence(e.Sender, minilandInteraction.Interaction, minilandInteraction.MapObject, ThisAction, e.MapObject); + return; + } + + if (e.MapObject.CharacterId != e.Sender.PlayerEntity.Id) + { + await e.Sender.NotifyStrangeBehavior(StrangeBehaviorSeverity.ABUSING, "Tried to repair a minigame that he doesn't own." + + $" 'SuspectCharacterId': {e.Sender.PlayerEntity.Id.ToString()} | 'VictimCharacterId': {e.MapObject.CharacterId.ToString()}"); + return; + } + + bool applyGoldCost = _minigameConfiguration.Configuration.RepairDurabilityGoldCost > 0; + + if (applyGoldCost + && e.GoldToExpend < _minigameConfiguration.Configuration.RepairDurabilityGoldCost) + { + return; + } + + long durabilityToRepair = e.GoldToExpend / _minigameConfiguration.Configuration.RepairDurabilityGoldCost; + + if (e.MapObject.InventoryItem.ItemInstance.DurabilityPoint + durabilityToRepair > e.MapObject.InventoryItem.ItemInstance.GameItem.MinilandObjectPoint) + { + return; + } + + if (!e.Sender.PlayerEntity.RemoveGold(durabilityToRepair * _minigameConfiguration.Configuration.RepairDurabilityGoldCost)) + { + e.Sender.SendInfo(_languageService.GetLanguage(GameDialogKey.INTERACTION_MESSAGE_NOT_ENOUGH_GOLD, e.Sender.UserLanguage)); + return; + } + + _minigameManager.RegisterInteraction(e.Sender, new MinilandInteractionInformationHolder(ThisAction, e.MapObject)); + e.MapObject.InventoryItem.ItemInstance.DurabilityPoint += Convert.ToInt32(durabilityToRepair); + e.Sender.SendInfo(_languageService.GetLanguageFormat(GameDialogKey.MINIGAME_INFO_REFILL, e.Sender.UserLanguage, durabilityToRepair)); + e.Sender.SendMinilandDurabilityInfo(e.MapObject, _minigameConfiguration); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Miniland/MinigameRewardEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Miniland/MinigameRewardEventHandler.cs new file mode 100644 index 0000000..73e6d0e --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Miniland/MinigameRewardEventHandler.cs @@ -0,0 +1,156 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsAPI.Game.Extensions.MinilandExtensions; +using WingsAPI.Packets.Enums.Shells; +using WingsEmu.Game; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Configurations.Miniland; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Items; +using WingsEmu.Game.Miniland; +using WingsEmu.Game.Miniland.Events; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Miniland; + +public class MinigameRewardEventHandler : IAsyncEventProcessor +{ + private readonly IGameItemInstanceFactory _gameItemInstanceFactory; + private readonly IGameLanguageService _languageService; + private readonly MinigameConfiguration _minigameConfiguration; + private readonly IMinigameManager _minigameManager; + private readonly IRandomGenerator _randomGenerator; + private readonly MinigameInteraction ThisAction = MinigameInteraction.GetReward; + + public MinigameRewardEventHandler(IMinigameManager minigameManager, IRandomGenerator randomGenerator, MinigameConfiguration minigameConfiguration, IGameLanguageService languageService, + IGameItemInstanceFactory gameItemInstanceFactory) + { + _minigameManager = minigameManager; + _randomGenerator = randomGenerator; + _minigameConfiguration = minigameConfiguration; + _languageService = languageService; + _gameItemInstanceFactory = gameItemInstanceFactory; + } + + public async Task HandleAsync(MinigameRewardEvent e, CancellationToken cancellation) + { + MinilandInteractionInformationHolder possibleOldScore = _minigameManager.GetLastInteraction(e.Sender); + if (possibleOldScore.Interaction != MinigameInteraction.DeclarateScore + && possibleOldScore.MapObject != e.MapObject) + { + _minigameManager.ReportInteractionIncoherence(e.Sender, possibleOldScore.Interaction, possibleOldScore.MapObject, ThisAction, e.MapObject); + return; + } + + if (possibleOldScore.MapObject != e.MapObject || possibleOldScore.SavedRewards == default) + { + return; + } + + if ((int)e.RewardLevel > (int)possibleOldScore.SavedRewards.maxRewardLevel) + { + await e.Sender.NotifyStrangeBehavior(StrangeBehaviorSeverity.ABUSING, "Incoherence in the rewards offered and the reward reclaimed."); + e.Sender.SendMinigameRewardLevel(possibleOldScore.SavedRewards.maxRewardLevel); + return; + } + + MinigameRewards rewardsToGive = possibleOldScore.SavedRewards.rewards.FirstOrDefault(x => x.RewardLevel == e.RewardLevel); + if (rewardsToGive == default || rewardsToGive.Rewards.Count < 1) + { + e.Sender.SendInfo(_languageService.GetLanguage(GameDialogKey.MINIGAME_INFO_NO_REWARD, e.Sender.UserLanguage)); + e.Sender.SendMinigameReward(0, 0); + return; + } + + if (e.Coupon && !e.Sender.PlayerEntity.HasItem(_minigameConfiguration.Configuration.DoubleRewardCouponVnum)) + { + e.Sender.SendInfo(_languageService.GetLanguage(GameDialogKey.MINILAND_INFO_NO_ENOUGH_REWARD_COUPON, e.Sender.UserLanguage)); + await e.Sender.NotifyStrangeBehavior(StrangeBehaviorSeverity.NORMAL, "Tried to do coupon reward without coupon."); + return; + } + + if (e.MapObject.InventoryItem.ItemInstance.DurabilityPoint < rewardsToGive.DurabilityCost) + { + e.Sender.SendInfo(_languageService.GetLanguage(GameDialogKey.MINILAND_INFO_NOT_ENOUGH_DURABILITY_POINT, e.Sender.UserLanguage)); + return; + } + + if (e.Sender.PlayerEntity.MinilandPoint < _minigameConfiguration.Configuration.MinigamePointsCostPerMinigame) + { + e.Sender.SendInfo(_languageService.GetLanguage(GameDialogKey.MINILAND_INFO_NOT_ENOUGH_PRODUCTION_POINTS, e.Sender.UserLanguage)); + return; + } + + e.MapObject.InventoryItem.ItemInstance.DurabilityPoint -= rewardsToGive.DurabilityCost; + + int toRemove = _minigameConfiguration.Configuration.MinigamePointsCostPerMinigame - e.Sender.PlayerEntity.GetMaxArmorShellValue(ShellEffectType.ReducedProductionPointConsumed); + e.Sender.RemoveMinigamePoints((short)toRemove, _minigameConfiguration); + + _minigameManager.RegisterInteraction(e.Sender, new MinilandInteractionInformationHolder(ThisAction, e.MapObject)); + + MinigameReward rewardToGive = rewardsToGive.Rewards[_randomGenerator.RandomNumber(0, rewardsToGive.Rewards.Count)]; + short itemAmount = (short)Math.Round(rewardToGive.Amount * (1 + e.Sender.PlayerEntity.GetMaxArmorShellValue(ShellEffectType.IncreasedProductionPossibility) * 0.01)); + if (e.Coupon) + { + await e.Sender.RemoveItemFromInventory(_minigameConfiguration.Configuration.DoubleRewardCouponVnum); + //This is because amount > 1 in things like weapons doesn't work. + await GiveReward(e.Sender, rewardToGive.Vnum, itemAmount); + } + + await GiveReward(e.Sender, rewardToGive.Vnum, itemAmount); + e.Sender.SendMinigameReward(rewardToGive.Vnum, e.Coupon ? itemAmount * 2 : itemAmount); + + Minigame minigameConfiguration = _minigameManager.GetSpecificMinigameConfiguration(e.MapObject.InventoryItem.ItemInstance.ItemVNum); + await e.Sender.EmitEventAsync(new MinigameRewardClaimedEvent + { + OwnerId = e.MapObject.CharacterId, + MinigameVnum = e.MapObject.InventoryItem.ItemInstance.ItemVNum, + MinigameType = minigameConfiguration.Type, + RewardLevel = rewardsToGive.RewardLevel, + Coupon = e.Coupon, + ItemVnum = rewardToGive.Vnum, + Amount = e.Coupon ? (short)(rewardToGive.Amount * 2) : (short)rewardToGive.Amount + }); + + if (e.MapObject.CharacterId == e.Sender.PlayerEntity.Id) + { + return; + } + + switch (e.RewardLevel) + { + case RewardLevel.FirstReward: + e.MapObject.Level1BoxAmount++; + break; + + case RewardLevel.SecondReward: + e.MapObject.Level2BoxAmount++; + break; + + case RewardLevel.ThirdReward: + e.MapObject.Level3BoxAmount++; + break; + + case RewardLevel.FourthReward: + e.MapObject.Level4BoxAmount++; + break; + + case RewardLevel.FifthReward: + e.MapObject.Level5BoxAmount++; + break; + } + + //TODO Message notificating player did minigame. Should be configurable individuallly (per player) + } + + private async Task GiveReward(IClientSession session, int vnum, short amount) + { + GameItemInstance item = _gameItemInstanceFactory.CreateItem(vnum, amount); + await session.AddNewItemToInventory(item, sendGiftIsFull: true); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Miniland/MinigameScoreEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Miniland/MinigameScoreEventHandler.cs new file mode 100644 index 0000000..f4d0a20 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Miniland/MinigameScoreEventHandler.cs @@ -0,0 +1,258 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Mapster; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.MinilandExtensions; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Configurations.Miniland; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Miniland; +using WingsEmu.Game.Miniland.Events; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Miniland; + +public class MinigameScoreEventHandler : IAsyncEventProcessor +{ + private readonly IGameLanguageService _languageService; + private readonly MinigameConfiguration _minigameConfiguration; + private readonly IMinigameManager _minigameManager; + private readonly Dictionary> _minigamesDone = new(); + private readonly MinigameInteraction ThisAction = MinigameInteraction.DeclarateScore; + + public MinigameScoreEventHandler(MinigameConfiguration minigameConfiguration, IMinigameManager minigameManager, IGameLanguageService languageService) + { + _minigameConfiguration = minigameConfiguration; + _minigameManager = minigameManager; + _languageService = languageService; + } + + public async Task HandleAsync(MinigameScoreEvent e, CancellationToken cancellation) + { + MinigameScoresHolder scores = _minigameManager.GetScores(e.MapObject.InventoryItem.ItemInstance.ItemVNum); + + ScoreHolder scoreHolder = GetScoreHolder(e, scores); + + MinilandInteractionInformationHolder minilandInteraction = _minigameManager.GetLastInteraction(e.Sender); + + if (minilandInteraction.Interaction != MinigameInteraction.DeclaratePlay && minilandInteraction.MapObject != e.MapObject) + { + _minigameManager.ReportInteractionIncoherence(e.Sender, minilandInteraction.Interaction, minilandInteraction.MapObject, ThisAction, e.MapObject); + return; + } + + bool noReward = false; + + ScoreValidity scoreValidity = CheckScoreValidity(e, scoreHolder, scores, minilandInteraction); + switch (scoreValidity) + { + case ScoreValidity.Valid: + break; + case ScoreValidity.NotValid: + if (!_minigameConfiguration.Configuration.AntiExploitConfiguration.GiveRewardsToPossibleFalsePositives) + { + noReward = true; + } + + break; + case ScoreValidity.Abusive: + noReward = true; + break; + default: + throw new ArgumentOutOfRangeException(); + } + + bool isProbableBot = IsProbableBot(e, minilandInteraction); + if (isProbableBot) + { + if (!_minigameConfiguration.Configuration.AntiExploitConfiguration.GiveRewardsToPossibleFalsePositives) + { + noReward = true; + } + } + + Minigame minigameConfiguration = _minigameManager.GetSpecificMinigameConfiguration(e.MapObject.InventoryItem.ItemInstance.ItemVNum); + await e.Sender.EmitEventAsync(new MinigameScoreLogEvent + { + OwnerId = e.MapObject.CharacterId, + CompletionTime = DateTime.UtcNow.Subtract(minilandInteraction.TimeOfInteraction), + MinigameVnum = e.MapObject.InventoryItem.ItemInstance.ItemVNum, + MinigameType = minigameConfiguration.Type, + Score1 = e.Score1, + Score2 = e.Score2 + }); + + if (e.Sender.PlayerEntity.MinilandPoint < _minigameConfiguration.Configuration.MinigamePointsCostPerMinigame) + { + e.Sender.SendInfo(_languageService.GetLanguage(GameDialogKey.MINILAND_INFO_NOT_ENOUGH_PRODUCTION_POINTS, e.Sender.UserLanguage)); + return; + } + + if (noReward) + { + scoreHolder.RewardLevel = RewardLevel.NoReward; + } + + if (scoreHolder.RewardLevel == RewardLevel.NoReward) + { + e.Sender.SendInfo(_languageService.GetLanguage(GameDialogKey.MINIGAME_INFO_NO_REWARD, e.Sender.UserLanguage)); + return; + } + + List rewards = _minigameManager.GetSpecificMinigameConfiguration(e.MapObject.InventoryItem.ItemInstance.ItemVNum).Rewards; + MinigameRewards reward = rewards.FirstOrDefault(x => x.RewardLevel == scoreHolder.RewardLevel); + if (reward != null && e.MapObject.InventoryItem.ItemInstance.DurabilityPoint < reward.DurabilityCost) + { + e.Sender.SendInfo(_languageService.GetLanguage(GameDialogKey.MINILAND_INFO_NOT_ENOUGH_DURABILITY_POINT, e.Sender.UserLanguage)); + return; + } + + _minigameManager.RegisterInteraction(e.Sender, new MinilandInteractionInformationHolder(ThisAction, e.MapObject, (scoreHolder.RewardLevel, rewards))); + e.Sender.SendMinigameRewardLevel(scoreHolder.RewardLevel); + } + + /// + /// Returns true if it is valid. + /// + /// + private ScoreValidity CheckScoreValidity(MinigameScoreEvent e, ScoreHolder scoreHolder, MinigameScoresHolder minigameScoresHolder, MinilandInteractionInformationHolder minilandInteractionInfo) + { + long number1 = Math.Max(e.Score1, e.Score2); + long number2 = Math.Min(e.Score1, e.Score2); + long result = Math.Abs(number1 - number2); + + if (result > 10) + { + e.Sender.NotifyStrangeBehavior(StrangeBehaviorSeverity.ABUSING, $"Minigame score incoherence -> Score1: {e.Score1.ToString()} | Score2: {e.Score2.ToString()}"); + return ScoreValidity.Abusive; + } + + TimeSpan timeInWhichTheMinigameWasDone = DateTime.UtcNow.Subtract(minilandInteractionInfo.TimeOfInteraction); + + if (scoreHolder.MinimumTimeOfCompletion <= timeInWhichTheMinigameWasDone) + { + return ScoreValidity.Valid; + } + + if (timeInWhichTheMinigameWasDone <= scoreHolder.MinimumTimeOfCompletion * _minigameConfiguration.Configuration.AntiExploitConfiguration.MinigameAbuseDetectionThreshold) + { + e.Sender.NotifyStrangeBehavior(StrangeBehaviorSeverity.SEVERE_ABUSE, + $"Minigame was done under the Abuse Detection Threshold -> MinigameType: {minigameScoresHolder.Type} | RewardLevel: {scoreHolder.RewardLevel}" + + $" | TimeSpan of minigame's completion: {timeInWhichTheMinigameWasDone.ToString()}"); + return ScoreValidity.Abusive; + } + + e.Sender.NotifyStrangeBehavior(StrangeBehaviorSeverity.ABUSING, + $"Minigame was done under the Expected Minimum Time Of Completion -> MinigameType: {minigameScoresHolder.Type} | RewardLevel: {scoreHolder.RewardLevel}" + + $" | TimeSpan of minigame's completion: {timeInWhichTheMinigameWasDone.ToString()}"); + return ScoreValidity.NotValid; + } + + /// + /// Returns true if it is probably a bot. + /// + /// + private bool IsProbableBot(MinigameScoreEvent e, MinilandInteractionInformationHolder minilandInteractionInfo) + { + TimeSpan timeOfAllMinigameCompletion = DateTime.UtcNow.Subtract(minilandInteractionInfo.TimeOfInteraction); + AddMinigameDone(e.Sender, e.Score1, timeOfAllMinigameCompletion); + + MinigameDone[] minigamesDone = GetMinigamesDone(e.Sender).ToArray(); + + bool isBot = false; + + minigamesDone.Aggregate(timeOfAllMinigameCompletion, (current, minigameDone) => current.Add(minigameDone.TimeSpan)); + + if (timeOfAllMinigameCompletion >= _minigameConfiguration.Configuration.AntiExploitConfiguration.CommonTimeExpendedInMinigamesPerDay) + { + e.Sender.NotifyStrangeBehavior(StrangeBehaviorSeverity.ABUSING, + $"In 1 day {timeOfAllMinigameCompletion.ToString()} has been expended playing minigames have been played, possible bot."); + isBot = true; + } + + if (minigamesDone.Length < _minigameConfiguration.Configuration.AntiExploitConfiguration.UseSameScoreCheckAtXMinigames) + { + return isBot; + } + + MinigameDone[] minigamesWithUniqueScore = minigamesDone.Distinct().ToArray(); + + foreach (MinigameDone minigameDone in minigamesWithUniqueScore) + { + int count = minigamesDone.Count(m => m.Score == minigameDone.Score); + + if (count / Convert.ToDouble(minigamesDone.Length) < _minigameConfiguration.Configuration.AntiExploitConfiguration.PercentageForSameScoreCheck) + { + continue; + } + + isBot = true; + e.Sender.NotifyStrangeBehavior(StrangeBehaviorSeverity.ABUSING, $"In 1 day {count.ToString()} minigames have had the same score, possible bot."); + } + + return isBot; + } + + private ScoreHolder GetScoreHolder(MinigameScoreEvent e, MinigameScoresHolder scoresHolder) + { + ScoreHolder scores = scoresHolder.Scores.FirstOrDefault(x => x.ScoreRange.Minimum <= e.Score1 && e.Score1 <= x.ScoreRange.Maximum); + return scores?.Adapt(); + } + + private void AddMinigameDone(IClientSession session, long score, TimeSpan timeSpan) + { + var minigameDone = new MinigameDone + { + DateTime = DateTime.UtcNow, + TimeSpan = timeSpan, + Score = score + }; + + if (_minigamesDone.TryGetValue(session.PlayerEntity.Id, out List list)) + { + list.Add(minigameDone); + _minigamesDone[session.PlayerEntity.Id] = list; + return; + } + + _minigamesDone.TryAdd(session.PlayerEntity.Id, new List + { + minigameDone + }); + } + + private IEnumerable GetMinigamesDone(IClientSession session) + { + if (!_minigamesDone.TryGetValue(session.PlayerEntity.Id, out List list)) + { + return new List(); + } + + var modifiedList = list.Where(x => x.DateTime.Day < 1).ToList(); + _minigamesDone[session.PlayerEntity.Id] = modifiedList; + return modifiedList; + } +} + +public class MinigameDone +{ + public DateTime DateTime { get; set; } + + public TimeSpan TimeSpan { get; set; } + + public long Score { get; set; } + public static bool operator ==(MinigameDone a, MinigameDone b) => b != null && a != null && a.Score == b.Score; + + public static bool operator !=(MinigameDone a, MinigameDone b) => !(a == b); +} + +public enum ScoreValidity +{ + Valid, + NotValid, + Abusive +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Miniland/MinigameStopEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Miniland/MinigameStopEventHandler.cs new file mode 100644 index 0000000..55639bc --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Miniland/MinigameStopEventHandler.cs @@ -0,0 +1,39 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Miniland; +using WingsEmu.Game.Miniland.Events; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Miniland; + +public class MinigameStopEventHandler : IAsyncEventProcessor +{ + private const MinigameInteraction ThisAction = MinigameInteraction.DeclarateStop; + private readonly IMinigameManager _minigameManager; + + public MinigameStopEventHandler(IMinigameManager minigameManager) => _minigameManager = minigameManager; + + public Task HandleAsync(MinigameStopEvent e, CancellationToken cancellation) + { + MinilandInteractionInformationHolder lastMinilandInteraction = _minigameManager.GetLastInteraction(e.Sender); + + if (e.MinigameObject != lastMinilandInteraction.MapObject) + { + return Task.CompletedTask; + } + + if (lastMinilandInteraction.Interaction != MinigameInteraction.DeclaratePlay + && lastMinilandInteraction.Interaction != MinigameInteraction.DeclarateScore + && lastMinilandInteraction.Interaction != MinigameInteraction.GetReward) + { + _minigameManager.ReportInteractionIncoherence(e.Sender, lastMinilandInteraction.Interaction, lastMinilandInteraction.MapObject, ThisAction, e.MinigameObject); + return Task.CompletedTask; + } + + e.Sender.PlayerEntity.CurrentMinigame = 0; + e.Sender.BroadcastGuri(6, 0, e.Sender.PlayerEntity.Id); + _minigameManager.RegisterInteraction(e.Sender, new MinilandInteractionInformationHolder(ThisAction, e.MinigameObject)); + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Miniland/MinilandIntroEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Miniland/MinilandIntroEventHandler.cs new file mode 100644 index 0000000..473b211 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Miniland/MinilandIntroEventHandler.cs @@ -0,0 +1,40 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.MinilandExtensions; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Miniland; +using WingsEmu.Game.Miniland.Events; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Miniland; + +public class MinilandIntroEventHandler : IAsyncEventProcessor +{ + private readonly IGameLanguageService _languageService; + private readonly IMinilandManager _minilandManager; + + public MinilandIntroEventHandler(IMinilandManager minilandManager, IGameLanguageService languageService) + { + _minilandManager = minilandManager; + _languageService = languageService; + } + + public async Task HandleAsync(MinilandIntroEvent e, CancellationToken cancellation) + { + e.Sender.PlayerEntity.MinilandMessage = e.RequestedMinilandIntro; + + foreach (IClientSession session in e.Sender.PlayerEntity.Miniland.Sessions) + { + if (session.PlayerEntity.Id == e.Sender.PlayerEntity.Id) + { + continue; + } + + session.SendMinilandPublicInformation(_minilandManager, _languageService); + } + + e.Sender.SendInfo(_languageService.GetLanguage(GameDialogKey.MINILAND_INFO_CHANGED, e.Sender.UserLanguage)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Miniland/MinilandSignPostJoinEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Miniland/MinilandSignPostJoinEventHandler.cs new file mode 100644 index 0000000..501ec8d --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Miniland/MinilandSignPostJoinEventHandler.cs @@ -0,0 +1,73 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Miniland; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Miniland; + +public class MinilandSignPostJoinEventHandler : IAsyncEventProcessor +{ + private readonly IMinilandManager _minilandManager; + + public MinilandSignPostJoinEventHandler(IMinilandManager minilandManager) => _minilandManager = minilandManager; + + public async Task HandleAsync(MinilandSignPostJoinEvent e, CancellationToken cancellation) + { + long minilandPlayerId = e.PlayerId; + + IClientSession session = e.Sender; + if (minilandPlayerId == session.PlayerEntity.Id) + { + return; + } + + if (!session.CurrentMapInstance.HasMapFlag(MapFlags.HAS_SIGNPOSTS_ENABLED)) + { + return; + } + + INpcEntity findSignPost = session.CurrentMapInstance.GetPassiveNpcs().FirstOrDefault(x => x.MinilandOwner != null && x.MinilandOwner.Id == minilandPlayerId); + IPlayerEntity minilandOwner = findSignPost?.MinilandOwner; + if (minilandOwner == null) + { + return; + } + + // Don't use minilandOwner.Miniland :peepoGun: + IMapInstance miniland = _minilandManager.GetMinilandByCharacterId(minilandOwner.Id); + if (miniland == null) + { + session.SendInfo(session.GetLanguage(GameDialogKey.INFORMATION_INFO_PLAYER_OFFLINE)); + return; + } + + if (minilandOwner.MinilandState == MinilandState.LOCK || minilandOwner.MinilandState == MinilandState.PRIVATE + && !session.PlayerEntity.IsFriend(minilandOwner.Id) && !session.PlayerEntity.IsMarried(minilandOwner.Id) && !session.IsGameMaster()) + { + session.SendMsg(session.GetLanguage(GameDialogKey.MINILAND_SHOUTMESSAGE_CLOSED), MsgMessageType.Middle); + return; + } + + int count = miniland.Sessions.Count(x => x.PlayerEntity.Id != minilandOwner.Id && !x.GmMode); + int capacity = _minilandManager.GetMinilandMaximumCapacity(minilandOwner.Id); + + if (count > capacity) + { + session.SendMsg(session.GetLanguage(GameDialogKey.MINILAND_SHOUTMESSAGE_FULL), MsgMessageType.Middle); + return; + } + + session.ChangeMap(miniland); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Miniland/MinilandStateEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Miniland/MinilandStateEventHandler.cs new file mode 100644 index 0000000..112ef69 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Miniland/MinilandStateEventHandler.cs @@ -0,0 +1,68 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Miniland; +using WingsEmu.Game.Miniland.Events; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Miniland; + +public class MinilandStateEventHandler : IAsyncEventProcessor +{ + private readonly IGameLanguageService _languageService; + private readonly IMinilandManager _minilandManager; + + public MinilandStateEventHandler(IGameLanguageService languageService, IMinilandManager minilandManager) + { + _languageService = languageService; + _minilandManager = minilandManager; + } + + public async Task HandleAsync(MinilandStateEvent e, CancellationToken cancellation) + { + GameDialogKey gameDialog; + switch (e.DesiredMinilandState) + { + case MinilandState.OPEN: + gameDialog = GameDialogKey.MINILAND_SHOUTMESSAGE_PUBLIC; + break; + case MinilandState.PRIVATE: + gameDialog = GameDialogKey.MINILAND_SHOUTMESSAGE_PRIVATE; + KickCharactersFromMiniland(e.Sender); + break; + case MinilandState.LOCK: + gameDialog = GameDialogKey.MINILAND_SHOUTMESSAGE_LOCK; + KickCharactersFromMiniland(e.Sender, true); + break; + default: + throw new ArgumentOutOfRangeException(); + } + + e.Sender.SendMsg(_languageService.GetLanguage(gameDialog, e.Sender.UserLanguage), MsgMessageType.Middle); + e.Sender.PlayerEntity.MinilandState = e.DesiredMinilandState; + } + + private void KickCharactersFromMiniland(IClientSession minilandOwner, bool kickFriends = false) + { + foreach (IClientSession session in minilandOwner.PlayerEntity.Miniland.Sessions) + { + if (session.PlayerEntity.Id == minilandOwner.PlayerEntity.Id) + { + continue; + } + + if (!kickFriends && (session.PlayerEntity.IsFriend(minilandOwner.PlayerEntity.Id) || session.PlayerEntity.IsMarried(minilandOwner.PlayerEntity.Id) || session.IsGameMaster())) + { + continue; + } + + session.ChangeToLastBaseMap(); + session.SendMsg(_languageService.GetLanguage(GameDialogKey.MINILAND_SHOUTMESSAGE_CLOSED, session.UserLanguage), MsgMessageType.Middle); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Miniland/RmvObjMinilandEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Miniland/RmvObjMinilandEventHandler.cs new file mode 100644 index 0000000..9f29bcd --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Miniland/RmvObjMinilandEventHandler.cs @@ -0,0 +1,63 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.MinilandExtensions; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Miniland; +using WingsEmu.Game.Miniland.Events; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Miniland; + +public class RmvObjMinilandEventHandler : IAsyncEventProcessor +{ + private readonly IGameLanguageService _languageService; + private readonly IMinilandManager _minilandManager; + + public RmvObjMinilandEventHandler(IMinilandManager minilandManager, IGameLanguageService languageService) + { + _minilandManager = minilandManager; + _languageService = languageService; + } + + public async Task HandleAsync(RmvObjMinilandEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + IPlayerEntity character = e.Sender.PlayerEntity; + if (character.MapInstanceId != character.Miniland.Id) + { + return; + } + + if (character.MinilandState != MinilandState.LOCK) + { + session.SendMsg(session.GetLanguage(GameDialogKey.MINILAND_SHOUTMESSAGE_NEED_BE_LOCKED), MsgMessageType.Middle); + return; + } + + MapDesignObject mapObject = character.Miniland.MapDesignObjects.FirstOrDefault(x => x.InventoryItem.Slot == e.Slot); + if (mapObject == default) + { + return; + } + + if (mapObject.InventoryItem.ItemInstance.GameItem.IsWarehouse) + { + character.WareHouseSize = 0; + } + + if (mapObject.InventoryItem.ItemInstance.GameItem.ItemType == ItemType.House) + { + _minilandManager.RelativeUpdateMinilandCapacity(e.Sender.PlayerEntity.Id, -mapObject.InventoryItem.ItemInstance.GameItem.MinilandObjectPoint); + } + + character.Miniland.MapDesignObjects.Remove(mapObject); + character.Miniland.Broadcast(mapObject.GenerateEffect(true)); + character.Miniland.Broadcast(mapObject.GenerateMinilandObject(true)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Miniland/UseObjMinilandEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Miniland/UseObjMinilandEventHandler.cs new file mode 100644 index 0000000..d8446bb --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Miniland/UseObjMinilandEventHandler.cs @@ -0,0 +1,111 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.MinilandExtensions; +using WingsEmu.Game; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Configurations.Miniland; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Miniland; +using WingsEmu.Game.Miniland.Events; +using WingsEmu.Game.Warehouse; +using WingsEmu.Game.Warehouse.Events; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Miniland; + +public class UseObjMinilandEventHandler : IAsyncEventProcessor +{ + private const MinigameInteraction ThisAction = MinigameInteraction.GetMinigameInformation; + private readonly IAccountWarehouseManager _accountWarehouseManager; + private readonly IGameLanguageService _languageService; + private readonly MinigameConfiguration _minigameConfiguration; + private readonly IMinigameManager _minigameManager; + + public UseObjMinilandEventHandler(MinigameConfiguration minigameConfiguration, IMinigameManager minigameManager, IGameLanguageService languageService, + IAccountWarehouseManager accountWarehouseManager) + { + _minigameConfiguration = minigameConfiguration; + _minigameManager = minigameManager; + _languageService = languageService; + _accountWarehouseManager = accountWarehouseManager; + } + + public async Task HandleAsync(UseObjMinilandEvent e, CancellationToken cancellation) + { + if (e.Sender.CurrentMapInstance.MapInstanceType != MapInstanceType.Miniland) + { + return; + } + + MapDesignObject mapObject = e.Sender.CurrentMapInstance.MapDesignObjects.FirstOrDefault(x => x.InventoryItem.Slot == e.Slot); + if (mapObject == default) + { + return; + } + + + switch (mapObject.InventoryItem.ItemInstance.GameItem.ItemType) + { + // last data = miniland chest + case ItemType.Minigame when mapObject.InventoryItem.ItemInstance.GameItem.Data[^1] == 1: // miniland chest flag + if (e.Sender.PlayerEntity.Miniland.Id != e.Sender.CurrentMapInstance.Id) + { + // not the owner of the miniland + return; + } + + e.Sender.EmitEvent(new MinilandChestViewContentEvent(mapObject.InventoryItem.ItemInstance.ItemVNum)); + break; + + // last data = crafting structures + case ItemType.Terrace when mapObject.InventoryItem.ItemInstance.GameItem.Data[^1] == 2: + case ItemType.Garden when mapObject.InventoryItem.ItemInstance.GameItem.Data[^1] == 2: + case ItemType.Minigame when mapObject.InventoryItem.ItemInstance.GameItem.Data[^1] == 2: // crafting structures + if (e.Sender.PlayerEntity.Miniland.Id != e.Sender.CurrentMapInstance.Id) + { + // not the owner of the miniland + return; + } + + e.Sender.EmitEvent(new RecipeOpenWindowEvent(mapObject.InventoryItem.ItemInstance.ItemVNum)); + break; + case ItemType.Minigame: + MinilandInteractionInformationHolder lastMinilandInteraction = _minigameManager.GetLastInteraction(e.Sender); + + if (lastMinilandInteraction.Interaction != MinigameInteraction.None + && lastMinilandInteraction.Interaction != MinigameInteraction.GetMinigameInformation + && lastMinilandInteraction.Interaction != MinigameInteraction.DeclarateStop + && lastMinilandInteraction.Interaction != MinigameInteraction.GetMinigameDurability + && lastMinilandInteraction.Interaction != MinigameInteraction.GetYieldInformation + && lastMinilandInteraction.Interaction != MinigameInteraction.GetYieldReward + && lastMinilandInteraction.Interaction != MinigameInteraction.UseDurabilityCoupon + && lastMinilandInteraction.Interaction != MinigameInteraction.RepairMinigameDurability + && !((lastMinilandInteraction.Interaction == MinigameInteraction.DeclaratePlay || lastMinilandInteraction.Interaction == MinigameInteraction.GetReward) + && lastMinilandInteraction.MapObject.Id == mapObject.Id)) + { + _minigameManager.ReportInteractionIncoherence(e.Sender, lastMinilandInteraction.Interaction, lastMinilandInteraction.MapObject, ThisAction, mapObject); + } + + Minigame minigameConfig = _minigameManager.GetSpecificMinigameConfiguration(mapObject.InventoryItem.ItemInstance.ItemVNum); + if (e.Sender.PlayerEntity.Level < minigameConfig.MinimumLevel + || e.Sender.PlayerEntity.Reput < minigameConfig.MinimumReputation) + { + e.Sender.SendErrorChatMessage(string.Format( + _languageService.GetLanguage(GameDialogKey.MINILAND_CHATMESSAGE_NOT_FULLFILLING_MINIGAME_REQUIREMENTS, e.Sender.UserLanguage), + minigameConfig.MinimumLevel.ToString(), + minigameConfig.MinimumReputation.ToString())); + return; + } + + _minigameManager.RegisterInteraction(e.Sender, new MinilandInteractionInformationHolder(ThisAction, mapObject)); + e.Sender.SendMinigameInfo(mapObject, _minigameConfiguration, _minigameManager.GetScores(mapObject.InventoryItem.ItemInstance.ItemVNum)); + break; + case ItemType.House when mapObject.InventoryItem.ItemInstance.GameItem.IsWarehouse: + await e.Sender.EmitEventAsync(new AccountWarehouseOpenEvent()); + break; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Monster/MonsterDeathEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Monster/MonsterDeathEventHandler.cs new file mode 100644 index 0000000..93b48bc --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Monster/MonsterDeathEventHandler.cs @@ -0,0 +1,230 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Scripting.Object.Timespace; +using WingsEmu.DTOs.BCards; +using WingsEmu.Game._enum; +using WingsEmu.Game.Act4; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Battle.Managers; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Monster.Event; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Quests.Event; +using WingsEmu.Game.TimeSpaces; +using WingsEmu.Game.TimeSpaces.Events; +using WingsEmu.Game.Triggers; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Monster; + +public class MonsterDeathEventHandler : IAsyncEventProcessor +{ + private readonly IAct4FlagManager _act4FlagManager; + private readonly IAsyncEventPipeline _asyncEventPipeline; + private readonly IBCardEffectHandlerContainer _bCardEffectHandlerContainer; + private readonly IPhantomPositionManager _phantomPositionManager; + private readonly ITimeSpaceManager _timeSpaceManager; + + public MonsterDeathEventHandler(IBCardEffectHandlerContainer bCardEffectHandlerContainer, IAsyncEventPipeline asyncEventPipeline, + ITimeSpaceManager timeSpaceManager, IAct4FlagManager act4FlagManager, IPhantomPositionManager phantomPositionManager) + { + _bCardEffectHandlerContainer = bCardEffectHandlerContainer; + _asyncEventPipeline = asyncEventPipeline; + _timeSpaceManager = timeSpaceManager; + _act4FlagManager = act4FlagManager; + _phantomPositionManager = phantomPositionManager; + } + + public async Task HandleAsync(MonsterDeathEvent e, CancellationToken cancellation) + { + IMonsterEntity monsterEntity = e.MonsterEntity; + + DateTime dateTimeNow = DateTime.UtcNow; + monsterEntity.IsStillAlive = false; + monsterEntity.Hp = 0; + monsterEntity.Death = dateTimeNow; + monsterEntity.Killer = e.Killer; + + if (monsterEntity.SummonerType is not VisualType.Player) + { + monsterEntity.MapInstance.IncreaseMonsterDeathsOnMap(); + } + + monsterEntity.MapInstance.DeactivateMode(monsterEntity); + + if (monsterEntity.DeathEffect != 0) + { + monsterEntity.BroadcastEffectInRange(monsterEntity.DeathEffect); + } + + if (monsterEntity.IsPhantom()) + { + _phantomPositionManager.AddPosition(monsterEntity.UniqueId, monsterEntity.Position); + } + + await monsterEntity.TriggerEvents(BattleTriggers.OnDeath); + if (!e.IsByCommand) + { + IEnumerable triggerBCard = monsterEntity.BCards.Where(b => b.TriggerType == BCardNpcMonsterTriggerType.ON_DEATH); + foreach (BCardDTO bCard in triggerBCard) + { + _bCardEffectHandlerContainer.Execute(monsterEntity, monsterEntity, bCard, triggerType: BCardNpcMonsterTriggerType.ON_DEATH); + } + } + + if (monsterEntity.MapInstance.MapInstanceType == MapInstanceType.TimeSpaceInstance) + { + await CheckTargetMonstersInRoom(monsterEntity); + await _asyncEventPipeline.ProcessEventAsync(new TimeSpaceCheckMonsterEvent(monsterEntity), cancellation); + } + + monsterEntity.Mp = 0; + + switch (e.Killer) + { + case IPlayerEntity player: + await player.Session.EmitEventAsync(new KillBonusEvent + { + MonsterEntity = monsterEntity + }); + if (player.IsInGroup()) + { + foreach (IPlayerEntity member in player.GetGroup().Members.ToArray()) + { + await member.Session.EmitEventAsync(new QuestMonsterDeathEvent { MonsterEntity = monsterEntity }); + } + } + else + { + await player.Session.EmitEventAsync(new QuestMonsterDeathEvent { MonsterEntity = monsterEntity }); + } + + player.HitsByMonsters.TryRemove(monsterEntity.Id, out _); + break; + case IMateEntity mate: + await mate.Owner.Session.EmitEventAsync(new KillBonusEvent + { + MonsterEntity = monsterEntity + }); + + if (mate.Owner.IsInGroup()) + { + foreach (IPlayerEntity member in mate.Owner.GetGroup().Members.ToArray()) + { + await member.Session.EmitEventAsync(new QuestMonsterDeathEvent { MonsterEntity = monsterEntity }); + } + } + else + { + await mate.Owner.Session.EmitEventAsync(new QuestMonsterDeathEvent { MonsterEntity = monsterEntity }); + } + + break; + case IMonsterEntity mapMonster: + if (!mapMonster.SummonerId.HasValue) + { + break; + } + + if (mapMonster.IsMateTrainer) + { + break; + } + + if (mapMonster.SummonerType != VisualType.Player) + { + break; + } + + IClientSession summoner = mapMonster.MapInstance.GetCharacterById(mapMonster.SummonerId.Value)?.Session; + if (summoner == null) + { + break; + } + + if (monsterEntity.MonsterVNum == (short)MonsterVnum.BOMB) + { + summoner.PlayerEntity.SkillComponent.BombEntityId = null; + } + + await summoner.EmitEventAsync(new KillBonusEvent + { + MonsterEntity = monsterEntity + }); + + if (summoner.PlayerEntity.IsInGroup()) + { + foreach (IPlayerEntity member in summoner.PlayerEntity.GetGroup().Members.ToArray()) + { + await member.Session.EmitEventAsync(new QuestMonsterDeathEvent { MonsterEntity = monsterEntity }); + } + } + else + { + await summoner.EmitEventAsync(new QuestMonsterDeathEvent { MonsterEntity = monsterEntity }); + } + + break; + } + + switch ((MonsterVnum)monsterEntity.MonsterVNum) + { + case MonsterVnum.DEMON_CAMP: + _act4FlagManager.RemoveDemonFlag(); + break; + case MonsterVnum.ANGEL_CAMP: + _act4FlagManager.RemoveAngelFlag(); + break; + } + + await monsterEntity.RemoveAllBuffsAsync(true); + monsterEntity.MapInstance.ForgetAll(monsterEntity, dateTimeNow); + } + + private async Task CheckTargetMonstersInRoom(IMonsterEntity monsterEntity) + { + if (!monsterEntity.IsTarget) + { + return; + } + + Guid guid = monsterEntity.MapInstance.Id; + TimeSpaceParty timeSpace = _timeSpaceManager.GetTimeSpaceByMapInstanceId(guid); + if (timeSpace == null) + { + return; + } + + if (timeSpace.Instance.TimeSpaceObjective.KillMonsterVnum.HasValue && monsterEntity.MonsterVNum == timeSpace.Instance.TimeSpaceObjective.KillMonsterVnum.Value) + { + timeSpace.Instance.TimeSpaceObjective.KilledMonsterAmount++; + await _asyncEventPipeline.ProcessEventAsync(new TimeSpaceRefreshObjectiveProgressEvent + { + MapInstanceId = guid + }); + } + + if (monsterEntity.MapInstance.GetAliveMonsters(x => x.IsTarget).Any()) + { + return; + } + + TimeSpaceSubInstance timeSpaceSubInstance = _timeSpaceManager.GetSubInstance(guid); + if (timeSpaceSubInstance == null) + { + return; + } + + await timeSpaceSubInstance.TriggerEvents(TimespaceConstEventKeys.OnAllTargetMobsDead); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Monster/MonsterSummonEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Monster/MonsterSummonEventHandler.cs new file mode 100644 index 0000000..6cfa286 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Monster/MonsterSummonEventHandler.cs @@ -0,0 +1,209 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Core.Extensions; +using WingsEmu.Game; +using WingsEmu.Game._enum; +using WingsEmu.Game.Battle.Managers; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Entities.Event; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Monster.Event; +using WingsEmu.Game.Npcs.Event; +using WingsEmu.Game.Triggers; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Monster; + +public class MonsterSummonEventHandler : IAsyncEventProcessor +{ + private readonly IMonsterEntityFactory _monsterEntityFactory; + private readonly INpcMonsterManager _npcMonsterManager; + private readonly IPhantomPositionManager _phantomPositionManager; + private readonly IRandomGenerator _randomGenerator; + + public MonsterSummonEventHandler(IRandomGenerator randomGenerator, INpcMonsterManager npcMonsterManager, IMonsterEntityFactory monsterEntityFactory, IPhantomPositionManager phantomPositionManager) + { + _randomGenerator = randomGenerator; + _npcMonsterManager = npcMonsterManager; + _monsterEntityFactory = monsterEntityFactory; + _phantomPositionManager = phantomPositionManager; + } + + public async Task HandleAsync(MonsterSummonEvent e, CancellationToken cancellation) + { + List toSummon = new(); + short? scaledAmount = e.ScaledWithPlayerAmount; + + if (scaledAmount.HasValue) + { + for (int i = 0; i < scaledAmount.Value; i++) + { + toSummon.AddRange(e.Monsters); + } + } + else + { + toSummon = e.Monsters.ToList(); + } + + foreach (ToSummon summon in toSummon) + { + IMonsterData npcMonster = _npcMonsterManager.GetNpc(summon.VNum); + if (npcMonster == null || _randomGenerator.RandomNumber() > summon.SummonChance) + { + continue; + } + + if (e.Map.IsSummonLimitReached(e.Summoner?.Id, summon.SummonType)) + { + continue; + } + + + IMonsterEntity mapMonster = summon.MonsterEntity ?? _monsterEntityFactory.CreateMonster(npcMonster, e.Map, new MonsterEntityBuilder + { + IsMateTrainer = summon.IsMateTrainer, + IsBonus = summon.IsBonusOrProtected, + IsBoss = summon.IsBossOrMate, + IsTarget = summon.IsTarget, + IsHostile = summon.IsHostile, + IsWalkingAround = summon.IsMoving, + IsVesselMonster = summon.IsVesselMonster, + SummonType = summon.SummonType, + SummonerId = e.Summoner?.Id, + SummonerType = e.Summoner?.Type, + FactionType = summon.FactionType ?? e.Summoner?.Faction, + HpMultiplier = summon.HpMultiplier, + MpMultiplier = summon.MpMultiplier, + SetHitChance = summon.SetHitChance != 0 && !summon.IsBossOrMate ? summon.SetHitChance : npcMonster.BasicHitChance, + Direction = summon.Direction, + GoToBossPosition = summon.GoToBossPosition, + IsInstantBattle = summon.IsInstantBattle, + Level = summon.Level + }); + + mapMonster.Target = summon.Target; + mapMonster.Waypoints = summon.Waypoints; + + if (summon.TriggerEvents != null) + { + foreach ((string key, IAsyncEvent asyncEvent, bool removeOnUse) in summon.TriggerEvents) + { + mapMonster.AddEvent(key, asyncEvent, removeOnUse); + } + } + + if (e.Summoner != null) + { + if (e.GetSummonerLevel) + { + mapMonster.Level = e.Summoner.Level; + } + + if (e.Summoner is IPlayerEntity player && summon.VNum == (short)MonsterVnum.BOMB) + { + player.SkillComponent.BombEntityId = mapMonster.Id; + } + } + + Position? phantomPosition = e.NpcId.HasValue ? _phantomPositionManager.GetPosition(e.NpcId.Value) : null; + Position spawnCell = phantomPosition ?? summon.SpawnCell ?? e.Map.GetRandomPosition(); + + if (summon.AtAroundMobId.HasValue && summon.AtAroundMobRange.HasValue) + { + IMonsterEntity originalEntity = e.Map.GetMonsterByUniqueId(summon.AtAroundMobId.Value); + byte range = summon.AtAroundMobRange.Value; + if (originalEntity is not null) + { + spawnCell = new Position((short)(originalEntity.PositionX + _randomGenerator.RandomNumber(-range, range)), + (short)(originalEntity.PositionY + _randomGenerator.RandomNumber(-range, range))); + + if (originalEntity.MapInstance.IsBlockedZone(spawnCell.X, spawnCell.Y)) + { + spawnCell = originalEntity.Position; + } + } + } + + await mapMonster.EmitEventAsync(new MapJoinMonsterEntityEvent(mapMonster, spawnCell.X, spawnCell.Y, e.ShowEffect)); + + CheckBeriosPhantom(mapMonster); + + DateTime now = DateTime.UtcNow; + mapMonster.AttentionTime = now + TimeSpan.FromSeconds(10); + if (!summon.RemoveTick) + { + continue; + } + + mapMonster.NextTick = DateTime.UtcNow.AddSeconds(-1000); + mapMonster.NextAttackReady = DateTime.UtcNow.AddSeconds(-1000); + } + } + + private void CheckBeriosPhantom(IMonsterEntity mapMonster) + { + switch ((MonsterVnum)mapMonster.MonsterVNum) + { + case MonsterVnum.EMERALD_PHANTOM: + + mapMonster.AddEvent(BattleTriggers.OnDeath, new NpcSummonEvent + { + Map = mapMonster.MapInstance, + MonsterId = mapMonster.UniqueId, + Npcs = Lists.Create(new ToSummon + { + VNum = (short)MonsterVnum.EMERALD_SHADOW_PHANTOM, + SpawnCell = mapMonster.Position, + IsMoving = true, + IsHostile = true + }) + }, true); + + break; + + case MonsterVnum.SAPPHIRE_PHANTOM: + + mapMonster.AddEvent(BattleTriggers.OnDeath, new NpcSummonEvent + { + Map = mapMonster.MapInstance, + MonsterId = mapMonster.UniqueId, + Npcs = Lists.Create(new ToSummon + { + VNum = (short)MonsterVnum.SAPPHIRE_SHADOW_PHANTOM, + SpawnCell = mapMonster.Position, + IsMoving = true, + IsHostile = true + }) + }, true); + + break; + + case MonsterVnum.RUBY_PHANTOM: + + mapMonster.AddEvent(BattleTriggers.OnDeath, new NpcSummonEvent + { + Map = mapMonster.MapInstance, + MonsterId = mapMonster.UniqueId, + Npcs = Lists.Create(new ToSummon + { + VNum = (short)MonsterVnum.RUBY_SHADOW_PHANTOM, + SpawnCell = mapMonster.Position, + IsMoving = true, + IsHostile = true + }) + }, true); + + break; + + default: + return; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Npcs/MapNpcGenerateDeathEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Npcs/MapNpcGenerateDeathEventHandler.cs new file mode 100644 index 0000000..a6a535b --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Npcs/MapNpcGenerateDeathEventHandler.cs @@ -0,0 +1,75 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Battle.Managers; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Npcs.Event; +using WingsEmu.Game.TimeSpaces; +using WingsEmu.Game.TimeSpaces.Enums; +using WingsEmu.Game.TimeSpaces.Events; +using WingsEmu.Game.Triggers; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Npcs; + +public class MapNpcGenerateDeathEventHandler : IAsyncEventProcessor +{ + private readonly IAsyncEventPipeline _asyncEventPipeline; + private readonly IPhantomPositionManager _phantomPositionManager; + private readonly ITimeSpaceManager _timeSpaceManager; + + public MapNpcGenerateDeathEventHandler(ITimeSpaceManager timeSpaceManager, IAsyncEventPipeline asyncEventPipeline, IPhantomPositionManager phantomPositionManager) + { + _timeSpaceManager = timeSpaceManager; + _asyncEventPipeline = asyncEventPipeline; + _phantomPositionManager = phantomPositionManager; + } + + public async Task HandleAsync(MapNpcGenerateDeathEvent e, CancellationToken cancellation) + { + INpcEntity npc = e.NpcEntity; + DateTime currentTime = DateTime.UtcNow; + npc.IsStillAlive = false; + npc.Hp = 0; + npc.Mp = 0; + npc.Death = currentTime; + await npc.RemoveAllBuffsAsync(true); + npc.Target = null; + npc.Killer = e.Killer; + + if (npc.IsPhantom()) + { + _phantomPositionManager.AddPosition(npc.UniqueId, npc.Position); + } + + await npc.TriggerEvents(BattleTriggers.OnDeath); + + if (npc.MapInstance.MapInstanceType != MapInstanceType.TimeSpaceInstance) + { + return; + } + + if (!npc.IsProtected) + { + return; + } + + TimeSpaceParty timeSpace = _timeSpaceManager.GetTimeSpaceByMapInstanceId(npc.MapInstance.Id); + if (timeSpace == null) + { + return; + } + + timeSpace.Instance.KilledProtectedNpcs++; + + if (!timeSpace.Instance.TimeSpaceObjective.ProtectNPC) + { + return; + } + + await _asyncEventPipeline.ProcessEventAsync(new TimeSpaceInstanceFinishEvent(timeSpace, TimeSpaceFinishType.NPC_DIED)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Npcs/NpcDialogEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Npcs/NpcDialogEventHandler.cs new file mode 100644 index 0000000..ea99084 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Npcs/NpcDialogEventHandler.cs @@ -0,0 +1,19 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game._NpcDialog; +using WingsEmu.Game._NpcDialog.Event; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Npcs; + +public class NpcDialogEventHandler : IAsyncEventProcessor +{ + private readonly INpcDialogHandlerContainer _npcDialogHandler; + + public NpcDialogEventHandler(INpcDialogHandlerContainer npcDialogHandler) => _npcDialogHandler = npcDialogHandler; + + public async Task HandleAsync(NpcDialogEvent e, CancellationToken cancellation) + { + await _npcDialogHandler.ExecuteAsync(e.Sender, e); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Npcs/NpcSummonEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Npcs/NpcSummonEventHandler.cs new file mode 100644 index 0000000..e338d7c --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Npcs/NpcSummonEventHandler.cs @@ -0,0 +1,127 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Core.Extensions; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game; +using WingsEmu.Game._enum; +using WingsEmu.Game.Battle.Managers; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Entities.Event; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Monster.Event; +using WingsEmu.Game.Npcs; +using WingsEmu.Game.Npcs.Event; +using WingsEmu.Game.Triggers; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Npcs; + +public class NpcSummonEventHandler : IAsyncEventProcessor +{ + private readonly INpcEntityFactory _npcEntityFactory; + private readonly INpcMonsterManager _npcMonsterManager; + private readonly IPhantomPositionManager _phantomPositionManager; + + public NpcSummonEventHandler(INpcEntityFactory npcEntityFactory, INpcMonsterManager npcMonsterManager, IPhantomPositionManager phantomPositionManager) + { + _npcEntityFactory = npcEntityFactory; + _npcMonsterManager = npcMonsterManager; + _phantomPositionManager = phantomPositionManager; + } + + public async Task HandleAsync(NpcSummonEvent e, CancellationToken cancellation) + { + foreach (ToSummon summon in e.Npcs) + { + var npcMonster = new MonsterData(_npcMonsterManager.GetNpc(summon.VNum)); + INpcEntity mapNpc = _npcEntityFactory.CreateNpc(npcMonster, e.Map, npcAdditionalData: new NpcAdditionalData + { + CanMove = summon.IsMoving, + CanAttack = summon.IsHostile, + IsHostile = summon.IsHostile, + NpcDirection = summon.Direction, + NpcShouldRespawn = false, + FactionType = e.Map.HasMapFlag(MapFlags.ACT_4) ? FactionType.Angel : FactionType.Neutral // Just random Faction + }); + + mapNpc.Target = summon.Target; + + if (summon.TriggerEvents != null) + { + foreach ((string key, IAsyncEvent asyncEvent, bool removeOnUse) in summon.TriggerEvents) + { + mapNpc.AddEvent(key, asyncEvent, removeOnUse); + } + } + + Position? phantomPosition = e.MonsterId.HasValue ? _phantomPositionManager.GetPosition(e.MonsterId.Value) : null; + Position spawnCell = phantomPosition ?? summon.SpawnCell ?? e.Map.GetRandomPosition(); + + await mapNpc.EmitEventAsync(new MapJoinNpcEntityEvent(mapNpc, spawnCell.X, spawnCell.Y)); + + CheckBeriosPhantom(mapNpc); + + if (!summon.RemoveTick) + { + continue; + } + + mapNpc.NextTick = DateTime.UtcNow.AddSeconds(-1000); + mapNpc.NextAttackReady = DateTime.UtcNow.AddSeconds(-1000); + } + } + + private void CheckBeriosPhantom(INpcEntity mapNpc) + { + switch ((MonsterVnum)mapNpc.MonsterVNum) + { + case MonsterVnum.EMERALD_SHADOW_PHANTOM: + + mapNpc.AddEvent(BattleTriggers.OnDeath, new MonsterSummonEvent(mapNpc.MapInstance, Lists.Create(new ToSummon + { + VNum = (short)MonsterVnum.EMERALD_PHANTOM, + SpawnCell = mapNpc.Position, + IsMoving = true, + IsHostile = true + })) + { + NpcId = mapNpc.UniqueId + }, true); + + break; + + case MonsterVnum.SAPPHIRE_SHADOW_PHANTOM: + + mapNpc.AddEvent(BattleTriggers.OnDeath, new MonsterSummonEvent(mapNpc.MapInstance, Lists.Create(new ToSummon + { + VNum = (short)MonsterVnum.SAPPHIRE_PHANTOM, + SpawnCell = mapNpc.Position, + IsMoving = true, + IsHostile = true + })) + { + NpcId = mapNpc.UniqueId + }, true); + + break; + + case MonsterVnum.RUBY_SHADOW_PHANTOM: + + mapNpc.AddEvent(BattleTriggers.OnDeath, new MonsterSummonEvent(mapNpc.MapInstance, Lists.Create(new ToSummon + { + VNum = (short)MonsterVnum.RUBY_PHANTOM, + SpawnCell = mapNpc.Position, + IsMoving = true, + IsHostile = true + })) + { + NpcId = mapNpc.UniqueId + }, true); + + break; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Relations/AddRelationEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Relations/AddRelationEventHandler.cs new file mode 100644 index 0000000..d0b718a --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Relations/AddRelationEventHandler.cs @@ -0,0 +1,32 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Communication.Relation; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Relations; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Relations; + +public class AddRelationEventHandler : IAsyncEventProcessor +{ + private readonly IRelationService _relationService; + + public AddRelationEventHandler(IRelationService relationService) => _relationService = relationService; + + public async Task HandleAsync(AddRelationEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + + RelationAddResponse response = await _relationService.AddRelationAsync(new RelationAddRequest + { + CharacterId = session.PlayerEntity.Id, + CharacterName = session.PlayerEntity.Name, + RelationType = e.RelationType, + TargetId = e.TargetCharacterId + }); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Relations/InvitationEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Relations/InvitationEventHandler.cs new file mode 100644 index 0000000..b4abf74 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Relations/InvitationEventHandler.cs @@ -0,0 +1,88 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Exchange.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Groups.Events; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Raids.Events; +using WingsEmu.Game.Relations; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Relations; + +public class InvitationEventHandler : IAsyncEventProcessor +{ + private readonly IGameLanguageService _gameLanguage; + private readonly IInvitationManager _invitationManager; + private readonly ISessionManager _sessionManager; + + public InvitationEventHandler(IInvitationManager invitationManager, ISessionManager sessionManager, IGameLanguageService gameLanguage) + { + _invitationManager = invitationManager; + _sessionManager = sessionManager; + _gameLanguage = gameLanguage; + } + + public async Task HandleAsync(InvitationEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + IClientSession otherSession = _sessionManager.GetSessionByCharacterId(e.TargetCharacterId); + if (otherSession == null) + { + return; + } + + bool invitationExist = _invitationManager.ContainsPendingInvitation(session.PlayerEntity.Id, e.TargetCharacterId, e.Type); + if (invitationExist) + { + return; + } + + switch (e.Type) + { + case InvitationType.Friend: + otherSession.SendDialog($"fins 2 {session.PlayerEntity.Id}", $"fins 0 {session.PlayerEntity.Id}", + _gameLanguage.GetLanguageFormat(GameDialogKey.FRIEND_DIALOG_DO_YOU_WANT_TO_ADD, otherSession.UserLanguage, session.PlayerEntity.Name)); + break; + case InvitationType.HiddenSpouse: + break; + case InvitationType.Spouse: + otherSession.SendDialog($"guri 603 1 {session.PlayerEntity.Id}", $"guri 603 0 {session.PlayerEntity.Id}", + _gameLanguage.GetLanguageFormat(GameDialogKey.WEDDING_DIALOG_REQUEST_RECEIVED, otherSession.UserLanguage, session.PlayerEntity.Name)); + break; + case InvitationType.Group: + otherSession.SendDialog($"pjoin 3 {session.PlayerEntity.Id}", $"pjoin 4 {session.PlayerEntity.Id}", + $"{_gameLanguage.GetLanguageFormat(GameDialogKey.GROUP_DIALOG_INVITED_YOU, otherSession.UserLanguage, session.PlayerEntity.Name)}"); + await session.EmitEventAsync(new GroupInvitedEvent { TargetId = e.TargetCharacterId }); + break; + case InvitationType.Exchange: + otherSession.SendDialog( + $"req_exc 2 {session.PlayerEntity.Id}", + $"req_exc 5 {session.PlayerEntity.Id}", + _gameLanguage.GetLanguageFormat(GameDialogKey.TRADE_DIALOG_INCOMING_EXCHANGE, otherSession.UserLanguage, session.PlayerEntity.Name, session.PlayerEntity.Level, + session.PlayerEntity.HeroLevel) + ); + await session.EmitEventAsync(new TradeRequestedEvent { TargetId = e.TargetCharacterId }); + break; + case InvitationType.Raid: + otherSession.SendDialog( + $"rd 1 {session.PlayerEntity.Id} 1", $"rd 1 {session.PlayerEntity.Id} 2", + _gameLanguage.GetLanguageFormat(GameDialogKey.RAID_DIALOG_INVITED_YOU, otherSession.UserLanguage, session.PlayerEntity.Name) + ); + await session.EmitEventAsync(new RaidInvitedEvent { TargetId = e.TargetCharacterId }); + break; + case InvitationType.GroupPointShare: + otherSession.SendDialog( + $"pjoin 6 {session.PlayerEntity.Id}", $"pjoin 7 {session.PlayerEntity.Id}", + _gameLanguage.GetLanguageFormat(GameDialogKey.GROUP_DIALOG_ASK_SHARE_POINT, otherSession.UserLanguage, session.PlayerEntity.Name) + ); + break; + } + + _invitationManager.AddPendingInvitation(session.PlayerEntity.Id, e.TargetCharacterId, e.Type); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Relations/RelationBlockEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Relations/RelationBlockEventHandler.cs new file mode 100644 index 0000000..d033198 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Relations/RelationBlockEventHandler.cs @@ -0,0 +1,81 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Communication.ServerApi.Protocol; +using WingsAPI.Game.Extensions.RelationsExtensions; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Relations; +using WingsEmu.Packets.Enums.Relations; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Relations; + +public class RelationBlockEventHandler : IAsyncEventProcessor +{ + private readonly IGameLanguageService _gameLanguage; + private readonly SerializableGameServer _serializableGameServer; + private readonly ISessionManager _sessionManager; + + public RelationBlockEventHandler(IGameLanguageService gameLanguage, ISessionManager sessionManager, SerializableGameServer serializableGameServer) + { + _gameLanguage = gameLanguage; + _sessionManager = sessionManager; + _serializableGameServer = serializableGameServer; + } + + public async Task HandleAsync(RelationBlockEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + + if (session.CurrentMapInstance == null) + { + return; + } + + if (session.PlayerEntity.Id == e.CharacterId) + { + return; + } + + IClientSession target = _sessionManager.GetSessionByCharacterId(e.CharacterId); + if (target == null) + { + return; + } + + if (session.PlayerEntity.IsMarried(e.CharacterId)) + { + return; + } + + if (session.PlayerEntity.IsInFamily() && session.PlayerEntity.GetFamilyMembers().FirstOrDefault(x => x.CharacterId == e.CharacterId) != null) + { + return; + } + + if (session.PlayerEntity.IsFriend(e.CharacterId)) + { + return; + } + + if (session.PlayerEntity.IsBlocking(e.CharacterId)) + { + return; + } + + if (_serializableGameServer.ChannelType == GameChannelType.ACT_4) + { + if (target.PlayerEntity.Faction != session.PlayerEntity.Faction) + { + return; + } + } + + await session.AddRelationAsync(e.CharacterId, CharacterRelationType.Blocked); + string targetName = target.PlayerEntity.Name; + session.SendInfo(_gameLanguage.GetLanguageFormat(GameDialogKey.BLACKLIST_INFO_ADDED, session.UserLanguage, targetName ?? "?")); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Relations/RelationFriendEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Relations/RelationFriendEventHandler.cs new file mode 100644 index 0000000..7144216 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Relations/RelationFriendEventHandler.cs @@ -0,0 +1,121 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.RelationsExtensions; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Relations; +using WingsEmu.Packets.ClientPackets; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Relations; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Relations; + +public class RelationFriendEventHandler : IAsyncEventProcessor +{ + private readonly IInvitationManager _invitationManager; + private readonly IGameLanguageService _language; + private readonly ISessionManager _sessionManager; + + public RelationFriendEventHandler(IGameLanguageService language, ISessionManager sessionManager, IInvitationManager invitationManager) + { + _sessionManager = sessionManager; + _invitationManager = invitationManager; + _language = language; + } + + public async Task HandleAsync(RelationFriendEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + FInsPacketType type = e.RequestType; + + if (session.PlayerEntity.IsFriendsListFull()) + { + session.SendInfo(_language.GetLanguage(GameDialogKey.FRIEND_INFO_FRIENDLIST_FULL, session.UserLanguage)); + return; + } + + long characterId = e.CharacterId; + + if (session.PlayerEntity.Id == characterId) + { + return; + } + + if (session.PlayerEntity.IsBlocking(characterId)) + { + session.SendInfo(_language.GetLanguage(GameDialogKey.BLACKLIST_INFO_BLOCKING, session.UserLanguage)); + return; + } + + if (session.CantPerformActionOnAct4()) + { + return; + } + + IClientSession otherSession = _sessionManager.GetSessionByCharacterId(characterId); + if (otherSession == null) + { + return; + } + + if (session.PlayerEntity.IsFriend(characterId)) + { + session.SendInfo(_language.GetLanguageFormat(GameDialogKey.FRIEND_INFO_ALREADY_FRIEND, session.UserLanguage, otherSession.PlayerEntity.Name)); + return; + } + + if (session.PlayerEntity.IsMarried(characterId)) + { + session.SendInfo(_language.GetLanguageFormat(GameDialogKey.FRIEND_INFO_ALREADY_FRIEND, session.UserLanguage, otherSession.PlayerEntity.Name)); + return; + } + + if (otherSession.PlayerEntity.FriendRequestBlocked) + { + session.SendInfo(_language.GetLanguage(GameDialogKey.FRIEND_INFO_REQUEST_BLOCKED, session.UserLanguage)); + return; + } + + if (otherSession.PlayerEntity.IsFriendsListFull()) + { + session.SendInfo(_language.GetLanguage(GameDialogKey.FRIEND_INFO_FRIENDLIST_FULL, session.UserLanguage)); + return; + } + + if (otherSession.PlayerEntity.IsBlocking(session.PlayerEntity.Id)) + { + session.SendInfo(_language.GetLanguage(GameDialogKey.BLACKLIST_INFO_BLOCKED, session.UserLanguage)); + return; + } + + if (type != FInsPacketType.INVITE) + { + if (!_invitationManager.ContainsPendingInvitation(otherSession.PlayerEntity.Id, session.PlayerEntity.Id, InvitationType.Friend)) + { + return; + } + + _invitationManager.RemovePendingInvitation(otherSession.PlayerEntity.Id, session.PlayerEntity.Id, InvitationType.Friend); + } + + switch (type) + { + case FInsPacketType.INVITE: + await session.EmitEventAsync(new InvitationEvent(otherSession.PlayerEntity.Id, InvitationType.Friend)); + break; + case FInsPacketType.ACCEPT: + session.SendInfo(_language.GetLanguage(GameDialogKey.FRIEND_INFO_ADDED, session.UserLanguage)); + otherSession.SendInfo(_language.GetLanguage(GameDialogKey.FRIEND_INFO_ADDED, otherSession.UserLanguage)); + + await session.AddRelationAsync(characterId, CharacterRelationType.Friend); + break; + case FInsPacketType.REFUSE: + otherSession.SendInfo(_language.GetLanguage(GameDialogKey.FRIEND_INFO_REFUSED, otherSession.UserLanguage)); + break; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Relations/RemoveRelationEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Relations/RemoveRelationEventHandler.cs new file mode 100644 index 0000000..c851fea --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Relations/RemoveRelationEventHandler.cs @@ -0,0 +1,28 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Communication.Relation; +using WingsEmu.Game.Relations; + +namespace WingsEmu.Plugins.BasicImplementations.Event.Relations; + +public class RemoveRelationEventHandler : IAsyncEventProcessor +{ + private readonly IRelationService _relationService; + + public RemoveRelationEventHandler(IRelationService relationService) => _relationService = relationService; + + public async Task HandleAsync(RemoveRelationEvent e, CancellationToken cancellation) + { + await _relationService.RemoveRelationAsync(new RelationRemoveRequest + { + CharacterId = e.Sender.PlayerEntity.Id, + RelationType = e.Type, + TargetId = e.TargetCharacterId + }); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/RespawnReturn/RespawnChangeEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/RespawnReturn/RespawnChangeEventHandler.cs new file mode 100644 index 0000000..0897d94 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/RespawnReturn/RespawnChangeEventHandler.cs @@ -0,0 +1,52 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Packets.Enums; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Game.RespawnReturn.Event; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.Event.RespawnReturn; + +public class RespawnChangeEventHandler : IAsyncEventProcessor +{ + private readonly IGameLanguageService _gameLanguage; + + public RespawnChangeEventHandler(IGameLanguageService gameLanguage) => _gameLanguage = gameLanguage; + + public async Task HandleAsync(RespawnChangeEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + + switch ((MapIds)e.MapId) + { + case MapIds.NOSVILLE: + session.PlayerEntity.HomeComponent.ChangeRespawn(RespawnType.NOSVILLE_SPAWN); + break; + case MapIds.KREM: + session.PlayerEntity.HomeComponent.ChangeRespawn(RespawnType.KREM_SPAWN); + break; + case MapIds.ALVEUS: + session.PlayerEntity.HomeComponent.ChangeRespawn(RespawnType.ALVEUS_SPAWN); + break; + case MapIds.MORTAZ_DESERT_PORT: + session.PlayerEntity.HomeComponent.ChangeAct5Respawn(Act5RespawnType.MORTAZ_DESERT_PORT); + break; + case MapIds.AKAMUR_CAMP: + session.PlayerEntity.HomeComponent.ChangeAct5Respawn(Act5RespawnType.AKAMUR_CAMP); + break; + case MapIds.DESERT_EAGLY_CITY: + session.PlayerEntity.HomeComponent.ChangeAct5Respawn(Act5RespawnType.DESERT_EAGLE_CITY); + break; + default: + await session.NotifyStrangeBehavior(StrangeBehaviorSeverity.ABUSING, "Player wanted change spawn point, but map doesn't have Teleporter NPC to change spawn."); + return; + } + + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.RESPAWNLOCATION_SHOUTMESSAGE_CHANGED, session.UserLanguage), MsgMessageType.Middle); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/RespawnReturn/RespawnPlayerEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/RespawnReturn/RespawnPlayerEventHandler.cs new file mode 100644 index 0000000..1c40fff --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/RespawnReturn/RespawnPlayerEventHandler.cs @@ -0,0 +1,73 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game; +using WingsEmu.Game._enum; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Networking; +using WingsEmu.Game.RespawnReturn.Event; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.Event.RespawnReturn; + +public class RespawnPlayerEventHandler : IAsyncEventProcessor +{ + private readonly IMapManager _mapManager; + private readonly IRandomGenerator _randomGenerator; + private readonly IRespawnDefaultConfiguration _respawnDefaultConfiguration; + + public RespawnPlayerEventHandler(IRandomGenerator randomGenerator, IRespawnDefaultConfiguration respawnDefaultConfiguration, IMapManager mapManager) + { + _randomGenerator = randomGenerator; + _respawnDefaultConfiguration = respawnDefaultConfiguration; + _mapManager = mapManager; + } + + public async Task HandleAsync(RespawnPlayerEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + IMapInstance lastMap = _mapManager.GetBaseMapInstanceByMapId(session.PlayerEntity.MapId); + + if (lastMap == null) + { + return; + } + + RespawnDefault getRespawn = _respawnDefaultConfiguration.GetReturn(session.PlayerEntity.HomeComponent.RespawnType); + if (lastMap.HasMapFlag(MapFlags.ACT_4)) + { + getRespawn = _respawnDefaultConfiguration.GetReturn(session.PlayerEntity.Faction == FactionType.Angel ? RespawnType.ACT4_ANGEL_SPAWN : RespawnType.ACT4_DEMON_SPAWN); + } + + if (lastMap.HasMapFlag(MapFlags.ACT_5_1) || lastMap.HasMapFlag(MapFlags.ACT_5_2)) + { + getRespawn = _respawnDefaultConfiguration.GetReturnAct5(session.PlayerEntity.HomeComponent.Act5RespawnType); + } + + if (getRespawn == null) + { + return; + } + + IMapInstance mapInstance = _mapManager.GetBaseMapInstanceByMapId(getRespawn.MapId); + + if (mapInstance == null) + { + return; + } + + int randomX = getRespawn.MapX + _randomGenerator.RandomNumber(getRespawn.Radius, -getRespawn.Radius); + int randomY = getRespawn.MapY + _randomGenerator.RandomNumber(getRespawn.Radius, -getRespawn.Radius); + + if (mapInstance.IsBlockedZone(randomX, randomY)) + { + randomX = getRespawn.MapX; + randomY = getRespawn.MapY; + } + + session.ChangeMap(getRespawn.MapId, (short)randomX, (short)randomY); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/RespawnReturn/ReturnChangeEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/RespawnReturn/ReturnChangeEventHandler.cs new file mode 100644 index 0000000..e84dea3 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/RespawnReturn/ReturnChangeEventHandler.cs @@ -0,0 +1,40 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.DTOs.Maps; +using WingsEmu.DTOs.Respawns; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Game.RespawnReturn.Event; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.Event.RespawnReturn; + +public class ReturnChangeEventHandler : IAsyncEventProcessor +{ + private readonly IGameLanguageService _gameLanguage; + + public ReturnChangeEventHandler(IGameLanguageService gameLanguage) => _gameLanguage = gameLanguage; + + public async Task HandleAsync(ReturnChangeEvent e, CancellationToken cancellation) + { + int mapId = e.MapId; + short mapX = e.MapX; + short mapY = e.MapY; + IClientSession session = e.Sender; + + if (!session.PlayerEntity.MapInstance.HasMapFlag(MapFlags.IS_BASE_MAP) && !e.IsByGroup) + { + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.ITEM_CHATMESSAGE_CANT_USE_THAT, session.UserLanguage), ChatMessageColorType.Yellow); + return; + } + + session.PlayerEntity.HomeComponent.ChangeReturn(new CharacterReturnDto + { + MapId = (short)mapId, + MapX = mapX, + MapY = mapY + }); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Factories/IGameObjectFactory.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Factories/IGameObjectFactory.cs new file mode 100644 index 0000000..401073c --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Factories/IGameObjectFactory.cs @@ -0,0 +1,6 @@ +namespace WingsEmu.Plugins.BasicImplementations.Factories; + +public interface IGameObjectFactory +{ + TGameObject CreateGameObject(TDto dto); +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Factories/MapDesignObjectFactory.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Factories/MapDesignObjectFactory.cs new file mode 100644 index 0000000..191e4dd --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Factories/MapDesignObjectFactory.cs @@ -0,0 +1,46 @@ +using WingsAPI.Data.Miniland; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Miniland; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.Factories; + +public interface IMapDesignObjectFactory +{ + MapDesignObject CreateGameObject(long characterId, CharacterMinilandObjectDto dto); +} + +public class MapDesignObjectFactory : IMapDesignObjectFactory +{ + private readonly ISessionManager _sessionManager; + + public MapDesignObjectFactory(ISessionManager sessionManager) => _sessionManager = sessionManager; + + public MapDesignObject CreateGameObject(long characterId, CharacterMinilandObjectDto dto) + { + IClientSession session = _sessionManager.GetSessionByCharacterId(characterId); + + InventoryItem item = session.PlayerEntity.GetItemBySlotAndType(dto.InventorySlot, InventoryType.Miniland); + if (item == null) + { + return null; + } + + return new MapDesignObject + { + Id = dto.Id, + CharacterId = characterId, + InventorySlot = item.Slot, + InventoryItem = item, + Level1BoxAmount = dto.Level1BoxAmount, + Level2BoxAmount = dto.Level2BoxAmount, + Level3BoxAmount = dto.Level3BoxAmount, + Level4BoxAmount = dto.Level4BoxAmount, + Level5BoxAmount = dto.Level5BoxAmount, + MapX = dto.MapX, + MapY = dto.MapY + }; + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Factories/MateTransportFactory.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Factories/MateTransportFactory.cs new file mode 100644 index 0000000..23d0d19 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Factories/MateTransportFactory.cs @@ -0,0 +1,26 @@ +using System.Threading; +using WingsEmu.Game.Mates; + +namespace WingsEmu.Plugins.BasicImplementations.Factories; + +public class MateTransportFactory : IMateTransportFactory +{ + private readonly ReaderWriterLockSlim _lock = new(); + + private int _mateId = 1_000_000; + + public int GenerateTransportId() + { + _lock.EnterWriteLock(); + try + { + _mateId += 1; + + return _mateId; + } + finally + { + _lock.ExitWriteLock(); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ForbiddenNames/ReloadableForbiddenNamesManager.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ForbiddenNames/ReloadableForbiddenNamesManager.cs new file mode 100644 index 0000000..281baba --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ForbiddenNames/ReloadableForbiddenNamesManager.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using WingsAPI.Communication; +using WingsAPI.Communication.Translations; +using WingsEmu.Game.Managers; + +namespace WingsEmu.Plugins.BasicImplementations.ForbiddenNames; + +public class ReloadableForbiddenNamesManager : IForbiddenNamesManager +{ + private readonly ITranslationService _translationService; + private List _bannedNames = new(); + + public ReloadableForbiddenNamesManager(ITranslationService translationService) => _translationService = translationService; + + public bool IsBanned(string name, out string s) + { + string lowerCharName = name.ToLowerInvariant(); + + // shallow copy reference + List scopedBannedNames = _bannedNames; + foreach (string bannedName in scopedBannedNames) + { + if (!lowerCharName.Contains(bannedName)) + { + continue; + } + + s = bannedName; + return true; + } + + s = string.Empty; + return false; + } + + public async Task Reload() + { + GetForbiddenWordsResponse response = await _translationService.GetForbiddenWords(new EmptyRpcRequest()); + Interlocked.Exchange(ref _bannedNames, response.ForbiddenWords?.ToList() ?? new List()); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/GameManagerPlugin.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/GameManagerPlugin.cs new file mode 100644 index 0000000..1868bee --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/GameManagerPlugin.cs @@ -0,0 +1,45 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using WingsAPI.Plugins; +using WingsEmu.Game; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Families; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Managers.ServerData; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Miniland; + +namespace WingsEmu.Plugins.BasicImplementations; + +public class GameManagerPlugin : IGamePlugin +{ + private readonly IServiceProvider _container; + + public GameManagerPlugin(IServiceProvider container) => _container = container; + + public string Name => nameof(GameManagerPlugin); + + public void OnLoad() + { + StaticMapManager.Initialize(_container.GetService()); + StaticSessionManager.Initialize(_container.GetService()); + StaticCardsManager.Initialize(_container.GetService()); + StaticItemsManager.Initialize(_container.GetService()); + StaticNpcMonsterManager.Initialize(_container.GetService()); + StaticDropManager.Initialize(_container.GetService()); + StaticSkillsManager.Initialize(_container.GetService()); + StaticMinilandManager.Initialize(_container.GetService()); + StaticScriptedInstanceManager.Initialize(_container.GetService()); + StaticRandomGenerator.Initialize(_container.GetService()); + StaticMateTransportFactory.Initialize(_container.GetService()); + StaticGameLanguageService.Initialize(_container.GetService()); + StaticBuffFactory.Initialize(_container.GetRequiredService()); + StaticSkillExecutor.Initialize(_container.GetService()); + StaticMeditationManager.Initialize(_container.GetService()); + StaticFamilyManager.Initialize(_container.GetService()); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/GameManagersPluginCore.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/GameManagersPluginCore.cs new file mode 100644 index 0000000..15282fe --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/GameManagersPluginCore.cs @@ -0,0 +1,198 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using PhoenixLib.Caching; +using PhoenixLib.Configuration; +using PhoenixLib.DAL.Redis.Locks; +using WingsAPI.Plugins; +using WingsEmu.Game; +using WingsEmu.Game.Act4.Configuration; +using WingsEmu.Game.Algorithm; +using WingsEmu.Game.Arena; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Battle.Managers; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Compliments; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Features; +using WingsEmu.Game.GameEvent; +using WingsEmu.Game.Groups; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Managers.ServerData; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Miniland; +using WingsEmu.Game.Relations; +using WingsEmu.Game.Skills; +using WingsEmu.Game.SnackFood; +using WingsEmu.Plugins.BasicImplementations.Algorithms; +using WingsEmu.Plugins.BasicImplementations.Arena; +using WingsEmu.Plugins.BasicImplementations.Bazaar; +using WingsEmu.Plugins.BasicImplementations.Compliments; +using WingsEmu.Plugins.BasicImplementations.DbServer; +using WingsEmu.Plugins.BasicImplementations.Entities; +using WingsEmu.Plugins.BasicImplementations.Event.Items; +using WingsEmu.Plugins.BasicImplementations.Factories; +using WingsEmu.Plugins.BasicImplementations.ForbiddenNames; +using WingsEmu.Plugins.BasicImplementations.InterChannel; +using WingsEmu.Plugins.BasicImplementations.Inventory; +using WingsEmu.Plugins.BasicImplementations.ItemUsage; +using WingsEmu.Plugins.BasicImplementations.Mail; +using WingsEmu.Plugins.BasicImplementations.Managers; +using WingsEmu.Plugins.BasicImplementations.Managers.StaticData; +using WingsEmu.Plugins.BasicImplementations.Miniland; +using WingsEmu.Plugins.BasicImplementations.ServerConfigs; +using WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects.Drops; +using WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects.ItemBoxes; +using WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects.Maps; +using WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects.Monsters; +using WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects.Npcs; +using WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects.Portals; +using WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects.Recipes; +using WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects.Teleporters; +using WingsEmu.Plugins.BasicImplementations.Ship; +using WingsEmu.Plugins.GameEvents; + +namespace WingsEmu.Plugins.BasicImplementations; + +public class GameManagersPluginCore : IGameServerPlugin +{ + public string Name => nameof(GameManagersPluginCore); + + + public void AddDependencies(IServiceCollection services, GameServerLoader gameServer) + { + // server configs + services.AddConfigurationsFromDirectory("map_teleporters"); + services.TryAddSingleton(); + + services.AddConfigurationsFromDirectory("random_boxes"); + services.AddConfigurationsFromDirectory("item_boxes"); + services.TryAddSingleton(); + + services.AddConfigurationsFromDirectory("recipes"); + services.TryAddSingleton(); + services.TryAddSingleton(); + + services.AddConfigurationsFromDirectory("global_drops"); + services.TryAddSingleton(); + + services.AddConfigurationsFromDirectory("map_npc_placement"); + services.TryAddSingleton(); + services.TryAddSingleton(); + + services.AddConfigurationsFromDirectory("map_monster_placement"); + services.TryAddSingleton(); + + services.AddConfigurationsFromDirectory("map_portals"); + services.AddConfigurationsFromDirectory("maps"); + services.TryAddSingleton(); + + // core client data + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + + // mails + services.TryAddSingleton(); + services.AddHostedService(s => s.GetRequiredService()); + + // other managers + services.TryAddSingleton(); + services.TryAddSingleton(); + services.AddBazaarModule(); + services.AddInterChannelModule(); + services.AddShipModule(gameServer); + services.AddDbServerModule(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(typeof(ILongKeyCachedRepository<>), typeof(InMemoryCacheRepository<>)); + services.TryAddSingleton(typeof(IUuidKeyCachedRepository<>), typeof(InMemoryUuidCacheRepository<>)); + services.TryAddSingleton(typeof(IKeyValueCache<>), typeof(InMemoryKeyValueCache<>)); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + + services.TryAddSingleton(); + + services.AddFileConfiguration(); + services.AddFileConfiguration("snack_food_configuration"); + + services.AddFileConfiguration("relict_configuration"); + services.AddFileConfiguration("item_sum_configuration"); + services.AddFileConfiguration("upgrade_normal_item_configuration"); + services.AddFileConfiguration("upgrade_phenomenal_item_configuration"); + + services.AddFileConfiguration("gambling_configuration"); + services.TryAddSingleton(); + + services.AddFileConfiguration("drop_rarity_configuration"); + services.TryAddSingleton(); + + services.AddFileConfiguration(); + services.TryAddSingleton(); + + services.AddMultipleConfigurationOneFile("time_space_configuration"); + services.TryAddSingleton(); + + services.AddMultipleConfigurationOneFile("time_space_npc_run_configuration"); + services.TryAddSingleton(); + + services.AddMultipleConfigurationOneFile("chest_drop_item_configuration"); + services.TryAddSingleton(); + + services.AddMultipleConfigurationOneFile("subacts_configuration"); + services.TryAddSingleton(); + + services.AddFileConfiguration("buffs_to_remove_configuration"); + services.TryAddSingleton(); + + services.AddMultipleConfigurationOneFile("gibberish_configuration"); + services.TryAddSingleton(); + + services.AddMultipleConfigurationOneFile("act5_npc_run_item_configuration"); + services.TryAddSingleton(); + + services.AddMultipleConfigurationOneFile("partner_specialist_basic_configuration"); + services.TryAddSingleton(); + + services.AddMultipleConfigurationOneFile("monster_talking_configuration"); + services.TryAddSingleton(); + + services.AddFileConfiguration("rainbow_configuration"); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/GenericEventPluginCore.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/GenericEventPluginCore.cs new file mode 100644 index 0000000..229493b --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/GenericEventPluginCore.cs @@ -0,0 +1,15 @@ +using Microsoft.Extensions.DependencyInjection; +using PhoenixLib.Events; +using WingsAPI.Plugins; + +namespace WingsEmu.Plugins.BasicImplementations; + +public class GenericEventPluginCore : IGameServerPlugin +{ + public string Name => nameof(GenericEventPluginCore); + + public void AddDependencies(IServiceCollection services, GameServerLoader gameServer) + { + services.AddEventHandlersInAssembly(); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/DanceGuriHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/DanceGuriHandler.cs new file mode 100644 index 0000000..02dec26 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/DanceGuriHandler.cs @@ -0,0 +1,50 @@ +using System; +using System.Threading.Tasks; +using WingsEmu.Game._enum; +using WingsEmu.Game._Guri; +using WingsEmu.Game._Guri.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Plugins.BasicImplementations.Guri; + +public class DanceGuriHandler : IGuriHandler +{ + public long GuriEffectId => 5; + + public async Task ExecuteAsync(IClientSession session, GuriEvent e) + { + int value = Convert.ToInt32(e.Value); + + switch (value) + { + case 5: + + if (e.Packet.Length < 5) + { + return; + } + + int isProcessing = Convert.ToInt32(e.Packet[5]); + + if (isProcessing == -1) + { + session.PlayerEntity.LastUnfreezedPlayer = DateTime.MinValue; + } + + break; + case 245: + DateTime now = DateTime.UtcNow; + if (session.PlayerEntity.LastRainbowArrowEffect.AddSeconds(2) > now) + { + return; + } + + session.PlayerEntity.LastRainbowArrowEffect = now; + await session.PlayerEntity.RemoveInvisibility(); + + session.BroadcastEffect(EffectType.YellowArrow); + break; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/EmoticonGuriHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/EmoticonGuriHandler.cs new file mode 100644 index 0000000..4f9ca03 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/EmoticonGuriHandler.cs @@ -0,0 +1,53 @@ +using System; +using System.Threading.Tasks; +using WingsEmu.Game._Guri; +using WingsEmu.Game._Guri.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Networking.Broadcasting; + +namespace WingsEmu.Plugins.BasicImplementations.Guri; + +public class EmoticonGuriHandler : IGuriHandler +{ + public long GuriEffectId => 10; + + public async Task ExecuteAsync(IClientSession session, GuriEvent e) + { + if (!e.User.HasValue) + { + return; + } + + int effect; + + if (e.Data >= 973 && e.Data <= 999) + { + effect = e.Data + 4099; + } + else if (e.Data == 1000) + { + effect = e.Data + 4116; + } + else if (e.Data >= 9000 && e.Data <= 9028) + { + effect = e.Data - 3883; + } + else + { + return; + } + + int id = Convert.ToInt32(e.User.Value); + + if (id == session.PlayerEntity.Id) + { + session.BroadcastEffect(effect, new EmoticonsBroadcast()); + return; + } + + IMateEntity mateEntity = session.PlayerEntity.MateComponent.GetMate(s => s.Id == id); + mateEntity?.MapInstance.Broadcast(mateEntity.GenerateEffectPacket(effect), new EmoticonsBroadcast()); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/FactionSwitchGuriHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/FactionSwitchGuriHandler.cs new file mode 100644 index 0000000..01130bd --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/FactionSwitchGuriHandler.cs @@ -0,0 +1,78 @@ +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.Game._Guri; +using WingsEmu.Game._Guri.Event; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Families.Event; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.Guri; + +public class FactionSwitchGuriHandler : IGuriHandler +{ + private const int IndividualAngelEgg = 1; + private const int IndividualDemonEgg = 2; + private const int FamilyAngelEgg = 3; + private const int FamilyDemonEgg = 4; + public long GuriEffectId => 750; + + public async Task ExecuteAsync(IClientSession session, GuriEvent e) + { + int eggType = e.Data; + int vnum = 1623 + eggType; + var targetFaction = (FactionType)eggType; + + bool hasItem = session.PlayerEntity.HasItem(vnum); + + if (!hasItem) + { + return; + } + + if (session.PlayerEntity.IsOnVehicle) + { + return; + } + + if (session.CantPerformActionOnAct4()) + { + return; + } + + switch (eggType) + { + case IndividualAngelEgg: + case IndividualDemonEgg: + { + if (session.PlayerEntity.IsInFamily()) + { + return; + } + + if (session.PlayerEntity.Faction == targetFaction) + { + return; + } + + await session.EmitEventAsync(new ChangeFactionEvent + { + NewFaction = targetFaction + }); + + await session.RemoveItemFromInventory(vnum); + break; + } + case FamilyAngelEgg: + case FamilyDemonEgg: + { + await session.EmitEventAsync(new FamilyChangeFactionEvent + { + Faction = eggType / 2 + }); + break; + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/FairyBeadGuriHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/FairyBeadGuriHandler.cs new file mode 100644 index 0000000..c0dee80 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/FairyBeadGuriHandler.cs @@ -0,0 +1,86 @@ +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.DTOs.Items; +using WingsEmu.Game._enum; +using WingsEmu.Game._Guri; +using WingsEmu.Game._Guri.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Items; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.Guri; + +public class FairyBeadGuriHandler : IGuriHandler +{ + public long GuriEffectId => 209; + + public async Task ExecuteAsync(IClientSession session, GuriEvent guriPacket) + { + if (guriPacket.Data != 0) + { + return; + } + + if (guriPacket.User == null) + { + return; + } + + if (!short.TryParse(guriPacket.User.Value.ToString(), out short beadSlot)) + { + return; + } + + if (!short.TryParse(guriPacket.Value, out short fairySlot)) + { + return; + } + + InventoryItem fairy = session.PlayerEntity.GetItemBySlotAndType(fairySlot, InventoryType.Equipment); + InventoryItem bead = session.PlayerEntity.GetItemBySlotAndType(beadSlot, InventoryType.Equipment); + + if (fairy == null || bead == null) + { + return; + } + + if (bead.ItemInstance.Type != ItemInstanceType.BoxInstance) + { + return; + } + + if (fairy.ItemInstance.Type != ItemInstanceType.WearableInstance) + { + return; + } + + GameItemInstance beadItem = bead.ItemInstance; + GameItemInstance fairyItem = fairy.ItemInstance; + + session.SendGuriPacket(27); + session.SendEsfPacket(0); + + if (beadItem.HoldingVNum != 0 && beadItem.HoldingVNum != null) + { + return; + } + + if (fairyItem.GameItem.IsLimited || fairyItem.GameItem.Id >= (short)ItemVnums.FIRE_FAIRY && fairyItem.GameItem.Id <= (short)ItemVnums.SHADOW_FAIRY) + { + return; + } + + if (beadItem.GameItem.ItemSubType != 5 || fairyItem.GameItem.ItemType != ItemType.Jewelry || fairyItem.GameItem.ItemSubType != 3) + { + return; + } + + beadItem.HoldingVNum = fairyItem.ItemVNum; + beadItem.ElementRate = fairyItem.ElementRate; + beadItem.Xp = fairyItem.Xp; + + await session.RemoveItemFromInventory(item: fairy); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/FifthSceneGuriHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/FifthSceneGuriHandler.cs new file mode 100644 index 0000000..bd00de3 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/FifthSceneGuriHandler.cs @@ -0,0 +1,13 @@ +using System.Threading.Tasks; +using WingsEmu.Game._Guri; +using WingsEmu.Game._Guri.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Plugins.BasicImplementations.Guri; + +public class FifthSceneGuriHandler : IGuriHandler +{ + public long GuriEffectId => 44; + public async Task ExecuteAsync(IClientSession session, GuriEvent e) => session.SendScene(44, true); +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/FirstSceneGuriHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/FirstSceneGuriHandler.cs new file mode 100644 index 0000000..18b2701 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/FirstSceneGuriHandler.cs @@ -0,0 +1,14 @@ +using System.Threading.Tasks; +using WingsEmu.Game._Guri; +using WingsEmu.Game._Guri.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Plugins.BasicImplementations.Guri; + +public class FirstSceneGuriHandler : IGuriHandler +{ + public long GuriEffectId => 40; + + public async Task ExecuteAsync(IClientSession session, GuriEvent e) => session.SendScene(40, true); +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/FourthSceneGuriHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/FourthSceneGuriHandler.cs new file mode 100644 index 0000000..f78da26 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/FourthSceneGuriHandler.cs @@ -0,0 +1,13 @@ +using System.Threading.Tasks; +using WingsEmu.Game._Guri; +using WingsEmu.Game._Guri.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Plugins.BasicImplementations.Guri; + +public class FourthSceneGuriHandler : IGuriHandler +{ + public long GuriEffectId => 43; + public async Task ExecuteAsync(IClientSession session, GuriEvent e) => session.SendScene(43, true); +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/GenerateGuriGuriHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/GenerateGuriGuriHandler.cs new file mode 100644 index 0000000..8abd58a --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/GenerateGuriGuriHandler.cs @@ -0,0 +1,25 @@ +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.CharacterExtensions; +using WingsEmu.Game._Guri; +using WingsEmu.Game._Guri.Event; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Networking.Broadcasting; + +namespace WingsEmu.Plugins.BasicImplementations.Guri; + +public class GenerateGuriGuriHandler : IGuriHandler +{ + private readonly IMeditationManager _meditation; + + public GenerateGuriGuriHandler(IMeditationManager meditation) => _meditation = meditation; + + public long GuriEffectId => 2; + + public async Task ExecuteAsync(IClientSession session, GuriEvent guriPacket) + { + session.Broadcast(session.GenerateGuriPacket(2, 1), new RangeBroadcast(session.PlayerEntity.PositionX, session.PlayerEntity.PositionY)); + session.PlayerEntity.RemoveMeditation(_meditation); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/HarvestGuriHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/HarvestGuriHandler.cs new file mode 100644 index 0000000..8c5d6e6 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/HarvestGuriHandler.cs @@ -0,0 +1,210 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using PhoenixLib.Events; +using PhoenixLib.Logging; +using WingsAPI.Data.Drops; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsAPI.Game.Extensions.ItemExtension.Item; +using WingsEmu.Game; +using WingsEmu.Game._enum; +using WingsEmu.Game._Guri; +using WingsEmu.Game._Guri.Event; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Entities.Extensions; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Npcs.Event; +using WingsEmu.Game.Quests.Event; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.Guri; + +public class HarverstGuriHandler : IGuriHandler +{ + private readonly IAsyncEventPipeline _asyncEventPipeline; + private readonly IDelayManager _delayManager; + private readonly IGameItemInstanceFactory _gameItemInstanceFactory; + private readonly IGameLanguageService _gameLanguageService; + private readonly IItemsManager _itemsManager; + private readonly IRandomGenerator _randomGenerator; + + private readonly IServerManager _serverManager; + + public HarverstGuriHandler(IServerManager serverManager, + IGameLanguageService gameLanguageService, IRandomGenerator randomGenerator, + IItemsManager itemsManager, IGameItemInstanceFactory gameItemInstanceFactory, IDelayManager delayManager, IAsyncEventPipeline asyncEventPipeline) + { + _randomGenerator = randomGenerator; + _serverManager = serverManager; + _gameLanguageService = gameLanguageService; + _itemsManager = itemsManager; + _gameItemInstanceFactory = gameItemInstanceFactory; + _delayManager = delayManager; + _asyncEventPipeline = asyncEventPipeline; + } + + public long GuriEffectId => 400; + + public async Task ExecuteAsync(IClientSession session, GuriEvent guriPacket) + { + if (guriPacket == null) + { + return; + } + + if (guriPacket.Data == 0) + { + return; + } + + if (!session.HasCurrentMapInstance) + { + return; + } + + INpcEntity npcEntity = session.CurrentMapInstance.GetNpcById(guriPacket.Data); + if (npcEntity == null) + { + Log.Debug($"Npc not found. GuriEffectId : {GuriEffectId}"); + return; + } + + if (!npcEntity.IsAlive()) + { + await session.NotifyStrangeBehavior(StrangeBehaviorSeverity.ABUSING, "[ITEM_HARVEST] Tried to harvest a dead NPC."); + return; + } + + if (!npcEntity.IsInRange(session.PlayerEntity.PositionX, session.PlayerEntity.PositionY, 5)) + { + await session.NotifyStrangeBehavior(StrangeBehaviorSeverity.ABUSING, "[ITEM_HARVEST] Tried to harvest an NPC that is too far away."); + return; + } + + if (!await _delayManager.CanPerformAction(session.PlayerEntity, DelayedActionType.Mining)) + { + return; + } + + await _delayManager.CompleteAction(session.PlayerEntity, DelayedActionType.Mining); + + if (npcEntity.CurrentCollection <= 0) + { + int delayTime = (int)(npcEntity.LastCollection - DateTime.UtcNow).TotalSeconds; + session.SendMsg(_gameLanguageService.GetLanguageFormat(GameDialogKey.HARVEST_SHOUTMESSAGE_FAIL_TRY_AGAIN, session.UserLanguage, delayTime), MsgMessageType.Middle); + return; + } + + if (npcEntity.VNumRequired != 0 && npcEntity.AmountRequired != 0) + { + if (!session.PlayerEntity.HasItem(npcEntity.VNumRequired, npcEntity.AmountRequired)) + { + string itemName = _itemsManager.GetItem(npcEntity.VNumRequired).GetItemName(_gameLanguageService, session.UserLanguage); + session.SendMsg(_gameLanguageService.GetLanguageFormat(GameDialogKey.INVENTORY_SHOUTMESSAGE_NOT_ENOUGH_ITEMS, session.UserLanguage, npcEntity.AmountRequired, itemName), + MsgMessageType.Middle); + return; + } + } + + double chance = _randomGenerator.RandomNumber(0, 100_000); + DropDTO drop; + + if (npcEntity.Drops.Count <= 1) + { + drop = npcEntity.Drops.FirstOrDefault(s => s.MonsterVNum == npcEntity.NpcVNum); + + if (drop != null && chance > drop.DropChance) + { + session.SendMsg(_gameLanguageService.GetLanguage(GameDialogKey.HARVEST_SHOUTMESSAGE_TRY_FAIL, session.UserLanguage), MsgMessageType.Middle); + return; + } + } + else + { + var randomBag = new RandomBag(_randomGenerator); + foreach (DropDTO dropDto in npcEntity.Drops) + { + randomBag.AddEntry(dropDto, dropDto.DropChance); + } + + drop = randomBag.GetRandom(); + } + + if (drop == null) + { + session.SendMsg(_gameLanguageService.GetLanguage(GameDialogKey.HARVEST_SHOUTMESSAGE_TRY_FAIL, session.UserLanguage), MsgMessageType.Middle); + return; + } + + int vnum = drop.ItemVNum; + + if (npcEntity.VNumRequired != 0 && npcEntity.AmountRequired != 0) + { + await session.RemoveItemFromInventory(npcEntity.VNumRequired, npcEntity.AmountRequired); + } + + npcEntity.CurrentCollection--; + + GameItemInstance newItem = _gameItemInstanceFactory.CreateItem(vnum, drop.Amount); + session.SendMsg( + _gameLanguageService.GetLanguageFormat(GameDialogKey.INVENTORY_CHATMESSAGE_X_ITEM_ACQUIRED, session.UserLanguage, newItem.Amount.ToString(), + _gameLanguageService.GetItemName(newItem.GameItem, session)), MsgMessageType.Middle); + + switch ((MonsterVnum)npcEntity.NpcVNum) + { + case MonsterVnum.POISON_PLANT_OF_DAMNATION: + case MonsterVnum.ICE_FLOWER: + npcEntity.MapInstance.Broadcast(npcEntity.GenerateOut()); + await _asyncEventPipeline.ProcessEventAsync(new MapNpcGenerateDeathEvent(npcEntity, null)); + Position newPosition = npcEntity.MapInstance.GetRandomPosition(); + npcEntity.FirstX = newPosition.X; + npcEntity.FirstY = newPosition.Y; + break; + case MonsterVnum.ROBBER_GANG_CHEST: + npcEntity.MapInstance.Broadcast(npcEntity.GenerateOut()); + await _asyncEventPipeline.ProcessEventAsync(new MapNpcGenerateDeathEvent(npcEntity, null)); + break; + } + + // Quest logic + bool isHarvestObjective = session.PlayerEntity.GetCurrentQuestsByType(QuestType.COLLECT) + .Any(s => s.Quest.Objectives.Any(o => + newItem.ItemVNum == o.Data1 && npcEntity.NpcVNum == o.Data0 && s.ObjectiveAmount[o.ObjectiveIndex].CurrentAmount <= s.ObjectiveAmount[o.ObjectiveIndex].RequiredAmount)); + + if (!isHarvestObjective) + { + await session.AddNewItemToInventory(newItem, true, sendGiftIsFull: true); + return; + } + + if (session.PlayerEntity.IsInGroup()) + { + foreach (IPlayerEntity member in session.PlayerEntity.GetGroup().Members) + { + await member.Session.EmitEventAsync(new QuestHarvestEvent + { + ItemVnum = newItem.ItemVNum, + NpcVnum = npcEntity.NpcVNum + }); + } + } + else + { + await session.EmitEventAsync(new QuestHarvestEvent + { + ItemVnum = newItem.ItemVNum, + NpcVnum = npcEntity.NpcVNum + }); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/IcebreakerEventGuriHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/IcebreakerEventGuriHandler.cs new file mode 100644 index 0000000..93192d0 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/IcebreakerEventGuriHandler.cs @@ -0,0 +1,52 @@ +/* +using System.Linq; +using System.Threading.Tasks; +using WingsEmu.Core.Logging; +using WingsAPI.Game._Guri; +using WingsAPI.Game._Guri.Event; +using WingsAPI.Game.Managers; +using WingsAPI.Game.Networking; +using WingsAPI.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.Guri +{ + public class IcebreakerEventGuriHandler : IGuriHandler + { + public long GuriEffectId => 501; + + private readonly IServerManager _serverManager; + private readonly IMapManager _mapManager; + + public IcebreakerEventGuriHandler(IServerManager serverManager, IMapManager mapManager) + { + _serverManager = serverManager; + _mapManager = mapManager; + } + + public async Task ExecuteAsync(IClientSession Session, GuriEvent guriPacket) + { + if (!_serverManager.IceBreakerInWaiting && IceBreaker.Map.Sessions.Count() > IceBreaker.MaxAllowedPlayers) + { + Logger.Log.Debug($"The maximum number of players has been reached. GurriEffectId {GuriEffectId}"); + return; + } + + await _mapManager.TeleportOnRandomPlaceInMapAsync(Session, IceBreaker.Map.MapInstanceId); + var group = new WingsAPI.Game.Group(GroupType.IceBreaker); + if (Session.Character.Group == null) + { + group.Characters.Add(Session); + goto IceBreaker; + } + + foreach (IClientSession session in Session.Character.Group.Characters) + { + group.Characters.Add(session); + } + + IceBreaker: + IceBreaker.AddGroup(group); + } + } +} */ + diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/InstantBattleGuriHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/InstantBattleGuriHandler.cs new file mode 100644 index 0000000..30542e7 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/InstantBattleGuriHandler.cs @@ -0,0 +1,38 @@ +using System; +using System.Threading.Tasks; +using WingsEmu.Game._Guri; +using WingsEmu.Game._Guri.Event; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.GameEvent; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.Guri; + +public class InstantBattleGuriHandler : IGuriHandler +{ + private const GameEventType Type = GameEventType.InstantBattle; + + private readonly IGameEventRegistrationManager _gameEventRegistrationManager; + + public InstantBattleGuriHandler(IGameEventRegistrationManager gameEventRegistrationManager) => _gameEventRegistrationManager = gameEventRegistrationManager; + + public long GuriEffectId => 506; + + public async Task ExecuteAsync(IClientSession session, GuriEvent e) + { + if (!_gameEventRegistrationManager.IsGameEventRegistrationOpen(Type, DateTime.UtcNow)) + { + return; + } + + if (session.IsMuted()) + { + session.SendMsg(session.GetLanguage(GameDialogKey.MUTE_SHOUTMESSAGE_YOU_ARE_MUTED), MsgMessageType.Middle); + return; + } + + _gameEventRegistrationManager.SetCharacterGameEventInclination(session.PlayerEntity.Id, Type); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/InteractionGuriHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/InteractionGuriHandler.cs new file mode 100644 index 0000000..8c7e3e0 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/InteractionGuriHandler.cs @@ -0,0 +1,306 @@ +using System; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.Groups; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.Game._enum; +using WingsEmu.Game._Guri; +using WingsEmu.Game._Guri.Event; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage.Configuration; +using WingsEmu.Game.Chat; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Extensions.Mates; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; +using ChatType = WingsEmu.Game._playerActionLogs.ChatType; + +namespace WingsEmu.Plugins.BasicImplementations.Guri; + +public class InteractionGuriHandler : IGuriHandler +{ + private readonly IForbiddenNamesManager _forbiddenNamesManager; + private readonly IItemsManager _itemsManager; + + private readonly IGameLanguageService _languageService; + private readonly ISpPartnerConfiguration _spPartner; + + public InteractionGuriHandler(IGameLanguageService languageService, IItemsManager itemsManager, ISpPartnerConfiguration spPartner, IForbiddenNamesManager forbiddenNamesManager) + { + _languageService = languageService; + _itemsManager = itemsManager; + _spPartner = spPartner; + _forbiddenNamesManager = forbiddenNamesManager; + } + + public long GuriEffectId => 4; + + public async Task ExecuteAsync(IClientSession session, GuriEvent guriPacket) + { + if (!long.TryParse(guriPacket.Packet[3], out long guriType)) + { + return; + } + + if (!long.TryParse(guriPacket.Packet[5], out long itemSpeaker)) + { + return; + } + + if (itemSpeaker == 999) + { + guriType = 999; + } + + string message; + string[] valueSplit; + + switch (guriType) + { + case 1: + if (session.CantPerformActionOnAct4()) + { + return; + } + + if (session.PlayerEntity.RainbowBattleComponent.IsInRainbowBattle) + { + return; + } + + if (string.IsNullOrWhiteSpace(guriPacket.Value)) + { + return; + } + + if (!guriPacket.User.HasValue) + { + return; + } + + IMateEntity mateEntity = session.PlayerEntity.MateComponent.GetMate(s => s.Id == guriPacket.User && s.MateType == MateType.Pet); + if (guriPacket.Value.Length > 15) + { + session.SendInfo(_languageService.GetLanguage(GameDialogKey.ITEM_INFO_PET_NAME_TOO_LONG, session.UserLanguage)); + return; + } + + if (mateEntity == null) + { + return; + } + + var rg = new Regex(@"^[a-zA-Z0-9_\-\*]*$"); + if (rg.Matches(guriPacket.Value).Count != 1) + { + return; + } + + string expectedName = guriPacket.Value; + mateEntity.MateName = expectedName; + + if (mateEntity.Position == default) + { + mateEntity.ChangePosition(session.PlayerEntity.Position); + } + + mateEntity.MapInstance.Broadcast(s => mateEntity.GenerateIn(_languageService, s.UserLanguage, _spPartner)); + + session.RefreshParty(_spPartner); + session.SendScnPackets(); + session.SendScpPackets(); + await session.RemoveItemFromInventory((short)ItemVnums.NAME_TAG); + + break; + // Presentation message + case 2: + if (guriPacket.Value == null) + { + return; + } + + bool isLimitedItem = session.PlayerEntity.HasItem((short)ItemVnums.SELF_INTRODUCTION_LIMITED); + if (!session.PlayerEntity.HasItem((short)ItemVnums.SELF_INTRODUCTION) && !isLimitedItem) + { + return; + } + + short itemVnumToRemove = isLimitedItem ? (short)ItemVnums.SELF_INTRODUCTION_LIMITED : (short)ItemVnums.SELF_INTRODUCTION; + + message = string.Empty; + valueSplit = guriPacket.Value.Split(' '); + message = valueSplit.Aggregate(message, (current, t) => current + t + "^"); + message = message[..^1]; // Remove the last ^ + message = message.Trim(); + if (message.Length > 60) + { + message = message.Substring(0, 60); + } + + session.PlayerEntity.Biography = message; + session.SendChatMessage(_languageService.GetLanguage(GameDialogKey.INTERACTION_CHATMESSAGE_INTRODUCTION_SET, session.UserLanguage), ChatMessageColorType.Yellow); + await session.RemoveItemFromInventory(itemVnumToRemove); + + break; + + case 3: + bool haveLimitedSpeaker = session.PlayerEntity.HasItem((short)ItemVnums.SPEAKER_LIMITED); + bool haveSpeaker = session.PlayerEntity.HasItem((short)ItemVnums.SPEAKER); + if (!haveLimitedSpeaker && !haveSpeaker) + { + return; + } + + if (session.PlayerEntity.RainbowBattleComponent.IsInRainbowBattle) + { + session.SendChatMessage(session.GetLanguage(GameDialogKey.ITEM_MESSAGE_CANT_USE), ChatMessageColorType.Yellow); + return; + } + + if (guriPacket.Value == null) + { + return; + } + + valueSplit = guriPacket.Value.Split(' '); + message = valueSplit.Aggregate("", (current, t) => current + t + " "); + + if (session.IsMuted()) + { + session.SendMuteMessage(); + return; + } + + await session.EmitEventAsync(new ChatSpeakerEvent(SpeakerType.Normal_Speaker, message)); + + if (haveLimitedSpeaker) + { + await session.RemoveItemFromInventory((short)ItemVnums.SPEAKER_LIMITED); + } + else + { + await session.RemoveItemFromInventory((short)ItemVnums.SPEAKER); + } + + break; + + case 4: + if (session.CantPerformActionOnAct4()) + { + return; + } + + bool haveLimitedBubble = session.PlayerEntity.HasItem((short)ItemVnums.BUBBLE_LIMITED); + bool haveBubble = session.PlayerEntity.HasItem((short)ItemVnums.BUBBLE); + if (!haveLimitedBubble && !haveBubble) + { + return; + } + + if (session.PlayerEntity.RainbowBattleComponent.IsInRainbowBattle) + { + session.SendChatMessage(session.GetLanguage(GameDialogKey.ITEM_MESSAGE_CANT_USE), ChatMessageColorType.Yellow); + return; + } + + if (session.IsMuted()) + { + session.SendMuteMessage(); + return; + } + + session.PlayerEntity.Bubble = DateTime.UtcNow; + string bubbleMessage = guriPacket.Value; + if (bubbleMessage.Length > 60) + { + bubbleMessage = bubbleMessage.Substring(0, 60); + } + + session.PlayerEntity.SaveBubble(bubbleMessage); + session.SendPacket(new CsprPacket { Message = bubbleMessage }); + + if (haveLimitedBubble) + { + await session.RemoveItemFromInventory((short)ItemVnums.BUBBLE_LIMITED); + } + else + { + await session.RemoveItemFromInventory((short)ItemVnums.BUBBLE); + } + + await session.EmitEventAsync(new ChatGenericEvent + { + Message = bubbleMessage, + ChatType = ChatType.SpeechBubble + }); + break; + + case 999: + haveLimitedSpeaker = session.PlayerEntity.HasItem((short)ItemVnums.SPEAKER_LIMITED); + haveSpeaker = session.PlayerEntity.HasItem((short)ItemVnums.SPEAKER); + if (!haveLimitedSpeaker && !haveSpeaker) + { + return; + } + + if (!Enum.TryParse(guriPacket.Packet[6], out InventoryType type)) + { + return; + } + + if (!short.TryParse(guriPacket.Packet[7], out short slot)) + { + return; + } + + if (session.PlayerEntity.RainbowBattleComponent.IsInRainbowBattle) + { + session.SendChatMessage(session.GetLanguage(GameDialogKey.ITEM_MESSAGE_CANT_USE), ChatMessageColorType.Yellow); + return; + } + + if (session.IsMuted()) + { + session.SendMuteMessage(); + return; + } + + InventoryItem item = session.PlayerEntity.GetItemBySlotAndType(slot, type); + + if (item == null) + { + return; + } + + string messageItem = $"{guriPacket.Value.Remove(0, 4).Replace(" ", "|")}"; + + // check, if itemName is the same as in {} + string itemName = _languageService.GetLanguage(GameDataType.Item, _itemsManager.GetItem(item.ItemInstance.ItemVNum).Name, session.UserLanguage).Replace(" ", "|"); + int firstItem = messageItem.IndexOf("{", StringComparison.Ordinal) + "{".Length; + int lastItem = messageItem.LastIndexOf("}", StringComparison.Ordinal); + string result = messageItem.Substring(firstItem, lastItem - firstItem); + messageItem = messageItem.Replace(result, "%s"); + messageItem = messageItem.Trim(); + + await session.EmitEventAsync(new ChatSpeakerEvent(SpeakerType.Items_Speaker, messageItem, item.ItemInstance)); + if (haveLimitedSpeaker) + { + await session.RemoveItemFromInventory((short)ItemVnums.SPEAKER_LIMITED); + } + else + { + await session.RemoveItemFromInventory((short)ItemVnums.SPEAKER); + } + + break; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/MapsTeleportersGuriHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/MapsTeleportersGuriHandler.cs new file mode 100644 index 0000000..9f3f2b7 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/MapsTeleportersGuriHandler.cs @@ -0,0 +1,97 @@ +using System.Linq; +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsAPI.Game.Extensions.ItemExtension.Item; +using WingsEmu.DTOs.ServerDatas; +using WingsEmu.Game._Guri; +using WingsEmu.Game._Guri.Event; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.Guri; + +public class MapsTeleportersGuriHandler : IGuriHandler +{ + private readonly IDelayManager _delayManager; + private readonly IGameLanguageService _gameLanguageService; + private readonly IItemsManager _itemsManager; + private readonly ITeleporterManager _teleporterManager; + + public MapsTeleportersGuriHandler(IDelayManager delayManager, ITeleporterManager teleporterManager, IGameLanguageService gameLanguageService, IItemsManager itemsManager) + { + _delayManager = delayManager; + _teleporterManager = teleporterManager; + _gameLanguageService = gameLanguageService; + _itemsManager = itemsManager; + } + + public long GuriEffectId => 710; + + public async Task ExecuteAsync(IClientSession session, GuriEvent guriPacket) + { + if (guriPacket.User == null) + { + return; + } + + if (guriPacket.Packet.Length < 6) + { + return; + } + + if (!int.TryParse(guriPacket.Packet[5], out int npcId)) + { + return; + } + + INpcEntity npcEntity = session.CurrentMapInstance.GetNpcById(npcId); + if (npcEntity == null) + { + return; + } + + if (npcEntity.VNumRequired != 0 && npcEntity.AmountRequired != 0) + { + if (!session.PlayerEntity.HasItem(npcEntity.VNumRequired, npcEntity.AmountRequired)) + { + string itemName = _itemsManager.GetItem(npcEntity.VNumRequired).GetItemName(_gameLanguageService, session.UserLanguage); + session.SendMsg(_gameLanguageService.GetLanguageFormat(GameDialogKey.INVENTORY_SHOUTMESSAGE_NOT_ENOUGH_ITEMS, session.UserLanguage, npcEntity.AmountRequired, itemName), + MsgMessageType.Middle); + return; + } + } + + TeleporterDTO tp = _teleporterManager.GetTeleportByNpcId(npcEntity.Id)?.FirstOrDefault(t => t?.Type == TeleporterType.TELEPORT_ON_MAP); + if (tp == null) + { + return; + } + + if (tp.MapX != guriPacket.Data || tp.MapY != guriPacket.User.Value) + { + return; + } + + bool canTeleport = await _delayManager.CanPerformAction(session.PlayerEntity, DelayedActionType.UseTeleporter); + if (!canTeleport) + { + return; + } + + await _delayManager.CompleteAction(session.PlayerEntity, DelayedActionType.UseTeleporter); + + if (npcEntity.VNumRequired != 0 && npcEntity.TeleportRemoveFromInventory) + { + await session.RemoveItemFromInventory(npcEntity.VNumRequired, npcEntity.AmountRequired); + } + + session.PlayerEntity.TeleportOnMap((short)guriPacket.Data, (short)guriPacket.User.Value, true); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/MeteoreEventGuriHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/MeteoreEventGuriHandler.cs new file mode 100644 index 0000000..4df226c --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/MeteoreEventGuriHandler.cs @@ -0,0 +1,29 @@ +using System.Threading.Tasks; +using WingsEmu.Game._Guri; +using WingsEmu.Game._Guri.Event; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Plugins.BasicImplementations.Guri; + +public class MeteoreEventGuriHandler : IGuriHandler +{ + private readonly IServerManager _serverManager; + + public MeteoreEventGuriHandler(IServerManager serverManager) => _serverManager = serverManager; + + public long GuriEffectId => -1; + + public async Task ExecuteAsync(IClientSession session, GuriEvent guriPacket) + { + /*if (!_serverManager.EventInWaiting && session.Character.IsWaitingForEvent) + { + Log.Debug($"The player is not registered for this event. GuriEffectId : {GuriEffectId}"); + return; + } + + session.SendBsInfoPacket(BsInfoType.OpenWindow, GameType.DodgeMeteor, 50, QuequeWindow.WaitForEntry); + session.SendEsfPacket(2); + session.Character.IsWaitingForEvent = true;*/ + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/MountBeadGuriHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/MountBeadGuriHandler.cs new file mode 100644 index 0000000..6477cb7 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/MountBeadGuriHandler.cs @@ -0,0 +1,64 @@ +using System.Threading.Tasks; +using PhoenixLib.Logging; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.DTOs.Items; +using WingsEmu.Game._Guri; +using WingsEmu.Game._Guri.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Items; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.Guri; + +public class MountBeadGuriHandler : IGuriHandler +{ + public long GuriEffectId => 208; + + public async Task ExecuteAsync(IClientSession session, GuriEvent guriPacket) + { + if (guriPacket.User == null) + { + Log.Warn($"The guriPacket doesn't contain any value for User. GuriEffectId : {GuriEffectId}"); + return; + } + + if (!short.TryParse(guriPacket.User.Value.ToString(), out short pearlSlot) || !short.TryParse(guriPacket.Value, out short mountSlot)) + { + Log.Warn($"The pearlSlot or mountSlot value is null. GuriEffectId : {GuriEffectId}"); + return; + } + + InventoryItem mount = session.PlayerEntity.GetItemBySlotAndType(mountSlot, InventoryType.Main); + InventoryItem bead = session.PlayerEntity.GetItemBySlotAndType(pearlSlot, InventoryType.Equipment); + + if (mount == null || bead == null) + { + return; + } + + if (bead.ItemInstance.Type != ItemInstanceType.BoxInstance) + { + return; + } + + GameItemInstance beadItem = bead.ItemInstance; + + if (beadItem.HoldingVNum != 0 && beadItem.HoldingVNum != null) + { + return; + } + + if (beadItem.GameItem.ItemSubType != 4 || mount.ItemInstance.GameItem.ItemType != ItemType.Special || mount.ItemInstance.GameItem.Effect != 1000) + { + return; + } + + beadItem.HoldingVNum = mount.ItemInstance.ItemVNum; + await session.RemoveItemFromInventory(mount.ItemInstance.ItemVNum); + + session.SendGuriPacket(25); + session.SendEsfPacket(0); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/PartnerBackPackGuriHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/PartnerBackPackGuriHandler.cs new file mode 100644 index 0000000..a499288 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/PartnerBackPackGuriHandler.cs @@ -0,0 +1,23 @@ +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.Warehouse; +using WingsEmu.Game._Guri; +using WingsEmu.Game._Guri.Event; +using WingsEmu.Game.Networking; +using WingsEmu.Game.TimeSpaces; + +namespace WingsEmu.Plugins.BasicImplementations.Guri; + +public class PartnerBackPackGuriHandler : IGuriHandler +{ + public long GuriEffectId => 202; + + public async Task ExecuteAsync(IClientSession session, GuriEvent guriPacket) + { + if (session.PlayerEntity.IsSeal || session.IsInSpecialOrHiddenTimeSpace()) + { + return; + } + + session.RefreshPartnerWarehouseItems(); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/PerfumeGuriHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/PerfumeGuriHandler.cs new file mode 100644 index 0000000..ec4852b --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/PerfumeGuriHandler.cs @@ -0,0 +1,100 @@ +using System.Threading.Tasks; +using PhoenixLib.Logging; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.DTOs.Items; +using WingsEmu.Game._enum; +using WingsEmu.Game._Guri; +using WingsEmu.Game._Guri.Event; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Plugins.BasicImplementations.Algorithms.Shells; + +namespace WingsEmu.Plugins.BasicImplementations.Guri; + +public class PerfumeGuriHandler : IGuriHandler +{ + private readonly IGameLanguageService _gameLanguageService; + private readonly IItemsManager _itemsManager; + + private readonly IShellPerfumeConfiguration _perfumeConfiguration; + + public PerfumeGuriHandler(IShellPerfumeConfiguration perfumeConfiguration, IGameLanguageService gameLanguageService, IItemsManager itemsManager) + { + _perfumeConfiguration = perfumeConfiguration; + _gameLanguageService = gameLanguageService; + _itemsManager = itemsManager; + } + + public long GuriEffectId => 205; + + public async Task ExecuteAsync(IClientSession session, GuriEvent guriPacket) + { + if (guriPacket.User == null) + { + return; + } + + const int perfumeVnum = (short)ItemVnums.PERFUME; + var perfumeInventoryType = (InventoryType)guriPacket.Data; + InventoryItem eq = session.PlayerEntity.GetItemBySlotAndType((short)guriPacket.User.Value, perfumeInventoryType); + + if (eq == null) + { + return; + } + + if (eq.ItemInstance.Type != ItemInstanceType.WearableInstance) + { + return; + } + + GameItemInstance eqItem = eq.ItemInstance; + if (!eqItem.BoundCharacterId.HasValue) + { + return; + } + + // Item already yours + if (eqItem.BoundCharacterId == session.PlayerEntity.Id) + { + return; + } + + int? perfumesNeeded = _perfumeConfiguration.GetPerfumesByLevelAndRarity(eqItem.GameItem.LevelMinimum, (byte)eqItem.Rarity, eqItem.GameItem.IsHeroic); + if (perfumesNeeded == null) + { + Log.Debug($"[ERROR] A valid perfume configuration for LV: {eqItem.GameItem.LevelMinimum}, Rarity: {eqItem.Rarity}, IsHeroic: {eqItem.GameItem.IsHeroic} was not found."); + return; + } + + if (!session.PlayerEntity.HasItem(perfumeVnum, (short)perfumesNeeded)) + { + session.SendInfo(_gameLanguageService.GetLanguage(GameDialogKey.INVENTORY_SHOUTMESSAGE_NOT_ENOUGH_ITEMS, session.UserLanguage)); + return; + } + + int? goldNeeded = _perfumeConfiguration.GetGoldByLevel(eqItem.GameItem.LevelMinimum, eqItem.GameItem.IsHeroic); + if (goldNeeded == null) + { + Log.Debug($"[ERROR] A valid gold configuration for LV: {eqItem.GameItem.LevelMinimum}, IsHeroic: {eqItem.GameItem.IsHeroic} was not found."); + return; + } + + if (session.PlayerEntity.Gold < goldNeeded) + { + session.SendInfo(_gameLanguageService.GetLanguage(GameDialogKey.INTERACTION_MESSAGE_NOT_ENOUGH_GOLD, session.UserLanguage)); + return; + } + + + session.PlayerEntity.Gold -= (long)goldNeeded; + session.RefreshGold(); + await session.RemoveItemFromInventory(perfumeVnum, (short)perfumesNeeded); + eqItem.BoundCharacterId = session.PlayerEntity.Id; + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/PetBasketGuriHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/PetBasketGuriHandler.cs new file mode 100644 index 0000000..3bbb3e1 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/PetBasketGuriHandler.cs @@ -0,0 +1,31 @@ +using System.Threading.Tasks; +using PhoenixLib.Logging; +using WingsEmu.DTOs.Bonus; +using WingsEmu.Game._Guri; +using WingsEmu.Game._Guri.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Warehouse; +using WingsEmu.Game.Warehouse.Events; + +namespace WingsEmu.Plugins.BasicImplementations.Guri; + +public class PetBasketGuriHandler : IGuriHandler +{ + private readonly IAccountWarehouseManager _accountWarehouseManager; + + public PetBasketGuriHandler(IAccountWarehouseManager accountWarehouseManager) => _accountWarehouseManager = accountWarehouseManager; + + public long GuriEffectId => 201; + + public async Task ExecuteAsync(IClientSession session, GuriEvent guriPacket) + { + if (!session.PlayerEntity.HaveStaticBonus(StaticBonusType.PetBasket)) + { + Log.Debug($"No static bonus for PetBasket on CharacterId : {session.PlayerEntity.Id}. GuriEffectId : {GuriEffectId}"); + return; + } + + await session.EmitEventAsync(new AccountWarehouseOpenEvent()); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/PositionGuriHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/PositionGuriHandler.cs new file mode 100644 index 0000000..60b3728 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/PositionGuriHandler.cs @@ -0,0 +1,54 @@ +using System.Threading.Tasks; +using WingsEmu.Game._Guri; +using WingsEmu.Game._Guri.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.Guri; + +public class PositionGuriHandler : IGuriHandler +{ + public long GuriEffectId => 1; + + public async Task ExecuteAsync(IClientSession session, GuriEvent e) + { + session.SendDebugMessage($"[GURI] Old position: {session.PlayerEntity.PositionX}/{session.PlayerEntity.PositionY}", ChatMessageColorType.Red); + + if (e.Packet.Length <= 4) + { + return; + } + + if (!short.TryParse(e.Packet[5], out short newX)) + { + return; + } + + if (!short.TryParse(e.Packet[6], out short newY)) + { + return; + } + + if (session.CurrentMapInstance.IsBlockedZone(newX, newY)) + { + return; + } + + if (session.PlayerEntity.IsSeal) + { + return; + } + + int distance = session.PlayerEntity.Position.GetDistance(newX, newY); + if (distance > 30) + { + return; + } + + session.PlayerEntity.TeleportOnMap(newX, newY); + session.SendDebugMessage($"[GURI] New position: {session.PlayerEntity.PositionX}/{session.PlayerEntity.PositionY}", ChatMessageColorType.Red); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/QuestTeleportDialogGuriHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/QuestTeleportDialogGuriHandler.cs new file mode 100644 index 0000000..af067f1 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/QuestTeleportDialogGuriHandler.cs @@ -0,0 +1,73 @@ +using System.Linq; +using System.Threading.Tasks; +using WingsEmu.DTOs.Quests; +using WingsEmu.Game._Guri; +using WingsEmu.Game._Guri.Event; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Quests; + +namespace WingsEmu.Plugins.BasicImplementations.Guri; + +public class QuestTeleportDialogGuriHandler : IGuriHandler +{ + private readonly IMapManager _mapManager; + private readonly IQuestManager _questManager; + + private readonly QuestTeleportDialogConfiguration _questTeleportDialogConfiguration; + + public QuestTeleportDialogGuriHandler(QuestTeleportDialogConfiguration questTeleportDialogConfiguration, IMapManager mapManager, IQuestManager questManager) + { + _questTeleportDialogConfiguration = questTeleportDialogConfiguration; + _mapManager = mapManager; + _questManager = questManager; + } + + public long GuriEffectId => 1000; + + public async Task ExecuteAsync(IClientSession session, GuriEvent e) + { + if (e.Packet.Length != 6) + { + return; + } + + if (!int.TryParse(e.Packet[3], out int mapId)) + { + return; + } + + if (!int.TryParse(e.Packet[4], out int posX)) + { + return; + } + + if (!int.TryParse(e.Packet[5], out int posY)) + { + return; + } + + QuestTeleportDialogInfo teleportDialogInfo = _questTeleportDialogConfiguration.FirstOrDefault(s => s.MapId == mapId && s.PositionX == posX && s.PositionY == posY); + if (teleportDialogInfo == null) + { + return; + } + + TutorialDto scriptWithTp = _questManager.GetScriptsTutorialByType(TutorialActionType.RUN).FirstOrDefault(s => s.Data == teleportDialogInfo.RunId); + if (scriptWithTp == null) + { + return; + } + + TutorialDto completedScript = _questManager.GetScriptTutorialById(scriptWithTp.Id - 1); + if (!session.PlayerEntity.HasCompletedScriptByIndex(completedScript.ScriptId, completedScript.ScriptIndex)) + { + return; + } + + IMapInstance mapInstance = _mapManager.GetBaseMapInstanceByMapId(teleportDialogInfo.MapId); + session.ChangeMap(mapInstance.Id, teleportDialogInfo.PositionX, teleportDialogInfo.PositionY); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/RainbowBattleCaptureFlagGuriHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/RainbowBattleCaptureFlagGuriHandler.cs new file mode 100644 index 0000000..1823de3 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/RainbowBattleCaptureFlagGuriHandler.cs @@ -0,0 +1,41 @@ +using System.Threading.Tasks; +using WingsEmu.Game._Guri; +using WingsEmu.Game._Guri.Event; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Networking; +using WingsEmu.Game.RainbowBattle.Event; + +namespace WingsEmu.Plugins.BasicImplementations.Guri; + +public class RainbowBattleCaptureFlagGuriHandler : IGuriHandler +{ + public long GuriEffectId => 504; + + public async Task ExecuteAsync(IClientSession session, GuriEvent e) + { + long npcId = e.Data; + + if (session.CurrentMapInstance is not { MapInstanceType: MapInstanceType.RainbowBattle }) + { + return; + } + + if (!session.PlayerEntity.RainbowBattleComponent.IsInRainbowBattle) + { + return; + } + + INpcEntity npcEntity = session.CurrentMapInstance.GetNpcById(npcId); + if (npcEntity == null) + { + return; + } + + await session.EmitEventAsync(new RainbowBattleCaptureFlagEvent + { + NpcEntity = npcEntity, + IsConfirm = true + }); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/RainbowBattleRegisterGuriHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/RainbowBattleRegisterGuriHandler.cs new file mode 100644 index 0000000..d398344 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/RainbowBattleRegisterGuriHandler.cs @@ -0,0 +1,38 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using WingsEmu.Game._Guri; +using WingsEmu.Game._Guri.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Game.RainbowBattle; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.Guri; + +public class RainbowBattleRegisterGuriHandler : IGuriHandler +{ + private readonly IRainbowBattleManager _rainbowBattleManager; + + public RainbowBattleRegisterGuriHandler(IRainbowBattleManager rainbowBattleManager) => _rainbowBattleManager = rainbowBattleManager; + + public long GuriEffectId => 503; + + public async Task ExecuteAsync(IClientSession session, GuriEvent e) + { + if (!_rainbowBattleManager.IsRegistrationActive) + { + return; + } + + if (_rainbowBattleManager.RegisteredPlayers.Contains(session.PlayerEntity.Id)) + { + return; + } + + _rainbowBattleManager.RegisterPlayer(session.PlayerEntity.Id); + + double timeLeft = (_rainbowBattleManager.RegistrationStartTime - DateTime.UtcNow).TotalSeconds; + session.SendBsInfoPacket(BsInfoType.OpenWindow, GameType.RainbowBattle, (ushort)timeLeft, QueueWindow.WaitForEntry); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/RainbowBattleUnfreezeGuriHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/RainbowBattleUnfreezeGuriHandler.cs new file mode 100644 index 0000000..f59faf0 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/RainbowBattleUnfreezeGuriHandler.cs @@ -0,0 +1,62 @@ +using System.Threading.Tasks; +using WingsEmu.Game._Guri; +using WingsEmu.Game._Guri.Event; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Game.RainbowBattle; +using WingsEmu.Game.RainbowBattle.Event; + +namespace WingsEmu.Plugins.BasicImplementations.Guri; + +public class RainbowBattleUnfreezeGuriHandler : IGuriHandler +{ + private readonly IDelayManager _delayManager; + + public RainbowBattleUnfreezeGuriHandler(IDelayManager delayManager) => _delayManager = delayManager; + + public long GuriEffectId => 505; + + public async Task ExecuteAsync(IClientSession session, GuriEvent e) + { + if (!await _delayManager.CanPerformAction(session.PlayerEntity, DelayedActionType.RainbowBattleUnfreeze)) + { + return; + } + + await _delayManager.CompleteAction(session.PlayerEntity, DelayedActionType.RainbowBattleUnfreeze); + + RainbowBattleParty rainbowBattleParty = session.PlayerEntity.RainbowBattleComponent.RainbowBattleParty; + if (rainbowBattleParty?.MapInstance == null) + { + return; + } + + long characterId = e.Data; + if (characterId == session.PlayerEntity.Id) + { + return; + } + + IPlayerEntity target = rainbowBattleParty.MapInstance.GetCharacterById(characterId); + if (target?.RainbowBattleComponent.RainbowBattleParty == null) + { + return; + } + + if (!target.RainbowBattleComponent.IsFrozen) + { + return; + } + + if (target.RainbowBattleComponent.Team != session.PlayerEntity.RainbowBattleComponent.Team) + { + return; + } + + await target.Session.EmitEventAsync(new RainbowBattleUnfreezeEvent + { + Unfreezer = session.PlayerEntity + }); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/RelictExaminationEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/RelictExaminationEventHandler.cs new file mode 100644 index 0000000..0401916 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/RelictExaminationEventHandler.cs @@ -0,0 +1,120 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.DTOs.Items; +using WingsEmu.Game; +using WingsEmu.Game._enum; +using WingsEmu.Game._Guri; +using WingsEmu.Game._Guri.Event; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.Guri; + +public class RelictExaminationEventHandler : IGuriHandler +{ + private readonly IDropRarityConfigurationProvider _dropRarityConfigurationProvider; + private readonly IGameItemInstanceFactory _gameItemInstanceFactory; + private readonly IItemsManager _itemsManager; + private readonly IRandomGenerator _randomGenerator; + + private readonly RelictConfiguration _relictConfiguration; + + public RelictExaminationEventHandler(RelictConfiguration relictConfiguration, IRandomGenerator randomGenerator, IItemsManager itemsManager, IGameItemInstanceFactory gameItemInstanceFactory, + IDropRarityConfigurationProvider dropRarityConfigurationProvider) + { + _relictConfiguration = relictConfiguration; + _randomGenerator = randomGenerator; + _itemsManager = itemsManager; + _gameItemInstanceFactory = gameItemInstanceFactory; + _dropRarityConfigurationProvider = dropRarityConfigurationProvider; + } + + public long GuriEffectId => 1502; + + public async Task ExecuteAsync(IClientSession session, GuriEvent e) + { + // 10000 => UNKNOWN RELICT + // 30000 => MYSTERIOUS RELICT + if (e.Packet.Length != 4) + { + return; + } + + if (session.PlayerEntity.Level < 60) + { + session.SendMsg(session.GetLanguage(GameDialogKey.ITEM_SHOUTMESSAGE_RELICT_LOW_LEVEL), MsgMessageType.Middle); + return; + } + + if (!int.TryParse(e.Packet[3], out int effect)) + { + return; + } + + int relictVnum = effect == 30000 ? (int)ItemVnums.MYSTERIOUS_RELICT : (int)ItemVnums.UNKNOWN_RELICT; + + InventoryItem relictItem = session.PlayerEntity.GetFirstItemByVnum(relictVnum); + + if (relictItem == null) + { + await e.Sender.NotifyStrangeBehavior(StrangeBehaviorSeverity.ABUSING, "Tried to examinate a relict without having one."); + return; + } + + RelictConfigurationInfo relictInfo = _relictConfiguration.FirstOrDefault(s => s.RelictVnum == relictVnum); + if (relictInfo == null) + { + return; + } + + if (session.PlayerEntity.Gold < relictInfo.ExaminationCost) + { + return; + } + + session.PlayerEntity.RemoveGold(relictInfo.ExaminationCost); + await session.RemoveItemFromInventory(item: relictItem, amount: 1); + + var randomBag = new RandomBag(_randomGenerator); + foreach (RelictRollCategory relictRollCategory in relictInfo.Rolls) + { + randomBag.AddEntry(relictRollCategory, relictRollCategory.Chance); + } + + RelictRollCategory rndCategory = randomBag.GetRandom(); + RelictRollItem rndItem = rndCategory.Items.ElementAt(_randomGenerator.RandomNumber(rndCategory.Items.Count)); + + IGameItem item = _itemsManager.GetItem(rndItem.ItemVnum); + sbyte rarity = _dropRarityConfigurationProvider.GetRandomRarity(item.ItemType); + + GameItemInstance itemToAdd = _gameItemInstanceFactory.CreateItem(rndItem.ItemVnum, rndItem.Amount, 0, rarity); + + InventoryItem inventoryItem = await session.AddNewItemToInventory(itemToAdd, true, sendGiftIsFull: true); + session.SendPdtiPacket(PdtiType.ItemIdentificationSuccessful, rndItem.ItemVnum, (short)rndItem.Amount, inventoryItem.Slot, itemToAdd.Upgrade, rarity); + session.SendSound(SoundType.CRAFTING_SUCCESS); + + session.RefreshGold(); + session.SendShopEndPacket(ShopEndType.Npc); + + ItemInstanceDTO reward = _gameItemInstanceFactory.CreateDto(itemToAdd); + + await session.EmitEventAsync(new BoxOpenedEvent + { + Box = relictItem.ItemInstance, + Rewards = new List + { + reward + } + }); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/ResetSpGuriHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/ResetSpGuriHandler.cs new file mode 100644 index 0000000..ac15670 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/ResetSpGuriHandler.cs @@ -0,0 +1,83 @@ +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsAPI.Game.Extensions.ItemExtension.Item; +using WingsEmu.Game._enum; +using WingsEmu.Game._Guri; +using WingsEmu.Game._Guri.Event; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Algorithm; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.Guri; + +public class ResetSpGuriHandler : IGuriHandler +{ + private readonly ICharacterAlgorithm _characterAlgorithm; + + private readonly IGameLanguageService _gameLanguageService; + private readonly IItemsManager _itemsManager; + private readonly ISessionManager _sessionManager; + + public ResetSpGuriHandler(IGameLanguageService gameLanguageService, ISessionManager sessionManager, ICharacterAlgorithm characterAlgorithm, IItemsManager itemsManager) + { + _gameLanguageService = gameLanguageService; + _sessionManager = sessionManager; + _characterAlgorithm = characterAlgorithm; + _itemsManager = itemsManager; + } + + public long GuriEffectId => 203; + + public async Task ExecuteAsync(IClientSession session, GuriEvent guriPacket) + { + if (guriPacket.Data != 0) + { + return; + } + + if (session.PlayerEntity.IsSeal) + { + return; + } + + bool hasPotion = session.PlayerEntity.HasItem((short)ItemVnums.RESET_SP_POINT); + + if (!hasPotion) + { + string itemName = _itemsManager.GetItem((short)ItemVnums.RESET_SP_POINT).GetItemName(_gameLanguageService, session.UserLanguage); + session.SendChatMessage(_gameLanguageService.GetLanguageFormat(GameDialogKey.INVENTORY_SHOUTMESSAGE_NOT_ENOUGH_ITEMS, session.UserLanguage, 1, itemName), ChatMessageColorType.Yellow); + return; + } + + if (!session.PlayerEntity.UseSp) + { + session.SendChatMessage(_gameLanguageService.GetLanguage(GameDialogKey.SPECIALIST_CHATMESSAGE_TRANSFORMATION_NEEDED, session.UserLanguage), ChatMessageColorType.Yellow); + return; + } + + GameItemInstance specialistInstance = session.PlayerEntity.Specialist; + if (specialistInstance == null || !session.PlayerEntity.UseSp) + { + await session.NotifyStrangeBehavior(StrangeBehaviorSeverity.NORMAL, "tried to reset SP points without SP."); + return; + } + + specialistInstance.SlDamage = 0; + specialistInstance.SlDefence = 0; + specialistInstance.SlElement = 0; + specialistInstance.SlHP = 0; + + await session.RemoveItemFromInventory((short)ItemVnums.RESET_SP_POINT); + session.SendCondPacket(); + session.SendSpecialistCardInfo(specialistInstance, _characterAlgorithm); + session.RefreshLevel(_characterAlgorithm); + session.RefreshStatChar(); + session.SendMsg(_gameLanguageService.GetLanguage(GameDialogKey.SPECIALIST_SHOUTMESSAGE_POINTS_RESET, session.UserLanguage), MsgMessageType.Middle); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/RollGeneratedItemGuriHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/RollGeneratedItemGuriHandler.cs new file mode 100644 index 0000000..a351194 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/RollGeneratedItemGuriHandler.cs @@ -0,0 +1,39 @@ +using System.Threading.Tasks; +using WingsEmu.Game._Guri; +using WingsEmu.Game._Guri.Event; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.Guri; + +public class RollGeneratedItemGuriHandler : IGuriHandler +{ + public long GuriEffectId => 4999; + + public async Task ExecuteAsync(IClientSession session, GuriEvent guriPacket) + { + if (guriPacket.Data != 8023) + { + return; + } + + if (guriPacket.User == null) + { + return; + } + + short slot = (short)guriPacket.User.Value; + InventoryItem box = session.PlayerEntity.GetItemBySlotAndType(slot, InventoryType.Main); + if (box == null) + { + return; + } + + await session.EmitEventAsync(new RollItemBoxEvent + { + Item = box + }); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/SecondSceneGuriHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/SecondSceneGuriHandler.cs new file mode 100644 index 0000000..751eaf3 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/SecondSceneGuriHandler.cs @@ -0,0 +1,13 @@ +using System.Threading.Tasks; +using WingsEmu.Game._Guri; +using WingsEmu.Game._Guri.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Plugins.BasicImplementations.Guri; + +public class SecondSceneGuriHandler : IGuriHandler +{ + public long GuriEffectId => 41; + public async Task ExecuteAsync(IClientSession session, GuriEvent e) => session.SendScene(41, true); +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/SheepEventGuriHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/SheepEventGuriHandler.cs new file mode 100644 index 0000000..b0cba99 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/SheepEventGuriHandler.cs @@ -0,0 +1,29 @@ +using System.Threading.Tasks; +using WingsEmu.Game._Guri; +using WingsEmu.Game._Guri.Event; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Plugins.BasicImplementations.Guri; + +public class SheepEventGuriHandler : IGuriHandler +{ + private readonly IServerManager _serverManager; + + public SheepEventGuriHandler(IServerManager serverManager) => _serverManager = serverManager; + + public long GuriEffectId => 514; + + public async Task ExecuteAsync(IClientSession session, GuriEvent guriPacket) + { + /*if (!_serverManager.EventInWaiting && session.Character.IsWaitingForEvent) + { + Log.Debug($"The player is not registered for this event. GuriEffectId : {GuriEffectId}"); + return; + } + + session.SendBsInfoPacket(BsInfoType.OpenWindow, GameType.SheepFarm, 30, QuequeWindow.WaitForEntry); + session.SendEsfPacket(2); + session.Character.IsWaitingForEvent = true;*/ + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/ShellIdGuriHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/ShellIdGuriHandler.cs new file mode 100644 index 0000000..2c57623 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/ShellIdGuriHandler.cs @@ -0,0 +1,96 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsAPI.Packets.Enums.Shells; +using WingsEmu.DTOs.Items; +using WingsEmu.Game._enum; +using WingsEmu.Game._Guri; +using WingsEmu.Game._Guri.Event; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Algorithm; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Items; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.Guri; + +public class ShellIdGuriHandler : IGuriHandler +{ + private readonly IGameLanguageService _lang; + private readonly IShellGenerationAlgorithm _shellGenerationAlgorithm; + + public ShellIdGuriHandler(IGameLanguageService gameLanguageService, IShellGenerationAlgorithm shellGenerationAlgorithm) + { + _lang = gameLanguageService; + _shellGenerationAlgorithm = shellGenerationAlgorithm; + } + + public long GuriEffectId => 204; + + public async Task ExecuteAsync(IClientSession session, GuriEvent guriPacket) + { + if (guriPacket.User == null) + { + // WRONG PACKET + return; + } + + var inventoryType = (InventoryType)guriPacket.Data; + InventoryItem shell = session.PlayerEntity.GetItemBySlotAndType((short)guriPacket.User.Value, inventoryType); + + if (shell == null) + { + return; + } + + if (shell.ItemInstance.Type != ItemInstanceType.WearableInstance) + { + return; + } + + GameItemInstance shellItem = shell.ItemInstance; + + if (!session.PlayerEntity.HasItem((short)ItemVnums.RAINBOW_PEARL)) + { + return; + } + + if (shellItem.EquipmentOptions != null && shellItem.EquipmentOptions.Any()) + { + // ALREADY IDENTIFIED + session.SendMsg(_lang.GetLanguage(GameDialogKey.SHELLS_SHOUTMESSAGE_ALREADY_IDENTIFIED, session.UserLanguage), MsgMessageType.Middle); + return; + } + + ShellType shellType = shellItem.GameItem.ShellType; + short perlsNeeded = (short)(shellItem.Upgrade / 10 + shellItem.Rarity); + + if (!session.PlayerEntity.HasItem((short)ItemVnums.RAINBOW_PEARL, perlsNeeded)) + { + // NOT ENOUGH PEARLS + return; + } + + IEnumerable shellOptions = _shellGenerationAlgorithm.GenerateShell((byte)shellType, shellItem.Rarity, shellItem.Upgrade).ToList(); + if (!shellOptions.Any()) + { + session.SendInfo(_lang.GetLanguage(GameDialogKey.SHELLS_INFO_SHELL_CANT_BE_IDENTIFIED, session.UserLanguage)); + return; + } + + shellItem.EquipmentOptions ??= new List(); + shellItem.EquipmentOptions.AddRange(shellOptions); + session.SendMsg(_lang.GetLanguage(GameDialogKey.SHELLS_SHOUTMESSAGE_IDENTIFIED, session.UserLanguage), MsgMessageType.Middle); + session.BroadcastEffect(EffectType.UpgradeSuccess); + await session.RemoveItemFromInventory((short)ItemVnums.RAINBOW_PEARL, perlsNeeded); + await session.EmitEventAsync(new ShellIdentifiedEvent + { + Shell = shellItem + }); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/ThirdSceneGuriHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/ThirdSceneGuriHandler.cs new file mode 100644 index 0000000..8a64af5 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/ThirdSceneGuriHandler.cs @@ -0,0 +1,13 @@ +using System.Threading.Tasks; +using WingsEmu.Game._Guri; +using WingsEmu.Game._Guri.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Plugins.BasicImplementations.Guri; + +public class ThirdSceneGuriHandler : IGuriHandler +{ + public long GuriEffectId => 42; + public async Task ExecuteAsync(IClientSession session, GuriEvent e) => session.SendScene(42, true); +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/TitleGuriHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/TitleGuriHandler.cs new file mode 100644 index 0000000..d592d55 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/TitleGuriHandler.cs @@ -0,0 +1,58 @@ +using System.Linq; +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.DTOs.Titles; +using WingsEmu.Game._Guri; +using WingsEmu.Game._Guri.Event; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.Guri; + +public class TitleGuriHandler : IGuriHandler +{ + private readonly IItemsManager _itemManager; + + public TitleGuriHandler(IItemsManager itemManager) => _itemManager = itemManager; + + public long GuriEffectId => 306; + + public async Task ExecuteAsync(IClientSession session, GuriEvent e) + { + if (e.User == null) + { + return; + } + + IGameItem title = _itemManager.GetItem(e.Data); + + if (title == null) + { + return; + } + + if (title.ItemType != ItemType.Title) + { + return; + } + + if (session.PlayerEntity.Titles.Any(s => s.ItemVnum == title.Id)) + { + return; + } + + session.PlayerEntity.Titles.Add(new CharacterTitleDto + { + TitleId = _itemManager.GetTitleId(title.Id), + ItemVnum = title.Id + }); + + await session.RemoveItemFromInventory(title.Id); + session.SendInfo(session.GetLanguage(GameDialogKey.TITLE_INFO_UNLOCKED)); + session.SendTitlePacket(); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/UseBoxGuriHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/UseBoxGuriHandler.cs new file mode 100644 index 0000000..fd841ff --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/UseBoxGuriHandler.cs @@ -0,0 +1,42 @@ +using System.Threading.Tasks; +using PhoenixLib.Logging; +using WingsEmu.Game._Guri; +using WingsEmu.Game._Guri.Event; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.Guri; + +public class UseBoxGuriHandler : IGuriHandler +{ + public long GuriEffectId => 300; + + public async Task ExecuteAsync(IClientSession session, GuriEvent guriPacket) + { + if (guriPacket.Data != 8023) + { + return; + } + + if (guriPacket.User == null) + { + return; + } + + short slot = (short)guriPacket.User.Value; + InventoryItem box = session.PlayerEntity.GetItemBySlotAndType(slot, InventoryType.Equipment); + + if (box == null) + { + Log.Info("No box"); + return; + } + + await session.EmitEventAsync(new RollItemBoxEvent + { + Item = box + }); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/WeddingGuriHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/WeddingGuriHandler.cs new file mode 100644 index 0000000..6da3ca3 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/WeddingGuriHandler.cs @@ -0,0 +1,78 @@ +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.RelationsExtensions; +using WingsEmu.Game._Guri; +using WingsEmu.Game._Guri.Event; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Groups.Events; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Relations; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; +using WingsEmu.Packets.Enums.Relations; + +namespace WingsEmu.Plugins.BasicImplementations.Guri; + +public class WeddingGuriHandler : IGuriHandler +{ + private readonly IGameLanguageService _gameLanguage; + private readonly IInvitationManager _invitationManager; + + private readonly ISessionManager _sessionManager; + + public WeddingGuriHandler(ISessionManager sessionManager, IGameLanguageService gameLanguage, IInvitationManager invitationManager) + { + _sessionManager = sessionManager; + _gameLanguage = gameLanguage; + _invitationManager = invitationManager; + } + + public long GuriEffectId => 603; + + public async Task ExecuteAsync(IClientSession session, GuriEvent e) + { + if (e.User == null) + { + return; + } + + long targetId = e.User.Value; + + if (!_invitationManager.ContainsPendingInvitation(targetId, session.PlayerEntity.Id, InvitationType.Spouse)) + { + return; + } + + _invitationManager.RemovePendingInvitation(targetId, session.PlayerEntity.Id, InvitationType.Spouse); + + IClientSession otherSession = _sessionManager.GetSessionByCharacterId(targetId); + if (otherSession == null) + { + return; + } + + switch (e.Data) + { + case 1: + await session.RemoveRelationAsync(otherSession.PlayerEntity.Id, CharacterRelationType.Friend); + await session.AddRelationAsync(otherSession.PlayerEntity.Id, CharacterRelationType.Spouse); + _sessionManager.Broadcast(s => + { + string message = _gameLanguage.GetLanguageFormat(GameDialogKey.WEDDING_SHOUTMESSAGE_BROADCAST, s.UserLanguage, session.PlayerEntity.Name, otherSession.PlayerEntity.Name); + return s.GenerateMsgPacket(message, MsgMessageType.Middle); + }); + await session.EmitEventAsync(new GroupWeedingEvent()); + + break; + + case 0: + _sessionManager.Broadcast(s => + { + string message = _gameLanguage.GetLanguageFormat(GameDialogKey.WEDDING_SHOUTMESSAGE_REFUSED_BROADCAST, s.UserLanguage, session.PlayerEntity.Name, otherSession.PlayerEntity.Name); + return s.GenerateMsgPacket(message, MsgMessageType.Middle); + }); + break; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/WingsOfFriendshipGuriHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/WingsOfFriendshipGuriHandler.cs new file mode 100644 index 0000000..868eb26 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Guri/WingsOfFriendshipGuriHandler.cs @@ -0,0 +1,143 @@ +using System; +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game._enum; +using WingsEmu.Game._Guri; +using WingsEmu.Game._Guri.Event; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.Guri; + +public class WingsOfFriendshipGuriHandler : IGuriHandler +{ + private readonly IDelayManager _delay; + + private readonly IGameLanguageService _gameLanguageService; + private readonly ISessionManager _sessionManager; + + public WingsOfFriendshipGuriHandler(IGameLanguageService gameLanguageService, IMapManager mapManager, ISessionManager sessionManager, IDelayManager delay) + { + _sessionManager = sessionManager; + _delay = delay; + _gameLanguageService = gameLanguageService; + } + + public long GuriEffectId => 199; + + public async Task ExecuteAsync(IClientSession session, GuriEvent guriPacket) + { + if (guriPacket.User == null) + { + return; + } + + if (!long.TryParse(guriPacket.User.Value.ToString(), out long charId)) + { + return; + } + + if (session.CantPerformActionOnAct4()) + { + return; + } + + IClientSession otherSession = _sessionManager.GetSessionByCharacterId(charId); + + bool isMarriedToTarget = session.PlayerEntity.IsMarried(charId); + + if (!session.PlayerEntity.IsFriend(charId) && !isMarriedToTarget) + { + session.SendMsg(_gameLanguageService.GetLanguage(GameDialogKey.FRIEND_SHOUTMESSAGE_CHARACTER_NOT_FRIEND, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (otherSession == null) + { + session.SendMsg(_gameLanguageService.GetLanguage(GameDialogKey.INFORMATION_MESSAGE_USER_NOT_CONNECTED, session.UserLanguage), MsgMessageType.Middle); + return; + } + + bool hasItem = session.PlayerEntity.HasItem((short)ItemVnums.WING_OF_FRIENDSHIP); + bool hasLimitedItem = session.PlayerEntity.HasItem((short)ItemVnums.WING_OF_FRIENDSHIP_LIMITED); + + if (!hasItem && !hasLimitedItem && !isMarriedToTarget) + { + session.SendChatMessage(_gameLanguageService.GetLanguage(GameDialogKey.INFORMATION_CHATMESSAGE_NO_WINGS, session.UserLanguage), ChatMessageColorType.Red); + return; + } + + if (!session.CurrentMapInstance.HasMapFlag(MapFlags.IS_BASE_MAP) && !session.CurrentMapInstance.HasMapFlag(MapFlags.ACT_4)) + { + session.SendMsg(_gameLanguageService.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_MUST_BE_IN_CLASSIC_MAP, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (!otherSession.CurrentMapInstance.HasMapFlag(MapFlags.IS_BASE_MAP) && !otherSession.CurrentMapInstance.HasMapFlag(MapFlags.ACT_4)) + { + session.SendMsg(_gameLanguageService.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_USER_NOT_BASEMAP, session.UserLanguage), MsgMessageType.Middle); + return; + } + + // Act 5 + if (session.IsInAct5() && !otherSession.IsInAct5()) + { + session.SendMsg(_gameLanguageService.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_MUST_BE_IN_CLASSIC_MAP, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (!session.IsInAct5() && otherSession.IsInAct5()) + { + session.SendMsg(_gameLanguageService.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_USER_NOT_BASEMAP, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (guriPacket.Data != 2) + { + DateTime waitUntil = await _delay.RegisterAction(session.PlayerEntity, DelayedActionType.WingOfFriendship); + session.SendDelay((int)(waitUntil - DateTime.UtcNow).TotalMilliseconds, GuriType.UsingItem, $"guri 199 2 {charId}"); + return; + } + + if (!await _delay.CanPerformAction(session.PlayerEntity, DelayedActionType.WingOfFriendship)) + { + return; + } + + await _delay.CompleteAction(session.PlayerEntity, DelayedActionType.WingOfFriendship); + + switch (otherSession.CurrentMapInstance.MapInstanceType) + { + case MapInstanceType.Act4Instance: + if (session.PlayerEntity.Faction != otherSession.PlayerEntity.Faction) + { + return; + } + + short mapInstanceX = otherSession.PlayerEntity.PositionY; + short mapInstanceY = otherSession.PlayerEntity.PositionX; + + session.ChangeMap(otherSession.CurrentMapInstance, mapInstanceX, mapInstanceY); + break; + default: + short mapY = otherSession.PlayerEntity.PositionY; + short mapX = otherSession.PlayerEntity.PositionX; + int mapId = otherSession.PlayerEntity.MapInstance.MapId; + + session.ChangeMap(mapId, mapX, mapY); + + break; + } + + if (!isMarriedToTarget) + { + await session.RemoveItemFromInventory(hasLimitedItem ? (short)ItemVnums.WING_OF_FRIENDSHIP_LIMITED : (short)ItemVnums.WING_OF_FRIENDSHIP); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/GuriPlugin.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/GuriPlugin.cs new file mode 100644 index 0000000..9714238 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/GuriPlugin.cs @@ -0,0 +1,44 @@ +using System; +using PhoenixLib.Extensions; +using PhoenixLib.Logging; +using WingsAPI.Plugins; +using WingsEmu.Game._Guri; + +namespace WingsEmu.Plugins.BasicImplementations; + +public class GuriPlugin : IGamePlugin +{ + private readonly IServiceProvider _container; + private readonly IGuriHandlerContainer _handlers; + + public GuriPlugin(IGuriHandlerContainer handlers, IServiceProvider container) + { + _handlers = handlers; + _container = container; + } + + public string Name => nameof(GuriPlugin); + + + public void OnLoad() + { + foreach (Type handlerType in typeof(GuriPlugin).Assembly.GetTypesImplementingInterface()) + { + try + { + object tmp = _container.GetService(handlerType); + if (!(tmp is IGuriHandler real)) + { + continue; + } + + Log.Debug($"[GURI][ADD_HANDLER] {handlerType}"); + _handlers.Register(real); + } + catch (Exception e) + { + Log.Error("[GURI][FAIL_ADD]", e); + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/GuriPluginCore.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/GuriPluginCore.cs new file mode 100644 index 0000000..8eb689e --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/GuriPluginCore.cs @@ -0,0 +1,14 @@ +using Microsoft.Extensions.DependencyInjection; +using WingsAPI.Plugins; + +namespace WingsEmu.Plugins.BasicImplementations; + +public class GuriPluginCore : IGameServerPlugin +{ + public string Name => nameof(GuriPluginCore); + + public void AddDependencies(IServiceCollection services, GameServerLoader gameServer) + { + services.AddGuriHandlers(); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Helpers/GameHelpersPlugin.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Helpers/GameHelpersPlugin.cs new file mode 100644 index 0000000..804d51d --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Helpers/GameHelpersPlugin.cs @@ -0,0 +1,22 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using WingsAPI.Plugins; +using WingsEmu.Game.Algorithm; +using WingsEmu.Game.Buffs; + +namespace WingsEmu.Plugins.BasicImplementations; + +public class GameHelpersPlugin : IGamePlugin +{ + private readonly IServiceProvider _services; + + public GameHelpersPlugin(IServiceProvider services) => _services = services; + + public string Name => nameof(GameHelpersPlugin); + + public void OnLoad() + { + StaticCharacterAlgorithmService.Initialize(_services.GetService()); + StaticBCardEffectHandlerService.Initialize(_services.GetService()); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/Consumer/BazaarNotificationMessageConsumer.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/Consumer/BazaarNotificationMessageConsumer.cs new file mode 100644 index 0000000..a17db34 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/Consumer/BazaarNotificationMessageConsumer.cs @@ -0,0 +1,41 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; +using WingsAPI.Game.Extensions.ItemExtension.Item; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums.Chat; +using WingsEmu.Plugins.DistributedGameEvents.InterChannel; + +namespace WingsEmu.Plugins.BasicImplementations.InterChannel.Consumer; + +public class BazaarNotificationMessageConsumer : IMessageConsumer +{ + private readonly IGameLanguageService _gameLanguage; + private readonly IItemsManager _itemsManager; + private readonly ISessionManager _sessionManager; + + public BazaarNotificationMessageConsumer(ISessionManager sessionManager, IItemsManager itemsManager, IGameLanguageService gameLanguage) + { + _sessionManager = sessionManager; + _itemsManager = itemsManager; + _gameLanguage = gameLanguage; + } + + public async Task HandleAsync(BazaarNotificationMessage notification, CancellationToken token) + { + IClientSession owner = _sessionManager.GetSessionByCharacterName(notification.OwnerName); + if (owner == null) + { + return; + } + + string itemName = _itemsManager.GetItem(notification.ItemVnum).GetItemName(_gameLanguage, owner.UserLanguage); + int amount = notification.Amount; + + owner.SendChatMessage(owner.GetLanguageFormat(GameDialogKey.BAZAAR_CHATMESSAGE_ITEM_SOLD, amount, itemName), ChatMessageColorType.Green); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/Consumer/ChatShoutAdminMessageConsumer.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/Consumer/ChatShoutAdminMessageConsumer.cs new file mode 100644 index 0000000..d1a1597 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/Consumer/ChatShoutAdminMessageConsumer.cs @@ -0,0 +1,28 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums.Chat; +using WingsEmu.Plugins.DistributedGameEvents.InterChannel; + +namespace WingsEmu.Plugins.BasicImplementations.InterChannel.Consumer; + +public class ChatShoutAdminMessageConsumer : IMessageConsumer +{ + private readonly ISessionManager _serverManager; + + public ChatShoutAdminMessageConsumer(ISessionManager serverManager) => _serverManager = serverManager; + + public async Task HandleAsync(ChatShoutAdminMessage notification, CancellationToken token) + { + string message = notification.Message; + + string msg = ((IClientSession)null).GenerateMsgPacket(message, MsgMessageType.MiddleYellow); + _serverManager.Broadcast(x => msg); + + _serverManager.Broadcast(session => session.GenerateSayPacket($"({session.GetLanguage(GameDialogKey.ADMIN_BROADCAST_CHATMESSAGE_SENDER)}): {message}", ChatMessageColorType.Yellow)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/Consumer/InterChannelChatMessageBroadcastMessageConsumer.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/Consumer/InterChannelChatMessageBroadcastMessageConsumer.cs new file mode 100644 index 0000000..90a9b50 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/Consumer/InterChannelChatMessageBroadcastMessageConsumer.cs @@ -0,0 +1,26 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers; +using WingsEmu.Plugins.DistributedGameEvents.InterChannel; + +namespace WingsEmu.Plugins.BasicImplementations.InterChannel.Consumer; + +public class InterChannelChatMessageBroadcastMessageConsumer : IMessageConsumer +{ + private readonly IGameLanguageService _languageService; + private readonly ISessionManager _sessionManager; + + public InterChannelChatMessageBroadcastMessageConsumer(ISessionManager sessionManager, IGameLanguageService languageService) + { + _sessionManager = sessionManager; + _languageService = languageService; + } + + public async Task HandleAsync(InterChannelChatMessageBroadcastMessage notification, CancellationToken token) + { + _sessionManager.Broadcast(x => x.GenerateSayPacket(_languageService.GetLanguageFormat(notification.DialogKey, x.UserLanguage, notification.Args), notification.ChatMessageColorType)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/Consumer/InterChannelSendChatMsgByCharIdMessageConsumer.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/Consumer/InterChannelSendChatMsgByCharIdMessageConsumer.cs new file mode 100644 index 0000000..a8102c9 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/Consumer/InterChannelSendChatMsgByCharIdMessageConsumer.cs @@ -0,0 +1,28 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Plugins.DistributedGameEvents.InterChannel; + +namespace WingsEmu.Plugins.BasicImplementations.InterChannel.Consumer; + +public class InterChannelSendChatMsgByCharIdMessageConsumer : IMessageConsumer +{ + private readonly IGameLanguageService _languageService; + private readonly ISessionManager _sessionManager; + + public InterChannelSendChatMsgByCharIdMessageConsumer(ISessionManager sessionManager, IGameLanguageService languageService) + { + _sessionManager = sessionManager; + _languageService = languageService; + } + + public async Task HandleAsync(InterChannelSendChatMsgByCharIdMessage e, CancellationToken cancellation) + { + IClientSession localSession = _sessionManager.GetSessionByCharacterId(e.CharacterId); + localSession?.SendChatMessage(_languageService.GetLanguage(e.DialogKey, localSession.UserLanguage), e.ChatMessageColorType); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/Consumer/InterChannelSendChatMsgByNicknameMessageConsumer.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/Consumer/InterChannelSendChatMsgByNicknameMessageConsumer.cs new file mode 100644 index 0000000..be2142d --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/Consumer/InterChannelSendChatMsgByNicknameMessageConsumer.cs @@ -0,0 +1,28 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Plugins.DistributedGameEvents.InterChannel; + +namespace WingsEmu.Plugins.BasicImplementations.InterChannel.Consumer; + +public class InterChannelSendChatMsgByNicknameMessageConsumer : IMessageConsumer +{ + private readonly IGameLanguageService _languageService; + private readonly ISessionManager _sessionManager; + + public InterChannelSendChatMsgByNicknameMessageConsumer(ISessionManager sessionManager, IGameLanguageService languageService) + { + _sessionManager = sessionManager; + _languageService = languageService; + } + + public async Task HandleAsync(InterChannelSendChatMsgByNicknameMessage e, CancellationToken cancellation) + { + IClientSession localSession = _sessionManager.GetSessionByCharacterName(e.Nickname); + localSession?.SendChatMessage(_languageService.GetLanguage(e.DialogKey, localSession.UserLanguage), e.ChatMessageColorType); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/Consumer/InterChannelSendInfoByCharIdMessageConsumer.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/Consumer/InterChannelSendInfoByCharIdMessageConsumer.cs new file mode 100644 index 0000000..54e2164 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/Consumer/InterChannelSendInfoByCharIdMessageConsumer.cs @@ -0,0 +1,28 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Plugins.DistributedGameEvents.InterChannel; + +namespace WingsEmu.Plugins.BasicImplementations.InterChannel.Consumer; + +public class InterChannelSendInfoByCharIdMessageConsumer : IMessageConsumer +{ + private readonly IGameLanguageService _languageService; + private readonly ISessionManager _sessionManager; + + public InterChannelSendInfoByCharIdMessageConsumer(ISessionManager sessionManager, IGameLanguageService languageService) + { + _sessionManager = sessionManager; + _languageService = languageService; + } + + public async Task HandleAsync(InterChannelSendInfoByCharIdMessage e, CancellationToken cancellation) + { + IClientSession localSession = _sessionManager.GetSessionByCharacterId(e.CharacterId); + localSession?.SendInfo(_languageService.GetLanguage(e.DialogKey, localSession.UserLanguage)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/Consumer/InterChannelSendInfoByNicknameMessageConsumer.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/Consumer/InterChannelSendInfoByNicknameMessageConsumer.cs new file mode 100644 index 0000000..1e9cb65 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/Consumer/InterChannelSendInfoByNicknameMessageConsumer.cs @@ -0,0 +1,28 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Plugins.DistributedGameEvents.InterChannel; + +namespace WingsEmu.Plugins.BasicImplementations.InterChannel.Consumer; + +public class InterChannelSendInfoByNicknameMessageConsumer : IMessageConsumer +{ + private readonly IGameLanguageService _languageService; + private readonly ISessionManager _sessionManager; + + public InterChannelSendInfoByNicknameMessageConsumer(ISessionManager sessionManager, IGameLanguageService languageService) + { + _sessionManager = sessionManager; + _languageService = languageService; + } + + public async Task HandleAsync(InterChannelSendInfoByNicknameMessage e, CancellationToken cancellation) + { + IClientSession localSession = _sessionManager.GetSessionByCharacterName(e.Nickname); + localSession?.SendInfo(_languageService.GetLanguage(e.DialogKey, localSession.UserLanguage)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/Consumer/InterChannelSendWhisperMessageConsumer.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/Consumer/InterChannelSendWhisperMessageConsumer.cs new file mode 100644 index 0000000..f36b5a2 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/Consumer/InterChannelSendWhisperMessageConsumer.cs @@ -0,0 +1,28 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; +using WingsEmu.Game.InterChannel; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Plugins.DistributedGameEvents.InterChannel; + +namespace WingsEmu.Plugins.BasicImplementations.InterChannel.Consumer; + +public class InterChannelSendWhisperMessageConsumer : IMessageConsumer +{ + private readonly ISessionManager _sessionManager; + + public InterChannelSendWhisperMessageConsumer(ISessionManager sessionManager) => _sessionManager = sessionManager; + + public async Task HandleAsync(InterChannelSendWhisperMessage e, CancellationToken cancellation) + { + IClientSession session = _sessionManager.GetSessionByCharacterName(e.ReceiverNickname); + + if (session == null) + { + return; + } + + await session.EmitEventAsync(new InterChannelReceiveWhisperEvent(e.SenderCharacterId, e.SenderNickname, e.SenderChannelId, e.AuthorityType, e.Message)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/Event/ChatShoutAdminEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/Event/ChatShoutAdminEventHandler.cs new file mode 100644 index 0000000..c9bbe64 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/Event/ChatShoutAdminEventHandler.cs @@ -0,0 +1,37 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using PhoenixLib.ServiceBus; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.InterChannel; +using WingsEmu.Game.Managers; +using WingsEmu.Packets.Enums.Chat; +using WingsEmu.Plugins.DistributedGameEvents.InterChannel; + +namespace WingsEmu.Plugins.BasicImplementations.InterChannel.Event; + +public class ChatShoutAdminEventHandler : IAsyncEventProcessor +{ + private readonly IMessagePublisher _messagePublisher; + private readonly ISessionManager _serverManager; + + public ChatShoutAdminEventHandler(IMessagePublisher messagePublisher, ISessionManager serverManager) + { + _messagePublisher = messagePublisher; + _serverManager = serverManager; + } + + public async Task HandleAsync(ChatShoutAdminEvent e, CancellationToken cancellation) + { + string msg = e.Sender.GenerateMsgPacket(e.Message, MsgMessageType.MiddleYellow); + _serverManager.Broadcast(x => msg); + + string message = e.Message; + _serverManager.Broadcast(session => session.GenerateSayPacket($"({session.GetLanguage(GameDialogKey.ADMIN_BROADCAST_CHATMESSAGE_SENDER)}): {message}", ChatMessageColorType.Yellow)); + await _messagePublisher.PublishAsync(new ChatShoutAdminMessage + { + Message = e.Message + }, cancellation); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/Event/InterChannelChatMessageBroadcastEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/Event/InterChannelChatMessageBroadcastEventHandler.cs new file mode 100644 index 0000000..d3951f5 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/Event/InterChannelChatMessageBroadcastEventHandler.cs @@ -0,0 +1,37 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using PhoenixLib.ServiceBus; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.InterChannel; +using WingsEmu.Game.Managers; +using WingsEmu.Plugins.DistributedGameEvents.InterChannel; + +namespace WingsEmu.Plugins.BasicImplementations.InterChannel.Event; + +public class InterChannelChatMessageBroadcastEventHandler : IAsyncEventProcessor +{ + private readonly IGameLanguageService _languageService; + private readonly IMessagePublisher _messagePublisher; + private readonly ISessionManager _sessionManager; + + public InterChannelChatMessageBroadcastEventHandler(ISessionManager sessionManager, IGameLanguageService languageService, + IMessagePublisher messagePublisher) + { + _sessionManager = sessionManager; + _languageService = languageService; + _messagePublisher = messagePublisher; + } + + public async Task HandleAsync(InterChannelChatMessageBroadcastEvent e, CancellationToken cancellation) + { + await _messagePublisher.PublishAsync(new InterChannelChatMessageBroadcastMessage + { + Args = e.Args, + ChatMessageColorType = e.ChatMessageColorType, + DialogKey = e.DialogKey + }, cancellation); + _sessionManager.Broadcast(x => x.GenerateSayPacket(_languageService.GetLanguageFormat(e.DialogKey, x.UserLanguage, e.Args), e.ChatMessageColorType)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/Event/InterChannelReceiveWhisperEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/Event/InterChannelReceiveWhisperEventHandler.cs new file mode 100644 index 0000000..1c5896d --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/Event/InterChannelReceiveWhisperEventHandler.cs @@ -0,0 +1,46 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using PhoenixLib.MultiLanguage; +using WingsEmu.DTOs.Account; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.InterChannel; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.InterChannel.Event; + +public class InterChannelReceiveWhisperEventHandler : IAsyncEventProcessor +{ + private readonly IGameLanguageService _languageService; + + public InterChannelReceiveWhisperEventHandler(IGameLanguageService languageService) => _languageService = languageService; + + public async Task HandleAsync(InterChannelReceiveWhisperEvent e, CancellationToken cancellation) + { + if (e.Sender.PlayerEntity.WhisperBlocked && e.AuthorityType < AuthorityType.GameMaster) + { + await e.Sender.EmitEventAsync(new InterChannelSendChatMsgByCharIdEvent(e.SenderCharacterId, GameDialogKey.INFORMATION_CHATMESSAGE_USER_WHISPER_BLOCKED, ChatMessageColorType.Red)); + return; + } + + if (e.Sender.PlayerEntity.IsBlocking(e.SenderCharacterId) && e.AuthorityType < AuthorityType.GameMaster) + { + await e.Sender.EmitEventAsync(new InterChannelSendChatMsgByCharIdEvent(e.SenderCharacterId, GameDialogKey.BLACKLIST_INFO_BLOCKING, ChatMessageColorType.Red)); + return; + } + + e.Sender.ReceiveSpeakWhisper(e.SenderCharacterId, e.SenderNickname, AddChannelToMessage(e.Message, e.SenderChannelId, e.Sender.UserLanguage), + e.AuthorityType >= AuthorityType.GameMaster ? SpeakType.GameMaster : SpeakType.Player); + } + + private string AddChannelToMessage(string message, int senderChannelId, RegionLanguageType userLanguage) + { + if (senderChannelId == -1) + { + return message; + } + + return message + $" <{_languageService.GetLanguage(GameDialogKey.INFORMATION_CHANNEL, userLanguage)}: {senderChannelId.ToString()}>"; + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/Event/InterChannelSendChatMsgByCharIdEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/Event/InterChannelSendChatMsgByCharIdEventHandler.cs new file mode 100644 index 0000000..afc7d26 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/Event/InterChannelSendChatMsgByCharIdEventHandler.cs @@ -0,0 +1,49 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using PhoenixLib.ServiceBus; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.InterChannel; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Plugins.DistributedGameEvents.InterChannel; + +namespace WingsEmu.Plugins.BasicImplementations.InterChannel.Event; + +public class InterChannelSendChatMsgByCharIdEventHandler : IAsyncEventProcessor +{ + private readonly IGameLanguageService _languageService; + private readonly IMessagePublisher _messagePublisher; + private readonly ISessionManager _sessionManager; + + public InterChannelSendChatMsgByCharIdEventHandler(IMessagePublisher messagePublisher, ISessionManager sessionManager, IGameLanguageService languageService) + { + _messagePublisher = messagePublisher; + _sessionManager = sessionManager; + _languageService = languageService; + } + + public async Task HandleAsync(InterChannelSendChatMsgByCharIdEvent e, CancellationToken cancellation) + { + IClientSession localSession = _sessionManager.GetSessionByCharacterId(e.CharacterId); + + if (localSession == null) + { + if (!_sessionManager.IsOnline(e.CharacterId)) + { + return; + } + + await _messagePublisher.PublishAsync(new InterChannelSendChatMsgByCharIdMessage + { + CharacterId = e.CharacterId, + DialogKey = e.DialogKey, + ChatMessageColorType = e.ChatMessageColorType + }, cancellation); + return; + } + + localSession.SendChatMessage(_languageService.GetLanguage(e.DialogKey, localSession.UserLanguage), e.ChatMessageColorType); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/Event/InterChannelSendChatMsgByNicknameEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/Event/InterChannelSendChatMsgByNicknameEventHandler.cs new file mode 100644 index 0000000..6cc8e85 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/Event/InterChannelSendChatMsgByNicknameEventHandler.cs @@ -0,0 +1,50 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using PhoenixLib.ServiceBus; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.InterChannel; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Plugins.DistributedGameEvents.InterChannel; + +namespace WingsEmu.Plugins.BasicImplementations.InterChannel.Event; + +public class InterChannelSendChatMsgByNicknameEventHandler : IAsyncEventProcessor +{ + private readonly IGameLanguageService _languageService; + private readonly IMessagePublisher _messagePublisher; + private readonly ISessionManager _sessionManager; + + public InterChannelSendChatMsgByNicknameEventHandler(ISessionManager sessionManager, IGameLanguageService languageService, + IMessagePublisher messagePublisher) + { + _sessionManager = sessionManager; + _languageService = languageService; + _messagePublisher = messagePublisher; + } + + public async Task HandleAsync(InterChannelSendChatMsgByNicknameEvent e, CancellationToken cancellation) + { + IClientSession localSession = _sessionManager.GetSessionByCharacterName(e.Nickname); + + if (localSession == null) + { + if (!_sessionManager.IsOnline(e.Nickname)) + { + return; + } + + await _messagePublisher.PublishAsync(new InterChannelSendChatMsgByNicknameMessage + { + Nickname = e.Nickname, + DialogKey = e.DialogKey, + ChatMessageColorType = e.ChatMessageColorType + }, cancellation); + return; + } + + localSession.SendChatMessage(_languageService.GetLanguage(e.DialogKey, localSession.UserLanguage), e.ChatMessageColorType); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/Event/InterChannelSendInfoByCharIdEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/Event/InterChannelSendInfoByCharIdEventHandler.cs new file mode 100644 index 0000000..4877b9c --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/Event/InterChannelSendInfoByCharIdEventHandler.cs @@ -0,0 +1,48 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using PhoenixLib.ServiceBus; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.InterChannel; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Plugins.DistributedGameEvents.InterChannel; + +namespace WingsEmu.Plugins.BasicImplementations.InterChannel.Event; + +public class InterChannelSendInfoByCharIdEventHandler : IAsyncEventProcessor +{ + private readonly IGameLanguageService _languageService; + private readonly IMessagePublisher _messagePublisher; + private readonly ISessionManager _sessionManager; + + public InterChannelSendInfoByCharIdEventHandler(IMessagePublisher messagePublisher, ISessionManager sessionManager, IGameLanguageService languageService) + { + _messagePublisher = messagePublisher; + _sessionManager = sessionManager; + _languageService = languageService; + } + + public async Task HandleAsync(InterChannelSendInfoByCharIdEvent e, CancellationToken cancellation) + { + IClientSession localSession = _sessionManager.GetSessionByCharacterId(e.CharacterId); + + if (localSession == null) + { + if (!_sessionManager.IsOnline(e.CharacterId)) + { + return; + } + + await _messagePublisher.PublishAsync(new InterChannelSendInfoByCharIdMessage + { + CharacterId = e.CharacterId, + DialogKey = e.DialogKey + }, cancellation); + return; + } + + localSession.SendInfo(_languageService.GetLanguage(e.DialogKey, localSession.UserLanguage)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/Event/InterChannelSendInfoByNicknameEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/Event/InterChannelSendInfoByNicknameEventHandler.cs new file mode 100644 index 0000000..3eb676d --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/Event/InterChannelSendInfoByNicknameEventHandler.cs @@ -0,0 +1,48 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using PhoenixLib.ServiceBus; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.InterChannel; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Plugins.DistributedGameEvents.InterChannel; + +namespace WingsEmu.Plugins.BasicImplementations.InterChannel.Event; + +public class InterChannelSendInfoByNicknameEventHandler : IAsyncEventProcessor +{ + private readonly IGameLanguageService _languageService; + private readonly IMessagePublisher _messagePublisher; + private readonly ISessionManager _sessionManager; + + public InterChannelSendInfoByNicknameEventHandler(IMessagePublisher messagePublisher, ISessionManager sessionManager, IGameLanguageService languageService) + { + _messagePublisher = messagePublisher; + _sessionManager = sessionManager; + _languageService = languageService; + } + + public async Task HandleAsync(InterChannelSendInfoByNicknameEvent e, CancellationToken cancellation) + { + IClientSession localSession = _sessionManager.GetSessionByCharacterName(e.Nickname); + + if (localSession == null) + { + if (!_sessionManager.IsOnline(e.Nickname)) + { + return; + } + + await _messagePublisher.PublishAsync(new InterChannelSendInfoByNicknameMessage + { + Nickname = e.Nickname, + DialogKey = e.DialogKey + }, cancellation); + return; + } + + localSession.SendInfo(_languageService.GetLanguage(e.DialogKey, localSession.UserLanguage)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/Event/InterChannelSendWhisperEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/Event/InterChannelSendWhisperEventHandler.cs new file mode 100644 index 0000000..638e3e0 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/Event/InterChannelSendWhisperEventHandler.cs @@ -0,0 +1,95 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using PhoenixLib.ServiceBus; +using WingsAPI.Communication.Player; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Chat; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.InterChannel; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums.Chat; +using WingsEmu.Plugins.DistributedGameEvents.InterChannel; +using ChatType = WingsEmu.Game._playerActionLogs.ChatType; + +namespace WingsEmu.Plugins.BasicImplementations.InterChannel.Event; + +public class InterChannelSendWhisperEventHandler : IAsyncEventProcessor +{ + private readonly IGameLanguageService _languageService; + private readonly IMessagePublisher _messagePublisher; + private readonly IServerManager _serverManager; + private readonly ISessionManager _sessionManager; + + public InterChannelSendWhisperEventHandler(ISessionManager sessionManager, IMessagePublisher messagePublisher, IGameLanguageService languageService, + IServerManager serverManager) + { + _sessionManager = sessionManager; + _messagePublisher = messagePublisher; + _languageService = languageService; + _serverManager = serverManager; + } + + public async Task HandleAsync(InterChannelSendWhisperEvent e, CancellationToken cancellation) + { + if (e.Nickname == e.Sender.PlayerEntity.Name) + { + return; + } + + IClientSession session = e.Sender; + IClientSession localSession = _sessionManager.GetSessionByCharacterName(e.Nickname); + + if (localSession == null) + { + if (!_sessionManager.IsOnline(e.Nickname)) + { + e.Sender.SendErrorChatMessage(_languageService.GetLanguage(GameDialogKey.INFORMATION_MESSAGE_USER_NOT_CONNECTED, e.Sender.UserLanguage)); + return; + } + + e.Sender.SendSpeak(e.Message, SpeakType.Player); + await _messagePublisher.PublishAsync(new InterChannelSendWhisperMessage + { + SenderCharacterId = e.Sender.PlayerEntity.Id, + SenderNickname = e.Sender.PlayerEntity.Name, + SenderChannelId = _serverManager.ChannelId, + AuthorityType = e.Sender.PlayerEntity.Authority, + Message = e.Message, + ReceiverNickname = e.Nickname + }, cancellation); + + ClusterCharacterInfo messageTarget = _sessionManager.GetOnlineCharacterByName(e.Nickname); + e.Sender.SendChatMessage(_languageService.GetLanguageFormat(GameDialogKey.INFORMATION_CHATMESSAGE_USER_WHISPER_SENT, e.Sender.UserLanguage, e.Nickname, messageTarget.ChannelId), + ChatMessageColorType.Red); + await e.Sender.EmitEventAsync(new ChatGenericEvent + { + Message = e.Message, + ChatType = ChatType.Whisper, + TargetCharacterId = messageTarget.Id + }); + return; + } + + if (session.CurrentMapInstance.HasMapFlag(MapFlags.ACT_4) && localSession.CurrentMapInstance.HasMapFlag(MapFlags.ACT_4)) + { + if (session.PlayerEntity.Faction != localSession.PlayerEntity.Faction && !session.IsGameMaster() && !localSession.IsGameMaster()) + { + session.SendChatMessage(session.GetLanguage(GameDialogKey.FRIEND_TALKMESSAGE_DIFFRENT_FACTION), ChatMessageColorType.Red); + return; + } + } + + e.Sender.SendSpeak(e.Message, SpeakType.Player); + await localSession.EmitEventAsync(new InterChannelReceiveWhisperEvent(e.Sender.PlayerEntity.Id, e.Sender.PlayerEntity.Name, -1, e.Sender.PlayerEntity.Authority, + e.Message)); + await e.Sender.EmitEventAsync(new ChatGenericEvent + { + Message = e.Message, + ChatType = ChatType.Whisper, + TargetCharacterId = localSession.PlayerEntity.Id + }); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/InterChannelModuleExtensions.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/InterChannelModuleExtensions.cs new file mode 100644 index 0000000..fffef44 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/InterChannel/InterChannelModuleExtensions.cs @@ -0,0 +1,36 @@ +using Microsoft.Extensions.DependencyInjection; +using PhoenixLib.ServiceBus.Extensions; +using WingsEmu.Plugins.BasicImplementations.InterChannel.Consumer; +using WingsEmu.Plugins.DistributedGameEvents.InterChannel; + +namespace WingsEmu.Plugins.BasicImplementations.InterChannel; + +public static class InterChannelModuleExtensions +{ + public static void AddInterChannelModule(this IServiceCollection services) + { + services.AddMessagePublisher(); + services.AddMessageSubscriber(); + + services.AddMessagePublisher(); + services.AddMessageSubscriber(); + + services.AddMessagePublisher(); + services.AddMessageSubscriber(); + + services.AddMessagePublisher(); + services.AddMessageSubscriber(); + + services.AddMessagePublisher(); + services.AddMessageSubscriber(); + + services.AddMessagePublisher(); + services.AddMessageSubscriber(); + + services.AddMessagePublisher(); + services.AddMessageSubscriber(); + + services.AddMessagePublisher(); + services.AddMessageSubscriber(); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Inventory/InventoryAddItemEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Inventory/InventoryAddItemEventHandler.cs new file mode 100644 index 0000000..c11d73e --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Inventory/InventoryAddItemEventHandler.cs @@ -0,0 +1,103 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.ItemExtension.Item; +using WingsEmu.DTOs.Mails; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Inventory.Event; +using WingsEmu.Game.Items; +using WingsEmu.Game.Mails.Events; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.Inventory; + +public class InventoryAddItemEventHandler : IAsyncEventProcessor +{ + private readonly IGameLanguageService _gameLanguage; + + public InventoryAddItemEventHandler(IGameLanguageService gameLanguage) => _gameLanguage = gameLanguage; + + public async Task HandleAsync(InventoryAddItemEvent e, CancellationToken cancellation) + { + InventoryItem item = e.InventoryItem; + GameItemInstance newItem = e.InventoryItem.ItemInstance; + IClientSession session = e.Sender; + + if (newItem == null) + { + return; + } + + if (newItem.ItemVNum == (short)ItemVnums.GOLD) + { + return; + } + + if (!session.PlayerEntity.HasSpaceFor(newItem.ItemVNum, (short)newItem.Amount)) + { + if (e.SendAsGiftIfFull) + { + await session.EmitEventAsync(new MailCreateEvent(session.PlayerEntity.Name, session.PlayerEntity.Id, MailGiftType.Normal, newItem)); + return; + } + + switch (e.MessageErrorType) + { + case MessageErrorType.Chat: + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.INTERACTION_MESSAGE_NOT_ENOUGH_PLACE, session.UserLanguage), ChatMessageColorType.Yellow); + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.INTERACTION_MESSAGE_NOT_ENOUGH_PLACE, session.UserLanguage), MsgMessageType.Middle); + break; + case MessageErrorType.Shop: + session.SendSMemo(SmemoType.Error, _gameLanguage.GetLanguage(GameDialogKey.INTERACTION_MESSAGE_NOT_ENOUGH_PLACE, session.UserLanguage)); + break; + } + + return; + } + + if (newItem.SerialTracker != null) + { + IEnumerable currentItems = session.PlayerEntity.GetAllPlayerInventoryItems(); + if (currentItems.Any(s => s?.ItemInstance.SerialTracker != null && s.ItemInstance.SerialTracker == newItem.SerialTracker)) + { + await session.NotifyStrangeBehavior(StrangeBehaviorSeverity.DANGER, + $"[DUPLICATED_ITEM] player: {session.PlayerEntity.Name} | masterAccountId: {session.Account.MasterAccountId} | hwid: {session.HardwareId} | has duplicated item => vnum: {newItem.ItemVNum} | serialTracker: {newItem.SerialTracker}"); + } + } + + // check, if player has the same item + InventoryItem secondItem = session.PlayerEntity.FindItemWithoutFullStack(newItem.ItemVNum, (short)newItem.Amount); + if (secondItem != null && !secondItem.ItemInstance.GameItem.IsNotStackableInventoryType() && !e.IsByMovePacket) + { + secondItem.ItemInstance.Amount += newItem.Amount; + session.SendInventoryAddPacket(secondItem); + } + else + { + if (item.InventoryType == InventoryType.Equipment && item.ItemInstance.Amount != 1) + { + item.ItemInstance.Amount = 1; + } + + session.PlayerEntity.AddItemToInventory(item); + session.SendInventoryAddPacket(item); + } + + if (!e.ShowMessage) + { + return; + } + + string itemName = newItem.GameItem.GetItemName(_gameLanguage, session.UserLanguage); + ChatMessageColorType messageType = e.ItemMessageType; + session.SendChatMessage(_gameLanguage.GetLanguageFormat(GameDialogKey.INVENTORY_CHATMESSAGE_X_ITEM_ACQUIRED, session.UserLanguage, newItem.Amount, itemName), messageType); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Inventory/InventoryDropItemEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Inventory/InventoryDropItemEventHandler.cs new file mode 100644 index 0000000..c6b3d62 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Inventory/InventoryDropItemEventHandler.cs @@ -0,0 +1,111 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsAPI.Game.Extensions.PacketGeneration; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Inventory.Event; +using WingsEmu.Game.Items; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.Inventory; + +public class InventoryDropItemEventHandler : IAsyncEventProcessor +{ + private readonly IGameLanguageService _gameLanguage; + + public InventoryDropItemEventHandler(IGameLanguageService gameLanguage) => _gameLanguage = gameLanguage; + + public async Task HandleAsync(InventoryDropItemEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + InventoryType inventoryType = e.InventoryType; + short slot = e.Slot; + short amount = e.Amount; + + if (session.PlayerEntity.LastPutItem > DateTime.UtcNow) + { + return; + } + + if (session.PlayerEntity.HasShopOpened) + { + return; + } + + if (session.PlayerEntity.IsSeal) + { + return; + } + + if (inventoryType != InventoryType.Equipment && inventoryType != InventoryType.Main && inventoryType != InventoryType.Etc) + { + return; + } + + if (session.IsActionForbidden()) + { + return; + } + + InventoryItem invItem = session.PlayerEntity.GetItemBySlotAndType(slot, inventoryType); + if (invItem == null) + { + return; + } + + if (session.PlayerEntity.IsInExchange()) + { + return; + } + + if (invItem.ItemInstance.Amount < amount) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.DROP_SHOUTMESSAGE_BAD_DROP_AMOUNT, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (!invItem.ItemInstance.GameItem.IsDroppable) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.ITEM_SHOUTMESSAGE_NOT_DROPPABLE, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (amount <= 0) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.DROP_SHOUTMESSAGE_BAD_DROP_AMOUNT, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (amount > 999) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.DROP_SHOUTMESSAGE_BAD_DROP_AMOUNT, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (session.CurrentMapInstance.Drops.Count > 200) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.DROP_SHOUTMESSAGE_FULL, session.UserLanguage), MsgMessageType.Middle); + return; + } + + GameItemInstance item = invItem.ItemInstance; + + MapItem droppedItem = session.CurrentMapInstance.PutItem((ushort)amount, ref item, session); + if (droppedItem == null) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.ITEM_SHOUTMESSAGE_NOT_DROPPABLE_HERE, session.UserLanguage), MsgMessageType.Middle); + return; + } + + await session.RemoveItemFromInventory(amount: amount, item: invItem); + droppedItem.BroadcastDrop(); + session.PlayerEntity.LastPutItem = DateTime.UtcNow + TimeSpan.FromMilliseconds(100); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Inventory/InventoryEquipItemEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Inventory/InventoryEquipItemEventHandler.cs new file mode 100644 index 0000000..b8ca257 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Inventory/InventoryEquipItemEventHandler.cs @@ -0,0 +1,232 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.DTOs.Items; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Algorithm; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Inventory.Event; +using WingsEmu.Game.Items; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.Inventory; + +public class InventoryEquipItemEventHandler : IAsyncEventProcessor +{ + private readonly HashSet _bindItems = new() + { + EquipmentType.CostumeHat, + EquipmentType.CostumeSuit, + EquipmentType.WeaponSkin + }; + + private readonly ICharacterAlgorithm _characterAlgorithm; + + private readonly IGameLanguageService _gameLanguage; + + public InventoryEquipItemEventHandler(IGameLanguageService gameLanguage, ICharacterAlgorithm characterAlgorithm) + { + _gameLanguage = gameLanguage; + _characterAlgorithm = characterAlgorithm; + } + + public async Task HandleAsync(InventoryEquipItemEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + bool isSpecialType = e.IsSpecialType; + InventoryType? inventoryType = e.InventoryType; + bool bindItem = e.BoundItem; + + if (session.PlayerEntity.IsInExchange() || !session.HasCurrentMapInstance) + { + return; + } + + if (session.PlayerEntity.HasShopOpened || session.PlayerEntity.ShopComponent.Items != null) + { + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.ITEM_MESSAGE_CANT_USE, session.UserLanguage), ChatMessageColorType.Yellow); + return; + } + + InventoryItem inv; + if (isSpecialType && inventoryType.HasValue) + { + inv = session.PlayerEntity.GetItemBySlotAndType(e.Slot, inventoryType.Value); + } + else + { + inv = session.PlayerEntity.GetItemBySlotAndType(e.Slot, InventoryType.Equipment); + } + + if (inv == null) + { + return; + } + + if (inv.ItemInstance.Type != ItemInstanceType.WearableInstance && inv.ItemInstance.Type != ItemInstanceType.SpecialistInstance) + { + return; + } + + GameItemInstance item = inv.ItemInstance; + EquipmentType equipmentType = item.GameItem.EquipmentSlot; + ItemType itemType = item.GameItem.ItemType; + + if (equipmentType == EquipmentType.Sp) + { + if (item.GameItem.IsPartnerSpecialist) + { + return; + } + + if (session.PlayerEntity.UseSp) + { + return; + } + + if (item.Rarity == -2) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.ITEM_SHOUTMESSAGE_CANT_WEAR_SP_DESTROYED, session.UserLanguage), MsgMessageType.Middle); + return; + } + } + + if (itemType != ItemType.Weapon && itemType != ItemType.Armor && itemType != ItemType.Fashion && itemType != ItemType.Jewelry && itemType != ItemType.Specialist) + { + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.ITEM_MESSAGE_CANT_WEAR, session.UserLanguage), ChatMessageColorType.Yellow); + return; + } + + if (item.GameItem.LevelMinimum > (item.GameItem.IsHeroic ? session.PlayerEntity.HeroLevel : session.PlayerEntity.Level) || + item.GameItem.Sex != 0 && item.GameItem.Sex != ((byte)session.PlayerEntity.Gender + 1)) + { + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.ITEM_MESSAGE_CANT_WEAR, session.UserLanguage), ChatMessageColorType.Yellow); + return; + } + + if (itemType != ItemType.Jewelry && equipmentType != EquipmentType.Boots && equipmentType != EquipmentType.Gloves && (item.GameItem.Class >> (byte)session.PlayerEntity.Class & 1) != 1) + { + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.ITEM_MESSAGE_CANT_WEAR, session.UserLanguage), ChatMessageColorType.Yellow); + return; + } + + GameItemInstance specialist = session.PlayerEntity.Specialist; + if (session.PlayerEntity.UseSp && specialist != null) + { + if (specialist.GameItem.Element != 0 && equipmentType == EquipmentType.Fairy && item.GameItem.Element != specialist.GameItem.Element && item.GameItem.Element != (byte)ElementType.All) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.ITEM_SHOUTMESSAGE_FAIRY_WRONG_ELEMENT, session.UserLanguage), MsgMessageType.Middle); + return; + } + } + + if (itemType == ItemType.Weapon || itemType == ItemType.Armor) + { + if (item.BoundCharacterId.HasValue && item.BoundCharacterId.Value != session.PlayerEntity.Id) + { + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.ITEM_MESSAGE_CANT_WEAR, session.UserLanguage), ChatMessageColorType.Yellow); + return; + } + } + + if (session.PlayerEntity.UseSp && equipmentType == EquipmentType.Sp) + { + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.SPECIALIST_CHATMESSAGE_SP_BLOCKED, session.UserLanguage), ChatMessageColorType.Yellow); + return; + } + + if (session.PlayerEntity.JobLevel < item.GameItem.LevelJobMinimum) + { + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.INFORMATION_MESSAGE_LOW_JOB, session.UserLanguage), ChatMessageColorType.Yellow); + return; + } + + if (item.IsBound && bindItem) + { + return; + } + + if (!item.IsBound && (item.GameItem.ItemValidTime != 0 && (_bindItems.Contains(item.GameItem.EquipmentSlot) || item.GameItem.ItemType == ItemType.Jewelry) + || equipmentType == EquipmentType.Fairy && (item.GameItem.MaxElementRate == 70 || item.GameItem.MaxElementRate == 80))) + { + if (!bindItem) + { + session.SendQnaPacket($"wear {inv.Slot} 0 1", _gameLanguage.GetLanguage(GameDialogKey.ITEM_DIALOG_ASK_BIND, session.UserLanguage)); + return; + } + + item.BoundCharacterId = session.PlayerEntity.Id; + + if (item.GameItem.ItemValidTime == -1) + { + item.ItemDeleteTime = null; + } + else if (item.GameItem.ItemValidTime > 0) + { + item.ItemDeleteTime = DateTime.UtcNow.AddSeconds(item.GameItem.ItemValidTime); + } + } + + bool buffAmulet = false; + if ((item.ItemDeleteTime.HasValue || item.DurabilityPoint != 0) && !_bindItems.Contains(item.GameItem.EquipmentSlot)) + { + session.SendAmuletBuffPacket(item); + buffAmulet = true; + } + + bool removeAmuletBuff = false; + InventoryItem itemInEquipment = session.PlayerEntity.GetInventoryItemFromEquipmentSlot(equipmentType); + if (itemInEquipment == null) + { + session.SendInventoryRemovePacket(inv); + session.PlayerEntity.EquipItem(inv, equipmentType); + } + else + { + if ((itemInEquipment.ItemInstance.ItemDeleteTime.HasValue || itemInEquipment.ItemInstance.DurabilityPoint != 0) + && !_bindItems.Contains(itemInEquipment.ItemInstance.GameItem.EquipmentSlot)) + { + removeAmuletBuff = true; + } + + session.PlayerEntity.TakeOffItem(equipmentType, inv.Slot, isSpecialType && inventoryType.HasValue ? inventoryType.Value : InventoryType.Equipment); + session.PlayerEntity.EquipItem(inv, equipmentType); + session.PlayerEntity.RefreshEquipmentValues(itemInEquipment.ItemInstance, true); + } + + if (removeAmuletBuff && !buffAmulet) + { + session.SendEmptyAmuletBuffPacket(); + } + + session.PlayerEntity.RefreshEquipmentValues(item); + session.RefreshStatChar(); + session.RefreshEquipment(); + if (itemInEquipment != null) + { + session.SendInventoryAddPacket(itemInEquipment); + } + + session.BroadcastEq(); + session.SendCondPacket(); + session.RefreshStat(); + session.SendIncreaseRange(); + + switch (equipmentType) + { + case EquipmentType.Fairy: + session.BroadcastPairy(); + break; + case EquipmentType.Amulet: + session.BroadcastEffectInRange(EffectType.EquipAmulet); + break; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Inventory/InventoryMoveItemEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Inventory/InventoryMoveItemEventHandler.cs new file mode 100644 index 0000000..9990c59 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Inventory/InventoryMoveItemEventHandler.cs @@ -0,0 +1,146 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Inventory.Event; +using WingsEmu.Game.Items; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.Inventory; + +public class InventoryMoveItemEventHandler : IAsyncEventProcessor +{ + private readonly IGameItemInstanceFactory _gameItemInstance; + + public InventoryMoveItemEventHandler(IGameItemInstanceFactory gameItemInstance) => _gameItemInstance = gameItemInstance; + + public async Task HandleAsync(InventoryMoveItemEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + InventoryType inventoryType = e.InventoryType; + short slot = e.Slot; + short amount = e.Amount; + bool sendPackets = e.SendPackets; + + InventoryType destinationType = e.DestinationType; + short destinationSlot = e.DestinationSlot; + + InventoryItem item = session.PlayerEntity.GetItemBySlotAndType(slot, inventoryType); + if (item == null) + { + return; + } + + if (amount > item.ItemInstance.Amount) + { + return; + } + + InventoryItem anotherItem = session.PlayerEntity.GetItemBySlotAndType(destinationSlot, destinationType); + if (anotherItem == null) + { + GameItemInstance itemCopy = _gameItemInstance.DuplicateItem(item.ItemInstance); + if (item.ItemInstance.Amount != amount) + { + if (!session.PlayerEntity.HasSpaceFor(item.ItemInstance.ItemVNum, amount)) + { + return; + } + + await session.RemoveItemFromInventory(amount: amount, item: item); + GameItemInstance sameItem = itemCopy; + sameItem.Amount = amount; + await session.AddNewItemToInventory(sameItem, slot: destinationSlot, isByMovePacket: true); + return; + } + + if (item.InventoryType == InventoryType.EquippedItems && item.IsEquipped) + { + return; + } + + if (inventoryType != destinationType) + { + if (!session.PlayerEntity.HaveSlotInSpecialInventoryType(destinationType)) + { + return; + } + + await session.RemoveItemFromInventory(amount: amount, item: item); + await session.AddNewItemToInventory(itemCopy, slot: destinationSlot, type: destinationType); + return; + } + + if (sendPackets) + { + session.SendInventoryRemovePacket(item); + } + + item.Slot = destinationSlot; + + if (sendPackets) + { + session.SendInventoryAddPacket(item); + } + + return; + } + + if (item.ItemInstance.ItemVNum == anotherItem.ItemInstance.ItemVNum && !item.ItemInstance.GameItem.IsNotStackableInventoryType()) + { + int itemAmount = amount; + int anotherItemAmount = anotherItem.ItemInstance.Amount; + if (itemAmount + anotherItemAmount > 999) // configure if we wanna increase amount stack + { + itemAmount = (short)(999 - anotherItemAmount); + if (itemAmount <= 0) + { + return; + } + } + + anotherItem.ItemInstance.Amount += itemAmount; + await session.RemoveItemFromInventory(amount: (short)itemAmount, item: item); + + if (sendPackets) + { + session.SendInventoryAddPacket(anotherItem); + } + + return; + } + + if (item.InventoryType != anotherItem.InventoryType) + { + if (sendPackets) + { + session.SendInventoryRemovePacket(item); + } + + item.Slot = destinationSlot; + + if (sendPackets) + { + session.SendInventoryAddPacket(item); + } + + return; + } + + item.Slot = destinationSlot; + if (sendPackets) + { + session.SendInventoryAddPacket(item); + } + + anotherItem.Slot = slot; + + if (sendPackets) + { + session.SendInventoryAddPacket(anotherItem); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Inventory/InventoryPickUpItemEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Inventory/InventoryPickUpItemEventHandler.cs new file mode 100644 index 0000000..099f85b --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Inventory/InventoryPickUpItemEventHandler.cs @@ -0,0 +1,554 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsAPI.Game.Extensions.ItemExtension.Item; +using WingsAPI.Packets.Enums.Shells; +using WingsEmu.DTOs.BCards; +using WingsEmu.DTOs.Bonus; +using WingsEmu.DTOs.Items; +using WingsEmu.Game; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Extensions.Mates; +using WingsEmu.Game.Groups; +using WingsEmu.Game.Helpers; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Inventory.Event; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Quests; +using WingsEmu.Game.Quests.Event; +using WingsEmu.Game.Raids; +using WingsEmu.Game.TimeSpaces.Events; +using WingsEmu.Game.Warehouse.Events; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.Inventory; + +public class InventoryPickUpItemEventHandler : IAsyncEventProcessor +{ + private readonly IBCardEffectHandlerContainer _bCardEffectHandler; + private readonly IChestDropItemConfig _chestDropItemConfig; + private readonly IDelayManager _delayManager; + private readonly IGameItemInstanceFactory _gameItemInstanceFactory; + private readonly IGameLanguageService _gameLanguage; + private readonly IItemsManager _itemsManager; + private readonly IRandomGenerator _randomGenerator; + private readonly IDropRarityConfigurationProvider _rarityConfigurationProvider; + private readonly IServerManager _serverManager; + + public InventoryPickUpItemEventHandler(IServerManager serverManager, IGameLanguageService gameLanguage, IItemsManager itemsManager, + IGameItemInstanceFactory gameItemInstanceFactory, IDelayManager delayManager, IDropRarityConfigurationProvider rarityConfigurationProvider, + IRandomGenerator randomGenerator, IChestDropItemConfig chestDropItemConfig, + IBCardEffectHandlerContainer bCardEffectHandler) + { + _serverManager = serverManager; + _gameLanguage = gameLanguage; + _itemsManager = itemsManager; + _gameItemInstanceFactory = gameItemInstanceFactory; + _delayManager = delayManager; + _rarityConfigurationProvider = rarityConfigurationProvider; + _randomGenerator = randomGenerator; + _chestDropItemConfig = chestDropItemConfig; + _bCardEffectHandler = bCardEffectHandler; + } + + public async Task HandleAsync(InventoryPickUpItemEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + if (!session.HasCurrentMapInstance) + { + return; + } + + if (session.PlayerEntity.IsOnVehicle) + { + return; + } + + if (session.PlayerEntity.IsSeal) + { + return; + } + + if (session.PlayerEntity.IsInExchange()) + { + return; + } + + MapItem mapItem = session.CurrentMapInstance.GetDrop(e.DropId); + + if (mapItem == null) + { + return; + } + + if (mapItem.Amount <= 0) + { + return; + } + + bool canPickUpItem = false; + var itemPosition = new Position(mapItem.PositionX, mapItem.PositionY); + IMateEntity mateEntity = null; + switch (e.PickerVisualType) + { + case VisualType.Player: + canPickUpItem = session.PlayerEntity.Position.IsInRange(itemPosition, 5); + break; + case VisualType.Npc: + mateEntity = session.PlayerEntity.MateComponent.GetMate(s => s.Id == e.PickerId); + if (mateEntity == null) + { + return; + } + + canPickUpItem = mateEntity.MateType switch + { + MateType.Partner => session.PlayerEntity.HaveStaticBonus(StaticBonusType.PartnerBackpack), + MateType.Pet => mateEntity.CanPickUp, + _ => false + }; + + break; + } + + if (!canPickUpItem) + { + return; + } + + if (mapItem.ItemVNum == (short)ItemVnums.GOLD) + { + await HandleGoldDrop(e.PickerVisualType, e.PickerId, session, mapItem); + return; + } + + switch (mapItem) + { + case ButtonMapItem button: + if (button.CanBeMovedOnlyOnce.HasValue && button.CanBeMovedOnlyOnce.Value) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.RAID_SHOUTMESSAGE_BUTTON_ALREADY_USED, session.UserLanguage), MsgMessageType.Middle); + return; + } + + DateTime dateOfEnd = await _delayManager.RegisterAction(session.PlayerEntity, DelayedActionType.ButtonSwitch, + button.CustomDanceDuration.HasValue ? TimeSpan.FromMilliseconds(button.CustomDanceDuration.Value) : default); + session.SendDelay((int)(dateOfEnd - DateTime.UtcNow).TotalMilliseconds, GuriType.ButtonSwitch, $"git {button.TransportId.ToString()}"); + return; + case TimeSpaceMapItem timeSpaceMapItem: + + if (!timeSpaceMapItem.DancingTime.HasValue) + { + await session.EmitEventAsync(new TimeSpacePickUpItemEvent + { + TimeSpaceMapItem = timeSpaceMapItem, + MateEntity = mateEntity + }); + return; + } + + dateOfEnd = await _delayManager.RegisterAction(session.PlayerEntity, DelayedActionType.ButtonSwitch, TimeSpan.FromMilliseconds(timeSpaceMapItem.DancingTime.Value)); + session.SendDelay((int)(dateOfEnd - DateTime.UtcNow).TotalMilliseconds, GuriType.OpeningBox, $"git {timeSpaceMapItem.TransportId.ToString()}"); + return; + } + + if (mateEntity is { MateType: MateType.Partner }) + { + if (!session.PlayerEntity.HasSpaceForPartnerItemWarehouse(mapItem.ItemVNum, (short)mapItem.Amount) && mapItem is not MonsterMapItem { IsQuest: true }) + { + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.PARTNER_WAREHOUSE_MESSAGE_NO_ENOUGH_PLACE, session.UserLanguage), ChatMessageColorType.Yellow); + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.PARTNER_WAREHOUSE_MESSAGE_NO_ENOUGH_PLACE, session.UserLanguage), MsgMessageType.Middle); + return; + } + } + else + { + GameItemInstance instance = mapItem.GetItemInstance(); + bool isMap = instance.GameItem.ItemType == ItemType.Map; + if (!isMap && !session.PlayerEntity.HasSpaceFor(mapItem.ItemVNum, (short)mapItem.Amount) && mapItem is not MonsterMapItem { IsQuest: true }) + { + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.INTERACTION_MESSAGE_NOT_ENOUGH_PLACE, session.UserLanguage), ChatMessageColorType.Yellow); + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.INTERACTION_MESSAGE_NOT_ENOUGH_PLACE, session.UserLanguage), MsgMessageType.Middle); + return; + } + } + + GameItemInstance itemInstance = null; + if (mapItem is MonsterMapItem item) + { + if (item.OwnerId.HasValue && item.OwnerId.Value != -1) + { + PlayerGroup group = session.PlayerEntity.GetGroup(); + if (group == null) + { + if (item.CreatedDate?.AddSeconds(30) > DateTime.UtcNow && item.OwnerId != session.PlayerEntity.Id) + { + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.INFORMATION_CHATMESSAGE_NOT_YOUR_ITEM, session.UserLanguage), ChatMessageColorType.Yellow); + return; + } + } + else + { + switch (group.SharingMode) + { + case GroupSharingType.ByOrder: + if (item.CreatedDate?.AddSeconds(30) > DateTime.UtcNow && item.OwnerId != session.PlayerEntity.Id) + { + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.INFORMATION_CHATMESSAGE_NOT_YOUR_ITEM, session.UserLanguage), ChatMessageColorType.Yellow); + return; + } + + break; + case GroupSharingType.Everyone: + bool canPickUp = session.PlayerEntity.GetGroup().Members.Any(entity => entity.Id == item.OwnerId.Value); + + if (!canPickUp && item.CreatedDate?.AddSeconds(30) > DateTime.UtcNow) + { + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.INFORMATION_CHATMESSAGE_NOT_YOUR_ITEM, session.UserLanguage), ChatMessageColorType.Yellow); + return; + } + + break; + } + } + } + + itemInstance = item.GetItemInstance(); + if (itemInstance != null && itemInstance.GameItem.Type == InventoryType.Equipment && itemInstance.GameItem.ItemType is ItemType.Weapon or ItemType.Armor && + itemInstance.Type == ItemInstanceType.WearableInstance) + { + short rarity = _rarityConfigurationProvider.GetRandomRarity(itemInstance.GameItem.ItemType); + itemInstance.Rarity = rarity; + itemInstance.SetRarityPoint(_randomGenerator); + } + } + + itemInstance ??= mapItem.GetItemInstance(); + if (itemInstance?.GameItem.ItemType == ItemType.Map) + { + await HandleMapItemDrop(e.PickerVisualType, e.PickerId, session, mapItem, itemInstance); + await CheckCollectingQuests(session, mateEntity, mapItem, itemInstance); + return; + } + + if (!session.CurrentMapInstance.RemoveDrop(mapItem.TransportId)) + { + return; + } + + switch (e.PickerVisualType) + { + case VisualType.Player: + session.BroadcastGetPacket(mapItem.TransportId); + session.PlayerEntity.SendIconPacket(true, mapItem.ItemVNum); + break; + case VisualType.Npc: + mateEntity?.BroadcastMateGetPacket(mapItem.TransportId); + mateEntity?.Owner.Session.SendCondMate(mateEntity); + mateEntity?.SendIconPacket(true, mapItem.ItemVNum); + mateEntity?.Owner?.Session.SendPacket(mateEntity.GenerateEffectPacket(EffectType.PetPickUp)); + break; + } + + await CheckCollectingQuests(session, mateEntity, mapItem, itemInstance); + } + + private async Task CheckCollectingQuests(IClientSession session, IMateEntity mateEntity, MapItem mapItem, GameItemInstance mapItemInstance) + { + // Quest logic + if (mapItem is CharacterMapItem or MonsterMapItem { IsQuest: false } && mapItemInstance.GameItem.ItemType != ItemType.Map) + { + if (mateEntity is { MateType: MateType.Partner }) + { + await session.EmitEventAsync(new PartnerWarehouseAddItemEvent + { + ItemInstance = mapItemInstance + }); + + await session.EmitEventAsync(new InventoryPickedUpItemEvent + { + ItemVnum = mapItemInstance.ItemVNum, + Amount = mapItem.Amount, + Location = new Location(mapItem.MapInstance.MapId, mapItem.PositionX, mapItem.PositionY) + }); + + return; + } + + await session.AddNewItemToInventory(mapItemInstance, true); + if (mapItem is CharacterMapItem item) + { + await session.EmitEventAsync(new InventoryPickedUpPlayerItemEvent + { + ItemInstance = item.GetItemInstance(), + Amount = item.ItemVNum, + Location = new Location(mapItem.MapInstance.MapId, mapItem.PositionX, mapItem.PositionY) + }); + return; + } + + await session.EmitEventAsync(new InventoryPickedUpItemEvent + { + ItemVnum = mapItemInstance.ItemVNum, + Amount = mapItem.Amount, + Location = new Location(mapItem.MapInstance.MapId, mapItem.PositionX, mapItem.PositionY) + }); + return; + } + + IEnumerable characterQuests = session.PlayerEntity.GetCurrentQuests().Where(s => + s.Quest.QuestType is QuestType.DROP_CHANCE or QuestType.DROP_CHANCE_2 or QuestType.DROP_HARDCODED or QuestType.DROP_IN_TIMESPACE); + + bool wasQuestItem = characterQuests.Any(s => s.Quest.Objectives.Any(o => mapItem.ItemVNum == (s.Quest.QuestType == QuestType.DROP_HARDCODED ? o.Data0 : o.Data1))); + if (!wasQuestItem) + { + return; + } + + if (session.PlayerEntity.IsInGroup()) + { + foreach (IPlayerEntity member in session.PlayerEntity.GetGroup().Members) + { + await member.Session.EmitEventAsync(new QuestItemPickUpEvent + { + ItemVnum = mapItem.ItemVNum, + Amount = mapItem.Amount, + SendMessage = true + }); + } + } + else + { + await session.EmitEventAsync(new QuestItemPickUpEvent + { + ItemVnum = mapItem.ItemVNum, + Amount = mapItem.Amount, + SendMessage = true + }); + } + } + + private async Task HandleMapItemDrop(VisualType type, long pickerId, IClientSession session, MapItem mapItem, GameItemInstance mapItemInstance) + { + if (mapItemInstance == null) + { + session.CurrentMapInstance.RemoveDrop(mapItem.TransportId); + return; + } + + if (mapItemInstance.GameItem.Effect == 71) + { + session.PlayerEntity.SpPointsBasic += mapItemInstance.GameItem.EffectValue; + if (session.PlayerEntity.SpPointsBasic > _serverManager.MaxBasicSpPoints) + { + session.PlayerEntity.SpPointsBasic = _serverManager.MaxBasicSpPoints; + } + + session.SendMsg(_gameLanguage.GetLanguageFormat(GameDialogKey.SPECIALIST_SHOUTMESSAGE_POINTS_ADDED, session.UserLanguage, mapItem.GetItemInstance().GameItem.EffectValue), + MsgMessageType.Middle); + session.RefreshSpPoint(); + } + + switch ((ItemVnums)mapItemInstance.ItemVNum) + { + case ItemVnums.WILD_SOUND_FLOWER: + await session.EmitEventAsync(new AddSoundFlowerQuestEvent + { + SoundFlowerType = SoundFlowerType.WILD_SOUND_FLOWER + }); + break; + + case ItemVnums.FAKE_MIMIC_POTION: + + session.SendMsg(session.GetLanguage(GameDialogKey.ITEM_SHOUTMESSAGE_MIMIC_FAKE_POTION), MsgMessageType.Middle); + + foreach (BCardDTO bCard in mapItemInstance.GameItem.BCards) + { + _bCardEffectHandler.Execute(session.PlayerEntity, session.PlayerEntity, bCard); + } + + break; + + case ItemVnums.BONUS_POINTS: + + if (session.CurrentMapInstance.MapInstanceType != MapInstanceType.TimeSpaceInstance && !session.PlayerEntity.TimeSpaceComponent.IsInTimeSpaceParty) + { + break; + } + + await session.EmitEventAsync(new TimeSpaceIncreaseScoreEvent + { + AmountToIncrease = _randomGenerator.RandomNumber(1, 11), + TimeSpaceParty = session.PlayerEntity.TimeSpaceComponent.TimeSpace + }); + + break; + } + + if (mapItemInstance.GameItem.IsTimeSpaceChest()) + { + switch (mapItemInstance.GameItem.Data[0]) + { + case 4: + + ChestDropItemConfiguration config = _chestDropItemConfig.GetChestByDataId(mapItemInstance.GameItem.Data[2]); + + if (config?.PossibleItems == null) + { + break; + } + + if (_randomGenerator.RandomNumber() > config.ItemChance) + { + session.SendMsg(session.GetLanguage(GameDialogKey.ITEM_SHOUTMESSAGE_CHEST_EMPTY), MsgMessageType.Middle); + break; + } + + ChestDropItemDrop getRandomItem = config.PossibleItems[_randomGenerator.RandomNumber(config.PossibleItems.Count)]; + if (getRandomItem == null) + { + break; + } + + GameItemInstance item = _gameItemInstanceFactory.CreateItem(getRandomItem.ItemVnum, getRandomItem.Amount); + await session.AddNewItemToInventory(item, sendGiftIsFull: true); + + string itemName = item.GameItem.GetItemName(_gameLanguage, session.UserLanguage); + session.SendMsg(session.GetLanguageFormat(GameDialogKey.INVENTORY_CHATMESSAGE_X_ITEM_ACQUIRED, getRandomItem.Amount, itemName), MsgMessageType.Middle); + break; + } + } + + session.CurrentMapInstance.RemoveDrop(mapItem.TransportId); + switch (type) + { + case VisualType.Player: + session.BroadcastGetPacket(mapItem.TransportId); + break; + case VisualType.Npc: + IMateEntity mateEntity = session.PlayerEntity.MateComponent.GetMate(s => s.Id == pickerId); + mateEntity?.BroadcastMateGetPacket(mapItem.TransportId); + mateEntity?.Owner.Session.SendCondMate(mateEntity); + mateEntity?.Owner?.Session.SendPacket(mateEntity.GenerateEffectPacket(EffectType.PetPickUp)); + break; + } + } + + private async Task HandleGoldDrop(VisualType type, long pickerId, IClientSession session, MapItem mapItem) + { + if (mapItem is not MonsterMapItem droppedGold) + { + return; + } + + if (droppedGold.OwnerId.HasValue && droppedGold.OwnerId.Value != -1) + { + PlayerGroup group = session.PlayerEntity.GetGroup(); + if (group == null) + { + if (droppedGold.CreatedDate?.AddSeconds(30) > DateTime.UtcNow && droppedGold.OwnerId != session.PlayerEntity.Id) + { + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.INFORMATION_CHATMESSAGE_NOT_YOUR_ITEM, session.UserLanguage), ChatMessageColorType.Yellow); + return; + } + } + else + { + switch (group.SharingMode) + { + case GroupSharingType.ByOrder: + if (droppedGold.CreatedDate?.AddSeconds(30) > DateTime.UtcNow && droppedGold.OwnerId != session.PlayerEntity.Id) + { + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.INFORMATION_CHATMESSAGE_NOT_YOUR_ITEM, session.UserLanguage), ChatMessageColorType.Yellow); + return; + } + + break; + case GroupSharingType.Everyone: + bool canPickUp = session.PlayerEntity.GetGroup().Members.Any(entity => entity.Id == droppedGold.OwnerId.Value); + + if (!canPickUp && droppedGold.CreatedDate?.AddSeconds(30) > DateTime.UtcNow) + { + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.INFORMATION_CHATMESSAGE_NOT_YOUR_ITEM, session.UserLanguage), ChatMessageColorType.Yellow); + return; + } + + break; + } + } + } + + long maxGold = _serverManager.MaxGold; + + double multiplier = 1 + (session.PlayerEntity.BCardComponent.GetAllBCardsInformation(BCardType.Item, (byte)AdditionalTypes.Item.IncreaseEarnedGold, session.PlayerEntity.Level).firstData * 0.01 + + session.PlayerEntity.GetMaxWeaponShellValue(ShellEffectType.GainMoreGold, true) * 0.01); + int basicDrop = droppedGold.Amount; + int goldDropped = (int)(droppedGold.Amount * multiplier); + + if (session.PlayerEntity.Gold + goldDropped > maxGold) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.INFORMATION_MESSAGE_MAX_GOLD, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (!session.CurrentMapInstance.RemoveDrop(mapItem.TransportId)) + { + return; + } + + session.PlayerEntity.Gold += goldDropped; + if (session.PlayerEntity.Gold > maxGold) + { + session.PlayerEntity.Gold = maxGold; + } + + await session.EmitEventAsync(new InventoryPickedUpItemEvent + { + ItemVnum = mapItem.ItemVNum, + Amount = goldDropped, + Location = new Location(mapItem.MapInstance.MapId, mapItem.PositionX, mapItem.PositionY) + }); + + string itemName = _itemsManager.GetItem((short)ItemVnums.GOLD).GetItemName(_gameLanguage, session.UserLanguage); + + if (basicDrop != goldDropped) + { + session.SendChatMessage(_gameLanguage.GetLanguageFormat(GameDialogKey.INVENTORY_CHATMESSAGE_GOLD_BONUS_ACQUIRED, session.UserLanguage, + basicDrop, itemName, goldDropped - basicDrop), ChatMessageColorType.Green); + } + else + { + session.SendChatMessage(_gameLanguage.GetLanguageFormat(GameDialogKey.INVENTORY_CHATMESSAGE_X_ITEM_ACQUIRED, session.UserLanguage, goldDropped, itemName), ChatMessageColorType.Green); + } + + session.RefreshGold(); + + switch (type) + { + case VisualType.Player: + session.BroadcastGetPacket(mapItem.TransportId); + break; + case VisualType.Npc: + IMateEntity mateEntity = session.PlayerEntity.MateComponent.GetMate(s => s.Id == pickerId); + mateEntity?.BroadcastMateGetPacket(mapItem.TransportId); + mateEntity?.Owner.Session.SendCondMate(mateEntity); + mateEntity?.Owner?.Session.SendPacket(mateEntity.GenerateEffectPacket(EffectType.PetPickUp)); + break; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Inventory/InventoryRemoveItemEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Inventory/InventoryRemoveItemEventHandler.cs new file mode 100644 index 0000000..c1c9d3f --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Inventory/InventoryRemoveItemEventHandler.cs @@ -0,0 +1,154 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Inventory.Event; +using WingsEmu.Game.Items; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.Inventory; + +public class InventoryRemoveItemEventHandler : IAsyncEventProcessor +{ + public async Task HandleAsync(InventoryRemoveItemEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + int itemVnum = e.ItemVnum; + short amount = e.Amount; + InventoryItem inventoryItem = e.InventoryItem; + bool sendPackets = e.SendPackets; + + if (inventoryItem == null) // find item by vnum + { + if (!session.PlayerEntity.HasItem(itemVnum, amount)) + { + return; + } + + short toRemove = amount; + foreach (InventoryItem invItem in session.PlayerEntity.GetAllPlayerInventoryItems()) + { + if (toRemove <= 0) + { + break; + } + + GameItemInstance item = invItem?.ItemInstance; + if (item == null) + { + continue; + } + + if (item.GameItem.Id != itemVnum) + { + continue; + } + + if (item.Amount - toRemove <= 0) + { + toRemove = (short)(toRemove - item.Amount); + item.Amount = 0; + } + else + { + item.Amount -= toRemove; + toRemove -= amount; + } + + if (item.Amount > 0) + { + if (sendPackets) + { + session.SendInventoryAddPacket(invItem); + } + + continue; + } + + if (!session.PlayerEntity.RemoveItemFromSlotAndType(invItem.Slot, invItem.InventoryType, out InventoryItem removedItemAmount)) + { + continue; + } + + await session.EmitEventAsync(new InventoryItemDeletedEvent + { + ItemInstance = removedItemAmount.ItemInstance, + ItemAmount = toRemove + }); + + if (sendPackets) + { + session.SendInventoryRemovePacket(removedItemAmount); + } + } + + return; + } + + inventoryItem.ItemInstance.Amount -= amount; + if (inventoryItem.ItemInstance.Amount > 0) + { + if (sendPackets) + { + session.SendInventoryAddPacket(inventoryItem); + } + + return; + } + + bool shouldResetStats = false; + if (e.IsEquipped) + { + if (e.InventoryItem.ItemInstance.GameItem.EquipmentSlot == EquipmentType.Amulet) + { + await session.EmitEventAsync(new InventoryTakeOffItemEvent(e.InventoryItem.Slot) + { + ForceToRandomSlot = true + }); + shouldResetStats = true; + } + + if (e.InventoryItem.ItemInstance.GameItem.EquipmentSlot != EquipmentType.Amulet + && (e.InventoryItem.ItemInstance.ItemDeleteTime == null || e.InventoryItem.ItemInstance.ItemDeleteTime < DateTime.UtcNow)) + { + await session.EmitEventAsync(new InventoryTakeOffItemEvent(e.InventoryItem.Slot) + { + ForceToRandomSlot = true + }); + shouldResetStats = true; + } + } + + if (!session.PlayerEntity.RemoveItemFromSlotAndType(inventoryItem.Slot, inventoryItem.InventoryType, out InventoryItem itemBySlot)) + { + return; + } + + await session.EmitEventAsync(new InventoryItemDeletedEvent + { + ItemInstance = itemBySlot.ItemInstance, + ItemAmount = amount + }); + + if (sendPackets) + { + session.SendInventoryRemovePacket(itemBySlot); + } + + if (!e.IsEquipped && !shouldResetStats) + { + return; + } + + if (!sendPackets) + { + return; + } + + session.SendEmptyAmuletBuffPacket(); + session.RefreshStatChar(); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Inventory/InventorySortItemEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Inventory/InventorySortItemEventHandler.cs new file mode 100644 index 0000000..93abb88 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Inventory/InventorySortItemEventHandler.cs @@ -0,0 +1,60 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Inventory.Event; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.Inventory; + +public class InventorySortItemEventHandler : IAsyncEventProcessor +{ + public async Task HandleAsync(InventorySortItemEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + InventoryType inventoryType = e.InventoryType; + bool confirm = e.Confirm; + + if (inventoryType != InventoryType.Etc && inventoryType != InventoryType.Main && inventoryType != InventoryType.Equipment) + { + return; + } + + DateTime now = DateTime.UtcNow; + if (session.PlayerEntity.LastInventorySort.AddMinutes(1) > now) + { + return; + } + + if (!confirm) + { + session.SendQnaPacket($"isort {(byte)inventoryType} 1", session.GetLanguage(GameDialogKey.INVENTORY_DIALOG_ASK_SORT)); + return; + } + + session.PlayerEntity.LastInventorySort = now; + InventoryItem[] items = session.PlayerEntity.GetItemsByInventoryType(inventoryType).OrderBy(x => x?.ItemInstance.ItemVNum).ToArray(); + for (short i = 0; i < items.Length; i++) + { + InventoryItem item = items.ElementAt(i); + if (item == null) + { + continue; + } + + if (item.Slot != i) + { + session.SendInventoryRemovePacket(item); + } + + item.Slot = i; + } + + session.SendStartStartupInventory(); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Inventory/InventoryTakeOffItemEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Inventory/InventoryTakeOffItemEventHandler.cs new file mode 100644 index 0000000..713859d --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Inventory/InventoryTakeOffItemEventHandler.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.ItemExtension.Item; +using WingsEmu.DTOs.Items; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Inventory.Event; +using WingsEmu.Game.Items; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.Inventory; + +public class InventoryTakeOffItemEventHandler : IAsyncEventProcessor +{ + private readonly IGameLanguageService _gameLanguage; + + private readonly HashSet _shells = new() + { + EquipmentType.Armor, + EquipmentType.MainWeapon, + EquipmentType.SecondaryWeapon + }; + + public InventoryTakeOffItemEventHandler(IGameLanguageService gameLanguage) => _gameLanguage = gameLanguage; + + public async Task HandleAsync(InventoryTakeOffItemEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + if (session.PlayerEntity.IsInExchange() || !session.HasCurrentMapInstance) + { + return; + } + + if (session.PlayerEntity.HasShopOpened || session.PlayerEntity.ShopComponent.Items != null) + { + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.ITEM_MESSAGE_CANT_USE, session.UserLanguage), ChatMessageColorType.Yellow); + return; + } + + if (!Enum.TryParse(e.Slot.ToString(), out EquipmentType equipmentType)) + { + return; + } + + InventoryItem inv = session.PlayerEntity.GetInventoryItemFromEquipmentSlot(equipmentType); + if (inv == null) + { + return; + } + + if (inv.ItemInstance.Type != ItemInstanceType.WearableInstance && inv.ItemInstance.Type != ItemInstanceType.SpecialistInstance) + { + return; + } + + GameItemInstance item = inv.ItemInstance; + + switch (e.Slot) + { + case (byte)EquipmentType.Sp when session.PlayerEntity.UseSp: + if (session.PlayerEntity.LastSkillUse.AddSeconds(2) > DateTime.UtcNow) + { + return; + } + + if (session.PlayerEntity.IsSitting) + { + await session.RestAsync(); + } + + if (session.PlayerEntity.BuffComponent.HasBuff(BuffGroup.Bad)) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.SPECIALIST_SHOUTMESSAGE_NO_REMOVE_DEBUFFS, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (session.PlayerEntity.IsOnVehicle) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_REMOVE_VEHICLE, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (session.PlayerEntity.IsMorphed) + { + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.ITEM_MESSAGE_CANT_USE, session.UserLanguage), ChatMessageColorType.Yellow); + return; + } + + await session.EmitEventAsync(new SpUntransformEvent()); + break; + } + + if (!session.PlayerEntity.HasSpaceFor(inv.ItemInstance.ItemVNum) && !e.ForceToRandomSlot) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.INTERACTION_MESSAGE_NOT_ENOUGH_PLACE, session.UserLanguage), MsgMessageType.Middle); + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.INTERACTION_MESSAGE_NOT_ENOUGH_PLACE, session.UserLanguage), ChatMessageColorType.Yellow); + return; + } + + if ((item.ItemDeleteTime.HasValue || item.DurabilityPoint != 0) && session.ShouldSendAmuletPacket(item.GameItem.EquipmentSlot)) + { + session.SendEmptyAmuletBuffPacket(); + } + + session.PlayerEntity.RefreshEquipmentValues(item, true); + session.PlayerEntity.TakeOffItem(equipmentType, e.ForceToRandomSlot ? 255 : null); + session.RefreshStatChar(); + session.RefreshEquipment(); + session.SendInventoryAddPacket(inv); + session.BroadcastEq(); + session.BroadcastPairy(); + session.SendCondPacket(); + session.RefreshStat(); + session.SendIncreaseRange(); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Inventory/InventoryUseItemEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Inventory/InventoryUseItemEventHandler.cs new file mode 100644 index 0000000..6ed9ea0 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Inventory/InventoryUseItemEventHandler.cs @@ -0,0 +1,19 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Event; + +namespace WingsEmu.Plugins.BasicImplementations.Inventory; + +public class InventoryUseItemEventHandler : IAsyncEventProcessor +{ + private readonly IItemHandlerContainer _itemHandler; + + public InventoryUseItemEventHandler(IItemHandlerContainer itemHandler) => _itemHandler = itemHandler; + + public async Task HandleAsync(InventoryUseItemEvent e, CancellationToken cancellation) + { + await Task.Run(() => _itemHandler.Handle(e.Sender, e), cancellation); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Inventory/ItemInstanceFactory.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Inventory/ItemInstanceFactory.cs new file mode 100644 index 0000000..d6635b3 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Inventory/ItemInstanceFactory.cs @@ -0,0 +1,178 @@ +using System; +using System.Collections.Generic; +using Mapster; +using WingsEmu.DTOs.Items; +using WingsEmu.Game; +using WingsEmu.Game._enum; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Skills; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.Inventory; + +public class GameItemInstanceFactory : IGameItemInstanceFactory +{ + private readonly IItemsManager _itemsManager; + private readonly IRandomGenerator _randomGenerator; + private readonly IDropRarityConfigurationProvider _rarityConfigurationProvider; + + public GameItemInstanceFactory(IItemsManager itemsManager, IRandomGenerator randomGenerator, IDropRarityConfigurationProvider rarityConfigurationProvider) + { + _itemsManager = itemsManager; + _randomGenerator = randomGenerator; + _rarityConfigurationProvider = rarityConfigurationProvider; + } + + public GameItemInstance CreateItem(ItemInstanceDTO dto) + { + GameItemInstance instance = dto.Adapt(); + if (instance.SerialTracker == null && _itemsManager.GetItem(instance.ItemVNum)?.IsNotStackableInventoryType() == true) + { + instance.SerialTracker = Guid.NewGuid(); + } + + return instance; + } + + public ItemInstanceDTO CreateDto(GameItemInstance instance) + { + ItemInstanceDTO dto = instance.Adapt(); + if (dto.SerialTracker == null && _itemsManager.GetItem(dto.ItemVNum)?.IsNotStackableInventoryType() == true) + { + dto.SerialTracker = Guid.NewGuid(); + } + + return dto; + } + + public GameItemInstance CreateItem(int itemVnum) => CreateItem(itemVnum, 1, 0, 0, 0); + public GameItemInstance CreateItem(int itemVnum, bool isMateLimited) => CreateItem(itemVnum, 1, 0, 0, 0, isMateLimited); + + public GameItemInstance CreateItem(int itemVnum, int amount) => CreateItem(itemVnum, amount, 0, 0, 0); + + public GameItemInstance CreateItem(int itemVnum, int amount, byte upgrade) => CreateItem(itemVnum, amount, upgrade, 0, 0); + public GameItemInstance CreateItem(int itemVnum, int amount, byte upgrade, sbyte rare) => CreateItem(itemVnum, amount, upgrade, rare, 0); + + public GameItemInstance CreateItem(int itemVnum, int amount, byte upgrade, sbyte rare, byte design, bool isMateLimited = false) + { + IGameItem newGameItem = _itemsManager.GetItem(itemVnum); + if (newGameItem == null) + { + return null; + } + + bool isNotStackable = newGameItem.IsNotStackableInventoryType(); + + if (amount > 999 && itemVnum != (int)ItemVnums.GOLD) + { + amount = 999; + } + + if (isNotStackable && amount != 1) + { + amount = 1; + } + + switch (newGameItem.ItemType) + { + case ItemType.Shell: + return new GameItemInstance + { + Type = ItemInstanceType.WearableInstance, + ItemVNum = itemVnum, + Amount = amount, + Upgrade = upgrade == 0 ? (byte)_randomGenerator.RandomNumber(newGameItem.ShellMinimumLevel, newGameItem.ShellMaximumLevel) : upgrade, + Rarity = rare == 0 ? _rarityConfigurationProvider.GetRandomRarity(ItemType.Shell) : rare, + Design = design, + DurabilityPoint = newGameItem.LeftUsages, + EquipmentOptions = new List() + }; + case ItemType.Weapon: + case ItemType.Armor: + case ItemType.Fashion: + case ItemType.Jewelry: + var item = new GameItemInstance + { + Type = ItemInstanceType.WearableInstance, + ItemVNum = itemVnum, + Amount = amount, + Upgrade = upgrade, + Rarity = rare, + Design = design, + DurabilityPoint = newGameItem.LeftUsages, + EquipmentOptions = new List() + }; + if (item.Rarity != 0) + { + item.SetRarityPoint(_randomGenerator); + } + + return item; + case ItemType.Specialist: + if (newGameItem.IsPartnerSpecialist) + { + return new GameItemInstance + { + Type = ItemInstanceType.SpecialistInstance, + ItemVNum = itemVnum, + Amount = amount, + Agility = 0, + PartnerSkills = new List() + }; + } + + return new GameItemInstance + { + Type = ItemInstanceType.SpecialistInstance, + ItemVNum = itemVnum, + Amount = amount, + SpLevel = 1, + Upgrade = upgrade + }; + case ItemType.Box: + byte level = newGameItem.ItemSubType switch + { + 0 => (byte)newGameItem.Data[2], + 1 => (byte)newGameItem.Data[2], + _ => 1 + }; + + return new GameItemInstance + { + Type = ItemInstanceType.BoxInstance, + ItemVNum = itemVnum, + HoldingVNum = newGameItem.Data[1], + Amount = amount, + Rarity = rare, + Design = design, + SpLevel = level, + IsLimitedMatePearl = isMateLimited + }; + } + + return new GameItemInstance(itemVnum, amount, upgrade, rare, design); + } + + public GameItemInstance CreateSpecialistCard(int itemVnum, byte spLevel = 1, byte upgrade = 0, byte design = 0) + { + IGameItem newGameItem = _itemsManager.GetItem(itemVnum); + if (newGameItem == null) + { + return null; + } + + return new GameItemInstance + { + Type = ItemInstanceType.SpecialistInstance, + ItemVNum = itemVnum, + Amount = 1, + SpLevel = spLevel, + Upgrade = upgrade, + Design = design + }; + } + + public GameItemInstance DuplicateItem(GameItemInstance gameInstance) => gameInstance.Adapt(); +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Inventory/PartnerInventoryEquipItemEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Inventory/PartnerInventoryEquipItemEventHandler.cs new file mode 100644 index 0000000..0064642 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Inventory/PartnerInventoryEquipItemEventHandler.cs @@ -0,0 +1,202 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.DTOs.Items; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Extensions.Mates; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Inventory.Event; +using WingsEmu.Game.Items; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.Inventory; + +public class PartnerInventoryEquipItemEventHandler : IAsyncEventProcessor +{ + private static readonly HashSet _equipmentTypes = new() + { + EquipmentType.Armor, EquipmentType.MainWeapon, EquipmentType.Sp, EquipmentType.Gloves, EquipmentType.Boots, EquipmentType.SecondaryWeapon + }; + + private static readonly int[] _partnerItemsWeapons = + { + (int)ItemVnums.PARTNER_WEAPON_MELEE, (int)ItemVnums.PARTNER_WEAPON_RANGED, (int)ItemVnums.PARTNER_WEAPON_MAGIC + }; + + private static readonly int[] _partnerItemsArmors = + { + (int)ItemVnums.PARTNER_ARMOR_MAGIC, (int)ItemVnums.PARTNER_ARMOR_RANGED, (int)ItemVnums.PARTNER_ARMOR_MELEE + }; + + private readonly IGameLanguageService _gameLanguage; + + public PartnerInventoryEquipItemEventHandler(IGameLanguageService gameLanguage) => _gameLanguage = gameLanguage; + + public async Task HandleAsync(PartnerInventoryEquipItemEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + + if (session.PlayerEntity.IsInExchange() || !session.HasCurrentMapInstance) + { + return; + } + + if (session.PlayerEntity.HasShopOpened || session.PlayerEntity.ShopComponent.Items != null) + { + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.ITEM_MESSAGE_CANT_USE, session.UserLanguage), ChatMessageColorType.Yellow); + return; + } + + short petId = (short)(e.PartnerSlot - 1); + byte slot = e.Slot; + + IMateEntity mateEntity = session.PlayerEntity.MateComponent.GetMate(x => x.PetSlot == petId && x.MateType == MateType.Partner); + if (mateEntity == null) + { + return; + } + + InventoryItem inv = session.PlayerEntity.GetItemBySlotAndType(slot, e.InventoryType); + if (inv == null) + { + return; + } + + short invSlot = inv.Slot; + + if (inv.ItemInstance.Type != ItemInstanceType.WearableInstance && inv.ItemInstance.Type != ItemInstanceType.SpecialistInstance) + { + return; + } + + GameItemInstance item = inv.ItemInstance; + + EquipmentType equipmentType = item.GameItem.EquipmentSlot; + if (!_equipmentTypes.Contains(equipmentType)) + { + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.ITEM_MESSAGE_CANT_WEAR_PARTNER, session.UserLanguage), ChatMessageColorType.Yellow); + return; + } + + switch (equipmentType) + { + case EquipmentType.MainWeapon: + if (!_partnerItemsWeapons.Contains(item.ItemVNum)) + { + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.ITEM_MESSAGE_CANT_WEAR_PARTNER, session.UserLanguage), ChatMessageColorType.Yellow); + return; + } + + break; + case EquipmentType.Armor: + if (!_partnerItemsArmors.Contains(item.ItemVNum)) + { + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.ITEM_MESSAGE_CANT_WEAR_PARTNER, session.UserLanguage), ChatMessageColorType.Yellow); + return; + } + + break; + } + + ItemType itemType = item.GameItem.ItemType; + + if (equipmentType == EquipmentType.Sp) + { + if (!item.GameItem.IsPartnerSpecialist) + { + return; + } + + if (mateEntity.IsUsingSp) + { + return; + } + + if (!mateEntity.CanWearSpecialist(inv.ItemInstance.GameItem)) + { + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.ITEM_MESSAGE_CANT_WEAR_PARTNER, session.UserLanguage), ChatMessageColorType.Yellow); + return; + } + + if (!mateEntity.IsUsingSp && !mateEntity.IsSpCooldownElapsed()) + { + session.SendMsg(_gameLanguage.GetLanguageFormat(GameDialogKey.SPECIALIST_SHOUTMESSAGE_IN_COOLDOWN, session.UserLanguage, mateEntity.GetSpCooldown()), MsgMessageType.Middle); + return; + } + } + + if (itemType != ItemType.Weapon && itemType != ItemType.Armor && itemType != ItemType.Specialist && itemType != ItemType.Fashion) + { + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.ITEM_MESSAGE_CANT_WEAR_PARTNER, session.UserLanguage), ChatMessageColorType.Yellow); + return; + } + + if (item.GameItem.IsHeroic) + { + if (mateEntity.Level < 80) + { + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.ITEM_MESSAGE_CANT_WEAR_PARTNER, session.UserLanguage), ChatMessageColorType.Yellow); + return; + } + + int itemLevel = item.GameItem.LevelMinimum + 80; + + if (itemLevel > mateEntity.Level) + { + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.ITEM_MESSAGE_CANT_WEAR_PARTNER, session.UserLanguage), ChatMessageColorType.Yellow); + return; + } + } + else + { + if (item.GameItem.LevelMinimum > mateEntity.Level) + { + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.ITEM_MESSAGE_CANT_WEAR_PARTNER, session.UserLanguage), ChatMessageColorType.Yellow); + return; + } + } + + if (!mateEntity.CanWearItem(item.GameItem)) + { + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.ITEM_MESSAGE_CANT_WEAR_PARTNER, session.UserLanguage), ChatMessageColorType.Yellow); + return; + } + + if (equipmentType == EquipmentType.SecondaryWeapon) + { + equipmentType = EquipmentType.MainWeapon; + } + + PartnerInventoryItem equippedItem = session.PlayerEntity.PartnerGetEquippedItem(equipmentType, mateEntity.PetSlot); + if (equippedItem != null) + { + GameItemInstance instance = equippedItem.ItemInstance; + + if (!session.PlayerEntity.HasSpaceFor(instance.ItemVNum, (short)instance.Amount)) + { + session.SendMsg(session.GetLanguage(GameDialogKey.INTERACTION_MESSAGE_NOT_ENOUGH_PLACE), MsgMessageType.Middle); + return; + } + + session.PlayerEntity.PartnerTakeOffItem(equipmentType, mateEntity.PetSlot); + await session.AddNewItemToInventory(instance, slot: invSlot, type: e.InventoryType); + mateEntity.RefreshEquipmentValues(instance, true); + } + + session.PlayerEntity.PartnerEquipItem(inv, petId); + session.SendScpPackets(); + session.SendScnPackets(); + await session.RemoveItemFromInventory(item: inv, sendPackets: equippedItem == null); + mateEntity.RefreshEquipmentValues(item, false); + session.SendPetInfo(mateEntity, _gameLanguage); + session.SendCondMate(mateEntity); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Inventory/PartnerInventoryTakeOffItemEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Inventory/PartnerInventoryTakeOffItemEventHandler.cs new file mode 100644 index 0000000..00832ee --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Inventory/PartnerInventoryTakeOffItemEventHandler.cs @@ -0,0 +1,120 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.DTOs.Items; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Extensions.Mates; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Inventory.Event; +using WingsEmu.Game.Items; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Mates.Events; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.Inventory; + +public class PartnerInventoryTakeOffItemEventHandler : IAsyncEventProcessor +{ + private readonly IGameLanguageService _gameLanguage; + + public PartnerInventoryTakeOffItemEventHandler(IGameLanguageService gameLanguage) => _gameLanguage = gameLanguage; + + public async Task HandleAsync(PartnerInventoryTakeOffItemEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + short petSlot = (short)(e.PetId - 1); + byte slot = e.Slot; + + if (session.PlayerEntity.IsInExchange() || !session.HasCurrentMapInstance) + { + return; + } + + if (session.PlayerEntity.HasShopOpened || session.PlayerEntity.ShopComponent.Items != null) + { + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.ITEM_MESSAGE_CANT_USE, session.UserLanguage), ChatMessageColorType.Yellow); + return; + } + + IMateEntity mateEntity = session.PlayerEntity.MateComponent.GetMate(x => x.PetSlot == petSlot && x.MateType == MateType.Partner); + if (mateEntity == null) + { + return; + } + + if (!Enum.TryParse(slot.ToString(), out EquipmentType equipmentType)) + { + return; + } + + PartnerInventoryItem inv = session.PlayerEntity.PartnerGetEquippedItem(equipmentType, petSlot); + if (inv == null) + { + return; + } + + if (inv.ItemInstance.Type != ItemInstanceType.WearableInstance && inv.ItemInstance.Type != ItemInstanceType.SpecialistInstance) + { + return; + } + + switch (e.Slot) + { + case (byte)EquipmentType.Sp when mateEntity.IsUsingSp: + if (mateEntity.LastSkillUse.AddSeconds(2) > DateTime.UtcNow) + { + return; + } + + if (mateEntity.IsSitting) + { + await session.EmitEventAsync(new MateRestEvent + { + MateEntity = mateEntity + }); + } + + if (mateEntity.BuffComponent.HasBuff(BuffGroup.Bad)) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.SPECIALIST_SHOUTMESSAGE_NO_REMOVE_DEBUFFS, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (session.PlayerEntity.IsOnVehicle) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_REMOVE_VEHICLE, session.UserLanguage), MsgMessageType.Middle); + return; + } + + await session.EmitEventAsync(new MateSpUntransformEvent + { + MateEntity = mateEntity + }); + + break; + case (byte)EquipmentType.Sp when !mateEntity.IsUsingSp && !mateEntity.IsSpCooldownElapsed(): + session.SendMsg(_gameLanguage.GetLanguageFormat(GameDialogKey.PARTNER_SHOUTMESSAGE_SP_IN_COOLDOWN, session.UserLanguage, mateEntity.GetSpCooldown()), MsgMessageType.Middle); + return; + } + + if (!session.PlayerEntity.HasSpaceFor(inv.ItemInstance.ItemVNum)) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.INTERACTION_MESSAGE_NOT_ENOUGH_PLACE, session.UserLanguage), MsgMessageType.Middle); + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.INTERACTION_MESSAGE_NOT_ENOUGH_PLACE, session.UserLanguage), ChatMessageColorType.Yellow); + return; + } + + GameItemInstance itemInstance = inv.ItemInstance; + await session.AddNewItemToInventory(itemInstance); + session.PlayerEntity.PartnerTakeOffItem(equipmentType, petSlot); + mateEntity.RefreshEquipmentValues(itemInstance, true); + session.SendPetInfo(mateEntity, _gameLanguage); + session.SendCondMate(mateEntity); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemHandlerContainer.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemHandlerContainer.cs new file mode 100644 index 0000000..73cff09 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemHandlerContainer.cs @@ -0,0 +1,120 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using PhoenixLib.Logging; +using WingsEmu.Core.Extensions; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Core.ItemHandling.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations; + +public class ItemHandlerContainer : IItemHandlerContainer +{ + private readonly Dictionary> _handlers = new(); + private readonly Dictionary _handlersByVnum = new(); + private readonly IItemUsageManager _itemUsageManager; + private readonly IItemUsageToggleManager _itemUsageToggleManager; + + public ItemHandlerContainer(IItemUsageManager itemUsageManager, IItemUsageToggleManager itemUsageToggleManager) + { + _itemUsageManager = itemUsageManager; + _itemUsageToggleManager = itemUsageToggleManager; + } + + public async Task RegisterItemHandler(IItemHandler handler) + { + Dictionary handlers = _handlers.GetOrDefault(handler.ItemType); + if (handlers == null) + { + handlers = new Dictionary(); + _handlers[handler.ItemType] = handlers; + } + + foreach (long effect in handler.Effects) + { + handlers.Add(effect, handler); + } + + Log.Debug($"[ITEM][REGISTER_HANDLER] UI_EFFECT : {handler.Effects} && TYPE : {handler.ItemType} REGISTERED !"); + } + + public async Task RegisterItemHandler(IItemUsageByVnumHandler handler) + { + foreach (long vnum in handler.Vnums) + { + _handlersByVnum.TryAdd(vnum, handler); + Log.Debug($"[ITEM][REGISTER_HANDLER] VNUM : {vnum}"); + } + } + + public async Task UnregisterAsync(IItemHandler handler) + { + Dictionary handlers = _handlers.GetOrDefault(handler.ItemType); + if (handlers == null) + { + return; + } + + foreach (long effect in handler.Effects) + { + handlers.Remove(effect); + } + } + + public async Task UnregisterAsync(IItemUsageByVnumHandler handler) + { + foreach (long vnum in handler.Vnums) + { + _handlersByVnum.Remove(vnum); + } + } + + public void Handle(IClientSession player, InventoryUseItemEvent e) + { + HandleAsync(player, e).ConfigureAwait(false).GetAwaiter().GetResult(); + } + + public async Task HandleAsync(IClientSession player, InventoryUseItemEvent e) + { + if (await _itemUsageToggleManager.IsItemBlocked(e.Item.ItemInstance.ItemVNum)) + { + player.SendMsg(GameDialogKey.ITEM_USAGE_MSG_ITEM_DISABLED_TEMPORARILY, MsgMessageType.Middle); + return; + } + + IItemUsageByVnumHandler vnumHandler = _handlersByVnum.GetValueOrDefault(e.Item.ItemInstance.ItemVNum); + if (vnumHandler == null) + { + Dictionary handlers = _handlers.GetOrDefault(e.Item.ItemInstance.GameItem.ItemType); + if (handlers == null) + { + Log.Debug($"[ITEM][HANDLER_NOT_FOUND] VNUM: {e.Item.ItemInstance.ItemVNum}"); + return; + } + + IItemHandler handler = handlers.GetOrDefault(e.Item.ItemInstance.GameItem.Effect); + if (handler == null) + { + Log.Debug($"[ITEM][HANDLER_NOT_FOUND] VNUM: {e.Item.ItemInstance.ItemVNum}"); + return; + } + + Log.Debug($"[ITEM][HANDLE] Effect: {e.Item.ItemInstance.GameItem.Effect} EffectValue: {e.Item.ItemInstance.GameItem.EffectValue} ItemType: {e.Item.ItemInstance.GameItem.ItemType}"); + _itemUsageManager.SetLastItemUsed(player.PlayerEntity.Id, e.Item.ItemInstance.ItemVNum); + await handler.HandleAsync(player, e); + return; + } + + Log.Debug($"[ITEM][HANDLE] VNUM: {e.Item.ItemInstance.ItemVNum}"); + _itemUsageManager.SetLastItemUsed(player.PlayerEntity.Id, e.Item.ItemInstance.ItemVNum); + await vnumHandler.HandleAsync(player, e); + await player.EmitEventAsync(new InventoryUsedItemEvent { Item = e.Item }); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemHandlerPlugin.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemHandlerPlugin.cs new file mode 100644 index 0000000..d0fdb00 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemHandlerPlugin.cs @@ -0,0 +1,63 @@ +using System; +using PhoenixLib.Extensions; +using PhoenixLib.Logging; +using WingsAPI.Plugins; +using WingsEmu.Game._ItemUsage; + +namespace WingsEmu.Plugins.BasicImplementations; + +public class ItemHandlerPlugin : IGamePlugin +{ + private readonly IServiceProvider _container; + private readonly IItemHandlerContainer _handlers; + + public ItemHandlerPlugin(IItemHandlerContainer handlers, IServiceProvider container) + { + _handlers = handlers; + _container = container; + } + + public string Name => nameof(ItemHandlerPlugin); + + + public void OnLoad() + { + foreach (Type handlerType in typeof(ItemHandlerPlugin).Assembly.GetTypesImplementingInterface()) + { + try + { + object tmp = _container.GetService(handlerType); + if (!(tmp is IItemHandler real)) + { + continue; + } + + Log.Debug($"[ITEM_USAGE][ADD_HANDLER] {handlerType}"); + _handlers.RegisterItemHandler(real).ConfigureAwait(false).GetAwaiter().GetResult(); + } + catch (Exception e) + { + Log.Error("[ITEM_USAGE][FAIL_ADD]", e); + } + } + + foreach (Type handlerType in typeof(ItemHandlerPlugin).Assembly.GetTypesImplementingInterface()) + { + try + { + object tmp = _container.GetService(handlerType); + if (!(tmp is IItemUsageByVnumHandler real)) + { + continue; + } + + Log.Debug($"[ITEM_USAGE][ADD_HANDLER_VNUM] {handlerType}"); + _handlers.RegisterItemHandler(real).ConfigureAwait(false).GetAwaiter().GetResult(); + } + catch (Exception e) + { + Log.Error("[ITEM_USAGE][FAIL_ADD_VNUM]", e); + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemHandlerPluginCore.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemHandlerPluginCore.cs new file mode 100644 index 0000000..931240b --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemHandlerPluginCore.cs @@ -0,0 +1,58 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using PhoenixLib.Configuration; +using WingsAPI.Plugins; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Configuration; +using WingsEmu.Game.Cellons; +using WingsEmu.Game.Managers; +using WingsEmu.Plugins.BasicImplementations.Algorithms.Shells; +using WingsEmu.Plugins.BasicImplementations.ItemUsage.Etc; +using WingsEmu.Plugins.BasicImplementations.Vehicles; + +namespace WingsEmu.Plugins.BasicImplementations; + +public class ItemHandlerPluginCore : IGameServerPlugin +{ + public string Name => nameof(ItemHandlerPluginCore); + + + public void AddDependencies(IServiceCollection services, GameServerLoader gameServer) + { + services.AddHandlers(); + services.AddHandlers(); + + services.AddMultipleConfigurationOneFile("sp_partner"); + services.AddSingleton(); + + services.AddFileConfiguration("mates_buffs"); + services.AddSingleton(); + + services.AddFileConfiguration("sp_wing_info"); + services.AddSingleton(); + + services.AddFileConfiguration("costume_scroll_morphs"); + services.AddSingleton(); + + services.AddSingleton(); + services.AddSingleton(); + + services.AddFileConfiguration("cellon_configuration"); + + services.AddMultipleConfigurationOneFile("shell_option_type_configuration"); + services.AddSingleton(); + + services.AddFileConfiguration("shell_categories_config"); + + services.AddMultipleConfigurationOneFile("shell_level_effect_configuration"); + services.AddSingleton(); + + services.AddMultipleConfigurationOneFile("perfume_configuration"); + services.AddSingleton(); + + services.AddMultipleConfigurationOneFile("vehicle"); + services.TryAddSingleton(); + + services.AddFileConfiguration("cella_refiners_configuration"); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemServiceCollectionExtensions.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemServiceCollectionExtensions.cs new file mode 100644 index 0000000..5e0625c --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemServiceCollectionExtensions.cs @@ -0,0 +1,17 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using PhoenixLib.Extensions; + +namespace WingsEmu.Plugins.BasicImplementations; + +public static class ItemServiceCollectionExtensions +{ + public static void AddHandlers(this IServiceCollection services) + { + Type[] types = typeof(TPlugin).Assembly.GetTypesImplementingInterface(); + foreach (Type handlerType in types) + { + services.AddTransient(handlerType); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Equipment/Box/GameGeneratedMateBeadHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Equipment/Box/GameGeneratedMateBeadHandler.cs new file mode 100644 index 0000000..33904ff --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Equipment/Box/GameGeneratedMateBeadHandler.cs @@ -0,0 +1,112 @@ +using System.Linq; +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Configuration; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Extensions.Mates; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Mates.Events; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Npcs; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Equipment.Box; + +/// +/// This handler is used by game generated beads (fibi bead etc...) +/// +public class GameGeneratedMateBeadHandler : IItemHandler +{ + private readonly IGameLanguageService _languageService; + private readonly IMateEntityFactory _mateEntityFactory; + private readonly INpcMonsterManager _npcMonsterManager; + private readonly ISpPartnerConfiguration _spPartner; + + public GameGeneratedMateBeadHandler(IGameLanguageService languageService, INpcMonsterManager npcMonsterManager, ISpPartnerConfiguration spPartner, IMateEntityFactory mateEntityFactory) + { + _languageService = languageService; + _npcMonsterManager = npcMonsterManager; + _spPartner = spPartner; + _mateEntityFactory = mateEntityFactory; + } + + public ItemType ItemType => ItemType.Box; + public long[] Effects => new long[] { 1 }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + IGameItem item = e.Item.ItemInstance.GameItem; + + // It's not Mate/Partner bead + if (item.ItemSubType != 0 && item.ItemSubType != 1) + { + return; + } + + if (e.Option == 0) + { + session.SendQnaPacket($"u_i 1 {session.PlayerEntity.Id} {(byte)e.Item.ItemInstance.GameItem.Type} {e.Item.Slot} 3", + _languageService.GetLanguage(GameDialogKey.ITEM_DIALOG_ASK_OPEN_PET_BEAD, session.UserLanguage)); + return; + } + + IMonsterData data = _npcMonsterManager.GetNpc((short)item.EffectValue); + + if (data == null) + { + return; + } + + + if (session.CurrentMapInstance != session.PlayerEntity.Miniland) + { + session.SendMsg(_languageService.GetLanguage(GameDialogKey.ITEM_SHOUTMESSAGE_ONLY_IN_MINILAND, session.UserLanguage), MsgMessageType.Middle); + return; + } + + var heldMonster = new MonsterData(data); + + if (session.PlayerEntity.MateComponent.GetMates(x => x.MonsterVNum == heldMonster.MonsterVNum && x.MateType == MateType.Partner).Any() && e.Item.ItemInstance.GameItem.ItemSubType == 1) + { + session.SendMsg(_languageService.GetLanguage(GameDialogKey.PARTNER_SHOUTMESSAGE_ALREADY_HAVE_SAME_PARTNER, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (!session.PlayerEntity.CanReceiveMate(e.Item.ItemInstance.GameItem.ItemSubType == 1 ? MateType.Partner : MateType.Pet)) + { + session.SendMsg( + _languageService.GetLanguage( + e.Item.ItemInstance.GameItem.ItemSubType == 1 ? GameDialogKey.INFORMATION_SHOUTMESSAGE_MAX_PARTNER_COUNT : GameDialogKey.INFORMATION_SHOUTMESSAGE_MAX_PET_COUNT, + session.UserLanguage), + MsgMessageType.Middle); + return; + } + + IMateEntity mateEntity = _mateEntityFactory.CreateMateEntity(session.PlayerEntity, heldMonster, e.Item.ItemInstance.GameItem.ItemSubType == 1 + ? MateType.Partner + : MateType.Pet, e.Item.ItemInstance.GameItem.LevelMinimum, e.Item.ItemInstance.IsLimitedMatePearl); + + await session.EmitEventAsync(new MateInitializeEvent + { + MateEntity = mateEntity + }); + + session.CurrentMapInstance.AddMate(mateEntity); + + session.CurrentMapInstance.Broadcast(s => mateEntity.GenerateIn(_languageService, s.UserLanguage, _spPartner)); + string mateName = mateEntity.MateName == mateEntity.Name ? _languageService.GetLanguage(GameDataType.NpcMonster, mateEntity.Name, session.UserLanguage) : mateEntity.MateName; + GameDialogKey key = mateEntity.MateType == MateType.Pet ? GameDialogKey.PET_CHATMESSAGE_BEAD_EXTRACT : GameDialogKey.PARTNER_CHATMESSAGE_BEAD_EXTRACT; + session.SendChatMessage(_languageService.GetLanguageFormat(key, session.UserLanguage, mateName), ChatMessageColorType.Green); + + await session.RemoveItemFromInventory(item: e.Item); + key = mateEntity.MateType == MateType.Pet ? GameDialogKey.PET_INFO_LEAVE_BEAD : GameDialogKey.PARTNER_INFO_LEAVE_BEAD; + session.SendInfo(_languageService.GetLanguage(key, session.UserLanguage)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Equipment/Box/MateBeadHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Equipment/Box/MateBeadHandler.cs new file mode 100644 index 0000000..a9b91c8 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Equipment/Box/MateBeadHandler.cs @@ -0,0 +1,148 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using PhoenixLib.Logging; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Configuration; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Extensions.Mates; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Mates.Events; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Npcs; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Equipment.Box; + +public class MateBeadHandler : IItemHandler +{ + private readonly IGameLanguageService _languageService; + private readonly IMateEntityFactory _mateEntityFactory; + private readonly INpcMonsterManager _npcMonsterManager; + private readonly ISpPartnerConfiguration _spPartner; + + public MateBeadHandler(IGameLanguageService languageService, INpcMonsterManager npcMonsterManager, ISpPartnerConfiguration spPartner, IMateEntityFactory mateEntityFactory) + { + _languageService = languageService; + _npcMonsterManager = npcMonsterManager; + _spPartner = spPartner; + _mateEntityFactory = mateEntityFactory; + } + + public ItemType ItemType => ItemType.Box; + public long[] Effects => new long[] { 0 }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + IGameItem item = e.Item.ItemInstance.GameItem; + + switch (item.ItemSubType) + { + case 7: // Magic Speed Booster + await CheckMagicSpeedBooster(session, e); + return; + // Mate/Partner bead + case 0: + case 1: + break; + // It's not Mate/Partner bead + default: + return; + } + + if (e.Option == 0) + { + session.SendQnaPacket($"u_i 1 {session.PlayerEntity.Id} {(byte)e.Item.ItemInstance.GameItem.Type} {e.Item.Slot} 3", + _languageService.GetLanguage(GameDialogKey.ITEM_DIALOG_ASK_OPEN_PET_BEAD, session.UserLanguage)); + return; + } + + IMonsterData data = _npcMonsterManager.GetNpc((short)e.Item.ItemInstance.GameItem.EffectValue); + if (data == null) + { + Log.Info($"Couldn't find monster with vnum {e.Item.ItemInstance.GameItem.EffectValue}"); + return; + } + + var heldMonster = new MonsterData(data); + + if (session.CurrentMapInstance != session.PlayerEntity.Miniland) + { + session.SendMsg(_languageService.GetLanguage(GameDialogKey.ITEM_SHOUTMESSAGE_ONLY_IN_MINILAND, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (session.PlayerEntity.MateComponent.GetMates(x => x.MonsterVNum == heldMonster.MonsterVNum && x.MateType == MateType.Partner).Any() && e.Item.ItemInstance.GameItem.ItemSubType == 1) + { + session.SendMsg(_languageService.GetLanguage(GameDialogKey.PARTNER_SHOUTMESSAGE_ALREADY_HAVE_SAME_PARTNER, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (!session.PlayerEntity.CanReceiveMate(e.Item.ItemInstance.GameItem.ItemSubType == 1 ? MateType.Partner : MateType.Pet)) + { + session.SendMsg( + _languageService.GetLanguage( + e.Item.ItemInstance.GameItem.ItemSubType == 1 ? GameDialogKey.INFORMATION_SHOUTMESSAGE_MAX_PARTNER_COUNT : GameDialogKey.INFORMATION_SHOUTMESSAGE_MAX_PET_COUNT, + session.UserLanguage), + MsgMessageType.Middle); + return; + } + + IMateEntity mateEntity = _mateEntityFactory.CreateMateEntity(session.PlayerEntity, heldMonster, e.Item.ItemInstance.GameItem.ItemSubType == 1 ? MateType.Partner : MateType.Pet, + e.Item.ItemInstance.GameItem.LevelMinimum, e.Item.ItemInstance.IsLimitedMatePearl); + + await session.EmitEventAsync(new MateInitializeEvent + { + MateEntity = mateEntity + }); + + session.CurrentMapInstance.AddMate(mateEntity); + session.CurrentMapInstance.Broadcast(s => mateEntity.GenerateIn(_languageService, s.UserLanguage, _spPartner)); + session.SendCondMate(mateEntity); + string mateName = _languageService.GetLanguage(GameDataType.NpcMonster, mateEntity.Name, session.UserLanguage); + GameDialogKey key = mateEntity.MateType == MateType.Pet ? GameDialogKey.PET_CHATMESSAGE_BEAD_EXTRACT : GameDialogKey.PARTNER_CHATMESSAGE_BEAD_EXTRACT; + session.SendChatMessage(_languageService.GetLanguageFormat(key, session.UserLanguage, mateName), ChatMessageColorType.Green); + + await session.RemoveItemFromInventory(item: e.Item); + + key = mateEntity.MateType == MateType.Pet ? GameDialogKey.PET_INFO_LEAVE_BEAD : GameDialogKey.PARTNER_INFO_LEAVE_BEAD; + session.SendInfo(_languageService.GetLanguage(key, session.UserLanguage)); + } + + private async Task CheckMagicSpeedBooster(IClientSession session, InventoryUseItemEvent inventoryUseItemEvent) + { + InventoryItem item = inventoryUseItemEvent.Item; + if (item.ItemInstance.IsBound) + { + await session.EmitEventAsync(new SpeedBoosterEvent()); + return; + } + + if (inventoryUseItemEvent.Option == 0) + { + session.SendQnaPacket($"u_i 1 {session.PlayerEntity.Id} {(byte)item.ItemInstance.GameItem.Type} {item.Slot} 1", + _languageService.GetLanguage(GameDialogKey.ITEM_DIALOG_ASK_NOT_TRADABLE, session.UserLanguage)); + return; + } + + item.ItemInstance.BoundCharacterId = session.PlayerEntity.Id; + + item.ItemInstance.ItemDeleteTime = item.ItemInstance.GameItem.ItemValidTime switch + { + -1 => null, + > 0 => DateTime.UtcNow.AddSeconds(item.ItemInstance.GameItem.ItemValidTime), + _ => item.ItemInstance.ItemDeleteTime + }; + + session.SendInventoryAddPacket(item); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Equipment/Box/RaidBoxHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Equipment/Box/RaidBoxHandler.cs new file mode 100644 index 0000000..a2140e3 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Equipment/Box/RaidBoxHandler.cs @@ -0,0 +1,55 @@ +using System.Threading.Tasks; +using WingsEmu.DTOs.Items; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Items; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Equipment.Box; + +public class RaidBoxHandler : IItemHandler +{ + private readonly IGameLanguageService _languageService; + + public RaidBoxHandler(IGameLanguageService languageService) => _languageService = languageService; + + public ItemType ItemType => ItemType.Box; + public long[] Effects => new long[] { 999 }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + InventoryItem box = session.PlayerEntity.GetItemBySlotAndType(e.Item.Slot, InventoryType.Equipment); + if (box == null) + { + return; + } + + if (box.ItemInstance.Type != ItemInstanceType.BoxInstance) + { + return; + } + + GameItemInstance boxItem = box.ItemInstance; + + if (boxItem.GameItem.ItemSubType != 3) + { + return; + } + + if (e.Option == 0) + { + session.SendQnaPacket($"guri 300 8023 {e.Item.Slot}", _languageService.GetLanguage(GameDialogKey.ITEM_DIALOG_ASK_OPEN_BOX, session.UserLanguage)); + return; + } + + await session.EmitEventAsync(new RollItemBoxEvent + { + Item = box + }); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Equipment/Box/SpHolderHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Equipment/Box/SpHolderHandler.cs new file mode 100644 index 0000000..602eff5 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Equipment/Box/SpHolderHandler.cs @@ -0,0 +1,101 @@ +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.DTOs.Items; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Items; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Equipment.Box; + +public class SpHolderHandler : IItemUsageByVnumHandler +{ + private readonly IGameItemInstanceFactory _gameItemInstanceFactory; + private readonly IGameLanguageService _languageService; + + public SpHolderHandler(IGameLanguageService languageService, IGameItemInstanceFactory gameItemInstanceFactory) + { + _languageService = languageService; + _gameItemInstanceFactory = gameItemInstanceFactory; + } + + public long[] Vnums => new[] { (long)ItemVnums.SPECIALIST_CARD_HOLDER, (long)ItemVnums.GOLDEN_SP_HOLDER, (long)ItemVnums.PSP_HOLDER }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + int holderVnum = e.Item.ItemInstance.ItemVNum; + InventoryItem box = session.PlayerEntity.GetItemBySlotAndType(e.Item.Slot, InventoryType.Equipment); + + if (box == null) + { + return; + } + + if (box.ItemInstance.Type != ItemInstanceType.BoxInstance) + { + return; + } + + GameItemInstance boxItem = box.ItemInstance; + + if (boxItem.HoldingVNum == null || boxItem.HoldingVNum == 0) + { + session.SendWopenPacket(WindowType.GOLDEN_SP_CARD_HOLDER, e.Item.Slot, holderVnum == (int)ItemVnums.PSP_HOLDER ? (byte)1 : (byte)0); + return; + } + + if (!session.PlayerEntity.HasSpaceFor(boxItem.HoldingVNum.Value)) + { + session.SendMsg(_languageService.GetLanguage(GameDialogKey.INTERACTION_MESSAGE_NOT_ENOUGH_PLACE, session.UserLanguage), MsgMessageType.Middle); + return; + } + + GameItemInstance newItem = _gameItemInstanceFactory.CreateSpecialistCard(boxItem.HoldingVNum.Value, boxItem.SpLevel, boxItem.Upgrade); + await session.AddNewItemToInventory(newItem); + + if (newItem.Type != ItemInstanceType.SpecialistInstance) + { + return; + } + + GameItemInstance sp = newItem; + + if (holderVnum == (int)ItemVnums.PSP_HOLDER) + { + sp.SkillRank1 = boxItem.SkillRank1; + sp.SkillRank2 = boxItem.SkillRank2; + sp.SkillRank3 = boxItem.SkillRank3; + sp.PartnerSkill1 = boxItem.PartnerSkill1; + sp.PartnerSkill2 = boxItem.PartnerSkill2; + sp.PartnerSkill3 = boxItem.PartnerSkill3; + sp.PartnerSkills = boxItem.PartnerSkills; + } + else + { + sp.SlDamage = boxItem.SlDamage; + sp.SlDefence = boxItem.SlDefence; + sp.SlElement = boxItem.SlElement; + sp.SlHP = boxItem.SlHP; + sp.SpDamage = boxItem.SpDamage; + sp.SpDark = boxItem.SpDark; + sp.SpDefence = boxItem.SpDefence; + sp.SpElement = boxItem.SpElement; + sp.SpFire = boxItem.SpFire; + sp.SpHP = boxItem.SpHP; + sp.SpLevel = boxItem.SpLevel; + sp.SpLight = boxItem.SpLight; + sp.SpStoneUpgrade = boxItem.SpStoneUpgrade; + sp.SpWater = boxItem.SpWater; + sp.Upgrade = boxItem.Upgrade; + sp.Xp = boxItem.Xp; + } + + await session.RemoveItemFromInventory(item: box); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Equipment/Box/UserBeadHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Equipment/Box/UserBeadHandler.cs new file mode 100644 index 0000000..bbfecf1 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Equipment/Box/UserBeadHandler.cs @@ -0,0 +1,260 @@ +using System.Linq; +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.DTOs.Items; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Configuration; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Extensions.Mates; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Mates.Events; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Npcs; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Equipment.Box; + +/// +/// This handler is used by player related bead (bead with custom item in or without item) +/// +public class UserBeadHandler : IItemUsageByVnumHandler +{ + private readonly IGameItemInstanceFactory _gameItemInstanceFactory; + private readonly IGameLanguageService _languageService; + private readonly IMateEntityFactory _mateEntityFactory; + private readonly INpcMonsterManager _npcMonsterManager; + private readonly ISpPartnerConfiguration _spPartner; + + public UserBeadHandler(IGameLanguageService languageService, INpcMonsterManager npcMonsterManager, ISpPartnerConfiguration spPartner, IGameItemInstanceFactory gameItemInstanceFactory, + IMateEntityFactory mateEntityFactory) + { + _languageService = languageService; + _npcMonsterManager = npcMonsterManager; + _spPartner = spPartner; + _gameItemInstanceFactory = gameItemInstanceFactory; + _mateEntityFactory = mateEntityFactory; + } + + public long[] Vnums => new[] { (long)ItemVnums.PET_BEAD, (long)ItemVnums.PARTNER_BEAD, (long)ItemVnums.MOUNT_BEAD, (long)ItemVnums.FAIRY_BEAD }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + int beadVnum = e.Item.ItemInstance.ItemVNum; + + InventoryItem box = session.PlayerEntity.GetItemBySlotAndType(e.Item.Slot, InventoryType.Equipment); + + if (box == null) + { + return; + } + + if (box.ItemInstance.Type != ItemInstanceType.BoxInstance) + { + return; + } + + GameItemInstance boxInstanceItem = box.ItemInstance; + + switch (beadVnum) + { + case (int)ItemVnums.FAIRY_BEAD when boxInstanceItem.HoldingVNum == 0 || boxInstanceItem.HoldingVNum == null: + session.SendGuriPacket(26, 0, e.Item.Slot); + return; + case (int)ItemVnums.FAIRY_BEAD: + { + if (!session.PlayerEntity.HasSpaceFor(boxInstanceItem.HoldingVNum.Value)) + { + session.SendMsg(session.GetLanguage(GameDialogKey.INTERACTION_MESSAGE_NOT_ENOUGH_PLACE), MsgMessageType.Middle); + session.SendChatMessage(session.GetLanguage(GameDialogKey.INTERACTION_MESSAGE_NOT_ENOUGH_PLACE), ChatMessageColorType.Yellow); + return; + } + + GameItemInstance newFairy = _gameItemInstanceFactory.CreateItem(boxInstanceItem.HoldingVNum.Value); + + await session.AddNewItemToInventory(newFairy, true, ChatMessageColorType.Green, true); + newFairy.ElementRate = boxInstanceItem.ElementRate; + newFairy.BoundCharacterId = session.PlayerEntity.Id; + newFairy.Xp = boxInstanceItem.Xp; + await session.RemoveItemFromInventory(item: e.Item); + return; + } + case (int)ItemVnums.MOUNT_BEAD when boxInstanceItem.HoldingVNum == 0 || boxInstanceItem.HoldingVNum == null: + session.SendGuriPacket(24, 0, e.Item.Slot); + return; + case (int)ItemVnums.MOUNT_BEAD: + { + if (!session.PlayerEntity.HasSpaceFor(boxInstanceItem.HoldingVNum.Value)) + { + session.SendMsg(session.GetLanguage(GameDialogKey.INTERACTION_MESSAGE_NOT_ENOUGH_PLACE), MsgMessageType.Middle); + session.SendChatMessage(session.GetLanguage(GameDialogKey.INTERACTION_MESSAGE_NOT_ENOUGH_PLACE), ChatMessageColorType.Yellow); + return; + } + + GameItemInstance newMount = _gameItemInstanceFactory.CreateItem(boxInstanceItem.HoldingVNum.Value); + await session.AddNewItemToInventory(newMount, true, ChatMessageColorType.Green, true); + await session.RemoveItemFromInventory(item: box); + return; + } + case (int)ItemVnums.PET_BEAD: + case (int)ItemVnums.PARTNER_BEAD: + { + if (e.Option == 0) + { + if (boxInstanceItem.HoldingVNum != null && boxInstanceItem.HoldingVNum != 0) + { + session.SendQnaPacket($"u_i 1 {session.PlayerEntity.Id} {(int)box.InventoryType} {e.Item.Slot} 1", + _languageService.GetLanguage(GameDialogKey.ITEM_DIALOG_ASK_OPEN_PET_BEAD, session.UserLanguage)); + return; + } + + if (e.Packet.Length < 4) + { + return; + } + + if (!int.TryParse(e.Packet[3], out int mateId)) + { + return; + } + + if (!session.PlayerEntity.MateComponent.GetMates(x => x.Id == mateId && x.MateType == (beadVnum == (int)ItemVnums.PET_BEAD ? MateType.Pet : MateType.Partner)).Any()) + { + return; + } + + session.SendQnaPacket($"u_i 1 {session.PlayerEntity.Id} {(int)box.InventoryType} {e.Item.Slot} {mateId}", + _languageService.GetLanguage(GameDialogKey.ITEM_DIALOG_ASK_PET_STORE, session.UserLanguage)); + return; + } + + if (boxInstanceItem.HoldingVNum == 0 || boxInstanceItem.HoldingVNum == null) + { + if (e.Packet.Length < 6) + { + return; + } + + if (!int.TryParse(e.Packet[6], out int petId)) + { + return; + } + + IMateEntity mateEntity = session.PlayerEntity.MateComponent.GetMate(s => s.Id == petId && s.MateType == (beadVnum == (int)ItemVnums.PET_BEAD ? MateType.Pet : MateType.Partner)); + if (mateEntity == null) + { + return; + } + + if (!mateEntity.IsAlive()) + { + return; + } + + if (mateEntity.IsTeamMember) + { + return; + } + + if (mateEntity.IsLimited) + { + session.SendMsg(session.GetLanguage(GameDialogKey.ITEM_SHOUTMESSAGE_IS_LIMITED), MsgMessageType.Middle); + return; + } + + if (mateEntity.MapInstance.Id != session.PlayerEntity.Miniland.Id) + { + session.SendMsg(_languageService.GetLanguage(GameDialogKey.ITEM_SHOUTMESSAGE_ONLY_IN_MINILAND, session.UserLanguage), MsgMessageType.Middle); + return; + } + + boxInstanceItem.MateType = mateEntity.MateType; + boxInstanceItem.HoldingVNum = mateEntity.NpcMonsterVNum; + boxInstanceItem.SpLevel = mateEntity.Level; + boxInstanceItem.SpDamage = mateEntity.Attack; + boxInstanceItem.SpDefence = mateEntity.Defence; + boxInstanceItem.Xp = mateEntity.Experience; + + await session.EmitEventAsync(new MateRemoveEvent + { + MateEntity = mateEntity + }); + + GameDialogKey gameKey = mateEntity.MateType == MateType.Pet ? GameDialogKey.PET_INFO_STORED : GameDialogKey.PARTNER_INFO_STORED; + session.SendInfo(_languageService.GetLanguage(gameKey, session.UserLanguage)); + return; + } + + if (boxInstanceItem.HoldingVNum == 0 || boxInstanceItem.HoldingVNum == null) + { + return; + } + + IMonsterData data = _npcMonsterManager.GetNpc(boxInstanceItem.HoldingVNum.Value); + if (data == null) + { + return; + } + + var heldMonster = new MonsterData(data); + + if (session.CurrentMapInstance != session.PlayerEntity.Miniland) + { + session.SendMsg(_languageService.GetLanguage(GameDialogKey.ITEM_SHOUTMESSAGE_ONLY_IN_MINILAND, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (session.PlayerEntity.MateComponent.GetMates(x => x.MonsterVNum == heldMonster.MonsterVNum && x.MateType == MateType.Partner).Any() && boxInstanceItem.MateType == MateType.Partner) + { + session.SendMsg(_languageService.GetLanguage(GameDialogKey.PARTNER_SHOUTMESSAGE_ALREADY_HAVE_SAME_PARTNER, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (!boxInstanceItem.MateType.HasValue) + { + return; + } + + if (!session.PlayerEntity.CanReceiveMate(boxInstanceItem.MateType.Value)) + { + session.SendMsg( + _languageService.GetLanguage( + boxInstanceItem.MateType == MateType.Pet ? GameDialogKey.INFORMATION_SHOUTMESSAGE_MAX_PET_COUNT : GameDialogKey.INFORMATION_SHOUTMESSAGE_MAX_PARTNER_COUNT, + session.UserLanguage), MsgMessageType.Middle); + return; + } + + IMateEntity mate = _mateEntityFactory.CreateMateEntity(session.PlayerEntity, heldMonster, boxInstanceItem.MateType.Value, + boxInstanceItem.SpLevel == 0 ? (byte)1 : boxInstanceItem.SpLevel); + mate.Attack = boxInstanceItem.SpDamage; + mate.Defence = boxInstanceItem.SpDefence; + mate.Experience = boxInstanceItem.Xp; + + await session.EmitEventAsync(new MateInitializeEvent + { + MateEntity = mate + }); + + session.CurrentMapInstance.AddMate(mate); + session.CurrentMapInstance.Broadcast(s => mate.GenerateIn(_languageService, s.UserLanguage, _spPartner)); + session.SendCondMate(mate); + string mateName = mate.MateName == mate.Name ? _languageService.GetLanguage(GameDataType.NpcMonster, mate.Name, session.UserLanguage) : mate.MateName; + GameDialogKey key = mate.MateType == MateType.Pet ? GameDialogKey.PET_CHATMESSAGE_BEAD_EXTRACT : GameDialogKey.PARTNER_CHATMESSAGE_BEAD_EXTRACT; + session.SendChatMessage(_languageService.GetLanguageFormat(key, session.UserLanguage, mateName), ChatMessageColorType.Green); + + await session.RemoveItemFromInventory(item: e.Item); + key = mate.MateType == MateType.Pet ? GameDialogKey.PET_INFO_LEAVE_BEAD : GameDialogKey.PARTNER_INFO_LEAVE_BEAD; + session.SendInfo(_languageService.GetLanguage(key, session.UserLanguage)); + break; + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Magical/Act6PassiveItemHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Magical/Act6PassiveItemHandler.cs new file mode 100644 index 0000000..23b7715 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Magical/Act6PassiveItemHandler.cs @@ -0,0 +1,90 @@ +using System.Linq; +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsAPI.Game.Extensions.Quicklist; +using WingsEmu.DTOs.Skills; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Skills; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Etc.Magical; + +public class Act6PassiveItemHandler : IItemHandler +{ + private readonly IGameLanguageService _gameLanguage; + private readonly ISkillsManager _skillsManager; + + public Act6PassiveItemHandler(IGameLanguageService gameLanguage, ISkillsManager skillsManager) + { + _gameLanguage = gameLanguage; + _skillsManager = skillsManager; + } + + public ItemType ItemType => ItemType.Magical; + public long[] Effects => new long[] { 99 }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + InventoryItem item = e.Item; + int heroLevel = item.ItemInstance.GameItem.LevelMinimum; + + if (session.PlayerEntity.HeroLevel < heroLevel) + { + session.SendErrorChatMessage(_gameLanguage.GetLanguage(GameDialogKey.INFORMATION_CHATMESSAGE_NOT_REQUIERED_LEVEL, session.UserLanguage)); + return; + } + + SkillDTO skill = _skillsManager.GetSkill(item.ItemInstance.GameItem.EffectValue); + + if (skill == null) + { + return; + } + + if (!skill.IsPassiveSkill()) + { + return; + } + + if (session.PlayerEntity.CharacterSkills.ContainsKey(item.ItemInstance.GameItem.EffectValue)) + { + return; + } + + CharacterSkill passive = session.PlayerEntity.CharacterSkills.Values.FirstOrDefault(x => x.Skill.CastId == skill.CastId); + + if (passive != null) + { + if ((passive.Skill.UpgradeSkill + 1) != skill.UpgradeSkill) + { + return; + } + } + + foreach (CharacterSkill ski in session.PlayerEntity.CharacterSkills.Values) + { + if (skill.CastId == ski.Skill.CastId && ski.Skill.IsPassiveSkill()) + { + session.PlayerEntity.CharacterSkills.TryRemove(ski.SkillVNum, out CharacterSkill _); + } + } + + session.PlayerEntity.CharacterSkills[skill.CastId] = new CharacterSkill + { + SkillVNum = item.ItemInstance.GameItem.EffectValue + }; + + session.RefreshPassiveBCards(); + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.SKILL_SHOUTMESSAGE_LEARNED, session.UserLanguage), MsgMessageType.Middle); + session.RefreshSkillList(); + session.RefreshQuicklist(); + await session.RemoveItemFromInventory(item.ItemInstance.ItemVNum); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Magical/BubbleHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Magical/BubbleHandler.cs new file mode 100644 index 0000000..faf8b36 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Magical/BubbleHandler.cs @@ -0,0 +1,46 @@ +using System.Threading.Tasks; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Etc.Magical; + +public class BubbleHandler : IItemHandler +{ + private readonly IGameLanguageService _languageService; + + public BubbleHandler(IGameLanguageService languageService) => _languageService = languageService; + + public ItemType ItemType => ItemType.Magical; + public long[] Effects => new long[] { 16 }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + IPlayerEntity character = session.PlayerEntity; + + if (character.IsOnVehicle) + { + string message = _languageService.GetLanguage(GameDialogKey.ITEM_CHATMESSAGE_CANT_USE_THAT, session.UserLanguage); + session.SendChatMessage(message, ChatMessageColorType.Yellow); + return; + } + + if (session.IsMuted()) + { + session.SendMuteMessage(); + return; + } + + if (e.Option != 0) + { + return; + } + + session.SendGuriPacket(10, 4, 1); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Magical/DignityPotionHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Magical/DignityPotionHandler.cs new file mode 100644 index 0000000..38fec29 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Magical/DignityPotionHandler.cs @@ -0,0 +1,54 @@ +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Etc.Magical; + +public class DignityPotionHandler : IItemHandler +{ + private readonly IGameLanguageService _languageService; + private readonly GameMinMaxConfiguration _minMaxConfiguration; + private readonly IRankingManager _rankingManager; + private readonly IReputationConfiguration _reputationConfiguration; + + public DignityPotionHandler(IGameLanguageService languageService, GameMinMaxConfiguration minMaxConfiguration, IReputationConfiguration reputationConfiguration, IRankingManager rankingManager) + { + _languageService = languageService; + _minMaxConfiguration = minMaxConfiguration; + _reputationConfiguration = reputationConfiguration; + _rankingManager = rankingManager; + } + + public ItemType ItemType => ItemType.Magical; + public long[] Effects => new long[] { 14 }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + IPlayerEntity character = session.PlayerEntity; + IGameItem gameItem = e.Item.ItemInstance.GameItem; + + if (character.IsOnVehicle) + { + string message = _languageService.GetLanguage(GameDialogKey.ITEM_CHATMESSAGE_CANT_USE_THAT, session.UserLanguage); + session.SendChatMessage(message, ChatMessageColorType.Yellow); + return; + } + + if (!character.AddDignity(gameItem.EffectValue, _minMaxConfiguration, _languageService, _reputationConfiguration, _rankingManager.TopReputation)) + { + return; + } + + await session.RemoveItemFromInventory(item: e.Item); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Magical/DyeBombHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Magical/DyeBombHandler.cs new file mode 100644 index 0000000..1ab2a39 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Magical/DyeBombHandler.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.Game; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Items; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Etc.Magical; + +public class DyeBombHandler : IItemHandler +{ + private readonly IGameLanguageService _languageService; + private readonly IRandomGenerator _randomGenerator; + + private readonly HashSet _wigs = new() + { + ItemVnums.WIG, ItemVnums.COLORFUL_WIG, ItemVnums.BROWN_WIG, ItemVnums.SPIKY_HAIRSTYLE, ItemVnums.RARE_SPIKY_HAIRSTYLE + }; + + public DyeBombHandler(IGameLanguageService languageService, IRandomGenerator randomGenerator) + { + _languageService = languageService; + _randomGenerator = randomGenerator; + } + + public ItemType ItemType => ItemType.Magical; + public long[] Effects => new long[] { 30 }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + GameItemInstance hat = session.PlayerEntity.Hat; + + if (hat == null) + { + session.SendMsg(_languageService.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_NO_WIG, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (!_wigs.Contains((ItemVnums)hat.ItemVNum)) + { + session.SendChatMessage(_languageService.GetLanguage(GameDialogKey.ITEM_CHATMESSAGE_CANT_USE_THAT, session.UserLanguage), ChatMessageColorType.Red); + return; + } + + hat.Design = (short)_randomGenerator.RandomNumber(15); + session.BroadcastEq(); + await session.RemoveItemFromInventory(item: e.Item); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Magical/GeneralMagicalItemHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Magical/GeneralMagicalItemHandler.cs new file mode 100644 index 0000000..c3e3215 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Magical/GeneralMagicalItemHandler.cs @@ -0,0 +1,57 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.DTOs.BCards; +using WingsEmu.Game._enum; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Etc.Magical; + +public class GeneralMagicalItemHandler : IItemHandler +{ + private readonly IBCardEffectHandlerContainer _bCardEffectHandlerContainer; + + public GeneralMagicalItemHandler(IBCardEffectHandlerContainer bCardEffectHandlerContainer) => _bCardEffectHandlerContainer = bCardEffectHandlerContainer; + + public ItemType ItemType => ItemType.Magical; + public long[] Effects => new long[] { 0 }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + InventoryItem item = e.Item; + switch ((ItemVnums)item.ItemInstance.ItemVNum) + { + case ItemVnums.LIMITED_BANK_CARD: + case ItemVnums.BANK_CARD: + await session.EmitEventAsync(new BankOpenEvent + { + BankCard = e.Item + }); + return; + case ItemVnums.BANDAGE: + + if (!session.PlayerEntity.BuffComponent.HasAnyBuff()) + { + return; + } + + foreach (BCardDTO bCard in item.ItemInstance.GameItem.BCards) + { + _bCardEffectHandlerContainer.Execute(session.PlayerEntity, session.PlayerEntity, bCard); + } + + await session.RemoveItemFromInventory(item: item); + + break; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Magical/HairDyeHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Magical/HairDyeHandler.cs new file mode 100644 index 0000000..4aee082 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Magical/HairDyeHandler.cs @@ -0,0 +1,90 @@ +using System; +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.Game; +using WingsEmu.Game._enum; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Items; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Character; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Etc.Magical; + +public class HairDyeHandler : IItemUsageByVnumHandler +{ + private readonly IRandomGenerator _randomGenerator; + + public HairDyeHandler(IRandomGenerator randomGenerator) => _randomGenerator = randomGenerator; + + public long[] Vnums => new[] + { + (long)ItemVnums.MYSTERIOUS_HAIR_DYE, (long)ItemVnums.MYSTERIOUS_HAIR_DYE_LIMITED, (long)ItemVnums.SAVAGE_DYES, + (long)ItemVnums.HAIR_DYE_11, + (long)ItemVnums.HAIR_DYE_12, + (long)ItemVnums.HAIR_DYE_13, + (long)ItemVnums.HAIR_DYE_14, + (long)ItemVnums.HAIR_DYE_15, + (long)ItemVnums.HAIR_DYE_16, + (long)ItemVnums.HAIR_DYE_17, + (long)ItemVnums.HAIR_DYE_18, + (long)ItemVnums.HAIR_DYE_19, + (long)ItemVnums.HAIR_DYE_20, + (long)ItemVnums.RED_DYE, + (long)ItemVnums.DARK_DYE, + (long)ItemVnums.BLUE_DYE, + (long)ItemVnums.GREY_DYE, + (long)ItemVnums.PINK_DYE, + (long)ItemVnums.BLACK_DYE, + (long)ItemVnums.BROWN_DYE, + (long)ItemVnums.GREEN_DYE, + (long)ItemVnums.WHITE_DYE, + (long)ItemVnums.ORANGE_DYE, + (long)ItemVnums.PURPLE_DYE, + (long)ItemVnums.YELLOW_DYE, + (long)ItemVnums.LIGHT_BLUE_DYE, + (long)ItemVnums.LIGHT_GREEN_DYE + }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + IGameItem gameItem = e.Item.ItemInstance.GameItem; + int itemVnum = e.Item.ItemInstance.ItemVNum; + + if (itemVnum == (short)ItemVnums.SAVAGE_DYES) + { + HairStyleType hairStyle = session.PlayerEntity.HairStyle; + if (hairStyle is HairStyleType.FemaleSpecialHair or HairStyleType.MaleSpecialHair) + { + return; + } + + if (hairStyle != HairStyleType.ChoppyBangs && hairStyle != HairStyleType.FrenchBraid && hairStyle != HairStyleType.FauxHawk && hairStyle != HairStyleType.JellyRolls) + { + return; + } + + session.PlayerEntity.HairStyle = session.PlayerEntity.Gender == GenderType.Female ? HairStyleType.FemaleSpecialHair : HairStyleType.MaleSpecialHair; + + session.BroadcastEq(); + await session.RemoveItemFromInventory(item: e.Item); + + return; + } + + if (itemVnum == (int)ItemVnums.MYSTERIOUS_HAIR_DYE || itemVnum == (int)ItemVnums.MYSTERIOUS_HAIR_DYE_LIMITED) + { + short nextValue = (short)_randomGenerator.RandomNumber(0, 127); + session.PlayerEntity.HairColor = Enum.TryParse(nextValue.ToString(), out HairColorType hairColorType) ? hairColorType : 0; + } + else + { + session.PlayerEntity.HairColor = Enum.TryParse(gameItem.EffectValue.ToString(), out HairColorType hairColorType) ? hairColorType : 0; + } + + session.BroadcastEq(); + await session.RemoveItemFromInventory(item: e.Item); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Magical/HairStyleHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Magical/HairStyleHandler.cs new file mode 100644 index 0000000..c4e114e --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Magical/HairStyleHandler.cs @@ -0,0 +1,63 @@ +using System; +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.Game._enum; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Items; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Character; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Etc.Magical; + +public class HairStyleHandler : IItemHandler +{ + public ItemType ItemType => ItemType.Magical; + public long[] Effects { get; } = { 11 }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + IPlayerEntity character = session.PlayerEntity; + IGameItem gameItem = e.Item.ItemInstance.GameItem; + + if (character.Class == ClassType.Adventurer && (gameItem.Id == (int)ItemVnums.SUPER_HAIR_GEL || gameItem.Id == (int)ItemVnums.SUPER_HAIR_WAX)) + { + return; + } + + if (!Enum.TryParse(gameItem.EffectValue.ToString(), out HairStyleType hairStyle)) + { + return; + } + + if (character.HairStyle == hairStyle && hairStyle != HairStyleType.A) + { + return; + } + + if ((hairStyle == HairStyleType.ChoppyBangs || hairStyle == HairStyleType.FrenchBraid) && character.Gender == GenderType.Male) + { + return; + } + + if ((hairStyle == HairStyleType.FauxHawk || hairStyle == HairStyleType.JellyRolls) && character.Gender == GenderType.Female) + { + return; + } + + if (hairStyle == HairStyleType.A) + { + character.HairStyle = character.HairStyle == HairStyleType.A ? HairStyleType.B : HairStyleType.A; + } + else + { + character.HairStyle = hairStyle; + } + + session.BroadcastEq(); + await session.RemoveItemFromInventory(item: e.Item); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Magical/MagicalBuffPotionHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Magical/MagicalBuffPotionHandler.cs new file mode 100644 index 0000000..ff6fa1f --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Magical/MagicalBuffPotionHandler.cs @@ -0,0 +1,51 @@ +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Etc.Magical; + +public class MagicalBuffPotionHandler : IItemHandler +{ + private readonly IBuffFactory _buffFactory; + + public MagicalBuffPotionHandler(IBuffFactory buffFactory) => _buffFactory = buffFactory; + + public ItemType ItemType => ItemType.Magical; + public long[] Effects => new long[] { 20 }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + if (!session.PlayerEntity.IsAlive()) + { + return; + } + + if (session.PlayerEntity.IsOnVehicle) + { + return; + } + + int buffVnum = e.Item.ItemInstance.GameItem.EffectValue; + Buff buffToCreate = _buffFactory.CreateBuff(buffVnum, session.PlayerEntity); + await session.PlayerEntity.AddBuffAsync(buffToCreate); + + foreach (IMateEntity mate in session.PlayerEntity.MateComponent.TeamMembers()) + { + if (!mate.IsAlive()) + { + continue; + } + + buffToCreate = _buffFactory.CreateBuff(buffVnum, mate); + await mate.AddBuffAsync(buffToCreate); + } + + await session.RemoveItemFromInventory(item: e.Item); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Magical/ShellItemHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Magical/ShellItemHandler.cs new file mode 100644 index 0000000..b15f37d --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Magical/ShellItemHandler.cs @@ -0,0 +1,201 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsAPI.Packets.Enums.Shells; +using WingsEmu.DTOs.Items; +using WingsEmu.Game; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Items; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Etc.Magical; + +public class ShellItemHandler : IItemHandler +{ + private static readonly ShellEffectType[] _mainWeaponShells = + { + ShellEffectType.AntiMagicDisorder, + ShellEffectType.GainMoreXP, + ShellEffectType.GainMoreCXP, + ShellEffectType.GainMoreGold + }; + + private readonly IGameLanguageService _gameLanguage; + private readonly IRandomGenerator _randomGenerator; + + public ShellItemHandler(IGameLanguageService gameLanguage, IRandomGenerator randomGenerator) + { + _gameLanguage = gameLanguage; + _randomGenerator = randomGenerator; + } + + public ItemType ItemType => ItemType.Shell; + public long[] Effects => new long[] { 0 }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + InventoryItem inv = e.Item; + string[] packetsplit = e.Packet; + + if (packetsplit == null) + { + return; + } + + if (e?.Item == null) + { + return; + } + + if (inv.ItemInstance.EquipmentOptions == null || !inv.ItemInstance.EquipmentOptions.Any()) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.SHELLS_SHOUTMESSAGE_MUST_BE_IDENTIFIED, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (packetsplit.Length < 9) + { + // MODIFIED PACKET + return; + } + + if (!short.TryParse(packetsplit[9], out short eqSlot) || !Enum.TryParse(packetsplit[8], out InventoryType eqType)) + { + return; + } + + if (!int.TryParse(packetsplit[6], out int requestType)) + { + return; + } + + GameItemInstance shell = inv.ItemInstance; + InventoryItem eq = session.PlayerEntity.GetItemBySlotAndType(eqSlot, eqType); + + if (eq == null) + { + // PACKET MODIFIED + return; + } + + if (eq.ItemInstance.Type != ItemInstanceType.WearableInstance) + { + return; + } + + GameItemInstance itemInstance = eq.ItemInstance; + + if (itemInstance.GameItem.ItemType != ItemType.Armor && shell.GameItem.ItemSubType == 1) + { + // ARMOR SHELL ONLY APPLY ON ARMORS + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.SHELLS_SHOUTMESSAGE_FOR_ARMOR_ONLY, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (itemInstance.GameItem.ItemType != ItemType.Weapon && shell.GameItem.ItemSubType == 0) + { + // WEAPON SHELL ONLY APPLY ON WEAPONS + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.SHELLS_SHOUTMESSAGE_FOR_WEAPON_ONLY, session.UserLanguage), MsgMessageType.Middle); + return; + } + + switch (requestType) + { + case 0: + session.SendPacket(itemInstance.EquipmentOptions?.Count > 0 + ? $"qna #u_i^1^{session.PlayerEntity.Id}^{(short)inv.ItemInstance.GameItem.Type}^{inv.Slot}^1^1^{(short)eqType}^{eqSlot} {_gameLanguage.GetLanguage(GameDialogKey.SHELLS_DIALOG_REPLACE_OPTIONS, session.UserLanguage)}" + : $"qna #u_i^1^{session.PlayerEntity.Id}^{(short)inv.ItemInstance.GameItem.Type}^{inv.Slot}^1^1^{(short)eqType}^{eqSlot} {_gameLanguage.GetLanguage(GameDialogKey.SHELLS_DIALOG_ADD_OPTIONS, session.UserLanguage)}"); + break; + case 1: + if (shell.EquipmentOptions == null) + { + // SHELL NOT IDENTIFIED + return; + } + + if (itemInstance.BoundCharacterId != session.PlayerEntity.Id && itemInstance.BoundCharacterId != null) + { + // NEED TO PERFUME STUFF BEFORE CHANGING SHELL + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.SHELL_SHOUTMESSAGE_NEED_PERFUM_TO_CHANGE_SHELL, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (itemInstance.Rarity < shell.Rarity) + { + // RARITY TOO HIGH ON SHELL + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.SHELLS_SHOUTMESSAGE_RARITY_TOO_HIGH, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (itemInstance.GameItem.IsHeroic) + { + // ITEM IS HEROIC + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.GAMBLING_MESSAGE_ITEM_IS_HEROIC, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (itemInstance.GameItem.LevelMinimum < shell.Upgrade) + { + // SHELL LEVEL TOO HIGH + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.SHELLS_SHOUTMESSAGE_LEVEL_TOO_HIGH, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (itemInstance.EquipmentOptions != null && itemInstance.EquipmentOptions.Any() && _randomGenerator.RandomNumber() > 50) + { + // BREAK BECAUSE DIDN'T USE MAGIC ERASER + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.SHELLS_SHOUTMESSAGE_BROKEN, session.UserLanguage), MsgMessageType.Middle); + await session.RemoveItemFromInventory(item: inv); + return; + } + + // If it's second weapon - remove all (Main Weapon) shells + if (itemInstance.GameItem.EquipmentSlot == EquipmentType.SecondaryWeapon) + { + var toRemove = new List(); + foreach (EquipmentOptionDTO i in shell.EquipmentOptions) + { + if (i == null) + { + continue; + } + + var type = (ShellEffectType)i.Type; + if (!_mainWeaponShells.Contains(type)) + { + continue; + } + + toRemove.Add(i); + } + + foreach (EquipmentOptionDTO remove in toRemove) + { + shell.EquipmentOptions.Remove(remove); + } + } + + itemInstance.EquipmentOptions?.Clear(); + itemInstance.EquipmentOptions ??= new List(); + foreach (EquipmentOptionDTO i in shell.EquipmentOptions) + { + session.SendGuriPacket(17, 1, session.PlayerEntity.Id); + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.SHELLS_SHOUTMESSAGE_OPTION_SET, session.UserLanguage), MsgMessageType.Middle); + itemInstance.EquipmentOptions.Add(i); + } + + itemInstance.BoundCharacterId = session.PlayerEntity.Id; + itemInstance.ShellRarity = shell.Rarity; + await session.RemoveItemFromInventory(item: inv); + break; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Magical/SpeakerHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Magical/SpeakerHandler.cs new file mode 100644 index 0000000..c38c472 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Magical/SpeakerHandler.cs @@ -0,0 +1,40 @@ +using System.Threading.Tasks; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Etc.Magical; + +public class SpeakerHandler : IItemHandler +{ + private readonly IGameLanguageService _languageService; + + public SpeakerHandler(IGameLanguageService languageService) => _languageService = languageService; + + public ItemType ItemType => ItemType.Magical; + public long[] Effects => new long[] { 15 }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + IPlayerEntity character = session.PlayerEntity; + + if (character.IsOnVehicle) + { + string message = _languageService.GetLanguage(GameDialogKey.ITEM_CHATMESSAGE_CANT_USE_THAT, session.UserLanguage); + session.SendChatMessage(message, ChatMessageColorType.Yellow); + return; + } + + if (e.Option != 0) + { + return; + } + + session.SendGuriPacket(10, 3, 1); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Magical/TeamStoneHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Magical/TeamStoneHandler.cs new file mode 100644 index 0000000..4f1e54a --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Magical/TeamStoneHandler.cs @@ -0,0 +1,69 @@ +using System.Linq; +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Etc.Magical; + +public class TeamStoneHandler : IItemHandler +{ + public ItemType ItemType => ItemType.Magical; + public long[] Effects => new long[] { 300 }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + IPlayerEntity character = session.PlayerEntity; + + if (!character.IsInRaidParty) + { + return; + } + + if (!character.IsRaidLeader(character.Id)) + { + return; + } + + if (session.CurrentMapInstance.Portals.All(x => x.Type != PortalType.Raid)) + { + return; + } + + foreach (IClientSession member in character.Raid.Members) + { + if (member.PlayerEntity.Id == character.Id) + { + continue; + } + + if (!member.CurrentMapInstance.HasMapFlag(MapFlags.IS_BASE_MAP)) + { + continue; + } + + if (member.CurrentMapInstance?.Id == character.MapInstance.Id) + { + continue; + } + + if (!member.PlayerEntity.IsAlive()) + { + member.PlayerEntity.Hp = 1; + member.PlayerEntity.Mp = 1; + } + + Position randomPosition = character.MapInstance.GetRandomPosition(); + member.ChangeMap(session.PlayerEntity.MapId, randomPosition.X, randomPosition.Y); + } + + await session.RemoveItemFromInventory(item: e.Item); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Magical/TeleportationItemHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Magical/TeleportationItemHandler.cs new file mode 100644 index 0000000..f5b65da --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Magical/TeleportationItemHandler.cs @@ -0,0 +1,335 @@ +using System; +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.Core.Extensions; +using WingsEmu.DTOs.Maps; +using WingsEmu.DTOs.Respawns; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Act4; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Helpers; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Networking; +using WingsEmu.Game.RespawnReturn.Event; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Etc.Magical; + +/// +/// This handler is used for Return Wing, Return Amulet, Return Scroll, Miniland Bell, Lod Scroll +/// +public class TeleportationItemHandler : IItemHandler +{ + private const int ReturnWing = 0; + private const int ReturnAmulet = 1; + private const int MinilandBell = 2; + private const int LodScroll = 4; + private const int BaseTeleporter = 5; + private const int ReturnScroll = 6; + private readonly IAct4FlagManager _act4FlagManager; + + private readonly IDelayManager _delayManager; + private readonly IGameLanguageService _languageService; + private readonly IMapManager _mapManager; + + public TeleportationItemHandler(IGameLanguageService languageService, IDelayManager delayManager, IAct4FlagManager act4FlagManager, IMapManager mapManager) + { + _languageService = languageService; + _delayManager = delayManager; + _act4FlagManager = act4FlagManager; + _mapManager = mapManager; + } + + public ItemType ItemType => ItemType.Magical; + public long[] Effects => new long[] { 1 }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + IGameItem gameItem = e.Item.ItemInstance.GameItem; + + if (session.PlayerEntity.IsOnVehicle) + { + session.SendChatMessage(_languageService.GetLanguage(GameDialogKey.ITEM_CHATMESSAGE_CANT_USE_THAT, session.UserLanguage), ChatMessageColorType.Yellow); + return; + } + + if (!session.PlayerEntity.IsAlive()) + { + return; + } + + if (session.PlayerEntity.IsInExchange()) + { + return; + } + + if (session.PlayerEntity.HasShopOpened) + { + return; + } + + switch (gameItem.EffectValue) + { + /* + * RETURN WING & RETURN SCROLL (ACT 5) + */ + case ReturnWing or ReturnScroll when session.CantPerformActionOnAct4(): + return; + case ReturnWing or ReturnScroll: + { + DelayedActionType actionType = gameItem.EffectValue == ReturnWing ? DelayedActionType.ReturnWing : DelayedActionType.ReturnScroll; + + bool isAct5Map = session.CurrentMapInstance.HasMapFlag(MapFlags.ACT_5_1) || session.CurrentMapInstance.HasMapFlag(MapFlags.ACT_5_2); + + if (isAct5Map && gameItem.EffectValue == ReturnWing || !isAct5Map && gameItem.EffectValue == ReturnScroll) + { + session.SendChatMessage(_languageService.GetLanguage(GameDialogKey.ITEM_CHATMESSAGE_CANT_USE_THAT, session.UserLanguage), ChatMessageColorType.Yellow); + return; + } + + if (e.Option == 0) + { + session.SendDialog( + $"u_i 2 {session.PlayerEntity.Id} {(byte)e.Item.ItemInstance.GameItem.Type} {e.Item.Slot} 1", + $"u_i 2 {session.PlayerEntity.Id} {(byte)e.Item.ItemInstance.GameItem.Type} {e.Item.Slot} 2", + _languageService.GetLanguage(GameDialogKey.ITEM_DIALOG_WANT_TO_SAVE_POSITION, session.UserLanguage)); + return; + } + + if (!int.TryParse(e.Packet[6], out int usageType)) + { + return; + } + + switch (usageType) + { + case 1: + // 1 - Save position, 2 - Don't save position + case 2: + { + DateTime targetTime = await _delayManager.RegisterAction(session.PlayerEntity, actionType); + session.SendDelay(targetTime.GetTotalMillisecondUntilNow(), GuriType.UsingItem, + $"u_i 2 {session.PlayerEntity.Id} {(byte)e.Item.ItemInstance.GameItem.Type} {e.Item.Slot} {(usageType == 1 ? 3 : 4)}"); + return; + } + case 3: + case 4: + { + bool canPerformAction = await _delayManager.CanPerformAction(session.PlayerEntity, actionType); + if (!canPerformAction) + { + return; + } + + if (!session.PlayerEntity.MapInstance.HasMapFlag(MapFlags.IS_BASE_MAP)) + { + session.SendChatMessage(_languageService.GetLanguage(GameDialogKey.ITEM_CHATMESSAGE_CANT_USE_THAT, session.UserLanguage), ChatMessageColorType.Yellow); + return; + } + + await _delayManager.CompleteAction(session.PlayerEntity, actionType); + + if (usageType == 3 && session.PlayerEntity.MapId != 10000) + { + await session.EmitEventAsync(new ReturnChangeEvent + { + MapId = session.PlayerEntity.MapId, + MapX = session.PlayerEntity.PositionX, + MapY = session.PlayerEntity.PositionY + }); + } + + await session.Respawn(); + + await session.RemoveItemFromInventory(item: e.Item); + break; + } + } + + return; + } + /* + * RETURN AMULET + */ + case ReturnAmulet when session.CantPerformActionOnAct4(): + return; + case ReturnAmulet: + { + if (!int.TryParse(e.Packet[6], out int usageType)) + { + return; + } + + if (!session.PlayerEntity.MapInstance.HasMapFlag(MapFlags.IS_BASE_MAP)) + { + session.SendChatMessage(_languageService.GetLanguage(GameDialogKey.ITEM_CHATMESSAGE_CANT_USE_THAT, session.UserLanguage), ChatMessageColorType.Yellow); + return; + } + + CharacterReturnDto returnPoint = session.PlayerEntity.HomeComponent.Return; + + if (returnPoint == null) + { + session.PlayerEntity.HomeComponent.ChangeReturn(new CharacterReturnDto()); + return; + } + + IMapInstance returnMap = _mapManager.GetBaseMapInstanceByMapId(returnPoint.MapId); + + if (returnPoint.MapId != 0 && returnMap != null) + { + bool isAct5 = returnMap.HasMapFlag(MapFlags.ACT_5_1) || returnMap.HasMapFlag(MapFlags.ACT_5_2); + if (!session.IsInAct5() && isAct5) + { + session.SendChatMessage(_languageService.GetLanguage(GameDialogKey.ACT5_MESSAGE_RETURN_DISABLED, session.UserLanguage), ChatMessageColorType.Yellow); + return; + } + + if (session.IsInAct5() && !isAct5) + { + session.SendChatMessage(_languageService.GetLanguage(GameDialogKey.ACT5_MESSAGE_RETURN_DISABLED, session.UserLanguage), ChatMessageColorType.Yellow); + return; + } + } + + switch (usageType) + { + case 0: + { + if (returnPoint.MapId == 0) + { + return; + } + + session.SendRpPacket(returnPoint.MapId, returnPoint.MapX, returnPoint.MapY, $"#u_i^2^{session.PlayerEntity.Id}^{(byte)e.Item.ItemInstance.GameItem.Type}^{e.Item.Slot}^1"); + return; + } + case 1: + { + DateTime targetTime = await _delayManager.RegisterAction(session.PlayerEntity, DelayedActionType.ReturnAmulet); + session.SendDelay(targetTime.GetTotalMillisecondUntilNow(), GuriType.UsingItem, $"u_i 2 {session.PlayerEntity.Id} {(byte)e.Item.ItemInstance.GameItem.Type} {e.Item.Slot} 2"); + return; + } + case 2: + { + bool canUseReturnAmulet = await _delayManager.CanPerformAction(session.PlayerEntity, DelayedActionType.ReturnAmulet); + if (!canUseReturnAmulet) + { + return; + } + + await _delayManager.CompleteAction(session.PlayerEntity, DelayedActionType.ReturnAmulet); + + if (returnPoint.MapId == 0) + { + return; + } + + session.ChangeMap(returnPoint.MapId, returnPoint.MapX, returnPoint.MapY); + await session.RemoveItemFromInventory(item: e.Item); + break; + } + } + + return; + } + case BaseTeleporter: + if (!session.PlayerEntity.MapInstance.HasMapFlag(MapFlags.ACT_4)) + { + return; + } + + if (!session.PlayerEntity.MapInstance.HasMapFlag(MapFlags.IS_BASE_MAP)) + { + return; + } + + if (e.Option == 0) + { + DateTime targetTime = await _delayManager.RegisterAction(session.PlayerEntity, DelayedActionType.BaseTeleporter); + session.SendDelay(targetTime.GetTotalMillisecondUntilNow(), GuriType.UsingItem, $"u_i 2 {session.PlayerEntity.Id} {(byte)e.Item.ItemInstance.GameItem.Type} {e.Item.Slot} 1"); + return; + } + + bool canPerformActionAction = await _delayManager.CanPerformAction(session.PlayerEntity, DelayedActionType.BaseTeleporter); + if (!canPerformActionAction) + { + return; + } + + await _delayManager.CompleteAction(session.PlayerEntity, DelayedActionType.BaseTeleporter); + + FactionType faction = session.PlayerEntity.Faction; + MapLocation map = faction == FactionType.Angel ? _act4FlagManager.AngelFlag : _act4FlagManager.DemonFlag; + if (map == null) + { + return; + } + + session.ChangeMap(map.MapInstanceId, map.X, map.Y); + await session.RemoveItemFromInventory(item: e.Item); + + break; + /* + * MINILAND BELL & LOD SCROLL + */ + case MinilandBell: + case LodScroll: + { + if (!session.PlayerEntity.MapInstance.HasMapFlag(MapFlags.IS_BASE_MAP)) + { + session.SendChatMessage(_languageService.GetLanguage(GameDialogKey.ITEM_CHATMESSAGE_CANT_USE_THAT, session.UserLanguage), ChatMessageColorType.Yellow); + return; + } + + DelayedActionType actionType = gameItem.EffectValue == MinilandBell ? DelayedActionType.MinilandBell : DelayedActionType.LodScroll; + if (e.Option == 0) + { + DateTime targetTime = await _delayManager.RegisterAction(session.PlayerEntity, actionType); + session.SendDelay(targetTime.GetTotalMillisecondUntilNow(), GuriType.UsingItem, $"u_i 2 {session.PlayerEntity.Id} {(byte)e.Item.ItemInstance.GameItem.Type} {e.Item.Slot} 1"); + return; + } + + bool canPerformAction = await _delayManager.CanPerformAction(session.PlayerEntity, actionType); + if (!canPerformAction) + { + return; + } + + await _delayManager.CompleteAction(session.PlayerEntity, actionType); + + if (actionType != DelayedActionType.MinilandBell) + { + return; + } + + if (session.CurrentMapInstance.HasMapFlag(MapFlags.ACT_4)) + { + bool canEnter = session.PlayerEntity.Faction switch + { + FactionType.Angel => session.CurrentMapInstance.MapId == (int)MapIds.ACT4_ANGEL_CITADEL, + FactionType.Demon => session.CurrentMapInstance.MapId == (int)MapIds.ACT4_DEMON_CITADEL, + _ => false + }; + + if (!canEnter) + { + session.SendMsg(_languageService.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_MUST_BE_IN_CLASSIC_MAP, session.UserLanguage), MsgMessageType.Middle); + return; + } + } + + session.ChangeMap(session.PlayerEntity.Miniland); + await session.RemoveItemFromInventory(item: e.Item); + break; + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/ProduceItemHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/ProduceItemHandler.cs new file mode 100644 index 0000000..c3bc422 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/ProduceItemHandler.cs @@ -0,0 +1,66 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using PhoenixLib.Logging; +using WingsEmu.Game; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Managers.ServerData; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Etc; + +public class ProduceItemHandler : IItemHandler +{ + private readonly IItemUsageManager _itemUsageManager; + private readonly IRecipeManager _recipeManager; + + public ProduceItemHandler(IRecipeManager recipeManager, IItemUsageManager itemUsageManager) + { + _recipeManager = recipeManager; + _itemUsageManager = itemUsageManager; + } + + public ItemType ItemType => ItemType.Production; + public long[] Effects => new long[] { 100 }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + if (session.PlayerEntity.IsCraftingItem) + { + return; + } + + if (session.PlayerEntity.IsOnVehicle) + { + return; + } + + session.PlayerEntity.LastNRunId = 0; + _itemUsageManager.SetLastItemUsed(session.PlayerEntity.Id, e.Item.ItemInstance.GameItem.Id); + IReadOnlyList recipeList = _recipeManager.GetRecipesByProducerItemVnum(e.Item.ItemInstance.GameItem.Id); + + if (recipeList == null || !recipeList.Any()) + { + Log.Debug($"No Recipe Found: {ItemType}|{e.Item.ItemInstance.GameItem.ItemSubType}|{e.Item.ItemInstance.GameItem.Effect}|{e.Item.ItemInstance.GameItem.EffectValue}"); + return; + } + + session.SendWopenPacket((byte)WindowType.CRAFTING_ITEMS, e.Item.Slot, 0); + + if (e.Item.Slot == 0) // Entwell :pepega: + { + session.PlayerEntity.IsCraftingItem = false; + } + + session.SendRecipeItemList(recipeList, e.Item.ItemInstance.GameItem); + session.SendInventoryAddPacket(e.Item); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/RefinerItemHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/RefinerItemHandler.cs new file mode 100644 index 0000000..a535601 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/RefinerItemHandler.cs @@ -0,0 +1,107 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using PhoenixLib.Logging; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.Game; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Etc; + +public class CellaRefinerConfiguration : List +{ +} + +public class RefinerConfiguration +{ + public int ItemVnum { get; set; } + public short MinimumCella { get; set; } + public short MaximumCella { get; set; } + public short ChanceOfOptionalItem { get; set; } + + public List Items { get; set; } +} + +public class RefinerItem +{ + public int Vnum { get; set; } + public short Quantity { get; set; } + public int Chance { get; set; } +} + +public class RefinerItemHandler : IItemHandler +{ + private readonly CellaRefinerConfiguration _config; + private readonly IGameItemInstanceFactory _gameItemInstanceFactory; + private readonly IGameLanguageService _gameLanguage; + private readonly IItemsManager _itemsManager; + private readonly IRandomGenerator _randomGenerator; + + public RefinerItemHandler(IRandomGenerator randomGenerator, IGameLanguageService gameLanguage, IItemsManager itemsManager, IGameItemInstanceFactory gameItemInstanceFactory, + CellaRefinerConfiguration config) + { + _randomGenerator = randomGenerator; + _gameLanguage = gameLanguage; + _itemsManager = itemsManager; + _gameItemInstanceFactory = gameItemInstanceFactory; + _config = config; + } + + public ItemType ItemType => ItemType.Main; + public long[] Effects => new long[] { 10 }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + if (!session.PlayerEntity.HasItem((short)ItemVnums.GILLION)) + { + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.ITEM_CHATMESSAGE_NO_GILLION, session.UserLanguage), ChatMessageColorType.Yellow); + return; + } + + RefinerConfiguration config = _config.FirstOrDefault(s => s.ItemVnum == e.Item.ItemInstance.ItemVNum); + if (config == null) + { + Log.Error($"No configuration for Cella refiner: {e.Item.ItemInstance.ItemVNum}", new Exception($"No configuration for Cella refiner: {e.Item.ItemInstance.ItemVNum}")); + return; + } + + short amountOfCella = (short)_randomGenerator.RandomNumber(config.MinimumCella, config.MaximumCella); + GameItemInstance cella = _gameItemInstanceFactory.CreateItem((short)ItemVnums.CELLA, amountOfCella); + await session.AddNewItemToInventory(cella, true, ChatMessageColorType.Yellow, true); + await session.RemoveItemFromInventory((short)ItemVnums.GILLION); + await session.RemoveItemFromInventory(item: e.Item); + + + if (_randomGenerator.RandomNumber() > config.ChanceOfOptionalItem) + { + return; + } + + var randomBag = new RandomBag(_randomGenerator); + + foreach (RefinerItem item in config.Items) + { + randomBag.AddEntry(item, item.Chance); + } + + RefinerItem optionalItem = randomBag.GetRandom(); + + + GameItemInstance newItem = _gameItemInstanceFactory.CreateItem(optionalItem.Vnum, optionalItem.Quantity); + await session.AddNewItemToInventory(newItem, true, sendGiftIsFull: true); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Special/AncelloanBlessingHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Special/AncelloanBlessingHandler.cs new file mode 100644 index 0000000..04bcd66 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Special/AncelloanBlessingHandler.cs @@ -0,0 +1,46 @@ +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Etc.Special; + +public class AncelloanBlessingHandler : IItemHandler +{ + private const int CARD_ID = 121; + private readonly IBuffFactory _buffFactory; + private readonly IGameLanguageService _gameLanguage; + + public AncelloanBlessingHandler(IGameLanguageService gameLanguage, IBuffFactory buffFactory) + { + _gameLanguage = gameLanguage; + _buffFactory = buffFactory; + } + + public ItemType ItemType => ItemType.Special; + public long[] Effects => new long[] { 208 }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + InventoryItem inv = e.Item; + Buff buff121 = session.PlayerEntity.BuffComponent.GetBuff(CARD_ID); + if (buff121 != null) + { + string buffName = _gameLanguage.GetLanguage(GameDataType.Card, buff121.Name, session.UserLanguage); + session.SendChatMessage(_gameLanguage.GetLanguageFormat(GameDialogKey.ITEM_CHATMESSAGE_CANT_USE_TWICE, session.UserLanguage, buffName), ChatMessageColorType.Yellow); + return; + } + + await session.PlayerEntity.AddBuffAsync(_buffFactory.CreateOneHourBuff(session.PlayerEntity, CARD_ID, BuffFlag.BIG_AND_KEEP_ON_LOGOUT)); + await session.RemoveItemFromInventory(inv.ItemInstance.ItemVNum); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Special/FairyBoostHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Special/FairyBoostHandler.cs new file mode 100644 index 0000000..628c7dd --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Special/FairyBoostHandler.cs @@ -0,0 +1,57 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Entities.Extensions; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Etc.Special; + +public class FairyBoostHandler : IItemHandler +{ + private const int CARD_ID = 131; + private readonly IBuffFactory _buffFactory; + private readonly IAsyncEventPipeline _eventPipeline; + private readonly IGameLanguageService _gameLanguage; + + public FairyBoostHandler(IGameLanguageService gameLanguage, IAsyncEventPipeline eventPipeline, IBuffFactory buffFactory) + { + _gameLanguage = gameLanguage; + _eventPipeline = eventPipeline; + _buffFactory = buffFactory; + } + + public ItemType ItemType => ItemType.Special; + public long[] Effects => new long[] { 250 }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + InventoryItem inv = e.Item; + Buff buff = session.PlayerEntity.BuffComponent.GetBuff(CARD_ID); + if (buff != null) + { + string buffName = _gameLanguage.GetLanguage(GameDataType.Card, buff.Name, session.UserLanguage); + session.SendPacket(session.PlayerEntity.GenerateSayPacket(_gameLanguage.GetLanguageFormat(GameDialogKey.ITEM_CHATMESSAGE_CANT_USE_TWICE, session.UserLanguage, buffName), + ChatMessageColorType.Yellow)); + return; + } + + await session.RemoveItemFromInventory(inv.ItemInstance.ItemVNum); + await session.PlayerEntity.AddBuffAsync(_buffFactory.CreateOneHourBuff(session.PlayerEntity, CARD_ID, BuffFlag.BIG_AND_KEEP_ON_LOGOUT)); + session.BroadcastPairy(); + session.RefreshFairy(); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Special/GuardianAngelHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Special/GuardianAngelHandler.cs new file mode 100644 index 0000000..030f4e4 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Special/GuardianAngelHandler.cs @@ -0,0 +1,46 @@ +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Etc.Special; + +public class GuardianAngelHandler : IItemHandler +{ + private const int CARD_ID = 122; + private readonly IBuffFactory _buffFactory; + private readonly IGameLanguageService _gameLanguage; + + public GuardianAngelHandler(IGameLanguageService gameLanguage, IBuffFactory buffFactory) + { + _gameLanguage = gameLanguage; + _buffFactory = buffFactory; + } + + public ItemType ItemType => ItemType.Special; + public long[] Effects => new long[] { 210 }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + InventoryItem inv = e.Item; + Buff buff = session.PlayerEntity.BuffComponent.GetBuff(CARD_ID); + if (buff != null) + { + string buffName = _gameLanguage.GetLanguage(GameDataType.Card, buff.Name, session.UserLanguage); + session.SendChatMessage(_gameLanguage.GetLanguageFormat(GameDialogKey.ITEM_CHATMESSAGE_CANT_USE_TWICE, session.UserLanguage, buffName), ChatMessageColorType.Yellow); + return; + } + + await session.PlayerEntity.AddBuffAsync(_buffFactory.CreateOneHourBuff(session.PlayerEntity, CARD_ID, BuffFlag.BIG_AND_KEEP_ON_LOGOUT)); + await session.RemoveItemFromInventory(inv.ItemInstance.ItemVNum); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Special/IceFlowerOilHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Special/IceFlowerOilHandler.cs new file mode 100644 index 0000000..6800471 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Special/IceFlowerOilHandler.cs @@ -0,0 +1,50 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.Game._enum; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Etc.Special; + +public class IceFlowerOilHandler : IItemUsageByVnumHandler +{ + private readonly IBuffFactory _buffFactory; + + public IceFlowerOilHandler(IBuffFactory buffFactory) => _buffFactory = buffFactory; + + public long[] Vnums => new[] + { + (long)ItemVnums.STRONG_ICE_FLOWER_OIL, (long)ItemVnums.ICE_FLOWER_OIL, + (long)ItemVnums.LARGE_HEAT_RESISTANCE_POTION, (long)ItemVnums.HEAT_RESISTANCE_POTION + }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + TimeSpan duration = TimeSpan.Zero; + switch (e.Item.ItemInstance.ItemVNum) + { + case (int)ItemVnums.LARGE_HEAT_RESISTANCE_POTION: + case (int)ItemVnums.STRONG_ICE_FLOWER_OIL: + duration = TimeSpan.FromHours(2); + break; + case (int)ItemVnums.HEAT_RESISTANCE_POTION: + case (int)ItemVnums.ICE_FLOWER_OIL: + duration = TimeSpan.FromMinutes(10); + break; + } + + await session.PlayerEntity.AddBuffAsync(_buffFactory.CreateBuff((short)BuffVnums.ICE_FLOWER, + session.PlayerEntity, duration, BuffFlag.BIG_AND_KEEP_ON_LOGOUT, true)); + Buff buff = session.PlayerEntity.BuffComponent.GetBuff((short)BuffVnums.ACT_52_FIRE_DEBUFF); + await session.PlayerEntity.RemoveBuffAsync(true, buff); + await session.RemoveItemFromInventory(item: e.Item); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Special/LuiniaHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Special/LuiniaHandler.cs new file mode 100644 index 0000000..db2be76 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Special/LuiniaHandler.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.Game._enum; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Etc.Special; + +public class LuiniaHandler : IItemUsageByVnumHandler +{ + public long[] Vnums => new[] { (long)ItemVnums.LUINIA_OF_RESTORATION }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + session.BroadcastEffectInRange(EffectType.AngelDignityRestore); + await session.RemoveItemFromInventory(item: e.Item, amount: 1); + + if (!session.PlayerEntity.TimeSpaceComponent.IsInTimeSpaceParty) + { + return; + } + + if (session.CurrentMapInstance.MapInstanceType != MapInstanceType.TimeSpaceInstance) + { + return; + } + + IReadOnlyList npcs = session.CurrentMapInstance.GetClosestNpcsInRange(session.PlayerEntity.Position, 10); + foreach (INpcEntity npc in npcs) + { + if (!npc.IsProtected && !npc.IsTimeSpaceMate) + { + continue; + } + + npc.BroadcastEffectInRange(EffectType.ShinyDust); + if (npc.MaxHp == npc.Hp) + { + continue; + } + + int hpToAdd = (int)(1000 + (npc.MaxHp - npc.Hp) * 0.3); + await npc.EmitEventAsync(new BattleEntityHealEvent + { + Entity = npc, + HpHeal = hpToAdd + }); + + session.SendStPacket(npc); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Special/RaidSealHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Special/RaidSealHandler.cs new file mode 100644 index 0000000..9364b56 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Special/RaidSealHandler.cs @@ -0,0 +1,23 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Threading.Tasks; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Raids.Events; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Etc.Special; + +public class RaidSealHandler : IItemHandler +{ + public ItemType ItemType => ItemType.Special; + public long[] Effects => new long[] { 301 }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + await session.EmitEventAsync(new RaidPartyCreateEvent((byte)e.Item.ItemInstance.GameItem.EffectValue, e.Item)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Special/ReinitializeItemHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Special/ReinitializeItemHandler.cs new file mode 100644 index 0000000..246b9b0 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Special/ReinitializeItemHandler.cs @@ -0,0 +1,176 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsAPI.Game.Extensions.PacketGeneration; +using WingsAPI.Packets.Enums; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Extensions.Mates; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Networking.Broadcasting; +using WingsEmu.Game.Skills; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Etc.Special; + +public class ReinitializeItemHandler : IItemUsageByVnumHandler +{ + private readonly IDelayManager _delayManager; + private readonly IGameLanguageService _gameLanguage; + private readonly ISessionManager _sessionManager; + + public ReinitializeItemHandler(IGameLanguageService gameLanguage, ISessionManager sessionManager, IDelayManager delayManager) + { + _gameLanguage = gameLanguage; + _sessionManager = sessionManager; + _delayManager = delayManager; + } + + public long[] Effects => new long[] { 11111 }; + + public long[] Vnums => new[] { (long)ItemVnums.PARTNER_SKILL_TICKET_SINGLE, (long)ItemVnums.PARTNER_SKILL_TICKET_SINGLE_LIMITED }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + byte option = e.Option; + IMateEntity partner = session.PlayerEntity.MateComponent.GetMate(s => s.IsTeamMember && s.MateType == MateType.Partner); + string[] packetsplit = e.Packet; + InventoryItem inv = e.Item; + + if (packetsplit == null) + { + _sessionManager.BroadcastToGameMaster(session, "ReinitializeItemHandler - packetsplit == null"); + // Packet Hacking + return; + } + + if (packetsplit.Length < 9) + { + _sessionManager.BroadcastToGameMaster(session, "ReinitializeItemHandler - packetsplit.Length < 9"); + // Packet hacking + return; + } + + if (!byte.TryParse(packetsplit[9], out byte skillSlot)) + { + // out of range + return; + } + + if (!Enum.TryParse(packetsplit[8], out EquipmentType sEqpType)) + { + // Out of range + return; + } + + if (!byte.TryParse(packetsplit[6], out byte sRequest)) + { + return; + } + + if (partner == null) + { + session.SendModal(_gameLanguage.GetLanguage(GameDialogKey.PARTNER_INFO_NO_PARTNER_IN_TEAM, session.UserLanguage), ModalType.Confirm); + return; + } + + if (partner.Specialist == null) + { + session.SendModal(_gameLanguage.GetLanguage(GameDialogKey.PARTNER_MESSAGE_NO_SP_EQUIPPED, session.UserLanguage), ModalType.Confirm); + return; + } + + if (partner.Specialist.PartnerSkills == null) + { + return; + } + + if (partner.IsUsingSp) + { + session.SendModal(_gameLanguage.GetLanguage(GameDialogKey.PARTNER_INFO_IS_WEARING_SP, session.UserLanguage), ModalType.Confirm); + return; + } + + if (!partner.HavePartnerSkill(skillSlot)) + { + return; + } + + List partnerSkills = partner.Specialist.PartnerSkills; + PartnerSkill skill = partnerSkills.Find(s => s.Slot == skillSlot); + int skillIndex = partnerSkills.FindIndex(i => i.Slot == skillSlot); + + if (skill == null) + { + return; + } + + if (sRequest == 3) + { + bool canReset = await _delayManager.CanPerformAction(session.PlayerEntity, DelayedActionType.PartnerResetSkill); + if (!canReset) + { + return; + } + + await _delayManager.CompleteAction(session.PlayerEntity, DelayedActionType.PartnerResetSkill); + + switch (skillSlot) + { + case 0: + + partner.Specialist.PartnerSkill1 = false; + partner.Specialist.SkillRank1 = 0; + break; + case 1: + + partner.Specialist.PartnerSkill2 = false; + partner.Specialist.SkillRank2 = 0; + break; + case 2: + + partner.Specialist.PartnerSkill3 = false; + partner.Specialist.SkillRank3 = 0; + break; + default: + _sessionManager.BroadcastToGameMaster(session, "ReinitializeItemHandler - switch (sPos) default"); + // Packet Hacking + return; + } + + skill.Rank = 0; + partner.Specialist.PartnerSkills.RemoveAt(skillIndex); + partner.Specialist.Agility = 100; + session.SendModal(_gameLanguage.GetLanguage(GameDialogKey.PSP_MESSAGE_ONE_SKILL_RESET, session.UserLanguage), ModalType.Confirm); + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.PSP_MESSAGE_ONE_SKILL_RESET, session.UserLanguage), ChatMessageColorType.Yellow); + session.SendPetInfo(partner, _gameLanguage); + await session.RemoveItemFromInventory(inv.ItemInstance.ItemVNum); + return; + } + + if (option == 0) + { + session.SendQnaPacket($"u_i 1 {session.PlayerEntity.Id} {(short)inv.ItemInstance.GameItem.Type} {inv.Slot} 1 1 {(short)sEqpType} {skillSlot}", + _gameLanguage.GetLanguage(GameDialogKey.PSP_DIALOG_ASK_RESET_SINGLE_SKILL, session.UserLanguage)); + } + else if (option == 255) + { + DateTime waitUntil = await _delayManager.RegisterAction(session.PlayerEntity, DelayedActionType.PartnerResetSkill); + session.SendMateDelay(partner, (int)(waitUntil - DateTime.UtcNow).TotalMilliseconds, GuriType.UsingItem, + $"#u_i^1^{session.PlayerEntity.Id}^{(short)inv.ItemInstance.GameItem.Type}^{inv.Slot}^3^1^{(short)sEqpType}^{skillSlot}"); + session.CurrentMapInstance?.Broadcast(partner.GenerateMateDance(), new RangeBroadcast(partner.PositionX, partner.PositionY)); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Special/ReinitializePartnerSpAllSkillsHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Special/ReinitializePartnerSpAllSkillsHandler.cs new file mode 100644 index 0000000..c4c1be7 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Special/ReinitializePartnerSpAllSkillsHandler.cs @@ -0,0 +1,145 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsAPI.Game.Extensions.PacketGeneration; +using WingsAPI.Packets.Enums; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Extensions.Mates; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Networking.Broadcasting; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Etc.Special; + +public class ReinitializePartnerSpAllSkillsHandler : IItemUsageByVnumHandler +{ + private readonly IDelayManager _delayManager; + private readonly IGameLanguageService _gameLanguage; + private readonly ISessionManager _sessionManager; + + public ReinitializePartnerSpAllSkillsHandler(IGameLanguageService languageService, ISessionManager sessionManager, IDelayManager delayManager) + { + _gameLanguage = languageService; + _sessionManager = sessionManager; + _delayManager = delayManager; + } + + public long[] Vnums => new[] { (long)ItemVnums.PARTNER_SKILL_TICKET_ALL, (long)ItemVnums.PARTNER_SKILL_TICKET_ALL_LIMITED }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + byte option = e.Option; + InventoryItem inv = e.Item; + IMateEntity partner = session.PlayerEntity.MateComponent.GetMate(s => s.IsTeamMember && s.MateType == MateType.Partner); + string[] packetsplit = e.Packet; + + if (packetsplit == null) + { + _sessionManager.BroadcastToGameMaster(session, "ReinitializePartnerSpAllSkills - packetsplit == null"); + // Packet Hacking + return; + } + + if (packetsplit.Length < 9) + { + _sessionManager.BroadcastToGameMaster(session, "ReinitializePartnerSpAllSkills - packetsplit.Length < 9"); + // Packet hacking + return; + } + + if (!byte.TryParse(packetsplit[9], out byte skillSlot)) + { + // out of range + return; + } + + if (!Enum.TryParse(packetsplit[8], out EquipmentType eqpType)) + { + // Out of range + return; + } + + if (!byte.TryParse(packetsplit[6], out byte request)) + { + return; + } + + if (partner == null) + { + session.SendModal(_gameLanguage.GetLanguage(GameDialogKey.PARTNER_INFO_NO_PARTNER_IN_TEAM, session.UserLanguage), ModalType.Confirm); + return; + } + + if (partner.Specialist == null) + { + session.SendModal(_gameLanguage.GetLanguage(GameDialogKey.PARTNER_MESSAGE_NO_SP_EQUIPPED, session.UserLanguage), ModalType.Confirm); + return; + } + + if (partner.Specialist.PartnerSkills == null) + { + return; + } + + if (partner.IsUsingSp) + { + session.SendModal(_gameLanguage.GetLanguage(GameDialogKey.PARTNER_INFO_IS_WEARING_SP, session.UserLanguage), ModalType.Confirm); + return; + } + + if (!partner.HavePartnerSkill(skillSlot)) + { + return; + } + + if (request == 3) + { + bool canReset = await _delayManager.CanPerformAction(partner, DelayedActionType.PartnerResetAllSkills); + if (!canReset) + { + return; + } + + partner.Specialist.PartnerSkill1 = false; + partner.Specialist.PartnerSkill2 = false; + partner.Specialist.PartnerSkill3 = false; + partner.Specialist.SkillRank1 = 0; + partner.Specialist.SkillRank2 = 0; + partner.Specialist.SkillRank3 = 0; + partner.Specialist.Agility = 100; + partner.Specialist.PartnerSkills.Clear(); + session.SendModal(_gameLanguage.GetLanguage(GameDialogKey.PSP_MESSAGE_ALL_SKILLS_RESET, session.UserLanguage), ModalType.Confirm); + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.PSP_MESSAGE_ALL_SKILLS_RESET, session.UserLanguage), ChatMessageColorType.Yellow); + session.SendPetInfo(partner, _gameLanguage); + await session.RemoveItemFromInventory(inv.ItemInstance.ItemVNum); + return; + } + + if (option == 0) + { + session.SendQnaPacket($"u_i 1 {session.PlayerEntity.Id} {(short)inv.ItemInstance.GameItem.Type} {inv.Slot} 1 1 {(short)eqpType} {skillSlot}", + _gameLanguage.GetLanguage(GameDialogKey.PSP_DIALOG_ASK_RESET_ALL_SKILLS, session.UserLanguage)); + return; + } + + if (option == 255) + { + DateTime waitUntil = await _delayManager.RegisterAction(partner, DelayedActionType.PartnerResetAllSkills); + session.SendMateDelay(partner, (int)(waitUntil - DateTime.UtcNow).TotalMilliseconds, GuriType.UsingItem, + $"#u_i^1^{session.PlayerEntity.Id}^{(short)inv.ItemInstance.GameItem.Type}^{inv.Slot}^3^1^{(short)eqpType}^{skillSlot}"); + session.CurrentMapInstance?.Broadcast(partner.GenerateMateDance(), new RangeBroadcast(partner.PositionX, partner.PositionY)); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Teacher/IncreaseLevelPartnerHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Teacher/IncreaseLevelPartnerHandler.cs new file mode 100644 index 0000000..6f7f6f8 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Teacher/IncreaseLevelPartnerHandler.cs @@ -0,0 +1,72 @@ +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.Groups; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Configuration; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Algorithm; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Extensions.Mates; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Etc.Teacher; + +public class IncreaseLevelPartnerHandler : IItemHandler +{ + private readonly IBattleEntityAlgorithmService _algorithm; + + private readonly IGameLanguageService _gameLanguage; + private readonly ISpPartnerConfiguration _spPartnerConfiguration; + + public IncreaseLevelPartnerHandler(IGameLanguageService gameLanguage, IBattleEntityAlgorithmService algorithm, ISpPartnerConfiguration spPartnerConfiguration) + { + _gameLanguage = gameLanguage; + _algorithm = algorithm; + _spPartnerConfiguration = spPartnerConfiguration; + } + + public ItemType ItemType => ItemType.PetPartnerItem; + public long[] Effects => new long[] { 12 }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + if (!int.TryParse(e.Packet[3], out int petId)) + { + return; + } + + IMateEntity mateEntity = session.PlayerEntity.MateComponent.GetMate(s => s.Id == petId && s.MateType == MateType.Partner); + if (mateEntity == null || mateEntity.Level >= session.PlayerEntity.Level - 5) + { + return; + } + + if (!mateEntity.IsAlive()) + { + return; + } + + if (session.PlayerEntity.IsOnVehicle) + { + return; + } + + int loyalty = mateEntity.Loyalty + 100 > 1000 ? 1000 - mateEntity.Loyalty : 100; + mateEntity.Loyalty += (short)loyalty; + mateEntity.Experience = 0; + mateEntity.Level++; + mateEntity.RefreshMaxHpMp(_algorithm); + session.RefreshParty(_spPartnerConfiguration); + mateEntity.Hp = mateEntity.MaxHp; + mateEntity.Mp = mateEntity.MaxMp; + session.SendPetInfo(mateEntity, _gameLanguage); + await session.RemoveItemFromInventory(item: e.Item); + mateEntity.BroadcastEffectInRange(EffectType.NormalLevelUp); + mateEntity.BroadcastEffectInRange(EffectType.NormalLevelUpSubEffect); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Teacher/IncreaseLevelPetFoodHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Teacher/IncreaseLevelPetFoodHandler.cs new file mode 100644 index 0000000..c84d833 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Teacher/IncreaseLevelPetFoodHandler.cs @@ -0,0 +1,82 @@ +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.Groups; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsAPI.Packets.Enums; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Configuration; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Algorithm; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Extensions.Mates; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Etc.Teacher; + +public class IncreaseLevelPetFoodHandler : IItemHandler +{ + private readonly IBattleEntityAlgorithmService _algorithm; + + private readonly IGameLanguageService _gameLanguage; + private readonly ISpPartnerConfiguration _spPartnerConfiguration; + + public IncreaseLevelPetFoodHandler(IGameLanguageService gameLanguage, IBattleEntityAlgorithmService algorithm, ISpPartnerConfiguration spPartnerConfiguration) + { + _gameLanguage = gameLanguage; + _algorithm = algorithm; + _spPartnerConfiguration = spPartnerConfiguration; + } + + public ItemType ItemType => ItemType.PetPartnerItem; + public long[] Effects => new long[] { 11 }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + if (!int.TryParse(e.Packet[3], out int petId)) + { + return; + } + + IMateEntity mateEntity = session.PlayerEntity.MateComponent.GetMate(s => s.Id == petId && s.MateType == MateType.Pet); + if (mateEntity == null || mateEntity.Level >= session.PlayerEntity.Level - 5) + { + return; + } + + if (!mateEntity.IsAlive()) + { + return; + } + + if (session.PlayerEntity.IsOnVehicle) + { + return; + } + + int loyalty = mateEntity.Loyalty + 100 > 1000 ? 1000 - mateEntity.Loyalty : 100; + mateEntity.Loyalty += (short)loyalty; + mateEntity.Experience = 0; + mateEntity.Level++; + mateEntity.RefreshMaxHpMp(_algorithm); + session.RefreshParty(_spPartnerConfiguration); + mateEntity.Hp = mateEntity.MaxHp; + mateEntity.Mp = mateEntity.MaxMp; + session.SendPetInfo(mateEntity, _gameLanguage); + await session.RemoveItemFromInventory(item: e.Item); + mateEntity.BroadcastEffectInRange(EffectType.NormalLevelUp); + mateEntity.BroadcastEffectInRange(EffectType.NormalLevelUpSubEffect); + + await session.EmitEventAsync(new LevelUpMateEvent + { + Level = mateEntity.Level, + LevelUpType = MateLevelUpType.ItemUsed, + ItemVnum = e.Item.ItemInstance.ItemVNum, + NosMateMonsterVnum = mateEntity.NpcMonsterVNum + }); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Teacher/MateFoodHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Teacher/MateFoodHandler.cs new file mode 100644 index 0000000..46c684c --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Teacher/MateFoodHandler.cs @@ -0,0 +1,61 @@ +using System.Threading.Tasks; +using PhoenixLib.Logging; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Extensions.Mates; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Etc.Teacher; + +public class MateFoodHandler : IItemHandler +{ + private readonly IGameLanguageService _gameLanguage; + + public MateFoodHandler(IGameLanguageService gameLanguage) => _gameLanguage = gameLanguage; + + public ItemType ItemType => ItemType.PetPartnerItem; + public long[] Effects => new long[] { 10 }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + if (!long.TryParse(e.Packet[3], out long mateId)) + { + Log.Debug("Mate not found"); + return; + } + + IMateEntity mate = session.PlayerEntity.MateComponent.GetMate(m => m.Id == mateId && m.MateType == MateType.Pet); + + if (mate == null) + { + return; + } + + if (!mate.IsAlive()) + { + return; + } + + if (mate.Loyalty == 1000) + { + return; + } + + int loyalty = mate.Loyalty + 100 > 1000 ? 1000 - mate.Loyalty : 100; + mate.Loyalty += (short)loyalty; + session.SendCondMate(mate); + session.SendPetInfo(mate, _gameLanguage); + session.SendMateEffect(mate, EffectType.PetLove); + session.SendMateEffect(mate, EffectType.ShinyStars); + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.PET_CHATMESSAGE_EAT_EVERYTHING, session.UserLanguage), ChatMessageColorType.Yellow); + await session.RemoveItemFromInventory(item: e.Item); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Teacher/MateGuriHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Teacher/MateGuriHandler.cs new file mode 100644 index 0000000..78749cf --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Teacher/MateGuriHandler.cs @@ -0,0 +1,30 @@ +using System.Threading.Tasks; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Etc.Teacher; + +public class MateGuriHandler : IItemHandler +{ + public ItemType ItemType => ItemType.PetPartnerItem; + + public long[] Effects => new long[] { 13 }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + if (!int.TryParse(e.Packet[3], out int x1)) + { + return; + } + + if (session.PlayerEntity.MateComponent.GetMate(x => x.Id == x1) == null) + { + return; + } + + session.SendGuriPacket(10, 1, x1); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Teacher/MateReleaseHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Teacher/MateReleaseHandler.cs new file mode 100644 index 0000000..d14dd48 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Teacher/MateReleaseHandler.cs @@ -0,0 +1,51 @@ +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Mates.Events; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Etc.Teacher; + +public class MateReleaseHandler : IItemHandler +{ + private readonly IGameLanguageService _gameLanguage; + + public MateReleaseHandler(IGameLanguageService gameLanguageService) => _gameLanguage = gameLanguageService; + + public ItemType ItemType => ItemType.PetPartnerItem; + public long[] Effects => new long[] { 1000 }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + if (!int.TryParse(e.Packet[3], out int x1)) + { + return; + } + + IMateEntity mateEntity = session.PlayerEntity.MateComponent.GetMate(s => s.Id == x1 && s.MateType == MateType.Pet); + if (mateEntity == null) + { + return; + } + + if (mateEntity.IsTeamMember) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_PET_IN_TEAM_UNRELEASABLE, e.Sender.UserLanguage), MsgMessageType.Middle); + return; + } + + await session.EmitEventAsync(new MateRemoveEvent + { + MateEntity = mateEntity + }); + + session.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.PET_INFO_RELEASED, e.Sender.UserLanguage)); + await session.RemoveItemFromInventory(item: e.Item); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Teacher/NosMateTrainerHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Teacher/NosMateTrainerHandler.cs new file mode 100644 index 0000000..57352da --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Teacher/NosMateTrainerHandler.cs @@ -0,0 +1,104 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Monster.Event; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Etc.Teacher; + +public class NosMateTrainerHandler : IItemHandler +{ + private const int MAX_DOLLS = 10; + private readonly IAsyncEventPipeline _eventPipeline; + private readonly IGameLanguageService _gameLanguage; + private readonly INpcMonsterManager _monsterManager; + + public NosMateTrainerHandler(IGameLanguageService gameLanguage, INpcMonsterManager monsterManager, IAsyncEventPipeline eventPipeline) + { + _gameLanguage = gameLanguage; + _monsterManager = monsterManager; + _eventPipeline = eventPipeline; + } + + public ItemType ItemType => ItemType.PetPartnerItem; + public long[] Effects => new long[] { 21, 20 }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + if (!session.PlayerEntity.IsAlive()) + { + return; + } + + if (session.PlayerEntity.IsInExchange()) + { + return; + } + + if (session.PlayerEntity.IsOnVehicle) + { + return; + } + + if (session.PlayerEntity.HasShopOpened) + { + return; + } + + int monsterVnum = e.Item.ItemInstance.GameItem.EffectValue; + + IMonsterData monster = _monsterManager.GetNpc(monsterVnum); + if (monster == null) + { + return; + } + + if (session.PlayerEntity.IsInMateDollZone()) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.DOLL_SHOUTMESSAGE_NOT_IN_ZONE, session.UserLanguage), MsgMessageType.Middle); + return; + } + + int dolls = session.CurrentMapInstance.GetAliveMonsters(x => x != null && x.IsAlive() && x.SummonerId == session.PlayerEntity.Id && x.SummonerType == VisualType.Player && x.IsMateTrainer) + .Count; + if (dolls >= MAX_DOLLS) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.DOLL_SHOUTMESSAGE_DOLLS_LIMIT, session.UserLanguage), MsgMessageType.Middle); + return; + } + + FactionType factionType = FactionType.Neutral; + if (session.CurrentMapInstance.HasMapFlag(MapFlags.ACT_4)) + { + factionType = session.PlayerEntity.Faction == FactionType.Angel ? FactionType.Demon : FactionType.Angel; + } + + var listToSummon = new List + { + new() + { + VNum = (short)monsterVnum, + SpawnCell = session.PlayerEntity.Position, + IsMoving = false, + IsMateTrainer = true, + FactionType = factionType + } + }; + + await _eventPipeline.ProcessEventAsync(new MonsterSummonEvent(session.CurrentMapInstance, listToSummon, session.PlayerEntity, false)); + await session.RemoveItemFromInventory(item: e.Item); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Teacher/PartnerFoodHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Teacher/PartnerFoodHandler.cs new file mode 100644 index 0000000..2657b8e --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Teacher/PartnerFoodHandler.cs @@ -0,0 +1,65 @@ +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Extensions.Mates; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Etc.Teacher; + +public class PartnerFoodHandler : IItemHandler +{ + private readonly IGameLanguageService _gameLanguage; + private readonly IItemUsageManager _itemUsageManager; + + public PartnerFoodHandler(IItemUsageManager itemUsageManager, IGameLanguageService gameLanguage) + { + _itemUsageManager = itemUsageManager; + _gameLanguage = gameLanguage; + } + + public ItemType ItemType => ItemType.PetPartnerItem; + public long[] Effects => new long[] { 10002 }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + if (!long.TryParse(e.Packet[3], out long mateId)) + { + return; + } + + IMateEntity mate = session.PlayerEntity.MateComponent.GetMate(m => m.Id == mateId && m.MateType == MateType.Partner); + + if (mate == null) + { + return; + } + + if (!mate.IsAlive()) + { + return; + } + + if (mate.Loyalty == 1000) + { + return; + } + + int loyalty = mate.Loyalty + 100 > 1000 ? 1000 - mate.Loyalty : 100; + mate.Loyalty += (short)loyalty; + session.SendCondMate(mate); + session.SendPetInfo(mate, _gameLanguage); + session.SendMateEffect(mate, EffectType.PetLove); + session.SendMateEffect(mate, EffectType.ShinyStars); + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.PARTNER_CHATMESSAGE_EAT_EVERYTHING, session.UserLanguage), ChatMessageColorType.Yellow); + await session.RemoveItemFromInventory(item: e.Item); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Teacher/PartnerReleaseHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Teacher/PartnerReleaseHandler.cs new file mode 100644 index 0000000..3d24048 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Teacher/PartnerReleaseHandler.cs @@ -0,0 +1,58 @@ +using System.Linq; +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Mates.Events; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Etc.Teacher; + +public class PartnerReleaseHandler : IItemHandler +{ + private readonly IGameLanguageService _gameLanguage; + + public PartnerReleaseHandler(IGameLanguageService gameLanguage) => _gameLanguage = gameLanguage; + + public ItemType ItemType => ItemType.PetPartnerItem; + public long[] Effects => new long[] { 1001 }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + if (!int.TryParse(e.Packet[3], out int x1)) + { + return; + } + + IMateEntity mateEntity = session.PlayerEntity.MateComponent.GetMate(s => s.Id == x1 && s.MateType == MateType.Partner); + if (mateEntity == null) + { + return; + } + + if (mateEntity.IsTeamMember) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_PARTNER_IN_TEAM_UNRELEASABLE, e.Sender.UserLanguage), MsgMessageType.Middle); + return; + } + + if (session.PlayerEntity.PartnerGetEquippedItems(mateEntity.PetSlot).Any(x => x != null)) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_PARTNER_EQ_UNRELEASABLE, e.Sender.UserLanguage), MsgMessageType.Middle); + return; + } + + await session.EmitEventAsync(new MateRemoveEvent + { + MateEntity = mateEntity + }); + + session.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.PARTNER_INFO_RELEASED, e.Sender.UserLanguage)); + await session.RemoveItemFromInventory(item: e.Item); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Teacher/PetSummoningScrollHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Teacher/PetSummoningScrollHandler.cs new file mode 100644 index 0000000..a95624e --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Teacher/PetSummoningScrollHandler.cs @@ -0,0 +1,54 @@ +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Etc.Teacher; + +public class PetSummoningScrollHandler : IItemHandler +{ + private readonly IGameLanguageService _gameLanguage; + + public PetSummoningScrollHandler(IGameLanguageService gameLanguage) => _gameLanguage = gameLanguage; + + public ItemType ItemType => ItemType.PetPartnerItem; + + public long[] Effects => new long[] { 17 }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + if (!int.TryParse(e.Packet[3], out int x1)) + { + return; + } + + IMateEntity mateEntity = session.PlayerEntity.MateComponent.GetMate(s => s.Id == x1); + if (mateEntity == null) + { + return; + } + + if (mateEntity.IsSummonable) + { + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.PET_MESSAGE_IS_ALREADY_SUMMONABLE, e.Sender.UserLanguage), ChatMessageColorType.Yellow); + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.PET_MESSAGE_IS_ALREADY_SUMMONABLE, e.Sender.UserLanguage), MsgMessageType.Middle); + return; + } + + await session.RemoveItemFromInventory(item: e.Item); + mateEntity.IsSummonable = true; + string mateName = string.IsNullOrEmpty(mateEntity.MateName) || mateEntity.MateName == mateEntity.Name + ? _gameLanguage.GetLanguage(GameDataType.NpcMonster, mateEntity.Name, session.UserLanguage) + : mateEntity.MateName; + session.SendScpPackets(); + session.SendScnPackets(); + session.SendChatMessage(_gameLanguage.GetLanguageFormat(GameDialogKey.PET_MESSAGE_SUMMONABLE, e.Sender.UserLanguage, mateName), ChatMessageColorType.Yellow); + session.SendMsg(_gameLanguage.GetLanguageFormat(GameDialogKey.PET_MESSAGE_SUMMONABLE, e.Sender.UserLanguage, mateName), MsgMessageType.Middle); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Teacher/PickUpPetFoodHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Teacher/PickUpPetFoodHandler.cs new file mode 100644 index 0000000..c924a14 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Teacher/PickUpPetFoodHandler.cs @@ -0,0 +1,59 @@ +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Etc.Teacher; + +public class PickUpPetFoodHandler : IItemHandler +{ + private readonly IGameLanguageService _gameLanguage; + + public PickUpPetFoodHandler(IGameLanguageService gameLanguage) => _gameLanguage = gameLanguage; + + public ItemType ItemType => ItemType.PetPartnerItem; + public long[] Effects => new long[] { 14 }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + if (e.Packet.Length < 4) + { + return; + } + + if (!int.TryParse(e.Packet[3], out int x1)) + { + return; + } + + IMateEntity mateEntity = session.PlayerEntity.MateComponent.GetMate(s => s.Id == x1 && s.MateType == MateType.Pet); + + if (mateEntity == null) + { + return; + } + + if (mateEntity.CanPickUp) + { + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.PET_CHATMESSAGE_ALREADY_CAN_PICK_UP, session.UserLanguage), ChatMessageColorType.Red); + return; + } + + session.SendPacket(mateEntity.GenerateEffectPacket(EffectType.ShinyStars)); + session.SendPacket(mateEntity.GenerateEffectPacket(EffectType.PetLove)); + + mateEntity.CanPickUp = true; + + session.SendScpPackets(); + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.PET_CHATMESSAGE_CAN_PICK_UP, e.Sender.UserLanguage), ChatMessageColorType.Yellow); + + await session.RemoveItemFromInventory(item: e.Item); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Teacher/SteelNetHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Teacher/SteelNetHandler.cs new file mode 100644 index 0000000..4bc3ed2 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Teacher/SteelNetHandler.cs @@ -0,0 +1,92 @@ +using System; +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Etc.Teacher; + +public class SteelNetHandler : IItemHandler +{ + public ItemType ItemType => ItemType.PetPartnerItem; + public long[] Effects => new long[] { 10001 }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + if (session.PlayerEntity == null) + { + return; + } + + IMonsterEntity monsterEntityToCapture = session.CurrentMapInstance?.GetMonsterById(session.PlayerEntity.LastEntity.Item2); + + if (monsterEntityToCapture == null) + { + return; + } + + int dist = session.PlayerEntity.GetDistance(monsterEntityToCapture); + if (dist > 2) + { + return; + } + + if (session.PlayerEntity.LastMonsterCaught.AddSeconds(2) > DateTime.UtcNow) + { + return; + } + + IPlayerEntity playerEntity = session.PlayerEntity; + + if (monsterEntityToCapture.Level > playerEntity.Level) + { + session.SendMsg(session.GetLanguage(GameDialogKey.SKILL_SHOUTMESSAGE_MONSTER_LEVEL_MUST_BE_LOWER_THAN_YOURS), MsgMessageType.Middle); + return; + } + + if (playerEntity.MapInstance.MapInstanceType == MapInstanceType.RaidInstance) + { + session.SendMsg(session.GetLanguage(GameDialogKey.SKILL_SHOUTMESSAGE_CAPTURE_IN_RAID), MsgMessageType.Middle); + return; + } + + if (monsterEntityToCapture.GetHpPercentage() >= 50) + { + session.SendMsg(session.GetLanguage(GameDialogKey.SKILL_SHOUTMESSAGE_MONSTER_MUST_BE_LOW_HP), MsgMessageType.Middle); + return; + } + + if (playerEntity.MaxPetCount <= playerEntity.MateComponent.GetMates(x => x.MateType == MateType.Pet).Count) + { + session.SendMsg(session.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_MAX_PET_COUNT), MsgMessageType.Middle); + return; + } + + if (!monsterEntityToCapture.CanBeCaught) + { + session.SendMsg(session.GetLanguage(GameDialogKey.SKILL_SHOUTMESSAGE_CAPTURE_IMPOSSIBLE), MsgMessageType.Middle); + return; + } + + if (playerEntity.GetDignityIco() > 3) + { + session.SendMsg(session.GetLanguage(GameDialogKey.SKILL_SHOUTMESSAGE_CAPTURE_DIGNITY_LOW), MsgMessageType.Middle); + return; + } + + await session.EmitEventAsync(new MonsterCaptureEvent(monsterEntityToCapture, false)); + session.PlayerEntity.LastMonsterCaught = DateTime.UtcNow; + + await session.RemoveItemFromInventory(item: e.Item); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Teacher/StrangePartnerFoodHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Teacher/StrangePartnerFoodHandler.cs new file mode 100644 index 0000000..5671d3d --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Teacher/StrangePartnerFoodHandler.cs @@ -0,0 +1,94 @@ +using System.Linq; +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.Groups; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Configuration; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Algorithm; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Extensions.Mates; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Etc.Special; + +public class StrangePartnerFoodHandler : IItemHandler +{ + private readonly IBattleEntityAlgorithmService _algorithm; + private readonly ICharacterAlgorithm _characterAlgorithm; + private readonly IGameLanguageService _gameLanguage; + private readonly ISpPartnerConfiguration _spPartnerConfiguration; + + public StrangePartnerFoodHandler(IGameLanguageService gameLanguage, IBattleEntityAlgorithmService algorithm, ICharacterAlgorithm characterAlgorithm, ISpPartnerConfiguration spPartnerConfiguration) + { + _gameLanguage = gameLanguage; + _algorithm = algorithm; + _characterAlgorithm = characterAlgorithm; + _spPartnerConfiguration = spPartnerConfiguration; + } + + public ItemType ItemType => ItemType.PetPartnerItem; + public long[] Effects => new long[] { 18 }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + if (e.Packet == null || e.Packet.Length == 0) + { + return; + } + + if (!int.TryParse(e.Packet[3], out int mateId)) + { + return; + } + + IMateEntity mateEntity = session.PlayerEntity.MateComponent.GetMate(x => x.Id == mateId && x.MateType == MateType.Partner); + if (mateEntity == null) + { + return; + } + + if (!mateEntity.IsAlive()) + { + return; + } + + if (mateEntity.Level - 1 <= 0) + { + return; + } + + if (mateEntity.IsUsingSp) + { + session.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.PARTNER_INFO_IS_WEARING_SP, e.Sender.UserLanguage)); + return; + } + + if (session.PlayerEntity.PartnerGetEquippedItems(mateEntity.PetSlot).Any(x => x != null)) + { + return; + } + + if (session.PlayerEntity.IsOnVehicle) + { + return; + } + + long mateXp = _characterAlgorithm.GetLevelXp((short)(mateEntity.Level - 1), true, mateEntity.MateType); + + mateEntity.Level -= 1; + mateEntity.Experience = mateXp; + mateEntity.RefreshMaxHpMp(_algorithm); + session.RefreshParty(_spPartnerConfiguration); + mateEntity.Hp = mateEntity.MaxHp; + mateEntity.Mp = mateEntity.MaxMp; + session.SendPetInfo(mateEntity, _gameLanguage); + session.SendMateEffect(mateEntity, EffectType.PetLoveBroke); + await session.RemoveItemFromInventory(item: e.Item); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Teacher/StrangePetFoodHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Teacher/StrangePetFoodHandler.cs new file mode 100644 index 0000000..66ba9a5 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/Teacher/StrangePetFoodHandler.cs @@ -0,0 +1,81 @@ +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.Groups; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Configuration; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Algorithm; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Extensions.Mates; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Etc.Special; + +public class StrangePetFoodHandler : IItemHandler +{ + private readonly IBattleEntityAlgorithmService _algorithm; + private readonly ICharacterAlgorithm _characterAlgorithm; + private readonly IGameLanguageService _gameLanguage; + private readonly ISpPartnerConfiguration _spPartnerConfiguration; + + public StrangePetFoodHandler(IGameLanguageService gameLanguage, IBattleEntityAlgorithmService algorithm, ICharacterAlgorithm characterAlgorithm, ISpPartnerConfiguration spPartnerConfiguration) + { + _gameLanguage = gameLanguage; + _algorithm = algorithm; + _characterAlgorithm = characterAlgorithm; + _spPartnerConfiguration = spPartnerConfiguration; + } + + public ItemType ItemType => ItemType.PetPartnerItem; + public long[] Effects => new long[] { 16 }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + if (e.Packet == null || e.Packet.Length == 0) + { + return; + } + + if (!int.TryParse(e.Packet[3], out int mateId)) + { + return; + } + + IMateEntity mateEntity = session.PlayerEntity.MateComponent.GetMate(x => x.Id == mateId && x.MateType == MateType.Pet); + if (mateEntity == null) + { + return; + } + + if (!mateEntity.IsAlive()) + { + return; + } + + if (mateEntity.Level - 1 <= 0) + { + return; + } + + if (session.PlayerEntity.IsOnVehicle) + { + return; + } + + long mateXp = _characterAlgorithm.GetLevelXp((short)(mateEntity.Level - 1), true, mateEntity.MateType); + mateEntity.Level -= 1; + mateEntity.Experience = mateXp; + mateEntity.RefreshMaxHpMp(_algorithm); + session.RefreshParty(_spPartnerConfiguration); + mateEntity.Hp = mateEntity.MaxHp; + mateEntity.Mp = mateEntity.MaxMp; + session.SendMateEffect(mateEntity, EffectType.PetLoveBroke); + session.SendPetInfo(mateEntity, _gameLanguage); + await session.RemoveItemFromInventory(item: e.Item); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/UpgradeItemsHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/UpgradeItemsHandler.cs new file mode 100644 index 0000000..58eed7d --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Etc/UpgradeItemsHandler.cs @@ -0,0 +1,137 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Linq; +using System.Threading.Tasks; +using PhoenixLib.Logging; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.DTOs.Items; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Etc; + +public class UpgradeItemsHandler : IItemHandler +{ + private readonly IGameLanguageService _gameLanguage; + private readonly IServerManager _serverManager; + + public UpgradeItemsHandler(IGameLanguageService gameLanguage, IServerManager serverManager) + { + _gameLanguage = gameLanguage; + _serverManager = serverManager; + } + + private int[] _scrolls => new[] { 26, 27, 28, 61 }; + + public ItemType ItemType => ItemType.Upgrade; + public long[] Effects => new long[] { 0 }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + string[] packetsplit = e.Packet; + InventoryItem inv = e.Item; + int effectValue = e.Item.ItemInstance.GameItem.EffectValue; + + // If it's a scroll + if (_scrolls.Contains(effectValue)) + { + session.SendGuriPacket(12, value: effectValue); + return; + } + + if (packetsplit == null || packetsplit.Length <= 9) + { + return; + } + + if (!byte.TryParse(packetsplit[8], out byte typeEquip) || + !short.TryParse(packetsplit[9], out short slotEquip)) + { + return; + } + + if (session.PlayerEntity.IsSitting) + { + await session.EmitEventAsync(new PlayerRestEvent + { + RestTeamMemberMates = false + }); + } + + + switch (inv.ItemInstance.ItemVNum) + { + case 1219: + InventoryItem equip = session.PlayerEntity.GetItemBySlotAndType(slotEquip, (InventoryType)typeEquip); + + if (equip == null) + { + return; + } + + if (equip.ItemInstance.Type != ItemInstanceType.WearableInstance) + { + return; + } + + if (!equip.ItemInstance.IsFixed) + { + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.GAMBLING_CHATMESSAGE_ITEM_IS_FIXED, session.UserLanguage), ChatMessageColorType.Red); + return; + } + + await session.RemoveItemFromInventory(inv.ItemInstance.ItemVNum); + equip.ItemInstance.IsFixed = false; + session.SendPacket(session.PlayerEntity.GenerateEffectPacket(3003)); + session.SendGuriPacket(17, 1, slotEquip); + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.ITEM_CHATMESSAGE_UNFIXED, session.UserLanguage), ChatMessageColorType.Green); + + break; + + case 1365: + case 9039: + InventoryItem specialist = session.PlayerEntity.GetItemBySlotAndType(slotEquip, (InventoryType)typeEquip); + if (specialist == null) + { + Log.Debug("Not a SP selected."); + return; + } + + if (specialist.ItemInstance.Type != ItemInstanceType.SpecialistInstance) + { + return; + } + + if (specialist.ItemInstance.Rarity != -2) + { + Log.Debug("SP is not destroyed."); + return; + } + + specialist.ItemInstance.Rarity = 0; + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.SPECIALIST_SHOUTMESSAGE_RESURRECTED, session.UserLanguage), MsgMessageType.Middle); + session.SendGuriPacket(13, 1, 1); + + session.PlayerEntity.SpPointsBasic = _serverManager.MaxBasicSpPoints; + if (session.PlayerEntity.SpPointsBasic > _serverManager.MaxBasicSpPoints) + { + session.PlayerEntity.SpPointsBasic = _serverManager.MaxBasicSpPoints; + } + + await session.RemoveItemFromInventory(inv.ItemInstance.ItemVNum); + session.RefreshSpPoint(); + session.SendInventoryAddPacket(specialist); + break; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/IItemUsageToggleManager.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/IItemUsageToggleManager.cs new file mode 100644 index 0000000..0879ee6 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/IItemUsageToggleManager.cs @@ -0,0 +1,54 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Generic; +using System.Net; +using System.Threading.Tasks; +using StackExchange.Redis; +using WingsEmu.Game.Inventory; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage; + +public class RedisItemUsageToggleManager : IItemUsageToggleManager +{ + private const string ITEM_TOGGLE_FEATURE_PREFIX = "game:item-toggle:"; + private readonly IDatabase _database; + + private readonly IConnectionMultiplexer _multiplexer; + + public RedisItemUsageToggleManager(IConnectionMultiplexer multiplexer) + { + _multiplexer = multiplexer; + _database = _multiplexer.GetDatabase(0); + } + + public async Task IsItemBlocked(int vnum) => await _database.KeyExistsAsync(ITEM_TOGGLE_FEATURE_PREFIX + vnum); + + public async Task BlockItemUsage(int vnum) + { + await _database.StringSetAsync(ITEM_TOGGLE_FEATURE_PREFIX + vnum, "blocked"); + } + + public async Task UnblockItemUsage(int vnum) + { + await _database.KeyDeleteAsync(ITEM_TOGGLE_FEATURE_PREFIX + vnum); + } + + public async Task> GetBlockedItemUsages() + { + var disabledItems = new List(); + foreach (EndPoint ep in _multiplexer.GetEndPoints()) + { + IAsyncEnumerable keys = _multiplexer.GetServer(ep).KeysAsync(0, ITEM_TOGGLE_FEATURE_PREFIX + "*"); + await foreach (RedisKey redisKey in keys) + { + string integerKey = redisKey.ToString().Split(':')[2]; + disabledItems.Add(Convert.ToInt32(integerKey)); + } + } + + return disabledItems; + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/FireworkHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/FireworkHandler.cs new file mode 100644 index 0000000..18c637d --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/FireworkHandler.cs @@ -0,0 +1,25 @@ +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Networking.Broadcasting; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Main; + +public class FireworkHandler : IItemHandler +{ + public ItemType ItemType => ItemType.Event; + public long[] Effects => new long[] { 800 }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + int effect = e.Item.ItemInstance.GameItem.Data[2]; + int sound = e.Item.ItemInstance.GameItem.Data[3]; + session.BroadcastEffect(effect); + session.Broadcast(session.GenerateSound((short)sound), new RangeBroadcast(session.PlayerEntity.PositionX, session.PlayerEntity.PositionY)); + await session.RemoveItemFromInventory(item: e.Item); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/FoodHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/FoodHandler.cs new file mode 100644 index 0000000..bfb419a --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/FoodHandler.cs @@ -0,0 +1,114 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using PhoenixLib.Logging; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.DTOs.BCards; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Items; +using WingsEmu.Game.Networking; +using WingsEmu.Game.SnackFood; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Character; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Main; + +public class FoodHandler : IItemHandler +{ + private readonly IBCardEffectHandlerContainer _bCardEffectHandlerContainer; + private readonly SnackFoodConfiguration _configuration; + private readonly IGameLanguageService _gameLanguage; + + public FoodHandler(IGameLanguageService gameLanguage, SnackFoodConfiguration configuration, IBCardEffectHandlerContainer bCardEffectHandlerContainer) + { + _gameLanguage = gameLanguage; + _configuration = configuration; + _bCardEffectHandlerContainer = bCardEffectHandlerContainer; + } + + public ItemType ItemType => ItemType.Food; + public long[] Effects => new long[] { 0 }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + IPlayerEntity character = session.PlayerEntity; + IGameItem gameItem = e.Item.ItemInstance.GameItem; + DateTime now = DateTime.UtcNow; + + if (character.RainbowBattleComponent.IsInRainbowBattle) + { + session.SendChatMessage(session.GetLanguage(GameDialogKey.ITEM_MESSAGE_CANT_USE), ChatMessageColorType.Yellow); + return; + } + + if (character.LastFood > now) + { + Log.Debug($"{nameof(FoodHandler)} Food in cooldown (You can only use food every {_configuration.DelayBetweenFood})"); + return; + } + + if (!session.PlayerEntity.IsAlive()) + { + return; + } + + if (session.PlayerEntity.IsOnVehicle) + { + return; + } + + if (!session.PlayerEntity.IsSitting) + { + await session.RestAsync(true); + } + + bool softCapReached = character.AddFood(gameItem); + if (softCapReached) + { + string message = _gameLanguage.GetLanguage(character.Gender == GenderType.Male ? GameDialogKey.INFORMATION_MESSAGE_NOT_HUNGRY_MALE : GameDialogKey.INFORMATION_MESSAGE_NOT_HUNGRY_FEMALE, + session.UserLanguage); + session.SendChatMessage(message, ChatMessageColorType.PlayerSay); + return; + } + + if (gameItem.BCards.Any()) + { + // Checks, if item has additional HP/MP inside item.BCards + foreach (BCardDTO bCard in gameItem.BCards) + { + if (bCard.Type != (short)BCardType.HPMP) + { + _bCardEffectHandlerContainer.Execute(character, character, bCard); + continue; + } + + int firstDataValue = bCard.FirstDataValue(character.Level); + int secondDataValue = bCard.SecondDataValue(character.Level); + + switch ((AdditionalTypes.HPMP)bCard.SubType) + { + case AdditionalTypes.HPMP.ReceiveAdditionalHP: + character.AddAdditionalFood(character.MaxHp, firstDataValue, firstDataValue >= 0, secondDataValue); + break; + case AdditionalTypes.HPMP.ReceiveAdditionalMP: + character.AddAdditionalFood(character.MaxMp, firstDataValue, firstDataValue < 0, secondDataValue); + break; + } + } + } + + character.LastFood = DateTime.UtcNow.AddMilliseconds(_configuration.DelayBetweenFood); + + await session.RemoveItemFromInventory(item: e.Item); + session.SendEffect(EffectType.Eat); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/PotionHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/PotionHandler.cs new file mode 100644 index 0000000..173595b --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/PotionHandler.cs @@ -0,0 +1,267 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using WingsAPI.Communication.ServerApi.Protocol; +using WingsAPI.Data.Families; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsAPI.Packets.Enums.Shells; +using WingsEmu.Core.Extensions; +using WingsEmu.DTOs.BCards; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Extensions.Mates; +using WingsEmu.Game.Items; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Networking; +using WingsEmu.Game.SnackFood; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Main; + +public class PotionHandler : IItemHandler +{ + private readonly IBCardEffectHandlerContainer _bCardEffectHandlerContainer; + private readonly SnackFoodConfiguration _configuration; + + private readonly SerializableGameServer _gameServer; + + public PotionHandler(SerializableGameServer gameServer, IBCardEffectHandlerContainer bCardEffectHandlerContainer, SnackFoodConfiguration configuration) + { + _gameServer = gameServer; + _bCardEffectHandlerContainer = bCardEffectHandlerContainer; + _configuration = configuration; + } + + public ItemType ItemType => ItemType.Potion; + public long[] Effects => new long[] { 0 }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + IPlayerEntity character = session.PlayerEntity; + GameItemInstance itemInstance = e.Item.ItemInstance; + DateTime now = DateTime.UtcNow; + + if (character.RainbowBattleComponent.IsInRainbowBattle) + { + session.SendChatMessage(session.GetLanguage(GameDialogKey.ITEM_MESSAGE_CANT_USE), ChatMessageColorType.Yellow); + return; + } + + if ((now - character.LastPotion).TotalMilliseconds < 500) + { + return; + } + + if (!character.IsAlive()) + { + return; + } + + if (character.Hp == character.MaxHp && character.Mp == character.MaxMp) + { + if (session.PlayerEntity.MateComponent.TeamMembers(m => m.IsAlive()).All(mate => mate.Hp == mate.MaxHp && mate.Mp == mate.MaxMp)) + { + return; + } + } + + bool isOnAct4 = _gameServer.ChannelType == GameChannelType.ACT_4 && session.CurrentMapInstance.MapInstanceType != MapInstanceType.Act4Dungeon; + + if (e.Item.ItemInstance.GameItem.Hp > 0) + { + await HealHp(session, e.Item.ItemInstance.GameItem.Hp); + } + + if (e.Item.ItemInstance.GameItem.Mp > 0) + { + HealMp(session, e.Item.ItemInstance.GameItem.Mp); + } + + switch (e.Item.ItemInstance.GameItem.Id) + { + case (int)ItemVnums.FULL_HP_POTION: + if (isOnAct4) + { + return; + } + + await HealHp(session); + break; + case (int)ItemVnums.FULL_MP_POTION: + if (isOnAct4) + { + return; + } + + HealMp(session); + break; + case (int)ItemVnums.FULL_HP_MP_POTION: + case (int)ItemVnums.FULL_HP_MP_POTION_LIMIT: + if (isOnAct4) + { + return; + } + + await HealHp(session); + HealMp(session); + break; + } + + foreach (BCardDTO bCard in e.Item.ItemInstance.GameItem.BCards) + { + _bCardEffectHandlerContainer.Execute(character, character, bCard); + } + + character.LastPotion = DateTime.UtcNow; + await session.RemoveItemFromInventory(item: e.Item); + session.RefreshStat(); + } + + private async Task HealHp(IClientSession session, int health = 0) + { + IPlayerEntity character = session.PlayerEntity; + int potionHp; + + foreach (IMateEntity mate in session.PlayerEntity.MateComponent.TeamMembers()) + { + if (!mate.IsAlive()) + { + continue; + } + + int mateMaxHp = mate.MaxHp; + potionHp = health; + if (health == 0) + { + potionHp = mateMaxHp; + } + else + { + int toAdd = character.BCardComponent.GetAllBCardsInformation(BCardType.LeonaPassiveSkill, (byte)AdditionalTypes.LeonaPassiveSkill.IncreaseRecoveryItems, character.Level).firstData; + toAdd += character.GetMaxArmorShellValue(ShellEffectType.IncreasedRecoveryItemSpeed); + toAdd += character.Family?.UpgradeValues.GetOrDefault(FamilyUpgradeType.INCREASE_POTION_REGEN) ?? 0; + int toRemove = character.BCardComponent.GetAllBCardsInformation(BCardType.LeonaPassiveSkill, (byte)AdditionalTypes.LeonaPassiveSkill.DecreaseRecoveryItems, character.Level).firstData; + + double finalHeal = (100 + (toAdd - toRemove)) * 0.01; + + potionHp = (int)(potionHp * finalHeal); + } + + if (health == 0) + { + int mateHpHeal = mate.Hp + potionHp > mateMaxHp ? mateMaxHp - mate.Hp : potionHp; + mate.Hp += mateHpHeal; + mate.BroadcastHeal(mateHpHeal); + } + else + { + await mate.EmitEventAsync(new BattleEntityHealEvent + { + Entity = mate, + HpHeal = potionHp + }); + } + + session.SendMateLife(mate); + } + + int maxHp = character.MaxHp; + potionHp = health; + if (health == 0) + { + potionHp = maxHp; + } + else + { + int toAdd = character.BCardComponent.GetAllBCardsInformation(BCardType.LeonaPassiveSkill, (byte)AdditionalTypes.LeonaPassiveSkill.IncreaseRecoveryItems, character.Level).firstData; + toAdd += character.GetMaxArmorShellValue(ShellEffectType.IncreasedRecoveryItemSpeed); + toAdd += character.Family?.UpgradeValues.GetOrDefault(FamilyUpgradeType.INCREASE_POTION_REGEN) ?? 0; + int toRemove = character.BCardComponent.GetAllBCardsInformation(BCardType.LeonaPassiveSkill, (byte)AdditionalTypes.LeonaPassiveSkill.DecreaseRecoveryItems, character.Level).firstData; + + double finalHeal = (100 + (toAdd - toRemove)) * 0.01; + + potionHp = (int)(potionHp * finalHeal); + } + + if (health == 0) + { + int hpHeal = session.PlayerEntity.Hp + potionHp > maxHp ? maxHp - session.PlayerEntity.Hp : potionHp; + session.PlayerEntity.BroadcastHeal(hpHeal); + session.PlayerEntity.Hp += hpHeal; + } + else + { + await session.PlayerEntity.EmitEventAsync(new BattleEntityHealEvent + { + Entity = session.PlayerEntity, + HpHeal = potionHp + }); + } + } + + private void HealMp(IClientSession session, int mana = 0) + { + IPlayerEntity character = session.PlayerEntity; + int potionMp; + + foreach (IMateEntity mate in session.PlayerEntity.MateComponent.TeamMembers()) + { + if (!mate.IsAlive()) + { + continue; + } + + int mateMaxMp = mate.MaxMp; + potionMp = mana; + if (mana == 0) + { + potionMp = mateMaxMp; + } + else + { + int toAdd = character.BCardComponent.GetAllBCardsInformation(BCardType.LeonaPassiveSkill, (byte)AdditionalTypes.LeonaPassiveSkill.IncreaseRecoveryItems, character.Level).firstData; + toAdd += character.GetMaxArmorShellValue(ShellEffectType.IncreasedRecoveryItemSpeed); + toAdd += character.Family?.UpgradeValues.GetOrDefault(FamilyUpgradeType.INCREASE_POTION_REGEN) ?? 0; + int toRemove = character.BCardComponent.GetAllBCardsInformation(BCardType.LeonaPassiveSkill, (byte)AdditionalTypes.LeonaPassiveSkill.DecreaseRecoveryItems, character.Level).firstData; + + double finalHeal = (100 + (toAdd - toRemove)) * 0.01; + + potionMp = (int)(potionMp * finalHeal); + } + + int mateMpHeal = mate.Mp + potionMp > mateMaxMp ? mateMaxMp - mate.Mp : potionMp; + mate.Mp += mateMpHeal; + session.SendMateLife(mate); + } + + int maxMp = character.MaxMp; + potionMp = mana; + if (mana == 0) + { + potionMp = maxMp; + } + else + { + int toAdd = character.BCardComponent.GetAllBCardsInformation(BCardType.LeonaPassiveSkill, (byte)AdditionalTypes.LeonaPassiveSkill.IncreaseRecoveryItems, character.Level).firstData; + toAdd += character.GetMaxArmorShellValue(ShellEffectType.IncreasedRecoveryItemSpeed); + toAdd += character.Family?.UpgradeValues.GetOrDefault(FamilyUpgradeType.INCREASE_POTION_REGEN) ?? 0; + int toRemove = character.BCardComponent.GetAllBCardsInformation(BCardType.LeonaPassiveSkill, (byte)AdditionalTypes.LeonaPassiveSkill.DecreaseRecoveryItems, character.Level).firstData; + + double finalHeal = (100 + (toAdd - toRemove)) * 0.01; + + potionMp = (int)(potionMp * finalHeal); + } + + int mpHeal = session.PlayerEntity.Mp + potionMp > maxMp ? maxMp - session.PlayerEntity.Mp : potionMp; + session.PlayerEntity.Mp += mpHeal; + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/SnackHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/SnackHandler.cs new file mode 100644 index 0000000..a14fd56 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/SnackHandler.cs @@ -0,0 +1,124 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using PhoenixLib.Logging; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.DTOs.BCards; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Items; +using WingsEmu.Game.Networking; +using WingsEmu.Game.SnackFood; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Character; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Main; + +public class SnackHandler : IItemHandler +{ + private readonly IBCardEffectHandlerContainer _bCardEffectHandlerContainer; + private readonly IBuffFactory _buff; + private readonly SnackFoodConfiguration _configuration; + private readonly IGameLanguageService _gameLanguage; + + public SnackHandler(IGameLanguageService gameLanguage, SnackFoodConfiguration configuration, IBuffFactory buff, IBCardEffectHandlerContainer bCardEffectHandlerContainer) + { + _gameLanguage = gameLanguage; + _configuration = configuration; + _buff = buff; + _bCardEffectHandlerContainer = bCardEffectHandlerContainer; + } + + public ItemType ItemType => ItemType.Snack; + public long[] Effects => new long[] { 0 }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + IPlayerEntity character = session.PlayerEntity; + IGameItem gameItem = e.Item.ItemInstance.GameItem; + DateTime now = DateTime.UtcNow; + + if (character.RainbowBattleComponent.IsInRainbowBattle) + { + session.SendChatMessage(session.GetLanguage(GameDialogKey.ITEM_MESSAGE_CANT_USE), ChatMessageColorType.Yellow); + return; + } + + if (character.LastSnack > now) + { + Log.Debug($"{nameof(FoodHandler)} Food in cooldown (You can only use snack every {_configuration.DelayBetweenSnack})"); + return; + } + + if (!session.PlayerEntity.IsAlive()) + { + return; + } + + if (gameItem.Id == (int)ItemVnums.BATTLE_POTION) + { + if (session.PlayerEntity.IsInRaidParty) + { + return; + } + + if (session.CantPerformActionOnAct4()) + { + return; + } + + Buff buff = _buff.CreateBuff((int)BuffVnums.PVP, character); + await character.AddBuffAsync(buff); + await character.Session.RemoveItemFromInventory(item: e.Item); + return; + } + + bool softCapReached = character.AddSnack(gameItem); + if (softCapReached) + { + string message = _gameLanguage.GetLanguage(character.Gender == GenderType.Male ? GameDialogKey.INFORMATION_MESSAGE_NOT_HUNGRY_MALE : GameDialogKey.INFORMATION_MESSAGE_NOT_HUNGRY_FEMALE, + session.UserLanguage); + session.SendChatMessage(message, ChatMessageColorType.PlayerSay); + + return; + } + + if (gameItem.BCards.Any()) + { + // Checks, if item has additional HP/MP inside item.BCards + foreach (BCardDTO bCard in gameItem.BCards) + { + if (bCard.Type != (short)BCardType.HPMP) + { + _bCardEffectHandlerContainer.Execute(character, character, bCard); + continue; + } + + int firstDataValue = bCard.FirstDataValue(character.Level); + int secondDataValue = bCard.SecondDataValue(character.Level); + + switch ((AdditionalTypes.HPMP)bCard.SubType) + { + case AdditionalTypes.HPMP.ReceiveAdditionalHP: + character.AddAdditionalSnack(character.MaxHp, firstDataValue, firstDataValue >= 0, secondDataValue); + break; + case AdditionalTypes.HPMP.ReceiveAdditionalMP: + character.AddAdditionalSnack(character.MaxMp, firstDataValue, firstDataValue < 0, secondDataValue); + break; + } + } + } + + character.LastSnack = DateTime.UtcNow.AddMilliseconds(_configuration.DelayBetweenSnack); + + await session.RemoveItemFromInventory(item: e.Item); + session.SendEffect(EffectType.Eat); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/BackpackHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/BackpackHandler.cs new file mode 100644 index 0000000..5d9ecfa --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/BackpackHandler.cs @@ -0,0 +1,51 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.DTOs.Bonus; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Main.Special; + +public class BackpackHandler : IItemHandler +{ + private readonly IGameLanguageService _gameLanguage; + + public BackpackHandler(IGameLanguageService gameLanguage) => _gameLanguage = gameLanguage; + + public ItemType ItemType => ItemType.Special; + public long[] Effects { get; } = { 1005, 601 }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + if (session.PlayerEntity.HaveStaticBonus(StaticBonusType.Backpack)) + { + return; + } + + DateTime? effectDateEnd = e.Item.ItemInstance.GameItem.EffectValue == 0 ? null : DateTime.UtcNow.AddDays(e.Item.ItemInstance.GameItem.EffectValue); + + await session.EmitEventAsync(new AddStaticBonusEvent(new CharacterStaticBonusDto + { + DateEnd = effectDateEnd, + ItemVnum = e.Item.ItemInstance.GameItem.Id, + StaticBonusType = StaticBonusType.Backpack + })); + + await session.RemoveItemFromInventory(item: e.Item); + session.ShowInventoryExtensions(); + + string name = _gameLanguage.GetLanguage(GameDataType.Item, e.Item.ItemInstance.GameItem.Name, session.UserLanguage); + session.SendChatMessage(_gameLanguage.GetLanguageFormat(GameDialogKey.ITEM_CHATMESSAGE_EFFECT_ACTIVATED, session.UserLanguage, name), ChatMessageColorType.Green); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/ChangePartnerSkinHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/ChangePartnerSkinHandler.cs new file mode 100644 index 0000000..0d0ea1f --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/ChangePartnerSkinHandler.cs @@ -0,0 +1,122 @@ +using System.Linq; +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.Game._enum; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Extensions.Mates; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Main.Special; + +public class ChangePartnerSkinHandler : IItemHandler +{ + private static readonly int[] PartnersWithSkins = { 317, 318, 319, 2557, 2617, 2618, 2620, 2640, 2673 }; + public ItemType ItemType => ItemType.Special; + public long[] Effects => new long[] { 305 }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + IMateEntity partner = session.PlayerEntity.MateComponent.GetMate(x => x.IsTeamMember && x.MateType == MateType.Partner); + + if (partner == null) + { + return; + } + + if (!partner.IsAlive()) + { + return; + } + + if (partner.IsUsingSp) + { + return; + } + + if (!PartnersWithSkins.Contains(partner.NpcMonsterVNum)) + { + return; + } + + if (partner.Skin == e.Item.ItemInstance.GameItem.Morph) + { + return; + } + + switch (partner.NpcMonsterVNum) + { + case 317: // Bob + if (e.Item.ItemInstance.ItemVNum != (short)ItemVnums.SKIN_FOR_BOB) + { + return; + } + + partner.Skin = e.Item.ItemInstance.GameItem.Morph; + break; + case 318: // Tom + if (e.Item.ItemInstance.ItemVNum != (short)ItemVnums.SKIN_FOR_TOM) + { + return; + } + + partner.Skin = e.Item.ItemInstance.GameItem.Morph; + break; + case 319: // Kliff + if (e.Item.ItemInstance.ItemVNum != (short)ItemVnums.SKIN_FOR_KLIFF) + { + return; + } + + partner.Skin = e.Item.ItemInstance.GameItem.Morph; + break; + case 2617: // Frigg + if (e.Item.ItemInstance.ItemVNum != (short)ItemVnums.SKIN_FOR_FRIGG) + { + return; + } + + partner.Skin = e.Item.ItemInstance.GameItem.Morph; + break; + case 2618: // Ragnar + if (e.Item.ItemInstance.ItemVNum != (short)ItemVnums.SKIN_FOR_RAGNAR) + { + return; + } + + partner.Skin = e.Item.ItemInstance.GameItem.Morph; + break; + case 2620: // Erdimien + if (e.Item.ItemInstance.ItemVNum != (short)ItemVnums.SKIN_FOR_ERDIMIEN) + { + return; + } + + partner.Skin = e.Item.ItemInstance.GameItem.Morph; + break; + case 2640: // Jennifer + if (e.Item.ItemInstance.ItemVNum != (short)ItemVnums.SKIN_FOR_JENNIFER) + { + return; + } + + partner.Skin = e.Item.ItemInstance.GameItem.Morph; + break; + case 2673: // Yertirand + if (e.Item.ItemInstance.ItemVNum != (short)ItemVnums.SKIN_FOR_YERTIRAND) + { + return; + } + + partner.Skin = e.Item.ItemInstance.GameItem.Morph; + break; + } + + await session.RemoveItemFromInventory(item: e.Item); + session.CurrentMapInstance?.Broadcast(partner.GenerateCMode(partner.Skin)); + session.SendCondMate(partner); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/CostumeScrollHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/CostumeScrollHandler.cs new file mode 100644 index 0000000..d56bb11 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/CostumeScrollHandler.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.Game; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Configuration; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Networking.Broadcasting; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Main.Special; + +/// +/// This handle is use for Halloween, Winter & Bushtail costume scroll +/// +public class CostumeScrollHandler : IItemHandler +{ + private readonly ICostumeScrollConfiguration _costumeScrollConfiguration; + private readonly IDelayManager _delayManager; + + private readonly IGameLanguageService _languageService; + private readonly IRandomGenerator _randomGenerator; + + public CostumeScrollHandler(IGameLanguageService languageService, IDelayManager delayManager, IRandomGenerator randomGenerator, ICostumeScrollConfiguration costumeScrollConfiguration) + { + _languageService = languageService; + _delayManager = delayManager; + _randomGenerator = randomGenerator; + _costumeScrollConfiguration = costumeScrollConfiguration; + } + + public ItemType ItemType => ItemType.Special; + public long[] Effects => new long[] { 1001 }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + IReadOnlyList morphs = _costumeScrollConfiguration.GetScrollMorphs((short)e.Item.ItemInstance.ItemVNum); + + if (morphs == null || morphs.Count == 0) + { + return; + } + + if (session.PlayerEntity.UseSp) + { + session.SendChatMessage(_languageService.GetLanguage(GameDialogKey.ITEM_MESSAGE_CANT_USE, session.UserLanguage), ChatMessageColorType.Yellow); + return; + } + + if (session.PlayerEntity.IsOnVehicle) + { + session.SendChatMessage(_languageService.GetLanguage(GameDialogKey.ITEM_MESSAGE_CANT_USE, session.UserLanguage), ChatMessageColorType.Yellow); + return; + } + + if (session.PlayerEntity.IsSeal) + { + return; + } + + if (e.Option == 0) + { + if (session.PlayerEntity.IsMorphed) + { + session.PlayerEntity.IsMorphed = false; + session.PlayerEntity.Morph = 0; + + session.BroadcastCMode(); + return; + } + + DateTime waitUntil = await _delayManager.RegisterAction(session.PlayerEntity, DelayedActionType.MorphScroll); + session.SendDelay((int)(waitUntil - DateTime.UtcNow).TotalMilliseconds, GuriType.Transforming, + $"u_i 1 {session.PlayerEntity.Id} {(byte)e.Item.ItemInstance.GameItem.Type} {e.Item.Slot} 2"); + return; + } + + bool canUseScroll = await _delayManager.CanPerformAction(session.PlayerEntity, DelayedActionType.MorphScroll); + + if (!canUseScroll) + { + return; + } + + await _delayManager.CompleteAction(session.PlayerEntity, DelayedActionType.MorphScroll); + + session.PlayerEntity.IsMorphed = true; + session.PlayerEntity.Morph = morphs[_randomGenerator.RandomNumber(0, morphs.Count)] + 1000; + + session.BroadcastCMode(); + session.BroadcastEffect((int)EffectType.Transform, new RangeBroadcast(session.PlayerEntity.PositionX, session.PlayerEntity.PositionY)); + await session.RemoveItemFromInventory(item: e.Item); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/CupidArrowHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/CupidArrowHandler.cs new file mode 100644 index 0000000..ac25545 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/CupidArrowHandler.cs @@ -0,0 +1,78 @@ +using System.Linq; +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Relations; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Relations; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Main.Special; + +public class CupidArrowHandler : IItemHandler +{ + private readonly IInvitationManager _invitation; + private readonly IGameLanguageService _languageService; + + private readonly ISessionManager _sessionManager; + + public CupidArrowHandler(ISessionManager sessionManager, IGameLanguageService languageService, IInvitationManager invitation) + { + _sessionManager = sessionManager; + _languageService = languageService; + _invitation = invitation; + } + + public ItemType ItemType => ItemType.Special; + public long[] Effects => new long[] { 34 }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + if (e.Packet.Length < 4) + { + return; + } + + if (!long.TryParse(e.Packet[3], out long characterId)) + { + return; + } + + IClientSession otherSession = _sessionManager.GetSessionByCharacterId(characterId); + if (otherSession == null || otherSession.PlayerEntity.Id == session.PlayerEntity.Id) + { + return; + } + + if (session.PlayerEntity.GetRelations().Any(x => x.RelationType == CharacterRelationType.Spouse)) + { + return; + } + + if (otherSession.PlayerEntity.GetRelations().Any(x => x.RelationType == CharacterRelationType.Spouse)) + { + return; + } + + if (!session.PlayerEntity.IsFriend(characterId)) + { + session.SendInfo(_languageService.GetLanguage(GameDialogKey.FRIEND_MESSAGE_NOT_FRIEND, session.UserLanguage)); + return; + } + + if (e.Option == 0) + { + session.SendQnaPacket($"u_i 1 {otherSession.PlayerEntity.Id} {(byte)e.Item.ItemInstance.GameItem.Type} {e.Item.Slot} 2", + _languageService.GetLanguageFormat(GameDialogKey.WEDDING_DIALOG_REQUEST_VERIFICATION, session.UserLanguage, otherSession.PlayerEntity.Name)); + return; + } + + await session.EmitEventAsync(new InvitationEvent(characterId, InvitationType.Spouse)); + await session.RemoveItemFromInventory(item: e.Item); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/FactionEggHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/FactionEggHandler.cs new file mode 100644 index 0000000..ddc4581 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/FactionEggHandler.cs @@ -0,0 +1,82 @@ +using System.Threading.Tasks; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; +using WingsEmu.Packets.Enums.Families; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Main.Special; + +public class FactionEggHandler : IItemHandler +{ + private const int IndividualAngelEgg = 1; + private const int IndividualDemonEgg = 2; + private const int FamilyAngelEgg = 3; + private const int FamilyDemonEgg = 4; + + private readonly IGameLanguageService _languageService; + + public FactionEggHandler(IGameLanguageService languageService) => _languageService = languageService; + + public ItemType ItemType => ItemType.Special; + public long[] Effects => new long[] { 570 }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + if (session.PlayerEntity.IsOnVehicle) + { + session.SendMsg(_languageService.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_REMOVE_VEHICLE, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (session.CantPerformActionOnAct4()) + { + return; + } + + int eggType = e.Item.ItemInstance.GameItem.EffectValue; + FactionType targetFaction = eggType == IndividualAngelEgg || eggType == FamilyAngelEgg ? FactionType.Angel : FactionType.Demon; + + if (eggType == IndividualAngelEgg || eggType == IndividualDemonEgg) + { + if (session.PlayerEntity.Faction == targetFaction) + { + session.SendMsg(_languageService.GetLanguage(GameDialogKey.ITEM_SHOUTMESSAGE_SAME_FACTION, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (session.PlayerEntity.Family != null) + { + session.SendMsg(_languageService.GetLanguage(GameDialogKey.ITEM_SHOUTMESSAGE_FACTION_CANT_IN_FAMILY, session.UserLanguage), MsgMessageType.Middle); + return; + } + + session.SendQnaPacket($"guri 750 {eggType}", _languageService.GetLanguage(GameDialogKey.ITEM_DIALOG_ASK_CHANGE_FACTION, session.UserLanguage)); + } + else if (eggType == FamilyAngelEgg || eggType == FamilyDemonEgg) + { + if (!session.PlayerEntity.IsInFamily()) + { + session.SendMsg(_languageService.GetLanguage(GameDialogKey.FAMILY_INFO_NO_FAMILY, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (session.PlayerEntity.GetFamilyAuthority() != FamilyAuthority.Head) + { + session.SendMsg(_languageService.GetLanguage(GameDialogKey.FAMILY_INFO_NOT_FAMILY_HEAD, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if ((session.PlayerEntity.Family.Faction / 2) == eggType) + { + session.SendMsg(_languageService.GetLanguage(GameDialogKey.ITEM_SHOUTMESSAGE_SAME_FACTION, session.UserLanguage), MsgMessageType.Middle); + return; + } + + session.SendQnaPacket($"guri 750 {eggType}", _languageService.GetLanguage(GameDialogKey.ITEM_DIALOG_ASK_CHANGE_FAMILY_FACTION, session.UserLanguage)); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/GeneralItemsHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/GeneralItemsHandler.cs new file mode 100644 index 0000000..39eb9d2 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/GeneralItemsHandler.cs @@ -0,0 +1,243 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsAPI.Game.Extensions.MinilandExtensions; +using WingsEmu.DTOs.Bonus; +using WingsEmu.DTOs.Items; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Act4.Event; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Configurations.Miniland; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Main.Special; + +public class GeneralItemsHandler : IItemHandler +{ + private readonly IBankReputationConfiguration _bankReputationConfiguration; + private readonly IBuffFactory _buffFactory; + private readonly IGameItemInstanceFactory _gameItemInstanceFactory; + private readonly IGameLanguageService _gameLanguage; + private readonly MinigameConfiguration _minigameConfiguration; + private readonly IRandomGenerator _randomGenerator; + private readonly IRankingManager _rankingManager; + private readonly IReputationConfiguration _reputationConfiguration; + private readonly ISessionManager _sessionManager; + + private readonly HashSet _tarots = new() + { + ItemVnums.SEALED_TAROT_FOOL, + ItemVnums.SEALED_TAROT_MAGICIAN, + ItemVnums.SEALED_TAROT_LOVERS, + ItemVnums.SEALED_TAROT_HERMIT, + ItemVnums.SEALED_TAROT_DEATH, + ItemVnums.SEALED_TAROT_DEVIL, + ItemVnums.SEALED_TAROT_TOWER, + ItemVnums.SEALED_TAROT_STAR, + ItemVnums.SEALED_TAROT_MOON, + ItemVnums.SEALED_TAROT_SUN + }; + + public GeneralItemsHandler(IGameLanguageService gameLanguage, IBuffFactory buffFactory, IRandomGenerator randomGenerator, + ISessionManager sessionManager, MinigameConfiguration minigameConfiguration, IGameItemInstanceFactory gameItemInstanceFactory, IReputationConfiguration reputationConfiguration, + IBankReputationConfiguration bankReputationConfiguration, IRankingManager rankingManager) + { + _gameLanguage = gameLanguage; + _buffFactory = buffFactory; + _randomGenerator = randomGenerator; + _sessionManager = sessionManager; + _minigameConfiguration = minigameConfiguration; + _gameItemInstanceFactory = gameItemInstanceFactory; + _reputationConfiguration = reputationConfiguration; + _bankReputationConfiguration = bankReputationConfiguration; + _rankingManager = rankingManager; + } + + public ItemType ItemType => ItemType.Special; + public long[] Effects => new long[] { 0 }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + switch ((ItemVnums)e.Item.ItemInstance.ItemVNum) + { + case ItemVnums.SOULSTONE_BLESSING: + case ItemVnums.SOULSTONE_BLESSING_LIMITED: + Buff itemBuff = session.PlayerEntity.BuffComponent.GetBuff((short)BuffVnums.SOULSTONE_BLESSING); + if (itemBuff != null) + { + string buffName = _gameLanguage.GetLanguage(GameDataType.Card, itemBuff.Name, session.UserLanguage); + session.SendChatMessage(_gameLanguage.GetLanguageFormat(GameDialogKey.ITEM_CHATMESSAGE_CANT_USE_TWICE, session.UserLanguage, buffName), ChatMessageColorType.Yellow); + return; + } + + if (!session.PlayerEntity.UseSp) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_NO_SPECIALIST_CARD, session.UserLanguage), MsgMessageType.Middle); + return; + } + + await session.PlayerEntity.AddBuffAsync(_buffFactory.CreateOneHourBuff(session.PlayerEntity, (short)BuffVnums.SOULSTONE_BLESSING, BuffFlag.BIG_AND_KEEP_ON_LOGOUT)); + await session.RemoveItemFromInventory(item: e.Item); + break; + case ItemVnums.PRODUCTION_COUPON: + if (session.PlayerEntity.MinilandPoint >= _minigameConfiguration.Configuration.MaxmimumMinigamePoints) + { + session.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.MINIGAME_INFO_POINTS_FULL, session.UserLanguage)); + return; + } + + if (!session.PlayerEntity.HasItem(_minigameConfiguration.Configuration.ProductionCouponVnum)) + { + return; + } + + await session.RemoveItemFromInventory(_minigameConfiguration.Configuration.ProductionCouponVnum); + session.AddMinigamePoints(_minigameConfiguration.Configuration.ProductionCouponPointsAmount, _minigameConfiguration); + break; + case ItemVnums.SCROLL_CHICKEN: + session.SendWopenPacket(WindowType.CHICKEN_FREE_SCROLL); + break; + case ItemVnums.SCROLL_PYJAMA: + session.SendWopenPacket(WindowType.PAJAMA_FREE_SCROLL); + break; + case ItemVnums.SCROLL_PIRATE: + session.SendWopenPacket(WindowType.PIRATE_FREE_SCROLL); + break; + case ItemVnums.FAIRY_EXPERIENCE_POTION: + case ItemVnums.FAIRY_EXPERIENCE_POTION_LIMITED: + Buff buff = session.PlayerEntity.BuffComponent.GetBuff((short)BuffVnums.FAIRYXP_POTION); + if (buff != null) + { + string buffName = _gameLanguage.GetLanguage(GameDataType.Card, buff.Name, session.UserLanguage); + session.SendChatMessage(_gameLanguage.GetLanguageFormat(GameDialogKey.ITEM_CHATMESSAGE_CANT_USE_TWICE, session.UserLanguage, buffName), ChatMessageColorType.Yellow); + return; + } + + await session.RemoveItemFromInventory(item: e.Item); + session.PlayerEntity.AddBuffAsync(_buffFactory.CreateOneHourBuff(session.PlayerEntity, (short)BuffVnums.FAIRYXP_POTION, BuffFlag.BIG_AND_KEEP_ON_LOGOUT)) + .ConfigureAwait(false).GetAwaiter().GetResult(); + break; + case ItemVnums.PERFUME: + session.SendGuriPacket(18, 1); + break; + case ItemVnums.RAINBOW_PEARL: + session.SendGuriPacket(18); + break; + case ItemVnums.MAGIC_ERASER: + if (e.Packet == null) + { + return; + } + + if (e.Packet.Length < 9) + { + // MODIFIED PACKET + return; + } + + if (!short.TryParse(e.Packet[9], out short eqSlot) || + !Enum.TryParse(e.Packet[8], out InventoryType eqType)) + { + return; + } + + InventoryItem eq = session.PlayerEntity.GetItemBySlotAndType(eqSlot, eqType); + if (eq == null) + { + // PACKET MODIFIED + return; + } + + if (eq.ItemInstance.Type != ItemInstanceType.WearableInstance) + { + return; + } + + GameItemInstance eqItem = eq.ItemInstance; + + if (eqItem.GameItem.ItemType != ItemType.Armor && eqItem.GameItem.ItemType != ItemType.Weapon) + { + return; + } + + if (eqItem.EquipmentOptions == null || !eqItem.EquipmentOptions.Any()) + { + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.ITEM_MESSAGE_ERASER_NO_SHELL, session.UserLanguage), ChatMessageColorType.Red); + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.ITEM_MESSAGE_ERASER_NO_SHELL, session.UserLanguage), MsgMessageType.Middle); + return; + } + + eqItem.EquipmentOptions.Clear(); + eqItem.ShellRarity = null; + await session.RemoveItemFromInventory(item: e.Item); + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.SHELLS_SHOUTMESSAGE_ERASED, session.UserLanguage), MsgMessageType.Middle); + session.SendGuriPacket(17, 1); + break; + case ItemVnums.TAROT_CARD_GAME: + var tarots = _tarots.ToList(); + for (int i = 0; i < 5; i++) + { + int rndIndex = _randomGenerator.RandomNumber(tarots.Count); + int newItemVnum = (int)tarots[rndIndex]; + + GameItemInstance newItem = _gameItemInstanceFactory.CreateItem(newItemVnum); + await session.AddNewItemToInventory(newItem, sendGiftIsFull: true); + tarots.RemoveAt(rndIndex); + } + + await session.RemoveItemFromInventory(item: e.Item); + break; + case ItemVnums.CUARRY_BANK_SAVINGS_BOOK: + session.SendGbPacket(BankType.BankMoney, _reputationConfiguration, _bankReputationConfiguration, _rankingManager.TopReputation); + session.SendSMemo(SmemoType.BankInfo, _gameLanguage.GetLanguageFormat(GameDialogKey.BANK_MESSAGE_BALANCE, session.UserLanguage, session.Account.BankMoney)); + break; + case ItemVnums.ANGEL_BASE_FLAG: + case ItemVnums.DEMON_BASE_FLAG: + if (!session.CurrentMapInstance.HasMapFlag(MapFlags.ACT_4)) + { + return; + } + + await session.EmitEventAsync(new Act4PutFlagEvent + { + InventoryItem = e.Item + }); + break; + case ItemVnums.CUARRY_BANK_VIP_10: + case ItemVnums.CUARRY_BANK_VIP_30: + if (session.PlayerEntity.HaveStaticBonus(StaticBonusType.CuarryBankMedal)) + { + return; + } + + DateTime dateEnd = DateTime.UtcNow.AddDays(e.Item.ItemInstance.GameItem.Id == (short)ItemVnums.CUARRY_BANK_VIP_10 ? 10 : 30); + await session.EmitEventAsync(new AddStaticBonusEvent(new CharacterStaticBonusDto + { + DateEnd = dateEnd, + ItemVnum = e.Item.ItemInstance.GameItem.Id, + StaticBonusType = StaticBonusType.CuarryBankMedal + })); + + await session.RemoveItemFromInventory(item: e.Item); + string name = _gameLanguage.GetLanguage(GameDataType.Item, e.Item.ItemInstance.GameItem.Name, session.UserLanguage); + session.SendChatMessage(_gameLanguage.GetLanguageFormat(GameDialogKey.ITEM_CHATMESSAGE_EFFECT_ACTIVATED, session.UserLanguage, name), ChatMessageColorType.Green); + break; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/InventoryExpansionHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/InventoryExpansionHandler.cs new file mode 100644 index 0000000..d90be5d --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/InventoryExpansionHandler.cs @@ -0,0 +1,67 @@ +using System; +using System.Threading.Tasks; +using Serilog; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.DTOs.Bonus; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Main.Special; + +public class InventoryExpansionHandler : IItemHandler +{ + private readonly IGameLanguageService _gameLanguage; + + public InventoryExpansionHandler(IGameLanguageService gameLanguage) => _gameLanguage = gameLanguage; + + public ItemType ItemType { get; } = ItemType.Special; + public long[] Effects => new long[] { 604, 605 }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + if (session.PlayerEntity.HaveStaticBonus(StaticBonusType.InventoryExpansion)) + { + return; + } + + DateTime? effectDateEnd; + switch (e.Item.ItemInstance.ItemVNum) + { + case (int)ItemVnums.INVENTORY_EXPANSION_TICKET_30_DAYS: + effectDateEnd = DateTime.UtcNow.AddDays(30); + break; + case (int)ItemVnums.INVENTORY_EXPANSION_TICKET_60_DAYS: + effectDateEnd = DateTime.UtcNow.AddDays(60); + break; + case (int)ItemVnums.INVENTORY_EXPANSION_TICKET_PERMANENT: + effectDateEnd = null; + break; + default: + Log.Warning("Well, seems like another item that I didn't expect has the same effect."); + effectDateEnd = default; + break; + } + + await session.EmitEventAsync(new AddStaticBonusEvent(new CharacterStaticBonusDto + { + DateEnd = effectDateEnd, + ItemVnum = e.Item.ItemInstance.GameItem.Id, + StaticBonusType = StaticBonusType.InventoryExpansion + })); + + await session.RemoveItemFromInventory(item: e.Item); + session.ShowInventoryExtensions(); + + string name = _gameLanguage.GetLanguage(GameDataType.Item, e.Item.ItemInstance.GameItem.Name, session.UserLanguage); + session.SendChatMessage( + _gameLanguage.GetLanguageFormat(GameDialogKey.ITEM_CHATMESSAGE_EFFECT_ACTIVATED, session.UserLanguage, name), + ChatMessageColorType.Green); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/ItemSpawnHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/ItemSpawnHandler.cs new file mode 100644 index 0000000..128eba5 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/ItemSpawnHandler.cs @@ -0,0 +1,33 @@ +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.Game._enum; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Entities.Event; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Main.Special; + +public class ItemSpawnHandler : IItemHandler +{ + private readonly INpcEntityFactory _npcEntityFactory; + + public ItemSpawnHandler(INpcEntityFactory npcEntityFactory) => _npcEntityFactory = npcEntityFactory; + + public ItemType ItemType => ItemType.Special; + public long[] Effects { get; } = { (short)ItemEffectVnums.SPAWN_NPC }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + INpcEntity npcEntity = _npcEntityFactory.CreateNpc(e.Item.ItemInstance.GameItem.EffectValue, session.CurrentMapInstance); + if (npcEntity == null) + { + return; + } + + await session.RemoveItemFromInventory(item: e.Item, amount: 1); + await npcEntity.EmitEventAsync(new MapJoinNpcEntityEvent(npcEntity, session.PlayerEntity.PositionX, session.PlayerEntity.PositionY)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/MagicLampHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/MagicLampHandler.cs new file mode 100644 index 0000000..f8a1023 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/MagicLampHandler.cs @@ -0,0 +1,56 @@ +using System.Linq; +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Networking.Broadcasting; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Character; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Main.Special; + +public class MagicLampHandler : IItemUsageByVnumHandler +{ + private readonly IGameLanguageService _gameLanguage; + + public MagicLampHandler(IGameLanguageService gameLanguage) => _gameLanguage = gameLanguage; + + public long[] Vnums => new[] { (long)ItemVnums.MAGIC_LAMP }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + if (session.PlayerEntity.IsOnVehicle || !session.PlayerEntity.IsAlive() || session.PlayerEntity.UseSp) + { + return; + } + + if (session.PlayerEntity.EquippedItems.Any(i => i != null && i.ItemInstance.GameItem.Type == InventoryType.EquippedItems)) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_EQ_NOT_EMPTY, session.UserLanguage), MsgMessageType.Middle); + return; + } + + switch (e.Option) + { + case 0: + session.SendQnaPacket($"u_i 1 {session.PlayerEntity.Id} {(byte)e.Item.ItemInstance.GameItem.Type} {e.Item.Slot} 3 ", + _gameLanguage.GetLanguage(GameDialogKey.ITEM_DIALOG_ASK_USE, session.UserLanguage)); + break; + default: + session.PlayerEntity.Gender = session.PlayerEntity.Gender == GenderType.Female ? GenderType.Male : GenderType.Female; + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_SEX_CHANGED, session.UserLanguage), MsgMessageType.Middle); + + session.BroadcastEq(); + session.SendGenderPacket(); + session.BroadcastEffect(EffectType.Transform, new RangeBroadcast(session.PlayerEntity.PositionX, session.PlayerEntity.PositionY)); + await session.RemoveItemFromInventory(item: e.Item); + break; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/MateSlotExpansionHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/MateSlotExpansionHandler.cs new file mode 100644 index 0000000..09801fe --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/MateSlotExpansionHandler.cs @@ -0,0 +1,73 @@ +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsAPI.Game.Extensions.ItemExtension.Item; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Main.Special; + +public class MateSlotExpansionHandler : IItemUsageByVnumHandler +{ + /** + * These values can be configurable if needed + */ + private const int MaxPet = 90; + + private const int MaxPartner = 12; + private const int PetExpansionAdd = 10; + private const int PartnerExpansionAdd = 1; + + private readonly IGameLanguageService _languageService; + + public MateSlotExpansionHandler(IGameLanguageService languageService) => _languageService = languageService; + + public long[] Vnums => new[] + { + (long)ItemVnums.PARTNER_SLOT_EXPANSION, (long)ItemVnums.PARTNER_SLOT_EXPANSION_LIMITED, + (long)ItemVnums.PET_SLOT_EXPANSION, (long)ItemVnums.PET_SLOT_EXPANSION_LIMITED + }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + bool isPetExpansion = e.Item.ItemInstance.ItemVNum == (int)ItemVnums.PET_SLOT_EXPANSION || e.Item.ItemInstance.ItemVNum == (int)ItemVnums.PET_SLOT_EXPANSION_LIMITED; + if (isPetExpansion && session.PlayerEntity.MaxPetCount >= MaxPet) + { + session.SendMsg(_languageService.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_MAX_PET_SLOTS, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (!isPetExpansion && session.PlayerEntity.MaxPartnerCount >= MaxPartner) + { + session.SendMsg(_languageService.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_MAX_PARTNER_SLOTS, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (e.Option == 0) + { + session.SendPacket( + $"qna #u_i^1^{session.PlayerEntity.Id}^{(byte)e.Item.ItemInstance.GameItem.Type}^{e.Item.Slot}^2 {_languageService.GetLanguage(isPetExpansion ? GameDialogKey.PET_DIALOG_ASK_SLOT_INCREASE : GameDialogKey.PARTNER_DIALOG_ASK_SLOT_INCREASE, session.UserLanguage)}"); + return; + } + + if (isPetExpansion) + { + session.PlayerEntity.MaxPetCount += PetExpansionAdd; + } + else + { + session.PlayerEntity.MaxPartnerCount += PartnerExpansionAdd; + } + + string itemName = e.Item.ItemInstance.GameItem.GetItemName(_languageService, session.UserLanguage); + + session.SendChatMessage(_languageService.GetLanguageFormat(GameDialogKey.ITEM_CHATMESSAGE_EFFECT_ACTIVATED, session.UserLanguage, itemName), ChatMessageColorType.Green); + session.SendScpStcPacket(); + + await session.RemoveItemFromInventory(item: e.Item); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/MinilandSignHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/MinilandSignHandler.cs new file mode 100644 index 0000000..e33933d --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/MinilandSignHandler.cs @@ -0,0 +1,71 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Entities.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Main.Special; + +public class MinilandSignHandler : IItemHandler +{ + private readonly IAsyncEventPipeline _asyncEvent; + private readonly INpcEntityFactory _npcEntityFactory; + + public MinilandSignHandler(INpcEntityFactory npcEntityFactory, IAsyncEventPipeline asyncEvent) + { + _npcEntityFactory = npcEntityFactory; + _asyncEvent = asyncEvent; + } + + public ItemType ItemType => ItemType.Special; + public long[] Effects => new long[] { 100 }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + if (!session.PlayerEntity.IsAlive()) + { + return; + } + + if (session.PlayerEntity.IsOnVehicle) + { + return; + } + + if (!session.CurrentMapInstance.HasMapFlag(MapFlags.HAS_SIGNPOSTS_ENABLED)) + { + session.SendChatMessage(session.GetLanguage(GameDialogKey.ITEM_CHATMESSAGE_CANT_USE_THAT), ChatMessageColorType.Yellow); + return; + } + + INpcEntity findSignPost = session.CurrentMapInstance.GetPassiveNpcs().FirstOrDefault(x => x.MinilandOwner != null && x.MinilandOwner.Id == session.PlayerEntity.Id); + if (findSignPost != null) + { + string timeLeft = TimeSpan.FromSeconds(findSignPost.Hp / 5.0).ToString(@"hh\:mm\:ss"); + session.SendChatMessage(session.GetLanguageFormat(GameDialogKey.ITEM_CHATMESSAGE_SIGNPOST_ALREADY_IN, timeLeft), ChatMessageColorType.Yellow); + return; + } + + INpcEntity newSign = _npcEntityFactory.CreateNpc(e.Item.ItemInstance.GameItem.EffectValue, session.CurrentMapInstance, null, new NpcAdditionalData + { + MinilandOwner = session.PlayerEntity, + NpcShouldRespawn = false + }); + + newSign.Direction = 2; + + await _asyncEvent.ProcessEventAsync(new MapJoinNpcEntityEvent(newSign, session.PlayerEntity.PositionX, session.PlayerEntity.PositionY)); + await session.RemoveItemFromInventory(item: e.Item); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/NosBazaarGoldMedalHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/NosBazaarGoldMedalHandler.cs new file mode 100644 index 0000000..e751ba4 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/NosBazaarGoldMedalHandler.cs @@ -0,0 +1,47 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.DTOs.Bonus; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Main.Special; + +public class NosBazaarGoldMedalHandler : IItemHandler +{ + private readonly IGameLanguageService _gameLanguage; + + public NosBazaarGoldMedalHandler(IGameLanguageService gameLanguage) => _gameLanguage = gameLanguage; + + public ItemType ItemType => ItemType.Special; + public long[] Effects => new long[] { 1003 }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + if (session.PlayerEntity.HaveStaticBonus(StaticBonusType.BazaarMedalGold) || session.PlayerEntity.HaveStaticBonus(StaticBonusType.BazaarMedalSilver)) + { + return; + } + + await session.EmitEventAsync(new AddStaticBonusEvent(new CharacterStaticBonusDto + { + DateEnd = DateTime.UtcNow.AddDays(e.Item.ItemInstance.GameItem.EffectValue), + ItemVnum = e.Item.ItemInstance.GameItem.Id, + StaticBonusType = StaticBonusType.BazaarMedalGold + })); + + await session.RemoveItemFromInventory(item: e.Item); + string name = _gameLanguage.GetLanguage(GameDataType.Item, e.Item.ItemInstance.GameItem.Name, session.UserLanguage); + session.SendChatMessage(_gameLanguage.GetLanguageFormat(GameDialogKey.ITEM_CHATMESSAGE_EFFECT_ACTIVATED, session.UserLanguage, name), ChatMessageColorType.Green); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/NosBazaarSilverMedalHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/NosBazaarSilverMedalHandler.cs new file mode 100644 index 0000000..cd3b986 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/NosBazaarSilverMedalHandler.cs @@ -0,0 +1,47 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.DTOs.Bonus; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Main.Special; + +public class NosBazaarSilverMedalHandler : IItemHandler +{ + private readonly IGameLanguageService _gameLanguage; + + public NosBazaarSilverMedalHandler(IGameLanguageService gameLanguage) => _gameLanguage = gameLanguage; + + public ItemType ItemType => ItemType.Special; + public long[] Effects => new long[] { 1004 }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + if (session.PlayerEntity.HaveStaticBonus(StaticBonusType.BazaarMedalGold) || session.PlayerEntity.HaveStaticBonus(StaticBonusType.BazaarMedalSilver)) + { + return; + } + + await session.EmitEventAsync(new AddStaticBonusEvent(new CharacterStaticBonusDto + { + DateEnd = DateTime.UtcNow.AddDays(e.Item.ItemInstance.GameItem.EffectValue), + ItemVnum = e.Item.ItemInstance.GameItem.Id, + StaticBonusType = StaticBonusType.BazaarMedalSilver + })); + + await session.RemoveItemFromInventory(item: e.Item); + string name = _gameLanguage.GetLanguage(GameDataType.Item, e.Item.ItemInstance.GameItem.Name, session.UserLanguage); + session.SendChatMessage(_gameLanguage.GetLanguageFormat(GameDialogKey.ITEM_CHATMESSAGE_EFFECT_ACTIVATED, session.UserLanguage, name), ChatMessageColorType.Green); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/PartnerBackpackHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/PartnerBackpackHandler.cs new file mode 100644 index 0000000..35cb3a7 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/PartnerBackpackHandler.cs @@ -0,0 +1,53 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.DTOs.Bonus; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Main.Special; + +public class PartnerBackpackHandler : IItemHandler +{ + private readonly IGameLanguageService _gameLanguage; + + public PartnerBackpackHandler(IGameLanguageService gameLanguage) => _gameLanguage = gameLanguage; + + public ItemType ItemType => ItemType.Special; + public long[] Effects => new long[] { 602, 1008 }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + if (session.PlayerEntity.HaveStaticBonus(StaticBonusType.PartnerBackpack)) + { + return; + } + + DateTime? effectDateEnd = e.Item.ItemInstance.GameItem.EffectValue == 0 ? null : DateTime.UtcNow.AddDays(e.Item.ItemInstance.GameItem.EffectValue); + + await session.EmitEventAsync(new AddStaticBonusEvent(new CharacterStaticBonusDto + { + DateEnd = effectDateEnd, + ItemVnum = e.Item.ItemInstance.GameItem.Id, + StaticBonusType = StaticBonusType.PartnerBackpack + })); + + await session.RemoveItemFromInventory(item: e.Item); + session.ShowInventoryExtensions(); + + string name = _gameLanguage.GetLanguage(GameDataType.Item, e.Item.ItemInstance.GameItem.Name, session.UserLanguage); + session.SendChatMessage( + _gameLanguage.GetLanguageFormat(GameDialogKey.ITEM_CHATMESSAGE_EFFECT_ACTIVATED, session.UserLanguage, name), + ChatMessageColorType.Green); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/PetBasketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/PetBasketHandler.cs new file mode 100644 index 0000000..123a531 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/PetBasketHandler.cs @@ -0,0 +1,62 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.DTOs.Bonus; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Main.Special; + +public class PetBasketHandler : IItemHandler +{ + private readonly IGameLanguageService _gameLanguage; + + public PetBasketHandler(IGameLanguageService gameLanguage) => _gameLanguage = gameLanguage; + + public ItemType ItemType => ItemType.Special; + public long[] Effects => new long[] { 252, 603, 1007 }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + if (session.PlayerEntity.HaveStaticBonus(StaticBonusType.PetBasket)) + { + return; + } + + await session.EmitEventAsync(new AddStaticBonusEvent(new CharacterStaticBonusDto + { + DateEnd = EffectDateEndCalculation(e), + ItemVnum = e.Item.ItemInstance.GameItem.Id, + StaticBonusType = StaticBonusType.PetBasket + })); + + await session.RemoveItemFromInventory(item: e.Item); + session.ShowInventoryExtensions(); + session.SendPetBasketPacket(true); + + string name = _gameLanguage.GetLanguage(GameDataType.Item, e.Item.ItemInstance.GameItem.Name, session.UserLanguage); + session.SendChatMessage( + _gameLanguage.GetLanguageFormat(GameDialogKey.ITEM_CHATMESSAGE_EFFECT_ACTIVATED, session.UserLanguage, name), + ChatMessageColorType.Green); + } + + private static DateTime? EffectDateEndCalculation(InventoryUseItemEvent e) + { + if (e.Item.ItemInstance.GameItem.Effect == 252) + { + return DateTime.UtcNow.AddDays(10); + } + + return e.Item.ItemInstance.GameItem.EffectValue == 0 ? null : DateTime.UtcNow.AddDays(e.Item.ItemInstance.GameItem.EffectValue); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/PresentationMessageHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/PresentationMessageHandler.cs new file mode 100644 index 0000000..51d7209 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/PresentationMessageHandler.cs @@ -0,0 +1,30 @@ +using System.Threading.Tasks; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Main.Special; + +public class PresentationMessageHandler : IItemHandler +{ + public ItemType ItemType => ItemType.Special; + public long[] Effects => new long[] { 203 }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + if (session.PlayerEntity.IsOnVehicle || !session.PlayerEntity.IsAlive()) + { + return; + } + + if (e.Option != 0) + { + return; + } + + session.SendGuriPacket(10, 2, 1); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/ReputationMedalHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/ReputationMedalHandler.cs new file mode 100644 index 0000000..3b429bf --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/ReputationMedalHandler.cs @@ -0,0 +1,30 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Main.Special; + +public class ReputationMedalHandler : IItemHandler +{ + public ItemType ItemType => ItemType.Special; + public long[] Effects => new long[] { 69 }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + await session.EmitEventAsync(new GenerateReputationEvent + { + Amount = (int)e.Item.ItemInstance.GameItem.ReputPrice, + SendMessage = true + }); + + await session.RemoveItemFromInventory(item: e.Item); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/SealedVesselHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/SealedVesselHandler.cs new file mode 100644 index 0000000..e425e3d --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/SealedVesselHandler.cs @@ -0,0 +1,121 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Monster.Event; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Main.Special; + +public class SealedVesselHandler : IItemHandler +{ + private const int MAXIMUM_VESSELS_IN_MINILAND = 30; + private const int MAXIMUM_VESSELS_IN_NORMAL_MAP = 10; + private readonly IAsyncEventPipeline _asyncEventPipeline; + private readonly IDelayManager _delayManager; + private readonly INpcMonsterManager _npcMonsterManager; + private readonly IRandomGenerator _randomGenerator; + + private readonly HashSet _vNums = new() + { + 1386, 1387, 1388, 1389, 1390, 1391, 1392, + 1393, 1394, 1395, 1396, 1397, 1398, 1399, + 1400, 1401, 1402, 1403, 1404, 1405 + }; + + public SealedVesselHandler(IRandomGenerator randomGenerator, INpcMonsterManager npcMonsterManager, IAsyncEventPipeline asyncEventPipeline, IGameLanguageService gameLanguage, + IDelayManager delayManager) + { + _randomGenerator = randomGenerator; + _npcMonsterManager = npcMonsterManager; + _asyncEventPipeline = asyncEventPipeline; + _delayManager = delayManager; + } + + public ItemType ItemType => ItemType.Special; + public long[] Effects => new long[] { 1002 }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + if (session.CurrentMapInstance.HasMapFlag(MapFlags.HAS_SEALED_VESSELS_DISABLED)) + { + session.SendChatMessage(session.GetLanguage(GameDialogKey.ITEM_MESSAGE_CANT_USE), ChatMessageColorType.Yellow); + session.SendMsg(session.GetLanguage(GameDialogKey.ITEM_MESSAGE_CANT_USE), MsgMessageType.Middle); + return; + } + + if (!session.CurrentMapInstance.HasMapFlag(MapFlags.IS_BASE_MAP) && session.CurrentMapInstance.MapInstanceType != MapInstanceType.Miniland) + { + session.SendChatMessage(session.GetLanguage(GameDialogKey.ITEM_MESSAGE_CANT_USE), ChatMessageColorType.Yellow); + session.SendMsg(session.GetLanguage(GameDialogKey.ITEM_MESSAGE_CANT_USE), MsgMessageType.Middle); + return; + } + + if (session.PlayerEntity.MapInstance.IsBlockedZone(session.PlayerEntity.PositionX, session.PlayerEntity.PositionY)) + { + return; + } + + byte currentVessels = session.PlayerEntity.MapInstance.CurrentVessels(); + if (currentVessels >= (session.PlayerEntity.MapInstance.MapInstanceType == MapInstanceType.Miniland ? MAXIMUM_VESSELS_IN_MINILAND : MAXIMUM_VESSELS_IN_NORMAL_MAP)) + { + session.SendMsg(session.GetLanguage(GameDialogKey.INTERACTION_VESSEL_LIMIT_REACHED), MsgMessageType.Middle); + return; + } + + if (e.Option == 0) + { + DateTime waitUntil = await _delayManager.RegisterAction(session.PlayerEntity, DelayedActionType.SealedVessel); + session.SendDelay((int)(waitUntil - DateTime.UtcNow).TotalMilliseconds, GuriType.UsingItem, $"u_i 1 {session.PlayerEntity.Id} {(byte)e.Item.ItemInstance.GameItem.Type} {e.Item.Slot} 1"); + return; + } + + bool canEquipVehicle = await _delayManager.CanPerformAction(session.PlayerEntity, DelayedActionType.SealedVessel); + if (!canEquipVehicle) + { + return; + } + + await _delayManager.CompleteAction(session.PlayerEntity, DelayedActionType.SealedVessel); + + short vNum = _vNums.ElementAt(_randomGenerator.RandomNumber(_vNums.Count)); + IMonsterData npcMonster = _npcMonsterManager.GetNpc(vNum); + if (npcMonster == null) + { + return; + } + + await session.RemoveItemFromInventory(item: e.Item); + + await _asyncEventPipeline.ProcessEventAsync(new MonsterSummonEvent(session.CurrentMapInstance, new[] + { + new ToSummon + { + VNum = vNum, + SpawnCell = new Position(session.PlayerEntity.PositionX, session.PlayerEntity.PositionY), + IsVesselMonster = true, + IsMoving = true, + IsHostile = false + } + })); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/SeparationLetterHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/SeparationLetterHandler.cs new file mode 100644 index 0000000..9f423a6 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/SeparationLetterHandler.cs @@ -0,0 +1,57 @@ +using System.Linq; +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsAPI.Game.Extensions.RelationsExtensions; +using WingsEmu.DTOs.Relations; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Groups.Events; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Relations; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Main.Special; + +public class SeparationLetterHandler : IItemHandler +{ + private readonly IGameLanguageService _languageService; + private readonly ISessionManager _sessionManager; + + public SeparationLetterHandler(ISessionManager sessionManager, IGameLanguageService languageService) + { + _sessionManager = sessionManager; + _languageService = languageService; + } + + public ItemType ItemType => ItemType.Special; + public long[] Effects => new long[] { 6969 }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + CharacterRelationDTO rel = session.PlayerEntity.GetRelations().FirstOrDefault(x => x.RelationType == CharacterRelationType.Spouse); + if (rel == null) + { + return; + } + + if (e.Option == 0) + { + session.SendQnaPacket($"u_i 1 {session.PlayerEntity.Id} {(int)e.Item.InventoryType} {e.Item.Slot} 1", session.GetLanguage(GameDialogKey.WEDDING_DIALOG_ASK_DIVORCE_CONFIRM)); + return; + } + + await session.RemoveRelationAsync(rel.RelatedCharacterId, CharacterRelationType.Spouse); + + session.SendInfo(_languageService.GetLanguage(GameDialogKey.WEDDING_INFO_DIVORCED, session.UserLanguage)); + await session.RemoveItemFromInventory(item: e.Item); + + await session.EmitEventAsync(new GroupWeedingEvent + { + RemoveBuff = true, + RelatedId = rel.RelatedCharacterId + }); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/SpPointPotionHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/SpPointPotionHandler.cs new file mode 100644 index 0000000..550998d --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/SpPointPotionHandler.cs @@ -0,0 +1,47 @@ +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Main.Special; + +public class SpPointPotionHandler : IItemHandler +{ + private readonly IGameLanguageService _languageService; + private readonly IServerManager _serverManager; + + public SpPointPotionHandler(IGameLanguageService languageService, IServerManager serverManager) + { + _languageService = languageService; + _serverManager = serverManager; + } + + public ItemType ItemType => ItemType.Special; + public long[] Effects => new long[] { 150, 152 }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + if (session.PlayerEntity.SpPointsBonus == _serverManager.MaxAdditionalSpPoints) + { + session.SendChatMessage(_languageService.GetLanguage(GameDialogKey.ITEM_MESSAGE_CANT_USE, session.UserLanguage), ChatMessageColorType.Yellow); + return; + } + + int points = e.Item.ItemInstance.GameItem.Data[2]; + session.PlayerEntity.SpPointsBonus += points; + if (session.PlayerEntity.SpPointsBonus > _serverManager.MaxAdditionalSpPoints) + { + session.PlayerEntity.SpPointsBonus = _serverManager.MaxAdditionalSpPoints; + } + + session.SendMsg(_languageService.GetLanguageFormat(GameDialogKey.SPECIALIST_SHOUTMESSAGE_POINTS_ADDED, session.UserLanguage, points), MsgMessageType.Middle); + session.RefreshSpPoint(); + await session.RemoveItemFromInventory(item: e.Item); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/SpWingHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/SpWingHandler.cs new file mode 100644 index 0000000..2e34139 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/SpWingHandler.cs @@ -0,0 +1,107 @@ +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Configuration; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Main.Special; + +public class SpWingHandler : IItemHandler +{ + private readonly IBuffFactory _buffFactory; + private readonly IAsyncEventPipeline _eventPipeline; + private readonly IGameLanguageService _languageService; + private readonly ISpWingConfiguration _spWingConfiguration; + + public SpWingHandler(IGameLanguageService languageService, ISpWingConfiguration spWingConfiguration, IAsyncEventPipeline eventPipeline, IBuffFactory buffFactory) + { + _languageService = languageService; + _spWingConfiguration = spWingConfiguration; + _eventPipeline = eventPipeline; + _buffFactory = buffFactory; + } + + public ItemType ItemType => ItemType.Special; + public long[] Effects => new long[] { 650 }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + if (!session.HasCurrentMapInstance) + { + return; + } + + if (session.PlayerEntity.HasShopOpened) + { + return; + } + + if (session.PlayerEntity.IsOnVehicle) + { + return; + } + + if (!session.PlayerEntity.UseSp || session.PlayerEntity.Specialist == null) + { + session.SendMsg(_languageService.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_NO_SPECIALIST_CARD, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (session.PlayerEntity.Specialist.Design == e.Item.ItemInstance.GameItem.EffectValue && session.PlayerEntity.MorphUpgrade2 == e.Item.ItemInstance.GameItem.EffectValue) + { + session.SendMsg(_languageService.GetLanguage(GameDialogKey.ITEM_SHOUTMESSAGE_SAME_SP_WINGS_ALREADY_SET, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (session.PlayerEntity.Specialist.Upgrade == 0) + { + session.SendMsg(_languageService.GetLanguage(GameDialogKey.INTERACTION_SHOUTMESSAGE_NEED_SP_UPGRADE_FOR_WINGS, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (e.Option == 0) + { + session.SendQnaPacket($"u_i 1 {session.PlayerEntity.Id} {(byte)e.Item.ItemInstance.GameItem.Type} {e.Item.Slot} 3", + _languageService.GetLanguage(GameDialogKey.ITEM_DIALOG_ASK_CHANGE_SP_WINGS, session.UserLanguage)); + return; + } + + SpWingInfo newWingInfo = _spWingConfiguration.GetSpWingInfo(e.Item.ItemInstance.GameItem.EffectValue); + SpWingInfo oldWingInfo = _spWingConfiguration.GetSpWingInfo(session.PlayerEntity.MorphUpgrade2); + + if (oldWingInfo != null) + { + foreach (WingBuff buff in oldWingInfo.Buffs) + { + Buff wingBuff = session.PlayerEntity.BuffComponent.GetBuff(buff.BuffId); + await session.PlayerEntity.RemoveBuffAsync(buff.IsPermanent, wingBuff); + } + } + + session.PlayerEntity.Specialist.Design = (byte)e.Item.ItemInstance.GameItem.EffectValue; + session.PlayerEntity.MorphUpgrade2 = e.Item.ItemInstance.GameItem.EffectValue; + + session.BroadcastCMode(); + session.RefreshStat(); + session.RefreshStatChar(); + + if (newWingInfo != null) + { + foreach (WingBuff buff in newWingInfo.Buffs) + { + await session.PlayerEntity.AddBuffAsync(_buffFactory.CreateBuff(buff.BuffId, session.PlayerEntity, buff.IsPermanent ? BuffFlag.NO_DURATION : BuffFlag.NORMAL)); + } + } + + await session.RemoveItemFromInventory(item: e.Item); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/SpecialPotionHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/SpecialPotionHandler.cs new file mode 100644 index 0000000..aa3cb31 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/SpecialPotionHandler.cs @@ -0,0 +1,121 @@ +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Main.Special; + +/// +/// This handler is called for Small/Medium/Large NosMall sp point potions and Fafnir's Fried Dinner +/// +public class SpecialPotionHandler : IItemHandler +{ + /** + * Probably better to switch to enum ? + * Idk if it's used somewhere else + */ + private const int Small = 1; + + private const int Medium = 2; + private const int Large = 3; + private const int Fafnir = 4; + + private readonly IAsyncEventPipeline _eventPipeline; + private readonly IGameLanguageService _gameLanguage; + private readonly IServerManager _serverManager; + + public SpecialPotionHandler(IAsyncEventPipeline eventPipeline, IGameLanguageService gameLanguage, IServerManager serverManager) + { + _eventPipeline = eventPipeline; + _gameLanguage = gameLanguage; + _serverManager = serverManager; + } + + public ItemType ItemType => ItemType.Special; + public long[] Effects => new long[] { 151 }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + if (session.PlayerEntity.Hp == session.PlayerEntity.MaxHp && session.PlayerEntity.SpPointsBonus == _serverManager.MaxAdditionalSpPoints) + { + return; + } + + if (session.PlayerEntity.RainbowBattleComponent.IsInRainbowBattle) + { + session.SendChatMessage(session.GetLanguage(GameDialogKey.ITEM_MESSAGE_CANT_USE), ChatMessageColorType.Yellow); + return; + } + + int type = e.Item.ItemInstance.GameItem.EffectValue; + + /* + * Unlike classic Sp point potion/classic potion there is no values specified in Item.dat (for sp point), so it's hardcoded :c + * Actually these values was hardcoded directly at parsing, i've removed them so we can easily custom these values via a config file or something else in the future + */ + + if (type != Fafnir) + { + if (session.CantPerformActionOnAct4()) + { + return; + } + } + + switch (type) + { + case Small: + session.PlayerEntity.SpPointsBonus += 2500; + session.PlayerEntity.Hp = session.PlayerEntity.MaxHp; + session.SendMsg(_gameLanguage.GetLanguageFormat(GameDialogKey.SPECIALIST_SHOUTMESSAGE_POINTS_ADDED, session.UserLanguage, 2500), MsgMessageType.Middle); + break; + case Medium: + session.PlayerEntity.SpPointsBonus += 5000; + session.PlayerEntity.Hp = session.PlayerEntity.MaxHp; + session.PlayerEntity.Mp = session.PlayerEntity.MaxMp; + session.SendMsg(_gameLanguage.GetLanguageFormat(GameDialogKey.SPECIALIST_SHOUTMESSAGE_POINTS_ADDED, session.UserLanguage, 5000), MsgMessageType.Middle); + break; + case Large: + session.PlayerEntity.SpPointsBonus += 10000; + session.PlayerEntity.Hp = session.PlayerEntity.MaxHp; + session.PlayerEntity.Mp = session.PlayerEntity.MaxMp; + session.SendMsg(_gameLanguage.GetLanguageFormat(GameDialogKey.SPECIALIST_SHOUTMESSAGE_POINTS_ADDED, session.UserLanguage, 10000), MsgMessageType.Middle); + await session.PlayerEntity.RemoveNegativeBuffs(99); + break; + case Fafnir: + session.PlayerEntity.SpPointsBonus += 100; + session.PlayerEntity.Hp += session.PlayerEntity.MaxHp / 100 * 20; + session.PlayerEntity.Mp += session.PlayerEntity.MaxMp / 100 * 20; + session.SendMsg(_gameLanguage.GetLanguageFormat(GameDialogKey.SPECIALIST_SHOUTMESSAGE_POINTS_ADDED, session.UserLanguage, 100), MsgMessageType.Middle); + await session.PlayerEntity.RemoveNegativeBuffs(3); + break; + } + + if (session.PlayerEntity.Hp > session.PlayerEntity.MaxHp) + { + session.PlayerEntity.Hp = session.PlayerEntity.MaxHp; + } + + if (session.PlayerEntity.Mp > session.PlayerEntity.MaxMp) + { + session.PlayerEntity.Mp = session.PlayerEntity.MaxMp; + } + + if (session.PlayerEntity.SpPointsBonus > _serverManager.MaxAdditionalSpPoints) + { + session.PlayerEntity.SpPointsBonus = _serverManager.MaxAdditionalSpPoints; + } + + session.RefreshStat(); + session.RefreshSpPoint(); + + await session.RemoveItemFromInventory(item: e.Item); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/SpecialistSigilHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/SpecialistSigilHandler.cs new file mode 100644 index 0000000..ff669aa --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/SpecialistSigilHandler.cs @@ -0,0 +1,71 @@ +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.DTOs.Items; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; +using WingsEmu.Plugins.BasicImplementations.Event.Characters; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Main.Special; + +public class SpecialistSigilHandler : IItemHandler +{ + private readonly IGameLanguageService _gameLanguage; + + public SpecialistSigilHandler(IGameLanguageService gameLanguage) => _gameLanguage = gameLanguage; + + public ItemType ItemType => ItemType.Upgrade; + public long[] Effects => new long[] { 10000 }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + string[] packetsplit = e.Packet; + + if (packetsplit == null || packetsplit.Length <= 9) + { + return; + } + + if (!byte.TryParse(packetsplit[8], out byte typeEquip) || + !short.TryParse(packetsplit[9], out short slotEquip)) + { + return; + } + + if (session.PlayerEntity.IsSitting) + { + await session.EmitEventAsync(new PlayerRestEvent + { + RestTeamMemberMates = false + }); + } + + InventoryItem equip = session.PlayerEntity.GetItemBySlotAndType(slotEquip, (InventoryType)typeEquip); + if (equip == null) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.ITEM_MESSAGE_CANT_USE, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (equip.ItemInstance.Type != ItemInstanceType.SpecialistInstance) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.ITEM_MESSAGE_CANT_USE, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (equip.ItemInstance.Upgrade >= 15) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.ITEM_MESSAGE_CANT_USE, session.UserLanguage), MsgMessageType.Middle); + return; + } + + await session.EmitEventAsync(new SpUpgradeEvent(UpgradeProtection.Protected, equip, true)); + await session.RemoveItemFromInventory(item: e.Item); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/SpeedBoosterHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/SpeedBoosterHandler.cs new file mode 100644 index 0000000..fc78468 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/SpeedBoosterHandler.cs @@ -0,0 +1,44 @@ +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.Game._enum; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Plugins.BasicImplementations.Vehicles; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Main.Special; + +public class SpeedBoosterHandler : IItemHandler +{ + private readonly IVehicleConfigurationProvider _provider; + + public SpeedBoosterHandler(IVehicleConfigurationProvider provider) => _provider = provider; + + public ItemType ItemType => ItemType.Special; + public long[] Effects => new long[] { 998 }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + if (!session.PlayerEntity.IsOnVehicle) + { + return; + } + + if (session.PlayerEntity.HasBuff(BuffVnums.SPEED_BOOSTER)) + { + return; + } + + VehicleConfiguration vehicle = _provider.GetByMorph(session.PlayerEntity.Morph, session.PlayerEntity.Gender); + + if (vehicle?.VehicleBoostType == null) + { + return; + } + + await session.EmitEventAsync(new SpeedBoosterEvent()); + await session.RemoveItemFromInventory(item: e.Item); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/StatPotionHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/StatPotionHandler.cs new file mode 100644 index 0000000..8047928 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/StatPotionHandler.cs @@ -0,0 +1,32 @@ +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.Game._enum; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Main.Special; + +/// +/// This handler is called when you try to use Atk/Def/Hp/Exp potions +/// +public class StatPotionHandler : IItemUsageByVnumHandler +{ + private readonly IBCardEffectHandlerContainer _bCardEffectHandlerContainer; + + public StatPotionHandler(IBCardEffectHandlerContainer bCardEffectHandlerContainer) => _bCardEffectHandlerContainer = bCardEffectHandlerContainer; + + public long[] Vnums => new[] + { + (long)ItemVnums.ATTACK_POTION, (long)ItemVnums.ATTACK_POTION_LIMITED, + (long)ItemVnums.DEFENCE_POTION, (long)ItemVnums.DEFENCE_POTION_LIMITED, + (long)ItemVnums.ENERGY_POTION, (long)ItemVnums.ENERGY_POTION_LIMITED, + (long)ItemVnums.EXPERIENCE_POTION, (long)ItemVnums.EXPERIENCE_POTION_LIMITED + }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + await session.RemoveItemFromInventory(item: e.Item); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/SuctionFunnelHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/SuctionFunnelHandler.cs new file mode 100644 index 0000000..a835346 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/SuctionFunnelHandler.cs @@ -0,0 +1,68 @@ +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Items; +using WingsEmu.Game.Monster.Event; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Main.Special; + +public class SuctionFunnelHandler : IItemHandler +{ + private readonly IAsyncEventPipeline _eventPipeline; + private readonly IGameItemInstanceFactory _gameItemInstanceFactory; + + private readonly IGameLanguageService _gameLanguage; + + public SuctionFunnelHandler(IGameLanguageService gameLanguage, IAsyncEventPipeline eventPipeline, IGameItemInstanceFactory gameItemInstanceFactory) + { + _gameLanguage = gameLanguage; + _eventPipeline = eventPipeline; + _gameItemInstanceFactory = gameItemInstanceFactory; + } + + public ItemType ItemType => ItemType.Special; + public long[] Effects => new long[] { 400 }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + if (session.PlayerEntity == null) + { + return; + } + + IMonsterEntity kenko = session.CurrentMapInstance?.GetMonsterById(session.PlayerEntity.LastEntity.Item2); + if (kenko == null) + { + return; + } + + if (kenko.MonsterVNum > (short)MonsterVnum.ELITE_KENKO_RAIDER || kenko.MonsterVNum < (short)MonsterVnum.ROOKIE_KENKO_SWORDSMAN) + { + return; + } + + if (kenko.GetHpPercentage() >= 20) // check if HP is red + { + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.QUEST_CHATMESSAGE_KENKO_IS_TOO_STRONG, session.UserLanguage), ChatMessageColorType.Red); + return; + } + + await _eventPipeline.ProcessEventAsync(new MonsterDeathEvent(kenko)); + kenko.BroadcastDie(); + kenko.MapInstance.RemoveMonster(kenko); + + // Adds the bead and removes the Suction Funnel + GameItemInstance kenkoBead = _gameItemInstanceFactory.CreateItem((int)ItemVnums.KENKO_BEAD); + await session.AddNewItemToInventory(kenkoBead, sendGiftIsFull: true); + await session.RemoveItemFromInventory(item: e.Item); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/TimeSpaceStoneItemHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/TimeSpaceStoneItemHandler.cs new file mode 100644 index 0000000..321b2e4 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/TimeSpaceStoneItemHandler.cs @@ -0,0 +1,41 @@ +using System.Threading.Tasks; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Game.TimeSpaces.Events; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Main.Special; + +public class TimeSpaceStoneItemHandler : IItemHandler +{ + public ItemType ItemType => ItemType.Special; + public long[] Effects => new long[] { 140 }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + if (session.PlayerEntity.IsInGroup()) + { + session.SendMsg(session.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_NEED_LEAVE_GROUP), MsgMessageType.Middle); + return; + } + + if (session.CurrentMapInstance.HasMapFlag(MapFlags.ACT_4) || !session.CurrentMapInstance.HasMapFlag(MapFlags.IS_BASE_MAP)) + { + session.SendMsg(session.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_MUST_BE_IN_CLASSIC_MAP), MsgMessageType.Middle); + return; + } + + await session.EmitEventAsync(new TimeSpacePartyCreateEvent(e.Item.ItemInstance.GameItem.Data[2], e.Item)); + if (!session.PlayerEntity.TimeSpaceComponent.IsInTimeSpaceParty) + { + return; + } + + await session.EmitEventAsync(new TimeSpaceInstanceStartEvent()); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/VehicleHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/VehicleHandler.cs new file mode 100644 index 0000000..4713272 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/Special/VehicleHandler.cs @@ -0,0 +1,170 @@ +using System; +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.Groups; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Configuration; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Extensions.Mates; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Mates.Events; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Networking.Broadcasting; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Character; +using WingsEmu.Packets.Enums.Chat; +using WingsEmu.Plugins.BasicImplementations.Vehicles; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Main.Special; + +public class VehicleHandler : IItemHandler +{ + private readonly IDelayManager _delayManager; + private readonly IGameLanguageService _languageService; + private readonly IMapManager _mapManager; + private readonly IVehicleConfigurationProvider _provider; + private readonly ISpPartnerConfiguration _spPartner; + + public VehicleHandler(IDelayManager delayManager, IGameLanguageService languageService, IVehicleConfigurationProvider provider, ISpPartnerConfiguration spPartner, IMapManager mapManager) + { + _delayManager = delayManager; + _languageService = languageService; + _provider = provider; + _spPartner = spPartner; + _mapManager = mapManager; + } + + public ItemType ItemType => ItemType.Special; + public long[] Effects => new long[] { 1000 }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) + { + if (!session.HasCurrentMapInstance) + { + // log it and report + return; + } + + if (session.PlayerEntity.RainbowBattleComponent.IsInRainbowBattle) + { + session.SendChatMessage(session.GetLanguage(GameDialogKey.ITEM_MESSAGE_CANT_USE), ChatMessageColorType.Yellow); + return; + } + + if (session.PlayerEntity.HasShopOpened) + { + // log it and report for abusing + return; + } + + if (session.PlayerEntity.IsInExchange()) + { + return; + } + + if (!session.PlayerEntity.IsAlive()) + { + return; + } + + if (session.PlayerEntity.IsCastingSkill) + { + return; + } + + if (session.PlayerEntity.LastSkillUse.AddSeconds(2) > DateTime.UtcNow) + { + return; + } + + if (!session.PlayerEntity.IsOnVehicle && session.PlayerEntity.BuffComponent.HasBuff(BuffGroup.Bad)) + { + session.SendMsg(_languageService.GetLanguage(GameDialogKey.ITEM_SHOUTMESSAGE_VEHICLE_DEBUFF, session.UserLanguage), MsgMessageType.Middle); + return; + } + + VehicleConfiguration vehicle = _provider.GetByVehicleVnum(e.Item.ItemInstance.ItemVNum); + + if (vehicle == null) + { + return; + } + + if (session.PlayerEntity.IsMorphed) + { + session.SendChatMessage(_languageService.GetLanguage(GameDialogKey.ITEM_MESSAGE_CANT_USE, session.UserLanguage), ChatMessageColorType.Yellow); + return; + } + + if (e.Option == 0 && !session.PlayerEntity.IsOnVehicle) + { + if (session.PlayerEntity.IsSitting) + { + await session.RestAsync(); + } + + DateTime waitUntil = await _delayManager.RegisterAction(session.PlayerEntity, DelayedActionType.EquipVehicle); + session.SendDelay((int)(waitUntil - DateTime.UtcNow).TotalMilliseconds, GuriType.Transforming, + $"u_i 1 {session.PlayerEntity.Id} {(byte)e.Item.ItemInstance.GameItem.Type} {e.Item.Slot} 2"); + return; + } + + if (!session.PlayerEntity.IsOnVehicle && e.Option != 0) + { + bool canEquipVehicle = await _delayManager.CanPerformAction(session.PlayerEntity, DelayedActionType.EquipVehicle); + if (!canEquipVehicle) + { + return; + } + + await _delayManager.CompleteAction(session.PlayerEntity, DelayedActionType.EquipVehicle); + + session.PlayerEntity.IsOnVehicle = true; + session.PlayerEntity.VehicleSpeed = (byte)vehicle.DefaultSpeed; + session.PlayerEntity.MorphUpgrade = 0; + session.PlayerEntity.MorphUpgrade2 = 0; + session.PlayerEntity.Morph = session.PlayerEntity.Gender == GenderType.Male ? vehicle.MaleMorphId : vehicle.FemaleMorphId; + + foreach (IMateEntity x in session.PlayerEntity.MateComponent.TeamMembers()) + { + if (x.IsSitting && x.IsAlive()) + { + await session.EmitEventAsync(new MateRestEvent + { + MateEntity = x, + Force = true + }); + } + + session.Broadcast(x.GenerateOut()); + } + + session.RefreshParty(_spPartner); + + session.BroadcastEffect((int)EffectType.Transform, new RangeBroadcast(session.PlayerEntity.PositionX, session.PlayerEntity.PositionY)); + session.PlayerEntity.RefreshCharacterStats(); + session.SendCondPacket(); + session.BroadcastCMode(); + + session.PlayerEntity.LastSpeedChange = DateTime.UtcNow; + + await session.EmitEventAsync(new VehicleCheckMapSpeedEvent()); + + if (vehicle.RemoveItem is true) + { + await session.RemoveItemFromInventory(item: e.Item); + } + } + else if (session.PlayerEntity.IsOnVehicle) + { + await session.EmitEventAsync(new RemoveVehicleEvent(true)); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/TitleHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/TitleHandler.cs new file mode 100644 index 0000000..48f6711 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemUsage/Main/TitleHandler.cs @@ -0,0 +1,22 @@ +using System.Threading.Tasks; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.ItemUsage.Main; + +public class TitleHandler : IItemHandler +{ + private readonly IGameLanguageService _gameLanguage; + + public TitleHandler(IGameLanguageService gameLanguage) => _gameLanguage = gameLanguage; + + public ItemType ItemType => ItemType.Title; + public long[] Effects => new long[] { 0 }; + + public async Task HandleAsync(IClientSession session, InventoryUseItemEvent e) => session.SendQnaPacket($"guri 306 {e.Item.ItemInstance.ItemVNum} {e.Item.Slot}", + _gameLanguage.GetLanguage(GameDialogKey.TITLE_DIALOG_ASK_ADD, session.UserLanguage)); +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Mail/MailCreateEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Mail/MailCreateEventHandler.cs new file mode 100644 index 0000000..0a1ee65 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Mail/MailCreateEventHandler.cs @@ -0,0 +1,44 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Communication.Mail; +using WingsEmu.DTOs.Items; +using WingsEmu.DTOs.Mails; +using WingsEmu.Game.Items; +using WingsEmu.Game.Mails.Events; + +namespace WingsEmu.Plugins.BasicImplementations.Mail; + +public class MailCreateEventHandler : IAsyncEventProcessor +{ + private readonly IGameItemInstanceFactory _itemInstanceFactory; + private readonly MailCreationManager _mailCreationManager; + + public MailCreateEventHandler(MailCreationManager mailCreationManager, IGameItemInstanceFactory itemInstanceFactory) + { + _mailCreationManager = mailCreationManager; + _itemInstanceFactory = itemInstanceFactory; + } + + public async Task HandleAsync(MailCreateEvent e, CancellationToken cancellation) + { + GameItemInstance itemInstance = e.ItemInstance; + string senderName = e.SenderName; + long receiverId = e.ReceiverId; + MailGiftType mailGiftType = e.MailGiftType; + + if (itemInstance == null) + { + return; + } + + ItemInstanceDTO itemInstanceDto = _itemInstanceFactory.CreateDto(itemInstance); + _mailCreationManager.AddCreateMailRequest(new CreateMailRequest + { + SenderName = senderName, + ReceiverId = receiverId, + ItemInstance = itemInstanceDto, + MailGiftType = mailGiftType + }); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Mail/MailCreationManager.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Mail/MailCreationManager.cs new file mode 100644 index 0000000..231b11d --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Mail/MailCreationManager.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using PhoenixLib.Logging; +using Polly; +using Polly.Retry; +using WingsAPI.Communication; +using WingsAPI.Communication.Mail; +using WingsEmu.DTOs.Mails; + +namespace WingsEmu.Plugins.BasicImplementations.Mail; + +public class MailCreationManager : BackgroundService +{ + private static readonly TimeSpan RefreshDelay = TimeSpan.FromSeconds(Convert.ToInt32(Environment.GetEnvironmentVariable("MAIL_MANAGER_REFRESH_DELAY") ?? "5")); + + private readonly ConcurrentQueue _mailQueue = new(); + + private readonly IMailService _mailService; + + public MailCreationManager(IMailService mailService) => _mailService = mailService; + + public void AddCreateMailRequest(CreateMailRequest request) + { + _mailQueue.Enqueue(new CharacterMailDto + { + Date = DateTime.UtcNow, + SenderName = request.SenderName, + ReceiverId = request.ReceiverId, + MailGiftType = request.MailGiftType, + ItemInstance = request.ItemInstance + }); + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + try + { + await ProcessPendingMailsRequestsAsync(); + } + catch (Exception e) + { + Log.Error("[MAIL_MANAGER]", e); + } + + await Task.Delay(RefreshDelay, stoppingToken); + } + } + + private async Task ProcessPendingMailsRequestsAsync() + { + if (_mailQueue.IsEmpty) + { + return; + } + + List characterMailDtos = new(); + + while (_mailQueue.TryDequeue(out CharacterMailDto mailDto)) + { + characterMailDtos.Add(mailDto); + } + + List unsavedMails = new(); + AsyncRetryPolicy policy = Policy.Handle().RetryAsync(3, (exception, i1) => Log.Error($"[MAIL_MANAGER] Failed to handle the mails, try {i1.ToString()}. ", exception)); + foreach (List dtos in SplitList(characterMailDtos, 100)) + { + CreateMailBatchResponse response = null; + try + { + response = await policy.ExecuteAsync(() => _mailService.CreateMailBatchAsync(new CreateMailBatchRequest + { + Mails = dtos, + Bufferized = true + })); + } + catch (Exception e) + { + Log.Error("[MAIL_MANAGER]", e); + } + + if (response?.Status != RpcResponseType.SUCCESS) + { + unsavedMails.AddRange(dtos); + } + } + + foreach (CharacterMailDto unsavedMail in unsavedMails) + { + _mailQueue.Enqueue(unsavedMail); + } + } + + private static IEnumerable> SplitList(List locations, int nSize = 30) + { + for (int i = 0; i < locations.Count; i += nSize) + { + yield return locations.GetRange(i, Math.Min(nSize, locations.Count - i)); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Mail/MailOpenEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Mail/MailOpenEventHandler.cs new file mode 100644 index 0000000..65e2fdd --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Mail/MailOpenEventHandler.cs @@ -0,0 +1,69 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Communication; +using WingsAPI.Communication.Mail; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsAPI.Game.Extensions.PacketGeneration; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Items; +using WingsEmu.Game.Mails; +using WingsEmu.Game.Mails.Events; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.Mail; + +public class MailOpenEventHandler : IAsyncEventProcessor +{ + private readonly IGameLanguageService _gameLanguage; + private readonly IMailService _mailService; + + public MailOpenEventHandler(IMailService mailService, IGameLanguageService gameLanguage) + { + _mailService = mailService; + _gameLanguage = gameLanguage; + } + + public async Task HandleAsync(MailOpenEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + long mailId = e.MailId; + CharacterMail mail = session.PlayerEntity.MailNoteComponent.GetMail(mailId); + GameItemInstance itemInstance = mail?.ItemInstance; + if (itemInstance == null) + { + return; + } + + if (!session.PlayerEntity.HasSpaceFor(itemInstance.ItemVNum, (short)itemInstance.Amount)) + { + session.SendParcelDelete(5, mailId); + session.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.INTERACTION_MESSAGE_NOT_ENOUGH_PLACE, session.UserLanguage)); + return; + } + + BasicRpcResponse response = await _mailService.RemoveMailAsync(new RemoveMailRequest + { + CharacterId = session.PlayerEntity.Id, + MailId = mail.Id + }); + + if (response.ResponseType != RpcResponseType.SUCCESS) + { + return; + } + + await session.AddNewItemToInventory(itemInstance, true, ChatMessageColorType.Yellow); + session.SendParcelDelete(2, mailId); + session.PlayerEntity.MailNoteComponent.RemoveMail(mail); + + await session.EmitEventAsync(new MailClaimedEvent + { + MailId = mail.Id, + ItemInstance = itemInstance, + SenderName = mail.SenderName + }); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Mail/MailRemoveEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Mail/MailRemoveEventHandler.cs new file mode 100644 index 0000000..7593363 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Mail/MailRemoveEventHandler.cs @@ -0,0 +1,51 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Communication; +using WingsAPI.Communication.Mail; +using WingsAPI.Game.Extensions.PacketGeneration; +using WingsEmu.Game.Mails; +using WingsEmu.Game.Mails.Events; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Plugins.BasicImplementations.Mail; + +public class MailRemoveEventHandler : IAsyncEventProcessor +{ + private readonly IMailService _mailService; + + public MailRemoveEventHandler(IMailService mailService) => _mailService = mailService; + + public async Task HandleAsync(MailRemoveEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + long mailId = e.MailId; + + CharacterMail mail = session.PlayerEntity.MailNoteComponent.GetMail(mailId); + if (mail == null) + { + return; + } + + BasicRpcResponse response = await _mailService.RemoveMailAsync(new RemoveMailRequest + { + CharacterId = session.PlayerEntity.Id, + MailId = mail.Id + }); + + if (response.ResponseType != RpcResponseType.SUCCESS) + { + return; + } + + session.SendParcelDelete(7, mailId); + session.PlayerEntity.MailNoteComponent.RemoveMail(mail); + + await session.EmitEventAsync(new MailRemovedEvent + { + MailId = mail.Id, + ItemInstance = mail.ItemInstance, + SenderName = mail.SenderName + }); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Mail/NoteCreateEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Mail/NoteCreateEventHandler.cs new file mode 100644 index 0000000..bf6c914 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Mail/NoteCreateEventHandler.cs @@ -0,0 +1,107 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using PhoenixLib.Logging; +using WingsAPI.Communication; +using WingsAPI.Communication.DbServer.CharacterService; +using WingsAPI.Communication.Mail; +using WingsAPI.Data.Character; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Mails.Events; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Plugins.BasicImplementations.Mail; + +public class NoteCreateEventHandler : IAsyncEventProcessor +{ + private readonly ICharacterService _characterService; + private readonly IGameLanguageService _gameLanguage; + private readonly INoteService _noteService; + + public NoteCreateEventHandler(INoteService noteService, IGameLanguageService gameLanguage, ICharacterService characterService) + { + _noteService = noteService; + _gameLanguage = gameLanguage; + _characterService = characterService; + } + + public async Task HandleAsync(NoteCreateEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + string receiverName = e.ReceiverName; + string title = e.Title; + string message = e.Message; + + if (session.PlayerEntity.Name == receiverName) + { + return; + } + + if (session.PlayerEntity.LastSentNote.AddSeconds(5) > DateTime.UtcNow) + { + return; + } + + DbServerGetCharacterResponse characterResponse = null; + try + { + characterResponse = await _characterService.GetCharacterByName(new DbServerGetCharacterRequestByName + { + CharacterName = receiverName + }); + } + catch (Exception ex) + { + Log.Error("[NOTE_CREATE] Unexpected error: ", ex); + } + + if (characterResponse?.RpcResponseType != RpcResponseType.SUCCESS) + { + session.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.INFORMATION_MESSAGE_USER_NOT_FOUND, session.UserLanguage)); + return; + } + + CharacterDTO receiver = characterResponse.CharacterDto; + + if (title.Length > 30) + { + title = title.Substring(0, 30); + } + + if (message.Length > 200) + { + message = message.Substring(0, 200); + } + + session.PlayerEntity.LastSentNote = DateTime.UtcNow; + CreateNoteResponse response = await _noteService.CreateNoteAsync(new CreateNoteRequest + { + ReceiverId = receiver.Id, + ReceiverName = receiverName, + SenderId = session.PlayerEntity.Id, + SenderName = session.PlayerEntity.Name, + EquipmentPackets = session.GenerateEqListForPacket(), + Title = title, + Message = message, + SenderClass = session.PlayerEntity.Class, + SenderGender = session.PlayerEntity.Gender, + SenderHairColor = session.PlayerEntity.HairColor, + SenderHairStyle = session.PlayerEntity.HairStyle + }); + + if (response.Status != RpcResponseType.SUCCESS) + { + return; + } + + await session.EmitEventAsync(new NoteSentEvent + { + NoteId = response.SenderNote.Id, + ReceiverName = receiverName, + Message = message, + Title = title + }); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Mail/NoteOpenEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Mail/NoteOpenEventHandler.cs new file mode 100644 index 0000000..8fa706d --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Mail/NoteOpenEventHandler.cs @@ -0,0 +1,48 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Communication; +using WingsAPI.Communication.Mail; +using WingsAPI.Game.Extensions.PacketGeneration; +using WingsEmu.Game.Mails; +using WingsEmu.Game.Mails.Events; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Plugins.BasicImplementations.Mail; + +public class NoteOpenEventHandler : IAsyncEventProcessor +{ + private readonly INoteService _noteService; + + public NoteOpenEventHandler(INoteService noteService) => _noteService = noteService; + + public async Task HandleAsync(NoteOpenEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + long noteId = e.NoteId; + bool isSenderCopy = e.IsSenderCopy; + + CharacterNote note = session.PlayerEntity.MailNoteComponent.GetNote(noteId, isSenderCopy); + if (note == null) + { + return; + } + + if (!note.IsOpened && !note.IsSenderCopy) + { + BasicRpcResponse response = await _noteService.OpenNoteAsync(new OpenNoteRequest + { + NoteId = note.Id + }); + + if (response.ResponseType != RpcResponseType.SUCCESS) + { + return; + } + + note.IsOpened = true; + } + + session.SendPostMessage(note, (byte)(note.IsSenderCopy ? 2 : 1)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Mail/NoteRemoveEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Mail/NoteRemoveEventHandler.cs new file mode 100644 index 0000000..808a657 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Mail/NoteRemoveEventHandler.cs @@ -0,0 +1,44 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Communication; +using WingsAPI.Communication.Mail; +using WingsAPI.Game.Extensions.PacketGeneration; +using WingsEmu.Game.Mails; +using WingsEmu.Game.Mails.Events; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Plugins.BasicImplementations.Mail; + +public class NoteRemoveEventHandler : IAsyncEventProcessor +{ + private readonly INoteService _noteService; + + public NoteRemoveEventHandler(INoteService noteService) => _noteService = noteService; + + public async Task HandleAsync(NoteRemoveEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + long noteId = e.NoteId; + bool isSenderCopy = e.IsSenderCopy; + + CharacterNote note = session.PlayerEntity.MailNoteComponent.GetNote(noteId, isSenderCopy); + if (note == null) + { + return; + } + + BasicRpcResponse response = await _noteService.RemoveNoteAsync(new RemoveNoteRequest + { + NoteId = note.Id + }); + + if (response.ResponseType != RpcResponseType.SUCCESS) + { + return; + } + + session.PlayerEntity.MailNoteComponent.RemoveNote(note); + session.SendNoteDelete(noteId, note.IsSenderCopy); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Managers/DelayManager.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Managers/DelayManager.cs new file mode 100644 index 0000000..2e3a701 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Managers/DelayManager.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Concurrent; +using System.Linq; +using System.Threading.Tasks; +using WingsEmu.Core.Extensions; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Managers; + +namespace WingsEmu.Plugins.BasicImplementations.Managers; + +public class DelayManager : IDelayManager +{ + private readonly IDelayConfiguration _configuration; + private readonly ConcurrentDictionary _playerActions = new(); + + public DelayManager(IDelayConfiguration configuration) => _configuration = configuration; + + public ValueTask RegisterAction(IBattleEntity entity, DelayedActionType actionType, TimeSpan time = default) + { + if (_playerActions.TryGetValue(entity.Id, out DelayedAction action)) + { + CompleteAction(entity, action.Type); + } + + var delayedAction = new DelayedAction + { + Type = actionType, + Completion = DateTime.UtcNow.Add(time != default ? time : _configuration.GetDelayByAction(actionType)), + MapId = entity.MapInstance.MapId, + PositionX = entity.PositionX, + PositionY = entity.PositionY + }; + + _playerActions.TryAdd(entity.Id, delayedAction); + + return new ValueTask(delayedAction.Completion); + } + + public ValueTask CanPerformAction(IBattleEntity entity, DelayedActionType actionType) + { + DelayedAction action = _playerActions.GetOrDefault(entity.Id); + if (action == null || action.Type != actionType) + { + return new ValueTask(false); + } + + return new ValueTask(action.Completion <= DateTime.UtcNow && entity.MapInstance.MapId == action.MapId && entity.PositionX == action.PositionX && entity.PositionY == action.PositionY); + } + + public ValueTask CompleteAction(IBattleEntity entity, DelayedActionType actionType) + { + DelayedAction action = _playerActions.GetOrDefault(entity.Id); + if (action == null || action.Type != actionType) + { + return new ValueTask(false); + } + + return new ValueTask(_playerActions.TryRemove(entity.Id, out DelayedAction delayedAction)); + } + + public ValueTask RemoveAllOutdatedActions(TimeSpan time) + { + var keys = _playerActions.Where(x => x.Value.Completion.Add(time) < DateTime.UtcNow).Select(x => x.Key).ToList(); + + foreach (long key in keys) + { + _playerActions.TryRemove(key, out DelayedAction action); + } + + return new ValueTask(keys.Count); + } + + private class DelayedAction + { + public DelayedActionType Type { get; set; } + public DateTime Completion { get; set; } + public int MapId { get; set; } + public short PositionX { get; set; } + public short PositionY { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Managers/GroupManager.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Managers/GroupManager.cs new file mode 100644 index 0000000..ebe2de0 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Managers/GroupManager.cs @@ -0,0 +1,199 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; +using PhoenixLib.Logging; +using WingsAPI.Game.Extensions.Groups; +using WingsEmu.Game._ItemUsage.Configuration; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Groups; + +namespace WingsEmu.Plugins.BasicImplementations.Managers; + +public sealed class GroupManager : IGroupManager +{ + private readonly List _groups; + private readonly ConcurrentQueue<(IPlayerEntity, PlayerGroup)> _groupsToAdd = new(); + private readonly ConcurrentQueue<(IPlayerEntity, PlayerGroup)> _groupsToRemove = new(); + private readonly ConcurrentQueue<(long, PlayerGroup)> _leaderToChange = new(); + private readonly ConcurrentQueue<(IPlayerEntity, PlayerGroup)> _playersToAdd = new(); + private readonly ConcurrentQueue<(IPlayerEntity, PlayerGroup)> _playersToRemove = new(); + + private readonly ISpPartnerConfiguration _spPartnerConfiguration; + private int _lastGroupId; + private DateTime _lastGroupUiRefresh; + + public GroupManager(ISpPartnerConfiguration spPartnerConfiguration) + { + _spPartnerConfiguration = spPartnerConfiguration; + _groups = new List(); + _lastGroupUiRefresh = DateTime.MinValue; + Id = Guid.NewGuid(); + } + + public Guid Id { get; } + public string Name => nameof(GroupManager); + + public void ProcessTick(DateTime date) + { + RemoveGroup(); + AddNewGroup(); + + PlayerRemoveFromGroup(); + PlayerAddToGroup(); + ChangeLeader(); + + if (_lastGroupUiRefresh.AddSeconds(1) > date) + { + return; + } + + _lastGroupUiRefresh = DateTime.UtcNow; + var stopWatch = Stopwatch.StartNew(); + + ProcessGroupRefresh(); + + stopWatch.Stop(); + } + + public int GetNextGroupId() + { + Interlocked.Increment(ref _lastGroupId); + return _lastGroupId; + } + + public void JoinGroup(PlayerGroup group, IPlayerEntity character) + { + _groupsToAdd.Enqueue((character, group)); + } + + public void RemoveGroup(PlayerGroup group, IPlayerEntity character) + { + _groupsToRemove.Enqueue((character, group)); + } + + public void AddMemberGroup(PlayerGroup group, IPlayerEntity character) + { + _playersToAdd.Enqueue((character, group)); + } + + public void RemoveMemberGroup(PlayerGroup group, IPlayerEntity character) + { + _playersToRemove.Enqueue((character, group)); + } + + public void ChangeLeader(PlayerGroup group, long newLeaderId) + { + _leaderToChange.Enqueue((newLeaderId, group)); + } + + private void ProcessGroupRefresh() + { + foreach (PlayerGroup group in _groups) + { + try + { + IReadOnlyList groupMembers = group.Members; + foreach (IPlayerEntity member in groupMembers) + { + member.Session.RefreshPartyUi(); + } + } + catch (Exception e) + { + Log.Error("Group.RefreshMembers", e); + } + } + } + + private void RemoveGroup() + { + while (_groupsToRemove.TryDequeue(out (IPlayerEntity, PlayerGroup) playerGroup)) + { + PlayerGroup group = playerGroup.Item2; + IPlayerEntity character = playerGroup.Item1; + character.RemoveGroup(); + + foreach (IPlayerEntity member in group.Members) + { + member.Session.RefreshParty(_spPartnerConfiguration); + member.Session.BroadcastPidx(); + } + + _groups.Remove(group); + } + } + + private void AddNewGroup() + { + while (_groupsToAdd.TryDequeue(out (IPlayerEntity, PlayerGroup) playerGroup)) + { + IPlayerEntity character = playerGroup.Item1; + PlayerGroup group = playerGroup.Item2; + _groups.Add(group); + character.SetGroup(group); + + foreach (IPlayerEntity member in group.Members) + { + member.Session.RefreshParty(_spPartnerConfiguration); + if (!member.IsLeaderOfGroup(member.Id)) + { + continue; + } + + member.Session.BroadcastPidx(); + } + } + } + + private void PlayerRemoveFromGroup() + { + while (_playersToRemove.TryDequeue(out (IPlayerEntity, PlayerGroup) playerGroup)) + { + IPlayerEntity character = playerGroup.Item1; + PlayerGroup group = playerGroup.Item2; + + group.RemoveMember(character); + character.RemoveGroup(); + + character.Session.RefreshParty(_spPartnerConfiguration); + character.Session.BroadcastPidx(); + foreach (IPlayerEntity member in group.Members) + { + member.Session.RefreshParty(_spPartnerConfiguration); + member.Session.BroadcastPidx(); + } + } + } + + private void PlayerAddToGroup() + { + while (_playersToAdd.TryDequeue(out (IPlayerEntity character, PlayerGroup) playerGroup)) + { + IPlayerEntity character = playerGroup.Item1; + PlayerGroup group = playerGroup.Item2; + + character.SetGroup(group); + group.AddMember(character); + foreach (IPlayerEntity member in group.Members) + { + member.Session.RefreshParty(_spPartnerConfiguration); + if (character.Id != member.Id) + { + continue; + } + + member.Session.BroadcastPidx(); + } + } + } + + private void ChangeLeader() + { + while (_leaderToChange.TryDequeue(out (long, PlayerGroup) playerGroup)) + { + playerGroup.Item2.OwnerId = playerGroup.Item1; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Managers/IMapAttributeFactory.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Managers/IMapAttributeFactory.cs new file mode 100644 index 0000000..f4772ae --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Managers/IMapAttributeFactory.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; +using WingsEmu.Game.Maps; + +namespace WingsEmu.Plugins.BasicImplementations.Managers; + +public interface IMapAttributeFactory +{ + IEnumerable CreateMapAttributes(Dictionary attributesKeyValue); +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Managers/MapAttributeFactory.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Managers/MapAttributeFactory.cs new file mode 100644 index 0000000..407d47f --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Managers/MapAttributeFactory.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using WingsEmu.Game.Maps; + +namespace WingsEmu.Plugins.BasicImplementations.Managers; + +public class MapAttributeFactory : IMapAttributeFactory +{ + private readonly Dictionary _mapAttributesTypes = new(); + + public IEnumerable CreateMapAttributes(Dictionary attributesKeyValue) => throw new NotImplementedException(); +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Managers/RankingManager.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Managers/RankingManager.cs new file mode 100644 index 0000000..517f930 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Managers/RankingManager.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using PhoenixLib.Logging; +using WingsAPI.Communication; +using WingsAPI.Communication.DbServer.CharacterService; +using WingsAPI.Data.Character; +using WingsEmu.Game.Managers; + +namespace WingsEmu.Plugins.BasicImplementations.Managers; + +public class RankingManager : IRankingManager +{ + private readonly ICharacterService _characterService; + + public RankingManager(ICharacterService characterService) => _characterService = characterService; + + public IReadOnlyList TopCompliment { get; private set; } = new List(); + public IReadOnlyList TopPoints { get; private set; } = new List(); + public IReadOnlyList TopReputation { get; private set; } = new List(); + + public async Task TryRefreshRanking() + { + CharacterGetTopResponse response = null; + try + { + response = await _characterService.GetTopCompliment(new EmptyRpcRequest()); + } + catch (Exception e) + { + Log.Error("[RANKING_MANAGER][TRY_REFRESH_RANKING] Unexpected error: ", e); + } + + if (response?.ResponseType == RpcResponseType.SUCCESS) + { + TopCompliment = response.Top ?? new List(); + } + + response = null; + try + { + response = await _characterService.GetTopPoints(new EmptyRpcRequest()); + } + catch (Exception e) + { + Log.Error("[RANKING_MANAGER][TRY_REFRESH_RANKING] Unexpected error: ", e); + } + + if (response?.ResponseType == RpcResponseType.SUCCESS) + { + TopPoints = response.Top ?? new List(); + } + + response = null; + try + { + response = await _characterService.GetTopReputation(new EmptyRpcRequest()); + } + catch (Exception e) + { + Log.Error("[RANKING_MANAGER][TRY_REFRESH_RANKING] Unexpected error: ", e); + } + + if (response?.ResponseType == RpcResponseType.SUCCESS) + { + TopReputation = response.Top ?? new List(); + } + } + + public void RefreshRanking(IReadOnlyList topComplimented, IReadOnlyList topPoints, IReadOnlyList topReputation) + { + TopCompliment = topComplimented; + TopPoints = topPoints; + TopReputation = topReputation; + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Managers/RevivalManager.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Managers/RevivalManager.cs new file mode 100644 index 0000000..3df2b11 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Managers/RevivalManager.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Concurrent; +using WingsEmu.Game.Managers; + +namespace WingsEmu.Plugins.BasicImplementations.Managers; + +public class RevivalManager : IRevivalManager +{ + private readonly ConcurrentDictionary _pendentRevivals = new(); + + public Guid RegisterRevival(long id) + { + var newGuid = Guid.NewGuid(); + bool added = _pendentRevivals.TryAdd(id, newGuid); + return added ? newGuid : default; + } + + public bool UnregisterRevival(long id, Guid guid) + => _pendentRevivals.TryGetValue(id, out Guid storedGuid) && storedGuid == guid && _pendentRevivals.TryRemove(id, out _); + + public bool UnregisterRevival(long id) => _pendentRevivals.TryRemove(id, out _); + + public void TryUnregisterRevival(long id) + { + _pendentRevivals.TryRemove(id, out _); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Managers/ScriptedInstanceManager.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Managers/ScriptedInstanceManager.cs new file mode 100644 index 0000000..3a0b1f9 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Managers/ScriptedInstanceManager.cs @@ -0,0 +1,7 @@ +using WingsEmu.Game.Managers; + +namespace WingsEmu.Plugins.BasicImplementations.Managers; + +public class ScriptedInstanceManager : IScriptedInstanceManager +{ +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Managers/ServerManager.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Managers/ServerManager.cs new file mode 100644 index 0000000..9607e4c --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Managers/ServerManager.cs @@ -0,0 +1,250 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Threading; +using WingsAPI.Communication.ServerApi.Protocol; +using WingsAPI.Scripting.ScriptManager; +using WingsEmu.Game._ECS; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Arena; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Groups; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Managers.ServerData; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Quests; + +namespace WingsEmu.Plugins.BasicImplementations.Managers; + +public class ServerManager : IServerManager +{ + public void InitializeAsync() + { + State = GameServerState.STARTING; + InitializeConfigurations(); + _itemManager.Initialize(); + _skillManager.Initialize(); + _questManager.InitializeAsync().ConfigureAwait(false).GetAwaiter().GetResult(); + _dropManager.InitializeAsync().ConfigureAwait(false).GetAwaiter().GetResult(); + _npcMonsterManager.InitializeAsync().ConfigureAwait(false).GetAwaiter().GetResult(); + _mapMonsterManager.InitializeAsync().ConfigureAwait(false).GetAwaiter().GetResult(); + _shopManager.InitializeAsync().ConfigureAwait(false).GetAwaiter().GetResult(); + _recipeManager.InitializeAsync().ConfigureAwait(false).GetAwaiter().GetResult(); + _teleporterManager.InitializeAsync().ConfigureAwait(false).GetAwaiter().GetResult(); + _cardManager.Initialize(); + _mapNpcManager.InitializeAsync().ConfigureAwait(false).GetAwaiter().GetResult(); + _itemBoxManager.Initialize(); + _rankingManager.TryRefreshRanking().ConfigureAwait(false).GetAwaiter().GetResult(); + _mapManager.Initialize().ConfigureAwait(false).GetAwaiter().GetResult(); + _arenaManager.Initialize(); + _raidScriptManager.Load(); + _dungeonScriptManager.Load(); + _timeSpaceScriptManager.Load(); + _tickManager.AddProcessable(_groupManager); + _gameLanguageService.Reload(true).ConfigureAwait(false).GetAwaiter().GetResult(); + _forbiddenNamesManager.Reload().ConfigureAwait(false).GetAwaiter().GetResult(); + } + + public void TryStart() + { + if (IsRunning) + { + return; + } + + State = GameServerState.RUNNING; + IsRunning = true; + _tickManager.Start(); + } + + public void PutIdle() + { + if (IsRunning == false) + { + State = GameServerState.IDLE; + return; + } + + State = GameServerState.IDLE; + IsRunning = false; + _tickManager.Stop(); + } + + public void Shutdown() + { + InShutdown = true; + State = GameServerState.STOPPING; + _shutdownTokenSource?.Cancel(); + } + + + public void ListenCancellation(CancellationTokenSource stopServiceTokenSource) + { + _shutdownTokenSource = stopServiceTokenSource; + } + + private void InitializeConfigurations() + { + MobXpRate = _rateConfiguration.MobXpRate; + JobXpRate = _rateConfiguration.JobXpRate; + HeroXpRate = _rateConfiguration.HeroXpRate; + FairyXpRate = _rateConfiguration.FairyXpRate; + MateXpRate = _rateConfiguration.MateXpRate; + PartnerXpRate = _rateConfiguration.PartnerXpRate; + ReputRate = _rateConfiguration.ReputRate; + MobDropRate = _rateConfiguration.MobDropRate; + MobDropChance = _rateConfiguration.MobDropChance; + FamilyExpRate = _rateConfiguration.FamilyXpRate; + GoldDropRate = _rateConfiguration.GoldDropRate; + GoldRate = _rateConfiguration.GoldRate; + GoldDropChance = _rateConfiguration.GoldDropChance; + GenericDropRate = _rateConfiguration.GenericDropRate; + GenericDropChance = _rateConfiguration.GenericDropChance; + + /* + * Min Max Configurations + */ + MaxLevel = _gameMinMaxConfiguration.MaxLevel; + MaxMateLevel = _gameMinMaxConfiguration.MaxMateLevel; + MaxJobLevel = _gameMinMaxConfiguration.MaxJobLevel; + MaxSpLevel = _gameMinMaxConfiguration.MaxSpLevel; + MaxHeroLevel = _gameMinMaxConfiguration.MaxHeroLevel; + HeroicStartLevel = _gameMinMaxConfiguration.HeroMinLevel; + MaxGold = _gameMinMaxConfiguration.MaxGold; + MaxBankGold = _gameMinMaxConfiguration.MaxBankGold; + MaxNpcTalkRange = _gameMinMaxConfiguration.MaxNpcTalkRange; + MaxBasicSpPoints = _gameMinMaxConfiguration.MaxSpBasePoints; + MaxAdditionalSpPoints = _gameMinMaxConfiguration.MaxSpAdditionalPoints; + } + + private readonly IGroupManager _groupManager; + private readonly ITickManager _tickManager; + private readonly ITeleporterManager _teleporterManager; + private readonly IMapManager _mapManager; + private readonly IItemsManager _itemManager; + private readonly ICardsManager _cardManager; + private readonly IDropManager _dropManager; + private readonly INpcMonsterManager _npcMonsterManager; + private readonly IRecipeManager _recipeManager; + private readonly IShopManager _shopManager; + private readonly ISkillsManager _skillManager; + private readonly IMapNpcManager _mapNpcManager; + private readonly IQuestManager _questManager; + private readonly SerializableGameServer _gameServerInfos; + private readonly GameRateConfiguration _rateConfiguration; + private readonly GameMinMaxConfiguration _gameMinMaxConfiguration; + private readonly IItemBoxManager _itemBoxManager; + private readonly IRaidScriptManager _raidScriptManager; + private readonly IDungeonScriptManager _dungeonScriptManager; + private readonly IArenaManager _arenaManager; + private readonly IMapMonsterManager _mapMonsterManager; + private readonly ITimeSpaceScriptManager _timeSpaceScriptManager; + private readonly IGameLanguageService _gameLanguageService; + private readonly IRankingManager _rankingManager; + private readonly IForbiddenNamesManager _forbiddenNamesManager; + + private CancellationTokenSource _shutdownTokenSource; + + public ServerManager(ITeleporterManager teleporterManager, IMapManager mapManager, IItemsManager itemManager, ICardsManager cardManager, IDropManager dropManager, + INpcMonsterManager npcMonsterManager, IRecipeManager recipeManager, IShopManager shopManager, ISkillsManager skillManager, IMapNpcManager mapNpcManager, IQuestManager questManager, + SerializableGameServer gameServerInfos, GameRateConfiguration rateConfiguration, GameMinMaxConfiguration gameMinMaxConfiguration, + IGroupManager groupManager, ITickManager tickManager, IItemBoxManager itemBoxManager, IRaidScriptManager raidScriptManager, IDungeonScriptManager dungeonScriptManager, + IArenaManager arenaManager, IMapMonsterManager mapMonsterManager, ITimeSpaceScriptManager timeSpaceScriptManager, IGameLanguageService gameLanguageService, IRankingManager rankingManager, + IForbiddenNamesManager forbiddenNamesManager) + { + _teleporterManager = teleporterManager; + _mapManager = mapManager; + _itemManager = itemManager; + _cardManager = cardManager; + _dropManager = dropManager; + _npcMonsterManager = npcMonsterManager; + _recipeManager = recipeManager; + _shopManager = shopManager; + _skillManager = skillManager; + _mapNpcManager = mapNpcManager; + _questManager = questManager; + _gameServerInfos = gameServerInfos; + _rateConfiguration = rateConfiguration; + _gameMinMaxConfiguration = gameMinMaxConfiguration; + _groupManager = groupManager; + _tickManager = tickManager; + _itemBoxManager = itemBoxManager; + _raidScriptManager = raidScriptManager; + _dungeonScriptManager = dungeonScriptManager; + _arenaManager = arenaManager; + _mapMonsterManager = mapMonsterManager; + _timeSpaceScriptManager = timeSpaceScriptManager; + _gameLanguageService = gameLanguageService; + _rankingManager = rankingManager; + _forbiddenNamesManager = forbiddenNamesManager; + } + + public GameServerState State { get; private set; } + + public bool IsRunning { get; private set; } + + public int ChannelId => _gameServerInfos.ChannelId; + + public int MobDropRate { get; set; } + + public int MobDropChance { get; set; } + + public int FamilyExpRate { get; set; } + + public int JobXpRate { get; set; } + + public bool ExpEvent { get; set; } + + public int FairyXpRate { get; set; } + + public int GoldDropRate { get; set; } + + public int GoldRate { get; set; } + + public int GoldDropChance { get; set; } + + public int GenericDropRate { get; set; } + + public int GenericDropChance { get; set; } + + public int ReputRate { get; set; } + + public int HeroicStartLevel { get; set; } + + public int HeroXpRate { get; set; } + + public long MaxGold { get; set; } + + public long MaxBankGold { get; set; } + + public short MaxHeroLevel { get; set; } + + public short MaxJobLevel { get; set; } + + public short MaxLevel { get; set; } + + public short MaxSpLevel { get; set; } + + public int MateXpRate { get; set; } + + public int PartnerXpRate { get; set; } + + public short MaxMateLevel { get; set; } + + public short MaxNpcTalkRange { get; set; } + public int MaxBasicSpPoints { get; set; } + + public int MaxAdditionalSpPoints { get; set; } + + public string ServerGroup => _gameServerInfos.WorldGroup; + + public int MobXpRate { get; set; } + + public int AccountLimit => _gameServerInfos.AccountLimit; + + public bool InShutdown { get; set; } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Managers/SessionManager.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Managers/SessionManager.cs new file mode 100644 index 0000000..4f01042 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Managers/SessionManager.cs @@ -0,0 +1,451 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Caching; +using PhoenixLib.Logging; +using WingsAPI.Communication; +using WingsAPI.Communication.DbServer.CharacterService; +using WingsAPI.Communication.Player; +using WingsAPI.Data.Character; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Networking.Broadcasting; +using WingsEmu.Packets; + +namespace WingsEmu.Plugins.BasicImplementations.Managers; + +public class SessionManager : ISessionManager +{ + private static readonly IPacketSerializer Serializer = new PacketSerializer(); + + private readonly ILongKeyCachedRepository _characterCache; + private readonly ConcurrentDictionary _characterCacheByName = new(); + private readonly ICharacterService _characterService; + private readonly ConcurrentDictionary _isOnlineByCharacterId = new(); + private readonly ConcurrentDictionary _isOnlineByCharacterName = new(); + private readonly ReaderWriterLockSlim _lock = new(); + private readonly ConcurrentQueue _queue = new(); + private readonly IServerManager _serverManager; + private readonly List _sessionsAll = new(); + private readonly ConcurrentDictionary _sessionsByCharacterId = new(); + private readonly ConcurrentDictionary _sessionsByCharacterName = new(); + + public SessionManager(ILongKeyCachedRepository characterCache, IServerManager serverManager, ICharacterService characterService) + { + _characterCache = characterCache; + _serverManager = serverManager; + _characterService = characterService; + } + + private IReadOnlyDictionary SessionsByCharacterId => _sessionsByCharacterId; + private IReadOnlyDictionary SessionsByCharacterName => _sessionsByCharacterName; + + public IReadOnlyList Sessions + { + get + { + _lock.EnterReadLock(); + try + { + return _sessionsAll.ToArray(); + } + finally + { + _lock.ExitReadLock(); + } + } + } + + public int SessionsCount => _sessionsByCharacterId.Count; + + public async ValueTask GetOnlineCharacterById(long characterId) + { + return await _characterCache.GetOrSetAsync(characterId, () => FetchDelegate(characterId), TimeSpan.FromHours(2)); + } + + public ClusterCharacterInfo GetOnlineCharacterByName(string characterName) => _characterCacheByName.TryGetValue(characterName, out ClusterCharacterInfo characterInfo) ? characterInfo : null; + + public bool IsOnline(string charName) => _isOnlineByCharacterName.ContainsKey(charName); + + public bool IsOnline(long characterId) => _isOnlineByCharacterId.ContainsKey(characterId); + + public void AddOnline(ClusterCharacterInfo clusterCharacterInfo) + { + long characterId = clusterCharacterInfo.Id; + string charName = clusterCharacterInfo.Name; + _isOnlineByCharacterId[characterId] = true; + _isOnlineByCharacterName[charName] = true; + _characterCache.Set(clusterCharacterInfo.Id, clusterCharacterInfo, TimeSpan.FromHours(2)); + _characterCacheByName[charName] = clusterCharacterInfo; + } + + public void RemoveOnline(string charName, long characterId) + { + _isOnlineByCharacterId.Remove(characterId, out _); + _isOnlineByCharacterName.Remove(charName, out _); + _characterCache.Remove(characterId); + _characterCacheByName.Remove(charName, out _); + } + + public async Task DisconnectAllAsync() + { + foreach (IClientSession session in Sessions.ToArray()) + { + if (session == null) + { + continue; + } + + + if (session.HasSelectedCharacter) + { + if (session.PlayerEntity.Hp < 1) + { + session.PlayerEntity.Hp = 1; + } + } + + Log.Info($"[SESSION_DISCONNECT] {session.SessionId}:{session.PlayerEntity?.Name}"); + session.ForceDisconnect(); + } + } + + public IClientSession GetSessionByCharacterName(string name) => SessionsByCharacterName.TryGetValue(name, out IClientSession session) ? session : null; + + public async Task KickAsync(string characterName) + { + IClientSession session = GetSessionByCharacterName(characterName); + session?.ForceDisconnect(); + } + + public async Task KickAsync(long accountId) + { + IClientSession session = Sessions.FirstOrDefault(s => s.Account != null && s.Account.Id == accountId); + session?.ForceDisconnect(); + } + + public IClientSession GetSessionByCharacterId(long characterId) => SessionsByCharacterId.TryGetValue(characterId, out IClientSession session) ? session : null; + + public void Broadcast(string packet) + { + _queue.Enqueue(packet); + } + + public void Broadcast(T packet, params IBroadcastRule[] rules) where T : IServerPacket + { + _lock.EnterReadLock(); + try + { + foreach (IClientSession session in _sessionsAll) + { + if (session is null) + { + continue; + } + + bool all = true; + foreach (IBroadcastRule x in rules) + { + if (!x.Match(session)) + { + all = false; + break; + } + } + + if (!rules.Any() || all) + { + session.SendPacket(packet); + } + } + } + finally + { + _lock.ExitReadLock(); + } + } + + public void Broadcast(string packet, params IBroadcastRule[] rules) + { + _lock.EnterReadLock(); + try + { + foreach (IClientSession session in _sessionsAll) + { + if (session is null) + { + continue; + } + + bool all = true; + foreach (IBroadcastRule x in rules) + { + if (!x.Match(session)) + { + all = false; + break; + } + } + + if (!rules.Any() || all) + { + session.SendPacket(packet); + } + } + } + finally + { + _lock.ExitReadLock(); + } + } + + public void Broadcast(IEnumerable packets) + { + foreach (string packet in packets) + { + _queue.Enqueue(packet); + } + } + + public void Broadcast(IEnumerable packets, params IBroadcastRule[] rules) + { + _lock.EnterReadLock(); + try + { + foreach (IClientSession session in _sessionsAll) + { + if (session is null) + { + continue; + } + + bool all = true; + foreach (IBroadcastRule x in rules) + { + if (!x.Match(session)) + { + all = false; + break; + } + } + + if (!rules.Any() || all) + { + session.SendPackets(packets); + } + } + } + finally + { + _lock.ExitReadLock(); + } + } + + public void Broadcast(Func generatePacketCallback) + { + _lock.EnterReadLock(); + try + { + foreach (IClientSession session in _sessionsAll) + { + if (session is null) + { + continue; + } + + session.SendPacket(generatePacketCallback(session)); + } + } + finally + { + _lock.ExitReadLock(); + } + } + + public void Broadcast(Func generatePacketCallback, params IBroadcastRule[] rules) + { + _lock.EnterReadLock(); + try + { + foreach (IClientSession session in _sessionsAll) + { + if (session is null) + { + continue; + } + + bool all = true; + foreach (IBroadcastRule x in rules) + { + if (x.Match(session)) + { + continue; + } + + all = false; + break; + } + + if (!rules.Any() || all) + { + session.SendPacket(generatePacketCallback(session)); + } + } + } + finally + { + _lock.ExitReadLock(); + } + } + + public async Task BroadcastAsync(Func> lambdaAsync) + { + foreach (IClientSession session in Sessions) + { + if (session is null) + { + continue; + } + + session.SendPacket(await lambdaAsync(session)); + } + } + + public async Task BroadcastAsync(Func> generatePacketCallback, params IBroadcastRule[] rules) + { + foreach (IClientSession session in Sessions) + { + if (session is null) + { + continue; + } + + bool all = true; + foreach (IBroadcastRule x in rules) + { + if (!x.Match(session)) + { + all = false; + break; + } + } + + if (!rules.Any() || all) + { + session.SendPacket(await generatePacketCallback(session)); + } + } + } + + public void Broadcast(T packet) where T : IPacket + { + Broadcast(Serializer.Serialize(packet)); + } + + public virtual void RegisterSession(IClientSession session) + { + if (!session.HasSelectedCharacter) + { + return; + } + + long id = session.PlayerEntity.Id; + string name = session.PlayerEntity.Name; + + if (!_sessionsByCharacterId.TryGetValue(id, out _)) + { + _sessionsByCharacterId.TryAdd(session.PlayerEntity.Id, session); + } + else + { + _sessionsByCharacterId.TryRemove(session.PlayerEntity.Id, out _); + _sessionsByCharacterId.TryAdd(session.PlayerEntity.Id, session); + } + + if (!_sessionsByCharacterName.TryGetValue(name, out _)) + { + _sessionsByCharacterName.TryAdd(session.PlayerEntity.Name, session); + } + else + { + _sessionsByCharacterName.TryRemove(session.PlayerEntity.Name, out _); + _sessionsByCharacterName.TryAdd(session.PlayerEntity.Name, session); + } + + _lock.EnterWriteLock(); + try + { + _sessionsAll.Add(session); + } + finally + { + _lock.ExitWriteLock(); + } + + _serverManager.TryStart(); + } + + public virtual void UnregisterSession(IClientSession session) + { + _sessionsByCharacterId.TryRemove(session.PlayerEntity.Id, out _); + _sessionsByCharacterName.TryRemove(session.PlayerEntity.Name, out _); + + _lock.EnterWriteLock(); + try + { + _sessionsAll.Remove(session); + } + finally + { + _lock.ExitWriteLock(); + } + + if (_sessionsByCharacterId.IsEmpty) + { + _serverManager.PutIdle(); + } + } + + private async Task FetchDelegate(long characterId) + { + DbServerGetCharacterResponse response = null; + try + { + response = await _characterService.GetCharacterById(new DbServerGetCharacterByIdRequest + { + CharacterId = characterId + }); + } + catch (Exception e) + { + Log.Error("[SESSION_MANAGER] Unexpected error: ", e); + } + + if (response?.RpcResponseType == RpcResponseType.GENERIC_SERVER_ERROR) + { + Log.Error("[SESSION_MANAGER] FamilyManager", new InvalidOperationException($"Database corrupted: {characterId.ToString()} seems to have been removed but still used somewhere")); + } + + if (response?.RpcResponseType != RpcResponseType.SUCCESS) + { + return null; + } + + CharacterDTO characterDto = response.CharacterDto; + + return new ClusterCharacterInfo + { + Id = characterId, + Name = characterDto.Name, + Gender = characterDto.Gender, + Class = characterDto.Class, + Level = characterDto.Level, + HeroLevel = characterDto.HeroLevel, + MorphId = null, + ChannelId = null + }; + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Managers/StaticData/CardsManager.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Managers/StaticData/CardsManager.cs new file mode 100644 index 0000000..f023e12 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Managers/StaticData/CardsManager.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using Mapster; +using PhoenixLib.Caching; +using PhoenixLib.Logging; +using WingsAPI.Data.GameData; +using WingsEmu.DTOs.BCards; +using WingsEmu.DTOs.Buffs; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Managers.StaticData; + +namespace WingsEmu.Plugins.BasicImplementations.Managers.StaticData; + +public class CardsManager : ICardsManager +{ + private readonly ILongKeyCachedRepository _cachedCards; + private readonly IKeyValueCache> _cardByName; + private readonly IResourceLoader _cardDao; + + public CardsManager(ILongKeyCachedRepository cachedCards, IKeyValueCache> cardByName, IResourceLoader cardDao) + { + _cachedCards = cachedCards; + _cardByName = cardByName; + _cardDao = cardDao; + } + + public void Initialize() + { + int cardCount = 0; + IReadOnlyList cards = _cardDao.LoadAsync().GetAwaiter().GetResult(); + foreach (CardDTO cardDto in cards) + { + Card card = cardDto.Adapt(); + + card.BCards = new List(); + foreach (BCardDTO bCard in card.Bcards) + { + card.BCards.Add(bCard); + } + + _cachedCards.Set(card.Id, card); + _cardByName.GetOrSet(card.Name, () => new List()).Add(card); + cardCount++; + } + + Log.Info($"[DATABASE] Loaded {cardCount} cards."); + } + + + public List GetCardByName(string name) => _cardByName.Get(name); + + public Card GetCardByCardId(int cardId) => _cachedCards.Get(cardId); +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Managers/StaticData/ItemsManager.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Managers/StaticData/ItemsManager.cs new file mode 100644 index 0000000..7ca717d --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Managers/StaticData/ItemsManager.cs @@ -0,0 +1,66 @@ +using System.Collections.Generic; +using System.Linq; +using PhoenixLib.Caching; +using PhoenixLib.Logging; +using WingsAPI.Data.GameData; +using WingsEmu.Core.Extensions; +using WingsEmu.DTOs.Items; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.Managers.StaticData; + +public class ItemsManager : IItemsManager +{ + private readonly ILongKeyCachedRepository _cachedItems; + private readonly ILongKeyCachedRepository> _cachedItemsByType; + + private readonly IResourceLoader _itemResourceLoader; + private readonly IKeyValueCache> _itemsByName; + private readonly Dictionary _titleIdByItemVnum = new(); + + public ItemsManager(ILongKeyCachedRepository cachedItems, ILongKeyCachedRepository> cachedItemsByType, IKeyValueCache> itemsByName, + IResourceLoader itemResourceLoader) + { + _cachedItems = cachedItems; + _cachedItemsByType = cachedItemsByType; + _itemsByName = itemsByName; + _itemResourceLoader = itemResourceLoader; + } + + public void Initialize() + { + IEnumerable items = _itemResourceLoader.LoadAsync().ConfigureAwait(false).GetAwaiter().GetResult(); + var item = new Dictionary(); + foreach (ItemDTO itemDto in items) + { + var newGameItem = new GameItem(itemDto); + + _itemsByName.GetOrSet(itemDto.Name, () => new List()).Add(newGameItem); + _cachedItems.Set(itemDto.Id, newGameItem); + item[itemDto.Id] = newGameItem; + } + + foreach (IGrouping group in item.Values.GroupBy(x => x.ItemType)) + { + _cachedItemsByType.Set((int)group.Key, group.ToList()); + } + + var titles = GetItemsByType(ItemType.Title).Select(x => x.Id).ToList(); + for (int i = 0; i < titles.Count; i++) + { + _titleIdByItemVnum[titles[i]] = i; + } + + Log.Info($"[DATABASE] Loaded {item.Count} items."); + } + + public IGameItem GetItem(int vnum) => _cachedItems.Get(vnum); + + public IEnumerable GetItemsByType(ItemType type) => _cachedItemsByType.Get((int)type); + + public int GetTitleId(int itemVnum) => _titleIdByItemVnum.GetOrDefault(itemVnum); + + public List GetItem(string name) => _itemsByName.Get(name); +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Managers/StaticData/NpcMonsterManager.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Managers/StaticData/NpcMonsterManager.cs new file mode 100644 index 0000000..0933440 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Managers/StaticData/NpcMonsterManager.cs @@ -0,0 +1,89 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Mapster; +using PhoenixLib.Caching; +using PhoenixLib.Logging; +using WingsAPI.Data.Drops; +using WingsAPI.Data.GameData; +using WingsEmu.DTOs.BCards; +using WingsEmu.DTOs.NpcMonster; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Managers.ServerData; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Npcs; +using WingsEmu.Game.Skills; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.Managers.StaticData; + +public class NpcMonsterManager : INpcMonsterManager +{ + private readonly IDropManager _dropManager; + private readonly IEntitySkillFactory _entitySkillFactory; + + private readonly IKeyValueCache> _npcMonsterByName; + private readonly ILongKeyCachedRepository _npcMonsterCache; + private readonly IResourceLoader _npcMonsterLoader; + private readonly ISkillsManager _skillsManager; + + public NpcMonsterManager(ILongKeyCachedRepository npcMonsterCache, IKeyValueCache> npcMonsterByName, ISkillsManager skillsManager, IDropManager dropManager, + IResourceLoader npcMonsterLoader, IEntitySkillFactory entitySkillFactory) + { + _npcMonsterCache = npcMonsterCache; + _npcMonsterByName = npcMonsterByName; + _skillsManager = skillsManager; + _dropManager = dropManager; + _npcMonsterLoader = npcMonsterLoader; + _entitySkillFactory = entitySkillFactory; + } + + public async Task InitializeAsync() + { + int npcMonstersAdded = 0; + + foreach (NpcMonsterDto npcMonster in await _npcMonsterLoader.LoadAsync()) + { + if (npcMonster.Adapt() is not { } monster) + { + continue; + } + + var drop = new List(); + + if (npcMonster.BCards.Any()) + { + foreach (BCardDTO s in npcMonster.BCards) + { + if (s.Type == (short)BCardType.SpecialActions && s.SubType == (byte)AdditionalTypes.SpecialActions.SeeHiddenThings) + { + monster.CanSeeInvisible = true; + } + } + } + + if (npcMonster.Drops != null) + { + drop.AddRange(npcMonster.Drops); + } + + drop.AddRange(_dropManager.GetDropsByMonsterVnum(monster.MonsterVNum)); + monster.Drops = drop; + + _npcMonsterByName.GetOrSet(monster.Name, () => new List()).Add(monster); + _npcMonsterCache.Set(monster.Id, monster); + npcMonstersAdded++; + } + + Log.Info($"[DATABASE] Loaded {npcMonstersAdded} monsters."); + } + + public IMonsterData GetNpc(int vnum) => _npcMonsterCache.Get(vnum); + + public List GetNpc(string name) => _npcMonsterByName.Get(name); +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Managers/StaticData/SkillsManager.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Managers/StaticData/SkillsManager.cs new file mode 100644 index 0000000..887d671 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Managers/StaticData/SkillsManager.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using PhoenixLib.Caching; +using PhoenixLib.Logging; +using WingsAPI.Data.GameData; +using WingsEmu.DTOs.Skills; +using WingsEmu.Game.Managers.StaticData; + +namespace WingsEmu.Plugins.BasicImplementations.Managers.StaticData; + +public class SkillsManager : ISkillsManager +{ + private readonly IKeyValueCache> _skillByName; + private readonly ILongKeyCachedRepository _skillCache; + private readonly IResourceLoader _skillDao; + private SkillDTO[] _skills; + + public SkillsManager(ILongKeyCachedRepository skillCache, IKeyValueCache> skillByName, IResourceLoader skillDao) + { + _skillDao = skillDao; + _skillCache = skillCache; + _skillByName = skillByName; + } + + public async Task Initialize() + { + _skills = (await _skillDao.LoadAsync()).ToArray(); + int skillsLoaded = 0; + foreach (SkillDTO skillItem in _skills) + { + _skillCache.Set(skillItem.Id, skillItem); + _skillByName.GetOrSet(skillItem.Name, () => new List()).Add(skillItem); + skillsLoaded++; + } + + Log.Info($"[DATABASE] Loaded {skillsLoaded} skills."); + } + + public SkillDTO GetSkill(int s) => _skillCache.Get(s); + + public List GetSkill(string s) => _skillByName.Get(s); + + public IEnumerable GetSkills() => _skills; +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Miniland/DependencyInjectionExtensions.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Miniland/DependencyInjectionExtensions.cs new file mode 100644 index 0000000..5c3abf5 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Miniland/DependencyInjectionExtensions.cs @@ -0,0 +1,72 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using PhoenixLib.Configuration; +using WingsEmu.Game._enum; +using WingsEmu.Game.Configurations.Miniland; + +namespace WingsEmu.Plugins.BasicImplementations.Miniland; + +public static class DependencyInjectionExtensions +{ + public static void AddMinilandModule(this IServiceCollection services) + { + services.AddFileConfiguration(new MinilandConfiguration + { + new() + { + MapVnum = (int)MapIds.MINILAND, + RestrictedZones = new List + { + new() + { + RestrictionTag = RestrictionType.OnlyMates, + Corner1 = new SerializablePosition { X = 2, Y = 7 }, + Corner2 = new SerializablePosition { X = 17, Y = 8 } + } + }, + ForcedPlacings = new List + { + new() + { + SubType = MinilandItemSubType.HOUSE, ForcedLocation = new SerializablePosition + { + X = 24, + Y = 6 + } + }, + new() + { + SubType = MinilandItemSubType.SMALL_HOUSE, ForcedLocation = new SerializablePosition + { + X = 21, + Y = 4 + } + }, + new() + { + SubType = MinilandItemSubType.WAREHOUSE, ForcedLocation = new SerializablePosition + { + X = 31, + Y = 2 + } + } + } + } + }); + services.AddConfigurationsFromDirectory("minigame_scores"); + services.AddConfigurationsFromDirectory("minigames"); + + services.AddFileConfiguration(); + services.AddSingleton(s => new MinigameConfiguration + { + Minigames = s.GetRequiredService>().ToList(), + ScoresHolders = s.GetRequiredService>().ToList(), + Configuration = s.GetRequiredService() + }); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Miniland/MinigameManager.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Miniland/MinigameManager.cs new file mode 100644 index 0000000..11be13d --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Miniland/MinigameManager.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using PhoenixLib.DAL.Redis.Locks; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Configurations.Miniland; +using WingsEmu.Game.Miniland; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Plugins.BasicImplementations.Managers; + +public class MinigameManager : IMinigameManager +{ + private readonly IExpirableLockService _lockService; + private readonly MinigameConfiguration _minigameConfiguration; + private readonly Dictionary _minigameInteractions = new(); + + public MinigameManager(MinigameConfiguration minigameConfiguration, IExpirableLockService lockService) + { + _minigameConfiguration = minigameConfiguration; + _lockService = lockService; + } + + public async Task CanRefreshMinigamesFreeProductionPoints(long characterId) => + await _lockService.TryAddTemporaryLockAsync($"game:locks:minigame-refresh:{characterId}", DateTime.UtcNow.Date.AddDays(1)); + + + public MinigameScoresHolder GetScores(int minigameVnum) + { + Minigame minigame = GetSpecificMinigameConfiguration(minigameVnum); + + MinigameScoresHolder minigameScoresHolder = _minigameConfiguration.ScoresHolders.FirstOrDefault(m => m.Type == minigame.Type); + + return minigameScoresHolder; + } + + public Minigame GetSpecificMinigameConfiguration(int minigameVnum) + { + Minigame minigame = _minigameConfiguration.Minigames.FirstOrDefault(m => m.Vnum == minigameVnum); + + return minigame; + } + + public void RegisterInteraction(IClientSession session, MinilandInteractionInformationHolder minilandInteraction) + { + if (_minigameInteractions.TryAdd(session.PlayerEntity.Id, minilandInteraction)) + { + return; + } + + _minigameInteractions[session.PlayerEntity.Id] = minilandInteraction; + } + + public void ReportInteractionIncoherence(IClientSession session, MinigameInteraction lastInteraction, MapDesignObject lastMapObject, MinigameInteraction actualInteraction, + MapDesignObject actualMapObject) + { + session.NotifyStrangeBehavior(StrangeBehaviorSeverity.NORMAL, + $"Incoherence between minigame interactions detected - 'LastAction': {lastInteraction} | 'LastMapObjectSlot': {lastMapObject.InventoryItem.Slot.ToString()}" + + $" | 'ActionSent': {actualInteraction} | 'ActualMapObjectSlot': {actualMapObject.InventoryItem.Slot.ToString()}"); + } + + public MinilandInteractionInformationHolder GetLastInteraction(IClientSession session) + => _minigameInteractions.TryGetValue(session.PlayerEntity.Id, out MinilandInteractionInformationHolder interaction) + ? interaction + : new MinilandInteractionInformationHolder(MinigameInteraction.None, default); +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Miniland/Minigames/MinigameRefreshEventProcessor.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Miniland/Minigames/MinigameRefreshEventProcessor.cs new file mode 100644 index 0000000..9e59df0 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Miniland/Minigames/MinigameRefreshEventProcessor.cs @@ -0,0 +1,43 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Miniland; +using WingsEmu.Game.Miniland.Minigames; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Plugins.BasicImplementations.Miniland.Minigames; + +public class MinigameRefreshProductionEventProcessor : IAsyncEventProcessor +{ + private readonly IMinigameManager _minilandManager; + + public MinigameRefreshProductionEventProcessor(IMinigameManager minilandManager) => _minilandManager = minilandManager; + + public async Task HandleAsync(MinigameRefreshProductionEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + + bool canRefresh = await _minilandManager.CanRefreshMinigamesFreeProductionPoints(session.PlayerEntity.Id); + + if (canRefresh == false && e.Force == false) + { + session.SendDebugMessage("Miniland points already refreshed for today"); + return; + } + + if (session.PlayerEntity.MinilandPoint >= 2000) + { + session.SendDebugMessage("Miniland points already at maximum free"); + return; + } + + session.PlayerEntity.MinilandPoint = 2000; + session.SendInformationChatMessage(session.GetLanguage(GameDialogKey.MINIGAME_CHATMESSAGE_PRODUCTION_REFRESHED)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Miniland/MinilandManager.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Miniland/MinilandManager.cs new file mode 100644 index 0000000..ddaf15e --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Miniland/MinilandManager.cs @@ -0,0 +1,183 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using PhoenixLib.DAL.Redis.Locks; +using PhoenixLib.Logging; +using WingsAPI.Data.Miniland; +using WingsEmu.Game._enum; +using WingsEmu.Game.Configurations.Miniland; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Miniland; +using WingsEmu.Game.Miniland.Events; +using WingsEmu.Game.Networking; +using WingsEmu.Plugins.BasicImplementations.Factories; + +namespace WingsEmu.Plugins.BasicImplementations.Miniland; + +public class MinilandManager : IMinilandManager +{ + private readonly IExpirableLockService _expirableLock; + private readonly IMapDesignObjectFactory _mapDesignObjectFactory; + private readonly IMapManager _mapManager; + private readonly IMinigameManager _minigameManager; + private readonly Dictionary _minilandCapacities = new(); + private readonly MinilandConfiguration _minilandConfiguration; + private readonly ConcurrentDictionary _minilandInstances = new(); + private readonly Dictionary> _minilandInvitations = new(); + private readonly ISessionManager _sessionManager; + + public MinilandManager(IMapManager mapManager, IMapDesignObjectFactory mapDesignObjectFactory, ISessionManager sessionManager, + IMinigameManager minigameManager, MinilandConfiguration minilandConfiguration, IExpirableLockService expirableLock) + { + _mapManager = mapManager; + _mapDesignObjectFactory = mapDesignObjectFactory; + _sessionManager = sessionManager; + _minigameManager = minigameManager; + _minilandConfiguration = minilandConfiguration; + _expirableLock = expirableLock; + } + + public async Task IncreaseMinilandVisitCounter(long characterId) + { + IClientSession session = _sessionManager.GetSessionByCharacterId(characterId); + await _expirableLock.TryIncrementTemporaryLockCounter($"game:locks:miniland-visit-counter:{characterId}", int.MaxValue, DateTime.UtcNow.Date.AddDays(1)); + session.PlayerEntity.LifetimeStats.TotalMinilandVisits++; + } + + public async Task CanRefreshDailyVisitCounter(long characterId) => + await _expirableLock.TryAddTemporaryLockAsync($"game:locks:miniland-visit-counter:{characterId}", DateTime.UtcNow.Date.AddDays(1)); + + public async Task GetMinilandVisitCounter(long characterId) + { + (bool exists, int counter) = await _expirableLock.TryGetTemporaryCounterValue($"game:locks:miniland-visit-counter:{characterId}"); + return counter; + } + + public IMapInstance CreateMinilandByCharacterSession(IClientSession session) + { + _minigameManager.RegisterInteraction(session, new MinilandInteractionInformationHolder(MinigameInteraction.None, default)); + + IMapInstance mapInstance = _mapManager.GenerateMapInstanceByMapId((int)MapIds.MINILAND, MapInstanceType.Miniland); + + Game.Configurations.Miniland.Miniland minilandConfiguration = GetMinilandConfiguration(mapInstance); + + SetMinilandCapacity(session.PlayerEntity.Id, minilandConfiguration.DefaultMaximumCapacity); + + // It will remove object if somehow player doesn't have an item in inventory or the object just disappeared + List toRemove = new(); + foreach (CharacterMinilandObjectDto obj in session.PlayerEntity.MinilandObjects) + { + try + { + MapDesignObject mapObject = _mapDesignObjectFactory.CreateGameObject(session.PlayerEntity.Id, obj); + if (mapObject == null) + { + toRemove.Add(obj); + continue; + } + + session.EmitEventAsync(new AddObjMinilandEndLogicEvent(mapObject, mapInstance)); + } + catch (Exception e) + { + Log.Error("[MINILAND_MANAGER] Couldn't create map design object", e); + } + } + + foreach (CharacterMinilandObjectDto obj in toRemove) + { + session.PlayerEntity.MinilandObjects.Remove(obj); + } + + return !_minilandInstances.TryAdd(session.PlayerEntity.Id, mapInstance) ? null : mapInstance; + } + + public IMapInstance GetMinilandByCharacterId(long characterId) => _minilandInstances.GetValueOrDefault(characterId); + + public IClientSession GetSessionByMiniland(IMapInstance mapInstance) + { + foreach ((long characterId, IMapInstance instance) in _minilandInstances) + { + if (instance.Id == mapInstance.Id) + { + return _sessionManager.GetSessionByCharacterId(characterId); + } + } + + return default; + } + + public void SaveMinilandInvite(long senderId, long targetId) + { + if (!_minilandInvitations.TryGetValue(senderId, out List invites)) + { + invites = new List(); + _minilandInvitations[senderId] = invites; + } + + invites.Add(targetId); + } + + public bool ContainsMinilandInvite(long senderId) => _minilandInvitations.ContainsKey(senderId); + + public bool ContainsTargetInvite(long senderId, long targetId) => _minilandInvitations.TryGetValue(senderId, out List targetList) && targetList.Contains(targetId); + + public void RemoveMinilandInvite(long senderId, long targetId) + { + if (!_minilandInvitations.TryGetValue(senderId, out List invites)) + { + invites = new List(); + } + + invites.Remove(targetId); + } + + public int GetMinilandMaximumCapacity(long characterId) => _minilandCapacities.TryGetValue(characterId, out int capacity) ? capacity : default; + + public void RelativeUpdateMinilandCapacity(long characterId, int valueToAdd) + { + if (_minilandCapacities.TryAdd(characterId, valueToAdd)) + { + return; + } + + _minilandCapacities[characterId] += valueToAdd; + } + + public Game.Configurations.Miniland.Miniland GetMinilandConfiguration(IMapInstance mapInstance) + { + if (mapInstance == null) + { + return null; + } + + Game.Configurations.Miniland.Miniland minilandConfiguration = _minilandConfiguration.FirstOrDefault(x => x.MapVnum == mapInstance.MapId); + + return minilandConfiguration; + } + + public void RemoveMiniland(long characterId) + { + if (!_minilandInstances.TryGetValue(characterId, out IMapInstance miniland)) + { + return; + } + + _minilandInstances.TryRemove(characterId, out _); + _mapManager.RemoveMapInstance(miniland.Id); + miniland.Destroy(); + } + + private void SetMinilandCapacity(long characterId, int value) + { + if (_minilandCapacities.TryAdd(characterId, value)) + { + return; + } + + _minilandCapacities[characterId] = value; + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogHandlerContainer.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogHandlerContainer.cs new file mode 100644 index 0000000..823730f --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogHandlerContainer.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using PhoenixLib.Logging; +using WingsEmu.Game._NpcDialog; +using WingsEmu.Game._NpcDialog.Event; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations; + +public class NpcDialogHandlerContainer : INpcDialogHandlerContainer +{ + private readonly Dictionary _handlers; + + public NpcDialogHandlerContainer() => _handlers = new Dictionary(); + + public void Register(INpcDialogAsyncHandler handler) + { + foreach (NpcRunType npcRunType in handler.NpcRunTypes) + { + if (_handlers.ContainsKey(npcRunType)) + { + continue; + } + + _handlers.Add(npcRunType, handler); + Log.Debug($"[NPC_DIALOG][REGISTER_HANDLER] NPC_RUN_TYPE: {npcRunType.ToString()} REGISTERED!"); + } + } + + public void Unregister(INpcDialogAsyncHandler handler) + { + foreach (NpcRunType npcRunType in handler.NpcRunTypes) + { + Log.Debug($"[NPC_DIALOG][UNREGISTER_HANDLER] NPC_RUN_TYPE: {handler.NpcRunTypes} UNREGISTERED!"); + _handlers.Remove(npcRunType); + } + } + + public void Execute(IClientSession session, NpcDialogEvent e) + { + ExecuteAsync(session, e).ConfigureAwait(false).GetAwaiter().GetResult(); + } + + public async Task ExecuteAsync(IClientSession session, NpcDialogEvent e) + { + if (!_handlers.TryGetValue(e.NpcRunType, out INpcDialogAsyncHandler handler)) + { + Log.Debug($"[HANDLER_NOT_FOUND] NPC_RUN_TYPE: {e.NpcRunType}"); + return; + } + + Log.Debug($"[NPC_DIALOG][HANDLER] Handling: {e.NpcRunType}"); + await handler.Execute(session, e); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogPlugin.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogPlugin.cs new file mode 100644 index 0000000..3fd9012 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogPlugin.cs @@ -0,0 +1,43 @@ +using System; +using PhoenixLib.Extensions; +using PhoenixLib.Logging; +using WingsAPI.Plugins; +using WingsEmu.Game._NpcDialog; + +namespace WingsEmu.Plugins.BasicImplementations; + +public class NpcDialogPlugin : IGamePlugin +{ + private readonly IServiceProvider _container; + private readonly INpcDialogHandlerContainer _handlers; + + public NpcDialogPlugin(INpcDialogHandlerContainer handlers, IServiceProvider container) + { + _handlers = handlers; + _container = container; + } + + public string Name => nameof(NpcDialogPlugin); + + public void OnLoad() + { + foreach (Type handlerType in typeof(NpcDialogPlugin).Assembly.GetTypesImplementingInterface()) + { + try + { + object tmp = _container.GetService(handlerType); + if (tmp is not INpcDialogAsyncHandler real) + { + continue; + } + + Log.Debug($"[NPC_DIALOG][ADD_HANDLER] {handlerType}"); + _handlers.Register(real); + } + catch (Exception e) + { + Log.Error("[NPC_DIALOG][FAIL_ADD]", e); + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogPluginCore.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogPluginCore.cs new file mode 100644 index 0000000..3dc16d8 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogPluginCore.cs @@ -0,0 +1,16 @@ +using Microsoft.Extensions.DependencyInjection; +using WingsAPI.Plugins; +using WingsEmu.Game._NpcDialog; + +namespace WingsEmu.Plugins.BasicImplementations; + +public class NpcDialogPluginCore : IGameServerPlugin +{ + public string Name => nameof(NpcDialogPluginCore); + + public void AddDependencies(IServiceCollection services, GameServerLoader gameServer) + { + services.AddHandlers(); + services.AddSingleton(); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Act4_Act5/Act4EnterShipHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Act4_Act5/Act4EnterShipHandler.cs new file mode 100644 index 0000000..26f2cad --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Act4_Act5/Act4EnterShipHandler.cs @@ -0,0 +1,56 @@ +using System.Threading.Tasks; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game._i18n; +using WingsEmu.Game._NpcDialog; +using WingsEmu.Game._NpcDialog.Event; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Ship.Configuration; +using WingsEmu.Game.Ship.Event; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.NpcDialogs.Act4_Act5; + +public class Act4EnterShipHandler : INpcDialogAsyncHandler +{ + private readonly IGameLanguageService _languageService; + + public Act4EnterShipHandler(IGameLanguageService languageService) => _languageService = languageService; + + public NpcRunType[] NpcRunTypes => new[] { NpcRunType.ACT4_ENTER_SHIP }; + + public async Task Execute(IClientSession session, NpcDialogEvent e) + { + INpcEntity npcEntity = session.CurrentMapInstance.GetNpcById(e.NpcId); + if (npcEntity == null) + { + return; + } + + if (!session.CurrentMapInstance.HasMapFlag(MapFlags.IS_BASE_MAP)) + { + return; + } + + if (session.PlayerEntity.IsInRaidParty) + { + return; + } + + if (session.PlayerEntity.TimeSpaceComponent.IsInTimeSpaceParty) + { + return; + } + + if (session.PlayerEntity.Faction == FactionType.Neutral) + { + session.SendErrorChatMessage(_languageService.GetLanguage(GameDialogKey.ACT4_NEED_FACTION, session.UserLanguage)); + return; + } + + ShipType shipType = session.PlayerEntity.Faction == FactionType.Angel ? ShipType.Act4Angels : ShipType.Act4Demons; + + await e.Sender.EmitEventAsync(new ShipEnterEvent(shipType)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Act4_Act5/Act4LeaveHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Act4_Act5/Act4LeaveHandler.cs new file mode 100644 index 0000000..d1cd447 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Act4_Act5/Act4LeaveHandler.cs @@ -0,0 +1,41 @@ +using System.Threading.Tasks; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game._NpcDialog; +using WingsEmu.Game._NpcDialog.Event; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.NpcDialogs.Act4_Act5; + +public class Act4LeaveHandler : INpcDialogAsyncHandler +{ + public NpcRunType[] NpcRunTypes => new[] { NpcRunType.ACT4_LEAVE }; + + public async Task Execute(IClientSession session, NpcDialogEvent e) + { + INpcEntity npcEntity = session.CurrentMapInstance.GetNpcById(e.NpcId); + if (npcEntity == null) + { + return; + } + + if (session.PlayerEntity.IsInRaidParty) + { + return; + } + + if (session.PlayerEntity.TimeSpaceComponent.IsInTimeSpaceParty) + { + return; + } + + if (!session.CurrentMapInstance.HasMapFlag(MapFlags.ACT_4)) + { + return; + } + + await session.EmitEventAsync(new PlayerReturnFromAct4Event()); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Act4_Act5/Act4LeaveShipHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Act4_Act5/Act4LeaveShipHandler.cs new file mode 100644 index 0000000..21fbb61 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Act4_Act5/Act4LeaveShipHandler.cs @@ -0,0 +1,41 @@ +using System.Threading.Tasks; +using WingsEmu.Game._NpcDialog; +using WingsEmu.Game._NpcDialog.Event; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Ship.Event; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.NpcDialogs.Act4_Act5; + +public class Act4LeaveShipHandler : INpcDialogAsyncHandler +{ + public NpcRunType[] NpcRunTypes => new[] { NpcRunType.ACT4_LEAVE_SHIP }; + + public async Task Execute(IClientSession session, NpcDialogEvent e) + { + INpcEntity npcEntity = session.CurrentMapInstance.GetNpcById(e.NpcId); + if (npcEntity == null) + { + return; + } + + if (session.PlayerEntity.IsInRaidParty) + { + return; + } + + if (session.PlayerEntity.TimeSpaceComponent.IsInTimeSpaceParty) + { + return; + } + + if (session.CurrentMapInstance.MapInstanceType != MapInstanceType.NormalInstance) + { + return; + } + + await session.EmitEventAsync(new ShipLeaveEvent()); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Act4_Act5/Act5LeaveHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Act4_Act5/Act5LeaveHandler.cs new file mode 100644 index 0000000..6b7b4e6 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Act4_Act5/Act5LeaveHandler.cs @@ -0,0 +1,41 @@ +using System.Threading.Tasks; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game._NpcDialog; +using WingsEmu.Game._NpcDialog.Event; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.NpcDialogs.Act4_Act5; + +public class Act5LeaveHandler : INpcDialogAsyncHandler +{ + public NpcRunType[] NpcRunTypes => new[] { NpcRunType.ACT5_LEAVE }; + + public async Task Execute(IClientSession session, NpcDialogEvent e) + { + INpcEntity npcEntity = session.CurrentMapInstance.GetNpcById(e.NpcId); + if (npcEntity == null) + { + return; + } + + if (session.PlayerEntity.IsInRaidParty) + { + return; + } + + if (session.PlayerEntity.TimeSpaceComponent.IsInTimeSpaceParty) + { + return; + } + + if (!session.CurrentMapInstance.HasMapFlag(MapFlags.IS_BASE_MAP)) + { + return; + } + + session.ChangeMap(145, 52, 41); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Act4_Act5/Act5LeaveShipHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Act4_Act5/Act5LeaveShipHandler.cs new file mode 100644 index 0000000..0f20e74 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Act4_Act5/Act5LeaveShipHandler.cs @@ -0,0 +1,41 @@ +using System.Threading.Tasks; +using WingsEmu.Game._NpcDialog; +using WingsEmu.Game._NpcDialog.Event; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Ship.Event; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.NpcDialogs.Act4_Act5; + +public class Act5LeaveShipHandler : INpcDialogAsyncHandler +{ + public NpcRunType[] NpcRunTypes => new[] { NpcRunType.ACT5_LEAVE_SHIP }; + + public async Task Execute(IClientSession session, NpcDialogEvent e) + { + INpcEntity npcEntity = session.CurrentMapInstance.GetNpcById(e.NpcId); + if (npcEntity == null) + { + return; + } + + if (session.PlayerEntity.IsInRaidParty) + { + return; + } + + if (session.PlayerEntity.TimeSpaceComponent.IsInTimeSpaceParty) + { + return; + } + + if (session.CurrentMapInstance.MapInstanceType != MapInstanceType.NormalInstance) + { + return; + } + + await session.EmitEventAsync(new ShipLeaveEvent()); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Act4_Act5/Act5ShipEnterHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Act4_Act5/Act5ShipEnterHandler.cs new file mode 100644 index 0000000..575b698 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Act4_Act5/Act5ShipEnterHandler.cs @@ -0,0 +1,48 @@ +using System.Threading.Tasks; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game._NpcDialog; +using WingsEmu.Game._NpcDialog.Event; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Ship.Configuration; +using WingsEmu.Game.Ship.Event; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.NpcDialogs.Act4_Act5; + +public class Act5ShipEnterHandler : INpcDialogAsyncHandler +{ + public NpcRunType[] NpcRunTypes => new[] { NpcRunType.ACT5_ENTER_SHIP }; + + public async Task Execute(IClientSession session, NpcDialogEvent e) + { + INpcEntity npcEntity = session.CurrentMapInstance.GetNpcById(e.NpcId); + if (npcEntity == null) + { + return; + } + + if (session.CantPerformActionOnAct4()) + { + return; + } + + if (session.PlayerEntity.IsInRaidParty) + { + return; + } + + if (session.PlayerEntity.TimeSpaceComponent.IsInTimeSpaceParty) + { + return; + } + + if (!session.CurrentMapInstance.HasMapFlag(MapFlags.IS_BASE_MAP)) + { + return; + } + + await e.Sender.EmitEventAsync(new ShipEnterEvent(ShipType.Act5)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Act6/Act6FirstMission.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Act6/Act6FirstMission.cs new file mode 100644 index 0000000..e1a3f22 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Act6/Act6FirstMission.cs @@ -0,0 +1,16 @@ +using System.Threading.Tasks; +using WingsEmu.Game._NpcDialog; +using WingsEmu.Game._NpcDialog.Event; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.NpcDialogs.Act6; + +public class Act6FirstMission : INpcDialogAsyncHandler +{ + public NpcRunType[] NpcRunTypes => new[] { NpcRunType.ACT61_MISSION_FIRST }; + + public async Task Execute(IClientSession session, NpcDialogEvent e) + { + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Act6/TeleportCylloanHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Act6/TeleportCylloanHandler.cs new file mode 100644 index 0000000..7e5fbaf --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Act6/TeleportCylloanHandler.cs @@ -0,0 +1,45 @@ +using System.Threading.Tasks; +using WingsEmu.Game._i18n; +using WingsEmu.Game._NpcDialog; +using WingsEmu.Game._NpcDialog.Event; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.NpcDialogs.Act6; + +public class TeleportCylloanHandler : INpcDialogAsyncHandler +{ + private readonly IGameLanguageService _gameLanguage; + private readonly IMapManager _mapManager; + + public TeleportCylloanHandler(IMapManager mapManager, IGameLanguageService gameLanguage) + { + _mapManager = mapManager; + _gameLanguage = gameLanguage; + } + + public NpcRunType[] NpcRunTypes => new[] { NpcRunType.ACT61_TELEPORT_CYLLOAN }; + + public async Task Execute(IClientSession session, NpcDialogEvent e) + { + /*INpcEntity npcEntity = session.CurrentMapInstance.GetNpcById(e.NpcId); + if (npcEntity == null) + { + return; + } + + if (session.CurrentMapInstance.MapInstanceType != MapInstanceType.BaseMapInstance) + { + return; + } + + if (session.PlayerEntity.Level < 88 && session.PlayerEntity.HeroLevel < 1) + { + session.SendInformationChatMessage(_gameLanguage.GetLanguage(GameDialogKey.TOO_LOW_LVL, session.UserLanguage)); + return; + } + + session.ChangeMap(228, 85, 104);*/ + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Arena/ArenaMastersRegisterHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Arena/ArenaMastersRegisterHandler.cs new file mode 100644 index 0000000..45a5c96 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Arena/ArenaMastersRegisterHandler.cs @@ -0,0 +1,16 @@ +using System.Threading.Tasks; +using WingsEmu.Game._NpcDialog; +using WingsEmu.Game._NpcDialog.Event; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.NpcDialogs.Arena; + +public class ArenaMastersRegisterHandler : INpcDialogAsyncHandler +{ + public NpcRunType[] NpcRunTypes => new[] { NpcRunType.ARENA_OF_MASTERS_REGISTRATION }; + + public async Task Execute(IClientSession session, NpcDialogEvent e) + { + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Arena/ArenaSpectatorHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Arena/ArenaSpectatorHandler.cs new file mode 100644 index 0000000..752ee62 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Arena/ArenaSpectatorHandler.cs @@ -0,0 +1,28 @@ +using System.Threading.Tasks; +using WingsEmu.Game._NpcDialog; +using WingsEmu.Game._NpcDialog.Event; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.NpcDialogs.Arena; + +public class ArenaSpectatorHandler : INpcDialogAsyncHandler +{ + public NpcRunType[] NpcRunTypes => new[] { NpcRunType.ARENA_OF_MASTERS_SPECTATOR_CHOICE }; + + public async Task Execute(IClientSession session, NpcDialogEvent e) + { + /*INpcEntity npcEntity = session.CurrentMapInstance.GetNpcById(e.NpcId); + if (npcEntity == null) + { + return; + } + + if (session.CurrentMapInstance.MapInstanceType != MapInstanceType.ArenaInstance) + { + return; + } + + session.SendSpectatorWindow();*/ + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Arena/ArenaTalentsRegisterHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Arena/ArenaTalentsRegisterHandler.cs new file mode 100644 index 0000000..fa43c74 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Arena/ArenaTalentsRegisterHandler.cs @@ -0,0 +1,27 @@ +using System.Threading.Tasks; +using WingsEmu.Game._i18n; +using WingsEmu.Game._NpcDialog; +using WingsEmu.Game._NpcDialog.Event; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.NpcDialogs.Arena; + +public class ArenaTalentsRegisterHandler : INpcDialogAsyncHandler +{ + private readonly IGameLanguageService _langService; + private readonly IServerManager _serverManager; + + public ArenaTalentsRegisterHandler(IGameLanguageService langService, IServerManager serverManager) + { + _langService = langService; + _serverManager = serverManager; + } + + public NpcRunType[] NpcRunTypes => new[] { NpcRunType.ARENA_OF_TALENTS_REGISTRATION }; + + public async Task Execute(IClientSession session, NpcDialogEvent e) + { + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Arena/JoinArenaHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Arena/JoinArenaHandler.cs new file mode 100644 index 0000000..8881752 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Arena/JoinArenaHandler.cs @@ -0,0 +1,64 @@ +using System; +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.Arena; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game._i18n; +using WingsEmu.Game._NpcDialog; +using WingsEmu.Game._NpcDialog.Event; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.NpcDialogs.Arena; + +public class JoinArenaHandler : INpcDialogAsyncHandler +{ + private readonly IGameLanguageService _langService; + private readonly ISessionManager _sessionManager; + + public JoinArenaHandler(IGameLanguageService langService, ISessionManager sessionManager) + { + _langService = langService; + _sessionManager = sessionManager; + } + + public long[] HandledIds => new long[] { 17 }; + + public NpcRunType[] NpcRunTypes => new[] { NpcRunType.ASK_ENTER_ARENA }; + + public async Task Execute(IClientSession session, NpcDialogEvent e) + { + double timeSpanSinceLastPortal = (DateTime.UtcNow - session.PlayerEntity.LastPortal).TotalSeconds; + + if (timeSpanSinceLastPortal < 4 || !session.HasCurrentMapInstance) + { + session.SendChatMessage(_langService.GetLanguage(GameDialogKey.PORTAL_CHATMESSAGE_TOO_EARLY, session.UserLanguage), ChatMessageColorType.Yellow); + return; + } + + if (session.PlayerEntity.IsInRaidParty) + { + return; + } + + if (session.PlayerEntity.TimeSpaceComponent.IsInTimeSpaceParty) + { + return; + } + + bool asksForFamilyArena = e.Argument != 0; + + if (session.CantPerformActionOnAct4() || !session.CurrentMapInstance.HasMapFlag(MapFlags.IS_BASE_MAP) || !session.PlayerEntity.IsAlive()) + { + return; + } + + long cost = ArenaExtensions.GetArenaEntryPrice(asksForFamilyArena); + + session.SendQnaPacket($"arena {(asksForFamilyArena ? 1 : 0).ToString()}", + _langService.GetLanguageFormat(asksForFamilyArena ? GameDialogKey.FAMILYARENA_INFO_ASK_ENTER : GameDialogKey.ARENA_INFO_ASK_ENTER, session.UserLanguage, cost.ToString())); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/BankSavingBookHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/BankSavingBookHandler.cs new file mode 100644 index 0000000..3756991 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/BankSavingBookHandler.cs @@ -0,0 +1,53 @@ +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game._NpcDialog; +using WingsEmu.Game._NpcDialog.Event; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Items; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.NpcDialogs; + +public class BankSavingBookHandler : INpcDialogAsyncHandler +{ + private readonly IGameItemInstanceFactory _gameItemInstanceFactory; + private readonly IGameLanguageService _langService; + + public BankSavingBookHandler(IGameLanguageService langService, IGameItemInstanceFactory gameItemInstanceFactory) + { + _langService = langService; + _gameItemInstanceFactory = gameItemInstanceFactory; + } + + public NpcRunType[] NpcRunTypes => new[] { NpcRunType.GET_BANK_BOOK }; + + public async Task Execute(IClientSession session, NpcDialogEvent e) + { + INpcEntity npcEntity = session.CurrentMapInstance.GetNpcById(e.NpcId); + if (npcEntity == null) + { + return; + } + + if (session.CantPerformActionOnAct4()) + { + return; + } + + bool hasItem = session.PlayerEntity.HasItem((short)ItemVnums.CUARRY_BANK_SAVINGS_BOOK); + + if (hasItem) + { + session.SendChatMessage(_langService.GetLanguage(GameDialogKey.BANK_CHATMESSAGE_HAS_DEBIT_CARD_ALREADY, session.UserLanguage), ChatMessageColorType.Red); + return; + } + + GameItemInstance item = _gameItemInstanceFactory.CreateItem((short)ItemVnums.CUARRY_BANK_SAVINGS_BOOK); + await session.AddNewItemToInventory(item, true, ChatMessageColorType.Yellow, true); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/ChangeClassHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/ChangeClassHandler.cs new file mode 100644 index 0000000..29d4d49 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/ChangeClassHandler.cs @@ -0,0 +1,73 @@ +using System.Linq; +using System.Threading.Tasks; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game._NpcDialog; +using WingsEmu.Game._NpcDialog.Event; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Character; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.NpcDialogs; + +public class ChangeClassHandler : INpcDialogAsyncHandler +{ + private readonly IGameLanguageService _langService; + + public ChangeClassHandler(IGameLanguageService langService) => _langService = langService; + + public NpcRunType[] NpcRunTypes => new[] { NpcRunType.CHANGE_CLASS }; + + public async Task Execute(IClientSession session, NpcDialogEvent e) + { + if (session.PlayerEntity.Class != (byte)ClassType.Adventurer) + { + session.SendMsg(_langService.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_NO_ADNVENTURER, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (session.CantPerformActionOnAct4()) + { + return; + } + + if (session.PlayerEntity.Level < 15 || session.PlayerEntity.JobLevel < 20) + { + session.SendMsg(_langService.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_TOO_LOW_LVL, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (session.PlayerEntity.IsInGroup()) + { + session.SendMsg(_langService.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_NEED_LEAVE_GROUP, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (e.Argument == (byte)session.PlayerEntity.Class) + { + return; + } + + if (e.Argument >= 4 || e.Argument < 0) + { + return; + } + + if (session.PlayerEntity.EquippedItems.Any(s => s != null && s.ItemInstance.GameItem.Class == (byte)ItemClassType.Adventurer)) + { + session.SendMsg(_langService.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_EQ_NOT_EMPTY, session.UserLanguage), MsgMessageType.Middle); + return; + } + + await session.EmitEventAsync(new ChangeClassEvent + { + NewClass = (ClassType)e.Argument, + ShouldObtainBasicItems = true, + ShouldObtainNewFaction = true, + ShouldResetJobLevel = true + }); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/ChangeSpawnHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/ChangeSpawnHandler.cs new file mode 100644 index 0000000..64038c6 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/ChangeSpawnHandler.cs @@ -0,0 +1,45 @@ +using System.Threading.Tasks; +using WingsEmu.Game._i18n; +using WingsEmu.Game._NpcDialog; +using WingsEmu.Game._NpcDialog.Event; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Game.RespawnReturn.Event; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.NpcDialogs; + +public class ChangeSpawnHandler : INpcDialogAsyncHandler +{ + private readonly IGameLanguageService _langService; + + public ChangeSpawnHandler(IGameLanguageService langService) => _langService = langService; + + public NpcRunType[] NpcRunTypes => new[] { NpcRunType.SET_REVIVAL_SPAWN }; + + public async Task Execute(IClientSession session, NpcDialogEvent e) + { + INpcEntity npcEntity = session.CurrentMapInstance.GetNpcById(e.NpcId); + if (npcEntity == null) + { + return; + } + + if (session.CantPerformActionOnAct4()) + { + return; + } + + if (e.VisualType == VisualType.Npc) + { + session.SendQnaPacket($"n_run^15^1^1^{npcEntity.Id}", _langService.GetLanguage(GameDialogKey.RESPAWN_DIALOG_ASK_CHANGE_SPAWN_LOCATION, session.UserLanguage)); + return; + } + + await session.EmitEventAsync(new RespawnChangeEvent + { + MapId = npcEntity.MapInstance.MapId + }); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/GenerateNpcDialogHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/GenerateNpcDialogHandler.cs new file mode 100644 index 0000000..50fa2b7 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/GenerateNpcDialogHandler.cs @@ -0,0 +1,14 @@ +using System.Threading.Tasks; +using WingsEmu.Game._NpcDialog; +using WingsEmu.Game._NpcDialog.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.NpcDialogs; + +public class GenerateNpcDialogHandler : INpcDialogAsyncHandler +{ + public NpcRunType[] NpcRunTypes => new[] { NpcRunType.CIRCLE_TIME_SKILL }; + public async Task Execute(IClientSession session, NpcDialogEvent e) => session.SendNpcDialog(17); +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/JewelryWindowHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/JewelryWindowHandler.cs new file mode 100644 index 0000000..1d484a7 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/JewelryWindowHandler.cs @@ -0,0 +1,25 @@ +using System.Threading.Tasks; +using WingsEmu.Game._NpcDialog; +using WingsEmu.Game._NpcDialog.Event; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.NpcDialogs; + +public class JewelryWindowHandler : INpcDialogAsyncHandler +{ + public NpcRunType[] NpcRunTypes => new[] { NpcRunType.JEWELRY_CELLON }; + + public async Task Execute(IClientSession session, NpcDialogEvent e) + { + INpcEntity npcEntity = session.CurrentMapInstance.GetNpcById(e.NpcId); + if (npcEntity == null) + { + return; + } + + session.SendWopenPacket(WindowType.MERGE_JEWELRY); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/JoinLodHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/JoinLodHandler.cs new file mode 100644 index 0000000..6083863 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/JoinLodHandler.cs @@ -0,0 +1,16 @@ +using System.Threading.Tasks; +using WingsEmu.Game._NpcDialog; +using WingsEmu.Game._NpcDialog.Event; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.NpcDialogs; + +public class JoinLodHandler : INpcDialogAsyncHandler +{ + public NpcRunType[] NpcRunTypes => new[] { NpcRunType.LAND_OF_CHAOS_ENTER }; + + public async Task Execute(IClientSession session, NpcDialogEvent e) + { + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/MateHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/MateHandler.cs new file mode 100644 index 0000000..17b0ad3 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/MateHandler.cs @@ -0,0 +1,136 @@ +using System; +using System.Threading.Tasks; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game._NpcDialog; +using WingsEmu.Game._NpcDialog.Event; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Mates.Events; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.NpcDialogs; + +public class MateHandler : INpcDialogAsyncHandler +{ + private readonly IDelayManager _delayManager; + private readonly IGameLanguageService _gameLanguage; + + public MateHandler(IGameLanguageService gameLanguage, IDelayManager delayManager) + { + _gameLanguage = gameLanguage; + _delayManager = delayManager; + } + + public NpcRunType[] NpcRunTypes => new[] { NpcRunType.MATE }; + + public async Task Execute(IClientSession session, NpcDialogEvent e) + { + IMateEntity mateEntity = session.PlayerEntity.MateComponent.GetMate(m => m.Id == e.NpcId); + + if (mateEntity == null) + { + return; + } + + if (!mateEntity.IsAlive()) + { + return; + } + + switch ((MateNrunType)e.Argument) + { + case MateNrunType.CompanyOrSendBack: + if (session.PlayerEntity.Miniland.Id != session.PlayerEntity.MapInstance.Id) + { + return; + } + + await session.EmitEventAsync(new MateJoinInMinilandEvent { MateEntity = mateEntity }); + break; + case MateNrunType.Stay: + if (session.PlayerEntity.Miniland.Id != session.PlayerEntity.MapInstance.Id) + { + return; + } + + await session.EmitEventAsync(new MateStayInsideMinilandEvent { MateEntity = mateEntity }); + break; + case MateNrunType.KickPetFromAnywhere: + if (session.PlayerEntity.Miniland.Id != session.PlayerEntity.MapInstance.Id) + { + session.SendQnaPacket($"n_run 4 5 3 {mateEntity.Id}", _gameLanguage.GetLanguage(GameDialogKey.PET_DIALOG_ASK_SEND_BACK, session.UserLanguage)); + return; + } + + await session.EmitEventAsync(new MateStayInsideMinilandEvent { MateEntity = mateEntity }); + break; + case MateNrunType.TriggerPetKick: + DateTime waitUntil = await _delayManager.RegisterAction(session.PlayerEntity, DelayedActionType.KickPet); + session.SendDelay((int)(waitUntil - DateTime.UtcNow).TotalMilliseconds, GuriType.SendBack, $"n_run 4 6 3 {mateEntity.Id}"); + break; + case MateNrunType.KickPet: + bool canKickPet = await _delayManager.CanPerformAction(session.PlayerEntity, DelayedActionType.KickPet); + if (session.PlayerEntity.Miniland.Id == session.PlayerEntity.MapInstance.Id) + { + return; + } + + if (!canKickPet) + { + return; + } + + await _delayManager.CompleteAction(session.PlayerEntity, DelayedActionType.KickPet); + string mateName = string.IsNullOrEmpty(mateEntity.MateName) || mateEntity.MateName == mateEntity.Name + ? _gameLanguage.GetLanguage(GameDataType.NpcMonster, mateEntity.Name, session.UserLanguage) + : mateEntity.MateName; + await session.EmitEventAsync(new MateLeaveTeamEvent { MateEntity = mateEntity }); + + GameDialogKey key = mateEntity.MateType == MateType.Pet ? GameDialogKey.PET_MESSAGE_KICKED : GameDialogKey.PARTNER_MESSAGE_KICKED; + + session.SendChatMessage(_gameLanguage.GetLanguageFormat(key, session.UserLanguage, mateName), ChatMessageColorType.Red); + session.SendMsg(_gameLanguage.GetLanguageFormat(key, session.UserLanguage, mateName), MsgMessageType.Middle); + break; + case MateNrunType.TriggerSummon: + if (!mateEntity.IsSummonable) + { + return; + } + + if (!mateEntity.IsAlive()) + { + return; + } + + if (e.Sender.PlayerEntity.MateComponent.GetMate(s => s.IsTeamMember && s.MateType == mateEntity.MateType) != null) + { + e.Sender.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.PET_MESSAGE_ALREADY_IN_TEAM, e.Sender.UserLanguage), ChatMessageColorType.Red); + e.Sender.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.PET_MESSAGE_ALREADY_IN_TEAM, e.Sender.UserLanguage), MsgMessageType.Middle); + return; + } + + waitUntil = await _delayManager.RegisterAction(e.Sender.PlayerEntity, DelayedActionType.SummonPet); + e.Sender.SendDelay((int)(waitUntil - DateTime.UtcNow).TotalMilliseconds, GuriType.Summon, $"n_run 4 9 3 {mateEntity.Id}"); + break; + + case MateNrunType.Summon: + bool canSummonPet = await _delayManager.CanPerformAction(e.Sender.PlayerEntity, DelayedActionType.SummonPet); + if (!canSummonPet) + { + return; + } + + await _delayManager.CompleteAction(e.Sender.PlayerEntity, DelayedActionType.SummonPet); + await session.EmitEventAsync(new MateSummonEvent + { + MateEntity = mateEntity + }); + break; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/OpenBankHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/OpenBankHandler.cs new file mode 100644 index 0000000..3ef840d --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/OpenBankHandler.cs @@ -0,0 +1,21 @@ +using System.Threading.Tasks; +using WingsEmu.Game._NpcDialog; +using WingsEmu.Game._NpcDialog.Event; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.NpcDialogs; + +public class OpenBankHandler : INpcDialogAsyncHandler +{ + public NpcRunType[] NpcRunTypes => new[] { NpcRunType.USE_BANK }; + + public async Task Execute(IClientSession session, NpcDialogEvent e) + { + await session.EmitEventAsync(new BankOpenEvent + { + NpcId = e.NpcId + }); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/OpenNosBazaarHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/OpenNosBazaarHandler.cs new file mode 100644 index 0000000..968d31c --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/OpenNosBazaarHandler.cs @@ -0,0 +1,31 @@ +using System.Threading.Tasks; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game._NpcDialog; +using WingsEmu.Game._NpcDialog.Event; +using WingsEmu.Game.Bazaar.Events; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.NpcDialogs; + +public class OpenNosBazaarHandler : INpcDialogAsyncHandler +{ + public NpcRunType[] NpcRunTypes => new[] { NpcRunType.OPEN_NOSBAZAAR }; + + public async Task Execute(IClientSession session, NpcDialogEvent e) + { + INpcEntity npcEntity = session.CurrentMapInstance.GetNpcById(e.NpcId); + if (npcEntity == null) + { + return; + } + + if (!session.CurrentMapInstance.HasMapFlag(MapFlags.IS_BASE_MAP) && !session.CurrentMapInstance.HasMapFlag(MapFlags.ACT_4)) + { + return; + } + + await session.EmitEventAsync(new BazaarOpenUiEvent(false)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/OpenWindowHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/OpenWindowHandler.cs new file mode 100644 index 0000000..61100e9 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/OpenWindowHandler.cs @@ -0,0 +1,14 @@ +using System.Threading.Tasks; +using WingsEmu.Game._NpcDialog; +using WingsEmu.Game._NpcDialog.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.NpcDialogs; + +public class OpenWindowHandler : INpcDialogAsyncHandler +{ + public NpcRunType[] NpcRunTypes => new[] { NpcRunType.OPEN_WINDOW }; + public async Task Execute(IClientSession session, NpcDialogEvent e) => session.SendWopenPacket((byte)e.Argument); +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Quests/QuestAdditionalAct5Handler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Quests/QuestAdditionalAct5Handler.cs new file mode 100644 index 0000000..b0fc6cc --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Quests/QuestAdditionalAct5Handler.cs @@ -0,0 +1,65 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.Quests; +using WingsEmu.DTOs.Quests; +using WingsEmu.Game._NpcDialog; +using WingsEmu.Game._NpcDialog.Event; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Quests; +using WingsEmu.Game.Quests.Event; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.NpcDialogs.Quests; + +public class QuestAdditionalAct5Handler : INpcDialogAsyncHandler +{ + // This nrun should be raised only for dialog-related quests + private static readonly HashSet NpcTalkQuests = new() + { + QuestType.DIALOG, + QuestType.DIALOG_2, + QuestType.DELIVER_ITEM_TO_NPC, + QuestType.GIVE_ITEM_TO_NPC, + QuestType.GIVE_ITEM_TO_NPC_2, + QuestType.GIVE_NPC_GOLD, + QuestType.DIALOG_WHILE_WEARING, + QuestType.DIALOG_WHILE_HAVING_ITEM, + QuestType.WIN_RAID_AND_TALK_TO_NPC + }; + + private readonly IQuestManager _questManager; + + public QuestAdditionalAct5Handler(IQuestManager questManager) => _questManager = questManager; + + public NpcRunType[] NpcRunTypes => new[] { NpcRunType.QUEST_ADDITIONAL_ACT5 }; + + public async Task Execute(IClientSession session, NpcDialogEvent e) + { + QuestNpcDto npcQuest = _questManager.GetNpcBlueAlertQuestByQuestId(e.Argument); + INpcEntity npcEntity = session.CurrentMapInstance.GetNpcById(e.NpcId); + + if (!session.PlayerEntity.HasQuestWithId(e.Argument)) + { + return; + } + + if (npcQuest == null || npcEntity == null) + { + return; + } + + if (npcQuest.NpcVnum != npcEntity.NpcVNum) + { + return; + } + + CharacterQuest characterQuest = session.PlayerEntity.GetQuestById(e.Argument); + if (!NpcTalkQuests.Contains(characterQuest.Quest.QuestType)) + { + return; + } + + await session.EmitEventAsync(new QuestNpcTalkEvent(characterQuest, npcEntity, true)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Quests/QuestAdditionalBlueHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Quests/QuestAdditionalBlueHandler.cs new file mode 100644 index 0000000..3daaa14 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Quests/QuestAdditionalBlueHandler.cs @@ -0,0 +1,75 @@ +using System.Threading.Tasks; +using WingsEmu.DTOs.Maps; +using WingsEmu.DTOs.Quests; +using WingsEmu.Game._i18n; +using WingsEmu.Game._NpcDialog; +using WingsEmu.Game._NpcDialog.Event; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Quests; +using WingsEmu.Game.Quests.Event; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.NpcDialogs.Quests; + +public class QuestAdditionalBlueHandler : INpcDialogAsyncHandler +{ + private readonly IGameLanguageService _langService; + private readonly IQuestManager _questManager; + + public QuestAdditionalBlueHandler(IQuestManager questManager, IGameLanguageService langService) + { + _questManager = questManager; + _langService = langService; + } + + public NpcRunType[] NpcRunTypes => new[] { NpcRunType.QUEST_RECEIVE_ADDITIONAL_BLUE }; + + public async Task Execute(IClientSession session, NpcDialogEvent e) + { + if (!session.HasCurrentMapInstance) + { + return; + } + + if (!session.CurrentMapInstance.HasMapFlag(MapFlags.IS_BASE_MAP)) + { + return; + } + + INpcEntity npcEntity = session.CurrentMapInstance.GetNpcById(e.NpcId); + if (npcEntity == null) + { + return; + } + + QuestDto quest = _questManager.GetQuestById(e.Argument); + if (quest == null) + { + return; + } + + if (!quest.IsBlue) + { + await session.NotifyStrangeBehavior(StrangeBehaviorSeverity.ABUSING, $"[N_RUN] Tried to add non-blue quest as a blue quest! QuestId: {e.Argument}"); + return; + } + + if (quest.RequiredQuestId != -1 && !session.PlayerEntity.HasCompletedQuest(quest.RequiredQuestId)) + { + await session.NotifyStrangeBehavior(StrangeBehaviorSeverity.ABUSING, $"[N_RUN] Tried to add blue quest that has a previous quest! QuestId: {e.Argument}"); + return; + } + + if (session.PlayerEntity.Level < quest.MinLevel) + { + session.SendMsg(_langService.GetLanguage(GameDialogKey.QUEST_SHOUTMESSAGE_LOW_LEVEL, session.UserLanguage), MsgMessageType.Middle); + return; + } + + await session.EmitEventAsync(new AddQuestEvent(e.Argument, QuestSlotType.SECONDARY)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Quests/QuestAdditionalHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Quests/QuestAdditionalHandler.cs new file mode 100644 index 0000000..3393928 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Quests/QuestAdditionalHandler.cs @@ -0,0 +1,72 @@ +using System.Threading.Tasks; +using WingsEmu.DTOs.Maps; +using WingsEmu.DTOs.Quests; +using WingsEmu.Game._i18n; +using WingsEmu.Game._NpcDialog; +using WingsEmu.Game._NpcDialog.Event; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Quests; +using WingsEmu.Game.Quests.Event; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.NpcDialogs.Quests; + +public class QuestAdditionalHandler : INpcDialogAsyncHandler +{ + private readonly GeneralQuestsConfiguration _generalQuestsConfiguration; + private readonly IGameLanguageService _langService; + private readonly IQuestManager _questManager; + + public QuestAdditionalHandler(IGameLanguageService langService, IQuestManager questManager, GeneralQuestsConfiguration generalQuestsConfiguration) + { + _langService = langService; + _questManager = questManager; + _generalQuestsConfiguration = generalQuestsConfiguration; + } + + public NpcRunType[] NpcRunTypes => new[] { NpcRunType.QUEST_ADDITIONAL }; + + public async Task Execute(IClientSession session, NpcDialogEvent e) + { + if (!session.HasCurrentMapInstance) + { + return; + } + + if (!session.CurrentMapInstance.HasMapFlag(MapFlags.IS_BASE_MAP)) + { + return; + } + + INpcEntity npcEntity = session.CurrentMapInstance.GetNpcById(e.NpcId); + if (npcEntity == null) + { + return; + } + + QuestDto quest = _questManager.GetQuestById(e.Argument); + if (quest == null) + { + return; + } + + if (!_generalQuestsConfiguration.GeneralQuests.Contains(quest.Id)) + { + await session.NotifyStrangeBehavior(StrangeBehaviorSeverity.ABUSING, $"[N_RUN] Tried to add a quest as a general quest, but it's not in the configuration! QuestId: {e.Argument}"); + return; + } + + if (session.PlayerEntity.Level < quest.MinLevel) + { + session.SendMsg(_langService.GetLanguage(GameDialogKey.QUEST_SHOUTMESSAGE_LOW_LEVEL, session.UserLanguage), MsgMessageType.Middle); + return; + } + + await session.EmitEventAsync(new AddQuestEvent(e.Argument, QuestSlotType.GENERAL)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Quests/QuestDailyHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Quests/QuestDailyHandler.cs new file mode 100644 index 0000000..c6af63d --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Quests/QuestDailyHandler.cs @@ -0,0 +1,79 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using WingsEmu.DTOs.Maps; +using WingsEmu.DTOs.Quests; +using WingsEmu.Game; +using WingsEmu.Game._i18n; +using WingsEmu.Game._NpcDialog; +using WingsEmu.Game._NpcDialog.Event; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Quests; +using WingsEmu.Game.Quests.Event; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.NpcDialogs.Quests; + +public class QuestDailyHandler : INpcDialogAsyncHandler +{ + private readonly IGameLanguageService _langService; + private readonly INpcRunTypeQuestsConfiguration _npcRunTypeQuestsConfiguration; + private readonly IQuestManager _questManager; + private readonly IRandomGenerator _randomGenerator; + + public QuestDailyHandler(INpcRunTypeQuestsConfiguration npcRunTypeQuestsConfiguration, IRandomGenerator randomGenerator, IQuestManager questManager, IGameLanguageService langService) + { + _npcRunTypeQuestsConfiguration = npcRunTypeQuestsConfiguration; + _randomGenerator = randomGenerator; + _questManager = questManager; + _langService = langService; + } + + public NpcRunType[] NpcRunTypes => new[] + { + NpcRunType.ICY_FLOWERS_MISSION, + NpcRunType.HEAT_POTION_MISSION, + NpcRunType.JOHN_MISSION, + NpcRunType.AKAMUR_MISSION, + NpcRunType.DAILY_MISSION_TAROT + }; + + public async Task Execute(IClientSession session, NpcDialogEvent e) + { + if (!session.HasCurrentMapInstance) + { + return; + } + + if (!session.CurrentMapInstance.HasMapFlag(MapFlags.IS_BASE_MAP)) + { + return; + } + + INpcEntity npcEntity = session.CurrentMapInstance.GetNpcById(e.NpcId); + if (npcEntity == null) + { + return; + } + + List possibleQuest = _npcRunTypeQuestsConfiguration.GetPossibleQuestsByNpcRunType(e.NpcRunType); + int rnd = _randomGenerator.RandomNumber(0, possibleQuest.Count); + + QuestDto randomQuest = _questManager.GetQuestById(possibleQuest[rnd]); + if (randomQuest == null) + { + return; + } + + if (session.PlayerEntity.Level < randomQuest.MinLevel) + { + session.SendMsg(_langService.GetLanguage(GameDialogKey.QUEST_SHOUTMESSAGE_LOW_LEVEL, session.UserLanguage), MsgMessageType.Middle); + return; + } + + await session.EmitEventAsync(new AddQuestEvent(randomQuest.Id, randomQuest.IsBlue ? QuestSlotType.SECONDARY : QuestSlotType.GENERAL)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Quests/QuestReceiveHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Quests/QuestReceiveHandler.cs new file mode 100644 index 0000000..a9b518d --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Quests/QuestReceiveHandler.cs @@ -0,0 +1,25 @@ +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.PacketGeneration; +using WingsEmu.Game._NpcDialog; +using WingsEmu.Game._NpcDialog.Event; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.NpcDialogs.Quests; + +public class QuestReceiveHandler : INpcDialogAsyncHandler +{ + public NpcRunType[] NpcRunTypes => new[] { NpcRunType.QUEST_RECEIVE }; + + public async Task Execute(IClientSession session, NpcDialogEvent e) + { + INpcEntity npcEntity = session.CurrentMapInstance.GetNpcById(e.NpcId); + if (npcEntity == null) + { + return; + } + + session.SendNpcQuestDialog(npcEntity); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Quests/QuestReceiveMainHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Quests/QuestReceiveMainHandler.cs new file mode 100644 index 0000000..52097f9 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Quests/QuestReceiveMainHandler.cs @@ -0,0 +1,82 @@ +using System.Linq; +using System.Threading.Tasks; +using Serilog; +using WingsAPI.Game.Extensions.Quests; +using WingsEmu.DTOs.Quests; +using WingsEmu.Game._i18n; +using WingsEmu.Game._NpcDialog; +using WingsEmu.Game._NpcDialog.Event; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Quests; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.NpcDialogs.Quests; + +public class QuestReceiveMainHandler : INpcDialogAsyncHandler +{ + private readonly IGameLanguageService _gameLanguage; + private readonly IQuestManager _questManager; + + public QuestReceiveMainHandler(IGameLanguageService gameLanguage, IQuestManager questManager) + { + _gameLanguage = gameLanguage; + _questManager = questManager; + } + + public NpcRunType[] NpcRunTypes => new[] { NpcRunType.QUEST_RECEIVE_MAIN }; + + public async Task Execute(IClientSession session, NpcDialogEvent e) + { + INpcEntity npcEntity = session.CurrentMapInstance.GetNpcById(e.NpcId); + if (npcEntity == null) + { + return; + } + + if (e.Confirmation == 1) + { + QuestNpcDto questNpc = _questManager.GetQuestNpcByScriptId(e.Argument); + if (questNpc == null) + { + Log.Debug($"A QuestNpc with StartingScriptId {e.Argument} was not found."); + return; + } + + if (session.PlayerEntity.GetCurrentQuests().Any(s => s.SlotType == QuestSlotType.MAIN)) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.QUEST_SHOUTMESSAGE_ALREADY_MAIN_QUEST, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (session.PlayerEntity.Level < questNpc.Level) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.QUEST_SHOUTMESSAGE_LOW_LEVEL, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (session.PlayerEntity.GetLastCompletedScript().ScriptId < questNpc.RequiredCompletedScript) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.QUEST_SHOUTMESSAGE_INCOMPLETE_QUESTS, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (session.PlayerEntity.GetLastCompletedScript().ScriptId > questNpc.RequiredCompletedScript) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.QUEST_SHOUTMESSAGE_ALREADY_COMPLETED_MAIN, session.UserLanguage), MsgMessageType.Middle); + return; + } + + + session.SendScriptPacket(e.Argument, 10); + } + + else + { + session.SendQnaPacket($"n_run {(short)NpcRunType.QUEST_RECEIVE_MAIN} {e.Argument} {(byte)e.VisualType} {e.NpcId} 1", + _gameLanguage.GetLanguage(GameDialogKey.QUEST_DIALOG_START_MAIN_QUEST, session.UserLanguage)); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/RecipeListHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/RecipeListHandler.cs new file mode 100644 index 0000000..ddf4f6b --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/RecipeListHandler.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using WingsEmu.Game; +using WingsEmu.Game._NpcDialog; +using WingsEmu.Game._NpcDialog.Event; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers.ServerData; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.NpcDialogs; + +public class RecipeListHandler : INpcDialogAsyncHandler +{ + private readonly IRecipeManager _recipeManager; + + public RecipeListHandler(IRecipeManager recipeManager) => _recipeManager = recipeManager; + + public NpcRunType[] NpcRunTypes => new[] { NpcRunType.OPEN_CRAFTING }; + + public async Task Execute(IClientSession session, NpcDialogEvent e) + { + INpcEntity npcEntity = session.CurrentMapInstance.GetNpcById(e.NpcId); + if (npcEntity == null) + { + return; + } + + IReadOnlyList recipes = _recipeManager.GetRecipesByNpcId(e.NpcId) ?? _recipeManager.GetRecipesByNpcMonsterVnum(npcEntity.NpcVNum); + if (recipes == null) + { + return; + } + + session.SendWopenPacket(WindowType.CRAFTING_RANDOM_ITEMS_RARITY); + session.SendRecipeNpcList(recipes); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/SP5_SP6/Act5ItemCraftingHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/SP5_SP6/Act5ItemCraftingHandler.cs new file mode 100644 index 0000000..4ff9184 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/SP5_SP6/Act5ItemCraftingHandler.cs @@ -0,0 +1,29 @@ +using System.Threading.Tasks; +using WingsEmu.Game._NpcDialog; +using WingsEmu.Game._NpcDialog.Event; +using WingsEmu.Game.Act5; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.NpcDialogs.SP5_SP6; + +public class Act5ItemCraftingHandler : INpcDialogAsyncHandler +{ + public NpcRunType[] NpcRunTypes => new[] + { + NpcRunType.GRENIGAS_SEAL_PRODUCTION, NpcRunType.ICE_FLOWERS_10_PRODUCTION, + NpcRunType.MAGIC_CAMEL_BOX_PRODUCTION, NpcRunType.MAGIC_CAMEL_PRODUCTION, + NpcRunType.DRACO_CLAW_SP5_PERF_STON_PRODUCTION, NpcRunType.DRACO_CLAW_SP5_PRODUCTION, + NpcRunType.DRACO_SEAL_PRODUCTION, NpcRunType.GLACERUS_MANE_SP6_PERF_STONE_PRODUCTION, + NpcRunType.GLACERUS_MANE_SP6_PRODUCTION, NpcRunType.GLACERUS_SEAL_PRODUCTION + }; + + public async Task Execute(IClientSession session, NpcDialogEvent e) + { + await session.EmitEventAsync(new Act5OpenNpcRunEvent + { + NpcRunType = e.NpcRunType, + IsConfirm = e.Confirmation.HasValue + }); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/SP5_SP6/WatterGrotoHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/SP5_SP6/WatterGrotoHandler.cs new file mode 100644 index 0000000..f4a7bb6 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/SP5_SP6/WatterGrotoHandler.cs @@ -0,0 +1,33 @@ +using System.Threading.Tasks; +using WingsEmu.Game._NpcDialog; +using WingsEmu.Game._NpcDialog.Event; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.NpcDialogs.SP5_SP6; + +public class WatterGrotoHandler : INpcDialogAsyncHandler +{ + public NpcRunType[] NpcRunTypes => new[] { NpcRunType.ENTER_TO_WATTER_GROTO }; + + public async Task Execute(IClientSession session, NpcDialogEvent e) + { + /*INpcEntity npcEntity = session.CurrentMapInstance.GetNpcById(e.NpcId); + if (npcEntity == null) + { + return; + } + + if (session.CantPerformActionOnAct4()) + { + return; + } + + if (!session.CurrentMapInstance.HasMapFlag(MapFlags.IS_BASE_MAP)) + { + return; + } + + session.ChangeMap(2587, 35, 14);*/ + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/SP8/Sp8PowderProductionHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/SP8/Sp8PowderProductionHandler.cs new file mode 100644 index 0000000..8cbf1b1 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/SP8/Sp8PowderProductionHandler.cs @@ -0,0 +1,53 @@ +using System.Threading.Tasks; +using WingsEmu.Game._i18n; +using WingsEmu.Game._NpcDialog; +using WingsEmu.Game._NpcDialog.Event; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.NpcDialogs.SP8; + +public class Sp8PowderProductionHandler : INpcDialogAsyncHandler +{ + private readonly IGameItemInstanceFactory _gameItemInstanceFactory; + private readonly IGameLanguageService _gameLanguage; + private readonly IItemsManager _itemsManager; + + public Sp8PowderProductionHandler(IGameLanguageService gameLanguage, IItemsManager itemsManager, IGameItemInstanceFactory gameItemInstanceFactory) + { + _gameLanguage = gameLanguage; + _itemsManager = itemsManager; + _gameItemInstanceFactory = gameItemInstanceFactory; + } + + public NpcRunType[] NpcRunTypes => new[] { NpcRunType.SP8_PRODUCTION_SOUL_SLIVER_3_POWDER }; + + public async Task Execute(IClientSession session, NpcDialogEvent e) + { + /*INpcEntity npcEntity = session.CurrentMapInstance.GetNpcById(e.NpcId); + string sliver = _gameLanguage.GetLanguage(GameDataType.Item, _itemsManager.GetItem((short)ItemVnums.SOUL_SLIVER).Name, session.UserLanguage); + + if (npcEntity == null) + { + return; + } + + if (session.CurrentMapInstance.MapInstanceType != MapInstanceType.BaseMapInstance) + { + return; + } + + if (!session.PlayerEntity.HasItem((short)ItemVnums.SOUL_SLIVER, 3)) + { + int amount = session.CountMissingItems((short)ItemVnums.SOUL_SLIVER, 3); + session.SendChatMessage( _gameLanguage.GetLanguageFormat(GameDialogKey.INVENTORY_SHOUTMESSAGE_NOT_ENOUGH_ITEMS, session.UserLanguage, amount, sliver), ChatMessageColorType.Yellow); + return; + } + + GameItemInstance powder = _gameItemInstanceFactory.CreateItem((short)ItemVnums.CLEANSING_POWDER); + await session.AddNewItemToInventory(powder, true, ChatMessageColorType.Yellow, true); + await session.RemoveItemFromInventory((short)ItemVnums.SOUL_SLIVER, 3);*/ + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/SP8/Sp8SealProductionHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/SP8/Sp8SealProductionHandler.cs new file mode 100644 index 0000000..73f7ef1 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/SP8/Sp8SealProductionHandler.cs @@ -0,0 +1,54 @@ +using System.Threading.Tasks; +using WingsEmu.Game._i18n; +using WingsEmu.Game._NpcDialog; +using WingsEmu.Game._NpcDialog.Event; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.NpcDialogs.SP8; + +public class Sp8SealProductionHandler : INpcDialogAsyncHandler +{ + private readonly IGameItemInstanceFactory _gameItemInstanceFactory; + private readonly IGameLanguageService _gameLanguage; + private readonly IItemsManager _itemsManager; + + public Sp8SealProductionHandler(IGameLanguageService gameLanguage, IItemsManager itemsManager, IGameItemInstanceFactory gameItemInstanceFactory) + { + _gameLanguage = gameLanguage; + _itemsManager = itemsManager; + _gameItemInstanceFactory = gameItemInstanceFactory; + } + + public NpcRunType[] NpcRunTypes => new[] { NpcRunType.SP8_PRODUCTION_5_SEED_DAMNATION_SEAL }; + + public async Task Execute(IClientSession session, NpcDialogEvent e) + { + /*INpcEntity npcEntity = session.CurrentMapInstance.GetNpcById(e.NpcId); + string seed = _gameLanguage.GetLanguage(GameDataType.Item, _itemsManager.GetItem((short)ItemVnums.SEED_OF_DAMNATION).Name, session.UserLanguage); + string sealName = _gameLanguage.GetLanguage(GameDataType.Item, _itemsManager.GetItem((short)ItemVnums.LAURENA_SEAL).Name, session.UserLanguage); + + if (npcEntity == null) + { + return; + } + + if (session.CurrentMapInstance.MapInstanceType != MapInstanceType.BaseMapInstance) + { + return; + } + + if (!session.PlayerEntity.HasItem((short)ItemVnums.SEED_OF_DAMNATION, 5)) + { + int amount = session.CountMissingItems((short)ItemVnums.SEED_OF_DAMNATION, 5); + session.SendChatMessage( _gameLanguage.GetLanguageFormat(GameDialogKey.INVENTORY_SHOUTMESSAGE_NOT_ENOUGH_ITEMS, session.UserLanguage, amount, seed), ChatMessageColorType.Yellow); + return; + } + + GameItemInstance seal = _gameItemInstanceFactory.CreateItem((short)ItemVnums.LAURENA_SEAL, 2); + await session.AddNewItemToInventory(seal, true, ChatMessageColorType.Yellow, true); + await session.RemoveItemFromInventory((short)ItemVnums.SEED_OF_DAMNATION, 5);*/ + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/ShowPlayerShopHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/ShowPlayerShopHandler.cs new file mode 100644 index 0000000..dbee8a8 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/ShowPlayerShopHandler.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.ItemExtension; +using WingsEmu.Game._NpcDialog; +using WingsEmu.Game._NpcDialog.Event; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Shops; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.NpcDialogs; + +public class ShowPlayerShopHandler : INpcDialogAsyncHandler +{ + public NpcRunType[] NpcRunTypes => new[] { NpcRunType.SHOW_PLAYER_SHOP }; + + public async Task Execute(IClientSession session, NpcDialogEvent e) + { + IPlayerEntity owner = session.CurrentMapInstance.GetCharacterById(e.NpcId); + IEnumerable items = owner?.ShopComponent.Items; + if (items == null) + { + return; + } + + session.SendShopContent(e.NpcId, items); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Teleport/TeleportGrenigasSquareHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Teleport/TeleportGrenigasSquareHandler.cs new file mode 100644 index 0000000..535468d --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Teleport/TeleportGrenigasSquareHandler.cs @@ -0,0 +1,29 @@ +using System.Threading.Tasks; +using WingsEmu.Game._NpcDialog; +using WingsEmu.Game._NpcDialog.Event; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.NpcDialogs.Teleport; + +public class TeleportGrenigasSquareHandler : INpcDialogAsyncHandler +{ + public NpcRunType[] NpcRunTypes => new[] { NpcRunType.TELEPORT_GRENIGAS_SQUARE }; + + public async Task Execute(IClientSession session, NpcDialogEvent e) + { + /*INpcEntity npcEntity = session.CurrentMapInstance.GetNpcById(e.NpcId); + if (npcEntity == null || !session.PlayerEntity.HasItem((short)ItemVnums.RUNE_PIECE)) + { + return; + } + + if (session.CurrentMapInstance.MapInstanceType != MapInstanceType.BaseMapInstance) + { + return; + } + + session.ChangeMap(2536, 26, 31); + await session.EmitEventAsync(new InventoryRemoveItemEvent((short)ItemVnums.RUNE_PIECE));*/ + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Teleport/TeleportHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Teleport/TeleportHandler.cs new file mode 100644 index 0000000..d388376 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Teleport/TeleportHandler.cs @@ -0,0 +1,105 @@ +using System.Threading.Tasks; +using WingsAPI.Data.Families; +using WingsEmu.Core.Extensions; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game._NpcDialog; +using WingsEmu.Game._NpcDialog.Event; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.NpcDialogs.Teleport; + +public class TeleportHandler : INpcDialogAsyncHandler +{ + private readonly IGameLanguageService _langService; + private readonly IMapManager _mapManager; + private readonly IRandomGenerator _randomGenerator; + private readonly IRespawnDefaultConfiguration _respawnDefaultConfiguration; + + public TeleportHandler(IGameLanguageService langService, IMapManager mapManager, IRespawnDefaultConfiguration respawnDefaultConfiguration, IRandomGenerator randomGenerator) + { + _langService = langService; + _mapManager = mapManager; + _respawnDefaultConfiguration = respawnDefaultConfiguration; + _randomGenerator = randomGenerator; + } + + public NpcRunType[] NpcRunTypes => new[] { NpcRunType.WARP_TELEPORT }; + + public async Task Execute(IClientSession session, NpcDialogEvent e) + { + INpcEntity npcEntity = session.CurrentMapInstance.GetNpcById(e.NpcId); + if (npcEntity == null) + { + return; + } + + if (!session.CurrentMapInstance.HasMapFlag(MapFlags.IS_BASE_MAP)) + { + return; + } + + if (session.CantPerformActionOnAct4()) + { + return; + } + + if (e.Argument > 3 || e.Argument < 0) + { + await session.NotifyStrangeBehavior(StrangeBehaviorSeverity.ABUSING, "WarpTeleport e.Argument > 3 || e.Argument < 0"); + return; + } + + int baseToRemove = 500 * e.Argument * 2; + short toRemove = session.PlayerEntity.Family?.UpgradeValues.GetOrDefault(FamilyUpgradeType.DECREASE_SHIP_TP_COST) ?? 0; + int amountToRemove = (int)(baseToRemove * (toRemove * 0.01)); + baseToRemove -= amountToRemove; + + if (session.PlayerEntity.Gold < baseToRemove) + { + session.SendChatMessage(_langService.GetLanguage(GameDialogKey.INTERACTION_MESSAGE_NOT_ENOUGH_GOLD, session.UserLanguage), ChatMessageColorType.Yellow); + return; + } + + RespawnDefault getRespawn = e.Argument switch + { + 0 => _respawnDefaultConfiguration.GetReturn(RespawnType.NOSVILLE_SPAWN), + 1 => _respawnDefaultConfiguration.GetReturn(RespawnType.KREM_SPAWN), + 2 => _respawnDefaultConfiguration.GetReturn(RespawnType.ALVEUS_SPAWN), + _ => null + }; + + if (getRespawn == null) + { + return; + } + + IMapInstance mapInstance = _mapManager.GetBaseMapInstanceByMapId(getRespawn.MapId); + if (mapInstance == null) + { + return; + } + + int randomX = getRespawn.MapX + _randomGenerator.RandomNumber(getRespawn.Radius, -getRespawn.Radius); + int randomY = getRespawn.MapY + _randomGenerator.RandomNumber(getRespawn.Radius, -getRespawn.Radius); + + if (mapInstance.IsBlockedZone(randomX, randomY)) + { + randomX = getRespawn.MapX; + randomY = getRespawn.MapY; + } + + session.ChangeMap(getRespawn.MapId, (short)randomX, (short)randomY); + session.PlayerEntity.Gold -= baseToRemove; + session.RefreshGold(); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Teleport/TeleportSpRockHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Teleport/TeleportSpRockHandler.cs new file mode 100644 index 0000000..b1778f6 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Teleport/TeleportSpRockHandler.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Serilog; +using WingsEmu.Game._enum; +using WingsEmu.Game._NpcDialog; +using WingsEmu.Game._NpcDialog.Event; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.NpcDialogs.Teleport; + +public class TeleportSpRockHandler : INpcDialogAsyncHandler +{ + private readonly IMapManager _mapManager; + + private readonly HashSet _spMapIds = new() + { + (int)MapIds.SP_STONE_PAJAMA, + (int)MapIds.SP_STONE_1, + (int)MapIds.SP_STONE_2, + (int)MapIds.SP_STONE_3, + (int)MapIds.SP_STONE_4 + }; + + public TeleportSpRockHandler(IMapManager mapManager) => _mapManager = mapManager; + + public NpcRunType[] NpcRunTypes => new[] { NpcRunType.QUEST_TELEPORT_TO_SP_MAP }; + + public async Task Execute(IClientSession session, NpcDialogEvent e) + { + IMapInstance mapInstance = _mapManager.GetBaseMapInstanceByMapId(e.Argument); + if (mapInstance == null) + { + Log.Debug($"There was not a map found for VNum {e.Argument.ToString()}"); + return; + } + + if (session.CantPerformActionOnAct4()) + { + return; + } + + if (!_spMapIds.Contains(mapInstance.MapId)) + { + await session.NotifyStrangeBehavior(StrangeBehaviorSeverity.ABUSING, "Tried to teleport on different map than SP Stone"); + return; + } + + await _mapManager.TeleportOnRandomPlaceInMapAsync(session, mapInstance); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Teleport/WarpTeleportAct5Handler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Teleport/WarpTeleportAct5Handler.cs new file mode 100644 index 0000000..c9277ad --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Teleport/WarpTeleportAct5Handler.cs @@ -0,0 +1,110 @@ +using System.Threading.Tasks; +using WingsAPI.Data.Families; +using WingsAPI.Packets.Enums; +using WingsEmu.Core.Extensions; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game; +using WingsEmu.Game._i18n; +using WingsEmu.Game._NpcDialog; +using WingsEmu.Game._NpcDialog.Event; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.NpcDialogs.Teleport; + +public class WarpTeleportAct5Handler : INpcDialogAsyncHandler +{ + private readonly IGameLanguageService _langService; + private readonly IMapManager _mapManager; + private readonly IRandomGenerator _randomGenerator; + private readonly IRespawnDefaultConfiguration _respawnDefaultConfiguration; + + public WarpTeleportAct5Handler(IGameLanguageService langService, IMapManager mapManager, IRespawnDefaultConfiguration respawnDefaultConfiguration, IRandomGenerator randomGenerator) + { + _langService = langService; + _mapManager = mapManager; + _respawnDefaultConfiguration = respawnDefaultConfiguration; + _randomGenerator = randomGenerator; + } + + public NpcRunType[] NpcRunTypes => new[] { NpcRunType.WARP_TELEPORT_ACT5 }; + + public async Task Execute(IClientSession session, NpcDialogEvent e) + { + INpcEntity npcEntity = session.CurrentMapInstance.GetNpcById(e.NpcId); + if (npcEntity == null) + { + return; + } + + if (session.CantPerformActionOnAct4()) + { + return; + } + + if (!session.CurrentMapInstance.HasMapFlag(MapFlags.IS_BASE_MAP)) + { + return; + } + + if (!session.CurrentMapInstance.HasMapFlag(MapFlags.ACT_5_1) && !session.CurrentMapInstance.HasMapFlag(MapFlags.ACT_5_2)) + { + return; + } + + int baseToRemove = 5000 * e.Argument; + short toRemove = session.PlayerEntity.Family?.UpgradeValues.GetOrDefault(FamilyUpgradeType.DECREASE_SHIP_TP_COST) ?? 0; + int amountToRemove = (int)(baseToRemove * (toRemove * 0.01)); + baseToRemove -= amountToRemove; + + if (session.PlayerEntity.Gold < baseToRemove) + { + session.SendChatMessage(_langService.GetLanguage(GameDialogKey.INTERACTION_MESSAGE_NOT_ENOUGH_GOLD, session.UserLanguage), ChatMessageColorType.Yellow); + return; + } + + if (e.Argument is > 3 or < 0) + { + await session.NotifyStrangeBehavior(StrangeBehaviorSeverity.ABUSING, "WarpTeleportAct5 e.Argument > 3 || e.Argument < 0"); + return; + } + + RespawnDefault getRespawn = e.Argument switch + { + 0 => _respawnDefaultConfiguration.GetReturnAct5(Act5RespawnType.MORTAZ_DESERT_PORT), + 1 => _respawnDefaultConfiguration.GetReturnAct5(Act5RespawnType.AKAMUR_CAMP), + 2 => _respawnDefaultConfiguration.GetReturnAct5(Act5RespawnType.DESERT_EAGLE_CITY), + _ => null + }; + + if (getRespawn == null) + { + return; + } + + IMapInstance mapInstance = _mapManager.GetBaseMapInstanceByMapId(getRespawn.MapId); + if (mapInstance == null) + { + return; + } + + int randomX = getRespawn.MapX + _randomGenerator.RandomNumber(getRespawn.Radius, -getRespawn.Radius); + int randomY = getRespawn.MapY + _randomGenerator.RandomNumber(getRespawn.Radius, -getRespawn.Radius); + + if (mapInstance.IsBlockedZone(randomX, randomY)) + { + randomX = getRespawn.MapX; + randomY = getRespawn.MapY; + } + + session.ChangeMap(getRespawn.MapId, (short)randomX, (short)randomY); + session.PlayerEntity.Gold -= baseToRemove; + session.RefreshGold(); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Teleport/WeedingGazeboHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Teleport/WeedingGazeboHandler.cs new file mode 100644 index 0000000..bfacc25 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/Teleport/WeedingGazeboHandler.cs @@ -0,0 +1,63 @@ +using System.Threading.Tasks; +using WingsAPI.Data.Families; +using WingsEmu.Core.Extensions; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game._i18n; +using WingsEmu.Game._NpcDialog; +using WingsEmu.Game._NpcDialog.Event; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.NpcDialogs.Teleport; + +public class WeedingGazeboHandler : INpcDialogAsyncHandler +{ + private readonly IGameLanguageService _langService; + private readonly IMapManager _mapManager; + + public WeedingGazeboHandler(IGameLanguageService langService, IMapManager mapManager) + { + _mapManager = mapManager; + _langService = langService; + } + + public NpcRunType[] NpcRunTypes => new[] { NpcRunType.WEDDING_GAZEBO }; + + public async Task Execute(IClientSession session, NpcDialogEvent e) + { + INpcEntity npcEntity = session.CurrentMapInstance.GetNpcById(e.NpcId); + if (npcEntity == null) + { + return; + } + + if (session.CantPerformActionOnAct4()) + { + return; + } + + if (!session.CurrentMapInstance.HasMapFlag(MapFlags.IS_BASE_MAP)) + { + return; + } + + int baseToRemove = 5000 * e.Argument * 2; + short toRemove = session.PlayerEntity.Family?.UpgradeValues.GetOrDefault(FamilyUpgradeType.DECREASE_SHIP_TP_COST) ?? 0; + int amountToRemove = (int)(baseToRemove * (toRemove * 0.01)); + baseToRemove -= amountToRemove; + + if (session.PlayerEntity.Gold < baseToRemove) + { + session.SendChatMessage(_langService.GetLanguage(GameDialogKey.INTERACTION_MESSAGE_NOT_ENOUGH_GOLD, session.UserLanguage), ChatMessageColorType.Yellow); + return; + } + + session.PlayerEntity.Gold -= baseToRemove; + session.RefreshGold(); + session.ChangeMap(2586, 35, 52); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/TimeSpace/GetPartnerHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/TimeSpace/GetPartnerHandler.cs new file mode 100644 index 0000000..7bc24e6 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/TimeSpace/GetPartnerHandler.cs @@ -0,0 +1,279 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game._NpcDialog; +using WingsEmu.Game._NpcDialog.Event; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Entities.Extensions; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Extensions.Mates; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Mates.Events; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Npcs; +using WingsEmu.Game.TimeSpaces; +using WingsEmu.Game.TimeSpaces.Events; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Battle; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.NpcDialogs.TimeSpace; + +public class GetPartnerHandler : INpcDialogAsyncHandler +{ + private readonly IGameItemInstanceFactory _gameItemInstanceFactory; + private readonly IGameLanguageService _gameLanguage; + private readonly IMateEntityFactory _mateEntityFactory; + private readonly INpcMonsterManager _npcMonsterManager; + + public GetPartnerHandler(INpcMonsterManager npcMonsterManager, IMateEntityFactory mateEntityFactory, IGameItemInstanceFactory gameItemInstanceFactory, IGameLanguageService gameLanguage) + { + _npcMonsterManager = npcMonsterManager; + _mateEntityFactory = mateEntityFactory; + _gameItemInstanceFactory = gameItemInstanceFactory; + _gameLanguage = gameLanguage; + } + + public NpcRunType[] NpcRunTypes => new[] { NpcRunType.GET_PARTNER }; + + public async Task Execute(IClientSession session, NpcDialogEvent e) + { + if (!session.HasCurrentMapInstance) + { + return; + } + + if (!session.PlayerEntity.TimeSpaceComponent.IsInTimeSpaceParty) + { + return; + } + + TimeSpaceParty timeSpace = session.PlayerEntity.TimeSpaceComponent.TimeSpace; + List partners = session.PlayerEntity.TimeSpaceComponent.Partners; + + if (timeSpace.Instance.StartTimeFreeze.HasValue) + { + await session.EmitEventAsync(new TimeSpaceAddTimeToTimerEvent + { + Time = DateTime.UtcNow - timeSpace.Instance.StartTimeFreeze.Value + }); + + timeSpace.Instance.StartTimeFreeze = null; + } + + if (!timeSpace.Instance.TimeSpaceSubInstances.TryGetValue(session.CurrentMapInstance.Id, out TimeSpaceSubInstance timeSpaceSubInstance)) + { + return; + } + + await session.EmitEventAsync(new TimeSpaceStartTaskEvent + { + TimeSpaceSubInstance = timeSpaceSubInstance + }); + + if (timeSpaceSubInstance.Task != null && timeSpaceSubInstance.Task.StartDialog.HasValue) + { + timeSpaceSubInstance.Task.StartDialog = null; + if (timeSpaceSubInstance.Task.StartDialogIsObjective) + { + timeSpaceSubInstance.Task.StartDialogIsObjective = false; + timeSpace.Instance.TimeSpaceObjective.ConversationsHad++; + await session.EmitEventAsync(new TimeSpaceRefreshObjectiveProgressEvent()); + } + } + + if (timeSpace.Instance.ObtainablePartnerVnum == null) + { + return; + } + + INpcEntity timeSpacePartner = partners.FirstOrDefault(x => x.MonsterVNum == timeSpace.Instance.ObtainablePartnerVnum.Value); + if (!partners.Any() || timeSpacePartner == null) + { + return; + } + + IMonsterData partner = _npcMonsterManager.GetNpc(timeSpace.Instance.ObtainablePartnerVnum.Value); + if (partner == null) + { + return; + } + + if (session.PlayerEntity.MateComponent.GetMates(x => x.MonsterVNum == partner.MonsterVNum && x.MateType == MateType.Partner).Any()) + { + session.SendChatMessage(session.GetLanguage(GameDialogKey.PARTNER_SHOUTMESSAGE_ALREADY_HAVE_SAME_PARTNER), ChatMessageColorType.Yellow); + return; + } + + if (!session.PlayerEntity.CanReceiveMate(MateType.Partner)) + { + session.SendChatMessage(session.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_MAX_PARTNER_COUNT), ChatMessageColorType.Yellow); + return; + } + + session.PlayerEntity.BroadcastEffectInRange(EffectType.NormalLevelUpSubEffect); + timeSpacePartner.BroadcastEffectInRange(EffectType.NormalLevelUpSubEffect); + timeSpacePartner.MapInstance.Broadcast(timeSpacePartner.GenerateOut()); + timeSpacePartner.MapInstance.RemoveNpc(timeSpacePartner); + + session.PlayerEntity.TimeSpaceComponent.Partners.Remove(timeSpacePartner); + + byte partnerLevel = session.PlayerEntity.Level < partner.BaseLevel ? session.PlayerEntity.Level : partner.BaseLevel; + + IMateEntity mateEntity = _mateEntityFactory.CreateMateEntity(session.PlayerEntity, new MonsterData(partner), MateType.Partner, partnerLevel); + await session.EmitEventAsync(new MateInitializeEvent + { + MateEntity = mateEntity + }); + + CreatePartnerEquipment(session, mateEntity); + + if (session.PlayerEntity.MateComponent.GetTeamMember(x => x.MateType == MateType.Partner) != null) + { + return; + } + + await session.EmitEventAsync(new MateJoinTeamEvent + { + MateEntity = mateEntity, + IsNewCreated = true + }); + } + + private void CreatePartnerEquipment(IClientSession session, IMateEntity mateEntity) + { + if (mateEntity.Level < 15) + { + return; + } + + int weapon = 0; + int armor = 0; + + switch (mateEntity.AttackType) + { + case AttackType.Melee: + + switch (mateEntity.Level) + { + case >= 20 and <= 30: + weapon = 19; // Short Sword + armor = 162; // Durable Armour + break; + case > 30 and <= 48: + weapon = 21; // Short Sword + armor = 97; // Leather Armour + break; + default: + weapon = 139; // Katzbalger + armor = 102; // Armour of Morale + break; + } + + break; + case AttackType.Ranged: + + switch (mateEntity.Level) + { + case >= 20 and <= 30: + weapon = 33; // Small Bow + armor = 108; // Lambskin Tunic + break; + case > 30 and <= 49: + weapon = 143; // Flame Bow + armor = 169; // Tunic of Nature + break; + default: + weapon = 146; // Wild Bow + armor = 116; // Peccary Tunic + break; + } + + break; + case AttackType.Magical: + + switch (mateEntity.Level) + { + case >= 20 and <= 30: + weapon = 47; // Red Bead Wand + armor = 121; // Cotton Robe + break; + case > 30 and <= 49: + weapon = 150; // Magic Wand of Flame + armor = 175; // Soft Robe + break; + default: + weapon = 153; // Magic Spell Wand + armor = 129; // Silk Robe + break; + } + + break; + } + + GameItemInstance weaponItem = _gameItemInstanceFactory.CreateItem(weapon, 1, 5, 5); + + switch (weaponItem.GameItem.EquipmentSlot) + { + case EquipmentType.SecondaryWeapon: + case EquipmentType.MainWeapon: + switch (weaponItem.GameItem.Class) + { + case (int)ItemClassType.Swordsman: + weaponItem.ItemVNum = weaponItem.GameItem.EquipmentSlot == (int)EquipmentType.MainWeapon ? (int)ItemVnums.PARTNER_WEAPON_MELEE : (int)ItemVnums.PARTNER_WEAPON_RANGED; + break; + case (int)ItemClassType.Archer: + weaponItem.ItemVNum = weaponItem.GameItem.EquipmentSlot == (int)EquipmentType.MainWeapon ? (int)ItemVnums.PARTNER_WEAPON_RANGED : (int)ItemVnums.PARTNER_WEAPON_MELEE; + break; + case (int)ItemClassType.Mage: + weaponItem.ItemVNum = weaponItem.GameItem.EquipmentSlot == (int)EquipmentType.MainWeapon ? (int)ItemVnums.PARTNER_WEAPON_MAGIC : (int)ItemVnums.PARTNER_WEAPON_RANGED; + break; + case (int)ItemClassType.MartialArtist: + weaponItem.ItemVNum = (int)ItemVnums.PARTNER_WEAPON_MELEE; + break; + default: + session.SendShopEndPacket(ShopEndType.Npc); + return; + } + + break; + } + + weaponItem.EquipmentOptions?.Clear(); + weaponItem.OriginalItemVnum = weapon; + weaponItem.BoundCharacterId = null; + + GameItemInstance armorItem = _gameItemInstanceFactory.CreateItem(armor, 1, 5, 5); + switch (armorItem.GameItem.Class) + { + case (int)ItemClassType.Swordsman: + case (int)ItemClassType.MartialArtist: + armorItem.ItemVNum = (int)ItemVnums.PARTNER_ARMOR_MELEE; + break; + case (int)ItemClassType.Archer: + armorItem.ItemVNum = (int)ItemVnums.PARTNER_ARMOR_RANGED; + break; + case (int)ItemClassType.Mage: + armorItem.ItemVNum = (int)ItemVnums.PARTNER_ARMOR_MAGIC; + break; + } + + armorItem.EquipmentOptions?.Clear(); + armorItem.OriginalItemVnum = armor; + armorItem.BoundCharacterId = null; + + session.PlayerEntity.PartnerEquipItem(weaponItem, mateEntity.PetSlot); + session.PlayerEntity.PartnerEquipItem(armorItem, mateEntity.PetSlot); + session.SendScpPackets(); + session.SendScnPackets(); + mateEntity.RefreshEquipmentValues(weaponItem, false); + mateEntity.RefreshEquipmentValues(armorItem, false); + session.SendPetInfo(mateEntity, _gameLanguage); + session.SendCondMate(mateEntity); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/TimeSpace/TimeSpaceDialogHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/TimeSpace/TimeSpaceDialogHandler.cs new file mode 100644 index 0000000..adeb24d --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/TimeSpace/TimeSpaceDialogHandler.cs @@ -0,0 +1,117 @@ +using System; +using System.Threading.Tasks; +using WingsEmu.Game._NpcDialog; +using WingsEmu.Game._NpcDialog.Event; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Networking; +using WingsEmu.Game.TimeSpaces; +using WingsEmu.Game.TimeSpaces.Events; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.NpcDialogs.TimeSpace; + +public class TimeSpaceDialogHandler : INpcDialogAsyncHandler +{ + public NpcRunType[] NpcRunTypes => new[] { NpcRunType.TIME_SPACE_END_DIALOG }; + + public async Task Execute(IClientSession session, NpcDialogEvent e) + { + if (!session.HasCurrentMapInstance) + { + return; + } + + if (!session.PlayerEntity.TimeSpaceComponent.IsInTimeSpaceParty) + { + return; + } + + if (session.CurrentMapInstance.MapInstanceType != MapInstanceType.TimeSpaceInstance) + { + return; + } + + TimeSpaceParty timeSpace = session.PlayerEntity.TimeSpaceComponent.TimeSpace; + if (timeSpace?.Instance == null) + { + return; + } + + if (!timeSpace.Instance.TimeSpaceSubInstances.TryGetValue(session.CurrentMapInstance.Id, out TimeSpaceSubInstance instance)) + { + return; + } + + if (instance.Task == null) + { + return; + } + + if (instance.Task.StartDialog.HasValue) + { + if (!instance.Task.DialogStartTask) + { + await TryAddFrozenTime(session, timeSpace); + return; + } + + instance.Task.StartDialog = null; + await TryAddFrozenTime(session, timeSpace); + + if (instance.Task.StartDialogIsObjective) + { + instance.Task.StartDialogIsObjective = false; + timeSpace.Instance.TimeSpaceObjective.ConversationsHad++; + await session.EmitEventAsync(new TimeSpaceRefreshObjectiveProgressEvent()); + } + + if (instance.Task.IsFinished) + { + return; + } + + await session.EmitEventAsync(new TimeSpaceStartTaskEvent + { + TimeSpaceSubInstance = instance + }); + return; + } + + if (!instance.Task.EndDialog.HasValue) + { + return; + } + + instance.Task.EndDialog = null; + await TryAddFrozenTime(session, timeSpace); + + if (!instance.Task.EndDialogIsObjective) + { + return; + } + + if (instance.Task.IsFinished) + { + return; + } + + instance.Task.EndDialogIsObjective = false; + timeSpace.Instance.TimeSpaceObjective.ConversationsHad++; + await session.EmitEventAsync(new TimeSpaceRefreshObjectiveProgressEvent()); + } + + private async Task TryAddFrozenTime(IClientSession session, TimeSpaceParty timeSpaceParty) + { + if (!timeSpaceParty.Instance.StartTimeFreeze.HasValue) + { + return; + } + + await session.EmitEventAsync(new TimeSpaceAddTimeToTimerEvent + { + Time = DateTime.UtcNow - timeSpaceParty.Instance.StartTimeFreeze.Value + }); + + timeSpaceParty.Instance.StartTimeFreeze = null; + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/TimeSpace/TimeSpaceEnterHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/TimeSpace/TimeSpaceEnterHandler.cs new file mode 100644 index 0000000..11f49ad --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/TimeSpace/TimeSpaceEnterHandler.cs @@ -0,0 +1,59 @@ +using System.Threading.Tasks; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game._i18n; +using WingsEmu.Game._NpcDialog; +using WingsEmu.Game._NpcDialog.Event; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Game.TimeSpaces.Events; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.NpcDialogs.TimeSpace; + +public class TimeSpaceEnterHandler : INpcDialogAsyncHandler +{ + private readonly ITimeSpaceNpcRunConfig _timeSpaceNpcRunConfig; + + public TimeSpaceEnterHandler(ITimeSpaceNpcRunConfig timeSpaceNpcRunConfig) => _timeSpaceNpcRunConfig = timeSpaceNpcRunConfig; + + public NpcRunType[] NpcRunTypes => new[] { NpcRunType.ENTER_TO_TS_ID }; + + public async Task Execute(IClientSession session, NpcDialogEvent e) + { + if (!_timeSpaceNpcRunConfig.DoesTimeSpaceExist(e.Argument)) + { + return; + } + + if (session.PlayerEntity.IsInGroup()) + { + session.SendMsg(session.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_NEED_LEAVE_GROUP), MsgMessageType.Middle); + return; + } + + if (session.CurrentMapInstance.HasMapFlag(MapFlags.ACT_4) || !session.CurrentMapInstance.HasMapFlag(MapFlags.IS_BASE_MAP)) + { + session.SendMsg(session.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_MUST_BE_IN_CLASSIC_MAP), MsgMessageType.Middle); + return; + } + + int? quest = _timeSpaceNpcRunConfig.GetQuestByTimeSpaceId(e.Argument); + if (quest.HasValue) + { + if (!session.PlayerEntity.HasQuestWithId(quest.Value)) + { + return; + } + } + + await session.EmitEventAsync(new TimeSpacePartyCreateEvent(e.Argument)); + if (!session.PlayerEntity.TimeSpaceComponent.IsInTimeSpaceParty) + { + return; + } + + await session.EmitEventAsync(new TimeSpaceInstanceStartEvent()); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/TimeSpace/TimeSpaceOnFinishDialogHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/TimeSpace/TimeSpaceOnFinishDialogHandler.cs new file mode 100644 index 0000000..436bac7 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/TimeSpace/TimeSpaceOnFinishDialogHandler.cs @@ -0,0 +1,62 @@ +using System.Linq; +using System.Threading.Tasks; +using WingsEmu.Game; +using WingsEmu.Game._NpcDialog; +using WingsEmu.Game._NpcDialog.Event; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Networking; +using WingsEmu.Game.TimeSpaces; +using WingsEmu.Game.TimeSpaces.Events; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.NpcDialogs.TimeSpace; + +public class TimeSpaceOnFinishDialogHandler : INpcDialogAsyncHandler +{ + public NpcRunType[] NpcRunTypes => new[] { NpcRunType.TIMESPACE_ON_FINISH_DIALOG }; + + public async Task Execute(IClientSession session, NpcDialogEvent e) + { + if (!session.HasCurrentMapInstance) + { + return; + } + + if (!session.PlayerEntity.TimeSpaceComponent.IsInTimeSpaceParty) + { + return; + } + + if (session.CurrentMapInstance.MapInstanceType != MapInstanceType.TimeSpaceInstance) + { + return; + } + + TimeSpaceParty timeSpace = session.PlayerEntity.TimeSpaceComponent.TimeSpace; + if (timeSpace?.Instance?.PreFinishDialog == null) + { + return; + } + + IPortalEntity portal = session.CurrentMapInstance.Portals.FirstOrDefault(s => s.Type == PortalType.TSEnd && + session.PlayerEntity.PositionY >= s.PositionY - 1 && + session.PlayerEntity.PositionY <= s.PositionY + 1 && + session.PlayerEntity.PositionX >= s.PositionX - 1 && + session.PlayerEntity.PositionX <= s.PositionX + 1); + + timeSpace.Instance.PreFinishDialog = null; + + if (timeSpace.Instance.PreFinishDialogIsObjective) + { + timeSpace.Instance.PreFinishDialogIsObjective = false; + timeSpace.Instance.TimeSpaceObjective.ConversationsHad++; + await session.EmitEventAsync(new TimeSpaceRefreshObjectiveProgressEvent()); + } + + await session.EmitEventAsync(new TimeSpaceCheckObjectivesEvent + { + TimeSpaceParty = timeSpace, + PlayerEnteredToEndPortal = portal != null + }); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/TimeSpaceTimerHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/TimeSpaceTimerHandler.cs new file mode 100644 index 0000000..6822690 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/TimeSpaceTimerHandler.cs @@ -0,0 +1,116 @@ +using System; +using System.Threading.Tasks; +using WingsEmu.Game._NpcDialog; +using WingsEmu.Game._NpcDialog.Event; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Networking; +using WingsEmu.Game.TimeSpaces; +using WingsEmu.Game.TimeSpaces.Events; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.NpcDialogs; + +public class TimeSpaceTimerHandler : INpcDialogAsyncHandler +{ + public NpcRunType[] NpcRunTypes => new[] { NpcRunType.TIME_SPACE_TIMER, NpcRunType.TIME_SPACE_UNKNOWN }; + + public async Task Execute(IClientSession session, NpcDialogEvent e) + { + if (!session.HasCurrentMapInstance) + { + return; + } + + if (!session.PlayerEntity.TimeSpaceComponent.IsInTimeSpaceParty) + { + return; + } + + if (session.CurrentMapInstance.MapInstanceType != MapInstanceType.TimeSpaceInstance) + { + return; + } + + TimeSpaceParty timeSpace = session.PlayerEntity.TimeSpaceComponent.TimeSpace; + if (timeSpace?.Instance == null) + { + return; + } + + if (timeSpace.Instance.PreFinishDialog.HasValue && timeSpace.Instance.PreFinishDialogTime.HasValue) + { + await session.EmitEventAsync(new NpcDialogEvent + { + NpcRunType = NpcRunType.TIMESPACE_ON_FINISH_DIALOG + }); + return; + } + + if (!timeSpace.Instance.TimeSpaceSubInstances.TryGetValue(session.CurrentMapInstance.Id, out TimeSpaceSubInstance instance)) + { + return; + } + + if (instance.Task == null) + { + return; + } + + if (instance.Task.StartDialog.HasValue) + { + if (!instance.Task.DialogStartTask) + { + await TryAddFrozenTime(session, timeSpace); + return; + } + + instance.Task.StartDialog = null; + await TryAddFrozenTime(session, timeSpace); + + if (instance.Task.StartDialogIsObjective) + { + instance.Task.StartDialogIsObjective = false; + timeSpace.Instance.TimeSpaceObjective.ConversationsHad++; + await session.EmitEventAsync(new TimeSpaceRefreshObjectiveProgressEvent()); + } + + await session.EmitEventAsync(new TimeSpaceStartTaskEvent + { + TimeSpaceSubInstance = instance + }); + return; + } + + if (!instance.Task.EndDialog.HasValue) + { + return; + } + + instance.Task.EndDialog = null; + await TryAddFrozenTime(session, timeSpace); + + if (!instance.Task.EndDialogIsObjective) + { + return; + } + + instance.Task.EndDialogIsObjective = false; + timeSpace.Instance.TimeSpaceObjective.ConversationsHad++; + await session.EmitEventAsync(new TimeSpaceRefreshObjectiveProgressEvent()); + } + + private async Task TryAddFrozenTime(IClientSession session, TimeSpaceParty timeSpaceParty) + { + if (!timeSpaceParty.Instance.StartTimeFreeze.HasValue) + { + return; + } + + await session.EmitEventAsync(new TimeSpaceAddTimeToTimerEvent + { + Time = DateTime.UtcNow - timeSpaceParty.Instance.StartTimeFreeze.Value + }); + + timeSpaceParty.Instance.StartTimeFreeze = null; + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/UpgradeItemNpcHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/UpgradeItemNpcHandler.cs new file mode 100644 index 0000000..3acca58 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/NpcDialogs/UpgradeItemNpcHandler.cs @@ -0,0 +1,25 @@ +using System.Threading.Tasks; +using WingsEmu.Game._NpcDialog; +using WingsEmu.Game._NpcDialog.Event; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.NpcDialogs; + +public class UpgradeItemNpcHandler : INpcDialogAsyncHandler +{ + public NpcRunType[] NpcRunTypes => new[] { NpcRunType.UPGRADE_ITEM_NPC }; + + public async Task Execute(IClientSession session, NpcDialogEvent e) + { + INpcEntity npcEntity = session.CurrentMapInstance.GetNpcById(e.NpcId); + if (npcEntity == null) + { + return; + } + + session.SendWopenPacket(WindowType.UPGRADE_ITEM_NPC); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/PlayerEntityFactory.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/PlayerEntityFactory.cs new file mode 100644 index 0000000..d7dc77c --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/PlayerEntityFactory.cs @@ -0,0 +1,216 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Mapster; +using PhoenixLib.Events; +using WingsAPI.Data.Character; +using WingsAPI.Data.Miniland; +using WingsEmu.DTOs.Inventory; +using WingsEmu.DTOs.Quests; +using WingsEmu.DTOs.Skills; +using WingsEmu.Game; +using WingsEmu.Game.Algorithm; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Families; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Portals; +using WingsEmu.Game.SnackFood; +using WingsEmu.Game.Warehouse; + +namespace WingsEmu.Plugins.BasicImplementations; + +public class PlayerEntityFactory : IPlayerEntityFactory +{ + private readonly IBattleEntityAlgorithmService _algorithm; + private readonly ICharacterAlgorithm _characterAlgorithm; + private readonly IAsyncEventPipeline _eventPipeline; + private readonly IFamilyManager _familyManager; + private readonly IFoodSnackComponentFactory _foodSnackComponentFactory; + private readonly IGameItemInstanceFactory _itemInstanceFactory; + private readonly IItemsManager _itemsManager; + private readonly IMapManager _mapManager; + private readonly IMateEntityFactory _mateEntityFactory; + private readonly IPortalFactory _portalFactory; + private readonly IRandomGenerator _randomGenerator; + + public PlayerEntityFactory(IFamilyManager familyManager, IRandomGenerator randomGenerator, IMapManager mapManager, ICharacterAlgorithm characterAlgorithm, + IFoodSnackComponentFactory foodSnackComponentFactory, IAsyncEventPipeline eventPipeline, IBattleEntityAlgorithmService algorithm, IMateEntityFactory mateEntityFactory, + IPortalFactory portalFactory, IItemsManager itemsManager, IGameItemInstanceFactory itemInstanceFactory) + { + _familyManager = familyManager; + _randomGenerator = randomGenerator; + _mapManager = mapManager; + _characterAlgorithm = characterAlgorithm; + _foodSnackComponentFactory = foodSnackComponentFactory; + _eventPipeline = eventPipeline; + _algorithm = algorithm; + _mateEntityFactory = mateEntityFactory; + _portalFactory = portalFactory; + _itemsManager = itemsManager; + _itemInstanceFactory = itemInstanceFactory; + } + + public IPlayerEntity CreatePlayerEntity(CharacterDTO characterDto) => new PlayerEntity(characterDto, _familyManager, _randomGenerator, _mapManager, _characterAlgorithm, _foodSnackComponentFactory, + _eventPipeline, _algorithm, _portalFactory, _itemsManager); + + public CharacterDTO CreateCharacterDto(IPlayerEntity playerEntity) + { + var characterDto = new CharacterDTO + { + Id = playerEntity.Id, + Hp = playerEntity.Hp, + Mp = playerEntity.Mp, + Level = playerEntity.Level, + Faction = playerEntity.Faction, + AccountId = playerEntity.AccountId, + Act4Dead = playerEntity.Act4Dead, + Act4Kill = playerEntity.Act4Kill, + Act4Points = playerEntity.Act4Points, + ArenaWinner = playerEntity.ArenaWinner, + Biography = playerEntity.Biography, + BuffBlocked = playerEntity.BuffBlocked, + Class = playerEntity.Class, + Compliment = playerEntity.Compliment, + Dignity = playerEntity.Dignity, + EmoticonsBlocked = playerEntity.EmoticonsBlocked, + ExchangeBlocked = playerEntity.ExchangeBlocked, + FamilyRequestBlocked = playerEntity.FamilyRequestBlocked, + FriendRequestBlocked = playerEntity.FriendRequestBlocked, + Gender = playerEntity.Gender, + Gold = playerEntity.Gold, + GroupRequestBlocked = playerEntity.GroupRequestBlocked, + HairColor = playerEntity.HairColor, + HairStyle = playerEntity.HairStyle, + HeroChatBlocked = playerEntity.HeroChatBlocked, + HeroLevel = playerEntity.HeroLevel, + HeroXp = playerEntity.HeroXp, + HpBlocked = playerEntity.HpBlocked, + IsPetAutoRelive = playerEntity.IsPetAutoRelive, + IsPartnerAutoRelive = playerEntity.IsPartnerAutoRelive, + JobLevel = playerEntity.JobLevel, + JobLevelXp = playerEntity.JobLevelXp, + LevelXp = playerEntity.LevelXp, + MapId = playerEntity.MapId, + MapX = playerEntity.MapX, + MapY = playerEntity.MapY, + MasterPoints = playerEntity.MasterPoints, + MasterTicket = playerEntity.MasterTicket, + MaxPetCount = playerEntity.MaxPetCount, + MaxPartnerCount = playerEntity.MaxPartnerCount, + MinilandInviteBlocked = playerEntity.MinilandInviteBlocked, + MinilandMessage = playerEntity.MinilandMessage, + MinilandPoint = playerEntity.MinilandPoint, + MinilandState = playerEntity.MinilandState, + MouseAimLock = playerEntity.MouseAimLock, + Name = playerEntity.Name, + QuickGetUp = playerEntity.QuickGetUp, + HideHat = playerEntity.HideHat, + UiBlocked = playerEntity.UiBlocked, + Reput = playerEntity.Reput, + Slot = playerEntity.Slot, + SpPointsBonus = playerEntity.SpPointsBonus, + SpPointsBasic = playerEntity.SpPointsBasic, + TalentLose = playerEntity.TalentLose, + TalentSurrender = playerEntity.TalentSurrender, + TalentWin = playerEntity.TalentWin, + WhisperBlocked = playerEntity.WhisperBlocked, + PartnerInventory = playerEntity.PartnerInventory, + NosMates = playerEntity.NosMates, + PartnerWarehouse = playerEntity.PartnerWarehouse, + Bonus = playerEntity.Bonus, + StaticBuffs = playerEntity.StaticBuffs, + Quicklist = playerEntity.Quicklist, + LearnedSkills = playerEntity.LearnedSkills, + Titles = playerEntity.Titles, + CompletedScripts = playerEntity.CompletedScripts, + CompletedQuests = playerEntity.CompletedQuests, + CompletedPeriodicQuests = playerEntity.CompletedPeriodicQuests, + ActiveQuests = playerEntity.ActiveQuests, + MinilandObjects = playerEntity.MinilandObjects, + Inventory = playerEntity.Inventory, + EquippedStuffs = playerEntity.EquippedStuffs, + LifetimeStats = playerEntity.LifetimeStats, + RespawnType = playerEntity.HomeComponent.RespawnType, + Act5RespawnType = playerEntity.HomeComponent.Act5RespawnType, + ReturnPoint = playerEntity.HomeComponent.Return, + CompletedTimeSpaces = playerEntity.CompletedTimeSpaces, + RaidRestrictionDto = playerEntity.RaidRestrictionDto, + RainbowBattleLeaverBusterDto = playerEntity.RainbowBattleLeaverBusterDto + }; + + characterDto.StaticBuffs.RemoveAll(s => s.RemainingTime <= 0); + characterDto.StaticBuffs = playerEntity.GetSavedBuffs(); + characterDto.Bonus.RemoveAll(s => s.DateEnd.HasValue && DateTime.UtcNow >= s.DateEnd.Value); + characterDto.Bonus = playerEntity.Bonus; + characterDto.Quicklist = playerEntity.QuicklistComponent.GetQuicklist(); + characterDto.LearnedSkills = playerEntity.CharacterSkills.Where(x => x.Value != null).Select(s => s.Value.Adapt()).ToList(); + characterDto.ActiveQuests = playerEntity.GetQuestsProgress().Where(x => x != null).Select(s => s.Adapt()).ToList(); + characterDto.CompletedScripts = playerEntity.GetCompletedScripts().Where(x => x != null).Select(s => s.Adapt()).ToList(); + characterDto.CompletedQuests = playerEntity.GetCompletedQuests().Where(x => x != null).Select(s => s.Adapt()).ToList(); + characterDto.CompletedPeriodicQuests = playerEntity.GetCompletedPeriodicQuests().Where(x => x != null).Select(s => s.Adapt()).ToList(); + characterDto.MinilandObjects = playerEntity.Miniland?.MapDesignObjects?.Where(x => x != null).Select(s => s.Adapt()).ToList() ?? + new List(); + + List notEquippedItems = new(); + List equippedItems = new(); + + foreach (InventoryItem item in playerEntity.GetAllPlayerInventoryItems()) + { + if (item == null) + { + continue; + } + + List items = item.IsEquipped ? equippedItems : notEquippedItems; + + CharacterInventoryItemDto tmp = item.Adapt(); + tmp.ItemInstance = _itemInstanceFactory.CreateDto(item.ItemInstance); + + items.Add(tmp); + } + + characterDto.Inventory = notEquippedItems; + characterDto.EquippedStuffs = equippedItems; + + List partnerWarehouse = new(); + foreach (PartnerWarehouseItem item in playerEntity.PartnerWarehouseItems()) + { + if (item == null) + { + continue; + } + + PartnerWarehouseItemDto tmp = item.Adapt(); + tmp.ItemInstance = _itemInstanceFactory.CreateDto(item.ItemInstance); + + partnerWarehouse.Add(tmp); + } + + characterDto.PartnerWarehouse = partnerWarehouse; + + characterDto.NosMates = playerEntity.MateComponent.GetMates().Select(mate => _mateEntityFactory.CreateMateDto(mate)).ToList(); + + List partnerInventory = new(); + foreach (PartnerInventoryItem item in playerEntity.GetPartnersEquippedItems()) + { + if (item == null) + { + continue; + } + + CharacterPartnerInventoryItemDto tmp = item.Adapt(); + tmp.ItemInstance = _itemInstanceFactory.CreateDto(item.ItemInstance); + + partnerInventory.Add(tmp); + } + + characterDto.PartnerInventory = partnerInventory; + + return characterDto; + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Quicklist/QuicklistAddEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Quicklist/QuicklistAddEventHandler.cs new file mode 100644 index 0000000..4f3fd7c --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Quicklist/QuicklistAddEventHandler.cs @@ -0,0 +1,52 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.Quicklist; +using WingsEmu.DTOs.Quicklist; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Quicklist; +using WingsEmu.Game.Skills; + +namespace WingsEmu.Plugins.BasicImplementations.Quicklist; + +public class QuicklistAddEventHandler : IAsyncEventProcessor +{ + public async Task HandleAsync(QuicklistAddEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + + short tab = e.Tab; + short slot = e.Slot; + int morphId = session.PlayerEntity.UseSp ? session.PlayerEntity.Specialist?.GameItem.Morph ?? 0 : 0; + + CharacterQuicklistEntryDto from = session.PlayerEntity.QuicklistComponent.GetQuicklistByTabSlotAndMorph(tab, slot, morphId); + + if (from != null) + { + session.PlayerEntity.QuicklistComponent.RemoveQuicklist(tab, slot, morphId); + session.SendEmptyQuicklistSlot(tab, slot); + } + + // Used only for combo skills :sadge: + IBattleEntitySkill? getSkill = e.DestinationType != 3 ? session.PlayerEntity.GetSkills().FirstOrDefault(x => x != null && x.Skill.Id > 200 && x.Skill.CastId == e.DestinationSlotOrVnum) : null; + + from = new CharacterQuicklistEntryDto + { + Morph = (short)morphId, + Type = e.Type, + QuicklistSlot = slot, + QuicklistTab = tab, + InventoryTypeOrSkillTab = e.DestinationType, + InvSlotOrSkillSlotOrSkillVnum = e.DestinationSlotOrVnum, + SkillVnum = (short?)getSkill?.Skill.Id + }; + + session.PlayerEntity.QuicklistComponent.AddQuicklist(from); + session.SendQuicklistSlot(from); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Quicklist/QuicklistRemoveEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Quicklist/QuicklistRemoveEventHandler.cs new file mode 100644 index 0000000..cc9292d --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Quicklist/QuicklistRemoveEventHandler.cs @@ -0,0 +1,30 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.Quicklist; +using WingsEmu.DTOs.Quicklist; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Quicklist; + +namespace WingsEmu.Plugins.BasicImplementations.Quicklist; + +public class QuicklistRemoveEventHandler : IAsyncEventProcessor +{ + public async Task HandleAsync(QuicklistRemoveEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + + short tab = e.Tab; + short slot = e.Slot; + int morphId = session.PlayerEntity.UseSp ? session.PlayerEntity.Specialist?.GameItem.Morph ?? 0 : 0; + + CharacterQuicklistEntryDto quicklist = session.PlayerEntity.QuicklistComponent.GetQuicklistByTabSlotAndMorph(tab, slot, morphId); + if (quicklist == null) + { + return; + } + + e.Sender.PlayerEntity.QuicklistComponent.RemoveQuicklist(tab, slot, morphId); + e.Sender.SendEmptyQuicklistSlot(tab, slot); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Quicklist/QuicklistSwapEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Quicklist/QuicklistSwapEventHandler.cs new file mode 100644 index 0000000..6161ec0 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Quicklist/QuicklistSwapEventHandler.cs @@ -0,0 +1,48 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.Quicklist; +using WingsEmu.DTOs.Quicklist; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Quicklist; + +namespace WingsEmu.Plugins.BasicImplementations.Quicklist; + +public class QuicklistSwapEventHandler : IAsyncEventProcessor +{ + public async Task HandleAsync(QuicklistSwapEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + + short tab = e.Tab; + short fromSlot = e.FromSlot; + short toSlot = e.ToSlot; + int morphId = session.PlayerEntity.UseSp ? session.PlayerEntity.Specialist?.GameItem.Morph ?? 0 : 0; + + CharacterQuicklistEntryDto from = session.PlayerEntity.QuicklistComponent.GetQuicklistByTabSlotAndMorph(tab, fromSlot, morphId); + if (from == null) + { + // incorrect packet + return; + } + + session.PlayerEntity.QuicklistComponent.RemoveQuicklist(tab, fromSlot, morphId); + from.QuicklistSlot = toSlot; + + CharacterQuicklistEntryDto to = session.PlayerEntity.QuicklistComponent.GetQuicklistByTabSlotAndMorph(tab, toSlot, morphId); + if (to != null) + { + session.PlayerEntity.QuicklistComponent.RemoveQuicklist(tab, toSlot, morphId); + to.QuicklistSlot = fromSlot; + session.PlayerEntity.QuicklistComponent.AddQuicklist(to); + session.SendQuicklistSlot(to); + } + else + { + session.SendEmptyQuicklistSlot(tab, fromSlot); + } + + session.PlayerEntity.QuicklistComponent.AddQuicklist(from); + session.SendQuicklistSlot(from); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/RandomFactory.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/RandomFactory.cs new file mode 100644 index 0000000..bc6cf96 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/RandomFactory.cs @@ -0,0 +1,25 @@ +using System; +using RandN; +using RandN.Compat; +using WingsEmu.Game; + +namespace WingsEmu.Plugins.BasicImplementations; + +public class RandomGenerator : IRandomGenerator +{ + private static readonly Random Local = RandomShim.Create(SmallRng.Create()); + + public int RandomNumber(int min, int max) + { + if (min > max) + { + return RandomNumber(max, min); + } + + return min == max ? max : Local.Next(min, max); + } + + public int RandomNumber(int max) => RandomNumber(0, max); + + public int RandomNumber() => RandomNumber(0, 100); +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/RecipeFactory.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/RecipeFactory.cs new file mode 100644 index 0000000..6695635 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/RecipeFactory.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using WingsEmu.DTOs.Recipes; +using WingsEmu.Game; + +namespace WingsEmu.Plugins.BasicImplementations; + +public class RecipeFactory : IRecipeFactory +{ + private readonly List _emptyItemDtos = new(); + + public Recipe CreateRecipe(RecipeDTO recipeDto) => new Recipe(recipeDto.Id, recipeDto.Amount, recipeDto.ProducerMapNpcId, recipeDto.ProducerItemVnum, recipeDto.ProducerNpcVnum, + recipeDto.ProducedItemVnum, recipeDto.Items ?? _emptyItemDtos); +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Recipes/RecipeOpenWindowEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Recipes/RecipeOpenWindowEventHandler.cs new file mode 100644 index 0000000..4091675 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Recipes/RecipeOpenWindowEventHandler.cs @@ -0,0 +1,63 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.ItemExtension.Item; +using WingsEmu.Game; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Managers.ServerData; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.Recipes; + +public class RecipeOpenWindowEventHandler : IAsyncEventProcessor +{ + private readonly IGameItemInstanceFactory _gameItemInstanceFactory; + private readonly IItemsManager _itemsManager; + private readonly IItemUsageManager _itemUsageManager; + private readonly IRecipeManager _recipeManager; + + public RecipeOpenWindowEventHandler(IRecipeManager recipeManager, IItemsManager itemsManager, IItemUsageManager itemUsageManager, IGameItemInstanceFactory gameItemInstanceFactory) + { + _recipeManager = recipeManager; + _itemsManager = itemsManager; + _itemUsageManager = itemUsageManager; + _gameItemInstanceFactory = gameItemInstanceFactory; + } + + public async Task HandleAsync(RecipeOpenWindowEvent e, CancellationToken cancellation) + { + IReadOnlyList recipes = _recipeManager.GetRecipesByProducerItemVnum(e.ItemVnum); + if (recipes.Count == 0) + { + return; + } + + IGameItem item = _itemsManager.GetItem(e.ItemVnum); + if (item == null) + { + return; + } + + IClientSession session = e.Sender; + + // Blowa: This is dirty, we know that. It's a fucking trick + InventoryItem inventory = _gameItemInstanceFactory.CreateInventoryItem(item.Id); + inventory.Slot = short.MaxValue; + + session.SendWopenPacket((byte)WindowType.CRAFTING_ITEMS, short.MaxValue, 0); + session.SendRecipeItemList(recipes, item); + session.SendInventoryAddPacket(inventory); + session.PlayerEntity.LastMinilandProducedItem = item.Id; + _itemUsageManager.SetLastItemUsed(session.PlayerEntity.Id, e.ItemVnum); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Revival/RevivalAskEventArenaHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Revival/RevivalAskEventArenaHandler.cs new file mode 100644 index 0000000..c4406a5 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Revival/RevivalAskEventArenaHandler.cs @@ -0,0 +1,36 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Revival; + +namespace WingsEmu.Plugins.BasicImplementations.Revival; + +public class RevivalAskEventArenaHandler : IAsyncEventProcessor +{ + private readonly IGameLanguageService _languageService; + private readonly PlayerRevivalConfiguration _revivalConfiguration; + + public RevivalAskEventArenaHandler(GameRevivalConfiguration gameRevivalConfiguration, IGameLanguageService languageService) + { + _revivalConfiguration = gameRevivalConfiguration.PlayerRevivalConfiguration; + _languageService = languageService; + } + + public async Task HandleAsync(RevivalAskEvent e, CancellationToken cancellation) + { + if (e.Sender.PlayerEntity.IsAlive() || e.AskRevivalType != AskRevivalType.ArenaRevival) + { + return; + } + + string message = _languageService.GetLanguageFormat(GameDialogKey.REVIVE_DIALOG_ARENA, e.Sender.UserLanguage, + _revivalConfiguration.PlayerRevivalPenalization.ArenaGoldPenalization.ToString()); + + e.Sender.SendDialog(CharacterPacketExtension.GenerateRevivalPacket(RevivalType.TryPayArenaRevival), + CharacterPacketExtension.GenerateRevivalPacket(RevivalType.DontPayRevival), message); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Revival/RevivalAskEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Revival/RevivalAskEventHandler.cs new file mode 100644 index 0000000..96e415a --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Revival/RevivalAskEventHandler.cs @@ -0,0 +1,38 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Revival; + +namespace WingsEmu.Plugins.BasicImplementations.Revival; + +public class RevivalAskEventHandler : IAsyncEventProcessor +{ + private readonly IGameLanguageService _languageService; + private readonly PlayerRevivalConfiguration _revivalConfiguration; + + public RevivalAskEventHandler(GameRevivalConfiguration gameRevivalConfiguration, IGameLanguageService languageService) + { + _revivalConfiguration = gameRevivalConfiguration.PlayerRevivalConfiguration; + _languageService = languageService; + } + + public async Task HandleAsync(RevivalAskEvent e, CancellationToken cancellation) + { + if (e.Sender.PlayerEntity.IsAlive() || e.AskRevivalType != AskRevivalType.BasicRevival) + { + return; + } + + PlayerRevivalPenalization playerRevivalPenalization = _revivalConfiguration.PlayerRevivalPenalization; + string message = e.Sender.PlayerEntity.Level > playerRevivalPenalization.MaxLevelWithoutRevivalPenalization + ? _languageService.GetLanguageFormat(GameDialogKey.REVIVE_DIALOG_SEEDS_OF_POWER, e.Sender.UserLanguage, playerRevivalPenalization.BaseMapRevivalPenalizationSaverAmount) + : _languageService.GetLanguageFormat(GameDialogKey.REVIVE_DIALOG_FREE, e.Sender.UserLanguage, playerRevivalPenalization.MaxLevelWithoutRevivalPenalization); + + e.Sender.SendDialog(CharacterPacketExtension.GenerateRevivalPacket(RevivalType.TryPayRevival), CharacterPacketExtension.GenerateRevivalPacket(RevivalType.DontPayRevival), + message); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Revival/RevivalEventArenaHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Revival/RevivalEventArenaHandler.cs new file mode 100644 index 0000000..2045882 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Revival/RevivalEventArenaHandler.cs @@ -0,0 +1,79 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.Groups; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage.Configuration; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Revival; + +namespace WingsEmu.Plugins.BasicImplementations.Revival; + +public class RevivalEventArenaHandler : IAsyncEventProcessor +{ + private readonly IGameLanguageService _gameLanguage; + private readonly PlayerRevivalPenalization _revivalPenalization; + private readonly ISpPartnerConfiguration _spPartnerConfiguration; + + public RevivalEventArenaHandler(GameRevivalConfiguration revivalConfiguration, IGameLanguageService gameLanguage, ISpPartnerConfiguration spPartnerConfiguration) + { + _gameLanguage = gameLanguage; + _spPartnerConfiguration = spPartnerConfiguration; + _revivalPenalization = revivalConfiguration.PlayerRevivalConfiguration.PlayerRevivalPenalization; + } + + public async Task HandleAsync(RevivalReviveEvent e, CancellationToken cancellation) + { + IClientSession sender = e.Sender; + IPlayerEntity character = e.Sender.PlayerEntity; + + if (!sender.HasCurrentMapInstance) + { + return; + } + + if (character.IsAlive() || character.MapInstance.MapInstanceType != MapInstanceType.ArenaInstance) + { + return; + } + + character.Hp = 1; + character.Mp = 1; + + bool hasPaidPenalization = false; + if (e.RevivalType == RevivalType.TryPayArenaRevival && e.Forced != ForcedType.HolyRevival) + { + hasPaidPenalization = character.RemoveGold(_revivalPenalization.ArenaGoldPenalization); + } + + if (e.Forced == ForcedType.HolyRevival) + { + hasPaidPenalization = true; + } + + if (hasPaidPenalization) + { + await character.Restore(restoreMates: false); + sender.RefreshStat(); + sender.BroadcastTeleportPacket(); + sender.BroadcastInTeamMembers(_gameLanguage, _spPartnerConfiguration); + sender.RefreshParty(_spPartnerConfiguration); + } + else + { + await sender.Respawn(); + } + + sender.BroadcastRevive(); + sender.UpdateVisibility(); + await sender.CheckPartnerBuff(); + e.Sender.SendBuffsPacket(); + sender.PlayerEntity.ArenaImmunity = DateTime.UtcNow; + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Revival/RevivalEventBaseHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Revival/RevivalEventBaseHandler.cs new file mode 100644 index 0000000..cbff4ef --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Revival/RevivalEventBaseHandler.cs @@ -0,0 +1,165 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.Groups; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage.Configuration; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Revival; + +namespace WingsEmu.Plugins.BasicImplementations.Revival; + +public class RevivalEventBaseHandler : IAsyncEventProcessor +{ + private readonly IBuffFactory _buffFactory; + private readonly IItemsManager _itemsManager; + private readonly IGameLanguageService _languageService; + private readonly PlayerRevivalConfiguration _revivalConfiguration; + private readonly IRevivalManager _revivalManager; + private readonly ISpPartnerConfiguration _spPartnerConfiguration; + + public RevivalEventBaseHandler(GameRevivalConfiguration gameRevivalConfiguration, + IBuffFactory buffFactory, IGameLanguageService languageService, IItemsManager itemsManager, IRevivalManager revivalManager, ISpPartnerConfiguration spPartnerConfiguration) + { + _revivalConfiguration = gameRevivalConfiguration.PlayerRevivalConfiguration; + _buffFactory = buffFactory; + _languageService = languageService; + _itemsManager = itemsManager; + _revivalManager = revivalManager; + _spPartnerConfiguration = spPartnerConfiguration; + } + + public async Task HandleAsync(RevivalReviveEvent e, CancellationToken cancellation) + { + IClientSession sender = e.Sender; + IPlayerEntity character = sender?.PlayerEntity; + + if (character?.MapInstance == null) + { + return; + } + + if (character.IsAlive()) + { + return; + } + + if (!sender.CurrentMapInstance.HasMapFlag(MapFlags.IS_BASE_MAP)) + { + return; + } + + if (sender.CurrentMapInstance.HasMapFlag(MapFlags.ACT_4)) + { + return; + } + + bool isInNosVille = character.MapId == (int)MapIds.NOSVILLE; + character.Hp = isInNosVille ? character.MaxHp / 2 : 1; + character.Mp = isInNosVille ? character.MaxMp / 2 : 1; + + bool hasPaidPenalization = false; + if (e.RevivalType == RevivalType.TryPayRevival && e.Forced != ForcedType.HolyRevival) + { + hasPaidPenalization = await TryPayPenalization(character, _revivalConfiguration.PlayerRevivalPenalization); + } + + if (e.Forced == ForcedType.HolyRevival) + { + hasPaidPenalization = true; + } + + if (hasPaidPenalization) + { + sender.RefreshStat(); + sender.BroadcastTeleportPacket(); + sender.BroadcastInTeamMembers(_languageService, _spPartnerConfiguration); + sender.RefreshParty(_spPartnerConfiguration); + } + else if (e.Forced != ForcedType.HolyRevival) + { + await sender.Respawn(); + } + + if (e.Forced == ForcedType.HolyRevival) + { + e.Sender.PlayerEntity.Hp = e.Sender.PlayerEntity.MaxHp; + e.Sender.PlayerEntity.Mp = e.Sender.PlayerEntity.MaxMp; + e.Sender.RefreshStat(); + } + + sender.BroadcastRevive(); + sender.UpdateVisibility(); + await sender.CheckPartnerBuff(); + sender.SendBuffsPacket(); + + if (character.Level > _revivalConfiguration.PlayerRevivalPenalization.MaxLevelWithoutRevivalPenalization && e.Forced != ForcedType.HolyRevival) + { + await character.AddBuffAsync(_buffFactory.CreateBuff(_revivalConfiguration.PlayerRevivalPenalization.BaseMapRevivalPenalizationDebuff, character)); + } + } + + public bool BasicUnregistering(long id, ForcedType forced, Guid expectedGuid) + { + switch (forced) + { + case ForcedType.Forced: + if (!_revivalManager.UnregisterRevival(id, expectedGuid)) + { + return false; + } + + break; + case ForcedType.NoForced: + if (!_revivalManager.UnregisterRevival(id)) + { + return false; + } + + break; + default: + _revivalManager.TryUnregisterRevival(id); + break; + } + + return true; + } + + private async Task TryPayPenalization(IPlayerEntity character, PlayerRevivalPenalization playerRevivalPenalization) + { + if (character.Level <= playerRevivalPenalization.MaxLevelWithoutRevivalPenalization) + { + await character.Restore(restoreMates: false); + return true; + } + + int item = playerRevivalPenalization.BaseMapRevivalPenalizationSaver; + short amount = (short)playerRevivalPenalization.BaseMapRevivalPenalizationSaverAmount; + string itemName = _languageService.GetItemName(_itemsManager.GetItem(item), character.Session); + + if (!character.HasItem(item, amount)) + { + character.Session.SendErrorChatMessage(_languageService.GetLanguageFormat(GameDialogKey.INVENTORY_SHOUTMESSAGE_NOT_ENOUGH_ITEMS, character.Session.UserLanguage, amount, itemName)); + return false; + } + + await character.Session.RemoveItemFromInventory(item, amount); + + character.Session.SendSuccessChatMessage(_languageService.GetLanguageFormat(GameDialogKey.INFORMATION_CHATMESSAGE_REQUIRED_ITEM_EXPENDED, character.Session.UserLanguage, itemName, amount)); + + character.Hp = character.MaxHp / 2; + character.Mp = character.MaxMp / 2; + return true; + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Revival/RevivalEventIceBreakerHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Revival/RevivalEventIceBreakerHandler.cs new file mode 100644 index 0000000..4093e0a --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Revival/RevivalEventIceBreakerHandler.cs @@ -0,0 +1,37 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Groups.Events; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Revival; + +namespace WingsEmu.Plugins.BasicImplementations.Revival; + +public class RevivalEventIceBreakerHandler : IAsyncEventProcessor +{ + private readonly RevivalEventNormalHandler _eventNormalHandler; + + public RevivalEventIceBreakerHandler(RevivalEventNormalHandler eventNormalHandler) => _eventNormalHandler = eventNormalHandler; + + public async Task HandleAsync(RevivalReviveEvent e, CancellationToken cancellation) + { + IPlayerEntity character = e.Sender.PlayerEntity; + if (e.Sender?.CurrentMapInstance == null) + { + return; + } + + if (character.IsAlive() || e.Sender.PlayerEntity.MapInstance.MapInstanceType != MapInstanceType.IceBreakerInstance) + { + return; + } + + e.Sender.SendBuffsPacket(); + // get group + e.Sender.EmitEvent(new LeaveGroupEvent()); + await _eventNormalHandler.BaseRevive(e); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Revival/RevivalEventNormalHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Revival/RevivalEventNormalHandler.cs new file mode 100644 index 0000000..280cedc --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Revival/RevivalEventNormalHandler.cs @@ -0,0 +1,69 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.Groups; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage.Configuration; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Revival; + +namespace WingsEmu.Plugins.BasicImplementations.Revival; + +public class RevivalEventNormalHandler : IAsyncEventProcessor +{ + private readonly IGameLanguageService _gameLanguageService; + private readonly ISpPartnerConfiguration _spPartnerConfiguration; + + public RevivalEventNormalHandler(IGameLanguageService gameLanguageService, ISpPartnerConfiguration spPartnerConfiguration) + { + _gameLanguageService = gameLanguageService; + _spPartnerConfiguration = spPartnerConfiguration; + } + + public async Task HandleAsync(RevivalReviveEvent e, CancellationToken cancellation) + { + if (e.Sender.PlayerEntity == null) + { + return; + } + + IPlayerEntity character = e.Sender.PlayerEntity; + if (character.IsAlive() || character.MapInstance == null || + character.MapInstance.MapInstanceType != MapInstanceType.NormalInstance + && e.Sender.PlayerEntity.MapInstance.MapInstanceType != MapInstanceType.EventGameInstance + && character.MapInstance.MapInstanceType != MapInstanceType.Miniland) + { + return; + } + + await BaseRevive(e); + } + + public async Task BaseRevive(RevivalReviveEvent e) + { + IPlayerEntity character = e.Sender.PlayerEntity; + await character.Restore(restoreMates: false); + + if (character.MapInstance.MapInstanceType != MapInstanceType.Miniland && e.RevivalType == RevivalType.DontPayRevival && e.Forced != ForcedType.HolyRevival) + { + await e.Sender.Respawn(); + } + + if (e.Forced == ForcedType.HolyRevival) + { + e.Sender.PlayerEntity.Hp = e.Sender.PlayerEntity.MaxHp; + e.Sender.PlayerEntity.Mp = e.Sender.PlayerEntity.MaxMp; + e.Sender.RefreshStat(); + } + + e.Sender.BroadcastRevive(); + e.Sender.UpdateVisibility(); + e.Sender.BroadcastInTeamMembers(_gameLanguageService, _spPartnerConfiguration); + e.Sender.RefreshParty(_spPartnerConfiguration); + await e.Sender.CheckPartnerBuff(); + e.Sender.SendBuffsPacket(); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Revival/RevivalStartProcedureEventArenaHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Revival/RevivalStartProcedureEventArenaHandler.cs new file mode 100644 index 0000000..985c489 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Revival/RevivalStartProcedureEventArenaHandler.cs @@ -0,0 +1,119 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Revival; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.Revival; + +public class RevivalStartProcedureEventArenaHandler : IAsyncEventProcessor +{ + private readonly IGameLanguageService _gameLanguage; + private readonly PlayerRevivalConfiguration _revivalConfiguration; + + public RevivalStartProcedureEventArenaHandler(GameRevivalConfiguration gameRevivalConfiguration, IGameLanguageService gameLanguage) + { + _gameLanguage = gameLanguage; + _revivalConfiguration = gameRevivalConfiguration.PlayerRevivalConfiguration; + } + + public async Task HandleAsync(RevivalStartProcedureEvent e, CancellationToken cancellation) + { + if (e.Sender.PlayerEntity.MapInstance.MapInstanceType != MapInstanceType.ArenaInstance) + { + return; + } + + DateTime actualTime = DateTime.UtcNow; + + if (e.Sender.PlayerEntity.IsOnVehicle) + { + await e.Sender.EmitEventAsync(new RemoveVehicleEvent()); + } + + IBattleEntity killer = e.Killer; + if (killer != null) + { + IPlayerEntity playerKiller = killer switch + { + IPlayerEntity playerEntity => playerEntity, + IMateEntity mateEntity => mateEntity.Owner, + IMonsterEntity monsterEntity => monsterEntity.SummonerType != null && monsterEntity.SummonerId != null && monsterEntity.SummonerType == VisualType.Player + ? monsterEntity.MapInstance.GetCharacterById(monsterEntity.SummonerId.Value) + : null, + _ => null + }; + + if (playerKiller != null) + { + e.Sender.CurrentMapInstance.Broadcast(x => + { + return x.GenerateMsgPacket(_gameLanguage.GetLanguageFormat(GameDialogKey.ARENA_SHOUTMESSAGE_PVP_KILL, + x.UserLanguage, e.Sender.PlayerEntity.Name, playerKiller.Name), MsgMessageType.Middle); + }); + + e.Sender.CurrentMapInstance.Broadcast(x => + { + return x.GenerateSayPacket(_gameLanguage.GetLanguageFormat(GameDialogKey.ARENA_SHOUTMESSAGE_PVP_KILL, + x.UserLanguage, e.Sender.PlayerEntity.Name, playerKiller.Name), ChatMessageColorType.Yellow); + }); + + e.Sender.PlayerEntity.LifetimeStats.TotalArenaDeaths++; + playerKiller.LifetimeStats.TotalArenaKills++; + + if (e.Sender.PlayerEntity.IsInGroup()) + { + e.Sender.PlayerEntity.GetGroup().ArenaDeaths++; + foreach (IPlayerEntity member in e.Sender.PlayerEntity.GetGroup().Members) + { + if (member?.MapInstance is not { MapInstanceType: MapInstanceType.ArenaInstance }) + { + continue; + } + + member.Session.SendArenaStatistics(false); + } + } + else + { + e.Sender.SendArenaStatistics(false); + } + + if (playerKiller.IsInGroup()) + { + playerKiller.GetGroup().ArenaKills++; + foreach (IPlayerEntity member in playerKiller.GetGroup().Members) + { + if (member?.MapInstance is not { MapInstanceType: MapInstanceType.ArenaInstance }) + { + continue; + } + + member.Session.SendArenaStatistics(false); + } + } + else + { + playerKiller.Session.SendArenaStatistics(false); + } + } + } + + await e.Sender.PlayerEntity.RemoveBuffsOnDeathAsync(); + e.Sender.RefreshStat(); + + e.Sender.PlayerEntity.UpdateAskRevival(actualTime + _revivalConfiguration.RevivalDialogDelay, AskRevivalType.ArenaRevival); + e.Sender.PlayerEntity.UpdateRevival(actualTime + _revivalConfiguration.ForcedRevivalDelay, RevivalType.TryPayRevival, ForcedType.Forced); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Revival/RevivalStartProcedureEventBaseHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Revival/RevivalStartProcedureEventBaseHandler.cs new file mode 100644 index 0000000..4969f16 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Revival/RevivalStartProcedureEventBaseHandler.cs @@ -0,0 +1,92 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Revival; + +namespace WingsEmu.Plugins.BasicImplementations.Revival; + +public class RevivalStartProcedureEventBaseHandler : IAsyncEventProcessor +{ + private readonly IGameLanguageService _languageService; + private readonly GameMinMaxConfiguration _minMaxConfiguration; + private readonly IRankingManager _rankingManager; + private readonly IReputationConfiguration _reputationConfiguration; + private readonly PlayerRevivalConfiguration _revivalConfiguration; + + public RevivalStartProcedureEventBaseHandler(GameMinMaxConfiguration minMaxConfiguration, IGameLanguageService languageService, GameRevivalConfiguration gameRevivalConfiguration, + IReputationConfiguration reputationConfiguration, IRankingManager rankingManager) + { + _minMaxConfiguration = minMaxConfiguration; + _languageService = languageService; + _reputationConfiguration = reputationConfiguration; + _rankingManager = rankingManager; + _revivalConfiguration = gameRevivalConfiguration.PlayerRevivalConfiguration; + } + + public async Task HandleAsync(RevivalStartProcedureEvent e, CancellationToken cancellation) + { + if (e.Sender.PlayerEntity.IsAlive()) + { + return; + } + + if (!e.Sender.CurrentMapInstance.HasMapFlag(MapFlags.IS_BASE_MAP)) + { + return; + } + + if (e.Sender.CurrentMapInstance.HasMapFlag(MapFlags.ACT_4)) + { + return; + } + + await BaseMapDeathPenalization(e); + AskBaseMapRevival(e); + } + + private void AskBaseMapRevival(RevivalStartProcedureEvent e) + { + DateTime actualTime = DateTime.UtcNow; + + if (e.Sender.PlayerEntity.MapId == (int)MapIds.NOSVILLE) + { + e.Sender.PlayerEntity.UpdateRevival(actualTime + _revivalConfiguration.RevivalDialogDelay, RevivalType.DontPayRevival, ForcedType.Forced); + return; + } + + e.Sender.PlayerEntity.UpdateRevival(actualTime + _revivalConfiguration.ForcedRevivalDelay, RevivalType.DontPayRevival, ForcedType.Forced); + e.Sender.PlayerEntity.UpdateAskRevival(actualTime + _revivalConfiguration.RevivalDialogDelay, AskRevivalType.BasicRevival); + } + + private async Task BaseMapDeathPenalization(RevivalStartProcedureEvent e) + { + if (e.Sender.PlayerEntity.IsOnVehicle) + { + await e.Sender.EmitEventAsync(new RemoveVehicleEvent()); + } + + await e.Sender.PlayerEntity.RemoveBuffsOnDeathAsync(); + e.Sender.RefreshStat(); + + PlayerRevivalPenalization playerRevivalPenalization = _revivalConfiguration.PlayerRevivalPenalization; + if (e.Sender.PlayerEntity.Level <= playerRevivalPenalization.MaxLevelWithoutRevivalPenalization) + { + return; + } + + int amount = e.Sender.PlayerEntity.Level < playerRevivalPenalization.MaxLevelWithDignityPenalizationIncrement + ? e.Sender.PlayerEntity.Level * playerRevivalPenalization.DignityPenalizationIncrementMultiplier + : playerRevivalPenalization.MaxLevelWithDignityPenalizationIncrement * playerRevivalPenalization.DignityPenalizationIncrementMultiplier; + + await e.Sender.PlayerEntity.RemoveDignity(amount, _minMaxConfiguration, _languageService, _reputationConfiguration, _rankingManager.TopReputation); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Revival/RevivalStartProcedureEventNormalHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Revival/RevivalStartProcedureEventNormalHandler.cs new file mode 100644 index 0000000..b17065e --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Revival/RevivalStartProcedureEventNormalHandler.cs @@ -0,0 +1,44 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Revival; + +namespace WingsEmu.Plugins.BasicImplementations.Revival; + +public class RevivalStartProcedureEventNormalHandler : IAsyncEventProcessor +{ + private readonly PlayerRevivalConfiguration _revivalConfiguration; + + public RevivalStartProcedureEventNormalHandler(GameRevivalConfiguration gameRevivalConfiguration) => _revivalConfiguration = gameRevivalConfiguration.PlayerRevivalConfiguration; + + public async Task HandleAsync(RevivalStartProcedureEvent e, CancellationToken cancellation) + { + if (e.Sender.PlayerEntity.MapInstance.MapInstanceType != MapInstanceType.NormalInstance + && e.Sender.PlayerEntity.MapInstance.MapInstanceType != MapInstanceType.EventGameInstance + && e.Sender.PlayerEntity.MapInstance.MapInstanceType != MapInstanceType.Miniland) + { + return; + } + + if (e.Sender.CurrentMapInstance.MapInstanceType is MapInstanceType.Act4Instance or MapInstanceType.Act4Dungeon) + { + return; + } + + if (e.Sender.PlayerEntity.IsOnVehicle) + { + await e.Sender.EmitEventAsync(new RemoveVehicleEvent()); + } + + await e.Sender.PlayerEntity.RemoveBuffsOnDeathAsync(); + e.Sender.RefreshStat(); + + e.Sender.PlayerEntity.UpdateRevival(DateTime.UtcNow + _revivalConfiguration.RevivalDialogDelay, RevivalType.DontPayRevival, ForcedType.Forced); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/DropManager.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/DropManager.cs new file mode 100644 index 0000000..4652f27 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/DropManager.cs @@ -0,0 +1,98 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using PhoenixLib.Caching; +using WingsAPI.Data.Drops; +using WingsEmu.Game._enum; +using WingsEmu.Game.Managers.ServerData; +using WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects; +using WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects.Drops; + +namespace WingsEmu.Plugins.BasicImplementations.ServerConfigs; + +public class DropManager : IDropManager +{ + private static readonly List EmptyList = new(); + private readonly IKeyValueCache> _dropCache; + private readonly IEnumerable _dropConfigurations; + + private readonly List _generalDrops = new(); + + public DropManager(IEnumerable dropConfigurations, IKeyValueCache> dropCache) + { + _dropConfigurations = dropConfigurations; + _dropCache = dropCache; + } + + public async Task InitializeAsync() + { + var drops = new List(); + + foreach (DropImportFile dropImportExportFile in _dropConfigurations) + { + drops.AddRange(dropImportExportFile.Drops.SelectMany(s => s.ToDto())); + } + + foreach (DropDTO drop in drops) + { + if (drop.MonsterVNum != null) + { + string key = $"monsterVnum-{drop.MonsterVNum.Value}"; + List list = _dropCache.Get(key); + if (list == null) + { + list = new List { drop }; + _dropCache.Set(key, list); + continue; + } + + list.Add(drop); + continue; + } + + if (drop.MapId != null) + { + string key = $"mapId-{drop.MapId.Value}"; + List list = _dropCache.Get(key); + if (list == null) + { + list = new List { drop }; + _dropCache.Set(key, list); + continue; + } + + list.Add(drop); + continue; + } + + if (drop.RaceType != null && drop.RaceSubType != null) + { + string key = $"race-{drop.RaceType.Value}.{drop.RaceSubType.Value}"; + List list = _dropCache.Get(key); + if (list == null) + { + list = new List { drop }; + _dropCache.Set(key, list); + continue; + } + + list.Add(drop); + continue; + } + + _generalDrops.Add(drop); + } + } + + public IEnumerable GetGeneralDrops() => _generalDrops; + + public IReadOnlyList GetDropsByMapId(int mapId) => _dropCache.Get($"mapId-{mapId}") ?? EmptyList; + + public IReadOnlyList GetDropsByMonsterVnum(int monsterVnum) => _dropCache.Get($"monsterVnum-{monsterVnum}") ?? EmptyList; + + public IReadOnlyList GetDropsByMonsterRace(MonsterRaceType monsterRaceType, byte monsterSubRaceType) => _dropCache.Get($"race-{(byte)monsterRaceType}.{monsterSubRaceType}") ?? EmptyList; +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Drops/DropImportFile.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Drops/DropImportFile.cs new file mode 100644 index 0000000..e9d5b83 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Drops/DropImportFile.cs @@ -0,0 +1,15 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects.Files; +using YamlDotNet.Serialization; + +namespace WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects.Drops; + +public class DropImportFile : IFileData +{ + [YamlMember(Alias = "drops", ApplyNamingConventions = true)] + public List Drops { get; set; } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Drops/DropObject.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Drops/DropObject.cs new file mode 100644 index 0000000..eea00e7 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Drops/DropObject.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using YamlDotNet.Serialization; + +namespace WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects.Drops; + +public class DropObject +{ + [YamlMember(Alias = "chance", ApplyNamingConventions = true)] + public int Chance { get; set; } + + [YamlMember(Alias = "itemVnum", ApplyNamingConventions = true)] + public int ItemVNum { get; set; } + + [YamlMember(Alias = "quantity", ApplyNamingConventions = true)] + public int Quantity { get; set; } + + [YamlMember(Alias = "races", ApplyNamingConventions = true)] + public List Races { get; set; } + + [YamlMember(Alias = "mapIds", ApplyNamingConventions = true)] + public int[] MapIds { get; set; } + + [YamlMember(Alias = "monsterVnums", ApplyNamingConventions = true)] + public int[] MonsterVnums { get; set; } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Files/IExportableFile.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Files/IExportableFile.cs new file mode 100644 index 0000000..7b9185c --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Files/IExportableFile.cs @@ -0,0 +1,7 @@ +namespace WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects.Files; + +public interface IExportableFile where T : IFileData +{ + string FileName { get; } + T Data { get; } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Files/IFileData.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Files/IFileData.cs new file mode 100644 index 0000000..6aaaa32 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Files/IFileData.cs @@ -0,0 +1,5 @@ +namespace WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects.Files; + +public interface IFileData +{ +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/ImportObjectExtensions.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/ImportObjectExtensions.cs new file mode 100644 index 0000000..4bb6559 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/ImportObjectExtensions.cs @@ -0,0 +1,265 @@ +using System.Collections.Generic; +using System.Linq; +using WingsAPI.Data.Drops; +using WingsAPI.Data.Shops; +using WingsEmu.DTOs.Maps; +using WingsEmu.DTOs.Recipes; +using WingsEmu.DTOs.ServerDatas; +using WingsEmu.DTOs.Shops; +using WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects.Drops; +using WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects.ItemBoxes; +using WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects.Maps; +using WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects.Monsters; +using WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects.Npcs; +using WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects.Portals; +using WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects.Recipes; +using WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects.Teleporters; + +namespace WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects; + +public static class ImportObjectExtensions +{ + public static List ToDto(this DropObject obj) + { + var drops = new List(); + + if (obj.MonsterVnums != null) + { + drops.AddRange(obj.MonsterVnums.Select(vnum => new DropDTO + { + Amount = obj.Quantity, + DropChance = obj.Chance, + ItemVNum = obj.ItemVNum, + MonsterVNum = vnum + })); + } + + if (obj.MapIds != null) + { + drops.AddRange(obj.MapIds.Select(mapId => new DropDTO + { + Amount = obj.Quantity, + DropChance = obj.Chance, + ItemVNum = obj.ItemVNum, + MapId = mapId + })); + } + + if (obj.Races != null) + { + drops.AddRange(obj.Races.Select(raceDrop => new DropDTO + { + Amount = obj.Quantity, + DropChance = obj.Chance, + ItemVNum = obj.ItemVNum, + RaceType = raceDrop[0], + RaceSubType = raceDrop[1] + })); + } + + if (obj.MonsterVnums?.Any() != false || obj.MapIds?.Any() != false || obj.Races?.Any() != false) + { + return drops; + } + + drops.Add(new DropDTO + { + Amount = obj.Quantity, + DropChance = obj.Chance, + ItemVNum = obj.ItemVNum + }); + + return drops; + } + + public static PortalDTO ToDto(this PortalObject portal) => + new PortalDTO + { + Type = portal.Type, + DestinationMapId = portal.DestinationMapId, + DestinationX = portal.DestinationX, + DestinationY = portal.DestinationY, + IsDisabled = portal.IsDisabled, + SourceX = portal.SourceX, + SourceY = portal.SourceY, + SourceMapId = portal.SourceMapId, + RaidType = portal.RaidType, + MapNameId = portal.MapNameId + }; + + public static ServerMapDto ToDto(this ConfiguredMapObject toDto) => + new ServerMapDto + { + Id = toDto.MapId, + AmbientId = toDto.AmbientId, + MapVnum = toDto.MapVnum, + MusicId = toDto.MusicId, + NameId = toDto.NameId, + Flags = toDto.Flags ?? new List() + }; + + public static ItemBoxDto ToDtos(this RandomBoxObject obj) + { + if (obj.Categories.Count < 0) + { + return null; + } + + var list = new List(); + + int i = 0; + + foreach (RandomBoxCategory category in obj.Categories) + { + if (category.Items == null || !category.Items.Any()) + { + continue; + } + + i++; + + foreach (RandomBoxItem item in category.Items) + { + list.Add(new ItemBoxItemDto + { + ItemGeneratedAmount = (short)item.Quantity, + ItemGeneratedUpgrade = item.Upgrade, + ItemGeneratedVNum = (short)item.ItemVnum, + ItemGeneratedRandomRarity = item.RandomRarity, + MaximumOriginalItemRare = item.MaximumRandomRarity, + MinimumOriginalItemRare = item.MinimumRandomRarity, + Probability = (short)category.Chance + }); + } + } + + return new ItemBoxDto + { + Id = obj.ItemVnum, + MinimumRewards = obj.MinimumRewards, + MaximumRewards = obj.MaximumRewards, + ItemBoxType = ItemBoxType.RANDOM_PICK, + ShowsRaidBoxPanelOnOpen = obj.HideRewardInfo == false, + Items = list + }; + } + + public static ItemBoxDto ToDto(this ItemBoxImportFile obj) + { + var list = new List(); + + if (obj.Items.Count == 0) + { + return null; + } + + foreach (RandomBoxItem item in obj.Items) + { + list.Add(new ItemBoxItemDto + { + ItemGeneratedAmount = (short)item.Quantity, + ItemGeneratedUpgrade = item.Upgrade, + ItemGeneratedVNum = (short)item.ItemVnum, + ItemGeneratedRandomRarity = item.RandomRarity, + MaximumOriginalItemRare = item.MaximumRandomRarity, + MinimumOriginalItemRare = item.MinimumRandomRarity + }); + } + + return new ItemBoxDto + { + Id = obj.ItemVnum, + MinimumRewards = obj.MinimumRewards, + MaximumRewards = obj.MaximumRewards, + ItemBoxType = ItemBoxType.BUNDLE, + ShowsRaidBoxPanelOnOpen = obj.ShowRaidBoxModalOnOpen, + Items = list + }; + } + + public static ShopSkillDTO ToDto(this MapNpcShopSkillObject obj, byte tabId, short position) => new() + { + SkillVNum = obj.SkillVnum, + Type = tabId, + Slot = position + }; + + + public static RecipeDTO ToDto(this RecipeObject obj) => new() + { + ProducedItemVnum = obj.ItemVnum, + Amount = obj.Quantity, + ProducerNpcVnum = obj.ProducerNpcVnum, + ProducerMapNpcId = obj.ProducerMapNpcId, + ProducerItemVnum = obj.ProducerItemVnum + }; + + public static RecipeItemDTO ToDto(this RecipeItemObject obj, short slot) => new() + { + ItemVNum = obj.ItemVnum, + Slot = slot, + Amount = obj.Quantity + }; + + public static ShopItemDTO ToDto(this MapNpcShopItemObject obj, byte shopType, short position) => + new() + { + Type = shopType, + ItemVNum = (short)obj.ItemVnum, + Rare = obj.Rarity, + Slot = position, + Upgrade = obj.Upgrade, + Price = obj.Price, + Color = obj.Design + }; + + public static ShopDTO ToDto(this MapNpcShopObject shop) => + new() + { + Name = shop.Name, + MenuType = shop.MenuType, + ShopType = shop.ShopType + }; + + public static MapNpcDTO ToDto(this MapNpcObject obj) => + new() + { + Id = obj.MapNpcId, + NpcVNum = (short)obj.NpcMonsterVnum, + MapX = obj.PosX, + MapY = obj.PosY, + MapId = obj.MapId, + Dialog = (short)obj.DialogId, + QuestDialog = obj.QuestDialog, + Effect = (short)obj.Effect, + EffectDelay = (short)obj.EffectDelay, + Direction = obj.Direction, + IsMoving = obj.IsMoving, + IsSitting = obj.IsSitting, + IsDisabled = obj.IsDisabled, + CanAttack = obj.CanAttack, + HasGodMode = obj.HasGodMode, + CustomName = obj.CustomName + }; + + + public static TeleporterDTO ToDto(this TeleporterObject teleporterObject) => new() + { + Index = teleporterObject.Index, + Type = teleporterObject.Type, + MapId = teleporterObject.MapId, + MapNpcId = teleporterObject.MapNpcId, + MapX = teleporterObject.MapX, + MapY = teleporterObject.MapY + }; + + public static MapMonsterDTO ToDto(this MapMonsterObject obj) => new() + { + MapId = obj.MapId, + MonsterVNum = obj.MonsterVNum, + Direction = obj.Position, + MapX = obj.MapX, + MapY = obj.MapY, + IsMoving = obj.IsMoving + }; +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/ItemBoxes/ItemBoxImportFile.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/ItemBoxes/ItemBoxImportFile.cs new file mode 100644 index 0000000..a0d53e1 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/ItemBoxes/ItemBoxImportFile.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects.Files; +using YamlDotNet.Serialization; + +namespace WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects.ItemBoxes; + +public class ItemBoxImportFile : IFileData +{ + [YamlMember(Alias = "item_vnum", ApplyNamingConventions = true)] + public int ItemVnum { get; set; } + + [YamlMember(Alias = "minimum_rewards", ApplyNamingConventions = true)] + public int? MinimumRewards { get; set; } + + [YamlMember(Alias = "maximum_rewards", ApplyNamingConventions = true)] + public int? MaximumRewards { get; set; } + + [YamlMember(Alias = "show_raidbox_modal_on_open", ApplyNamingConventions = true)] + public bool ShowRaidBoxModalOnOpen { get; set; } + + [YamlMember(Alias = "items", ApplyNamingConventions = true)] + public List Items { get; set; } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/ItemBoxes/RandomBoxCategory.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/ItemBoxes/RandomBoxCategory.cs new file mode 100644 index 0000000..109dd96 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/ItemBoxes/RandomBoxCategory.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using YamlDotNet.Serialization; + +namespace WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects.ItemBoxes; + +public class RandomBoxCategory +{ + [YamlMember(Alias = "chance", ApplyNamingConventions = true)] + public ushort Chance { get; set; } + + [YamlMember(Alias = "items", ApplyNamingConventions = true)] + public List Items { get; set; } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/ItemBoxes/RandomBoxImportFile.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/ItemBoxes/RandomBoxImportFile.cs new file mode 100644 index 0000000..a408f22 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/ItemBoxes/RandomBoxImportFile.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects.Files; +using YamlDotNet.Serialization; + +namespace WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects.ItemBoxes; + +public class RandomBoxImportFile : IFileData +{ + [YamlMember(Alias = "randomBoxes", ApplyNamingConventions = true)] + public List Items { get; set; } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/ItemBoxes/RandomBoxItem.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/ItemBoxes/RandomBoxItem.cs new file mode 100644 index 0000000..6e108dd --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/ItemBoxes/RandomBoxItem.cs @@ -0,0 +1,30 @@ +using YamlDotNet.Serialization; + +namespace WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects.ItemBoxes; + +public class RandomBoxItem +{ + [YamlMember(Alias = "itemVnum", ApplyNamingConventions = true)] + public int ItemVnum { get; set; } + + [YamlMember(Alias = "quantity", ApplyNamingConventions = true)] + public ushort Quantity { get; set; } + + [YamlMember(Alias = "upgrade", ApplyNamingConventions = true)] + public byte Upgrade { get; set; } + + [YamlMember(Alias = "randomRarity", ApplyNamingConventions = true)] + public bool RandomRarity { get; set; } + + [YamlMember(Alias = "isSuperReward", ApplyNamingConventions = true)] + public bool IsSuperReward { get; set; } + + [YamlMember(Alias = "minimumRandomRarity", ApplyNamingConventions = true)] + public short MinimumRandomRarity { get; set; } + + [YamlMember(Alias = "maximumRandomRarity", ApplyNamingConventions = true)] + public short MaximumRandomRarity { get; set; } + + [YamlMember(Alias = "message", ApplyNamingConventions = true)] + public string Message { get; set; } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/ItemBoxes/RandomBoxObject.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/ItemBoxes/RandomBoxObject.cs new file mode 100644 index 0000000..b074a99 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/ItemBoxes/RandomBoxObject.cs @@ -0,0 +1,26 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using YamlDotNet.Serialization; + +namespace WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects.ItemBoxes; + +public class RandomBoxObject +{ + [YamlMember(Alias = "itemVnum", ApplyNamingConventions = true)] + public int ItemVnum { get; set; } + + [YamlMember(Alias = "minimum_rewards", ApplyNamingConventions = true)] + public int? MinimumRewards { get; set; } + + [YamlMember(Alias = "maximum_rewards", ApplyNamingConventions = true)] + public int? MaximumRewards { get; set; } + + [YamlMember(Alias = "hideRewardInfo", ApplyNamingConventions = true)] + public bool HideRewardInfo { get; set; } + + [YamlMember(Alias = "categories", ApplyNamingConventions = true)] + public List Categories { get; set; } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Maps/ConfiguredMapImportFile.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Maps/ConfiguredMapImportFile.cs new file mode 100644 index 0000000..b50d85a --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Maps/ConfiguredMapImportFile.cs @@ -0,0 +1,12 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects.Files; + +namespace WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects.Maps; + +public class ConfiguredMapImportFile : List, IFileData +{ +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Maps/ConfiguredMapObject.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Maps/ConfiguredMapObject.cs new file mode 100644 index 0000000..28e38b6 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Maps/ConfiguredMapObject.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using WingsEmu.DTOs.Maps; +using YamlDotNet.Serialization; + +namespace WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects.Maps; + +public class ConfiguredMapObject +{ + [YamlMember(Alias = "map_id", ApplyNamingConventions = true)] + public int MapId { get; set; } + + [YamlMember(Alias = "map_vnum", ApplyNamingConventions = true)] + public int MapVnum { get; set; } + + [YamlMember(Alias = "map_name_id", ApplyNamingConventions = true)] + public int NameId { get; set; } + + [YamlMember(Alias = "map_music_id", ApplyNamingConventions = true)] + public int MusicId { get; set; } + + [YamlMember(Alias = "map_ambient_id", ApplyNamingConventions = true)] + public int AmbientId { get; set; } + + [YamlMember(Alias = "flags", ApplyNamingConventions = true)] + public List Flags { get; set; } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Monsters/MapMonsterImportFile.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Monsters/MapMonsterImportFile.cs new file mode 100644 index 0000000..247a7f3 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Monsters/MapMonsterImportFile.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects.Files; +using YamlDotNet.Serialization; + +namespace WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects.Monsters; + +public class MapMonsterImportFile : IFileData +{ + [YamlMember(Alias = "mapId", ApplyNamingConventions = true)] + public int MapId { get; set; } + + [YamlMember(Alias = "monsters", ApplyNamingConventions = true)] + public List Monsters { get; set; } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Monsters/MapMonsterObject.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Monsters/MapMonsterObject.cs new file mode 100644 index 0000000..60abc47 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Monsters/MapMonsterObject.cs @@ -0,0 +1,30 @@ +using YamlDotNet.Serialization; + +namespace WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects.Monsters; + +public class MapMonsterObject +{ + [YamlIgnore] + public int MapId { get; set; } + + [YamlMember(Alias = "mapMonsterId", ApplyNamingConventions = true)] + public int MapMonsterId { get; set; } + + [YamlMember(Alias = "vnum", ApplyNamingConventions = true)] + public int MonsterVNum { get; set; } + + [YamlMember(Alias = "mapX", ApplyNamingConventions = true)] + public short MapX { get; set; } + + [YamlMember(Alias = "mapY", ApplyNamingConventions = true)] + public short MapY { get; set; } + + [YamlMember(Alias = "position", ApplyNamingConventions = true)] + public byte Position { get; set; } = 2; + + [YamlMember(Alias = "canMove", ApplyNamingConventions = true)] + public bool IsMoving { get; set; } = true; + + [YamlMember(Alias = "isDisabled", ApplyNamingConventions = true)] + public bool IsDisabled { get; set; } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Npcs/MapNpcImportFile.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Npcs/MapNpcImportFile.cs new file mode 100644 index 0000000..88f88b2 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Npcs/MapNpcImportFile.cs @@ -0,0 +1,18 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects.Files; +using YamlDotNet.Serialization; + +namespace WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects.Npcs; + +public class MapNpcImportFile : IFileData +{ + [YamlMember(Alias = "mapId", ApplyNamingConventions = true)] + public int MapId { get; set; } + + [YamlMember(Alias = "npcs", ApplyNamingConventions = true)] + public List Npcs { get; set; } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Npcs/MapNpcObject.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Npcs/MapNpcObject.cs new file mode 100644 index 0000000..1dc2923 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Npcs/MapNpcObject.cs @@ -0,0 +1,60 @@ +using YamlDotNet.Serialization; + +namespace WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects.Npcs; + +public class MapNpcObject +{ + [YamlMember(Alias = "mapNpcId", ApplyNamingConventions = true)] + public int MapNpcId { get; set; } + + [YamlMember(Alias = "vnum", ApplyNamingConventions = true)] + public int NpcMonsterVnum { get; set; } + + [YamlMember(Alias = "posX", ApplyNamingConventions = true)] + public short PosX { get; set; } + + [YamlMember(Alias = "posY", ApplyNamingConventions = true)] + public short PosY { get; set; } + + [YamlMember(Alias = "canAttack", ApplyNamingConventions = true)] + public bool CanAttack { get; set; } + + [YamlMember(Alias = "hasGodMode", ApplyNamingConventions = true)] + public bool HasGodMode { get; set; } = true; + + [YamlIgnore] + public int MapId { get; set; } + + [YamlMember(Alias = "effectVnum", ApplyNamingConventions = true)] + public int Effect { get; set; } + + [YamlMember(Alias = "effectDelay", ApplyNamingConventions = true)] + public int EffectDelay { get; set; } + + [YamlMember(Alias = "dialogId", ApplyNamingConventions = true)] + public int DialogId { get; set; } + + [YamlMember(Alias = "questDialogId", ApplyNamingConventions = true)] + public int? QuestDialog { get; set; } + + [YamlMember(Alias = "directionFacing", ApplyNamingConventions = true)] + public byte Direction { get; set; } + + [YamlMember(Alias = "canMove", ApplyNamingConventions = true)] + public bool IsMoving { get; set; } + + [YamlMember(Alias = "isDisabled", ApplyNamingConventions = true)] + public bool IsDisabled { get; set; } + + [YamlMember(Alias = "isSitting", ApplyNamingConventions = true)] + public bool IsSitting { get; set; } + + [YamlMember(Alias = "customName", ApplyNamingConventions = true)] + public string CustomName { get; set; } + + [YamlMember(Alias = "itemShop", ApplyNamingConventions = true)] + public MapNpcShopObject ItemShop { get; set; } + + [YamlMember(Alias = "skillShop", ApplyNamingConventions = true)] + public MapNpcShopObject SkillShop { get; set; } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Npcs/MapNpcShopItemObject.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Npcs/MapNpcShopItemObject.cs new file mode 100644 index 0000000..6fad4a3 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Npcs/MapNpcShopItemObject.cs @@ -0,0 +1,21 @@ +using YamlDotNet.Serialization; + +namespace WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects.Npcs; + +public class MapNpcShopItemObject +{ + [YamlMember(Alias = "itemVnum", ApplyNamingConventions = true)] + public int ItemVnum { get; set; } + + [YamlMember(Alias = "design", ApplyNamingConventions = true)] + public byte Design { get; set; } + + [YamlMember(Alias = "rarity", ApplyNamingConventions = true)] + public short Rarity { get; set; } + + [YamlMember(Alias = "upgrade", ApplyNamingConventions = true)] + public byte Upgrade { get; set; } + + [YamlMember(Alias = "price", ApplyNamingConventions = true)] + public int? Price { get; set; } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Npcs/MapNpcShopObject.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Npcs/MapNpcShopObject.cs new file mode 100644 index 0000000..18ec134 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Npcs/MapNpcShopObject.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using YamlDotNet.Serialization; + +namespace WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects.Npcs; + +public class MapNpcShopObject +{ + [YamlMember(Alias = "name", ApplyNamingConventions = true)] + public string Name { get; set; } + + [YamlMember(Alias = "menuType", ApplyNamingConventions = true)] + public byte MenuType { get; set; } + + [YamlMember(Alias = "shopType", ApplyNamingConventions = true)] + public byte ShopType { get; set; } + + [YamlMember(Alias = "tabs", ApplyNamingConventions = true)] + public List> ShopTabs { get; set; } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Npcs/MapNpcShopSkillObject.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Npcs/MapNpcShopSkillObject.cs new file mode 100644 index 0000000..484fedd --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Npcs/MapNpcShopSkillObject.cs @@ -0,0 +1,9 @@ +using YamlDotNet.Serialization; + +namespace WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects.Npcs; + +public class MapNpcShopSkillObject +{ + [YamlMember(Alias = "skillVnum", ApplyNamingConventions = true)] + public short SkillVnum { get; set; } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Npcs/MapNpcShopTabObject.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Npcs/MapNpcShopTabObject.cs new file mode 100644 index 0000000..99d7c1e --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Npcs/MapNpcShopTabObject.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using YamlDotNet.Serialization; + +namespace WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects.Npcs; + +public class MapNpcShopTabObject +{ + [YamlMember(Alias = "shopTabId", ApplyNamingConventions = true)] + public int ShopTabId { get; set; } + + [YamlMember(Alias = "items", ApplyNamingConventions = true)] + public List Items { get; set; } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Portals/PortalImportFile.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Portals/PortalImportFile.cs new file mode 100644 index 0000000..925ef81 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Portals/PortalImportFile.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects.Files; +using YamlDotNet.Serialization; + +namespace WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects.Portals; + +public class PortalImportFile : IFileData +{ + [YamlMember(Alias = "portals", ApplyNamingConventions = true)] + public List Portals { get; set; } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Portals/PortalObject.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Portals/PortalObject.cs new file mode 100644 index 0000000..a5fe363 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Portals/PortalObject.cs @@ -0,0 +1,40 @@ +// WingsEmu +// +// Developed by NosWings Team + +using YamlDotNet.Serialization; + +namespace WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects.Portals; + +public class PortalObject +{ + [YamlMember(Alias = "destinationMapId", ApplyNamingConventions = true)] + public int DestinationMapId { get; set; } + + [YamlMember(Alias = "destinationMapX", ApplyNamingConventions = true)] + public short DestinationX { get; set; } + + [YamlMember(Alias = "destinationMapY", ApplyNamingConventions = true)] + public short DestinationY { get; set; } + + [YamlMember(Alias = "isDisabled", ApplyNamingConventions = true)] + public bool IsDisabled { get; set; } + + [YamlMember(Alias = "sourceMapId", ApplyNamingConventions = true)] + public int SourceMapId { get; set; } + + [YamlMember(Alias = "sourceMapX", ApplyNamingConventions = true)] + public short SourceX { get; set; } + + [YamlMember(Alias = "sourceMapY", ApplyNamingConventions = true)] + public short SourceY { get; set; } + + [YamlMember(Alias = "type", ApplyNamingConventions = true)] + public short Type { get; set; } + + [YamlMember(Alias = "raidType", ApplyNamingConventions = true)] + public short? RaidType { get; set; } + + [YamlMember(Alias = "mapNameId", ApplyNamingConventions = true)] + public short? MapNameId { get; set; } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Recipes/RecipeImportFile.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Recipes/RecipeImportFile.cs new file mode 100644 index 0000000..0707d87 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Recipes/RecipeImportFile.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects.Files; +using YamlDotNet.Serialization; + +namespace WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects.Recipes; + +public class RecipeImportFile : IFileData +{ + [YamlMember(Alias = "recipes", ApplyNamingConventions = true)] + public List Recipes { get; set; } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Recipes/RecipeItemObject.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Recipes/RecipeItemObject.cs new file mode 100644 index 0000000..a7febb4 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Recipes/RecipeItemObject.cs @@ -0,0 +1,12 @@ +using YamlDotNet.Serialization; + +namespace WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects.Recipes; + +public class RecipeItemObject +{ + [YamlMember(Alias = "itemVnum", ApplyNamingConventions = true)] + public short ItemVnum { get; set; } + + [YamlMember(Alias = "quantity", ApplyNamingConventions = true)] + public short Quantity { get; set; } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Recipes/RecipeObject.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Recipes/RecipeObject.cs new file mode 100644 index 0000000..fb36014 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Recipes/RecipeObject.cs @@ -0,0 +1,29 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using YamlDotNet.Serialization; + +namespace WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects.Recipes; + +public class RecipeObject +{ + [YamlMember(Alias = "itemVnum", ApplyNamingConventions = true)] + public int ItemVnum { get; set; } + + [YamlMember(Alias = "quantity", ApplyNamingConventions = true)] + public int Quantity { get; set; } + + [YamlMember(Alias = "producerItemVnum", ApplyNamingConventions = true)] + public int? ProducerItemVnum { get; set; } + + [YamlMember(Alias = "producerNpcVnum", ApplyNamingConventions = true)] + public int? ProducerNpcVnum { get; set; } + + [YamlMember(Alias = "producerMapNpcId", ApplyNamingConventions = true)] + public int? ProducerMapNpcId { get; set; } + + [YamlMember(Alias = "items", ApplyNamingConventions = true)] + public List Items { get; set; } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Teleporters/TeleporterImportFile.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Teleporters/TeleporterImportFile.cs new file mode 100644 index 0000000..c6da86a --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Teleporters/TeleporterImportFile.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects.Files; +using YamlDotNet.Serialization; + +namespace WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects.Teleporters; + +public class TeleporterImportFile : IFileData +{ + [YamlMember(Alias = "mapId", ApplyNamingConventions = true)] + public int MapId { get; set; } + + [YamlMember(Alias = "teleporters", ApplyNamingConventions = true)] + public List Teleporters { get; set; } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Teleporters/TeleporterObject.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Teleporters/TeleporterObject.cs new file mode 100644 index 0000000..0a74bb3 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ImportObjects/Teleporters/TeleporterObject.cs @@ -0,0 +1,25 @@ +using WingsEmu.Packets.Enums; +using YamlDotNet.Serialization; + +namespace WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects.Teleporters; + +public class TeleporterObject +{ + [YamlMember(Alias = "index", ApplyNamingConventions = true)] + public short Index { get; set; } + + [YamlMember(Alias = "type", ApplyNamingConventions = true)] + public TeleporterType Type { get; set; } + + [YamlIgnore] + public int MapId { get; set; } + + [YamlMember(Alias = "mapNpcId", ApplyNamingConventions = true)] + public int MapNpcId { get; set; } + + [YamlMember(Alias = "mapX", ApplyNamingConventions = true)] + public short MapX { get; set; } + + [YamlMember(Alias = "mapY", ApplyNamingConventions = true)] + public short MapY { get; set; } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ItemBoxManager.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ItemBoxManager.cs new file mode 100644 index 0000000..bf4fd6b --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ItemBoxManager.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; +using PhoenixLib.Caching; +using PhoenixLib.Logging; +using WingsEmu.DTOs.ServerDatas; +using WingsEmu.Game.Items; +using WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects; +using WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects.ItemBoxes; + +namespace WingsEmu.Plugins.BasicImplementations.ServerConfigs; + +public class ItemBoxManager : IItemBoxManager +{ + private readonly IEnumerable _itemBoxConfigurations; + private readonly IKeyValueCache _itemBoxesCache; + private readonly IEnumerable _randomBoxConfigurations; + + public ItemBoxManager(IEnumerable itemBoxConfigurations, IEnumerable randomBoxConfigurations, IKeyValueCache itemBoxesCache) + { + _itemBoxConfigurations = itemBoxConfigurations; + _randomBoxConfigurations = randomBoxConfigurations; + _itemBoxesCache = itemBoxesCache; + } + + public ItemBoxDto GetItemBoxByItemVnumAndDesign(int itemVnum) => _itemBoxesCache.Get(itemVnum.ToString()); + + public void Initialize() + { + int boxesCount = 0; + foreach (ItemBoxImportFile file in _itemBoxConfigurations) + { + ItemBoxDto box = file.ToDto(); + if (box == null) + { + continue; + } + + // just the item box itself + _itemBoxesCache.Set(box.Id.ToString(), box); + boxesCount++; + } + + foreach (RandomBoxImportFile file in _randomBoxConfigurations) + { + foreach (RandomBoxObject obj in file.Items) + { + ItemBoxDto box = obj.ToDtos(); + if (box == null) + { + continue; + } + + _itemBoxesCache.Set(box.Id.ToString(), box); + boxesCount++; + } + } + + Log.Info($"[ITEMBOX_MANAGER] Loaded {boxesCount} itemBoxes"); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/MapManager.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/MapManager.cs new file mode 100644 index 0000000..c082595 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/MapManager.cs @@ -0,0 +1,319 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using PhoenixLib.Logging; +using WingsAPI.Communication.ServerApi.Protocol; +using WingsAPI.Data.GameData; +using WingsEmu.Core.Extensions; +using WingsEmu.Core.Generics; +using WingsEmu.DTOs.Maps; +using WingsEmu.DTOs.ServerDatas; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Entities.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Managers.ServerData; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Portals; +using WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects; +using WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects.Maps; +using WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects.Portals; + +namespace WingsEmu.Plugins.BasicImplementations.ServerConfigs; + +public class MapManager : IMapManager +{ + private readonly Dictionary _baseMapInstancesByMapId = new(); + private readonly SerializableGameServer _gameServer; + private readonly Dictionary _mapConfigByMapId = new(); + private readonly IEnumerable _mapConfigurations; + + private readonly Dictionary _mapDataByMapVNum = new(); + private readonly Dictionary> _mapFlags = new(); + private readonly IMapInstanceFactory _mapInstanceFactory; + private readonly ConcurrentDictionary> _mapInstancesByFlags = new(); + private readonly ConcurrentDictionary _mapInstancesByGuid = new(); + private readonly IResourceLoader _mapLoader; + private readonly IMapMonsterManager _mapMonsterManager; + private readonly IMapNpcManager _mapNpcManager; + private readonly IMonsterEntityFactory _monsterEntityFactory; + private readonly INpcEntityFactory _npcEntityFactory; + private readonly IEnumerable _portalConfigurationFiles; + private readonly Dictionary> _portalDataByMapId = new(); + private readonly ITimeSpaceConfiguration _timeSpaceConfiguration; + private readonly ITimeSpacePortalFactory _timeSpacePortalFactory; + + public MapManager(IEnumerable portalConfigurationFiles, IResourceLoader mapLoader, IEnumerable mapConfigurations, + SerializableGameServer gameServer, IMapInstanceFactory mapInstanceFactory, + IMonsterEntityFactory monsterEntityFactory, INpcEntityFactory npcEntityFactory, IMapMonsterManager mapMonsterManager, IMapNpcManager mapNpcManager, + ITimeSpaceConfiguration timeSpaceConfiguration, ITimeSpacePortalFactory timeSpacePortalFactory) + { + _portalConfigurationFiles = portalConfigurationFiles; + _mapLoader = mapLoader; + _mapConfigurations = mapConfigurations; + _gameServer = gameServer; + _mapInstanceFactory = mapInstanceFactory; + _monsterEntityFactory = monsterEntityFactory; + _npcEntityFactory = npcEntityFactory; + _mapMonsterManager = mapMonsterManager; + _mapNpcManager = mapNpcManager; + _timeSpaceConfiguration = timeSpaceConfiguration; + _timeSpacePortalFactory = timeSpacePortalFactory; + } + + public async Task Initialize() + { + int count = 0; + IEnumerable maps = await _mapLoader.LoadAsync(); + foreach (MapDataDTO map in maps) + { + _mapDataByMapVNum[map.Id] = map; + count++; + } + + Log.Info($"[MAP_MANAGER] Loaded {count.ToString()} MapClientData"); + + count = 0; + IEnumerable portals = _portalConfigurationFiles.SelectMany(s => s.Portals.Select(p => p.ToDto())).ToList(); + foreach (PortalDTO portal in portals) + { + if (!_portalDataByMapId.TryGetValue(portal.SourceMapId, out List portalDtos)) + { + portalDtos = _portalDataByMapId[portal.SourceMapId] = new List(); + } + + portalDtos.Add(portal); + count++; + } + + Log.Info($"[MAP_MANAGER] Loaded {count.ToString()} MapPortals"); + DateTime initTime = DateTime.UtcNow; + count = 0; + int countBaseMaps = 0; + var configuredMaps = _mapConfigurations.SelectMany(s => s.Select(p => p.ToDto())).ToList(); + foreach (ServerMapDto configuredMap in configuredMaps) + { + // Load MapFlags at the beginning + _mapFlags[configuredMap.Id] = configuredMap.Flags.ToHashSet(); + + if (_gameServer.ChannelType == GameChannelType.ACT_4 + ? !configuredMap.Flags.Contains(MapFlags.ACT_4) && !configuredMap.Flags.Contains(MapFlags.IS_MINILAND_MAP) + : configuredMap.Flags.Contains(MapFlags.ACT_4)) + { + continue; + } + + _mapConfigByMapId[configuredMap.Id] = configuredMap; + count++; + + if (!configuredMap.Flags.Contains(MapFlags.IS_BASE_MAP)) + { + continue; + } + + IMapInstance mapInstance = GenerateMapInstanceByMapId(configuredMap.Id, MapInstanceType.BaseMapInstance); + mapInstance.Initialize(initTime); + _baseMapInstancesByMapId[configuredMap.Id] = mapInstance; + countBaseMaps++; + } + + Log.Info($"[MAP_MANAGER] Loaded {count.ToString()} MapConfigurations"); + Log.Info($"[MAP_MANAGER] Instantiated {countBaseMaps.ToString()} BaseMaps"); + } + + public MapDataDTO GetMapByMapId(int mapId) => _mapDataByMapVNum.GetOrDefault(mapId); + + public IMapInstance GenerateMapInstanceByMapId(int mapId, MapInstanceType type) + { + if (!_mapConfigByMapId.TryGetValue(mapId, out ServerMapDto mapConfiguration)) + { + Log.Warn($"[MAP_MANAGER] Couldn't find a ServerMapDto/MapConfiguration while trying to generate a MapInstance with MapId: '{mapId.ToString()}'"); + return null; + } + + if (!_mapDataByMapVNum.TryGetValue(mapConfiguration.MapVnum, out MapDataDTO map)) + { + Log.Warn($"[MAP_MANAGER] Couldn't find a MapDataDto while trying to generate a MapInstance with MapVNum: '{mapConfiguration.MapVnum.ToString()}'"); + return null; + } + + IMapInstance mapInstance = _mapInstanceFactory.CreateMap(new Map + { + Flags = mapConfiguration.Flags, + Grid = map.Grid, + Width = map.Width, + Height = map.Height, + MapId = mapConfiguration.Id, + MapVnum = map.Id, + MapNameId = mapConfiguration.NameId, + Music = mapConfiguration.MusicId + }, type); + + if (_portalDataByMapId.TryGetValue(mapId, out List portals)) + { + mapInstance.LoadPortals(portals); + } + + IEnumerable timeSpacePortals = _timeSpaceConfiguration.GetTimeSpaceConfigurationsByMapId(mapId); + if (timeSpacePortals != null) + { + foreach (TimeSpaceFileConfiguration timeSpacePortal in timeSpacePortals) + { + foreach (TimeSpacePlacement placement in timeSpacePortal.Placement.Where(x => x.MapId == mapId)) + { + ITimeSpacePortalEntity portal = _timeSpacePortalFactory.CreateTimeSpacePortal(timeSpacePortal, new Position(placement.MapX, placement.MapY)); + mapInstance.TimeSpacePortals.Add(portal); + } + } + } + + IEnumerable monsters = _mapMonsterManager.GetByMapId(mapId); + if (monsters != null) + { + foreach (MapMonsterDTO monster in monsters) + { + try + { + IMonsterEntity mapMonster = _monsterEntityFactory.CreateMapMonster(monster, mapInstance); + mapMonster.EmitEvent(new MapJoinMonsterEntityEvent(mapMonster)); + } + catch + { + Log.Warn("[MOB] Couldn't load monster: " + monster.MonsterVNum + $" on map: {mapId}"); + } + } + } + + IEnumerable npcs = _mapNpcManager.GetByMapId(mapId); + if (npcs != null) + { + foreach (MapNpcDTO npc in npcs) + { + try + { + INpcEntity npcEntity = _npcEntityFactory.CreateMapNpc(npc, mapInstance); + npcEntity.EmitEvent(new MapJoinNpcEntityEvent(npcEntity)); + } + catch + { + Log.Warn("[NPC] Couldn't load NPC: " + npc.NpcVNum + $" on map: {mapId}"); + } + } + } + + _mapInstancesByGuid.TryAdd(mapInstance.Id, mapInstance); + foreach (MapFlags flag in mapConfiguration.Flags) + { + _mapInstancesByFlags.GetOrAdd(flag, new ThreadSafeList()).Add(mapInstance); + } + + return mapInstance; + } + + public IMapInstance GenerateMapInstanceByMapVNum(ServerMapDto serverMapDto, MapInstanceType type) + { + if (!_mapDataByMapVNum.TryGetValue(serverMapDto.MapVnum, out MapDataDTO map)) + { + Log.Warn($"[MAP_MANAGER] Couldn't find a MapDataDto while trying to generate a MapInstance with MapVNum: '{serverMapDto.MapVnum.ToString()}'"); + return null; + } + + IMapInstance mapInstance = _mapInstanceFactory.CreateMap(new Map + { + Flags = serverMapDto.Flags, + Grid = map.Grid, + Width = map.Width, + Height = map.Height, + MapId = serverMapDto.Id, + MapVnum = map.Id, + MapNameId = serverMapDto.NameId, + Music = serverMapDto.MusicId + }, type); + + _mapInstancesByGuid.TryAdd(mapInstance.Id, mapInstance); + foreach (MapFlags flag in serverMapDto.Flags) + { + _mapInstancesByFlags.GetOrAdd(flag, new ThreadSafeList()).Add(mapInstance); + } + + return mapInstance; + } + + public async Task TeleportOnRandomPlaceInMapAsync(IClientSession session, IMapInstance mapInstance, bool isSameMap = false) + { + if (mapInstance == default) + { + return; + } + + Position pos = mapInstance.GetRandomPosition(); + + switch (isSameMap) + { + case false: + session.ChangeMap(mapInstance, pos.X, pos.Y); + break; + case true: + session.PlayerEntity.TeleportOnMap(pos.X, pos.Y); + break; + } + } + + public IEnumerable GetMapsWithFlag(MapFlags flags) + { + if (!_mapInstancesByFlags.TryGetValue(flags, out ThreadSafeList maps) || maps == null) + { + return Array.Empty(); + } + + return maps; + } + + public IMapInstance GetMapInstance(Guid id) => _mapInstancesByGuid.GetOrDefault(id); + + public IMapInstance GetBaseMapInstanceByMapId(int mapId) => _baseMapInstancesByMapId.GetOrDefault(mapId); + + public Guid GetBaseMapInstanceIdByMapId(int mapId) => _baseMapInstancesByMapId.TryGetValue(mapId, out IMapInstance map) ? map.Id : Guid.Empty; + + public bool HasMapFlagByMapId(int mapId, MapFlags mapFlag) + { + if (!_mapFlags.TryGetValue(mapId, out HashSet mapFlags)) + { + return false; + } + + return mapFlags != null && mapFlags.Contains(mapFlag); + } + + public IReadOnlyList GetMapFlagByMapId(int mapId) => _mapFlags.TryGetValue(mapId, out HashSet mapFlags) ? mapFlags.ToList() : Array.Empty(); + + public void RemoveMapInstance(Guid mapId) + { + if (!_mapInstancesByGuid.TryRemove(mapId, out IMapInstance map)) + { + return; + } + + if (map.HasMapFlag(MapFlags.IS_BASE_MAP)) + { + _baseMapInstancesByMapId.Remove(map.MapId); + } + + if (_mapConfigByMapId.TryGetValue(map.MapId, out ServerMapDto mapConfiguration)) + { + foreach (MapFlags flag in mapConfiguration.Flags) + { + if (_mapInstancesByFlags.TryGetValue(flag, out ThreadSafeList list) && list != null) + { + list.Remove(map); + } + } + } + + map.Destroy(); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/MapMonsterManager.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/MapMonsterManager.cs new file mode 100644 index 0000000..9967656 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/MapMonsterManager.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using PhoenixLib.Caching; +using PhoenixLib.Logging; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game.Managers.ServerData; +using WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects; +using WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects.Monsters; + +namespace WingsEmu.Plugins.BasicImplementations.ServerConfigs; + +public class MapMonsterManager : IMapMonsterManager +{ + private readonly IEnumerable _files; + private readonly ILongKeyCachedRepository _mapMonsterById; + private readonly IKeyValueCache> _mapMonsters; + + public MapMonsterManager(IEnumerable files, ILongKeyCachedRepository mapMonsterById, IKeyValueCache> mapMonsters) + { + _files = files; + _mapMonsterById = mapMonsterById; + _mapMonsters = mapMonsters; + } + + public async Task InitializeAsync() + { + var monsters = _files.SelectMany(x => x.Monsters.Select(s => + { + s.MapId = x.MapId; + return s.ToDto(); + })).ToList(); + + int count = 0; + foreach (MapMonsterDTO npcDto in monsters) + { + _mapMonsterById.Set(npcDto.Id, npcDto); + _mapMonsters.GetOrSet($"by-map-id-{npcDto.MapId.ToString()}", () => new List()).Add(npcDto); + _mapMonsters.GetOrSet($"by-monster-vnum-{npcDto.MonsterVNum.ToString()}", () => new List()).Add(npcDto); + count++; + } + + Log.Info($"[DATABASE] Loaded {count.ToString()} MapMonsters"); + } + + public MapMonsterDTO GetById(int mapNpcId) => _mapMonsterById.Get(mapNpcId); + + public IReadOnlyList GetByMapId(int mapId) => _mapMonsters.Get($"by-map-id-{mapId.ToString()}"); + + public IReadOnlyList GetMapMonstersPerVNum(int npcMonsterVnum) => _mapMonsters.Get($"by-monster-vnum-{npcMonsterVnum.ToString()}"); +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/MapNpcManager.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/MapNpcManager.cs new file mode 100644 index 0000000..b360146 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/MapNpcManager.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using PhoenixLib.Caching; +using PhoenixLib.Logging; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game.Managers.ServerData; +using WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects; +using WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects.Npcs; + +namespace WingsEmu.Plugins.BasicImplementations.ServerConfigs; + +public class MapNpcManager : IMapNpcManager +{ + private readonly ILongKeyCachedRepository _mapNpcById; + private readonly IEnumerable _mapNpcConfigurations; + private readonly IKeyValueCache> _mapNpcs; + + public MapNpcManager(IEnumerable mapNpcConfigurations, ILongKeyCachedRepository mapNpcById, IKeyValueCache> mapNpcs) + { + _mapNpcConfigurations = mapNpcConfigurations; + _mapNpcById = mapNpcById; + _mapNpcs = mapNpcs; + } + + public async Task InitializeAsync() + { + IEnumerable importedNpcs = _mapNpcConfigurations.SelectMany(x => x.Npcs.Select(s => + { + s.MapId = x.MapId; + return s; + })); + + var npcs = importedNpcs.Select(s => s.ToDto()).ToList(); + + int count = 0; + foreach (MapNpcDTO npcDto in npcs) + { + _mapNpcById.Set(npcDto.Id, npcDto); + _mapNpcs.GetOrSet($"by-map-id-{npcDto.MapId.ToString()}", () => new List()).Add(npcDto); + _mapNpcs.GetOrSet($"by-npc-vnum-{npcDto.NpcVNum.ToString()}", () => new List()).Add(npcDto); + count++; + } + + Log.Info($"[DATABASE] Loaded {count.ToString()} MapNPCs"); + } + + public MapNpcDTO GetById(int mapNpcId) => _mapNpcById.Get(mapNpcId); + + public IReadOnlyList GetByMapId(int mapId) => _mapNpcs.Get($"by-map-id-{mapId.ToString()}"); + + public IReadOnlyList GetMapNpcsPerVNum(int npcMonsterVnum) => _mapNpcs.Get($"by-npc-vnum-{npcMonsterVnum.ToString()}"); +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/RecipeManager.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/RecipeManager.cs new file mode 100644 index 0000000..a33f5d6 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/RecipeManager.cs @@ -0,0 +1,143 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using PhoenixLib.Caching; +using PhoenixLib.Logging; +using WingsEmu.DTOs.Recipes; +using WingsEmu.Game; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers.ServerData; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects; +using WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects.Recipes; + +namespace WingsEmu.Plugins.BasicImplementations.ServerConfigs; + +public class RecipeManager : IRecipeManager +{ + private readonly IEnumerable _files; + private readonly HashSet _generalRecipes = new(); + private readonly IItemsManager _itemsManager; + private readonly IRecipeFactory _recipeFactory; + private readonly IKeyValueCache> _recipes; + + public RecipeManager(IEnumerable files, IRecipeFactory recipeFactory, IKeyValueCache> recipes, IItemsManager itemsManager) + { + _files = files; + _recipes = recipes; + _itemsManager = itemsManager; + _recipeFactory = recipeFactory; + } + + public async Task InitializeAsync() + { + var recipes = new List(); + foreach (RecipeObject recipeObject in _files.SelectMany(x => x.Recipes)) + { + if (recipeObject == null) + { + continue; + } + + RecipeDTO recipe = recipeObject.ToDto(); + + IGameItem producerItem = _itemsManager.GetItem(recipe.ProducedItemVnum); + if (producerItem is null) + { + Log.Warn("[RECIPE] Item not found: " + recipe.Id + + $" on recipe ProducerItemVnum: {recipe.ProducerItemVnum} | ProducerNpc: {recipe.ProducerNpcVnum} | Producer: {recipe.ProducerMapNpcId}"); + } + + List items = new(); + if (recipeObject.Items != null) + { + short slot = 0; + foreach (RecipeItemObject recipeItem in recipeObject.Items) + { + if (recipeItem == null) + { + continue; + } + + IGameItem item = _itemsManager.GetItem(recipeItem.ItemVnum); + if (item is null) + { + Log.Warn("[RECIPE] Item not found: " + recipeItem.ItemVnum + + $" on recipe ProducerItemVnum: {recipe.ProducerItemVnum} | ProducerNpc: {recipe.ProducerNpcVnum} | Producer: {recipe.ProducerMapNpcId}"); + continue; + } + + items.Add(recipeItem.ToDto(slot)); + slot++; + } + } + + recipe.Items = items; + recipes.Add(recipe); + } + + int count = 0; + foreach (RecipeDTO recipe in recipes) + { + if (recipe.Items == null) + { + continue; + } + + Recipe gameRecipe = _recipeFactory.CreateRecipe(recipe); + _generalRecipes.Add(gameRecipe); + count++; + + if (gameRecipe.ProducerItemVnum.HasValue) + { + string key = $"item-{gameRecipe.ProducerItemVnum.Value}"; + List list = _recipes.Get(key); + if (list == null) + { + list = new List(); + _recipes.Set(key, list); + } + + list.Add(gameRecipe); + continue; + } + + if (gameRecipe.ProducerNpcVnum.HasValue) + { + string key = $"npcVnum-{gameRecipe.ProducerNpcVnum.Value}"; + List list = _recipes.Get(key); + if (list == null) + { + list = new List(); + _recipes.Set(key, list); + } + + list.Add(gameRecipe); + continue; + } + + if (gameRecipe.ProducerMapNpcId.HasValue) + { + string key = $"mapNpc-{gameRecipe.ProducerMapNpcId.Value}"; + List list = _recipes.Get(key); + if (list == null) + { + list = new List(); + _recipes.Set(key, list); + } + + list.Add(gameRecipe); + } + } + + Log.Info($"[RECIPE_MANAGER] Loaded {count.ToString()} recipes"); + } + + public IReadOnlyList GetRecipesByProducerItemVnum(int itemVnum) => _recipes.Get($"item-{itemVnum}"); + + public IReadOnlyList GetRecipesByNpcId(long mapNpcId) => _recipes.Get($"mapNpc-{mapNpcId}"); + + public IReadOnlyList GetRecipesByNpcMonsterVnum(int npcVNum) => _recipes.Get($"npcVnum-{npcVNum}"); + + public IReadOnlyList GetRecipeByProducedItemVnum(int itemVnum) => _generalRecipes.Where(x => x != null && x.ProducedItemVnum == itemVnum).ToList(); +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ShopManager.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ShopManager.cs new file mode 100644 index 0000000..a4c300d --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ShopManager.cs @@ -0,0 +1,106 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using PhoenixLib.Caching; +using PhoenixLib.Logging; +using WingsAPI.Data.Shops; +using WingsEmu.DTOs.Shops; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Managers.ServerData; +using WingsEmu.Game.Shops; +using WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects; +using WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects.Npcs; + +namespace WingsEmu.Plugins.BasicImplementations.ServerConfigs; + +public class ShopManager : IShopManager +{ + private readonly IEnumerable _importFile; + private readonly IShopFactory _shopFactory; + + private readonly ILongKeyCachedRepository _shopsByNpcId; + + public ShopManager(IEnumerable importFile, ILongKeyCachedRepository shopsByNpcId, IShopFactory shopFactory) + { + _importFile = importFile; + _shopsByNpcId = shopsByNpcId; + _shopFactory = shopFactory; + } + + public async Task InitializeAsync() + { + IEnumerable importedNpcs = _importFile.SelectMany(x => x.Npcs.Select(s => + { + s.MapId = x.MapId; + return s; + })).ToList(); + + int shopItemsCount = 0; + int shopSkillsCount = 0; + int count = 0; + + foreach (MapNpcObject npc in importedNpcs) + { + try + { + if (npc.ItemShop == null && npc.SkillShop == null) + { + continue; + } + + ShopDTO shop = npc.SkillShop?.ToDto() ?? npc.ItemShop.ToDto(); + + shop.MapNpcId = npc.MapNpcId; + + if (shop.MenuType == 1) + { + shop.Skills = new List(); + foreach (MapNpcShopTabObject tabs in npc.SkillShop.ShopTabs.Where(x => x.Items != null)) + { + short index = 0; + shop.Skills.AddRange(tabs.Items.Select(x => + { + ShopSkillDTO tpp = x.ToDto((byte)tabs.ShopTabId, index); + index++; + return tpp; + })); + } + } + else + { + short i = 0; + shop.Items = new List(); + foreach (MapNpcShopTabObject tabs in npc.ItemShop.ShopTabs.Where(tabs => tabs.Items != null)) + { + shop.Items.AddRange(tabs.Items.Select(s => + { + ShopItemDTO tpp = s.ToDto((byte)tabs.ShopTabId, i); + i++; + return tpp; + })); + } + } + + _shopsByNpcId.Set(shop.MapNpcId, _shopFactory.CreateShop(shop)); + shopItemsCount += shop.Items?.Count ?? 0; + shopSkillsCount += shop.Skills?.Count ?? 0; + count++; + } + catch (Exception e) + { + Log.Error("[MAPNPC_IMPORT] ERROR", e); + } + } + + Log.Info($"[SHOP_MANAGER] Loaded {count.ToString()} shops."); + Log.Info($"[SHOP_MANAGER] Loaded {shopItemsCount.ToString()} shops items."); + Log.Info($"[SHOP_MANAGER] Loaded {shopSkillsCount.ToString()} shops skills."); + } + + public ShopNpc GetShopByNpcId(int npcId) => _shopsByNpcId.Get(npcId); +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/TeleporterManager.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/TeleporterManager.cs new file mode 100644 index 0000000..a99c929 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/TeleporterManager.cs @@ -0,0 +1,54 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using PhoenixLib.Logging; +using WingsEmu.Core.Extensions; +using WingsEmu.DTOs.ServerDatas; +using WingsEmu.Game.Battle; +using WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects; +using WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects.Teleporters; + +namespace WingsEmu.Plugins.BasicImplementations.ServerConfigs; + +public class TeleporterManager : ITeleporterManager +{ + private readonly IEnumerable _teleporterConfigurations; + private readonly Dictionary> _teleporters = new(); + private readonly Dictionary> _teleportersByNpcId = new(); + + public TeleporterManager(IEnumerable teleporterConfigurations) => _teleporterConfigurations = teleporterConfigurations; + + public async Task InitializeAsync() + { + int count = 0; + foreach (TeleporterImportFile file in _teleporterConfigurations) + { + var teleporters = file.Teleporters.Select(s => + { + s.MapId = file.MapId; + count++; + return s.ToDto(); + }).ToList(); + _teleporters[file.MapId] = teleporters; + foreach (TeleporterDTO teleporter in teleporters) + { + if (!_teleportersByNpcId.TryGetValue(teleporter.MapNpcId, out List teleporterDtos)) + { + teleporterDtos = new List(); + _teleportersByNpcId[teleporter.MapNpcId] = teleporterDtos; + } + + teleporterDtos.Add(teleporter); + } + } + + Log.Info($"[DATABASE] Loaded {count.ToString()} teleporters."); + } + + public IReadOnlyList GetTeleportByNpcId(long npcId) => _teleportersByNpcId.GetOrDefault(npcId); + public IReadOnlyList GetTeleportByMapId(int mapId) => _teleporters.GetOrDefault(mapId); +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServiceCollectionExtensions.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..dbe6897 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServiceCollectionExtensions.cs @@ -0,0 +1,20 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using PhoenixLib.Extensions; +using WingsEmu.Game._Guri; + +namespace WingsEmu.Plugins.BasicImplementations; + +public static class ServiceCollectionExtensions +{ + public static void AddGuriHandlers(this IServiceCollection services) + { + Type[] types = typeof(GuriPlugin).Assembly.GetTypesImplementingInterface(); + foreach (Type handlerType in types) + { + services.AddTransient(handlerType); + } + + services.AddSingleton(); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Ship/Event/ShipEnterEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Ship/Event/ShipEnterEventHandler.cs new file mode 100644 index 0000000..2aed41d --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Ship/Event/ShipEnterEventHandler.cs @@ -0,0 +1,82 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Data.Families; +using WingsEmu.Core.Extensions; +using WingsEmu.Game; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Ship; +using WingsEmu.Game.Ship.Event; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.Ship.Event; + +public class ShipEnterEventHandler : IAsyncEventProcessor +{ + private readonly IGameLanguageService _languageService; + private readonly IRandomGenerator _randomGenerator; + private readonly IShipManager _shipManager; + + public ShipEnterEventHandler(IShipManager shipManager, IGameLanguageService languageService, IRandomGenerator randomGenerator) + { + _shipManager = shipManager; + _languageService = languageService; + _randomGenerator = randomGenerator; + } + + public async Task HandleAsync(ShipEnterEvent e, CancellationToken cancellation) + { + ShipInstance ship = _shipManager.GetShip(e.ShipType); + if (ship?.Configuration == null) + { + return; + } + + TimeSpan timeLeft = ship.LastDeparture + ship.Configuration.Departure - DateTime.UtcNow; + if (timeLeft.TotalMinutes < 1) + { + e.Sender.SendMsg(e.Sender.GetLanguage(GameDialogKey.SHIP_SHOUTMESSAGE_COOLDOWN), MsgMessageType.Middle); + return; + } + + long baseToRemove = ship.Configuration.ShipCost; + short toRemove = e.Sender.PlayerEntity.Family?.UpgradeValues.GetOrDefault(FamilyUpgradeType.DECREASE_SHIP_TP_COST) ?? 0; + long amountToRemove = (long)(baseToRemove * (toRemove * 0.01)); + baseToRemove -= amountToRemove; + + if (e.Sender.PlayerEntity.Gold < baseToRemove) + { + e.Sender.SendInformationChatMessage(_languageService.GetLanguage(GameDialogKey.INTERACTION_MESSAGE_NOT_ENOUGH_GOLD, e.Sender.UserLanguage)); + return; + } + + if (e.Sender.PlayerEntity.Level < ship.Configuration.ShipLevelRestriction) + { + e.Sender.SendInformationChatMessage(_languageService.GetLanguageFormat(GameDialogKey.SHIP_CHATMESSAGE_LEVEL_REQ, e.Sender.UserLanguage, ship.Configuration.ShipLevelRestriction)); + return; + } + + short rndX = (short)_randomGenerator.RandomNumber(ship.Configuration.ShipMapX.Minimum, ship.Configuration.ShipMapX.Maximum); + short rndY = (short)_randomGenerator.RandomNumber(ship.Configuration.ShipMapY.Minimum, ship.Configuration.ShipMapY.Maximum); + + if (ship.MapInstance.IsBlockedZone(rndX, rndY)) + { + rndX = ship.Configuration.ShipMapX.Minimum; + rndY = ship.Configuration.ShipMapY.Minimum; + } + + e.Sender.ChangeMap(ship.MapInstance.Id, rndX, rndY); + if (ship.DepartureWarnings.Count < 1) + { + return; + } + + TimeSpan currentWarning = ship.DepartureWarnings.First(); + TimeSpan shipLeft = ship.Configuration.Departure - currentWarning; + e.Sender.SendMsg(e.Sender.GetLanguageFormat(GameDialogKey.SHIP_SHOUTMESSAGE_MINUTES_REMAINING, shipLeft.Minutes.ToString()), MsgMessageType.Middle); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Ship/Event/ShipLeaveEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Ship/Event/ShipLeaveEventHandler.cs new file mode 100644 index 0000000..3da9bd0 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Ship/Event/ShipLeaveEventHandler.cs @@ -0,0 +1,12 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Ship.Event; + +namespace WingsEmu.Plugins.BasicImplementations.Ship.Event; + +public class ShipLeaveEventHandler : IAsyncEventProcessor +{ + public async Task HandleAsync(ShipLeaveEvent e, CancellationToken cancellation) => e.Sender.ChangeToLastBaseMap(); +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Ship/Event/ShipProcessEventAct4Handler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Ship/Event/ShipProcessEventAct4Handler.cs new file mode 100644 index 0000000..1f47df5 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Ship/Event/ShipProcessEventAct4Handler.cs @@ -0,0 +1,101 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using PhoenixLib.Logging; +using WingsAPI.Communication; +using WingsAPI.Communication.ServerApi; +using WingsAPI.Communication.ServerApi.Protocol; +using WingsAPI.Data.Families; +using WingsAPI.Packets.Enums; +using WingsEmu.Core.Extensions; +using WingsEmu.Game; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Ship; +using WingsEmu.Game.Ship.Configuration; +using WingsEmu.Game.Ship.Event; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.Ship.Event; + +public class ShipProcessEventAct4Handler : IAsyncEventProcessor +{ + private readonly IGameLanguageService _languageService; + private readonly IRandomGenerator _randomGenerator; + private readonly IServerApiService _serverApiService; + private readonly IServerManager _serverManager; + + public ShipProcessEventAct4Handler(IServerApiService serverApiService, IServerManager serverManager, IGameLanguageService languageService, IRandomGenerator randomGenerator) + { + _serverApiService = serverApiService; + _serverManager = serverManager; + _languageService = languageService; + _randomGenerator = randomGenerator; + } + + public async Task HandleAsync(ShipProcessEvent e, CancellationToken cancellation) + { + if (e.ShipInstance.ShipType != ShipType.Act4Angels && e.ShipInstance.ShipType != ShipType.Act4Demons) + { + return; + } + + await ProcessDeparture(e.ShipInstance, e.CurrentTime); + } + + private async Task ProcessDeparture(ShipInstance shipInstance, DateTime currentTime) + { + if (currentTime < shipInstance.LastDeparture + shipInstance.Configuration.Departure) + { + return; + } + + GetChannelInfoResponse response = null; + try + { + response = await _serverApiService.GetAct4ChannelInfo(new GetAct4ChannelInfoRequest + { + WorldGroup = _serverManager.ServerGroup + }); + } + catch (Exception e) + { + Log.Error("[SHIP_PROCESS_ACT4] Unexpected error happened while trying to obtain Act4 Channel's Info.", e); + } + + SerializableGameServer gameServer = response?.GameServer; + + if (response?.ResponseType != RpcResponseType.SUCCESS || gameServer == null) + { + shipInstance.MapInstance.Broadcast(x => x.GenerateMsgPacket(_languageService.GetLanguage(GameDialogKey.ACT4_CHANNEL_OFFLINE, x.UserLanguage), MsgMessageType.Middle)); + return; + } + + foreach (IClientSession session in shipInstance.MapInstance.Sessions.ToList()) + { + long baseToRemove = shipInstance.Configuration.ShipCost; + short toRemove = session.PlayerEntity.Family?.UpgradeValues.GetOrDefault(FamilyUpgradeType.DECREASE_SHIP_TP_COST) ?? 0; + long amountToRemove = (long)(baseToRemove * (toRemove * 0.01)); + baseToRemove -= amountToRemove; + + if (!session.PlayerEntity.RemoveGold(baseToRemove)) + { + session.SendErrorChatMessage(_languageService.GetLanguage(GameDialogKey.INTERACTION_MESSAGE_NOT_ENOUGH_GOLD, session.UserLanguage)); + session.ChangeToLastBaseMap(); + continue; + } + + await session.EmitEventAsync(new PlayerChangeChannelEvent(gameServer, ItModeType.ShipToAct4, (short)shipInstance.Configuration.DestinationMapId, + (short)_randomGenerator.RandomNumber(shipInstance.Configuration.DestinationMapX.Minimum, shipInstance.Configuration.DestinationMapX.Maximum + 1), + (short)_randomGenerator.RandomNumber(shipInstance.Configuration.DestinationMapY.Minimum, shipInstance.Configuration.DestinationMapY.Maximum + 1))); + } + + shipInstance.DepartureWarnings = shipInstance.Configuration.DepartureWarnings.ToList(); + shipInstance.LastDeparture = currentTime; + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Ship/Event/ShipProcessEventAct5Handler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Ship/Event/ShipProcessEventAct5Handler.cs new file mode 100644 index 0000000..d281b4b --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Ship/Event/ShipProcessEventAct5Handler.cs @@ -0,0 +1,61 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Ship; +using WingsEmu.Game.Ship.Configuration; +using WingsEmu.Game.Ship.Event; + +namespace WingsEmu.Plugins.BasicImplementations.Ship.Event; + +public class ShipProcessEventAct5Handler : IAsyncEventProcessor +{ + private readonly IGameLanguageService _languageService; + private readonly IRandomGenerator _randomGenerator; + + public ShipProcessEventAct5Handler(IRandomGenerator randomGenerator, IGameLanguageService languageService) + { + _randomGenerator = randomGenerator; + _languageService = languageService; + } + + public async Task HandleAsync(ShipProcessEvent e, CancellationToken cancellation) + { + if (e.ShipInstance.ShipType != ShipType.Act5) + { + return; + } + + ProcessDeparture(e.ShipInstance, e.CurrentTime); + } + + private void ProcessDeparture(ShipInstance shipInstance, DateTime currentTime) + { + if (currentTime < shipInstance.LastDeparture + shipInstance.Configuration.Departure) + { + return; + } + + foreach (IClientSession session in shipInstance.MapInstance.Sessions.ToArray()) + { + if (!session.PlayerEntity.RemoveGold(shipInstance.Configuration.ShipCost)) + { + session.SendErrorChatMessage(_languageService.GetLanguage(GameDialogKey.INTERACTION_MESSAGE_NOT_ENOUGH_GOLD, session.UserLanguage)); + session.ChangeToLastBaseMap(); + continue; + } + + session.ChangeMap(shipInstance.Configuration.DestinationMapId, + (short)_randomGenerator.RandomNumber(shipInstance.Configuration.DestinationMapX.Minimum, shipInstance.Configuration.DestinationMapX.Maximum + 1), + (short)_randomGenerator.RandomNumber(shipInstance.Configuration.DestinationMapY.Minimum, shipInstance.Configuration.DestinationMapY.Maximum + 1)); + } + + shipInstance.DepartureWarnings = shipInstance.Configuration.DepartureWarnings.ToList(); + shipInstance.LastDeparture = currentTime; + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Ship/Event/ShipProcessEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Ship/Event/ShipProcessEventHandler.cs new file mode 100644 index 0000000..03a65f6 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Ship/Event/ShipProcessEventHandler.cs @@ -0,0 +1,42 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Ship.Event; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.Ship.Event; + +public class ShipProcessEventHandler : IAsyncEventProcessor +{ + private readonly IGameLanguageService _languageService; + + public ShipProcessEventHandler(IGameLanguageService languageService) => _languageService = languageService; + + public async Task HandleAsync(ShipProcessEvent e, CancellationToken cancellation) + { + if (e.ShipInstance.DepartureWarnings.Count < 1) + { + return; + } + + TimeSpan currentWarning = e.ShipInstance.DepartureWarnings.First(); + + if (e.CurrentTime < e.ShipInstance.LastDeparture + currentWarning) + { + return; + } + + e.ShipInstance.DepartureWarnings.Remove(currentWarning); + + TimeSpan timeLeft = e.ShipInstance.Configuration.Departure - currentWarning; + bool isSeconds = timeLeft.TotalMinutes < 1; + GameDialogKey key = isSeconds ? GameDialogKey.SHIP_SHOUTMESSAGE_SECONDS_REMAINING : GameDialogKey.SHIP_SHOUTMESSAGE_MINUTES_REMAINING; + + e.ShipInstance.MapInstance.Broadcast(x => + x.GenerateMsgPacket(_languageService.GetLanguageFormat(key, x.UserLanguage, (isSeconds ? timeLeft.Seconds : timeLeft.Minutes).ToString()), MsgMessageType.Middle)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Ship/RecurrentJob/ShipSystem.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Ship/RecurrentJob/ShipSystem.cs new file mode 100644 index 0000000..5958d4d --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Ship/RecurrentJob/ShipSystem.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using PhoenixLib.Events; +using PhoenixLib.Logging; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Ship; +using WingsEmu.Game.Ship.Configuration; +using WingsEmu.Game.Ship.Event; + +namespace WingsEmu.Plugins.BasicImplementations.Ship.RecurrentJob; + +public class ShipSystem : BackgroundService +{ + private static readonly TimeSpan Interval = TimeSpan.FromSeconds(5); + private readonly IAsyncEventPipeline _asyncEventPipeline; + private readonly IMapManager _mapManager; + private readonly IShipConfigurationProvider _shipConfigurationProvider; + private readonly IShipManager _shipManager; + + public ShipSystem(IShipConfigurationProvider shipConfiguration, IShipManager shipManager, IMapManager mapManager, IAsyncEventPipeline asyncEventPipeline) + { + _shipConfigurationProvider = shipConfiguration; + _shipManager = shipManager; + _mapManager = mapManager; + _asyncEventPipeline = asyncEventPipeline; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + Log.Info("[SHIP_SYSTEM] Started!"); + DateTime currentTime = DateTime.UtcNow; + int count = 0; + foreach (Game.Ship.Configuration.Ship ship in _shipConfigurationProvider.GetShips()) + { + _shipManager.AddShip(new ShipInstance(_mapManager.GenerateMapInstanceByMapId(ship.ShipMapId, MapInstanceType.NormalInstance), ship, currentTime)); + count++; + } + + Log.Info($"[SHIP_SYSTEM] {count.ToString()} ships loaded!"); + + while (!stoppingToken.IsCancellationRequested) + { + await Process(); + await Task.Delay(Interval, stoppingToken); + } + } + + private async Task Process() + { + DateTime currentTime = DateTime.UtcNow; + IReadOnlyCollection ships = _shipManager.GetShips(); + foreach (ShipInstance ship in ships) + { + await _asyncEventPipeline.ProcessEventAsync(new ShipProcessEvent(ship, currentTime)); + } + + Log.Info($"[SHIP_SYSTEM] {ships.Count.ToString()} ships processed."); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Ship/ShipConfigurationProvider.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Ship/ShipConfigurationProvider.cs new file mode 100644 index 0000000..09b75d9 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Ship/ShipConfigurationProvider.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.Linq; +using WingsEmu.Game.Ship.Configuration; + +namespace WingsEmu.Plugins.BasicImplementations.Ship; + +public class ShipConfigurationProvider : IShipConfigurationProvider +{ + private readonly IReadOnlyList _ships; + + public ShipConfigurationProvider(ShipConfiguration shipConfiguration) => _ships = PrepareConfiguration(shipConfiguration); + + public IReadOnlyList GetShips() => _ships; + + private static List PrepareConfiguration(ShipConfiguration shipConfiguration) + { + var list = shipConfiguration.ToList(); + foreach (Game.Ship.Configuration.Ship ship in list) + { + ship.DepartureWarnings = ship.DepartureWarnings.OrderBy(w => w).ToList(); + } + + return list; + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Ship/ShipManager.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Ship/ShipManager.cs new file mode 100644 index 0000000..34c5f64 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Ship/ShipManager.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using WingsEmu.Game.Ship; +using WingsEmu.Game.Ship.Configuration; + +namespace WingsEmu.Plugins.BasicImplementations.Ship; + +public class ShipManager : IShipManager +{ + private readonly List _ships = new(); + private readonly Dictionary _shipsByShipType = new(); + + public void AddShip(ShipInstance shipInstance) + { + if (_shipsByShipType.TryAdd(shipInstance.ShipType, shipInstance)) + { + _ships.Add(shipInstance); + } + } + + public ShipInstance GetShip(ShipType shipType) => _shipsByShipType.TryGetValue(shipType, out ShipInstance shipInstance) ? shipInstance : null; + + public IReadOnlyCollection GetShips() => _ships; +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Ship/ShipModuleExtensions.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Ship/ShipModuleExtensions.cs new file mode 100644 index 0000000..1fe2f60 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Ship/ShipModuleExtensions.cs @@ -0,0 +1,33 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using PhoenixLib.Configuration; +using PhoenixLib.Logging; +using WingsAPI.Communication.ServerApi.Protocol; +using WingsAPI.Plugins; +using WingsEmu.Game.Ship; +using WingsEmu.Game.Ship.Configuration; +using WingsEmu.Plugins.BasicImplementations.Ship.RecurrentJob; + +namespace WingsEmu.Plugins.BasicImplementations.Ship; + +public static class ShipModuleExtensions +{ + public static void AddShipModule(this IServiceCollection services, GameServerLoader gameServer) + { + if (gameServer.Type == GameChannelType.ACT_4) + { + Log.Debug("Not loading Ships because this is an Act4 channel."); + return; + } + + services.AddSingleton(); + if (!bool.TryParse(Environment.GetEnvironmentVariable("ACT4_SHIP_ACTIVATED") ?? "true", out bool shipActivated) || !shipActivated) + { + return; + } + + services.AddHostedService(); + services.AddFileConfiguration(); + services.AddSingleton(); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Shop/BuyItemNpcShopEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Shop/BuyItemNpcShopEventHandler.cs new file mode 100644 index 0000000..fc97fc1 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Shop/BuyItemNpcShopEventHandler.cs @@ -0,0 +1,69 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Families; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Shops; +using WingsEmu.Game.Shops.Event; + +namespace WingsEmu.Plugins.BasicImplementations.Shop; + +public class BuyItemNpcShopEventHandler : IAsyncEventProcessor +{ + public async Task HandleAsync(BuyItemNpcShopEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + short amount = e.Amount; + short slot = e.Slot; + long ownerId = e.OwnerId; + + // load shop + + INpcEntity npcEntity = session.CurrentMapInstance.GetNpcById(ownerId); + if (npcEntity == null) + { + return; + } + + int dist = session.PlayerEntity.GetDistance(npcEntity); + if (npcEntity.ShopNpc == null) + { + return; + } + + if (dist > 5) + { + return; + } + + switch (npcEntity.ShopNpc.MenuType) + { + case ShopNpcMenuType.ITEMS: + case ShopNpcMenuType.MINILAND: + await session.EmitEventAsync(new BuyShopItemEvent + { + Amount = amount, + OwnerId = ownerId, + Slot = slot + }); + break; + case ShopNpcMenuType.SKILLS: + await session.EmitEventAsync(new BuyShopSkillEvent + { + OwnerId = ownerId, + Slot = slot, + Accept = e.Accept + }); + break; + case ShopNpcMenuType.FAMILIES: + await session.EmitEventAsync(new FamilyUpgradeBuyFromShopEvent + { + NpcId = ownerId, + Slot = slot + }); + break; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Shop/BuyShopItemEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Shop/BuyShopItemEventHandler.cs new file mode 100644 index 0000000..febf1ed --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Shop/BuyShopItemEventHandler.cs @@ -0,0 +1,176 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.DTOs.Shops; +using WingsEmu.Game; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Shops.Event; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.Shop; + +public class BuyShopItemEventHandler : IAsyncEventProcessor +{ + private readonly IGameItemInstanceFactory _gameItemInstanceFactory; + private readonly IGameLanguageService _gameLanguage; + private readonly IItemsManager _itemsManager; + private readonly IRandomGenerator _randomGenerator; + private readonly IRankingManager _rankingManager; + private readonly IReputationConfiguration _reputationConfiguration; + + public BuyShopItemEventHandler(IItemsManager itemsManager, IGameLanguageService gameLanguage, IRandomGenerator randomGenerator, IGameItemInstanceFactory gameItemInstanceFactory, + IReputationConfiguration reputationConfiguration, IRankingManager rankingManager) + { + _itemsManager = itemsManager; + _gameLanguage = gameLanguage; + _randomGenerator = randomGenerator; + _gameItemInstanceFactory = gameItemInstanceFactory; + _reputationConfiguration = reputationConfiguration; + _rankingManager = rankingManager; + } + + public async Task HandleAsync(BuyShopItemEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + if (session.PlayerEntity.IsInExchange()) + { + return; + } + + short amount = e.Amount; + short slot = e.Slot; + long ownerId = e.OwnerId; + + INpcEntity npcEntity = session.CurrentMapInstance.GetNpcById(ownerId); + ShopItemDTO item = npcEntity?.ShopNpc.ShopItems.FirstOrDefault(it => it.Slot == slot); + + if (item == null) + { + return; + } + + switch (amount) + { + case <= 0: + case > 999: + return; + } + + IGameItem gameItemInfo = _itemsManager.GetItem(item.ItemVNum); + long price = (item.Price ?? gameItemInfo.Price) * amount; + long reputationPrice = (item.Price ?? gameItemInfo.ReputPrice) * amount; + double percent; + switch (session.PlayerEntity.GetDignityIco()) + { + case 3: + percent = 1.10; + break; + + case 4: + percent = 1.20; + break; + + case 5: + case 6: + percent = 1.5; + break; + + default: + percent = 1; + break; + } + + short rare = item.Rare; + if (gameItemInfo.Type == InventoryType.Equipment) + { + amount = 1; + } + + if (gameItemInfo.ReputPrice == 0) + { + if (price < 0) + { + session.SendSMemo(SmemoType.Error, _gameLanguage.GetLanguage(GameDialogKey.INTERACTION_MESSAGE_NOT_ENOUGH_GOLD, session.UserLanguage)); + return; + } + + if (price * percent > session.PlayerEntity.Gold) + { + session.SendSMemo(SmemoType.Error, _gameLanguage.GetLanguage(GameDialogKey.INTERACTION_MESSAGE_NOT_ENOUGH_GOLD, session.UserLanguage)); + return; + } + } + else + { + if (reputationPrice <= 0 || reputationPrice > session.PlayerEntity.Reput) + { + session.SendSMemo(SmemoType.Error, _gameLanguage.GetLanguage(GameDialogKey.INFORMATION_CHATMESSAGE_NOT_ENOUGH_REPUT, session.UserLanguage)); + return; + } + + sbyte ra = (sbyte)_randomGenerator.RandomNumber(); + + int[] rareProb = { 100, 100, 70, 50, 30, 15, 5, 1 }; + if (gameItemInfo.ReputPrice != 0) + { + for (int i = 0; i < rareProb.Length; i++) + { + if (ra <= rareProb[i]) + { + rare = (sbyte)i; + } + } + } + } + + if (!session.PlayerEntity.HasSpaceFor(item.ItemVNum, amount)) + { + session.SendSMemo(SmemoType.Error, _gameLanguage.GetLanguage(GameDialogKey.INTERACTION_MESSAGE_NOT_ENOUGH_PLACE, session.UserLanguage)); + return; + } + + GameItemInstance newItem = _gameItemInstanceFactory.CreateItem(item.ItemVNum, amount, item.Upgrade, (sbyte)rare, item.Color); + await session.AddNewItemToInventory(newItem); + string itemName = _gameLanguage.GetLanguage(GameDataType.Item, gameItemInfo.Name, session.UserLanguage); + + session.SendSMemo(SmemoType.Balance, _gameLanguage.GetLanguageFormat(GameDialogKey.NPC_SHOP_LOG_PURCHASE, session.UserLanguage, itemName, amount)); + if (gameItemInfo.ReputPrice == 0) + { + long totalPrice = (long)(price * percent); + session.PlayerEntity.Gold -= totalPrice; + session.RefreshGold(); + await session.EmitEventAsync(new ShopNpcBoughtItemEvent + { + SellerId = ownerId, + CurrencyType = CurrencyType.GOLD, + TotalPrice = totalPrice, + ItemInstance = newItem, + Quantity = amount + }); + return; + } + + session.PlayerEntity.Reput -= reputationPrice; + session.RefreshReputation(_reputationConfiguration, _rankingManager.TopReputation); + session.SendChatMessage(_gameLanguage.GetLanguageFormat(GameDialogKey.INFORMATION_CHATMESSAGE_REPUT_DECREASE, session.UserLanguage, reputationPrice), ChatMessageColorType.Red); + newItem.BoundCharacterId = session.PlayerEntity.Id; + await session.EmitEventAsync(new ShopNpcBoughtItemEvent + { + SellerId = ownerId, + CurrencyType = CurrencyType.REPUTATION, + TotalPrice = reputationPrice, + ItemInstance = newItem, + Quantity = amount + }); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Shop/BuyShopSkillEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Shop/BuyShopSkillEventHandler.cs new file mode 100644 index 0000000..a2cddf6 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Shop/BuyShopSkillEventHandler.cs @@ -0,0 +1,227 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.Quicklist; +using WingsEmu.DTOs.Skills; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Algorithm; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Shops.Event; +using WingsEmu.Game.Skills; +using WingsEmu.Packets.Enums.Character; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.Shop; + +public class BuyShopSkillEventHandler : IAsyncEventProcessor +{ + private readonly ICharacterAlgorithm _characterAlgorithm; + private readonly IGameLanguageService _gameLanguage; + private readonly ISkillsManager _skillsManager; + + public BuyShopSkillEventHandler(IGameLanguageService gameLanguage, ISkillsManager skillsManager, ICharacterAlgorithm characterAlgorithm) + { + _gameLanguage = gameLanguage; + _skillsManager = skillsManager; + _characterAlgorithm = characterAlgorithm; + } + + public async Task HandleAsync(BuyShopSkillEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + short newSkillVnum = e.Slot; + long ownerId = e.OwnerId; + bool accept = e.Accept; + + INpcEntity npcEntity = session.CurrentMapInstance.GetNpcById(ownerId); + + if (npcEntity.ShopNpc.ShopSkills.All(s => s.SkillVNum != newSkillVnum)) + { + return; + } + + // skill shop + if (session.PlayerEntity.UseSp) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_REMOVE_SP, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (session.PlayerEntity.IsOnVehicle) + { + session.SendChatMessage(session.GetLanguage(GameDialogKey.SKILL_CHATMESSAGE_CANT_LEARN_MORPHED), ChatMessageColorType.Yellow); + return; + } + + if (session.PlayerEntity.IsInExchange()) + { + return; + } + + if (session.PlayerEntity.HasShopOpened) + { + return; + } + + if (session.PlayerEntity.CharacterSkills.Values.Any(s => !session.PlayerEntity.SkillCanBeUsed(s))) + { + session.SendMsg(session.GetLanguage(GameDialogKey.SKILL_SHOUTMESSAGE_CANT_LEARN_COOLDOWN), MsgMessageType.Middle); + return; + } + + SkillDTO skillInfo = _skillsManager.GetSkill(newSkillVnum); + + if (skillInfo == null) + { + return; + } + + if (session.PlayerEntity.CharacterSkills.Any(s => s.Value.SkillVNum == newSkillVnum)) + { + session.SendChatMessage(session.GetLanguage(GameDialogKey.SKILL_CHATMESSAGE_ALREADY_LEARNT), ChatMessageColorType.Yellow); + return; + } + + if (session.PlayerEntity.Gold < skillInfo.Price) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.INTERACTION_MESSAGE_NOT_ENOUGH_GOLD, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (session.PlayerEntity.GetCp() < skillInfo.CPCost && !skillInfo.IsPassiveSkill()) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_NOT_ENOUGH_CP, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (skillInfo.IsPassiveSkill()) + { + int skillMinimumLevel = 0; + if (skillInfo.MinimumSwordmanLevel == 0 && skillInfo.MinimumArcherLevel == 0 && skillInfo.MinimumMagicianLevel == 0) + { + skillMinimumLevel = skillInfo.MinimumAdventurerLevel; + } + else + { + skillMinimumLevel = session.PlayerEntity.Class switch + { + ClassType.Adventurer => skillInfo.MinimumAdventurerLevel, + ClassType.Swordman => skillInfo.MinimumSwordmanLevel, + ClassType.Archer => skillInfo.MinimumArcherLevel, + ClassType.Magician => skillInfo.MinimumMagicianLevel, + _ => skillMinimumLevel + }; + } + + if (skillMinimumLevel == 0) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.SKILL_SHOUTMESSAGE_CANT_LEARN, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (session.PlayerEntity.Level < skillMinimumLevel) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_TOO_LOW_LVL, session.UserLanguage), MsgMessageType.Middle); + return; + } + + // Find higher passive already in PlayerEntity + CharacterSkill findHigherPassive = session.PlayerEntity.CharacterSkills.Values.FirstOrDefault(x => x.Skill.IsPassiveSkill() + && x.Skill.CastId == skillInfo.CastId && x.Skill.Id > skillInfo.Id); + + if (findHigherPassive != null) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.SKILL_SHOUTMESSAGE_CANT_LEARN, session.UserLanguage), MsgMessageType.Middle); + return; + } + + foreach (CharacterSkill skill in session.PlayerEntity.CharacterSkills.Values) + { + if (skillInfo.CastId == skill.Skill.CastId && skill.Skill.IsPassiveSkill()) + { + session.PlayerEntity.CharacterSkills.TryRemove(skill.SkillVNum, out CharacterSkill value); + session.PlayerEntity.Skills.Remove(value); + } + } + } + else + { + if ((byte)session.PlayerEntity.Class != skillInfo.Class) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.SKILL_SHOUTMESSAGE_CANT_LEARN, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (session.PlayerEntity.JobLevel < skillInfo.LevelMinimum) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.INFORMATION_MESSAGE_LOW_JOB, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (skillInfo.UpgradeSkill != 0) + { + if (!session.PlayerEntity.CharacterSkills.ContainsKey(skillInfo.UpgradeSkill)) + { + session.SendChatMessage(session.GetLanguage(GameDialogKey.SKILL_CHATMESSAGE_CANT_LEARN_NEED_BASE), ChatMessageColorType.Yellow); + return; + } + + CharacterSkill oldUpgrade = session.PlayerEntity.CharacterSkills.Values.FirstOrDefault(s => + s.Skill.UpgradeSkill == skillInfo.UpgradeSkill && s.Skill.UpgradeType == skillInfo.UpgradeType && s.Skill.UpgradeSkill != 0); + if (oldUpgrade != null) + { + if (!accept) + { + session.SendQnaPacket($"buy 2 {npcEntity.Id} {newSkillVnum} 1", session.GetLanguage(GameDialogKey.SKILL_DIALOG_CONFIRM_REPLACE_UPGRADE)); + return; + } + + session.PlayerEntity.CharacterSkills.TryRemove(oldUpgrade.SkillVNum, out CharacterSkill value); + session.PlayerEntity.Skills.Remove(value); + if (session.PlayerEntity.SkillComponent.SkillUpgrades.TryGetValue(skillInfo.UpgradeSkill, out HashSet hashSet)) + { + hashSet.Remove(value); + } + } + } + } + + var newSkill = new CharacterSkill { SkillVNum = newSkillVnum }; + + short upgradeSkill = newSkill.Skill.UpgradeSkill; + if (upgradeSkill != 0) + { + if (!session.PlayerEntity.SkillComponent.SkillUpgrades.TryGetValue(upgradeSkill, out HashSet hashSet)) + { + hashSet = new HashSet(); + session.PlayerEntity.SkillComponent.SkillUpgrades[upgradeSkill] = hashSet; + } + + if (!hashSet.Contains(newSkill)) + { + hashSet.Add(newSkill); + } + } + + session.PlayerEntity.CharacterSkills[newSkillVnum] = newSkill; + session.PlayerEntity.Skills.Add(newSkill); + session.PlayerEntity.Gold -= skillInfo.Price; + session.RefreshGold(); + session.RefreshPassiveBCards(); + session.RefreshSkillList(); + session.RefreshQuicklist(); + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.SKILL_SHOUTMESSAGE_LEARNED, session.UserLanguage), MsgMessageType.Middle); + session.RefreshLevel(_characterAlgorithm); + session.SendSound(SoundType.BUY_SKILL); + await session.EmitEventAsync(new ShopSkillBoughtEvent + { + SkillVnum = newSkillVnum + }); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Shop/ShopNpcListItemsEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Shop/ShopNpcListItemsEventHandler.cs new file mode 100644 index 0000000..b2040bb --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Shop/ShopNpcListItemsEventHandler.cs @@ -0,0 +1,217 @@ +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.DTOs.Maps; +using WingsEmu.DTOs.Shops; +using WingsEmu.DTOs.Skills; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Families; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Shops; +using WingsEmu.Game.Shops.Event; + +namespace WingsEmu.Plugins.BasicImplementations.Shop; + +public class ShopNpcListItemsEventHandler : IAsyncEventProcessor +{ + private readonly IItemsManager _itemManager; + private readonly ISkillsManager _skillManager; + + public ShopNpcListItemsEventHandler(IItemsManager itemManager, ISkillsManager skillManager) + { + _itemManager = itemManager; + _skillManager = skillManager; + } + + public async Task HandleAsync(ShopNpcListItemsEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + + byte type = e.ShopType; + int npcId = e.NpcId; + + INpcEntity npcEntity = session.CurrentMapInstance.GetNpcById(npcId); + if (npcEntity?.ShopNpc == null) + { + return; + } + + HandleFamilyShopListing(session, npcEntity, type); + HandleItemShopListing(session, npcEntity, type); + HandleShopSkillListing(session, npcEntity, type); + } + + private void HandleFamilyShopListing(IClientSession session, INpcEntity npcEntity, byte type) + { + if (npcEntity.ShopNpc.MenuType != ShopNpcMenuType.FAMILIES) + { + return; + } + + if (npcEntity.ShopNpc.ShopItems.Count == 0) + { + return; + } + + var shopList = new StringBuilder($"n_inv 2 {npcEntity.Id} 0"); + + int i = 0; + IFamily family = session.PlayerEntity.Family; + foreach (ShopItemDTO shopItem in npcEntity.ShopNpc.ShopItems.Where(s => s.Type == type).OrderBy(x => x.ItemVNum)) + { + IGameItem gameItemByVnum = _itemManager.GetItem(shopItem.ItemVNum); + + FamilyUpgradeBuyableState familyItemBuyableStat; + + if (family == null) + { + familyItemBuyableStat = FamilyUpgradeBuyableState.REQUIREMENTS_NOT_MET; + } + else + { + if (family.HasAlreadyBoughtUpgrade(gameItemByVnum.Id)) + { + familyItemBuyableStat = FamilyUpgradeBuyableState.ALREADY_OWNED; + } + else if (family.Level < gameItemByVnum.Effect) + { + familyItemBuyableStat = FamilyUpgradeBuyableState.REQUIREMENTS_NOT_MET; + } + else + { + familyItemBuyableStat = FamilyUpgradeBuyableState.AVAILABLE; + } + } + + shopList.AppendFormat(" {0}|{1}|{2}", shopItem.ItemVNum, (byte)familyItemBuyableStat, i); + i++; + } + + session.SendPacket(shopList.ToString()); + } + + private void HandleShopSkillListing(IClientSession session, INpcEntity npcEntity, byte type) + { + if (npcEntity.ShopNpc.MenuType != ShopNpcMenuType.SKILLS) + { + return; + } + + int shopType = 0; + var shopList = new StringBuilder(); + foreach (ShopSkillDTO skill in npcEntity.ShopNpc.ShopSkills.Where(s => s.Type.Equals(type)).OrderBy(s => s.Slot)) + { + SkillDTO skillInfo = _skillManager.GetSkill(skill.SkillVNum); + + if (skill.Type != 0) + { + shopType = 1; + if (skillInfo.Class == (byte)session.PlayerEntity.Class) + { + shopList.Append($" {skillInfo.Id}"); + } + } + else + { + shopList.Append($" {skillInfo.Id}"); + } + } + + session.SendPacket($"n_inv 2 {npcEntity.Id} 0 {shopType}{shopList}"); + } + + private void HandleItemShopListing(IClientSession session, INpcEntity npcEntity, byte type) + { + if (npcEntity.ShopNpc.MenuType != ShopNpcMenuType.ITEMS && npcEntity.ShopNpc.MenuType != ShopNpcMenuType.MINILAND) + { + return; + } + + + byte shopType = 100; + double percent = 1; + + bool isOnAct4 = session.CurrentMapInstance.HasMapFlag(MapFlags.ACT_4); + + if (isOnAct4) + { + switch (session.PlayerEntity.GetDignityIco()) + { + default: + percent = 1.5; + shopType = 150; + break; + case 3: + + percent = 1.6; + shopType = 160; + break; + case 4: + + percent = 1.7; + shopType = 170; + break; + case 5: + case 6: + percent = 2; + shopType = 200; + break; + } + } + else + { + switch (session.PlayerEntity.GetDignityIco()) + { + case 3: + percent = 1.1; + shopType = 110; + break; + + case 4: + percent = 1.2; + shopType = 120; + break; + + case 5: + percent = 1.5; + shopType = 150; + break; + + case 6: + percent = 1.5; + shopType = 150; + break; + } + } + + var shopList = new StringBuilder($"n_inv 2 {npcEntity.Id} 0 {shopType}"); + foreach (ShopItemDTO shopItem in npcEntity.ShopNpc.ShopItems.Where(s => s.Type == type)) + { + IGameItem gameItemByVnum = _itemManager.GetItem(shopItem.ItemVNum); + switch (gameItemByVnum.ReputPrice) + { + case > 0 when gameItemByVnum.Type == 0: + shopList.Append( + $" {(byte)gameItemByVnum.Type}.{shopItem.Slot}.{shopItem.ItemVNum}.{shopItem.Rare}.{(gameItemByVnum.IsColorable ? shopItem.Color : shopItem.Upgrade)}.{shopItem.Price ?? gameItemByVnum.ReputPrice}.0.0"); + break; + case > 0 when gameItemByVnum.Type != 0: + shopList.Append($" {(byte)gameItemByVnum.Type}.{shopItem.Slot}.{shopItem.ItemVNum}.-1.{shopItem.Price ?? gameItemByVnum.ReputPrice}"); + break; + default: + { + shopList.Append(gameItemByVnum.Type != 0 + ? $" {(byte)gameItemByVnum.Type}.{shopItem.Slot}.{shopItem.ItemVNum}.-1.{(shopItem.Price ?? gameItemByVnum.Price) * percent}" + : $" {(byte)gameItemByVnum.Type}.{shopItem.Slot}.{shopItem.ItemVNum}.{shopItem.Rare}.{(gameItemByVnum.IsColorable ? shopItem.Color : shopItem.Upgrade)}.{(shopItem.Price ?? gameItemByVnum.Price) * percent}.0.0"); + + break; + } + } + } + + session.SendPacket(shopList.ToString()); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Shop/ShopPlayerBuyItemEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Shop/ShopPlayerBuyItemEventHandler.cs new file mode 100644 index 0000000..91c5d42 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Shop/ShopPlayerBuyItemEventHandler.cs @@ -0,0 +1,153 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.ItemExtension; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsAPI.Game.Extensions.ItemExtension.Item; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Shops; +using WingsEmu.Game.Shops.Event; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.Shop; + +public class ShopPlayerBuyItemEventHandler : IAsyncEventProcessor +{ + private static readonly SemaphoreSlim SemaphoreSlim = new(1, 1); + private readonly IGameItemInstanceFactory _gameItemInstanceFactory; + private readonly IGameLanguageService _language; + private readonly IServerManager _serverManager; + private readonly ISessionManager _sessionManager; + + public ShopPlayerBuyItemEventHandler(IGameLanguageService language, ISessionManager sessionManager, IServerManager serverManager, IGameItemInstanceFactory gameItemInstanceFactory) + { + _language = language; + _sessionManager = sessionManager; + _serverManager = serverManager; + _gameItemInstanceFactory = gameItemInstanceFactory; + } + + public async Task HandleAsync(ShopPlayerBuyItemEvent e, CancellationToken cancellation) + { + await SemaphoreSlim.WaitAsync(cancellation); + try + { + IClientSession session = e.Sender; + short amount = e.Amount; + short slot = e.Slot; + long ownerId = e.OwnerId; + + if (session.IsActionForbidden()) + { + return; + } + + if (amount is < 1 or > 999 || ownerId == e.Sender.PlayerEntity.Id) + { + return; + } + + IPlayerEntity owner = session.CurrentMapInstance.GetCharacterById(ownerId); + IEnumerable shop = owner?.ShopComponent.Items; + if (shop == null) + { + return; + } + + ShopPlayerItem playerItem = owner.ShopComponent.GetItem(slot); + + if (playerItem == null || playerItem.SellAmount < amount + || playerItem.InventoryItem.InventoryType != InventoryType.Equipment && playerItem.InventoryItem.InventoryType != InventoryType.Etc && + playerItem.InventoryItem.InventoryType != InventoryType.Main) + { + return; + } + + IClientSession shopOwner = owner.Session; + + long goldDifference = Math.Abs(playerItem.PricePerUnit * amount); + + if (goldDifference + shopOwner.PlayerEntity.Gold > _serverManager.MaxGold) + { + session.SendSMemo(SmemoType.Error, _language.GetLanguage(GameDialogKey.SHOP_INFO_TARGET_MAX_GOLD, session.UserLanguage)); + return; + } + + if (!session.PlayerEntity.HasSpaceFor(playerItem.InventoryItem.ItemInstance.ItemVNum, amount)) + { + session.SendSMemo(SmemoType.Error, _language.GetLanguage(GameDialogKey.INTERACTION_MESSAGE_NOT_ENOUGH_PLACE, session.UserLanguage)); + return; + } + + if (!session.PlayerEntity.RemoveGold(goldDifference)) + { + session.SendSMemo(SmemoType.Error, _language.GetLanguage(GameDialogKey.INTERACTION_MESSAGE_NOT_ENOUGH_GOLD, session.UserLanguage)); + return; + } + + shopOwner.PlayerEntity.Gold += goldDifference; + shopOwner.PlayerEntity.ShopComponent.Sell += goldDifference; + shopOwner.RefreshGold(); + + string itemNameOwner = playerItem.InventoryItem.ItemInstance.GameItem.GetItemName(_language, shopOwner.UserLanguage); + string itemNameClient = playerItem.InventoryItem.ItemInstance.GameItem.GetItemName(_language, session.UserLanguage); + + + shopOwner.SendSMemo(SmemoType.Balance, + _language.GetLanguageFormat(GameDialogKey.PERSONAL_SHOP_LOG_PURCHASE_OWNER, shopOwner.UserLanguage, session.PlayerEntity.Name, itemNameOwner, amount)); + session.SendSMemo(SmemoType.Balance, + _language.GetLanguageFormat(GameDialogKey.PERSONAL_SHOP_LOG_PURCHASE_BUYER, session.UserLanguage, itemNameClient, amount, shopOwner.PlayerEntity.Name)); + + InventoryItem inventoryItem = playerItem.InventoryItem; + if (playerItem.InventoryItem.InventoryType != InventoryType.Equipment) + { + GameItemInstance newItem = _gameItemInstanceFactory.CreateItem(inventoryItem.ItemInstance.ItemVNum, amount); + await shopOwner.RemoveItemFromInventory(amount: amount, item: inventoryItem); + await session.AddNewItemToInventory(newItem); + playerItem.SellAmount -= amount; + } + else + { + await shopOwner.RemoveItemFromInventory(amount: amount, item: inventoryItem); + await session.AddNewItemToInventory(inventoryItem.ItemInstance); + playerItem.SellAmount = 0; + } + + if (playerItem.SellAmount == 0) + { + shopOwner.PlayerEntity.ShopComponent.RemoveShopItem(playerItem); + } + + shopOwner.SendSellList(shopOwner.PlayerEntity.ShopComponent.Sell, playerItem.ShopSlot, amount, playerItem.SellAmount); + await session.EmitEventAsync(new ShopPlayerBoughtItemEvent + { + Quantity = amount, + ItemInstance = inventoryItem.ItemInstance, + TotalPrice = goldDifference, + SellerId = shopOwner.PlayerEntity.Id, + SellerName = shopOwner.PlayerEntity.Name + }); + + if (shopOwner.PlayerEntity.ShopComponent.Items.All(x => x == null)) + { + await shopOwner.EmitEventAsync(new ShopPlayerCloseEvent()); + return; + } + + session.SendShopContent(ownerId, shop); + } + finally + { + SemaphoreSlim.Release(); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Shop/ShopPlayerCloseEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Shop/ShopPlayerCloseEventHandler.cs new file mode 100644 index 0000000..4721e62 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Shop/ShopPlayerCloseEventHandler.cs @@ -0,0 +1,35 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Shops.Event; + +namespace WingsEmu.Plugins.BasicImplementations.Shop; + +public class ShopPlayerCloseEventHandler : IAsyncEventProcessor +{ + public async Task HandleAsync(ShopPlayerCloseEvent e, CancellationToken cancellation) + { + IClientSession session = e.Sender; + if (!session.PlayerEntity.HasShopOpened || !session.HasCurrentMapInstance) + { + return; + } + + session.PlayerEntity.ShopComponent.RemoveShop(); + session.PlayerEntity.HasShopOpened = false; + session.PlayerEntity.IsShopping = false; + session.BroadcastShop(); + session.BroadcastPlayerShopFlag(0); + session.SendCondPacket(); + + await session.EmitEventAsync(new PlayerRestEvent + { + RestTeamMemberMates = false + }); + + await session.EmitEventAsync(new ShopClosedEvent()); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Shop/ShopPlayerOpenEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Shop/ShopPlayerOpenEventHandler.cs new file mode 100644 index 0000000..ff6462b --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Shop/ShopPlayerOpenEventHandler.cs @@ -0,0 +1,75 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Shops.Event; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.Shop; + +public class ShopPlayerOpenEventHandler : IAsyncEventProcessor +{ + private readonly IGameLanguageService _languageService; + + public ShopPlayerOpenEventHandler(IGameLanguageService languageService) => _languageService = languageService; + + public async Task HandleAsync(ShopPlayerOpenEvent e, CancellationToken cancellation) + { + const int amountPersonalShopItems = 20; + if (amountPersonalShopItems != e.Items.Count) + { + return; + } + + if (!e.Sender.CurrentMapInstance.ShopAllowed) + { + e.Sender.SendShopEndPacket(ShopEndType.Player); + e.Sender.SendInfo(_languageService.GetLanguage(GameDialogKey.SHOP_INFO_NOT_ALLOWED, e.Sender.UserLanguage)); + return; + } + + if (e.Sender.PlayerEntity.HasShopOpened || e.Sender.PlayerEntity.ShopComponent.Items != null) + { + e.Sender.SendShopEndPacket(ShopEndType.Player); + e.Sender.SendInfo(_languageService.GetLanguage(GameDialogKey.SHOP_INFO_ALREADY_OPEN, e.Sender.UserLanguage)); + return; + } + + if (e.Sender.PlayerEntity.IsInRaidParty) + { + e.Sender.SendShopEndPacket(ShopEndType.Player); + e.Sender.SendInfo(_languageService.GetLanguage(GameDialogKey.SHOP_INFO_NOT_ALLOWED_IN_RAID, e.Sender.UserLanguage)); + return; + } + + if (e.Sender.CurrentMapInstance.Portals.Any(por => Math.Abs(e.Sender.PlayerEntity.PositionX - por.PositionX) < 6 && Math.Abs(e.Sender.PlayerEntity.PositionY - por.PositionY) < 6)) + { + e.Sender.SendShopEndPacket(ShopEndType.Player); + e.Sender.SendInfo(_languageService.GetLanguage(GameDialogKey.SHOP_INFO_NEAR_PORTAL, e.Sender.UserLanguage)); + return; + } + + e.Sender.PlayerEntity.ShopComponent.AddShop(e.Items); + e.Sender.PlayerEntity.ShopComponent.Name = e.ShopTitle; + e.Sender.PlayerEntity.HasShopOpened = true; + e.Sender.PlayerEntity.IsShopping = true; + + e.Sender.BroadcastShop(); + e.Sender.BroadcastPlayerShopFlag((long)DialogVnums.SHOP_PLAYER); + e.Sender.SendInfo(_languageService.GetLanguage(GameDialogKey.SHOP_INFO_OPEN, e.Sender.UserLanguage)); + e.Sender.SendCondPacket(); + e.Sender.EmitEvent(new PlayerRestEvent + { + RestTeamMemberMates = false + }); + await e.Sender.EmitEventAsync(new ShopOpenedEvent + { + ShopName = e.ShopTitle + }); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Vehicles/IVehicleConfigurationProvider.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Vehicles/IVehicleConfigurationProvider.cs new file mode 100644 index 0000000..623c247 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Vehicles/IVehicleConfigurationProvider.cs @@ -0,0 +1,9 @@ +using WingsEmu.Packets.Enums.Character; + +namespace WingsEmu.Plugins.BasicImplementations.Vehicles; + +public interface IVehicleConfigurationProvider +{ + VehicleConfiguration GetByVehicleVnum(int vnum); + VehicleConfiguration GetByMorph(int morph, GenderType genderType); +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Vehicles/VehicleConfiguration.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Vehicles/VehicleConfiguration.cs new file mode 100644 index 0000000..36a3395 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Vehicles/VehicleConfiguration.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; + +namespace WingsEmu.Plugins.BasicImplementations.Vehicles; + +public class VehicleConfiguration +{ + /// + /// Vehicle vnum id + /// + public int VehicleVnum { get; set; } + + public int DefaultSpeed { get; set; } + + public int MaleMorphId { get; set; } + + public int FemaleMorphId { get; set; } + + public bool? RemoveItem { get; set; } + + public List VehicleBoostType { get; set; } + + public List VehicleMapSpeeds { get; set; } +} + +public class VehicleBoost +{ + public BoostType BoostType { get; set; } + + public short? FirstValue { get; set; } + + public short? SecondValue { get; set; } +} + +public enum BoostType : byte +{ + INCREASE_SPEED = 0, + REMOVE_BAD_EFFECTS = 2, + RANDOM_TELEPORT_ON_MAP = 3, + TELEPORT_FORWARD = 4, + REGENERATE_HP_MP = 5, + CREATE_BUFF = 6, + CREATE_BUFF_ON_END = 7 +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Vehicles/VehicleConfigurationProvider.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Vehicles/VehicleConfigurationProvider.cs new file mode 100644 index 0000000..6e5ada3 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Vehicles/VehicleConfigurationProvider.cs @@ -0,0 +1,28 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using WingsEmu.Core.Extensions; +using WingsEmu.Packets.Enums.Character; + +namespace WingsEmu.Plugins.BasicImplementations.Vehicles; + +public class VehicleConfigurationProvider : IVehicleConfigurationProvider +{ + private readonly ImmutableDictionary _configurations; + private readonly Dictionary _femaleVehicles; + private readonly Dictionary _maleVehicles; + + public VehicleConfigurationProvider(IEnumerable configurations) + { + _configurations = configurations.ToImmutableDictionary(s => s.VehicleVnum); + _maleVehicles = configurations.GroupBy(s => s.MaleMorphId).ToDictionary(s => s.Key, s => s.First()); + _femaleVehicles = configurations.GroupBy(s => s.FemaleMorphId).ToDictionary(s => s.Key, s => s.First()); + } + + public VehicleConfiguration GetByVehicleVnum(int vnum) => _configurations.GetValueOrDefault(vnum); + public VehicleConfiguration GetByMorph(int morph, GenderType genderType) => genderType == GenderType.Male ? _maleVehicles.GetOrDefault(morph) : _femaleVehicles.GetOrDefault(morph); +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Vehicles/VehicleMapSpeedConfiguration.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Vehicles/VehicleMapSpeedConfiguration.cs new file mode 100644 index 0000000..117e9f6 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Vehicles/VehicleMapSpeedConfiguration.cs @@ -0,0 +1,16 @@ +using WingsEmu.DTOs.Maps; + +namespace WingsEmu.Plugins.BasicImplementations.Vehicles; + +public class VehicleMapSpeed +{ + /// + /// MapTypeId for the bonus + /// + public MapFlags MapFlag { get; set; } + + /// + /// Bonus to apply to the default speed + /// + public int SpeedBonus { get; set; } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Warehouse/AccountWarehouseAddEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Warehouse/AccountWarehouseAddEventHandler.cs new file mode 100644 index 0000000..66a0c4a --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Warehouse/AccountWarehouseAddEventHandler.cs @@ -0,0 +1,88 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Data.Account; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsAPI.Game.Extensions.Warehouse; +using WingsEmu.DTOs.Items; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Features; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Warehouse; +using WingsEmu.Game.Warehouse.Events; + +namespace WingsEmu.Plugins.BasicImplementations.Warehouse; + +public class AccountWarehouseAddEventHandler : IAsyncEventProcessor +{ + private readonly IAccountWarehouseManager _accountWarehouseManager; + private readonly IGameFeatureToggleManager _gameFeatureToggleManager; + private readonly IGameItemInstanceFactory _gameItemInstanceFactory; + private readonly IItemsManager _itemsManager; + private readonly IGameLanguageService _languageService; + + public AccountWarehouseAddEventHandler(IGameItemInstanceFactory gameItemInstanceFactory, IAccountWarehouseManager accountWarehouseManager, IGameLanguageService languageService, + IItemsManager itemsManager, IGameFeatureToggleManager gameFeatureToggleManager) + { + _gameItemInstanceFactory = gameItemInstanceFactory; + _accountWarehouseManager = accountWarehouseManager; + _languageService = languageService; + _itemsManager = itemsManager; + _gameFeatureToggleManager = gameFeatureToggleManager; + } + + public async Task HandleAsync(AccountWarehouseAddItemEvent e, CancellationToken cancellation) + { + bool disabled = await _gameFeatureToggleManager.IsDisabled(GameFeature.Warehouse); + if (disabled) + { + e.Sender.SendInfo(_languageService.GetLanguage(GameDialogKey.GAME_FEATURE_DISABLED, e.Sender.UserLanguage)); + return; + } + + IClientSession session = e.Sender; + IPlayerEntity character = session.PlayerEntity; + InventoryItem inventoryItem = e.Item; + + if (!character.IsWarehouseOpen || character.HasShopOpened || character.IsInExchange() + || inventoryItem.ItemInstance.Amount < e.Amount || e.SlotDestination >= character.WareHouseSize) + { + return; + } + + ItemInstanceDTO mapped = _gameItemInstanceFactory.CreateDto(inventoryItem.ItemInstance); + mapped.Amount = e.Amount; + + await e.Sender.RemoveItemFromInventory(amount: e.Amount, item: inventoryItem); + + (AccountWarehouseItemDto updatedItem, ManagerResponseType? response) = await _accountWarehouseManager.AddWarehouseItem(new AccountWarehouseItemDto + { + AccountId = session.Account.Id, + ItemInstance = mapped, + Slot = e.SlotDestination + }); + + if (response == ManagerResponseType.Success) + { + e.Sender.SendStashPacket(_itemsManager, updatedItem); + await e.Sender.EmitEventAsync(new WarehouseItemPlacedEvent + { + ItemInstance = mapped, + Amount = e.Amount, + DestinationSlot = e.SlotDestination + }); + return; + } + + await e.Sender.AddNewItemToInventory(_gameItemInstanceFactory.CreateItem(mapped), sendGiftIsFull: true); + e.Sender.SendInfo(response == ManagerResponseType.Maintenance + ? _languageService.GetLanguage(GameDialogKey.ACCOUNT_INFO_SERVICE_MAINTENANCE_MODE, e.Sender.UserLanguage) + : _languageService.GetLanguage(GameDialogKey.ACCOUNT_INFO_WAREHOUSE_UNEXPECTED_ERROR, e.Sender.UserLanguage)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Warehouse/AccountWarehouseManager.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Warehouse/AccountWarehouseManager.cs new file mode 100644 index 0000000..f8eddce --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Warehouse/AccountWarehouseManager.cs @@ -0,0 +1,170 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using PhoenixLib.Caching; +using PhoenixLib.Logging; +using WingsAPI.Communication; +using WingsAPI.Communication.DbServer.WarehouseService; +using WingsAPI.Data.Account; +using WingsAPI.Game.Extensions; +using WingsEmu.Core.Extensions; +using WingsEmu.DTOs.Items; +using WingsEmu.Game._enum; +using WingsEmu.Game.Warehouse; + +namespace WingsEmu.Plugins.BasicImplementations.Warehouse; + +public class AccountWarehouseManager : IAccountWarehouseManager +{ + private static readonly TimeSpan LifeTime = TimeSpan.FromMinutes(15); + + private readonly IAccountWarehouseService _accountWarehouseService; + + private readonly ILongKeyCachedRepository> _cachedAccountItems; + + public AccountWarehouseManager(ILongKeyCachedRepository> cachedAccountItems, IAccountWarehouseService accountWarehouseService) + { + _cachedAccountItems = cachedAccountItems; + _accountWarehouseService = accountWarehouseService; + } + + public async Task<(IDictionary accountWarehouseItemDtos, ManagerResponseType?)> GetWarehouse(long accountId) + { + Dictionary retrievedItems = _cachedAccountItems.Get(accountId); + + if (retrievedItems != null) + { + _cachedAccountItems.Set(accountId, retrievedItems, LifeTime); + return (retrievedItems, ManagerResponseType.Success); + } + + AccountWarehouseGetItemsResponse response = null; + + try + { + response = await _accountWarehouseService.GetItems(new AccountWarehouseGetItemsRequest + { + AccountId = accountId + }); + } + catch (Exception ex) + { + Log.Error("[ACCOUNT_WAREHOUSE_MANAGER][GET_ITEMS] ", ex); + } + + Dictionary dictionary = response?.Items?.ToDictionary(x => x.Slot) ?? new Dictionary(); + + if (response?.ResponseType == RpcResponseType.SUCCESS) + { + _cachedAccountItems.Set(accountId, dictionary, LifeTime); + } + + return (dictionary, response?.ResponseType.ToManagerType()); + } + + public async Task<(AccountWarehouseItemDto, ManagerResponseType?)> GetWarehouseItem(long accountId, short slot) + { + (IDictionary familyWarehouseItemDtos, ManagerResponseType? responseType) = await GetWarehouse(accountId); + return (familyWarehouseItemDtos.GetOrDefault(slot), responseType); + } + + public async Task<(AccountWarehouseItemDto, ManagerResponseType?)> AddWarehouseItem(AccountWarehouseItemDto warehouseItemDtoToAdd) + { + AccountWarehouseAddItemResponse response = null; + + try + { + response = await _accountWarehouseService.AddItem(new AccountWarehouseAddItemRequest + { + Item = warehouseItemDtoToAdd + }); + } + catch (Exception ex) + { + Log.Error("[ACCOUNT_WAREHOUSE_MANAGER][ADD_ITEM] ", ex); + } + + if (response?.ResponseType == RpcResponseType.SUCCESS) + { + await UpdateWarehouseItem(warehouseItemDtoToAdd.AccountId, response.Item.Slot, response.Item); + } + + return (response?.Item, response?.ResponseType.ToManagerType()); + } + + public async Task<(AccountWarehouseItemDto, ItemInstanceDTO, ManagerResponseType?)> WithdrawWarehouseItem(AccountWarehouseItemDto warehouseItemDtoToWithdraw, int amount) + { + AccountWarehouseWithdrawItemResponse response = null; + + try + { + response = await _accountWarehouseService.WithdrawItem(new AccountWarehouseWithdrawItemRequest + { + ItemToWithdraw = warehouseItemDtoToWithdraw, + Amount = amount + }); + } + catch (Exception ex) + { + Log.Error("[ACCOUNT_WAREHOUSE_MANAGER][WITHDRAW_ITEM] ", ex); + } + + if (response?.ResponseType == RpcResponseType.SUCCESS) + { + await UpdateWarehouseItem(warehouseItemDtoToWithdraw.AccountId, warehouseItemDtoToWithdraw.Slot, response.UpdatedItem); + } + + return (response?.UpdatedItem, response?.WithdrawnItem, response?.ResponseType.ToManagerType()); + } + + public async Task<(AccountWarehouseItemDto oldItem, AccountWarehouseItemDto newItem, ManagerResponseType?)> MoveWarehouseItem(AccountWarehouseItemDto warehouseItemDtoToMove, int amount, + short newSlot) + { + AccountWarehouseMoveItemResponse response = null; + + try + { + response = await _accountWarehouseService.MoveItem(new AccountWarehouseMoveItemRequest + { + WarehouseItemDtoToMove = warehouseItemDtoToMove, + Amount = amount, + NewSlot = newSlot + }); + } + catch (Exception ex) + { + Log.Error("[ACCOUNT_WAREHOUSE_MANAGER][MOVE_ITEM] ", ex); + } + + if (response?.ResponseType == RpcResponseType.SUCCESS) + { + await UpdateWarehouseItem(warehouseItemDtoToMove.AccountId, warehouseItemDtoToMove.Slot, response.OldItem); + await UpdateWarehouseItem(warehouseItemDtoToMove.AccountId, newSlot, response.NewItem); + } + + return (response?.OldItem, response?.NewItem, response?.ResponseType.ToManagerType()); + } + + public void CleanCache(long accountId) + { + _cachedAccountItems.Remove(accountId); + } + + private async Task UpdateWarehouseItem(long accountId, short slot, AccountWarehouseItemDto dto) + { + (IDictionary items, ManagerResponseType? responseType) = await GetWarehouse(accountId); + if (responseType != ManagerResponseType.Success) + { + return; + } + + if (dto == null) + { + items.Remove(slot); + return; + } + + items[slot] = dto; + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Warehouse/AccountWarehouseMoveEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Warehouse/AccountWarehouseMoveEventHandler.cs new file mode 100644 index 0000000..f6abcf2 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Warehouse/AccountWarehouseMoveEventHandler.cs @@ -0,0 +1,67 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Data.Account; +using WingsAPI.Game.Extensions.Warehouse; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Features; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Warehouse; +using WingsEmu.Game.Warehouse.Events; + +namespace WingsEmu.Plugins.BasicImplementations.Warehouse; + +public class AccountWarehouseMoveEventHandler : IAsyncEventProcessor +{ + private readonly IAccountWarehouseManager _accountWarehouseManager; + private readonly IGameFeatureToggleManager _gameFeatureToggleManager; + private readonly IItemsManager _itemsManager; + private readonly IGameLanguageService _languageService; + + public AccountWarehouseMoveEventHandler(IAccountWarehouseManager accountWarehouseManager, IGameLanguageService languageService, IItemsManager itemsManager, + IGameFeatureToggleManager gameFeatureToggleManager) + { + _accountWarehouseManager = accountWarehouseManager; + _languageService = languageService; + _itemsManager = itemsManager; + _gameFeatureToggleManager = gameFeatureToggleManager; + } + + public async Task HandleAsync(AccountWarehouseMoveEvent e, CancellationToken cancellation) + { + bool disabled = await _gameFeatureToggleManager.IsDisabled(GameFeature.Warehouse); + if (disabled) + { + e.Sender.SendInfo(_languageService.GetLanguage(GameDialogKey.GAME_FEATURE_DISABLED, e.Sender.UserLanguage)); + return; + } + + IClientSession session = e.Sender; + + if (!session.PlayerEntity.IsWarehouseOpen || session.PlayerEntity.HasShopOpened || session.PlayerEntity.IsInExchange() + || e.OriginalSlot < 0 || e.NewSlot < 0 || e.OriginalSlot == e.NewSlot || e.NewSlot >= session.PlayerEntity.WareHouseSize) + { + return; + } + + (AccountWarehouseItemDto oldItem, AccountWarehouseItemDto newItem, ManagerResponseType? responseType) = await _accountWarehouseManager.MoveWarehouseItem(new AccountWarehouseItemDto + { + AccountId = session.PlayerEntity.AccountId, + Slot = e.OriginalSlot + }, e.Amount, e.NewSlot); + + if (responseType != ManagerResponseType.Success) + { + e.Sender.SendInfo(responseType == ManagerResponseType.Maintenance + ? _languageService.GetLanguage(GameDialogKey.ACCOUNT_INFO_SERVICE_MAINTENANCE_MODE, e.Sender.UserLanguage) + : _languageService.GetLanguage(GameDialogKey.ACCOUNT_INFO_WAREHOUSE_UNEXPECTED_ERROR, e.Sender.UserLanguage)); + return; + } + + session.SendStashDynamicItemUpdate(_itemsManager, oldItem, e.OriginalSlot); + session.SendStashDynamicItemUpdate(_itemsManager, newItem, e.NewSlot); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Warehouse/AccountWarehouseOpenEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Warehouse/AccountWarehouseOpenEventHandler.cs new file mode 100644 index 0000000..ee8adc2 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Warehouse/AccountWarehouseOpenEventHandler.cs @@ -0,0 +1,82 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Data.Account; +using WingsAPI.Game.Extensions.Warehouse; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Features; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Miniland; +using WingsEmu.Game.Networking; +using WingsEmu.Game.TimeSpaces; +using WingsEmu.Game.Warehouse; +using WingsEmu.Game.Warehouse.Events; + +namespace WingsEmu.Plugins.BasicImplementations.Warehouse; + +public class AccountWarehouseOpenEventHandler : IAsyncEventProcessor +{ + private readonly IAccountWarehouseManager _accountWarehouseManager; + private readonly IGameFeatureToggleManager _gameFeatureToggleManager; + private readonly IItemsManager _itemsManager; + private readonly IGameLanguageService _languageService; + + public AccountWarehouseOpenEventHandler(IItemsManager itemsManager, IAccountWarehouseManager accountWarehouseManager, IGameLanguageService languageService, + IGameFeatureToggleManager gameFeatureToggleManager) + { + _itemsManager = itemsManager; + _accountWarehouseManager = accountWarehouseManager; + _languageService = languageService; + _gameFeatureToggleManager = gameFeatureToggleManager; + } + + public async Task HandleAsync(AccountWarehouseOpenEvent e, CancellationToken cancellation) + { + bool disabled = await _gameFeatureToggleManager.IsDisabled(GameFeature.Warehouse); + if (disabled) + { + e.Sender.SendInfo(_languageService.GetLanguage(GameDialogKey.GAME_FEATURE_DISABLED, e.Sender.UserLanguage)); + return; + } + + IClientSession session = e.Sender; + IPlayerEntity character = e.Sender.PlayerEntity; + + if (character.HasShopOpened || character.IsInExchange() || session.IsInSpecialOrHiddenTimeSpace()) + { + return; + } + + int capacity = character.WareHouseSize; + + if (capacity == 0) + { + // Try find warehouse + MapDesignObject warehouse = session.PlayerEntity.Miniland?.MapDesignObjects?.FirstOrDefault(x => x?.InventoryItem?.ItemInstance != null + && x.InventoryItem.ItemInstance.GameItem.IsWarehouse); + if (warehouse != null) + { + e.Sender.PlayerEntity.WareHouseSize = warehouse.InventoryItem.ItemInstance.GameItem.MinilandObjectPoint; + capacity = warehouse.InventoryItem.ItemInstance.GameItem.MinilandObjectPoint; + } + } + + (IDictionary accountWarehouseItemDtos, ManagerResponseType? responseType) = await _accountWarehouseManager.GetWarehouse(session.Account.Id); + + if (responseType != ManagerResponseType.Success) + { + e.Sender.SendInfo(responseType == ManagerResponseType.Maintenance + ? _languageService.GetLanguage(GameDialogKey.ACCOUNT_INFO_SERVICE_MAINTENANCE_MODE, e.Sender.UserLanguage) + : _languageService.GetLanguage(GameDialogKey.ACCOUNT_INFO_WAREHOUSE_UNEXPECTED_ERROR, e.Sender.UserLanguage)); + return; + } + + session.PlayerEntity.IsWarehouseOpen = true; + e.Sender.SendWarehouseStashAll(_itemsManager, capacity, accountWarehouseItemDtos?.Values ?? new List()); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Warehouse/AccountWarehouseShowItemEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Warehouse/AccountWarehouseShowItemEventHandler.cs new file mode 100644 index 0000000..64f722b --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Warehouse/AccountWarehouseShowItemEventHandler.cs @@ -0,0 +1,83 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Data.Account; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Algorithm; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Features; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Warehouse; +using WingsEmu.Game.Warehouse.Events; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.Warehouse; + +public class AccountWarehouseShowItemEventHandler : IAsyncEventProcessor +{ + private readonly IAccountWarehouseManager _accountWarehouseManager; + private readonly ICharacterAlgorithm _algorithm; + private readonly IGameFeatureToggleManager _gameFeatureToggleManager; + private readonly IGameItemInstanceFactory _gameItemInstanceFactory; + private readonly IItemsManager _itemsManager; + private readonly IGameLanguageService _languageService; + + public AccountWarehouseShowItemEventHandler(IAccountWarehouseManager accountWarehouseManager, IGameLanguageService languageService, IGameItemInstanceFactory gameItemInstanceFactory, + IItemsManager itemsManager, ICharacterAlgorithm algorithm, IGameFeatureToggleManager gameFeatureToggleManager) + { + _accountWarehouseManager = accountWarehouseManager; + _languageService = languageService; + _gameItemInstanceFactory = gameItemInstanceFactory; + _itemsManager = itemsManager; + _algorithm = algorithm; + _gameFeatureToggleManager = gameFeatureToggleManager; + } + + public async Task HandleAsync(AccountWarehouseShowItemEvent e, CancellationToken cancellation) + { + bool disabled = await _gameFeatureToggleManager.IsDisabled(GameFeature.Warehouse); + if (disabled) + { + e.Sender.SendInfo(_languageService.GetLanguage(GameDialogKey.GAME_FEATURE_DISABLED, e.Sender.UserLanguage)); + return; + } + + IClientSession session = e.Sender; + IPlayerEntity character = e.Sender.PlayerEntity; + + if (!character.IsWarehouseOpen || character.HasShopOpened || character.IsInExchange()) + { + return; + } + + (AccountWarehouseItemDto item, ManagerResponseType? response) = await _accountWarehouseManager.GetWarehouseItem(session.Account.Id, e.Slot); + + if (response != ManagerResponseType.Success) + { + e.Sender.SendInfo(_languageService.GetLanguage( + response == ManagerResponseType.Maintenance ? GameDialogKey.FAMILY_INFO_SERVICE_MAINTENANCE_MODE : GameDialogKey.FAMILY_INFO_WAREHOUSE_UNEXPECTED_ERROR, e.Sender.UserLanguage)); + return; + } + + GameItemInstance itemInstance = _gameItemInstanceFactory.CreateItem(item.ItemInstance); + if (itemInstance.GameItem.EquipmentSlot == EquipmentType.Sp) + { + if (itemInstance.GameItem.IsPartnerSpecialist) + { + session.SendPartnerSpecialistInfo(itemInstance); + } + else + { + session.SendSpecialistCardInfo(itemInstance, _algorithm); + } + + return; + } + + session.SendEInfoPacket(itemInstance, _itemsManager, _algorithm); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Warehouse/AccountWarehouseWithdrawEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Warehouse/AccountWarehouseWithdrawEventHandler.cs new file mode 100644 index 0000000..f2e01f6 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Warehouse/AccountWarehouseWithdrawEventHandler.cs @@ -0,0 +1,105 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Data.Account; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsAPI.Game.Extensions.Warehouse; +using WingsEmu.DTOs.Items; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Features; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Warehouse; +using WingsEmu.Game.Warehouse.Events; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.Warehouse; + +public class AccountWarehouseWithdrawEventHandler : IAsyncEventProcessor +{ + private readonly IAccountWarehouseManager _accountWarehouseManager; + private readonly IGameFeatureToggleManager _gameFeatureToggleManager; + private readonly IGameItemInstanceFactory _gameItemInstanceFactory; + private readonly IItemsManager _itemsManager; + private readonly IGameLanguageService _languageService; + + public AccountWarehouseWithdrawEventHandler(IGameLanguageService languageService, IGameItemInstanceFactory gameItemInstanceFactory, IAccountWarehouseManager accountWarehouseManager, + IItemsManager itemsManager, IGameFeatureToggleManager gameFeatureToggleManager) + { + _languageService = languageService; + _gameItemInstanceFactory = gameItemInstanceFactory; + _accountWarehouseManager = accountWarehouseManager; + _itemsManager = itemsManager; + _gameFeatureToggleManager = gameFeatureToggleManager; + } + + public async Task HandleAsync(AccountWarehouseWithdrawItemEvent e, CancellationToken cancellation) + { + bool disabled = await _gameFeatureToggleManager.IsDisabled(GameFeature.Warehouse); + if (disabled) + { + e.Sender.SendInfo(_languageService.GetLanguage(GameDialogKey.GAME_FEATURE_DISABLED, e.Sender.UserLanguage)); + return; + } + + IClientSession session = e.Sender; + IPlayerEntity character = session.PlayerEntity; + + if (!character.IsWarehouseOpen || character.HasShopOpened || character.IsInExchange()) + { + return; + } + + (AccountWarehouseItemDto itemToWithdraw, ManagerResponseType? responseType) = await _accountWarehouseManager.GetWarehouseItem(session.Account.Id, e.Slot); + + if (responseType == null) + { + e.Sender.SendInfo(_languageService.GetLanguage(GameDialogKey.FAMILY_INFO_WAREHOUSE_UNEXPECTED_ERROR, e.Sender.UserLanguage)); + return; + } + + if (itemToWithdraw?.ItemInstance == null) + { + e.Sender.SendInfo(_languageService.GetLanguage(GameDialogKey.FAMILY_INFO_WAREHOUSE_UNEXPECTED_ERROR, e.Sender.UserLanguage)); + return; + } + + if (!character.HasSpaceFor(itemToWithdraw.ItemInstance.ItemVNum, (short)itemToWithdraw.ItemInstance.Amount)) + { + session.SendMsg(session.GetLanguage(GameDialogKey.INTERACTION_MESSAGE_NOT_ENOUGH_PLACE), MsgMessageType.Middle); + return; + } + + if (responseType != ManagerResponseType.Success) + { + e.Sender.SendInfo(responseType == ManagerResponseType.Maintenance + ? _languageService.GetLanguage(GameDialogKey.FAMILY_INFO_SERVICE_MAINTENANCE_MODE, e.Sender.UserLanguage) + : _languageService.GetLanguage(GameDialogKey.FAMILY_INFO_WAREHOUSE_UNEXPECTED_ERROR, e.Sender.UserLanguage)); + + return; + } + + (AccountWarehouseItemDto updatedItem, ItemInstanceDTO withdrawnItem, ManagerResponseType? responseType2) = await _accountWarehouseManager.WithdrawWarehouseItem(itemToWithdraw, e.Amount); + + if (responseType2 is ManagerResponseType.Success) + { + session.SendStashDynamicItemUpdate(_itemsManager, updatedItem, e.Slot); + await session.AddNewItemToInventory(_gameItemInstanceFactory.CreateItem(withdrawnItem), sendGiftIsFull: true); + await session.EmitEventAsync(new WarehouseItemWithdrawnEvent + { + ItemInstance = withdrawnItem, + Amount = withdrawnItem.Amount, + FromSlot = e.Slot + }); + return; + } + + e.Sender.SendInfo(responseType2 is ManagerResponseType.Maintenance + ? _languageService.GetLanguage(GameDialogKey.FAMILY_INFO_SERVICE_MAINTENANCE_MODE, e.Sender.UserLanguage) + : _languageService.GetLanguage(GameDialogKey.FAMILY_INFO_WAREHOUSE_UNEXPECTED_ERROR, e.Sender.UserLanguage)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Warehouse/IWarehouseFactory.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Warehouse/IWarehouseFactory.cs new file mode 100644 index 0000000..58248d7 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Warehouse/IWarehouseFactory.cs @@ -0,0 +1,9 @@ +using WingsEmu.Game.Characters; +using WingsEmu.Game.Warehouse; + +namespace WingsEmu.Plugins.BasicImplementations.Warehouse; + +public interface IWarehouseFactory +{ + IWarehouse Create(IPlayerEntity entity); +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Warehouse/PartnerWarehouseAddItemEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Warehouse/PartnerWarehouseAddItemEventHandler.cs new file mode 100644 index 0000000..a5b1cae --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Warehouse/PartnerWarehouseAddItemEventHandler.cs @@ -0,0 +1,177 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.Warehouse; +using WingsEmu.DTOs.Bonus; +using WingsEmu.DTOs.Items; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Features; +using WingsEmu.Game.Items; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Warehouse; +using WingsEmu.Game.Warehouse.Events; + +namespace WingsEmu.Plugins.BasicImplementations.Warehouse; + +public class PartnerWarehouseAddItemEventHandler : IAsyncEventProcessor +{ + private readonly IGameFeatureToggleManager _gameFeatureToggleManager; + private readonly IGameItemInstanceFactory _gameItemInstance; + private readonly IGameLanguageService _gameLanguageService; + + public PartnerWarehouseAddItemEventHandler(IGameItemInstanceFactory gameItemInstance, IGameFeatureToggleManager gameFeatureToggleManager, IGameLanguageService gameLanguageService) + { + _gameItemInstance = gameItemInstance; + _gameFeatureToggleManager = gameFeatureToggleManager; + _gameLanguageService = gameLanguageService; + } + + public async Task HandleAsync(PartnerWarehouseAddItemEvent e, CancellationToken cancellation) + { + bool disabled = await _gameFeatureToggleManager.IsDisabled(GameFeature.PartnerWarehouse); + if (disabled) + { + e.Sender.SendInfo(_gameLanguageService.GetLanguage(GameDialogKey.GAME_FEATURE_DISABLED, e.Sender.UserLanguage)); + return; + } + + IClientSession session = e.Sender; + GameItemInstance itemInstance = e.ItemInstance; + + if (itemInstance == null) + { + return; + } + + if (!session.PlayerEntity.HaveStaticBonus(StaticBonusType.PartnerBackpack)) + { + return; + } + + if (itemInstance.Amount > 999) + { + return; + } + + IReadOnlyList warehouseItems = session.PlayerEntity.PartnerWarehouseItems(); + if (itemInstance.Type == ItemInstanceType.NORMAL_ITEM) + { + // Find exactly the same item + int vnum = itemInstance.GameItem.Id; + PartnerWarehouseItem sameItem = warehouseItems.FirstOrDefault(x => x?.ItemInstance != null && x.ItemInstance.ItemVNum == vnum); + if (sameItem == null) + { + byte? freeSlot = session.PlayerEntity.GetNextPartnerWarehouseSlot(); + if (!freeSlot.HasValue) + { + return; + } + + session.PlayerEntity.AddPartnerWarehouseItem(itemInstance, freeSlot.Value); + PartnerWarehouseItem partnerWarehouseItem = session.PlayerEntity.GetPartnerWarehouseItem(freeSlot.Value); + if (session.PlayerEntity.IsPartnerWarehouseOpen) + { + session.SendAddPartnerWarehouseItem(partnerWarehouseItem); + } + + return; + } + + GameItemInstance anotherInstance = sameItem.ItemInstance; + int amount = itemInstance.Amount; + + int total = amount + anotherInstance.Amount; + if (total > 999) + { + amount = total - 999; + + // Try find in warehouse exactly the same item and add left amount to it + foreach (PartnerWarehouseItem item in warehouseItems.Where(x => x?.ItemInstance.ItemVNum == vnum)) + { + if (item.ItemInstance == anotherInstance) + { + continue; + } + + if (item.ItemInstance.Amount >= 999) + { + continue; + } + + if (amount + item.ItemInstance.Amount > 999) + { + int left = 999 - item.ItemInstance.Amount; + amount -= left; + item.ItemInstance.Amount = 999; + continue; + } + + item.ItemInstance.Amount += amount; + amount = 0; + break; + } + + if (amount <= 0) + { + anotherInstance.Amount = 999; + if (session.PlayerEntity.IsPartnerWarehouseOpen) + { + session.SendAddPartnerWarehouseItem(sameItem); + } + + return; + } + + // Find new slot and create new item + byte? freeSlot = session.PlayerEntity.GetNextPartnerWarehouseSlot(); + if (!freeSlot.HasValue) + { + // if partner warehouse is full give another item full stack + anotherInstance.Amount = 999; + if (session.PlayerEntity.IsPartnerWarehouseOpen) + { + session.SendAddPartnerWarehouseItem(sameItem); + } + + return; + } + + anotherInstance.Amount = 999; + + GameItemInstance newItem = _gameItemInstance.CreateItem(anotherInstance.ItemVNum, amount); + session.PlayerEntity.AddPartnerWarehouseItem(newItem, freeSlot.Value); + PartnerWarehouseItem partnerWarehouseItem = session.PlayerEntity.GetPartnerWarehouseItem(freeSlot.Value); + if (session.PlayerEntity.IsPartnerWarehouseOpen) + { + session.SendAddPartnerWarehouseItem(partnerWarehouseItem); + } + + return; + } + + anotherInstance.Amount += amount; + if (session.PlayerEntity.IsPartnerWarehouseOpen) + { + session.SendAddPartnerWarehouseItem(sameItem); + } + + return; + } + + byte? freeItemSlot = session.PlayerEntity.GetNextPartnerWarehouseSlot(); + if (!freeItemSlot.HasValue) + { + return; + } + + session.PlayerEntity.AddPartnerWarehouseItem(itemInstance, freeItemSlot.Value); + PartnerWarehouseItem eqPartnerWarehouseItem = session.PlayerEntity.GetPartnerWarehouseItem(freeItemSlot.Value); + if (session.PlayerEntity.IsPartnerWarehouseOpen) + { + session.SendAddPartnerWarehouseItem(eqPartnerWarehouseItem); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Warehouse/PartnerWarehouseDepositEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Warehouse/PartnerWarehouseDepositEventHandler.cs new file mode 100644 index 0000000..ef44fae --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Warehouse/PartnerWarehouseDepositEventHandler.cs @@ -0,0 +1,150 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsAPI.Game.Extensions.Warehouse; +using WingsEmu.DTOs.Bonus; +using WingsEmu.DTOs.Items; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Features; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Items; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Warehouse; +using WingsEmu.Game.Warehouse.Events; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.Warehouse; + +public class PartnerWarehouseDepositEventHandler : IAsyncEventProcessor +{ + private readonly IGameFeatureToggleManager _gameFeatureToggleManager; + private readonly IGameLanguageService _gameLanguageService; + private readonly IGameItemInstanceFactory _instanceFactory; + + public PartnerWarehouseDepositEventHandler(IGameItemInstanceFactory instanceFactory, IGameFeatureToggleManager gameFeatureToggleManager, IGameLanguageService gameLanguageService) + { + _instanceFactory = instanceFactory; + _gameFeatureToggleManager = gameFeatureToggleManager; + _gameLanguageService = gameLanguageService; + } + + public async Task HandleAsync(PartnerWarehouseDepositEvent e, CancellationToken cancellation) + { + bool disabled = await _gameFeatureToggleManager.IsDisabled(GameFeature.PartnerWarehouse); + if (disabled) + { + e.Sender.SendInfo(_gameLanguageService.GetLanguage(GameDialogKey.GAME_FEATURE_DISABLED, e.Sender.UserLanguage)); + return; + } + + IClientSession session = e.Sender; + InventoryType inventoryType = e.InventoryType; + short inventorySlot = e.InventorySlot; + short amount = e.Amount; + short slotDestination = e.SlotDestination; + + if (!session.PlayerEntity.HaveStaticBonus(StaticBonusType.PartnerBackpack)) + { + return; + } + + if (!session.PlayerEntity.IsPartnerWarehouseOpen) + { + return; + } + + if (session.PlayerEntity.IsInExchange()) + { + return; + } + + if (session.PlayerEntity.HasShopOpened) + { + return; + } + + if (session.PlayerEntity.IsOnVehicle) + { + return; + } + + if (slotDestination >= session.PlayerEntity.GetPartnerWarehouseSlots()) + { + return; + } + + InventoryItem item = session.PlayerEntity.GetItemBySlotAndType(inventorySlot, inventoryType); + + if (item == null) + { + return; + } + + if (item.IsEquipped) + { + return; + } + + if (amount <= 0) + { + return; + } + + if (amount > item.ItemInstance.Amount) + { + return; + } + + if (amount > 999) + { + return; + } + + PartnerWarehouseItem anotherItem = session.PlayerEntity.GetPartnerWarehouseItem(slotDestination); + if (anotherItem == null) + { + if (!session.PlayerEntity.HasSpaceForPartnerWarehouseItem()) + { + return; + } + + GameItemInstance newItem = item.ItemInstance.Type != ItemInstanceType.NORMAL_ITEM + ? _instanceFactory.DuplicateItem(item.ItemInstance) + : _instanceFactory.CreateItem(item.ItemInstance.ItemVNum, amount); + + session.PlayerEntity.AddPartnerWarehouseItem(newItem, slotDestination); + PartnerWarehouseItem partnerWarehouseItem = session.PlayerEntity.GetPartnerWarehouseItem(slotDestination); + session.SendAddPartnerWarehouseItem(partnerWarehouseItem); + await session.RemoveItemFromInventory(item: item, amount: amount); + return; + } + + GameItemInstance itemInstance = item.ItemInstance; + GameItemInstance anotherInstance = anotherItem.ItemInstance; + + if (itemInstance.ItemVNum != anotherInstance.ItemVNum) + { + return; + } + + if (itemInstance.Type != ItemInstanceType.NORMAL_ITEM) + { + return; + } + + if (amount + anotherInstance.Amount > 999) + { + amount = (short)(999 - anotherInstance.Amount); + if (amount <= 0) + { + return; + } + } + + anotherInstance.Amount += amount; + session.SendAddPartnerWarehouseItem(anotherItem); + await session.RemoveItemFromInventory(item: item, amount: amount); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Warehouse/PartnerWarehouseMoveEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Warehouse/PartnerWarehouseMoveEventHandler.cs new file mode 100644 index 0000000..705b23e --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Warehouse/PartnerWarehouseMoveEventHandler.cs @@ -0,0 +1,177 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.Warehouse; +using WingsEmu.DTOs.Bonus; +using WingsEmu.DTOs.Items; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Features; +using WingsEmu.Game.Items; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Warehouse; +using WingsEmu.Game.Warehouse.Events; + +namespace WingsEmu.Plugins.BasicImplementations.Warehouse; + +public class PartnerWarehouseMoveEventHandler : IAsyncEventProcessor +{ + private readonly IGameFeatureToggleManager _gameFeatureToggleManager; + private readonly IGameLanguageService _gameLanguageService; + private readonly IGameItemInstanceFactory _instanceFactory; + + public PartnerWarehouseMoveEventHandler(IGameItemInstanceFactory instanceFactory, IGameFeatureToggleManager gameFeatureToggleManager, IGameLanguageService gameLanguageService) + { + _instanceFactory = instanceFactory; + _gameFeatureToggleManager = gameFeatureToggleManager; + _gameLanguageService = gameLanguageService; + } + + public async Task HandleAsync(PartnerWarehouseMoveEvent e, CancellationToken cancellation) + { + bool disabled = await _gameFeatureToggleManager.IsDisabled(GameFeature.PartnerWarehouse); + if (disabled) + { + e.Sender.SendInfo(_gameLanguageService.GetLanguage(GameDialogKey.GAME_FEATURE_DISABLED, e.Sender.UserLanguage)); + return; + } + + IClientSession session = e.Sender; + short originalSlot = e.OriginalSlot; + short amount = e.Amount; + short newSlot = e.NewSlot; + + if (!session.PlayerEntity.IsPartnerWarehouseOpen) + { + return; + } + + if (originalSlot < 0) + { + return; + } + + if (newSlot < 0) + { + return; + } + + if (newSlot >= session.PlayerEntity.GetPartnerWarehouseSlots()) + { + return; + } + + if (!session.PlayerEntity.HaveStaticBonus(StaticBonusType.PartnerBackpack)) + { + return; + } + + if (session.PlayerEntity.IsOnVehicle) + { + return; + } + + if (session.PlayerEntity.IsInExchange()) + { + return; + } + + if (session.PlayerEntity.HasShopOpened) + { + return; + } + + if (originalSlot == newSlot) + { + return; + } + + PartnerWarehouseItem itemToMove = session.PlayerEntity.GetPartnerWarehouseItem(originalSlot); + if (itemToMove == null) + { + return; + } + + if (amount <= 0) + { + return; + } + + if (amount > 999) + { + return; + } + + if (amount > itemToMove.ItemInstance.Amount) + { + return; + } + + PartnerWarehouseItem anotherItem = session.PlayerEntity.GetPartnerWarehouseItem(newSlot); + if (anotherItem == null) + { + if (amount == itemToMove.ItemInstance.Amount) + { + session.PlayerEntity.RemovePartnerWarehouseItem(itemToMove.Slot); + session.PlayerEntity.AddPartnerWarehouseItem(itemToMove.ItemInstance, newSlot); + session.SendRemovePartnerWarehouseItem(originalSlot); + + PartnerWarehouseItem moved = session.PlayerEntity.GetPartnerWarehouseItem(newSlot); + session.SendAddPartnerWarehouseItem(moved); + return; + } + + GameItemInstance newItem = itemToMove.ItemInstance.Type != ItemInstanceType.NORMAL_ITEM + ? _instanceFactory.DuplicateItem(itemToMove.ItemInstance) + : _instanceFactory.CreateItem(itemToMove.ItemInstance.ItemVNum, amount); + itemToMove.ItemInstance.Amount -= amount; + + session.PlayerEntity.AddPartnerWarehouseItem(newItem, newSlot); + PartnerWarehouseItem addedItem = session.PlayerEntity.GetPartnerWarehouseItem(newSlot); + session.SendAddPartnerWarehouseItem(addedItem); + session.SendAddPartnerWarehouseItem(itemToMove); + return; + } + + GameItemInstance movedInstance = itemToMove.ItemInstance; + GameItemInstance anotherInstance = anotherItem.ItemInstance; + + if (movedInstance.ItemVNum != anotherInstance.ItemVNum || movedInstance.Type != ItemInstanceType.NORMAL_ITEM) + { + session.PlayerEntity.RemovePartnerWarehouseItem(itemToMove.Slot); + session.PlayerEntity.RemovePartnerWarehouseItem(anotherItem.Slot); + + session.PlayerEntity.AddPartnerWarehouseItem(movedInstance, newSlot); + PartnerWarehouseItem movedItem = session.PlayerEntity.GetPartnerWarehouseItem(newSlot); + session.SendAddPartnerWarehouseItem(movedItem); + + session.PlayerEntity.AddPartnerWarehouseItem(anotherInstance, originalSlot); + movedItem = session.PlayerEntity.GetPartnerWarehouseItem(originalSlot); + session.SendAddPartnerWarehouseItem(movedItem); + return; + } + + if (amount + anotherInstance.Amount > 999) + { + amount = (short)(999 - anotherInstance.Amount); + if (amount == 0) + { + return; + } + } + + movedInstance.Amount -= amount; + anotherInstance.Amount += amount; + if (movedInstance.Amount <= 0) + { + session.PlayerEntity.RemovePartnerWarehouseItem(originalSlot); + session.SendRemovePartnerWarehouseItem(originalSlot); + } + else + { + session.SendAddPartnerWarehouseItem(itemToMove); + } + + session.SendAddPartnerWarehouseItem(anotherItem); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Warehouse/PartnerWarehouseWithdrawEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Warehouse/PartnerWarehouseWithdrawEventHandler.cs new file mode 100644 index 0000000..fb03f87 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Warehouse/PartnerWarehouseWithdrawEventHandler.cs @@ -0,0 +1,118 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsAPI.Game.Extensions.Warehouse; +using WingsEmu.DTOs.Items; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Features; +using WingsEmu.Game.Items; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Warehouse; +using WingsEmu.Game.Warehouse.Events; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.BasicImplementations.Warehouse; + +public class PartnerWarehouseWithdrawEventHandler : IAsyncEventProcessor +{ + private readonly IGameFeatureToggleManager _gameFeatureToggleManager; + private readonly IGameItemInstanceFactory _gameItemInstanceFactory; + private readonly IGameLanguageService _gameLanguage; + + public PartnerWarehouseWithdrawEventHandler(IGameLanguageService gameLanguage, IGameItemInstanceFactory gameItemInstanceFactory, IGameFeatureToggleManager gameFeatureToggleManager) + { + _gameLanguage = gameLanguage; + _gameItemInstanceFactory = gameItemInstanceFactory; + _gameFeatureToggleManager = gameFeatureToggleManager; + } + + public async Task HandleAsync(PartnerWarehouseWithdrawEvent e, CancellationToken cancellation) + { + bool disabled = await _gameFeatureToggleManager.IsDisabled(GameFeature.PartnerWarehouse); + if (disabled) + { + e.Sender.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.GAME_FEATURE_DISABLED, e.Sender.UserLanguage)); + return; + } + + IClientSession session = e.Sender; + short slot = e.Slot; + short amount = e.Amount; + + if (slot < 0) + { + return; + } + + if (!session.PlayerEntity.IsPartnerWarehouseOpen) + { + return; + } + + if (session.PlayerEntity.IsInExchange()) + { + return; + } + + if (session.PlayerEntity.HasShopOpened) + { + return; + } + + if (session.PlayerEntity.IsOnVehicle) + { + return; + } + + if (slot >= session.PlayerEntity.GetPartnerWarehouseSlotsWithoutBackpack()) + { + return; + } + + PartnerWarehouseItem item = session.PlayerEntity.GetPartnerWarehouseItem(slot); + if (item == null) + { + return; + } + + if (amount <= 0) + { + return; + } + + if (amount > item.ItemInstance.Amount) + { + return; + } + + if (amount > 999) + { + return; + } + + if (!session.PlayerEntity.HasSpaceFor(item.ItemInstance.ItemVNum, amount)) + { + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.INTERACTION_MESSAGE_NOT_ENOUGH_PLACE, session.UserLanguage), ChatMessageColorType.Yellow); + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.INTERACTION_MESSAGE_NOT_ENOUGH_PLACE, session.UserLanguage), MsgMessageType.Middle); + return; + } + + GameItemInstance itemInstance = item.ItemInstance.Type != ItemInstanceType.NORMAL_ITEM + ? _gameItemInstanceFactory.DuplicateItem(item.ItemInstance) + : _gameItemInstanceFactory.CreateItem(item.ItemInstance.ItemVNum, amount); + item.ItemInstance.Amount -= amount; + if (item.ItemInstance.Amount <= 0) + { + session.PlayerEntity.RemovePartnerWarehouseItem(slot); + session.SendRemovePartnerWarehouseItem(slot); + } + else + { + session.SendAddPartnerWarehouseItem(item); + } + + await session.AddNewItemToInventory(itemInstance); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Warehouse/Warehouse.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Warehouse/Warehouse.cs new file mode 100644 index 0000000..e68a248 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Warehouse/Warehouse.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using System.Linq; +using WingsEmu.Game.Characters; + +namespace WingsEmu.Game.Warehouse; + +public class Warehouse : IWarehouse +{ + private const byte WARHOUSE_SLOTS = 98; + + private readonly IPlayerEntity _playerEntity; + private readonly WarehouseItem[] _warehouseItems = new WarehouseItem[WARHOUSE_SLOTS]; + + public Warehouse(IPlayerEntity playerEntity) => _playerEntity = playerEntity; + + public void AddWarehouseItem(WarehouseItem item, bool force = false) + { + if (_playerEntity.WareHouseSize <= 0 && !force) + { + return; + } + + _warehouseItems[item.Slot] = item; + } + + public void RemoveWarehouseItem(short slot) + { + WarehouseItem partnerWarehouseItem = GetWarehouseItem(slot); + if (partnerWarehouseItem == null) + { + return; + } + + _warehouseItems[partnerWarehouseItem.Slot] = null; + } + + public WarehouseItem GetWarehouseItem(short slot) => _warehouseItems[slot]; + + public IReadOnlyList WarehouseItems() => _warehouseItems; + public int GetWarehouseSlots() => _playerEntity.WareHouseSize; + public bool HasSpaceForWarehouseItem() => _warehouseItems.Count(x => x != null) <= WARHOUSE_SLOTS; +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Warehouse/WarehouseExtensions.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Warehouse/WarehouseExtensions.cs new file mode 100644 index 0000000..3ff4440 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Warehouse/WarehouseExtensions.cs @@ -0,0 +1,18 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using PhoenixLib.Events; +using WingsEmu.Game.Warehouse; +using WingsEmu.Game.Warehouse.Events; + +namespace WingsEmu.Plugins.BasicImplementations.Warehouse; + +public static class WarehouseExtensions +{ + public static void AddWarehouseModule(this IServiceCollection services) + { + services.TryAddSingleton(); + services.TryAddTransient(); + services.TryAddTransient, AccountWarehouseAddEventHandler>(); + services.TryAddTransient, AccountWarehouseWithdrawEventHandler>(); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Warehouse/WarehouseFactory.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Warehouse/WarehouseFactory.cs new file mode 100644 index 0000000..5db6b5a --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Warehouse/WarehouseFactory.cs @@ -0,0 +1,9 @@ +using WingsEmu.Game.Characters; +using WingsEmu.Plugins.BasicImplementations.Warehouse; + +namespace WingsEmu.Game.Warehouse; + +public class WarehouseFactory : IWarehouseFactory +{ + public IWarehouse Create(IPlayerEntity entity) => new Warehouse(entity); +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/WingsEmu.Plugins.BasicImplementations.csproj b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/WingsEmu.Plugins.BasicImplementations.csproj new file mode 100644 index 0000000..b835af4 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/WingsEmu.Plugins.BasicImplementations.csproj @@ -0,0 +1,26 @@ + + + + net5.0 + preview + + + + + + + + + + + + + + + + + + + + + diff --git a/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/BotMessages/BotMessageEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/BotMessages/BotMessageEventHandler.cs new file mode 100644 index 0000000..158448e --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/BotMessages/BotMessageEventHandler.cs @@ -0,0 +1,25 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.DistributedGameEvents.BotMessages +{ + public class BotMessageConsumer : IMessageConsumer + { + private readonly ISessionManager _sessionManager; + + public BotMessageConsumer(ISessionManager sessionManager) => _sessionManager = sessionManager; + + public async Task HandleAsync(BotMessageMessage e, CancellationToken cancellation) + { + await _sessionManager.BroadcastAsync(async x => x.GenerateMsgPacket(e.Message, MsgMessageType.Middle)); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/BotMessages/BotMessageMessage.cs b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/BotMessages/BotMessageMessage.cs new file mode 100644 index 0000000..2f2e8a7 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/BotMessages/BotMessageMessage.cs @@ -0,0 +1,15 @@ +// WingsEmu +// +// Developed by NosWings Team + +using PhoenixLib.ServiceBus; +using PhoenixLib.ServiceBus.Routing; + +namespace WingsEmu.Plugins.DistributedGameEvents.BotMessages +{ + [MessageType("game.bot.broadcast-message")] + public class BotMessageMessage : IMessage + { + public string Message { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/BotMessages/Extensions/DependencyInjectionExtensions.cs b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/BotMessages/Extensions/DependencyInjectionExtensions.cs new file mode 100644 index 0000000..045a1ec --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/BotMessages/Extensions/DependencyInjectionExtensions.cs @@ -0,0 +1,25 @@ +// WingsEmu +// +// Developed by NosWings Team + +using Microsoft.Extensions.DependencyInjection; +using PhoenixLib.Configuration; +using PhoenixLib.Events; +using PhoenixLib.ServiceBus.Extensions; + +namespace WingsEmu.Plugins.DistributedGameEvents.BotMessages.Extensions +{ + public static class DependencyInjectionExtensions + { + public static void AddRecurrentBotMessagesGameModule(this IServiceCollection services) + { + services.AddEventHandlersInAssembly(); + services.AddMessageSubscriber(); + } + + public static void AddRecurrentBotMessagesSchedulerModule(this IServiceCollection services) + { + services.AddMultipleConfigurationOneFile("bot_messages"); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/BotMessages/ScheduledBotMessageConfiguration.cs b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/BotMessages/ScheduledBotMessageConfiguration.cs new file mode 100644 index 0000000..443048a --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/BotMessages/ScheduledBotMessageConfiguration.cs @@ -0,0 +1,9 @@ +using WingsEmu.Game._i18n; + +namespace WingsEmu.Plugins.DistributedGameEvents.BotMessages +{ + public class ScheduledBotMessageConfiguration : SchedulableConfiguration + { + public GameDialogKey Message { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/InterChannel/BazaarNotificationMessage.cs b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/InterChannel/BazaarNotificationMessage.cs new file mode 100644 index 0000000..bdbcba6 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/InterChannel/BazaarNotificationMessage.cs @@ -0,0 +1,15 @@ +using PhoenixLib.ServiceBus; +using PhoenixLib.ServiceBus.Routing; + +namespace WingsEmu.Plugins.DistributedGameEvents.InterChannel +{ + [MessageType("bazaar.notification")] + public class BazaarNotificationMessage : IMessage + { + public string OwnerName { get; set; } + + public int ItemVnum { get; set; } + + public int Amount { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/InterChannel/ChatShoutAdminMessage.cs b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/InterChannel/ChatShoutAdminMessage.cs new file mode 100644 index 0000000..c29dd6d --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/InterChannel/ChatShoutAdminMessage.cs @@ -0,0 +1,11 @@ +using PhoenixLib.ServiceBus; +using PhoenixLib.ServiceBus.Routing; + +namespace WingsEmu.Plugins.DistributedGameEvents.InterChannel +{ + [MessageType("chat.shout.adminmessage")] + public class ChatShoutAdminMessage : IMessage + { + public string Message { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/InterChannel/InterChannelChatMessageBroadcastMessage.cs b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/InterChannel/InterChannelChatMessageBroadcastMessage.cs new file mode 100644 index 0000000..372f21d --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/InterChannel/InterChannelChatMessageBroadcastMessage.cs @@ -0,0 +1,17 @@ +using PhoenixLib.ServiceBus; +using PhoenixLib.ServiceBus.Routing; +using WingsEmu.Game._i18n; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.DistributedGameEvents.InterChannel +{ + [MessageType("interchannel.broadcast.chatmessage")] + public class InterChannelChatMessageBroadcastMessage : IMessage + { + public GameDialogKey DialogKey { get; set; } + + public ChatMessageColorType ChatMessageColorType { get; set; } + + public object?[] Args { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/InterChannel/InterChannelSendChatMsgByCharIdMessage.cs b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/InterChannel/InterChannelSendChatMsgByCharIdMessage.cs new file mode 100644 index 0000000..7c06f7e --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/InterChannel/InterChannelSendChatMsgByCharIdMessage.cs @@ -0,0 +1,17 @@ +using PhoenixLib.ServiceBus; +using PhoenixLib.ServiceBus.Routing; +using WingsEmu.Game._i18n; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.DistributedGameEvents.InterChannel +{ + [MessageType("interchannel.sendbycharid.chatmsg")] + public class InterChannelSendChatMsgByCharIdMessage : IMessage + { + public long CharacterId { get; set; } + + public GameDialogKey DialogKey { get; set; } + + public ChatMessageColorType ChatMessageColorType { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/InterChannel/InterChannelSendChatMsgByNicknameMessage.cs b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/InterChannel/InterChannelSendChatMsgByNicknameMessage.cs new file mode 100644 index 0000000..0edd400 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/InterChannel/InterChannelSendChatMsgByNicknameMessage.cs @@ -0,0 +1,17 @@ +using PhoenixLib.ServiceBus; +using PhoenixLib.ServiceBus.Routing; +using WingsEmu.Game._i18n; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.DistributedGameEvents.InterChannel +{ + [MessageType("interchannel.sendbyname.chatmsg")] + public class InterChannelSendChatMsgByNicknameMessage : IMessage + { + public string Nickname { get; set; } + + public GameDialogKey DialogKey { get; set; } + + public ChatMessageColorType ChatMessageColorType { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/InterChannel/InterChannelSendInfoByCharIdMessage.cs b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/InterChannel/InterChannelSendInfoByCharIdMessage.cs new file mode 100644 index 0000000..ecd72dc --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/InterChannel/InterChannelSendInfoByCharIdMessage.cs @@ -0,0 +1,14 @@ +using PhoenixLib.ServiceBus; +using PhoenixLib.ServiceBus.Routing; +using WingsEmu.Game._i18n; + +namespace WingsEmu.Plugins.DistributedGameEvents.InterChannel +{ + [MessageType("interchannel.sendbycharid.info")] + public class InterChannelSendInfoByCharIdMessage : IMessage + { + public long CharacterId { get; set; } + + public GameDialogKey DialogKey { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/InterChannel/InterChannelSendInfoByNicknameMessage.cs b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/InterChannel/InterChannelSendInfoByNicknameMessage.cs new file mode 100644 index 0000000..2b0c091 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/InterChannel/InterChannelSendInfoByNicknameMessage.cs @@ -0,0 +1,14 @@ +using PhoenixLib.ServiceBus; +using PhoenixLib.ServiceBus.Routing; +using WingsEmu.Game._i18n; + +namespace WingsEmu.Plugins.DistributedGameEvents.InterChannel +{ + [MessageType("interchannel.sendbyname.info")] + public class InterChannelSendInfoByNicknameMessage : IMessage + { + public string Nickname { get; set; } + + public GameDialogKey DialogKey { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/InterChannel/InterChannelSendWhisperMessage.cs b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/InterChannel/InterChannelSendWhisperMessage.cs new file mode 100644 index 0000000..fa6d825 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/InterChannel/InterChannelSendWhisperMessage.cs @@ -0,0 +1,20 @@ +using PhoenixLib.ServiceBus; +using PhoenixLib.ServiceBus.Routing; +using WingsEmu.DTOs.Account; + +namespace WingsEmu.Plugins.DistributedGameEvents.InterChannel +{ + [MessageType("interchannel.sendbyname.whisper")] + public class InterChannelSendWhisperMessage : IMessage + { + public long SenderCharacterId { get; set; } + public string SenderNickname { get; set; } + public int SenderChannelId { get; set; } + + public AuthorityType AuthorityType { get; set; } + + public string ReceiverNickname { get; set; } + + public string Message { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Mails/MailReceivePendingOnConnectedMessage.cs b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Mails/MailReceivePendingOnConnectedMessage.cs new file mode 100644 index 0000000..76193c4 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Mails/MailReceivePendingOnConnectedMessage.cs @@ -0,0 +1,22 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using PhoenixLib.ServiceBus; +using PhoenixLib.ServiceBus.Routing; +using WingsEmu.DTOs.Mails; + +namespace WingsEmu.Plugins.DistributedGameEvents.Mails +{ + /// + /// Limit of 50 + /// + [MessageType("mail.connected.message")] + public class MailReceivePendingOnConnectedMessage : IMessage + { + public long CharacterId { get; set; } + + public List Mails { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Mails/MailReceivePendingOnConnectedMessageConsumer.cs b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Mails/MailReceivePendingOnConnectedMessageConsumer.cs new file mode 100644 index 0000000..6d2bf16 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Mails/MailReceivePendingOnConnectedMessageConsumer.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; +using WingsAPI.Game.Extensions.PacketGeneration; +using WingsEmu.DTOs.Mails; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Items; +using WingsEmu.Game.Mails; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.DistributedGameEvents.Mails +{ + public class MailReceivePendingOnConnectedMessageConsumer : IMessageConsumer + { + private readonly IGameLanguageService _gameLanguage; + private readonly IGameItemInstanceFactory _itemInstanceFactory; + private readonly ISessionManager _sessionManager; + + public MailReceivePendingOnConnectedMessageConsumer(IGameLanguageService gameLanguage, ISessionManager sessionManager, IGameItemInstanceFactory itemInstanceFactory) + { + _gameLanguage = gameLanguage; + _sessionManager = sessionManager; + _itemInstanceFactory = itemInstanceFactory; + } + + public async Task HandleAsync(MailReceivePendingOnConnectedMessage e, CancellationToken cancellation) + { + long characterId = e.CharacterId; + List mails = e.Mails; + + IClientSession session = _sessionManager.GetSessionByCharacterId(characterId); + if (session == null) + { + return; + } + + foreach (CharacterMailDto mail in mails) + { + byte slot = session.GetNextMailSlot(); + GameItemInstance itemInstance = _itemInstanceFactory.CreateItem(mail.ItemInstance); + var newMail = new CharacterMail(mail, slot, itemInstance); + + session.PlayerEntity.MailNoteComponent.AddMail(newMail); + session.SendParcel(newMail); + } + + session.SendChatMessage(_gameLanguage.GetLanguageFormat(GameDialogKey.MAIL_CHATMESSAGE_YOU_HAVE_X_MAILS, session.UserLanguage, mails.Count), ChatMessageColorType.Green); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Mails/MailReceivedMessage.cs b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Mails/MailReceivedMessage.cs new file mode 100644 index 0000000..34768b4 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Mails/MailReceivedMessage.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using PhoenixLib.ServiceBus; +using PhoenixLib.ServiceBus.Routing; +using WingsEmu.DTOs.Mails; + +namespace WingsEmu.Plugins.DistributedGameEvents.Mails +{ + [MessageType("mail.receive.message")] + public class MailReceivedMessage : IMessage + { + public long CharacterId { get; set; } + + public IEnumerable MailDtos { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Mails/MailReceivedMessageConsumer.cs b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Mails/MailReceivedMessageConsumer.cs new file mode 100644 index 0000000..4b79986 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Mails/MailReceivedMessageConsumer.cs @@ -0,0 +1,49 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; +using WingsAPI.Game.Extensions.PacketGeneration; +using WingsEmu.DTOs.Mails; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Items; +using WingsEmu.Game.Mails; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.DistributedGameEvents.Mails +{ + public class MailReceivedMessageConsumer : IMessageConsumer + { + private readonly IGameLanguageService _gameLanguage; + private readonly IGameItemInstanceFactory _itemInstanceFactory; + private readonly ISessionManager _sessionManager; + + public MailReceivedMessageConsumer(IGameLanguageService gameLanguage, ISessionManager sessionManager, IGameItemInstanceFactory itemInstanceFactory) + { + _gameLanguage = gameLanguage; + _sessionManager = sessionManager; + _itemInstanceFactory = itemInstanceFactory; + } + + public async Task HandleAsync(MailReceivedMessage e, CancellationToken cancellation) + { + IClientSession session = _sessionManager.GetSessionByCharacterId(e.CharacterId); + if (session == null) + { + return; + } + + foreach (CharacterMailDto dto in e.MailDtos.ToArray()) + { + GameItemInstance itemInstance = _itemInstanceFactory.CreateItem(dto.ItemInstance); + + var newMail = new CharacterMail(dto, session.GetNextMailSlot(), itemInstance); + session.PlayerEntity.MailNoteComponent.AddMail(newMail); + session.SendParcel(newMail); + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.MAIL_CHATMESSAGE_NEW_MAIL_RECEIVE, session.UserLanguage), ChatMessageColorType.Green); + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Mails/NoteReceivePendingOnConnectedMessage.cs b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Mails/NoteReceivePendingOnConnectedMessage.cs new file mode 100644 index 0000000..8732fb6 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Mails/NoteReceivePendingOnConnectedMessage.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using PhoenixLib.ServiceBus; +using PhoenixLib.ServiceBus.Routing; +using WingsEmu.DTOs.Mails; + +namespace WingsEmu.Plugins.DistributedGameEvents.Mails +{ + /// + /// Limit of X + /// + [MessageType("note.connected.message")] + public class NoteReceivePendingOnConnectedMessage : IMessage + { + public long CharacterId { get; set; } + + public List Notes { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Mails/NoteReceivePendingOnConnectedMessageConsumer.cs b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Mails/NoteReceivePendingOnConnectedMessageConsumer.cs new file mode 100644 index 0000000..381f708 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Mails/NoteReceivePendingOnConnectedMessageConsumer.cs @@ -0,0 +1,57 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; +using WingsAPI.Game.Extensions.PacketGeneration; +using WingsEmu.DTOs.Mails; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Mails; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.DistributedGameEvents.Mails +{ + public class NoteReceivePendingOnConnectedMessageConsumer : IMessageConsumer + { + private readonly IGameLanguageService _gameLanguage; + private readonly ISessionManager _sessionManager; + + public NoteReceivePendingOnConnectedMessageConsumer(IGameLanguageService gameLanguage, ISessionManager sessionManager) + { + _gameLanguage = gameLanguage; + _sessionManager = sessionManager; + } + + public async Task HandleAsync(NoteReceivePendingOnConnectedMessage e, CancellationToken cancellation) + { + long characterId = e.CharacterId; + List notes = e.Notes; + + IClientSession session = _sessionManager.GetSessionByCharacterId(characterId); + if (session == null) + { + return; + } + + foreach (CharacterNoteDto note in notes) + { + byte slot = session.GetNextNoteSlot(note.IsSenderCopy); + var newNote = new CharacterNote(note, slot); + + session.PlayerEntity.MailNoteComponent.AddNote(newNote); + session.SendMailPacket(newNote); + } + + int notOpenedNotes = notes.Count(x => !x.IsOpened && !x.IsSenderCopy); + if (notOpenedNotes == 0) + { + return; + } + + session.SendChatMessage(_gameLanguage.GetLanguageFormat(GameDialogKey.NOTE_CHATMESSAGE_YOU_HAVE_X_NOTES, session.UserLanguage, notOpenedNotes), ChatMessageColorType.Green); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Mails/NoteReceivedMessage.cs b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Mails/NoteReceivedMessage.cs new file mode 100644 index 0000000..0ef616c --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Mails/NoteReceivedMessage.cs @@ -0,0 +1,14 @@ +using PhoenixLib.ServiceBus; +using PhoenixLib.ServiceBus.Routing; +using WingsEmu.DTOs.Mails; + +namespace WingsEmu.Plugins.DistributedGameEvents.PlayerEvents +{ + [MessageType("note.receive.message")] + public class NoteReceivedMessage : IMessage + { + public CharacterNoteDto SenderNote { get; set; } + + public CharacterNoteDto ReceiverNote { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Mails/NoteReceivedMessageConsumer.cs b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Mails/NoteReceivedMessageConsumer.cs new file mode 100644 index 0000000..0b0c09a --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Mails/NoteReceivedMessageConsumer.cs @@ -0,0 +1,64 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; +using WingsAPI.Game.Extensions.PacketGeneration; +using WingsEmu.DTOs.Mails; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Mails; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums.Chat; +using WingsEmu.Plugins.DistributedGameEvents.PlayerEvents; + +namespace WingsEmu.Plugins.DistributedGameEvents.Consumer +{ + public class NoteReceivedMessageConsumer : IMessageConsumer + { + private readonly IGameLanguageService _gameLanguage; + private readonly ISessionManager _sessionManager; + + public NoteReceivedMessageConsumer(IGameLanguageService gameLanguage, ISessionManager sessionManager) + { + _gameLanguage = gameLanguage; + _sessionManager = sessionManager; + } + + public async Task HandleAsync(NoteReceivedMessage e, CancellationToken cancellation) + { + CharacterNoteDto senderNote = e.SenderNote; + CharacterNoteDto receiverNote = e.ReceiverNote; + + SenderNote(senderNote); + + IClientSession receiver = _sessionManager.GetSessionByCharacterId(receiverNote.ReceiverId); + if (receiver == null) + { + return; + } + + var newReceiverNote = new CharacterNote(receiverNote, receiver.GetNextNoteSlot(receiverNote.IsSenderCopy)); + receiver.PlayerEntity.MailNoteComponent.AddNote(newReceiverNote); + receiver.SendMailPacket(newReceiverNote); + receiver.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.NOTE_CHATMESSAGE_NEW_NOTE, receiver.UserLanguage), ChatMessageColorType.Green); + } + + private void SenderNote(CharacterNoteDto senderNote) + { + if (!senderNote.IsSenderCopy) + { + return; + } + + IClientSession session = _sessionManager.GetSessionByCharacterId(senderNote.SenderId); + if (session == null) + { + return; + } + + var newSenderNote = new CharacterNote(senderNote, session.GetNextNoteSlot(senderNote.IsSenderCopy)); + session.PlayerEntity.MailNoteComponent.AddNote(newSenderNote); + session.SendMailPacket(newSenderNote); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/PlayerEvents/KickAccountMessage.cs b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/PlayerEvents/KickAccountMessage.cs new file mode 100644 index 0000000..b5fd426 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/PlayerEvents/KickAccountMessage.cs @@ -0,0 +1,11 @@ +using PhoenixLib.ServiceBus; +using PhoenixLib.ServiceBus.Routing; + +namespace WingsEmu.Plugins.DistributedGameEvents.PlayerEvents +{ + [MessageType("kick.account")] + public class KickAccountMessage : IMessage + { + public long AccountId { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/PlayerEvents/PlayerConnectedOnChannelMessage.cs b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/PlayerEvents/PlayerConnectedOnChannelMessage.cs new file mode 100644 index 0000000..82f39a7 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/PlayerEvents/PlayerConnectedOnChannelMessage.cs @@ -0,0 +1,24 @@ +// WingsEmu +// +// Developed by NosWings Team + +using PhoenixLib.ServiceBus; +using PhoenixLib.ServiceBus.Routing; +using WingsEmu.Packets.Enums.Character; + +namespace WingsEmu.Plugins.DistributedGameEvents.PlayerEvents +{ + [MessageType("player.connected")] + public class PlayerConnectedOnChannelMessage : IMessage + { + public int ChannelId { get; init; } + public long CharacterId { get; init; } + public string CharacterName { get; init; } + public GenderType Gender { get; init; } + public ClassType Class { get; init; } + public byte Level { get; init; } + public byte HeroLevel { get; init; } + public long? FamilyId { get; init; } + public string HardwareId { get; init; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/PlayerEvents/PlayerDisconnectedChannelMessage.cs b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/PlayerEvents/PlayerDisconnectedChannelMessage.cs new file mode 100644 index 0000000..7bd3e09 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/PlayerEvents/PlayerDisconnectedChannelMessage.cs @@ -0,0 +1,16 @@ +using System; +using PhoenixLib.ServiceBus; +using PhoenixLib.ServiceBus.Routing; + +namespace WingsEmu.Plugins.DistributedGameEvents.PlayerEvents +{ + [MessageType("player.disconnected")] + public class PlayerDisconnectedChannelMessage : IMessage + { + public int ChannelId { get; set; } + public long CharacterId { get; set; } + public string CharacterName { get; set; } + public DateTime DisconnectionTime { get; set; } + public long? FamilyId { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Relation/RelationCharacterAddMessage.cs b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Relation/RelationCharacterAddMessage.cs new file mode 100644 index 0000000..dd31c6c --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Relation/RelationCharacterAddMessage.cs @@ -0,0 +1,13 @@ +using PhoenixLib.ServiceBus; +using PhoenixLib.ServiceBus.Routing; +using WingsEmu.DTOs.Relations; + +namespace WingsEmu.Plugins.DistributedGameEvents.Relation +{ + [MessageType("relation.character.add")] + public class RelationCharacterAddMessage : IMessage + { + public CharacterRelationDTO SenderRelation { get; set; } + public CharacterRelationDTO TargetRelation { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Relation/RelationCharacterAddMessageConsumer.cs b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Relation/RelationCharacterAddMessageConsumer.cs new file mode 100644 index 0000000..a1e9ded --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Relation/RelationCharacterAddMessageConsumer.cs @@ -0,0 +1,59 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; +using WingsEmu.DTOs.Relations; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums.Relations; + +namespace WingsEmu.Plugins.DistributedGameEvents.Relation +{ + public class RelationCharacterAddMessageConsumer : IMessageConsumer + { + private readonly ISessionManager _sessionManager; + + public RelationCharacterAddMessageConsumer(ISessionManager sessionManager) => _sessionManager = sessionManager; + + public async Task HandleAsync(RelationCharacterAddMessage notification, CancellationToken token) + { + CharacterRelationDTO senderRelation = notification.SenderRelation; + CharacterRelationDTO targetRelation = notification.TargetRelation; + + IClientSession session = null; + IClientSession target = null; + + if (senderRelation != null) + { + session = _sessionManager.GetSessionByCharacterId(senderRelation.CharacterId); + } + + if (targetRelation != null) + { + target = _sessionManager.GetSessionByCharacterId(targetRelation.CharacterId); + } + + if (senderRelation?.RelationType != CharacterRelationType.Blocked) + { + target?.PlayerEntity.AddRelation(targetRelation); + } + + session?.PlayerEntity.AddRelation(senderRelation); + + switch (senderRelation?.RelationType) + { + case CharacterRelationType.Blocked: + session?.RefreshBlackList(); + break; + case CharacterRelationType.Spouse: + case CharacterRelationType.Friend: + session?.RefreshFriendList(_sessionManager); + target?.RefreshFriendList(_sessionManager); + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Relation/RelationCharacterJoinMessage.cs b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Relation/RelationCharacterJoinMessage.cs new file mode 100644 index 0000000..9f103de --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Relation/RelationCharacterJoinMessage.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using PhoenixLib.ServiceBus; +using PhoenixLib.ServiceBus.Routing; +using WingsEmu.DTOs.Relations; + +namespace WingsEmu.Plugins.DistributedGameEvents.Relation +{ + [MessageType("relation.character.join")] + public class RelationCharacterJoinMessage : IMessage + { + public long CharacterId { get; set; } + + public string CharacterName { get; set; } + + public List Relations { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Relation/RelationCharacterJoinMessageConsumer.cs b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Relation/RelationCharacterJoinMessageConsumer.cs new file mode 100644 index 0000000..8452f8c --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Relation/RelationCharacterJoinMessageConsumer.cs @@ -0,0 +1,57 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; +using WingsAPI.Game.Extensions.CharacterExtensions; +using WingsEmu.DTOs.Relations; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums.Relations; + +namespace WingsEmu.Plugins.DistributedGameEvents.Relation +{ + public class RelationCharacterJoinMessageConsumer : IMessageConsumer + { + private readonly IGameLanguageService _gameLanguage; + private readonly ISessionManager _sessionManager; + + public RelationCharacterJoinMessageConsumer(ISessionManager sessionManager, IGameLanguageService gameLanguage) + { + _sessionManager = sessionManager; + _gameLanguage = gameLanguage; + } + + public async Task HandleAsync(RelationCharacterJoinMessage notification, CancellationToken token) + { + long characterId = notification.CharacterId; + string characterName = notification.CharacterName; + IClientSession session = _sessionManager.GetSessionByCharacterId(characterId); + + List relations = notification.Relations; + foreach (CharacterRelationDTO relation in relations) + { + session?.PlayerEntity.AddRelation(relation); + + if (relation.RelationType == CharacterRelationType.Blocked) + { + continue; + } + + IClientSession target = _sessionManager.GetSessionByCharacterId(relation.RelatedCharacterId); + if (target == null || !_sessionManager.IsOnline(relation.RelatedCharacterId)) + { + continue; + } + + string message = _gameLanguage.GetLanguageFormat(GameDialogKey.FRIEND_CHATMESSAGE_CHARACTER_LOGGED_IN, target.UserLanguage, characterName); + target.SendInformationChatMessage(message); + target.SendFriendOnlineInfo(characterId, characterName, true); + } + + session?.RefreshFriendList(_sessionManager); + session?.RefreshBlackList(); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Relation/RelationCharacterLeaveMessage.cs b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Relation/RelationCharacterLeaveMessage.cs new file mode 100644 index 0000000..066c01d --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Relation/RelationCharacterLeaveMessage.cs @@ -0,0 +1,12 @@ +using PhoenixLib.ServiceBus; +using PhoenixLib.ServiceBus.Routing; + +namespace WingsEmu.Plugins.DistributedGameEvents.Relation +{ + [MessageType("relation.character.leave")] + public class RelationCharacterLeaveMessage : IMessage + { + public long CharacterId { get; set; } + public string CharacterName { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Relation/RelationCharacterLeaveMessageConsumer.cs b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Relation/RelationCharacterLeaveMessageConsumer.cs new file mode 100644 index 0000000..ec77b3b --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Relation/RelationCharacterLeaveMessageConsumer.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Logging; +using PhoenixLib.ServiceBus; +using WingsAPI.Communication; +using WingsAPI.Communication.Relation; +using WingsAPI.Game.Extensions.CharacterExtensions; +using WingsEmu.DTOs.Relations; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums.Relations; + +namespace WingsEmu.Plugins.DistributedGameEvents.Relation +{ + public class RelationCharacterLeaveMessageConsumer : IMessageConsumer + { + private readonly IGameLanguageService _gameLanguage; + private readonly IRelationService _relationService; + private readonly ISessionManager _sessionManager; + + public RelationCharacterLeaveMessageConsumer(IGameLanguageService gameLanguage, ISessionManager sessionManager, IRelationService relationService) + { + _gameLanguage = gameLanguage; + _sessionManager = sessionManager; + _relationService = relationService; + } + + public async Task HandleAsync(RelationCharacterLeaveMessage notification, CancellationToken token) + { + long characterId = notification.CharacterId; + string characterName = notification.CharacterName; + + RelationGetAllResponse response = null; + try + { + response = await _relationService.GetRelationsByIdAsync(new RelationGetAllRequest + { + CharacterId = characterId + }); + } + catch (Exception e) + { + Log.Error("[RELATION_CHARACTER_LEAVE] Unexpected error: ", e); + } + + if (response?.ResponseType != RpcResponseType.SUCCESS) + { + return; + } + + IReadOnlyList relations = response.CharacterRelationDtos ?? new List(); + + if (!relations.Any()) + { + return; + } + + foreach (CharacterRelationDTO relation in relations) + { + if (relation.RelationType == CharacterRelationType.Blocked) + { + continue; + } + + IClientSession target = _sessionManager.GetSessionByCharacterId(relation.RelatedCharacterId); + if (target == null || !_sessionManager.IsOnline(relation.RelatedCharacterId)) + { + continue; + } + + string message = _gameLanguage.GetLanguageFormat(GameDialogKey.FRIEND_CHATMESSAGE_CHARACTER_LOGGED_OUT, target.UserLanguage, characterName); + target.SendInformationChatMessage(message); + target.SendFriendOnlineInfo(characterId, characterName, false); + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Relation/RelationCharacterRemoveMessage.cs b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Relation/RelationCharacterRemoveMessage.cs new file mode 100644 index 0000000..060e7eb --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Relation/RelationCharacterRemoveMessage.cs @@ -0,0 +1,14 @@ +using PhoenixLib.ServiceBus; +using PhoenixLib.ServiceBus.Routing; +using WingsEmu.Packets.Enums.Relations; + +namespace WingsEmu.Plugins.DistributedGameEvents.Relation +{ + [MessageType("relation.character.remove")] + public class RelationCharacterRemoveMessage : IMessage + { + public long CharacterId { get; set; } + public long TargetId { get; set; } + public CharacterRelationType RelationType { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Relation/RelationCharacterRemoveMessageConsumer.cs b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Relation/RelationCharacterRemoveMessageConsumer.cs new file mode 100644 index 0000000..2b933b3 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Relation/RelationCharacterRemoveMessageConsumer.cs @@ -0,0 +1,52 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums.Relations; + +namespace WingsEmu.Plugins.DistributedGameEvents.Relation +{ + public class RelationCharacterRemoveMessageConsumer : IMessageConsumer + { + private readonly ISessionManager _sessionManager; + + public RelationCharacterRemoveMessageConsumer(ISessionManager sessionManager) => _sessionManager = sessionManager; + + public async Task HandleAsync(RelationCharacterRemoveMessage notification, CancellationToken token) + { + long characterId = notification.CharacterId; + long targetId = notification.TargetId; + CharacterRelationType relationType = notification.RelationType; + IClientSession session = _sessionManager.GetSessionByCharacterId(characterId); + IClientSession target = _sessionManager.GetSessionByCharacterId(targetId); + + session?.PlayerEntity.RemoveRelation(targetId, relationType); + switch (relationType) + { + case CharacterRelationType.Blocked: + session?.RefreshBlackList(); + break; + case CharacterRelationType.Spouse: + case CharacterRelationType.Friend: + session?.RefreshFriendList(_sessionManager); + break; + } + + if (relationType == CharacterRelationType.Blocked) + { + return; + } + + target?.PlayerEntity.RemoveRelation(characterId, relationType); + switch (relationType) + { + case CharacterRelationType.Spouse: + case CharacterRelationType.Friend: + target?.RefreshFriendList(_sessionManager); + break; + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Relation/RelationSendTalkMessage.cs b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Relation/RelationSendTalkMessage.cs new file mode 100644 index 0000000..572475b --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Relation/RelationSendTalkMessage.cs @@ -0,0 +1,13 @@ +using PhoenixLib.ServiceBus; +using PhoenixLib.ServiceBus.Routing; + +namespace WingsEmu.Plugins.DistributedGameEvents.Relation +{ + [MessageType("relation.send.talk")] + public class RelationSendTalkMessage : IMessage + { + public long SenderId { get; set; } + public long TargetId { get; set; } + public string Message { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Relation/RelationSendTalkMessageConsumer.cs b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Relation/RelationSendTalkMessageConsumer.cs new file mode 100644 index 0000000..7091fb9 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/Relation/RelationSendTalkMessageConsumer.cs @@ -0,0 +1,26 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; +using WingsAPI.Game.Extensions.RelationsExtensions; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Plugins.DistributedGameEvents.Relation +{ + public class RelationSendTalkMessageConsumer : IMessageConsumer + { + private readonly ISessionManager _sessionManager; + + public RelationSendTalkMessageConsumer(ISessionManager sessionManager) => _sessionManager = sessionManager; + + public async Task HandleAsync(RelationSendTalkMessage notification, CancellationToken token) + { + long senderId = notification.SenderId; + long targetId = notification.TargetId; + string message = notification.Message; + + IClientSession target = _sessionManager.GetSessionByCharacterId(targetId); + target?.SendFriendMessage(senderId, message); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/SchedulableConfiguration.cs b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/SchedulableConfiguration.cs new file mode 100644 index 0000000..8302b78 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/SchedulableConfiguration.cs @@ -0,0 +1,7 @@ +namespace WingsEmu.Plugins.DistributedGameEvents +{ + public abstract class SchedulableConfiguration + { + public string CronExpression { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/ScheduledEventPublisherCorePlugin.cs b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/ScheduledEventPublisherCorePlugin.cs new file mode 100644 index 0000000..c7927ad --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/ScheduledEventPublisherCorePlugin.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.DependencyInjection; +using WingsAPI.Plugins; +using WingsEmu.Plugins.DistributedGameEvents.BotMessages.Extensions; + +namespace WingsEmu.Plugins.DistributedGameEvents +{ + public class ScheduledEventPublisherCorePlugin : IDependencyInjectorPlugin + { + public string Name => nameof(ScheduledEventPublisherCorePlugin); + + + public void AddDependencies(IServiceCollection services) + { + services.AddRecurrentBotMessagesSchedulerModule(); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/ScheduledEventSubscriberCorePlugin.cs b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/ScheduledEventSubscriberCorePlugin.cs new file mode 100644 index 0000000..1274cab --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/ScheduledEventSubscriberCorePlugin.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.DependencyInjection; +using WingsAPI.Plugins; +using WingsEmu.Plugins.DistributedGameEvents.BotMessages.Extensions; + +namespace WingsEmu.Plugins.DistributedGameEvents +{ + public class ScheduledEventSubscriberCorePlugin : IGameServerPlugin + { + public string Name => nameof(ScheduledEventSubscriberCorePlugin); + + + public void AddDependencies(IServiceCollection services, GameServerLoader gameServer) + { + services.AddRecurrentBotMessagesGameModule(); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/WingsEmu.Plugins.DistributedGameEvents.csproj b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/WingsEmu.Plugins.DistributedGameEvents.csproj new file mode 100644 index 0000000..4a95315 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.DistributedGameEvents/WingsEmu.Plugins.DistributedGameEvents.csproj @@ -0,0 +1,18 @@ + + + + net5.0 + disable + + + + + + + + + + + + + diff --git a/srcs/_plugins/WingsEmu.Plugins.Essentials/Account/AccountModule.cs b/srcs/_plugins/WingsEmu.Plugins.Essentials/Account/AccountModule.cs new file mode 100644 index 0000000..23c93c6 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.Essentials/Account/AccountModule.cs @@ -0,0 +1,136 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Threading.Tasks; +using PhoenixLib.MultiLanguage; +using Qmmands; +using WingsEmu.Commands.Checks; +using WingsEmu.Commands.Entities; +using WingsEmu.DTOs.Account; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Groups.Events; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Relations; +using WingsEmu.Packets.ClientPackets; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.Essentials.Account; + +[Name("Account")] +[Description("Module related to account commands.")] +[RequireAuthority(AuthorityType.User)] +public class AccountModule : SaltyModuleBase +{ + private readonly IGameLanguageService _language; + private readonly IServerManager _manager; + private readonly ISessionManager _sessionManager; + + public AccountModule(IServerManager manager, IGameLanguageService language, ISessionManager sessionManager) + { + _manager = manager; + _language = language; + _sessionManager = sessionManager; + } + + [Command("language", "lang", "getlang", "getlanguage")] + [Description("Check your language")] + public async Task GetLanguage() + { + IClientSession player = Context.Player; + player.SendChatMessage($"{_language.GetLanguage(GameDialogKey.COMMAND_CHATMESSAGE_LANGUAGE_CURRENT, player.UserLanguage)} {player.UserLanguage.ToString()}", ChatMessageColorType.Yellow); + return new SaltyCommandResult(true, ""); + } + + [Command("language", "lang", "setlang", "setlanguage")] + [Description("Sets your language")] + public async Task SetAccountLanguage([Description("EN, FR, CZ, PL, DE, IT, ES, TR")] RegionLanguageType languageType) + { + IClientSession player = Context.Player; + player.Account.ChangeLanguage(languageType); + player.SendChatMessage($"{_language.GetLanguage(GameDialogKey.COMMAND_CHATMESSAGE_LANGUAGE_CHANGED, player.UserLanguage)} {languageType.ToString()}", ChatMessageColorType.Yellow); + + return new SaltyCommandResult(true, ""); + } + + [Command("invite")] + public async Task InviteAsync([Remainder] string nickname) + { + await Context.Player.EmitEventAsync(new InviteJoinMinilandEvent(nickname, true)); + return new SaltyCommandResult(true); + } + + [Command("fl")] + [Description("Send friend request to the player")] + public async Task FriendRequestAsync([Remainder] [Description("Player nickname")] string nickname) + { + IClientSession session = Context.Player; + IPlayerEntity target = _sessionManager.GetSessionByCharacterName(nickname)?.PlayerEntity; + if (target == null) + { + session.SendMsg(_language.GetLanguage(GameDialogKey.INFORMATION_MESSAGE_USER_NOT_FOUND, session.UserLanguage), MsgMessageType.Middle); + return new SaltyCommandResult(true); + } + + await session.EmitEventAsync(new RelationFriendEvent + { + RequestType = FInsPacketType.INVITE, + CharacterId = target.Id + }); + + return new SaltyCommandResult(true); + } + + [Command("bl")] + [Description("Block the player.")] + public async Task BlockRequestAsync([Remainder] [Description("Player nickname")] string nickname) + { + IClientSession session = Context.Player; + IPlayerEntity target = _sessionManager.GetSessionByCharacterName(nickname)?.PlayerEntity; + if (target == null) + { + session.SendMsg(_language.GetLanguage(GameDialogKey.INFORMATION_MESSAGE_USER_NOT_FOUND, session.UserLanguage), MsgMessageType.Middle); + return new SaltyCommandResult(true); + } + + await session.EmitEventAsync(new RelationBlockEvent + { + CharacterId = target.Id + }); + + return new SaltyCommandResult(true); + } + + [Command("pinv")] + [Description("Send group request to the player")] + public async Task GroupRequestAsync([Remainder] [Description("Player's nickname")] string nickname) + { + IClientSession session = Context.Player; + IPlayerEntity target = _sessionManager.GetSessionByCharacterName(nickname)?.PlayerEntity; + if (target == null) + { + session.SendMsg(_language.GetLanguage(GameDialogKey.INFORMATION_MESSAGE_USER_NOT_FOUND, session.UserLanguage), MsgMessageType.Middle); + return new SaltyCommandResult(true); + } + + await session.EmitEventAsync(new GroupActionEvent + { + RequestType = GroupRequestType.Requested, + CharacterId = target.Id + }); + + return new SaltyCommandResult(true); + } + + [Command("fcancel")] + public async Task CancelFightMode() + { + Context.Player.PlayerEntity.CancelCastingSkill(); + return new SaltyCommandResult(true); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.Essentials/Administrator/AdministratorBazaarModule.cs b/srcs/_plugins/WingsEmu.Plugins.Essentials/Administrator/AdministratorBazaarModule.cs new file mode 100644 index 0000000..3780396 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.Essentials/Administrator/AdministratorBazaarModule.cs @@ -0,0 +1,57 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using System.Threading.Tasks; +using Qmmands; +using WingsAPI.Communication.Bazaar; +using WingsEmu.Commands.Checks; +using WingsEmu.Commands.Entities; +using WingsEmu.DTOs.Account; + +namespace WingsEmu.Plugins.Essentials.Administrator; + +[Name("Admin-BazaarModule")] +[Description("Module related to Administrator commands for Bazaar.")] +[Group("bazaar")] +[RequireAuthority(AuthorityType.GameAdmin)] +public class AdministratorBazaarModule : SaltyModuleBase +{ + private readonly IBazaarService _bazaarService; + + public AdministratorBazaarModule(IBazaarService bazaarService) => _bazaarService = bazaarService; + + [Command("unlist-vnum", "unlist", "remove-vnum", "remove")] + public async Task UnlistItemsFromBazaar([Remainder] string toUnlist) + { + string[] rawVnums = toUnlist.Split(' '); + var vnums = new List(); + + foreach (string vnum in rawVnums) + { + if (!int.TryParse(vnum, out int parsedVnum)) + { + return new SaltyCommandResult(false, $"Incorrect value: {vnum} is not an integer"); + } + + vnums.Add(parsedVnum); + } + + UnlistItemFromBazaarResponse tmp = await _bazaarService.UnlistItemsFromBazaarWithVnumAsync(new UnlistItemFromBazaarRequest + { + Vnum = vnums + }); + return new SaltyCommandResult(true, $"{tmp.UnlistedItems} items unlisted"); + } + + [Command("unlist-char", "remove-char")] + public async Task UnlistItemsFromBazaarByCharacterId(int characterId) + { + UnlistItemFromBazaarResponse tmp = await _bazaarService.UnlistCharacterItemsFromBazaarAsync(new UnlistCharacterItemsFromBazaarRequest + { + Id = characterId + }); + return new SaltyCommandResult(true, $"{tmp.UnlistedItems} items unlisted"); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.Essentials/Administrator/AdministratorCheatModule_Rune.cs b/srcs/_plugins/WingsEmu.Plugins.Essentials/Administrator/AdministratorCheatModule_Rune.cs new file mode 100644 index 0000000..dfbb897 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.Essentials/Administrator/AdministratorCheatModule_Rune.cs @@ -0,0 +1,31 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Threading.Tasks; +using Qmmands; +using WingsEmu.Commands.Checks; +using WingsEmu.Commands.Entities; +using WingsEmu.DTOs.Account; + +namespace WingsEmu.Plugins.Essentials.Administrator; + +[Name("Admin-RuneCheat")] +[Description("Module related to Administrator commands.")] +[RequireAuthority(AuthorityType.GameAdmin)] +public class AdministratorCheatModule_Rune : SaltyModuleBase +{ + [Command("addrune")] + public async Task AddRuneAsync(short slot, int bcardType, byte subType, byte value1, int value2) => new SaltyCommandResult(true); + + + [Command("infoRune")] + public async Task DumpRuneInformation(short slot) => new SaltyCommandResult(true); + + + [Command("clearrune")] + public async Task ClearRuneAsync(short slot) => new SaltyCommandResult(true); + + [Command("rmrune", "removerune")] + public async Task RemoveRuneAsync(short slot, int type) => new SaltyCommandResult(true); +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.Essentials/Administrator/AdministratorCheatModule_Shell.cs b/srcs/_plugins/WingsEmu.Plugins.Essentials/Administrator/AdministratorCheatModule_Shell.cs new file mode 100644 index 0000000..689a7c5 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.Essentials/Administrator/AdministratorCheatModule_Shell.cs @@ -0,0 +1,180 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Qmmands; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsAPI.Packets.Enums.Shells; +using WingsEmu.Commands.Checks; +using WingsEmu.Commands.Entities; +using WingsEmu.DTOs.Account; +using WingsEmu.DTOs.Items; +using WingsEmu.Game.Algorithm; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Items; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.Essentials.Administrator; + +[Name("Admin-ShellCheat")] +[Description("Module related to Administrator commands for shells")] +[RequireAuthority(AuthorityType.GameAdmin)] +public class AdministratorCheatModule_Shell : SaltyModuleBase +{ + private readonly IGameItemInstanceFactory _itemInstanceFactory; + private readonly IShellGenerationAlgorithm _shellGenerationAlgorithm; + + public AdministratorCheatModule_Shell(IShellGenerationAlgorithm shellGenerationAlgorithm, IGameItemInstanceFactory itemInstanceFactory) + { + _shellGenerationAlgorithm = shellGenerationAlgorithm; + _itemInstanceFactory = itemInstanceFactory; + } + + [Command("clearshell")] + public async Task ClearShell(short slot) + { + InventoryItem item = Context.Player.PlayerEntity.GetItemBySlotAndType(slot, InventoryType.Equipment); + if (item == null) + { + Context.Player.SendChatMessage("Wrong slot", ChatMessageColorType.Yellow); + return new SaltyCommandResult(false); + } + + if (item.ItemInstance.Type != ItemInstanceType.WearableInstance) + { + return new SaltyCommandResult(false); + } + + item.ItemInstance.EquipmentOptions?.RemoveAll(s => s.EquipmentOptionType == EquipmentOptionType.ARMOR_SHELL || s.EquipmentOptionType == EquipmentOptionType.WEAPON_SHELL); + + Context.Player.SendChatMessage($"[SHELL_CHEAT] Cleared the shell of the item at slot {slot}", ChatMessageColorType.Yellow); + return new SaltyCommandResult(true); + } + + [Command("addshell")] + public async Task AddShellAsync(short slot, int shellOptionType, byte level, int value) + { + InventoryItem item = Context.Player.PlayerEntity.GetItemBySlotAndType(slot, InventoryType.Equipment); + if (item == null) + { + Context.Player.SendChatMessage("Wrong slot", ChatMessageColorType.Yellow); + return new SaltyCommandResult(false); + } + + if (item.ItemInstance.Type != ItemInstanceType.WearableInstance) + { + return new SaltyCommandResult(false); + } + + item.ItemInstance.EquipmentOptions ??= new List(); + item.ItemInstance.EquipmentOptions.Add(new EquipmentOptionDTO + { + EquipmentOptionType = item.ItemInstance.GameItem.EquipmentSlot switch + { + EquipmentType.Armor => EquipmentOptionType.ARMOR_SHELL, + EquipmentType.SecondaryWeapon => EquipmentOptionType.WEAPON_SHELL, + EquipmentType.MainWeapon => EquipmentOptionType.WEAPON_SHELL, + _ => EquipmentOptionType.NONE + }, + Type = (byte)shellOptionType, + Level = level, + Value = value + }); + Context.Player.SendChatMessage("Added shell row", ChatMessageColorType.Yellow); + return new SaltyCommandResult(true); + } + + [Command("generateShell")] + public async Task GenerateShellAsync(byte slot) + { + InventoryItem item = Context.Player.PlayerEntity.GetItemBySlotAndType(slot, InventoryType.Equipment); + if (item == null) + { + Context.Player.SendChatMessage("Wrong slot", ChatMessageColorType.Yellow); + return new SaltyCommandResult(false); + } + + if (item.ItemInstance.Type != ItemInstanceType.WearableInstance) + { + return new SaltyCommandResult(false); + } + + ShellType shellType = item.ItemInstance.GameItem.ShellType; + IEnumerable shellOptions = _shellGenerationAlgorithm.GenerateShell((byte)shellType, item.ItemInstance.Rarity, item.ItemInstance.GameItem.LevelMinimum).ToList(); + + if (!shellOptions.Any()) + { + return new SaltyCommandResult(false); + } + + item.ItemInstance.EquipmentOptions ??= new List(); + item.ItemInstance.EquipmentOptions.AddRange(shellOptions); + + return new SaltyCommandResult(true); + } + + [Command("duplicate")] + public async Task DuplicateItemAsync(byte slot) + { + InventoryItem item = Context.Player.PlayerEntity.GetItemBySlotAndType(slot, InventoryType.Equipment); + if (item == null) + { + Context.Player.SendChatMessage("Wrong slot", ChatMessageColorType.Yellow); + return new SaltyCommandResult(false, "Wrong slot"); + } + + if (!Context.Player.PlayerEntity.HasSpaceFor(item.ItemInstance.ItemVNum)) + { + return new SaltyCommandResult(false, "Not enough space in inventory"); + } + + if (item.ItemInstance.Type != ItemInstanceType.WearableInstance) + { + return new SaltyCommandResult(false); + } + + GameItemInstance newItem = _itemInstanceFactory.DuplicateItem(item.ItemInstance); + await Context.Player.AddNewItemToInventory(newItem); + return new SaltyCommandResult(true, "Item duped !"); + } + + [Command("checkoptions")] + public async Task CheckOptionsAsync(byte slot) + { + IClientSession session = Context.Player; + InventoryItem item = session.PlayerEntity.GetItemBySlotAndType(slot, InventoryType.Equipment); + if (item == null) + { + return new SaltyCommandResult(false, "Wrong slot"); + } + + if (item.ItemInstance.Type != ItemInstanceType.WearableInstance) + { + return new SaltyCommandResult(false); + } + + if (item.ItemInstance.EquipmentOptions == null || !item.ItemInstance.EquipmentOptions.Any()) + { + return new SaltyCommandResult(false, "Item without options"); + } + + foreach (EquipmentOptionDTO option in item.ItemInstance.EquipmentOptions) + { + session.SendChatMessage("============[ ITEM OPTION ]============", ChatMessageColorType.Red); + session.SendChatMessage($"EquipmentType: {option.EquipmentOptionType}", ChatMessageColorType.Green); + session.SendChatMessage($"EffectVnum: {option.EffectVnum}", ChatMessageColorType.Green); + session.SendChatMessage($"Weight: {option.Weight}", ChatMessageColorType.Green); + session.SendChatMessage($"Type: {option.Type}", ChatMessageColorType.Green); + session.SendChatMessage($"Value {option.Value}", ChatMessageColorType.Green); + session.SendChatMessage($"Level: {option.Level}", ChatMessageColorType.Green); + } + + return new SaltyCommandResult(true); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.Essentials/Administrator/AdministratorCooldownModule.cs b/srcs/_plugins/WingsEmu.Plugins.Essentials/Administrator/AdministratorCooldownModule.cs new file mode 100644 index 0000000..6700c32 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.Essentials/Administrator/AdministratorCooldownModule.cs @@ -0,0 +1,31 @@ +using System.Threading.Tasks; +using Qmmands; +using WingsEmu.Commands.Checks; +using WingsEmu.Commands.Entities; +using WingsEmu.DTOs.Account; +using WingsEmu.Game.Families; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Plugins.Essentials.Administrator; + +[Name("Admin-Cooldown-Module")] +[Description("Module related to Administrator commands for hours/days cooldowns")] +[RequireAuthority(AuthorityType.GameAdmin)] +public class AdministratorCooldownModule : SaltyModuleBase +{ + private readonly IFamilyManager _familyManager; + + public AdministratorCooldownModule(IFamilyManager familyManager) => _familyManager = familyManager; + + [Command("family-cooldown")] + [Description("Remove family join cooldown")] + public async Task FamilyCooldownAsync(IClientSession target) + { + if (!await _familyManager.RemovePlayerJoinCooldownAsync(target.PlayerEntity.Id)) + { + return new SaltyCommandResult(false, "Player didn't had family any cooldown."); + } + + return new SaltyCommandResult(true, $"Family join cooldown has been removed from {target.PlayerEntity.Name}"); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.Essentials/Administrator/AdministratorLanguageModule.cs b/srcs/_plugins/WingsEmu.Plugins.Essentials/Administrator/AdministratorLanguageModule.cs new file mode 100644 index 0000000..3ae1ad7 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.Essentials/Administrator/AdministratorLanguageModule.cs @@ -0,0 +1,69 @@ +using System; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; +using Qmmands; +using WingsAPI.Communication.Translations; +using WingsEmu.Commands.Checks; +using WingsEmu.Commands.Entities; +using WingsEmu.DTOs.Account; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers; + +namespace WingsEmu.Plugins.Essentials.Administrator; + +[Name("administrator-language")] +[Description("Module related to Administrator commands.")] +[Group("translations", "i18n", "language")] +[RequireAuthority(AuthorityType.GameAdmin)] +public class AdministratorLanguageModule : SaltyModuleBase +{ + private readonly IForbiddenNamesManager _forbiddenNamesManager; + private readonly IGameLanguageService _language; + private readonly IMessagePublisher _messagePublisher; + + public AdministratorLanguageModule(IGameLanguageService language, IMessagePublisher messagePublisher, IForbiddenNamesManager forbiddenNamesManager) + { + _language = language; + _messagePublisher = messagePublisher; + _forbiddenNamesManager = forbiddenNamesManager; + } + + + [Command("verify", "check")] + public async Task CheckTranslations() + { + foreach (GameDialogKey enumValue in Enum.GetValues(typeof(GameDialogKey))) + { + GameDialogKey enm = enumValue; + string notTranslated = $"#{enm.ToString()}"; + + string translated = _language.GetLanguage(enumValue, Context.Player.UserLanguage); + if (translated != notTranslated) + { + continue; + } + + Context.Player.SendErrorChatMessage($"[{Context.Player.UserLanguage.ToString()}] {enumValue.ToString()} - not translated"); + } + + return new SaltyCommandResult(true); + } + + + [Command("reload", "refresh")] + public async Task ReloadTranslations(bool isFull = false, bool forAllChannels = false) + { + await _forbiddenNamesManager.Reload(); + await _language.Reload(isFull); + if (forAllChannels) + { + await _messagePublisher.PublishAsync(new TranslationsRefreshMessage + { + IsFullReload = isFull + }); + } + + return new SaltyCommandResult(true, "Translations reloaded"); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.Essentials/Administrator/AdministratorMailModule.cs b/srcs/_plugins/WingsEmu.Plugins.Essentials/Administrator/AdministratorMailModule.cs new file mode 100644 index 0000000..8ae61f8 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.Essentials/Administrator/AdministratorMailModule.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Qmmands; +using WingsAPI.Communication; +using WingsAPI.Communication.DbServer.CharacterService; +using WingsAPI.Communication.Mail; +using WingsAPI.Communication.Player; +using WingsEmu.Commands.Checks; +using WingsEmu.Commands.Entities; +using WingsEmu.Core.Extensions; +using WingsEmu.DTOs.Account; +using WingsEmu.DTOs.Items; +using WingsEmu.DTOs.Mails; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Items; +using WingsEmu.Game.Mails.Events; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.Essentials.Administrator; + +[Name("MailModule")] +[Description("Module related to mail admin commands.")] +[RequireAuthority(AuthorityType.Owner)] +public class AdministratorMailModule : SaltyModuleBase +{ + private readonly ICharacterService _characterService; + private readonly IGameItemInstanceFactory _gameItemInstanceFactory; + private readonly IMailService _mailService; + private readonly ISessionManager _sessionManager; + + public AdministratorMailModule(IGameItemInstanceFactory gameItemInstanceFactory, ISessionManager sessionManager, + ICharacterService characterService, IMailService mailService) + { + _gameItemInstanceFactory = gameItemInstanceFactory; + _sessionManager = sessionManager; + _characterService = characterService; + _mailService = mailService; + } + + [Command("parcel", "mail", "gift")] + [Description("Send gift mail to someone")] + public async Task ParcelAsync(string targetName, int itemVnum, short amount) + { + IClientSession session = Context.Player; + IClientSession target = _sessionManager.GetSessionByCharacterName(targetName); + if (target == null) + { + return await ParcelOffAsync(targetName, itemVnum, amount); + } + + GameItemInstance item = _gameItemInstanceFactory.CreateItem(itemVnum, amount); + await session.EmitEventAsync(new MailCreateEvent(session.PlayerEntity.Name, target.PlayerEntity.Id, MailGiftType.Normal, item)); + return new SaltyCommandResult(true, "Parcel has been sent."); + } + + private async Task ParcelOffAsync(string targetName, int itemVnum, short amount) + { + ClusterCharacterInfo tmp = _sessionManager.GetOnlineCharacterByName(targetName); + if (tmp != null) + { + return new SaltyCommandResult(false, $"Player found on channel: {tmp.ChannelId}"); + } + + DbServerGetCharacterResponse response = await _characterService.GetCharacterByName(new DbServerGetCharacterRequestByName + { + CharacterName = targetName + }); + + if (response?.RpcResponseType != RpcResponseType.SUCCESS) + { + return new SaltyCommandResult(false, "Player not found"); + } + + if (response.CharacterDto.Name != targetName) + { + return new SaltyCommandResult(false, "Player not found"); + } + + GameItemInstance item = _gameItemInstanceFactory.CreateItem(itemVnum, amount); + ItemInstanceDTO itemInstanceDto = _gameItemInstanceFactory.CreateDto(item); + + await _mailService.CreateMailBatchAsync(new CreateMailBatchRequest + { + Mails = Lists.Create(new CharacterMailDto + { + Date = DateTime.UtcNow, + SenderName = Context.Player.PlayerEntity.Name, + ReceiverId = response.CharacterDto.Id, + MailGiftType = MailGiftType.Normal, + ItemInstance = itemInstanceDto + }), + Bufferized = true + }); + + return new SaltyCommandResult(true, "Parcel has been sent."); + } + + [Command("parcel-all", "mail-all", "gift-all")] + [Description("Send gift mail to someone")] + public async Task ParcelAsync(int itemVnum, short amount) + { + string senderName = Context.Player.PlayerEntity.Name; + // todo - remove this shit :special: + HashSet ips = new(); + foreach (IClientSession session in _sessionManager.Sessions) + { + if (ips.Contains(session.IpAddress)) + { + Context.Player.SendChatMessage($"[{session.IpAddress}] {session.PlayerEntity.Name} got already this gift.", ChatMessageColorType.Red); + continue; + } + + GameItemInstance item = _gameItemInstanceFactory.CreateItem(itemVnum, amount); + await session.EmitEventAsync(new MailCreateEvent(senderName, session.PlayerEntity.Id, MailGiftType.Normal, item)); + ips.Add(session.IpAddress); + } + + return new SaltyCommandResult(true, "Parcel has been sent."); + } + + [Command("note")] + [Description("Send note to someone")] + public async Task NoteAsync(IClientSession target, string title, [Remainder] string message) + { + IClientSession session = Context.Player; + if (target == null) + { + return new SaltyCommandResult(false, "Player is offline."); + } + + await session.EmitEventAsync(new NoteCreateEvent(target.PlayerEntity.Name, title, message)); + return new SaltyCommandResult(true, "Note has been sent."); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.Essentials/Administrator/AdministratorMaintenanceModule.cs b/srcs/_plugins/WingsEmu.Plugins.Essentials/Administrator/AdministratorMaintenanceModule.cs new file mode 100644 index 0000000..cf26999 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.Essentials/Administrator/AdministratorMaintenanceModule.cs @@ -0,0 +1,311 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using PhoenixLib.Logging; +using Qmmands; +using WingsAPI.Communication; +using WingsAPI.Communication.Player; +using WingsAPI.Communication.ServerApi; +using WingsAPI.Communication.ServerApi.Protocol; +using WingsAPI.Communication.Services; +using WingsAPI.Communication.Services.Requests; +using WingsAPI.Communication.Services.Responses; +using WingsEmu.Commands.Checks; +using WingsEmu.Commands.Entities; +using WingsEmu.DTOs.Account; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Features; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Plugins.Essentials.Administrator; + +[Name("Maintenance")] +[Description("Module related to maintenance admin commands.")] +[Group("status", "maintenance")] +[RequireAuthority(AuthorityType.Owner)] +public class AdministratorMaintenanceModule : SaltyModuleBase +{ + private readonly IClusterCharacterService _characterService; + private readonly IClusterStatusService _service; + + public AdministratorMaintenanceModule(IClusterStatusService service, IClusterCharacterService characterService) + { + _service = service; + _characterService = characterService; + } + + [Command("show", "count", "players", "channels")] + [Description("Shows amount of players per channel")] + public async Task ShowStats() + { + ClusterCharacterGetSortedResponse response = null; + try + { + response = await _characterService.GetCharactersSortedByChannel(new EmptyRpcRequest()); + } + catch (Exception e) + { + Log.Error("[MAINTENANCE_MODULE][SHOW_STATS] Unexpected error: ", e); + } + + if (response?.ResponseType != RpcResponseType.SUCCESS || response.CharactersByChannel == null) + { + return new SaltyCommandResult(false, "Couldn't obtain the information from ClusterCharacterService."); + } + + Context.Player.SendInformationChatMessage("[======== CLUSTER =========]"); + foreach ((byte channelId, List characterInfos) in response.CharactersByChannel) + { + Context.Player.SendInformationChatMessage($"Channel {channelId.ToString()}: {(characterInfos?.Count ?? 0).ToString()} players"); + } + + return new SaltyCommandResult(true); + } + + [Command("schedule", "start")] + [Description("Schedules a general maintenance")] + public async Task ScheduleMaintenance([Description("The maintenance delay")] string inputTimeSpan, [Description("Reason of the maintenance")] params string[] reason) + { + if (!TimeSpan.TryParse(inputTimeSpan, out TimeSpan timeSpan)) + { + return new SaltyCommandResult(false, "Failed to parse the introduced delay"); + } + + StringBuilder stringBuilder = new(); + foreach (string substring in reason) + { + stringBuilder.Append(' '); + stringBuilder.Append(substring); + } + + BasicRpcResponse response; + try + { + response = await _service.ScheduleGeneralMaintenance(new ServiceScheduleGeneralMaintenanceRequest + { + ShutdownTimeSpan = timeSpan, + Reason = stringBuilder.ToString().TrimStart() + }); + } + catch (Exception e) + { + Log.Error("[MAINTENANCE_MODULE][SCHEDULE_MAINTENANCE]", e); + return new SaltyCommandResult(false, "Couldn't connect with the Cluster/Master"); + } + + return response.ResponseType != RpcResponseType.SUCCESS + ? new SaltyCommandResult(false, "Cluster/Master denied the general maintenance") + : new SaltyCommandResult(true, "A general maintenance has been scheduled!"); + } + + [Command("unschedule", "stop", "cancel")] + [Description("Unschedules a general maintenance")] + public async Task UnscheduleMaintenance() + { + BasicRpcResponse response; + try + { + response = await _service.UnscheduleGeneralMaintenance(new EmptyRpcRequest()); + } + catch (Exception e) + { + Log.Error("[MAINTENANCE_MODULE][UNSCHEDULE_MAINTENANCE]", e); + return new SaltyCommandResult(false, "Couldn't connect with the Cluster/Master"); + } + + return response.ResponseType != RpcResponseType.SUCCESS + ? new SaltyCommandResult(false, "Cluster/Master denied the unschedule of the maintenance") + : new SaltyCommandResult(true, "The maintenance has been unscheduled!"); + } + + [Command("emergency")] + [Description("Executes an emergency general maintenance")] + public async Task ExecuteEmergencyMaintenance([Description("Reason of the maintenance")] params string[] reason) + { + StringBuilder stringBuilder = new(); + foreach (string substring in reason) + { + stringBuilder.Append(' '); + stringBuilder.Append(substring); + } + + BasicRpcResponse response; + try + { + response = await _service.ExecuteGeneralEmergencyMaintenance(new ServiceExecuteGeneralEmergencyMaintenanceRequest + { + Reason = stringBuilder.ToString().TrimStart() + }); + } + catch (Exception e) + { + Log.Error("[MAINTENANCE_MODULE][EXECUTE_EMERGENCY_MAINTENANCE]", e); + return new SaltyCommandResult(false, "Couldn't connect with the Cluster/Master"); + } + + return response.ResponseType != RpcResponseType.SUCCESS + ? new SaltyCommandResult(false, "Cluster/Master denied the emergency maintenance") + : new SaltyCommandResult(true, "The Emergency Maintenance has been executed!"); + } + + [Command("lift")] + [Description("Lifts a general maintenance (even emergency ones)")] + public async Task LiftMaintenance() + { + BasicRpcResponse response; + try + { + response = await _service.LiftGeneralMaintenance(new EmptyRpcRequest()); + } + catch (Exception e) + { + Log.Error("[MAINTENANCE_MODULE][LIFT_MAINTENANCE]", e); + return new SaltyCommandResult(false, "Couldn't connect with the Cluster/Master"); + } + + return response.ResponseType != RpcResponseType.SUCCESS + ? new SaltyCommandResult(false, "Cluster/Master denied the maintenance lift") + : new SaltyCommandResult(true, "The Maintenance has been lifted!"); + } + + [Group("feature")] + [Description("Sub-module related to individual management of features")] + public class AdministratorMaintenanceFeatureModule : SaltyModuleBase + { + private readonly IGameFeatureToggleManager _gameFeatureToggleManager; + + public AdministratorMaintenanceFeatureModule(IGameFeatureToggleManager gameFeatureToggleManager) => _gameFeatureToggleManager = gameFeatureToggleManager; + + [Command("disable")] + [Description("Disable a enabled feature")] + public async Task DisableFeature([Description("Name of the feature")] GameFeature feature) + { + bool disabled = await _gameFeatureToggleManager.IsDisabled(feature); + if (disabled) + { + return new SaltyCommandResult(false, "This feature is already disabled"); + } + + await _gameFeatureToggleManager.Disable(feature); + + return new SaltyCommandResult(true, $"Successfully disabled: {feature}"); + } + + [Command("enable")] + [Description("Enable a game disabled feature")] + public async Task EnableFeature([Description("Name of the feature")] GameFeature feature) + { + bool disabled = await _gameFeatureToggleManager.IsDisabled(feature); + if (!disabled) + { + return new SaltyCommandResult(false, "This feature is already enabled"); + } + + await _gameFeatureToggleManager.Enable(feature); + + return new SaltyCommandResult(true, $"Successfully enabled: {feature}"); + } + + [Command("list")] + [Description("List all game feature status")] + public async Task List() + { + Context.Player.SendInformationChatMessage("[========= GAME FEATURES STATUS =========]"); + foreach (GameFeature feature in Enum.GetValues()) + { + bool disabled = await _gameFeatureToggleManager.IsDisabled(feature); + Context.Player.SendInformationChatMessage($"{feature}: {(!disabled ? "ON" : "OFF")}"); + } + + Context.Player.SendInformationChatMessage("[========================================]"); + return new SaltyCommandResult(true); + } + } + + [Group("service")] + [Description("Sub-Module related to individual management of services.")] + public class AdministratorMaintenanceServiceModule : SaltyModuleBase + { + private readonly IClusterStatusService _service; + + public AdministratorMaintenanceServiceModule(IClusterStatusService service) => _service = service; + + [Command("list")] + [Description("Lists the status of the cluster services")] + public async Task ListMaintenanceAsync() + { + IClientSession session = Context.Player; + ServiceGetAllResponse resp = await _service.GetAllServicesStatus(new EmptyRpcRequest()); + + session.SendInformationChatMessage("[========= CLUSTER STATUS =========]"); + foreach (Service service in resp.Services) + { + session.SendInformationChatMessage($"[{service.Id}] {service.Status.ToString()} - {service.LastUpdate:yyyy-MM-dd HH:mm:ss}"); + } + + session.SendInformationChatMessage("[=============================]"); + + return new SaltyCommandResult(true, "Services listed"); + } + + [Command("set", "enable", "activate")] + [Description("Sets a designated service in maintenance mode")] + public async Task EnableMaintenanceMode(string serviceName) + { + BasicRpcResponse resp = await _service.EnableMaintenanceMode(new ServiceBasicRequest + { + ServiceName = serviceName + }); + if (resp.ResponseType != RpcResponseType.SUCCESS) + { + return new SaltyCommandResult(false, $"{serviceName} could not be put in maintenance mode"); + } + + return new SaltyCommandResult(true, $"{serviceName} is now in maintenance mode"); + } + + [Command("unset", "disable", "deactivate")] + [Description("Sets a designated service in maintenance mode")] + public async Task DisableMaintenanceMode(string serviceName) + { + BasicRpcResponse resp = await _service.DisableMaintenanceMode(new ServiceBasicRequest + { + ServiceName = serviceName + }); + if (resp.ResponseType != RpcResponseType.SUCCESS) + { + return new SaltyCommandResult(false, $"{serviceName} could not be removed from maintenance mode"); + } + + return new SaltyCommandResult(true, $"{serviceName} is not in maintenance mode anymore"); + } + } + + [Group("channel-list", "cl")] + [Description("Sub-Module related to management of the Login's Channel List.")] + public class AdministratorMaintenanceChannelListModule : SaltyModuleBase + { + private readonly IServerApiService _serverApiService; + + public AdministratorMaintenanceChannelListModule(IServerApiService serverApiService) => _serverApiService = serverApiService; + + [Command("set")] + [Description("Sets a designated channel's visibility")] + public async Task SetVisibility(int channelId, string worldGroup = "NosWings", AuthorityType authorityType = AuthorityType.User) + { + BasicRpcResponse resp = await _serverApiService.SetWorldServerVisibility(new SetWorldServerVisibilityRequest + { + ChannelId = channelId, + WorldGroup = worldGroup, + AuthorityRequired = authorityType + }); + if (resp.ResponseType != RpcResponseType.SUCCESS) + { + return new SaltyCommandResult(false, $"[{channelId.ToString()}:{worldGroup}] Failed to set visibility"); + } + + return new SaltyCommandResult(true, $"[{channelId.ToString()}:{worldGroup}] Visibility set to '{authorityType.ToString()}'"); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.Essentials/Administrator/AdministratorModule.cs b/srcs/_plugins/WingsEmu.Plugins.Essentials/Administrator/AdministratorModule.cs new file mode 100644 index 0000000..d6c4e53 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.Essentials/Administrator/AdministratorModule.cs @@ -0,0 +1,1117 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using Qmmands; +using WingsAPI.Communication; +using WingsAPI.Communication.DbServer.CharacterService; +using WingsAPI.Communication.ServerApi; +using WingsAPI.Communication.ServerApi.Protocol; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsAPI.Game.Extensions.ItemExtension.Item; +using WingsAPI.Game.Extensions.Quicklist; +using WingsAPI.Packets.Enums; +using WingsEmu.Commands.Checks; +using WingsEmu.Commands.Entities; +using WingsEmu.DTOs.Account; +using WingsEmu.DTOs.Recipes; +using WingsEmu.DTOs.Skills; +using WingsEmu.DTOs.Titles; +using WingsEmu.Game; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Algorithm; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Compliments; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Entities.Extensions; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Extensions.Mates; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Inventory.Event; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Managers.ServerData; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Monster.Event; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Networking.Broadcasting; +using WingsEmu.Game.Skills; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.Essentials.Administrator; + +[Name("Administrator")] +[Description("Module related to Administrator commands.")] +[RequireAuthority(AuthorityType.GameAdmin)] +public class AdministratorModule : SaltyModuleBase +{ + private readonly IBuffFactory _buffFactory; + private readonly ICardsManager _cards; + private readonly ICharacterAlgorithm _characterAlgorithm; + private readonly ICharacterService _characterService; + private readonly IMonsterEntityFactory _entity; + private readonly IAsyncEventPipeline _eventPipeline; + private readonly IGameItemInstanceFactory _gameItemInstanceFactory; + private readonly SerializableGameServer _gameServer; + private readonly IItemsManager _itemManager; + private readonly IGameLanguageService _language; + private readonly IServerManager _manager; + private readonly INpcMonsterManager _npcMonsterManager; + private readonly IRecipeManager _recipeManager; + private readonly IServerApiService _serverApiService; + private readonly ISessionManager _sessionManager; + private readonly ISkillsManager _skillManager; + private readonly ISkillsManager _skillsManager; + + public AdministratorModule(IServerManager manager, IGameLanguageService language, ISessionManager sessionManager, + IItemsManager itemsManager, INpcMonsterManager npcMonsterManager, ISkillsManager skillManager, IAsyncEventPipeline eventPipeline, IBuffFactory buffFactory, IRecipeManager recipeManager, + ICharacterAlgorithm characterAlgorithm, ICardsManager cards, IGameItemInstanceFactory gameItemInstanceFactory, IMonsterEntityFactory entity, ICharacterService characterService, + ISkillsManager skillsManager, IServerApiService serverApiService, SerializableGameServer gameServer) + { + _itemManager = itemsManager; + _npcMonsterManager = npcMonsterManager; + _skillManager = skillManager; + _sessionManager = sessionManager; + _manager = manager; + _language = language; + _eventPipeline = eventPipeline; + _buffFactory = buffFactory; + _recipeManager = recipeManager; + _characterAlgorithm = characterAlgorithm; + _cards = cards; + _gameItemInstanceFactory = gameItemInstanceFactory; + _entity = entity; + _characterService = characterService; + _skillsManager = skillsManager; + _serverApiService = serverApiService; + _gameServer = gameServer; + } + + [Command("removeitem")] + [Description("Remove item from the target inventory")] + public async Task RemoveItem(IClientSession target, byte slot, InventoryType inventoryType, short? amount = null) + { + if (inventoryType == InventoryType.EquippedItems) + { + return new SaltyCommandResult(false, "Don't use it for this - use $remove-item-equipped"); + } + + InventoryItem item = target.PlayerEntity.GetItemBySlotAndType(slot, inventoryType); + if (item?.ItemInstance == null) + { + return new SaltyCommandResult(false, "Item not found in this slot and type"); + } + + await target.RemoveItemFromInventory(item: item, amount: (short)(amount ?? item.ItemInstance.Amount)); + + return new SaltyCommandResult(true, "Item has been removed from the player"); + } + + [Command("removeitem-equipped")] + [Description("Remove equipped item from the target inventory")] + public async Task RemoveItemEquipped(IClientSession target, EquipmentType slot) + { + InventoryItem item = target.PlayerEntity.GetAllPlayerInventoryItems().FirstOrDefault(x + => x?.ItemInstance != null && x.IsEquipped && x.ItemInstance.GameItem.EquipmentSlot == slot); + + if (item?.ItemInstance == null) + { + return new SaltyCommandResult(false, "Item not found in this slot."); + } + + await target.RemoveItemFromInventory(item: item, isEquiped: true); + + if (target.ShouldSendAmuletPacket(item.ItemInstance.GameItem.EquipmentSlot)) + { + target.SendEmptyAmuletBuffPacket(); + } + + return new SaltyCommandResult(true, "Item has been removed from the player"); + } + + [Command("skillscd")] + public async Task SkillsCd() + { + IClientSession session = Context.Player; + + foreach (CharacterSkill skill in session.PlayerEntity.CharacterSkills.Values) + { + session.SendChatMessage($"Skill {skill.Skill.Id} can be used: {session.PlayerEntity.SkillCanBeUsed(skill)} - LastUse: {skill.LastUse}", ChatMessageColorType.Yellow); + } + + return new SaltyCommandResult(true); + } + + [Command("check-monster")] + public async Task CheckMonster() + { + IClientSession session = Context.Player; + (VisualType visualType, long id) = session.PlayerEntity.LastEntity; + + IBattleEntity entity = session.CurrentMapInstance.GetBattleEntity(visualType, id); + if (entity is not IMonsterEntity monsterEntity) + { + return new SaltyCommandResult(false, "Entity is null or it's not the monster"); + } + + session.SendChatMessage($" Id: {monsterEntity.Id}", ChatMessageColorType.DarkGrey); + session.SendChatMessage($" IsStillAlive: {monsterEntity.IsStillAlive}", ChatMessageColorType.DarkGrey); + session.SendChatMessage($" SpawnDate: {monsterEntity.SpawnDate}", ChatMessageColorType.DarkGrey); + session.SendChatMessage($" Targets count: {monsterEntity.Targets.Count}", ChatMessageColorType.DarkGrey); + session.SendChatMessage($" Damagers count: {monsterEntity.Damagers.Count}", ChatMessageColorType.DarkGrey); + session.SendChatMessage($" NextTick: {monsterEntity.NextTick}", ChatMessageColorType.DarkGrey); + session.SendChatMessage($" NextAttackReady: {monsterEntity.NextAttackReady}", ChatMessageColorType.DarkGrey); + session.SendChatMessage($" Target: {monsterEntity.Target?.Type}|{monsterEntity.Target?.Id}", ChatMessageColorType.DarkGrey); + session.SendChatMessage($" IsApproachingTarget: {monsterEntity.IsApproachingTarget}", ChatMessageColorType.DarkGrey); + session.SendChatMessage($" ShouldFindNewTarget: {monsterEntity.ShouldFindNewTarget}", ChatMessageColorType.DarkGrey); + session.SendChatMessage($" FindNewPositionAroundTarget: {monsterEntity.FindNewPositionAroundTarget}", ChatMessageColorType.DarkGrey); + session.SendChatMessage($" ShouldRespawn: {monsterEntity.ShouldRespawn}", ChatMessageColorType.DarkGrey); + session.SendChatMessage($" IsMonsterSpawningMonstersForQuest: {monsterEntity.IsMonsterSpawningMonstersForQuest()}", ChatMessageColorType.DarkGrey); + session.SendChatMessage($" Speed: {monsterEntity.Speed}", ChatMessageColorType.DarkGrey); + + session.SendChatMessage("[TARGETS]", ChatMessageColorType.Green); + foreach (IBattleEntity target in monsterEntity.Targets) + { + session.SendChatMessage($"{target.Type}|{target.Id}", ChatMessageColorType.Yellow); + } + + session.SendChatMessage("[DAMAGERS]", ChatMessageColorType.Green); + foreach (IBattleEntity damager in monsterEntity.Damagers) + { + if (damager == null) + { + continue; + } + + if (damager.MapInstance == null) + { + session.SendChatMessage($"Damager: {damager.Type}|{damager.Id} MapInstance null", ChatMessageColorType.Red); + continue; + } + + if (monsterEntity.MapInstance.Id != damager.MapInstance.Id) + { + session.SendChatMessage($"Damager: {damager.Type}|{damager.Id} MapInstance is not at the same map", ChatMessageColorType.Red); + continue; + } + + session.SendChatMessage($"Damager: {damager.Type}|{damager.Id}", ChatMessageColorType.Yellow); + } + + return new SaltyCommandResult(true); + } + + [Command("setsp")] + public async Task setsp(int amount) + { + Context.Player.PlayerEntity.SpPointsBasic = amount; + Context.Player.RefreshSpPoint(); + return new SaltyCommandResult(true); + } + + [Command("mapdance")] + [Description("Dance!")] + public async Task MapDance() + { + Context.Player.CurrentMapInstance.IsDance = !Context.Player.CurrentMapInstance.IsDance; + Context.Player.CurrentMapInstance.Broadcast(x => x.GenerateDance(Context.Player.CurrentMapInstance.IsDance)); + return new SaltyCommandResult(true, $"Map dancing: {Context.Player.CurrentMapInstance.IsDance}"); + } + + [Command("mapmusic")] + [Description("Play music on map")] + public async Task MapMusic(short music) + { + Context.Player.CurrentMapInstance.MapMusic = music; + Context.Player.CurrentMapInstance.Broadcast(x => x.GenerateMapMusic(music)); + return new SaltyCommandResult(true, $"Map music: {music}"); + } + + [Command("mapmusic")] + [Description("Turn off the music on map")] + public async Task MapMusic() + { + Context.Player.CurrentMapInstance.MapMusic = null; + return new SaltyCommandResult(true); + } + + [Command("hlevel", "hlvl")] + [Description("Set player hero level")] + public async Task SetHLvl( + [Description("Hero level")] byte level) + { + IClientSession session = Context.Player; + + session.PlayerEntity.HeroLevel = level; + session.PlayerEntity.HeroXp = 0; + session.PlayerEntity.Hp = session.PlayerEntity.MaxHp; + session.PlayerEntity.Mp = session.PlayerEntity.MaxMp; + + session.RefreshStat(); + session.RefreshStatInfo(); + session.RefreshStatChar(); + session.RefreshLevel(_characterAlgorithm); + return new SaltyCommandResult(true, "Hero level has been updated."); + } + + [Command("debug")] + [Description("toggles the debug mode on the client side")] + public async Task ToggleDebugModeAsync() + { + Context.Player.DebugMode = !Context.Player.DebugMode; + Context.Player.SendChatMessage($"DEBUG_MODE: {(Context.Player.DebugMode ? "ON" : "OFF")}", ChatMessageColorType.Yellow); + if (Context.Player.DebugMode) + { + Context.Player.SendPacket("debug"); + } + + return new SaltyCommandResult(true); + } + + [Command("unlockalltitles")] + [Description("Unlocks all titles available")] + public async Task AddAllTitles() + { + IClientSession session = Context.Player; + + IEnumerable allTitles = _itemManager.GetItemsByType(ItemType.Title); + + foreach (IGameItem title in allTitles.Where(title => session.PlayerEntity.Titles.All(x => x.ItemVnum != title.Id))) + { + session.PlayerEntity.Titles.Add(new CharacterTitleDto + { + TitleId = _itemManager.GetTitleId(title.Id), + ItemVnum = title.Id + }); + } + + session.SendTitlePacket(); + return new SaltyCommandResult(true, "All titles have been unlocked."); + } + + [Command("entities")] + public async Task Entities(byte range) + { + IReadOnlyList entities = Context.Player.CurrentMapInstance.GetBattleEntitiesInRange(Context.Player.PlayerEntity.Position, range); + foreach (IBattleEntity entity in entities) + { + Context.Player.SendChatMessage($"Entity - Type: {entity.Type.ToString()}, Id: {entity.Id}", ChatMessageColorType.Green); + } + + Context.Player.SendChatMessage($"Entities: {entities.Count}", ChatMessageColorType.Red); + return new SaltyCommandResult(true); + } + + [Command("setitemdate")] + public async Task SetItemDate(short slot, short minutes) + { + IClientSession session = Context.Player; + InventoryItem item = session.PlayerEntity.GetItemBySlotAndType(slot, InventoryType.Equipment); + if (item?.ItemInstance == null) + { + return new SaltyCommandResult(false, "No item."); + } + + item.ItemInstance.ItemDeleteTime = DateTime.UtcNow.AddMinutes(minutes); + return new SaltyCommandResult(true, $"The item will disappear: {item.ItemInstance.ItemDeleteTime}"); + } + + [Command("check-pos")] + [Description("Checks if the position is alright")] + public async Task CheckPosition() + { + Position position = Context.Player.PlayerEntity.Position; + return new SaltyCommandResult(true, $"Position CanWalkAround: {Context.Player.CurrentMapInstance.CanWalkAround(position.X, position.Y).ToString()}"); + } + + [Command("nocd")] + [Description("Enable/disable cooldown reduction from skills")] + public async Task NoCd() + { + IClientSession session = Context.Player; + session.PlayerEntity.CheatComponent.HasNoCooldown = !session.PlayerEntity.CheatComponent.HasNoCooldown; + + return new SaltyCommandResult(true, $"No cooldown: {session.PlayerEntity.CheatComponent.HasNoCooldown}"); + } + + [Command("nolimit", "notargetlimit")] + [Description("Enable/disable cooldown reduction from skills")] + public async Task NoTargetLimit() + { + IClientSession session = Context.Player; + session.PlayerEntity.CheatComponent.HasNoTargetLimit = !session.PlayerEntity.CheatComponent.HasNoTargetLimit; + + return new SaltyCommandResult(true, $"No target limit: {session.PlayerEntity.CheatComponent.HasNoTargetLimit.ToString()}"); + } + + [Command("clearinv")] + [Description("Remove all items from your inventory.")] + public async Task ClearInv() + { + IClientSession session = Context.Player; + foreach (InventoryItem eqItem in session.PlayerEntity.EquippedItems) + { + if (eqItem?.ItemInstance == null) + { + continue; + } + + session.PlayerEntity.TakeOffItem(eqItem.ItemInstance.GameItem.EquipmentSlot); + session.PlayerEntity.RefreshEquipmentValues(eqItem.ItemInstance, true); + } + + foreach (InventoryItem item in session.PlayerEntity.GetAllPlayerInventoryItems()) + { + if (item?.ItemInstance == null) + { + continue; + } + + + session.PlayerEntity.RemoveItemFromSlotAndType(item.Slot, item.InventoryType, out InventoryItem removedItem); + session.SendInventoryRemovePacket(removedItem); + } + + Context.Player.PlayerEntity.MinilandObjects.Clear(); + Context.Player.RefreshEquipment(); + Context.Player.RefreshStatChar(); + Context.Player.RefreshStat(); + return new SaltyCommandResult(true, "Inventory has been cleared."); + } + + [Command("recv")] + [Description("Send recv packet to you.")] + public async Task Recv([Remainder] string packet) + { + Context.Player.SendPacket(packet); + return new SaltyCommandResult(true); + } + + [Command("recurrent-walk-bubble")] + [Description("put a bubble on your head with your coordinates")] + public async Task WalkBubble([Description("The amount of minutes you want your bubble")] int minutes) + { + var tmp = new CancellationTokenSource(TimeSpan.FromMinutes(minutes)); + Task.Run(async () => + { + while (!tmp.Token.IsCancellationRequested) + { + Context.Player.SendPacket(Context.Player.GenerateBubble($"X: {Context.Player.PlayerEntity.PositionX} | Y: {Context.Player.PlayerEntity.PositionY}")); + await Task.Delay(500, tmp.Token); + } + }); + return new SaltyCommandResult(true); + } + + [Command("haircolor")] + [Description("Sets your hair color")] + public async Task HairColor(byte hairColor) + { + IClientSession session = Context.Player; + if (!Enum.IsDefined(typeof(HairColorType), hairColor)) + { + return new SaltyCommandResult(false, "The given color was not found."); + } + + session.PlayerEntity.HairColor = (HairColorType)hairColor; + session.BroadcastEq(); + return new SaltyCommandResult(true); + } + + + [Command("recvtarget")] + [Description("Send recv packet to target.")] + public async Task RecvTarget(IClientSession target, [Remainder] string packet) + { + target.SendPacket(packet); + return new SaltyCommandResult(true, $"Receiving packet: {packet}"); + } + + [Command("kick-all", "clear-channel")] + [Description("Kick all players in the channel.")] + public async Task KickPlayers() + { + var tmp = _sessionManager.Sessions.ToList(); + foreach (IClientSession session in tmp) + { + if (session == Context.Player) + { + continue; + } + + Context.Player.SendSuccessChatMessage($"[KICK_ALL] Kicking {session.PlayerEntity.Name}..."); + session.ForceDisconnect(); + } + + return new SaltyCommandResult(true, "All player has been kicked on the channel."); + } + + [Command("flush-all", "flush-save")] + [Description("Kick all players in the channel.")] + public async Task FlushAll() + { + var stopwatch = Stopwatch.StartNew(); + await _characterService.FlushCharacterSaves(new DbServerFlushCharacterSavesRequest()); + stopwatch.Stop(); + return new SaltyCommandResult(true, $"Flushed after {stopwatch.ElapsedMilliseconds.ToString()}ms"); + } + + [Command("recipe")] + [Description("Get all items for recipe of item")] + public async Task Recipe( + [Description("Item vnum to create")] short vNum) + { + IClientSession session = Context.Player; + + IReadOnlyList recipes = _recipeManager.GetRecipeByProducedItemVnum(vNum); + + if (recipes == null) + { + return new SaltyCommandResult(false, "No recipe for given item."); + } + + foreach (Recipe item in recipes) + { + foreach (RecipeItemDTO recipeItem in item.Items) + { + GameItemInstance newItem = _gameItemInstanceFactory.CreateItem(recipeItem.ItemVNum, recipeItem.Amount); + await session.AddNewItemToInventory(newItem); + } + } + + return new SaltyCommandResult(true, "Items have been added to your inventory."); + } + + [Command("drop")] + [Description("Drop the item on the floor")] + public async Task DropAsync(short vNum, short amount) + { + IClientSession session = Context.Player; + + var position = new Position(session.PlayerEntity.PositionX, session.PlayerEntity.PositionY); + await _eventPipeline.ProcessEventAsync(new DropMapItemEvent(session.CurrentMapInstance, position, vNum, amount)); + return new SaltyCommandResult(true, "The item dropped."); + } + + [Command("sp")] + [Description("Transform to SP instantly")] + public async Task SpecialistAsync() + { + IClientSession session = Context.Player; + + GameItemInstance sp = session.PlayerEntity.Specialist; + if (sp == null) + { + return new SaltyCommandResult(false); + } + + await session.EmitEventAsync(new SpTransformEvent + { + Specialist = sp + }); + + return new SaltyCommandResult(true); + } + + [Command("sound")] + public async Task Sound(int value) + { + Context.Player.SendGuriPacket(19, 0, value); + return new SaltyCommandResult(true); + } + + [Command("sound")] + public async Task Sound(int value, IClientSession target) + { + if (target == null) + { + return new SaltyCommandResult(false); + } + + target.SendGuriPacket(19, 0, value); + return new SaltyCommandResult(true); + } + + [Command("bufftarget")] + public async Task BuffTarget(short buffId) + { + IClientSession session = Context.Player; + + IBattleEntity target = session.CurrentMapInstance.GetBattleEntity(session.PlayerEntity.LastEntity.Item1, session.PlayerEntity.LastEntity.Item2); + Card card = _cards.GetCardByCardId(buffId); + + if (card == null) + { + return new SaltyCommandResult(false, "Not a buff"); + } + + if (target == null) + { + return new SaltyCommandResult(false, "Target no exist!"); + } + + Buff buff = _buffFactory.CreateBuff(card.Id, session.PlayerEntity); + await target.AddBuffAsync(buff); + + return new SaltyCommandResult(true, + $"Added buff {_language.GetLanguage(GameDataType.Card, card.Name, session.UserLanguage)} for target ({target.Type.ToString()}|{target.Id})"); + } + + [Command("butcher")] + public async Task Butcher(byte range = 0) + { + IClientSession session = Context.Player; + + int count = 0; + IReadOnlyList entities = session.CurrentMapInstance.GetAliveMonsters(); + + foreach (IMonsterEntity monster in entities.ToList()) + { + if (range > 0 && !monster.Position.IsInAoeZone(session.PlayerEntity, range)) + { + continue; + } + + if (!monster.IsAlive()) + { + continue; + } + + monster.MapInstance.Broadcast(monster.GenerateOut()); + await _eventPipeline.ProcessEventAsync(new MonsterDeathEvent(monster) + { + IsByCommand = true + }); + count++; + } + + return new SaltyCommandResult(true, $"Killed {count} monsters"); + } + + [Command("kill")] + [Description("Kills a player in the map you are.")] + public async Task Kill( + [Description("Name of the character to kill")] + string nickname, + [Description("Should the players know that it was your fault?")] + bool anonymous = true) + { + IClientSession target = Context.Player.PlayerEntity.MapInstance.Sessions.FirstOrDefault(s => s.CharacterName() == nickname); + return BasicKillLogic(Context.Player.PlayerEntity, target.PlayerEntity, anonymous); + } + + [Command("killtarget")] + [Description("Kills a player in the map you are.")] + public async Task KillTarget( + [Description("Should the players know that it was your fault?")] + bool anonymous = true) + { + IClientSession session = Context.Player; + IBattleEntity target = session.CurrentMapInstance.GetBattleEntity(session.PlayerEntity.LastEntity.Item1, session.PlayerEntity.LastEntity.Item2); + return BasicKillLogic(session.PlayerEntity, target, anonymous); + } + + private SaltyCommandResult BasicKillLogic(IBattleEntity issuer, IBattleEntity victim, bool anonymous) => anonymous ? AnonymousKill(victim) : PublicKill(issuer, victim); + + private SaltyCommandResult AnonymousKill(IBattleEntity suicidalEntity) + { + if (suicidalEntity == default) + { + return new SaltyCommandResult(false, "The target provided is not valid"); + } + + var algorithmResult = new DamageAlgorithmResult(int.MaxValue, HitType.Critical, true, false); + _eventPipeline.ProcessEventAsync(new ApplyHitEvent(suicidalEntity, algorithmResult, new HitInformation(suicidalEntity, _skillsManager.GetSkill(299).GetInfo()))); + + return new SaltyCommandResult(true); + } + + private SaltyCommandResult PublicKill(IBattleEntity issuer, IBattleEntity victim) + { + if (issuer == default || victim == default) + { + return new SaltyCommandResult(false, "The target provided is not valid"); + } + + var algorithmResult = new DamageAlgorithmResult(int.MaxValue, HitType.Critical, true, false); + _eventPipeline.ProcessEventAsync(new ApplyHitEvent(victim, algorithmResult, new HitInformation(issuer, _skillsManager.GetSkill(1049).GetInfo()))); + return new SaltyCommandResult(true); + } + + + [Command("map-respawn")] + public async Task MapRespawn(byte range = 0) + { + IClientSession session = Context.Player; + + int count = 0; + IReadOnlyList entities = session.CurrentMapInstance.GetDeadMonsters(); + + foreach (IMonsterEntity monster in entities.ToList()) + { + if (range > 0 && !monster.Position.IsInAoeZone(session.PlayerEntity, range)) + { + continue; + } + + monster.Death = DateTime.MinValue; + count++; + } + + return new SaltyCommandResult(true, $"{count} monsters are now alive!"); + } + + [Command("cleardrops")] + public async Task ClearDrops(byte range = 0) + { + IClientSession session = Context.Player; + + int count = 0; + IReadOnlyList drops = session.CurrentMapInstance.Drops; + + foreach (MapItem drop in drops) + { + if (range > 0 && !session.PlayerEntity.Position.IsInAoeZone(new Position(drop.PositionX, drop.PositionY), range)) + { + continue; + } + + count++; + drop.BroadcastOut(); + session.CurrentMapInstance.RemoveDrop(drop.TransportId); + } + + return new SaltyCommandResult(true, $"{count} drops has been removed from the map."); + } + + [Command("removebuff")] + [Description("Remove the given buff from the target")] + public async Task RemoveBuff(IClientSession target, int cardId, bool force = false) + { + IClientSession session = Context.Player; + + if (target == null) + { + return new SaltyCommandResult(false, "target is offline"); + } + + Buff tmp = target.PlayerEntity.BuffComponent.GetBuff(cardId); + if (tmp == null) + { + return new SaltyCommandResult(false, $"Given target does not have Buff with cardId: {cardId}"); + } + + await target.PlayerEntity.RemoveBuffAsync(force, tmp); + + + return new SaltyCommandResult(true); + } + + [Command("sethp")] + [Description("Set your character hp to selected value")] + public async Task SetHp(int hp, bool mates = false) + { + IClientSession session = Context.Player; + if (hp == 0) + { + return new SaltyCommandResult(false); + } + + session.PlayerEntity.Hp = hp; + + session.RefreshStat(); + session.RefreshStatInfo(); + + if (!mates) + { + return new SaltyCommandResult(true); + } + + foreach (IMateEntity mate in session.PlayerEntity.MateComponent.TeamMembers()) + { + mate.Hp = hp; + } + + session.RefreshMateStats(); + + return new SaltyCommandResult(true); + } + + [Command("setmp")] + [Description("Set your character mp to selected value")] + public async Task SetMp(ushort mp, bool mates = false) + { + IClientSession session = Context.Player; + + session.PlayerEntity.Mp = mp; + + session.RefreshStat(); + session.RefreshStatInfo(); + + if (!mates) + { + return new SaltyCommandResult(true); + } + + foreach (IMateEntity mate in session.PlayerEntity.MateComponent.TeamMembers()) + { + mate.Mp = mp; + } + + session.RefreshMateStats(); + + return new SaltyCommandResult(true); + } + + [Command("addskill")] + [Description("Add skill.")] + public async Task AddSkillAsync( + [Description("Skill VNUM.")] short skillVNum) + { + IClientSession session = Context.Player; + SkillDTO skillinfo = _skillManager.GetSkill(skillVNum); + + if (skillinfo == null) + { + return new SaltyCommandResult(false, "The skill doesn't exist!"); + } + + if (skillinfo.Id < 200) + { + foreach (CharacterSkill skill in session.PlayerEntity.CharacterSkills.Select(s => s.Value)) + { + if (skillinfo.CastId == skill.Skill.CastId && skill.Skill.Id < 200) + { + session.PlayerEntity.CharacterSkills.TryRemove(skill.SkillVNum, out CharacterSkill _); + } + } + } + else + { + if (session.PlayerEntity.CharacterSkills.ContainsKey(skillVNum)) + { + return new SaltyCommandResult(true, "You have already this skill learnt."); + } + + if (skillinfo.UpgradeSkill != 0) + { + CharacterSkill oldupgrade = session.PlayerEntity.CharacterSkills.Select(s => s.Value).FirstOrDefault(s => + s.Skill.UpgradeSkill == skillinfo.UpgradeSkill && s.Skill.UpgradeType == skillinfo.UpgradeType && s.Skill.UpgradeSkill != 0); + if (oldupgrade != null) + { + session.PlayerEntity.CharacterSkills.TryRemove(oldupgrade.SkillVNum, out CharacterSkill _); + } + } + } + + var newSkill = new CharacterSkill { SkillVNum = skillVNum }; + + session.PlayerEntity.CharacterSkills[skillVNum] = newSkill; + session.PlayerEntity.Skills.Add(newSkill); + session.RefreshSkillList(); + session.RefreshQuicklist(); + session.RefreshLevel(_characterAlgorithm); + return new SaltyCommandResult(true, $"Skill {_language.GetLanguage(GameDataType.Skill, skillinfo.Name, session.UserLanguage)} has been added."); + } + + [Command("removeskill")] + [Description("Remove skill.")] + public async Task RemoveSkillAsync( + [Description("Skill VNUM.")] short skillvnum) + { + IClientSession session = Context.Player; + SkillDTO skillinfo = _skillManager.GetSkill(skillvnum); + + if (skillinfo == null) + { + return new SaltyCommandResult(false, "The skill doesn't exist!"); + } + + session.PlayerEntity.CharacterSkills.TryRemove(skillvnum, out CharacterSkill _); + IBattleEntitySkill toRemove = session.PlayerEntity.Skills.FirstOrDefault(x => x.Skill.Id == skillvnum); + session.PlayerEntity.Skills.Remove(toRemove); + session.RefreshSkillList(); + session.RefreshQuicklist(); + session.RefreshLevel(_characterAlgorithm); + return new SaltyCommandResult(true, "Skill has been removed."); + } + + + [Command("online")] + [Description("Show players in game")] + public async Task PlayersOnlineAsync() + { + IClientSession session = Context.Player; + + session.SendChatMessage("[Players on the server]", ChatMessageColorType.Red); + foreach (IClientSession s in _sessionManager.Sessions) + { + session.SendChatMessage($"{s.PlayerEntity.Name} | AccID: {s.Account.Id} | CVersion: {s.ClientVersion}", ChatMessageColorType.LightPurple); + } + + session.SendChatMessage($"Online: {_sessionManager.SessionsCount}", ChatMessageColorType.Red); + return new SaltyCommandResult(true); + } + + + [Command("list-hw")] + [Description("Show players in game")] + public async Task ListHardwareIdsAsync() + { + IClientSession session = Context.Player; + + session.SendChatMessage("[Players on the server]", ChatMessageColorType.Red); + foreach (IClientSession s in _sessionManager.Sessions) + { + session.SendChatMessage($"{s.PlayerEntity.Name} | HWID: {s.HardwareId}", ChatMessageColorType.LightPurple); + } + + session.SendChatMessage($"Online: {_sessionManager.SessionsCount}", ChatMessageColorType.Red); + return new SaltyCommandResult(true); + } + + [Command("stat")] + [Description("Current server configuration")] + public async Task StatAsync() + { + IClientSession session = Context.Player; + + session.SendChatMessage("[Current server rates]", ChatMessageColorType.Red); + session.SendChatMessage($"XP Rate: {_manager.MobXpRate}", ChatMessageColorType.LightPurple); + session.SendChatMessage($"JobXP Rate: {_manager.JobXpRate}", ChatMessageColorType.LightPurple); + session.SendChatMessage($"HeroXP Rate: {_manager.HeroXpRate}", ChatMessageColorType.LightPurple); + session.SendChatMessage($"FairyXP Rate: {_manager.FairyXpRate}", ChatMessageColorType.LightPurple); + session.SendChatMessage($"MateXP Rate: {_manager.MateXpRate}", ChatMessageColorType.LightPurple); + session.SendChatMessage($"FamilyXP Rate: {_manager.FamilyExpRate}", ChatMessageColorType.LightPurple); + session.SendChatMessage($"Reput Rate: {_manager.ReputRate}", ChatMessageColorType.LightPurple); + session.SendChatMessage($"Drop Rate: {_manager.MobDropRate}", ChatMessageColorType.LightPurple); + session.SendChatMessage($"GoldDrop Rate: {_manager.GoldDropRate}", ChatMessageColorType.LightPurple); + session.SendChatMessage($"Gold Rate: {_manager.GoldRate}", ChatMessageColorType.LightPurple); + session.SendChatMessage("[Current server configuration]", ChatMessageColorType.Red); + session.SendChatMessage($"EXP Event: {_manager.ExpEvent}", ChatMessageColorType.LightPurple); + return new SaltyCommandResult(true, ""); + } + + [Command("mapinfo")] + [Description("Current Map informations")] + public async Task DumpMapInformations() + { + IClientSession session = Context.Player; + IMapInstance mapInstance = Context.Player.CurrentMapInstance; + + session.SendChatMessage("[MAP_INFORMATIONS]", ChatMessageColorType.Red); + + foreach (PropertyInfo i in typeof(IMapInstance).GetProperties(BindingFlags.Instance | BindingFlags.Public) + .Where(s => !s.PropertyType.IsClass + && !s.PropertyType.IsAbstract + && !s.PropertyType.IsInterface).OrderBy(s => s.Name)) + { + session.SendChatMessage($"{i.Name} : {i.GetValue(mapInstance)}", ChatMessageColorType.LightPurple); + } + + return new SaltyCommandResult(true, ""); + } + + [Command("targetInfo")] + [Description("Current Map informations")] + public async Task DumpTargetInformation() + { + IClientSession session = Context.Player; + IBattleEntity entity = session.CurrentMapInstance.GetBattleEntity(session.PlayerEntity.LastEntity.Item1, session.PlayerEntity.LastEntity.Item2); + if (entity == null) + { + return new SaltyCommandResult(false); + } + + int? monsterVnum = entity switch + { + IMonsterEntity monsterEntity => monsterEntity.MonsterVNum, + INpcEntity npcEntity => npcEntity.MonsterVNum, + IMateEntity mateEntity => mateEntity.MonsterVNum, + _ => null + }; + + session.SendChatMessage("==========[ Target Info ]==========", ChatMessageColorType.Red); + session.SendChatMessage($"Type: {entity.Type}", ChatMessageColorType.Yellow); + session.SendChatMessage($"Id: {entity.Id}", ChatMessageColorType.Yellow); + session.SendChatMessage($"Position: X - {entity.PositionX} Y - {entity.PositionY}", ChatMessageColorType.Yellow); + session.SendChatMessage($"MonsterVnum: {monsterVnum?.ToString() ?? "None"}", ChatMessageColorType.Yellow); + + return new SaltyCommandResult(true, ""); + } + + [Command("charInfo", "characterInformation")] + [Description("Current Map informations")] + public async Task DumpCharacterInformation() => await DumpCharacterInformation(Context.Player.PlayerEntity.Name); + + [Command("charInfo", "characterInformation")] + [Description("Current Map informations")] + public async Task DumpCharacterInformation(string name) + { + IClientSession session = _sessionManager.GetSessionByCharacterName(name); + + session.SendChatMessage("[CHARACTER_INFORMATION]", ChatMessageColorType.Red); + + foreach (PropertyInfo i in typeof(IPlayerEntity).GetProperties(BindingFlags.Instance | BindingFlags.Public) + .Where(s => !s.PropertyType.IsClass + && !s.PropertyType.IsAbstract + && !s.PropertyType.IsInterface).OrderBy(s => s.Name)) + { + session.SendChatMessage($"{i.Name} : {i.GetValue(session.PlayerEntity)}", ChatMessageColorType.LightPurple); + } + + return new SaltyCommandResult(true, ""); + } + + [Command("buff")] + [Description("Add given buff")] + public async Task BuffAsync( + [Description("Buff vnum")] short vnum) + { + IClientSession session = Context.Player; + + Buff buff = _buffFactory.CreateBuff(vnum, session.PlayerEntity); + if (buff == null) + { + return new SaltyCommandResult(false); + } + + await session.PlayerEntity.AddBuffAsync(buff); + + return new SaltyCommandResult(true, $"Created buff: {_language.GetLanguage(GameDataType.Card, buff.Name, Context.Player.UserLanguage)}"); + } + + [Command("buff")] + [Description("Add given buff")] + public async Task BuffAsync( + [Description("Buff vnum")] short vnum, + [Description("Buff duration in seconds")] + int duration) + { + IClientSession session = Context.Player; + + Buff buff = _buffFactory.CreateBuff(vnum, session.PlayerEntity, TimeSpan.FromSeconds(duration)); + await session.PlayerEntity.AddBuffAsync(buff); + + return new SaltyCommandResult(true, $"Created buff: {_language.GetLanguage(GameDataType.Card, buff.Name, Context.Player.UserLanguage)}"); + } + + [Command("removebuff")] + [Description("Removes given buff")] + public async Task RemoveBuffAsync( + [Description("Buff vnum")] short vnum) + { + IClientSession session = Context.Player; + + if (!session.PlayerEntity.BuffComponent.HasBuff(vnum)) + { + return new SaltyCommandResult(false, "You don't have this buff!"); + } + + Buff buff = session.PlayerEntity.BuffComponent.GetBuff(vnum); + await session.PlayerEntity.RemoveBuffAsync(true, buff); + + return new SaltyCommandResult(true, $"Removed buff {_language.GetLanguage(GameDataType.Card, buff.Name, session.UserLanguage)}"); + } + + [Command("eff")] + [Description("Show effect")] + public async Task EffAsync( + [Description("Effect ID")] short vnum) + { + IClientSession session = Context.Player; + + session.BroadcastEffect(vnum, new RangeBroadcast(session.PlayerEntity.PositionX, session.PlayerEntity.PositionY)); + return new SaltyCommandResult(true); + } + + [Command("refresh-c", "refresh-compliments", "rc")] + [Description("Refresh your monthly compliments.")] + public async Task RefreshMonthlyCompliments([Description("Force refresh")] bool force = false) + { + Context.Player.EmitEvent(new ComplimentsMonthlyRefreshEvent { Force = force }); + return new SaltyCommandResult(true); + } + + [Command("changechannel")] + public async Task ChangeChannel(int channelId) + { + if (channelId == _gameServer.ChannelId) + { + return new SaltyCommandResult(false, "It's the same channel"); + } + + if (channelId == 51 || _gameServer.ChannelType == GameChannelType.ACT_4) + { + return new SaltyCommandResult(false, "Use $act4/$act4leave command instead"); + } + + IClientSession session = Context.Player; + + GetChannelInfoResponse response = await _serverApiService.GetChannelInfo(new GetChannelInfoRequest + { + WorldGroup = _gameServer.WorldGroup, + ChannelId = channelId + }); + + if (response?.ResponseType != RpcResponseType.SUCCESS) + { + return new SaltyCommandResult(false, "Channel doesn't exist"); + } + + IPlayerEntity player = session.PlayerEntity; + + await session.EmitEventAsync(new PlayerChangeChannelEvent(response.GameServer, ItModeType.ToPortAlveus, player.MapId, player.MapX, player.MapY)); + return new SaltyCommandResult(true); + } + + [Command("checkmate")] + public async Task CheckMate() + { + IClientSession session = Context.Player; + IMateEntity target = session.CurrentMapInstance.GetMateById(session.PlayerEntity.LastEntity.Item2); + if (target == null) + { + return new SaltyCommandResult(false, "Mate doesn't exist."); + } + + session.SendChatMessage($"Level: {target.Level}", ChatMessageColorType.Yellow); + session.SendChatMessage($"MonsterRaceType: {target.MonsterRaceType}", ChatMessageColorType.Yellow); + session.SendChatMessage($"AttackType: {target.AttackType}", ChatMessageColorType.Yellow); + session.SendChatMessage($"WeaponLevel: {target.WeaponLevel}", ChatMessageColorType.Yellow); + session.SendChatMessage($"WinfoValue: {target.WinfoValue}", ChatMessageColorType.Yellow); + session.SendChatMessage($"BaseLevel: {target.BaseLevel}", ChatMessageColorType.Yellow); + session.SendChatMessage($"GetModifier: {target.GetModifier()}", ChatMessageColorType.Yellow); + session.SendChatMessage($"CleanDamageMin: {target.CleanDamageMin}", ChatMessageColorType.Yellow); + session.SendChatMessage($"CleanDamageMax: {target.CleanDamageMax}", ChatMessageColorType.Yellow); + session.SendChatMessage($"MateType: {target.MateType}", ChatMessageColorType.Yellow); + + return new SaltyCommandResult(true); + } + + [Command("refreshmate")] + public async Task RefreshMate() + { + IClientSession session = Context.Player; + IMateEntity target = session.CurrentMapInstance.GetMateById(session.PlayerEntity.LastEntity.Item2); + if (target == null) + { + return new SaltyCommandResult(false, "Mate doesn't exist."); + } + + target.RefreshStatistics(); + + return new SaltyCommandResult(true, "Statistics refresh."); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.Essentials/Administrator/Items/ItemManagement.cs b/srcs/_plugins/WingsEmu.Plugins.Essentials/Administrator/Items/ItemManagement.cs new file mode 100644 index 0000000..d71a194 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.Essentials/Administrator/Items/ItemManagement.cs @@ -0,0 +1,66 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using System.Threading.Tasks; +using Qmmands; +using WingsEmu.Commands.Checks; +using WingsEmu.Commands.Entities; +using WingsEmu.DTOs.Account; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.Essentials.Administrator.Items; + +[Group("itemtoggle", "itoggle", "itog")] +[Name("Admin-ItemManagement")] +[Description("Module related to Administrator commands for management.")] +[RequireAuthority(AuthorityType.Owner)] +public class AdministratorItemManagementModule : SaltyModuleBase +{ + private readonly IGameLanguageService _gameLanguage; + private readonly IItemsManager _itemsManager; + private readonly IItemUsageToggleManager _itemUsageToggleManager; + + public AdministratorItemManagementModule(IItemUsageToggleManager itemUsageToggleManager, IItemsManager itemsManager, IGameLanguageService gameLanguage) + { + _itemUsageToggleManager = itemUsageToggleManager; + _itemsManager = itemsManager; + _gameLanguage = gameLanguage; + } + + [Command("block", "restrict", "disable", "d", "r")] + public async Task BlockItemUsage(int vnum) + { + await _itemUsageToggleManager.BlockItemUsage(vnum); + return new SaltyCommandResult(true, $"{vnum} has been temporarily blocked!"); + } + + [Command("unblock", "authorize", "enable", "e", "a")] + public async Task UnblockItemUsage(int vnum) + { + await _itemUsageToggleManager.UnblockItemUsage(vnum); + return new SaltyCommandResult(true, $"{vnum} has been unblocked!"); + } + + [Command("list", "get")] + public async Task ListCurrentlyBlockedItemUsage() + { + IEnumerable tmp = await _itemUsageToggleManager.GetBlockedItemUsages(); + Context.Player.SendChatMessage("[BLOCKED_ITEMS]", ChatMessageColorType.Green); + Context.Player.SendChatMessage("===============================", ChatMessageColorType.Green); + foreach (int itemBlocked in tmp) + { + IGameItem itemName = _itemsManager.GetItem(itemBlocked); + Context.Player.SendChatMessage($"[{itemBlocked}] => {_gameLanguage.GetLanguage(GameDataType.Item, itemName.Name, Context.Player.UserLanguage)}", ChatMessageColorType.Green); + } + + Context.Player.SendChatMessage("===============================", ChatMessageColorType.Green); + return new SaltyCommandResult(true); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.Essentials/Administrator/Items/RefundModule.cs b/srcs/_plugins/WingsEmu.Plugins.Essentials/Administrator/Items/RefundModule.cs new file mode 100644 index 0000000..25ff172 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.Essentials/Administrator/Items/RefundModule.cs @@ -0,0 +1,365 @@ +using System; +using System.Threading.Tasks; +using Qmmands; +using WingsAPI.Game.Extensions.Families; +using WingsAPI.Game.Extensions.Groups; +using WingsEmu.Commands.Checks; +using WingsEmu.Commands.Entities; +using WingsEmu.DTOs.Account; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage.Configuration; +using WingsEmu.Game.Algorithm; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Families.Enum; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Character; +using WingsEmu.Packets.Enums.Chat; +using WingsEmu.Packets.Enums.Families; + +namespace WingsEmu.Plugins.Essentials.Administrator.Items; + +[Name("Refund")] +[Description("Module related to Administrator commands for refunds.")] +[RequireAuthority(AuthorityType.GameAdmin)] +public class RefundModule : SaltyModuleBase +{ + private readonly ICharacterAlgorithm _characterAlgorithm; + + public RefundModule(ICharacterAlgorithm characterAlgorithm) => _characterAlgorithm = characterAlgorithm; + + [Command("resistances")] + public async Task Resistances(byte slot, byte upgrade, short fire, short water, short light, short shadow) + { + IClientSession session = Context.Player; + InventoryItem getItem = session.PlayerEntity.GetItemBySlotAndType(slot, InventoryType.Equipment); + if (getItem?.ItemInstance == null) + { + return new SaltyCommandResult(false, $"Couldn't find item on slot: {slot} in Equipment type"); + } + + GameItemInstance item = getItem.ItemInstance; + if (item.GameItem.EquipmentSlot != EquipmentType.Gloves && item.GameItem.EquipmentSlot != EquipmentType.Boots) + { + return new SaltyCommandResult(false, "The item is not gloves or boots"); + } + + item.Upgrade = upgrade; + item.DarkResistance = shadow; + item.LightResistance = light; + item.WaterResistance = water; + item.FireResistance = fire; + + session.SendInventoryAddPacket(getItem); + + return new SaltyCommandResult(true, "Item completed, check inventory."); + } + + [Command("addxp")] + public async Task AddXp(IClientSession target, long xp) + { + await target.EmitEventAsync(new AddExpEvent(xp, LevelType.Level)); + target.RefreshLevel(_characterAlgorithm); + + return new SaltyCommandResult(true, $"Added {xp} xp to the {target.PlayerEntity.Name}."); + } + + [Group("set")] + public class RefundSetModule : SaltyModuleBase + { + private readonly ICharacterAlgorithm _characterAlgorithm; + private readonly IRankingManager _rankingManager; + private readonly IReputationConfiguration _reputationConfiguration; + private readonly IServerManager _serverManager; + private readonly ISpPartnerConfiguration _spPartnerConfiguration; + + public RefundSetModule(IServerManager serverManager, ICharacterAlgorithm characterAlgorithm, ISpPartnerConfiguration spPartnerConfiguration, + IRankingManager rankingManager, IReputationConfiguration reputationConfiguration) + { + _serverManager = serverManager; + _characterAlgorithm = characterAlgorithm; + _spPartnerConfiguration = spPartnerConfiguration; + _rankingManager = rankingManager; + _reputationConfiguration = reputationConfiguration; + } + + [Command("level")] + public async Task Level(IClientSession target, byte level) + { + IPlayerEntity character = target.PlayerEntity; + + character.LevelXp = 0; + character.Level = level; + + if (character.Level >= _serverManager.MaxLevel) + { + character.Level = (byte)_serverManager.MaxLevel; + character.LevelXp = 0; + } + + character.Session.RefreshStatChar(); + + character.Hp = character.MaxHp; + character.Mp = character.MaxMp; + + character.Session.RefreshStat(); + + if (character.Level > 20 && (character.Level % 10) == 0) + { + await target.FamilyAddLogAsync(FamilyLogType.LevelUp, character.Name, character.Level.ToString()); + await target.FamilyAddExperience(character.Level * 20, FamXpObtainedFromType.LevelUp); + } + else if (character.Level > 80) + { + await target.FamilyAddLogAsync(FamilyLogType.LevelUp, character.Name, character.Level.ToString()); + } + + target.SendLevelUp(); + target.RefreshLevel(_characterAlgorithm); + target.RefreshGroupLevelUi(_spPartnerConfiguration); + target.SendMsg(target.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_LEVELUP), MsgMessageType.Middle); + target.BroadcastEffectInRange(EffectType.NormalLevelUp); + target.BroadcastEffectInRange(EffectType.NormalLevelUpSubEffect); + return new SaltyCommandResult(true, "Level successfully set."); + } + + [Command("job")] + public async Task Job(IClientSession target, byte level) + { + IPlayerEntity character = target.PlayerEntity; + character.JobLevelXp = 0; + character.JobLevel = level; + + if (character.JobLevel >= 20 && character.Class == ClassType.Adventurer) + { + character.JobLevel = 20; + character.JobLevelXp = 0; + } + else if (character.JobLevel >= _serverManager.MaxJobLevel) + { + character.JobLevel = (byte)_serverManager.MaxJobLevel; + character.JobLevelXp = 0; + } + + target.SendLevelUp(); + target.RefreshLevel(_characterAlgorithm); + target.SendMsg(target.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_JOB_LEVELUP), MsgMessageType.Middle); + target.BroadcastEffectInRange(EffectType.JobLevelUp); + character.SkillComponent.ResetSkillCooldowns = DateTime.UtcNow; + + return new SaltyCommandResult(true, "Job Level successfully set."); + } + + [Command("spjob")] + public async Task SpJob(IClientSession target, byte level) + { + IPlayerEntity character = target.PlayerEntity; + + if (character.Specialist == null || !character.UseSp) + { + return new SaltyCommandResult(false, "Player doesn't have Specialist Card in slot."); + } + + character.Specialist.Xp = 0; + character.Specialist.SpLevel = level; + + if (character.Specialist.SpLevel >= _serverManager.MaxSpLevel) + { + character.Specialist.SpLevel = (byte)_serverManager.MaxSpLevel; + character.Specialist.Xp = 0; + } + + target.RefreshLevel(_characterAlgorithm); + target.SendLevelUp(); + target.SendMsg(target.GetLanguage(GameDialogKey.SPECIALIST_SHOUTMESSAGE_LEVELUP), MsgMessageType.Middle); + target.BroadcastEffectInRange(EffectType.JobLevelUp); + character.SkillComponent.ResetSpSkillCooldowns = DateTime.UtcNow; + + return new SaltyCommandResult(true, "Specialist Level successfully set."); + } + + [Command("reput")] + public async Task Reputation(IClientSession target, long reput) + { + target.PlayerEntity.Reput = reput; + target.RefreshReputation(_reputationConfiguration, _rankingManager.TopReputation); + + return new SaltyCommandResult(true, "Reputation successfully set."); + } + + [Command("gold")] + public async Task Gold(IClientSession target, long gold) + { + if (gold < 0) + { + return new SaltyCommandResult(false, "Wrong value!"); + } + + if (gold > _serverManager.MaxGold) + { + return new SaltyCommandResult(false, "Wrong value!"); + } + + target.PlayerEntity.Gold = gold; + target.RefreshGold(); + + return new SaltyCommandResult(true, "Gold successfully set."); + } + } + + [Group("add")] + public class RefundAddModule : SaltyModuleBase + { + private readonly ICharacterAlgorithm _characterAlgorithm; + private readonly IRankingManager _rankingManager; + private readonly IReputationConfiguration _reputationConfiguration; + private readonly IServerManager _serverManager; + private readonly ISpPartnerConfiguration _spPartnerConfiguration; + + public RefundAddModule(IServerManager serverManager, ICharacterAlgorithm characterAlgorithm, ISpPartnerConfiguration spPartnerConfiguration, + IRankingManager rankingManager, IReputationConfiguration reputationConfiguration) + { + _serverManager = serverManager; + _characterAlgorithm = characterAlgorithm; + _spPartnerConfiguration = spPartnerConfiguration; + _rankingManager = rankingManager; + _reputationConfiguration = reputationConfiguration; + } + + [Command("level")] + public async Task Level(IClientSession target, byte level) + { + IPlayerEntity character = target.PlayerEntity; + + character.LevelXp = 0; + character.Level += level; + + if (character.Level >= _serverManager.MaxLevel) + { + character.Level = (byte)_serverManager.MaxLevel; + character.LevelXp = 0; + } + + character.Session.RefreshStatChar(); + + character.Hp = character.MaxHp; + character.Mp = character.MaxMp; + + character.Session.RefreshStat(); + + if (character.Level > 20 && (character.Level % 10) == 0) + { + await target.FamilyAddLogAsync(FamilyLogType.LevelUp, character.Name, character.Level.ToString()); + await target.FamilyAddExperience(character.Level * 20, FamXpObtainedFromType.LevelUp); + } + else if (character.Level > 80) + { + await target.FamilyAddLogAsync(FamilyLogType.LevelUp, character.Name, character.Level.ToString()); + } + + target.SendLevelUp(); + target.RefreshLevel(_characterAlgorithm); + target.RefreshGroupLevelUi(_spPartnerConfiguration); + target.SendMsg(target.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_LEVELUP), MsgMessageType.Middle); + target.BroadcastEffectInRange(EffectType.NormalLevelUp); + target.BroadcastEffectInRange(EffectType.NormalLevelUpSubEffect); + return new SaltyCommandResult(true, "Level successfully add."); + } + + [Command("job")] + public async Task Job(IClientSession target, byte level) + { + IPlayerEntity character = target.PlayerEntity; + character.JobLevelXp = 0; + character.JobLevel += level; + + if (character.JobLevel >= 20 && character.Class == ClassType.Adventurer) + { + character.JobLevel = 20; + character.JobLevelXp = 0; + } + else if (character.JobLevel >= _serverManager.MaxJobLevel) + { + character.JobLevel = (byte)_serverManager.MaxJobLevel; + character.JobLevelXp = 0; + } + + target.SendLevelUp(); + target.RefreshLevel(_characterAlgorithm); + target.SendMsg(target.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_JOB_LEVELUP), MsgMessageType.Middle); + target.BroadcastEffectInRange(EffectType.JobLevelUp); + character.SkillComponent.ResetSkillCooldowns = DateTime.UtcNow; + + return new SaltyCommandResult(true, "Job Level successfully add."); + } + + [Command("spjob")] + public async Task SpJob(IClientSession target, byte level) + { + IPlayerEntity character = target.PlayerEntity; + + if (character.Specialist == null || !character.UseSp) + { + return new SaltyCommandResult(false, "Player doesn't have Specialist Card in slot."); + } + + character.Specialist.Xp = 0; + character.Specialist.SpLevel += level; + + if (character.Specialist.SpLevel >= _serverManager.MaxSpLevel) + { + character.Specialist.SpLevel = (byte)_serverManager.MaxSpLevel; + character.Specialist.Xp = 0; + } + + target.RefreshLevel(_characterAlgorithm); + target.SendLevelUp(); + target.SendMsg(target.GetLanguage(GameDialogKey.SPECIALIST_SHOUTMESSAGE_LEVELUP), MsgMessageType.Middle); + target.BroadcastEffectInRange(EffectType.JobLevelUp); + character.SkillComponent.ResetSpSkillCooldowns = DateTime.UtcNow; + + return new SaltyCommandResult(true, "Specialist Level successfully add."); + } + + [Command("reput")] + public async Task Reputation(IClientSession target, long reput) + { + target.PlayerEntity.Reput += reput; + target.RefreshReputation(_reputationConfiguration, _rankingManager.TopReputation); + + return new SaltyCommandResult(true, "Reputation successfully add."); + } + + [Command("gold")] + public async Task Gold(IClientSession target, long gold) + { + if (gold < 0) + { + return new SaltyCommandResult(false, "Wrong value!"); + } + + if (gold > _serverManager.MaxGold) + { + return new SaltyCommandResult(false, "Wrong value!"); + } + + target.PlayerEntity.Gold += gold; + + if (target.PlayerEntity.Gold > _serverManager.MaxGold) + { + target.PlayerEntity.Gold = _serverManager.MaxGold; + } + + target.RefreshGold(); + + return new SaltyCommandResult(true, "Gold successfully add."); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.Essentials/EssentialsPlugin.cs b/srcs/_plugins/WingsEmu.Plugins.Essentials/EssentialsPlugin.cs new file mode 100644 index 0000000..d42a095 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.Essentials/EssentialsPlugin.cs @@ -0,0 +1,80 @@ +using WingsAPI.Plugins; +using WingsEmu.Commands.Interfaces; +using WingsEmu.Commands.TypeParsers; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Maps; +using WingsEmu.Plugins.Essentials.Account; +using WingsEmu.Plugins.Essentials.Administrator; +using WingsEmu.Plugins.Essentials.Administrator.Items; +using WingsEmu.Plugins.Essentials.GameMaster; +using WingsEmu.Plugins.Essentials.God; +using WingsEmu.Plugins.Essentials.Help; +using WingsEmu.Plugins.Essentials.NPC; +using WingsEmu.Plugins.Essentials.Skills; +using WingsEmu.Plugins.Essentials.Teleport; + +namespace WingsEmu.Plugins.Essentials; + +public class EssentialsPlugin : IGamePlugin +{ + private readonly ICommandContainer _commands; + private readonly IItemsManager _itemManager; + private readonly IMapManager _mapManager; + private readonly ISessionManager _sessionManager; + + public EssentialsPlugin(ICommandContainer commandContainer, IMapManager mapManager, ISessionManager sessionManager, IItemsManager itemManager) + { + _itemManager = itemManager; + _sessionManager = sessionManager; + _mapManager = mapManager; + _commands = commandContainer; + } + + public string Name => nameof(EssentialsPlugin); + + public void OnLoad() + { + _commands.AddTypeParser(new PlayerEntityTypeParser(_sessionManager)); + _commands.AddTypeParser(new MapInstanceTypeParser(_mapManager)); + _commands.AddTypeParser(new ItemTypeParser(_itemManager)); + // admin + _commands.AddModule(); + _commands.AddModule(); + _commands.AddModule(); + _commands.AddModule(); + _commands.AddModule(); + _commands.AddModule(); + _commands.AddModule(); + _commands.AddModule(); + + // item management + _commands.AddModule(); + _commands.AddModule(); + _commands.AddModule(); + + // bazaar + _commands.AddModule(); + + // inventory + + // super game master + _commands.AddModule(); + _commands.AddModule(); + + // character + _commands.AddModule(); + _commands.AddModule(); + _commands.AddModule(); + _commands.AddModule(); + _commands.AddModule(); + _commands.AddModule(); + _commands.AddModule(); + _commands.AddModule(); + _commands.AddModule(); + _commands.AddModule(); + _commands.AddModule(); + _commands.AddModule(); + _commands.AddModule(); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.Essentials/GameMaster/CharacterModule.cs b/srcs/_plugins/WingsEmu.Plugins.Essentials/GameMaster/CharacterModule.cs new file mode 100644 index 0000000..4272eb2 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.Essentials/GameMaster/CharacterModule.cs @@ -0,0 +1,448 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Qmmands; +using WingsAPI.Communication.DbServer.CharacterService; +using WingsAPI.Communication.Player; +using WingsAPI.Communication.ServerApi.Protocol; +using WingsAPI.Data.Character; +using WingsAPI.Game.Extensions.Families; +using WingsAPI.Game.Extensions.Groups; +using WingsEmu.Commands.Checks; +using WingsEmu.Commands.Entities; +using WingsEmu.DTOs.Account; +using WingsEmu.DTOs.Inventory; +using WingsEmu.DTOs.Maps; +using WingsEmu.DTOs.Titles; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage.Configuration; +using WingsEmu.Game.Algorithm; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Extensions.Mates; +using WingsEmu.Game.Families; +using WingsEmu.Game.InterChannel; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Networking.Broadcasting; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.Essentials.GameMaster; + +[Name("Game Master")] +[Description("Module related to Game Master commands.")] +[RequireAuthority(AuthorityType.GameMaster)] +public class CharacterModule : SaltyModuleBase +{ + private readonly IBuffFactory _buffFactory; + private readonly ICharacterAlgorithm _characterAlgorithm; + private readonly ICharacterService _characterService; + private readonly IGameLanguageService _gameLanguage; + private readonly SerializableGameServer _gameServer; + private readonly IItemsManager _itemManager; + private readonly IRankingManager _rankingManager; + private readonly IReputationConfiguration _reputationConfiguration; + private readonly ISessionManager _sessionManager; + private readonly ISpPartnerConfiguration _spPartnerConfiguration; + + public CharacterModule(SerializableGameServer gameServer, ICharacterAlgorithm characterAlgorithm, ISessionManager sessionManager, + IItemsManager itemsManager, IReputationConfiguration reputationConfiguration, IGameLanguageService gameLanguage, ISpPartnerConfiguration spPartnerConfiguration, + IRankingManager rankingManager, IBuffFactory buffFactory, ICharacterService characterService) + { + _itemManager = itemsManager; + _reputationConfiguration = reputationConfiguration; + _gameLanguage = gameLanguage; + _spPartnerConfiguration = spPartnerConfiguration; + _rankingManager = rankingManager; + _buffFactory = buffFactory; + _characterService = characterService; + _gameServer = gameServer; + _sessionManager = sessionManager; + _characterAlgorithm = characterAlgorithm; + } + + [Command("char-stats")] + [Description("Look others inventory")] + public async Task CharacterStats(IClientSession target) + { + IClientSession session = Context.Player; + + session.SendChatMessage($"Damage dealt: {target.PlayerEntity.LifetimeStats.TotalDamageDealt}", ChatMessageColorType.Green); + session.SendChatMessage($"Food used: {target.PlayerEntity.LifetimeStats.TotalFoodUsed}", ChatMessageColorType.Green); + session.SendChatMessage($"Gold dropped: {target.PlayerEntity.LifetimeStats.TotalGoldDropped}", ChatMessageColorType.Green); + session.SendChatMessage($"Gold spent: {target.PlayerEntity.LifetimeStats.TotalGoldSpent}", ChatMessageColorType.Green); + session.SendChatMessage($"Items used: {target.PlayerEntity.LifetimeStats.TotalItemsUsed}", ChatMessageColorType.Green); + session.SendChatMessage($"Monsters killed: {target.PlayerEntity.LifetimeStats.TotalMonstersKilled}", ChatMessageColorType.Green); + session.SendChatMessage($"Players killed: {target.PlayerEntity.LifetimeStats.TotalPlayersKilled}", ChatMessageColorType.Green); + session.SendChatMessage($"Potions used: {target.PlayerEntity.LifetimeStats.TotalPotionsUsed}", ChatMessageColorType.Green); + session.SendChatMessage($"Raids lost: {target.PlayerEntity.LifetimeStats.TotalRaidsLost}", ChatMessageColorType.Green); + session.SendChatMessage($"Raids won: {target.PlayerEntity.LifetimeStats.TotalRaidsWon}", ChatMessageColorType.Green); + session.SendChatMessage($"Skills casted: {target.PlayerEntity.LifetimeStats.TotalSkillsCasted}", ChatMessageColorType.Green); + session.SendChatMessage($"Snacks used: {target.PlayerEntity.LifetimeStats.TotalSnacksUsed}", ChatMessageColorType.Green); + session.SendChatMessage($"Total time online: {target.PlayerEntity.LifetimeStats.TotalTimeOnline}", ChatMessageColorType.Green); + session.SendChatMessage($"TimeSpace won: {target.PlayerEntity.LifetimeStats.TotalTimespacesWon}", ChatMessageColorType.Green); + session.SendChatMessage($"TimeSpace lost: {target.PlayerEntity.LifetimeStats.TotalTimespacesLost}", ChatMessageColorType.Green); + session.SendChatMessage($"Deaths by monster: {target.PlayerEntity.LifetimeStats.TotalDeathsByMonster}", ChatMessageColorType.Green); + session.SendChatMessage($"Deaths by player: {target.PlayerEntity.LifetimeStats.TotalDeathsByPlayer}", ChatMessageColorType.Green); + session.SendChatMessage($"Instant Battle won: {target.PlayerEntity.LifetimeStats.TotalInstantBattleWon}", ChatMessageColorType.Green); + session.SendChatMessage($"Gold earned in bazaar items: {target.PlayerEntity.LifetimeStats.TotalGoldEarnedInBazaarItems}", ChatMessageColorType.Green); + session.SendChatMessage($"Gold spent in bazaar fees: {target.PlayerEntity.LifetimeStats.TotalGoldSpentInBazaarFees}", ChatMessageColorType.Green); + session.SendChatMessage($"Gold spent in bazaar items: {target.PlayerEntity.LifetimeStats.TotalGoldSpentInBazaarItems}", ChatMessageColorType.Green); + session.SendChatMessage($"Gold spent in npc shop: {target.PlayerEntity.LifetimeStats.TotalGoldSpentInNpcShop}", ChatMessageColorType.Green); + + return new SaltyCommandResult(true); + } + + [Command("seeinv")] + [Description("Look target equipped inventory")] + public async Task SeeInv(IClientSession target, byte equipmentType) + { + IClientSession session = Context.Player; + + if (target == null) + { + return new SaltyCommandResult(false); + } + + if (!Enum.TryParse(equipmentType.ToString(), out EquipmentType eqType)) + { + return new SaltyCommandResult(false, "Wrong eqType slot."); + } + + InventoryItem inventory = target.PlayerEntity.GetInventoryItemFromEquipmentSlot(eqType); + if (inventory == null) + { + return new SaltyCommandResult(false, "Target isn't wearing any item on this slot."); + } + + if (inventory.ItemInstance.GameItem.EquipmentSlot == EquipmentType.Sp) + { + if (inventory.ItemInstance.GameItem.IsPartnerSpecialist) + { + session.SendPartnerSpecialistInfo(inventory.ItemInstance); + } + else + { + session.SendSpecialistCardInfo(inventory.ItemInstance, _characterAlgorithm); + } + + return new SaltyCommandResult(true); + } + + session.SendEInfoPacket(inventory.ItemInstance, _itemManager, _characterAlgorithm); + return new SaltyCommandResult(true); + } + + [Command("seeinv")] + [Description("Look others inventory")] + public async Task SeeInv(IClientSession target) + { + IClientSession session = Context.Player; + + if (target == null) + { + return new SaltyCommandResult(false); + } + + session.SendPacket(target.GenerateExtsPacket()); + session.SendTargetEq(target.PlayerEntity); + session.SendPacket(target.GenerateGoldPacket()); + session.SendMsg("Remember, it's not your inventory!", MsgMessageType.MiddleYellow); + + return new SaltyCommandResult(true); + } + + [Command("seeinv")] + [Description("Return to your inventory")] + public async Task SeeInv() + { + IClientSession session = Context.Player; + + session.ShowInventoryExtensions(); + session.SendStartStartupInventory(); + session.RefreshGold(); + + return new SaltyCommandResult(true); + } + + [Command("size")] + public async Task Size(byte desiredSize) + { + Context.Player.PlayerEntity.ChangeSize(desiredSize); + return new SaltyCommandResult(true); + } + + [Command("shout")] + [Description("Shout message to all players")] + public async Task ShoutAsync( + [Description("Message")] [Remainder] string message) + { + await Context.Player.EmitEventAsync(new ChatShoutAdminEvent + { + Message = message + }); + return new SaltyCommandResult(true, ""); + } + + [Command("gmmode")] + [Description("Turn on/off message [GM_ONLY]")] + public async Task GmMode() + { + Context.Player.GmMode = !Context.Player.GmMode; + Context.Player.SendChatMessage($"GM_MODE: {(Context.Player.GmMode ? "ON" : "OFF")}", ChatMessageColorType.Yellow); + return new SaltyCommandResult(true, ""); + } + + [Command("invisible", "visible")] + [Description("You become visible / invisible")] + public async Task Invisible() + { + IClientSession session = Context.Player; + + session.PlayerEntity.CheatComponent.IsInvisible = !session.PlayerEntity.CheatComponent.IsInvisible; + session.SendEqPacket(); + session.SendPacket(session.GenerateInvisible()); + + if (session.PlayerEntity.CheatComponent.IsInvisible) + { + session.BroadcastOut(new ExceptSessionBroadcast(session)); + foreach (IMateEntity mate in session.PlayerEntity.MateComponent.TeamMembers()) + { + session.Broadcast(mate.GenerateOut()); + } + } + else + { + foreach (IClientSession receiverSession in session.CurrentMapInstance.Sessions) + { + bool isAnonymous = session.CurrentMapInstance.HasMapFlag(MapFlags.ACT_4) + && receiverSession.PlayerEntity.Faction != session.PlayerEntity.Faction && !receiverSession.IsGameMaster(); + + receiverSession.SendTargetInPacket(session, _reputationConfiguration, _rankingManager.TopReputation, isAnonymous, true); + receiverSession.SendTargetGidxPacket(session, session.PlayerEntity.Family, _gameLanguage); + + receiverSession.SendTargetTitInfoPacket(session); + receiverSession.SendTargetConstBuffEffects(session.PlayerEntity); + + if (session.PlayerEntity.IsOnVehicle) + { + continue; + } + + foreach (IMateEntity mate in session.PlayerEntity.MateComponent.TeamMembers()) + { + mate.TeleportNearCharacter(); + string inPacket = mate.GenerateIn(_gameLanguage, receiverSession.UserLanguage, _spPartnerConfiguration, isAnonymous); + receiverSession.SendPacket(inPacket); + receiverSession.SendTargetConstBuffEffects(mate); + } + } + + session.RefreshParty(_spPartnerConfiguration); + } + + return new SaltyCommandResult(true, $"Invisibility: {session.PlayerEntity.CheatComponent.IsInvisible}"); + } + + [Command("addtitle", "unlockTitle")] + [Description("Add the given title to your character")] + public async Task AddTitle( + [Description("Title Vnum you want")] short titleVnum) + { + IClientSession session = Context.Player; + if (session.PlayerEntity.Titles.Any(s => s.ItemVnum == titleVnum)) + { + return new SaltyCommandResult(false, "Title already unlocked."); + } + + session.PlayerEntity.Titles.Add(new CharacterTitleDto { ItemVnum = titleVnum, TitleId = _itemManager.GetTitleId(titleVnum) }); + session.SendTitlePacket(); + return new SaltyCommandResult(true, "Title has been unlocked."); + } + + [Command("info")] + [Description("Information about player")] + public async Task InfoAsync() => await InfoAsync(Context.Player.PlayerEntity.Name); + + [Command("vfx")] + [Description("VFX for GameMaster and Blowa - type GM or Blowa")] + public async Task VfxAsync(string vfx) + { + if (string.IsNullOrEmpty(vfx)) + { + return new SaltyCommandResult(false, "VFX is empty"); + } + + IClientSession session = Context.Player; + + switch (vfx.ToLower()) + { + case "gamemaster": + case "gm": + + await session.PlayerEntity.AddBuffAsync(_buffFactory.CreateBuff(20001, session.PlayerEntity, TimeSpan.FromHours(1))); + + break; + case "blowa": + + if (session.PlayerEntity.Name != "Blowa") + { + return new SaltyCommandResult(false, "You're not Blowa wrr..."); + } + + await session.PlayerEntity.AddBuffAsync(_buffFactory.CreateBuff(20000, session.PlayerEntity, TimeSpan.FromHours(1))); + + break; + } + + return new SaltyCommandResult(true); + } + + [Command("info")] + [Description("Information about player")] + public async Task InfoAsync( + [Description("Player's nickname")] string targetName) + { + IClientSession session = Context.Player; + + IClientSession target = _sessionManager.GetSessionByCharacterName(targetName); + if (target != null) + { + session.SendChatMessage($"      [Session information: {target.PlayerEntity.Name}]", ChatMessageColorType.Red); + session.SendChatMessage($"SessionID: {target.SessionId}", ChatMessageColorType.LightPurple); + session.SendChatMessage($"AccountID: {target.Account.Id}", ChatMessageColorType.LightPurple); + session.SendChatMessage($"Master AccountID: {target.Account.MasterAccountId.ToString()}", ChatMessageColorType.LightPurple); + session.SendChatMessage($"Account Name: {target.Account.Name}", ChatMessageColorType.LightPurple); + session.SendChatMessage($"IP Address: {target.IpAddress}", ChatMessageColorType.LightPurple); + session.SendChatMessage($"Client Version: {target.ClientVersion}", ChatMessageColorType.LightPurple); + session.SendChatMessage($"HardwareID: {target.HardwareId}", ChatMessageColorType.LightPurple); + session.SendChatMessage($"Channel: {_gameServer.ChannelId}", ChatMessageColorType.LightPurple); + session.SendChatMessage($"Language: {target.UserLanguage}", ChatMessageColorType.LightPurple); + session.SendChatMessage("      [Character information]", ChatMessageColorType.Red); + session.SendChatMessage($"CharacterID: {target.PlayerEntity.Id}", ChatMessageColorType.LightPurple); + session.SendChatMessage($"Name: {target.PlayerEntity.Name}", ChatMessageColorType.LightPurple); + session.SendChatMessage($"Level: {target.PlayerEntity.Level}(+{target.PlayerEntity.HeroLevel}) | XP: {target.PlayerEntity.LevelXp} | HeroXP: {target.PlayerEntity.HeroXp}", + ChatMessageColorType.LightPurple); + session.SendChatMessage($"JobLevel: {target.PlayerEntity.JobLevel} | JobXP: {target.PlayerEntity.JobLevelXp}", ChatMessageColorType.LightPurple); + session.SendChatMessage($"Position: MapID: {target.PlayerEntity.MapId} X: {target.PlayerEntity.PositionX} Y: {target.PlayerEntity.PositionY}", ChatMessageColorType.LightPurple); + session.SendChatMessage($"Speed: {target.PlayerEntity.Speed}", ChatMessageColorType.LightPurple); + session.SendChatMessage($"Gold: {target.PlayerEntity.Gold}", ChatMessageColorType.LightPurple); + session.SendChatMessage($"Faction: {target.PlayerEntity.Faction}", ChatMessageColorType.LightPurple); + session.SendChatMessage($"HP: {target.PlayerEntity.Hp}/{target.PlayerEntity.MaxHp} MP: {target.PlayerEntity.Mp}/{target.PlayerEntity.MaxMp}", ChatMessageColorType.LightPurple); + IFamily family = target.PlayerEntity.Family; + session.SendChatMessage($"Family: {(family != null ? family.Name : "None")}", ChatMessageColorType.LightPurple); + return new SaltyCommandResult(true); + } + + DbServerGetCharacterResponse getCharacter = await _characterService.GetCharacterByName(new DbServerGetCharacterRequestByName + { + CharacterName = targetName + }); + + if (getCharacter?.CharacterDto == null) + { + return new SaltyCommandResult(false, "Character not found in database"); + } + + CharacterDTO character = getCharacter.CharacterDto; + + session.SendChatMessage($"AccountID: {character.AccountId}", ChatMessageColorType.LightPurple); + session.SendChatMessage("      [Character information]", ChatMessageColorType.Red); + session.SendChatMessage($"CharacterID: {character.Id}", ChatMessageColorType.LightPurple); + session.SendChatMessage($"Name: {character.Name}", ChatMessageColorType.LightPurple); + session.SendChatMessage($"Level: {character.Level}(+{character.HeroLevel}) | XP: {character.LevelXp} | HeroXP: {character.HeroXp}", ChatMessageColorType.LightPurple); + session.SendChatMessage($"JobLevel: {character.JobLevel} | JobXP: {character.JobLevelXp}", ChatMessageColorType.LightPurple); + session.SendChatMessage($"Last saved position: MapID: {character.MapId} X: {character.MapX} Y: {character.MapY}", ChatMessageColorType.LightPurple); + session.SendChatMessage($"Gold: {character.Gold}", ChatMessageColorType.LightPurple); + session.SendChatMessage($"Faction: {character.Faction}", ChatMessageColorType.LightPurple); + + if (character.Inventory != null) + { + session.SendChatMessage("      [Equipment information]", ChatMessageColorType.Red); + foreach (CharacterInventoryItemDto eq in character.Inventory) + { + if (eq?.ItemInstance == null) + { + continue; + } + + session.SendChatMessage("-=-=-=-=-=-=-=-", ChatMessageColorType.Yellow); + session.SendChatMessage($"- Slot: {eq.Slot}", ChatMessageColorType.Yellow); + session.SendChatMessage($"- ItemVnum: {eq.ItemInstance.ItemVNum}", ChatMessageColorType.Yellow); + session.SendChatMessage($"- Amount: {eq.ItemInstance.Amount}", ChatMessageColorType.Yellow); + session.SendChatMessage($"- Serial (only eq.): {eq.ItemInstance.SerialTracker.ToString()}", ChatMessageColorType.Yellow); + } + } + + if (character.EquippedStuffs != null) + { + session.SendChatMessage("      [Equipment equipped information]", ChatMessageColorType.Red); + foreach (CharacterInventoryItemDto eq in character.EquippedStuffs) + { + if (eq?.ItemInstance == null) + { + continue; + } + + session.SendChatMessage("-=-=-=-=-=-=-=-", ChatMessageColorType.Yellow); + session.SendChatMessage($"- Slot: {eq.Slot}", ChatMessageColorType.Yellow); + session.SendChatMessage($"- ItemVnum: {eq.ItemInstance.ItemVNum}", ChatMessageColorType.Yellow); + session.SendChatMessage($"- Amount: {eq.ItemInstance.Amount}", ChatMessageColorType.Yellow); + session.SendChatMessage($"- Serial (only eq.): {eq.ItemInstance.SerialTracker.ToString()}", ChatMessageColorType.Yellow); + } + } + + return new SaltyCommandResult(true); + } + + [Command("channel")] + [Description("Get channel id")] + public async Task GetChannelNumber(string characterName) + { + ClusterCharacterInfo tmp = _sessionManager.GetOnlineCharacterByName(characterName); + if (tmp == null) + { + return new SaltyCommandResult(false, $"Could not find {characterName} in online players"); + } + + return new SaltyCommandResult(true, $"[ONLINE] {characterName} is on channel {tmp.ChannelId}"); + } + + [Command("morph")] + [Description("Transform into any monster")] + public async Task MorphAsync( + [Description("Morph VNUM")] ushort morphVnum, + [Description("Upgrade")] byte upgrade, + [Description("Morph design")] byte morphDesign) + { + IClientSession session = Context.Player; + + session.PlayerEntity.IsMorphed = true; + session.PlayerEntity.Morph = morphVnum; + session.PlayerEntity.MorphUpgrade = upgrade; + session.PlayerEntity.MorphUpgrade2 = morphDesign; + session.BroadcastCMode(); + return new SaltyCommandResult(true, ""); + } + + [Command("morph")] + [Description("Disable morph")] + public async Task MorphAsync() + { + IClientSession session = Context.Player; + + await session.EmitEventAsync(new GetDefaultMorphEvent()); + session.PlayerEntity.IsMorphed = false; + session.SendCondPacket(); + session.RefreshLevel(_characterAlgorithm); + session.BroadcastCMode(); + return new SaltyCommandResult(true, ""); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.Essentials/GameMaster/ItemModule.cs b/srcs/_plugins/WingsEmu.Plugins.Essentials/GameMaster/ItemModule.cs new file mode 100644 index 0000000..2fdc74d --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.Essentials/GameMaster/ItemModule.cs @@ -0,0 +1,128 @@ +using System.Threading.Tasks; +using PhoenixLib.Scheduler; +using Qmmands; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsAPI.Game.Extensions.PacketGeneration; +using WingsEmu.Commands.Checks; +using WingsEmu.Commands.Entities; +using WingsEmu.DTOs.Account; +using WingsEmu.Game; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Items; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Portals; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.Essentials.GameMaster; + +[Name("Super Game Master")] +[Description("Module related to items Super Game Master commands.")] +[RequireAuthority(AuthorityType.SuperGameMaster)] +public class ItemModule : SaltyModuleBase +{ + private readonly IGameItemInstanceFactory _gameItemInstanceFactory; + private readonly IGameLanguageService _language; + private readonly IPortalFactory _portalFactory; + private readonly IScheduler _scheduler; + + public ItemModule(IGameLanguageService language, IGameItemInstanceFactory gameItemInstanceFactory, IScheduler scheduler, IPortalFactory portalFactory) + { + _language = language; + _gameItemInstanceFactory = gameItemInstanceFactory; + _scheduler = scheduler; + _portalFactory = portalFactory; + } + + [Command("cportal")] + [Description("Creates a temporary portal to the place you specify.")] + public async Task CreateAPortal( + [Description("The Id of the Destination Map.")] + short mapDestId, + [Description("The value of the coordinate X where the portal will deploy you.")] + short mapDestX, + [Description("The value of the coordinate Y where the portal will deploy you.")] + short mapDestY, + [Description("The Portal Duration In Seconds")] + int timeInSeconds = 0) + { + IPortalEntity portal = _portalFactory.CreatePortal(PortalType.TSNormal, Context.Player.CurrentMapInstance, Context.Player.PlayerEntity.Position, mapDestId, new Position(mapDestX, mapDestY)); + Context.Player.CurrentMapInstance.AddPortalToMap(portal, _scheduler, timeInSeconds, timeInSeconds > 0); + return new SaltyCommandResult(true); + } + + [Command("rportal", "remove-portal")] + [Description("Removes the closest temporary portal.")] + public async Task RemovePortal() + { + IPortalEntity portalToDelete = Context.Player.CurrentMapInstance.GetClosestPortal(Context.Player.PlayerEntity.PositionX, Context.Player.PlayerEntity.PositionY); + + if (portalToDelete == null) + { + return new SaltyCommandResult(false, "There are no temporary portals in your area."); + } + + if (!Context.Player.PlayerEntity.Position.IsInRange(new Position(portalToDelete.PositionX, portalToDelete.PositionY), 3)) + { + return new SaltyCommandResult(false, $"You're not close enough to the temporary portal. (X: {portalToDelete.PositionX}; Y: {portalToDelete.PositionY})"); + } + + Context.Player.CurrentMapInstance.DeletePortal(portalToDelete); + return new SaltyCommandResult(true, "The temporary portal has been removed successfully!"); + } + + [Command("pearl")] + [Description("Creates a peral with the vnum of the mate you desire.")] + public async Task CreateMatePearl( + [Description("The VNum of the mate you want.")] + int item, bool isLimited) + { + IClientSession session = Context.Player; + + GameItemInstance itemInstance = _gameItemInstanceFactory.CreateItem(item, isLimited); + + await session.AddNewItemToInventory(itemInstance); + return new SaltyCommandResult(true); + } + + [Command("item")] + [Description("Create an Item")] + public async Task CreateitemAsync( + [Description("VNUM Item.")] short itemvnum) + { + IClientSession session = Context.Player; + GameItemInstance newItem = _gameItemInstanceFactory.CreateItem(itemvnum); + await session.AddNewItemToInventory(newItem); + return new SaltyCommandResult(true, $"Created item: {_language.GetLanguage(GameDataType.Item, newItem.GameItem.Name, Context.Player.UserLanguage)}"); + } + + [Command("fairy-level", "flevel", "flvl", "fairylevel")] + [Description("Set equipped fairy level.")] + public async Task FairyLevel([Description("Fairy level.")] short fairyLevel) + { + IClientSession session = Context.Player; + if (session.PlayerEntity.Fairy == null || session.PlayerEntity.Fairy.IsEmpty) + { + return new SaltyCommandResult(false, "No fairy equipped."); + } + + session.PlayerEntity.Fairy.ElementRate = fairyLevel; + session.RefreshFairy(); + + return new SaltyCommandResult(true, $"Your fairy level has been set to {fairyLevel}%!"); + } + + [Command("sp")] + [Description("Create a Specialist Card with upgrade.")] + public async Task SpAsync( + [Description("SP VNUM.")] short spvnum, + [Description("Upgrade.")] byte upgrade = 0) + { + IClientSession session = Context.Player; + + GameItemInstance newItem = _gameItemInstanceFactory.CreateSpecialistCard(spvnum, upgrade: upgrade); + await session.AddNewItemToInventory(newItem); + return new SaltyCommandResult(true, $"Specialist Card: {_language.GetLanguage(GameDataType.Item, newItem.GameItem.Name, session.UserLanguage)} created."); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.Essentials/GameMaster/MonsterSummoningModule.cs b/srcs/_plugins/WingsEmu.Plugins.Essentials/GameMaster/MonsterSummoningModule.cs new file mode 100644 index 0000000..560fef8 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.Essentials/GameMaster/MonsterSummoningModule.cs @@ -0,0 +1,145 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using System.Threading.Tasks; +using PhoenixLib.Events; +using Qmmands; +using WingsEmu.Commands.Checks; +using WingsEmu.Commands.Entities; +using WingsEmu.DTOs.Account; +using WingsEmu.Game; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Entities.Event; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Monster.Event; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Plugins.Essentials.GameMaster; + +[Name("MonsterSummoning")] +[Description("Module related to Monster Summoning commands.")] +[RequireAuthority(AuthorityType.CommunityManager)] +public class MonsterSummoningModule : SaltyModuleBase +{ + private readonly IMonsterEntityFactory _entity; + private readonly IAsyncEventPipeline _eventPipeline; + private readonly IGameLanguageService _language; + private readonly INpcMonsterManager _npcMonsterManager; + + public MonsterSummoningModule(INpcMonsterManager npcMonsterManager, IMonsterEntityFactory entity, IGameLanguageService language, IAsyncEventPipeline eventPipeline) + { + _npcMonsterManager = npcMonsterManager; + _entity = entity; + _language = language; + _eventPipeline = eventPipeline; + } + + [Command("mob")] + [Description("Spawn monster with given vnum")] + public async Task SpawnMob( + [Description("Mob vnum")] short vnum, + [Description("Amount")] short amount, + [Description("Can move (false, true)")] + bool canMove = false, bool isHostile = false) + { + IMonsterData monster = _npcMonsterManager.GetNpc(vnum); + + if (monster == null) + { + return new SaltyCommandResult(false, "The monster doesn't exist!"); + } + + for (int i = 0; i < amount; i++) + { + IMonsterEntity monsterEntity = _entity.CreateMonster(monster, Context.Player.CurrentMapInstance, new MonsterEntityBuilder + { + IsWalkingAround = canMove, + IsHostile = isHostile + }); + monsterEntity.ChangePosition(Context.Player.PlayerEntity.Position); + monsterEntity.FirstX = Context.Player.PlayerEntity.Position.X; + monsterEntity.FirstY = Context.Player.PlayerEntity.Position.Y; + await monsterEntity.EmitEventAsync(new MapJoinMonsterEntityEvent(monsterEntity)); + } + + return new SaltyCommandResult(true, $"Spawned {amount}x of {_language.GetLanguage(GameDataType.NpcMonster, monster.Name, Context.Player.UserLanguage)}"); + } + + [Command("mobs")] + [Description("Spawn monster with given vnum in a square")] + public async Task SpawnMobs( + [Description("Mob vnum")] short vnum, + [Description("Range")] byte range) + { + IClientSession session = Context.Player; + IMonsterData monster = _npcMonsterManager.GetNpc(vnum); + + if (monster == null) + { + return new SaltyCommandResult(false, "The monster doesn't exist!"); + } + + var list = new List(); + + for (short y = 0; y < range; y++) + { + for (short x = 0; x < range; x++) + { + list.Add(new ToSummon + { + VNum = vnum, + SpawnCell = new Position((short)(session.PlayerEntity.PositionX + x), (short)(session.PlayerEntity.PositionY + y)), + IsMoving = false, + IsHostile = true + }); + } + } + + await _eventPipeline.ProcessEventAsync(new MonsterSummonEvent(session.CurrentMapInstance, list)); + + return new SaltyCommandResult(true, $"Spawned {list.Count}x of {_language.GetLanguage(GameDataType.NpcMonster, monster.Name, Context.Player.UserLanguage)}"); + } + + + [RequireAuthority(AuthorityType.GameAdmin)] + [Command("fill-mobs", "fill-mob", "fill-monsters", "fill-monster")] + [Description("Spawn monster with given vnum")] + public async Task FillMapWithMonsters( + [Description("Mob vnum")] ushort vnum, + [Description("Amount")] short amount, + [Description("Can move (false, true)")] + bool canMove = false, + [Description("IsAggressive (false, true)")] + bool isAggresive = false) + { + IClientSession session = Context.Player; + IMonsterData monster = _npcMonsterManager.GetNpc(vnum); + + if (monster == null) + { + return new SaltyCommandResult(false, "The monster doesn't exist!"); + } + + var list = new List(); + + for (int i = 0; i < amount; i++) + { + var toSummon = new ToSummon + { + VNum = vnum, + SpawnCell = session.CurrentMapInstance.GetRandomPosition(), + IsMoving = canMove, + IsHostile = isAggresive + }; + list.Add(toSummon); + } + + await _eventPipeline.ProcessEventAsync(new MonsterSummonEvent(session.CurrentMapInstance, list)); + + return new SaltyCommandResult(true, $"Spawned {amount}x of {_language.GetLanguage(GameDataType.NpcMonster, monster.Name, session.UserLanguage)}"); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.Essentials/GameMaster/PunishmentModule.cs b/srcs/_plugins/WingsEmu.Plugins.Essentials/GameMaster/PunishmentModule.cs new file mode 100644 index 0000000..7e874bb --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.Essentials/GameMaster/PunishmentModule.cs @@ -0,0 +1,535 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using PhoenixLib.Logging; +using PhoenixLib.ServiceBus; +using Qmmands; +using WingsAPI.Communication; +using WingsAPI.Communication.DbServer.AccountService; +using WingsAPI.Communication.DbServer.CharacterService; +using WingsAPI.Communication.Punishment; +using WingsAPI.Data.Account; +using WingsAPI.Data.Character; +using WingsEmu.Commands.Checks; +using WingsEmu.Commands.Entities; +using WingsEmu.DTOs.Account; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.Essentials.GameMaster; + +[Name("Punishment")] +[Description("Module related to player punishment system")] +[RequireAuthority(AuthorityType.GameMaster)] +public class PunishmentModule : SaltyModuleBase +{ + private readonly IAccountService _accountService; + private readonly ICharacterService _characterService; + private readonly IMessagePublisher _kickMessage; + private readonly ISessionManager _sessionManager; + + public PunishmentModule(IMessagePublisher kickMessage, ISessionManager sessionManager, ICharacterService characterService, IAccountService accountService) + { + _kickMessage = kickMessage; + _sessionManager = sessionManager; + _characterService = characterService; + _accountService = accountService; + } + + [Command("penaltyinfo")] + [Description("Get player penalty history")] + public async Task PenaltyInfo(IClientSession target) + { + if (!target.Account.Logs.Any()) + { + return new SaltyCommandResult(true, "Player doesn't have any penalties!"); + } + + IClientSession session = Context.Player; + foreach (AccountPenaltyDto penalty in target.Account.Logs) + { + session.SendErrorChatMessage($"------[Id: {penalty.Id}]-------"); + session.SendInformationChatMessage($"Penalty type: {penalty.PenaltyType.ToString()}"); + session.SendInformationChatMessage($"Judge name: {penalty.JudgeName}"); + session.SendInformationChatMessage($"Target name: {penalty.TargetName}"); + session.SendInformationChatMessage($"Start: {penalty.Start}"); + session.SendInformationChatMessage($"Mute remaining time: {penalty.RemainingTime}"); + session.SendInformationChatMessage($"Reason: {penalty.Reason}"); + if (!string.IsNullOrEmpty(penalty.UnlockReason)) + { + session.SendInformationChatMessage($"Unlock reason: {penalty.UnlockReason}"); + } + + session.SendErrorChatMessage("----------------"); + } + + return new SaltyCommandResult(true); + } + + [Command("penaltyinfo")] + [Description("Get penalty history by account id")] + public async Task PenaltyInfo(long accountId) + { + AccountPenaltyGetAllResponse response = null; + try + { + response = await _accountService.GetAccountPenalties(new AccountPenaltyGetRequest + { + AccountId = accountId + }); + } + catch (Exception e) + { + Log.Error("[PUNISHMENT_MODULE][Command: 'penaltyinfo'] Unexpected error: ", e); + } + + if (response?.ResponseType != RpcResponseType.SUCCESS) + { + return new SaltyCommandResult(false, "Couldn't retrieve the account's penalties from AccountService"); + } + + IEnumerable penalties = response.AccountPenaltyDtos.ToList(); + + if (!penalties.Any()) + { + return new SaltyCommandResult(true, $"No history was found in the database by account id: {accountId}."); + } + + IClientSession session = Context.Player; + foreach (AccountPenaltyDto penalty in penalties) + { + session.SendErrorChatMessage($"------[Id: {penalty.Id}]-------"); + session.SendInformationChatMessage($"Penalty type: {penalty.PenaltyType.ToString()}"); + session.SendInformationChatMessage($"Judge name: {penalty.JudgeName}"); + session.SendInformationChatMessage($"Target name: {penalty.TargetName}"); + session.SendInformationChatMessage($"Start: {penalty.Start}"); + session.SendInformationChatMessage($"Mute remaining time: {penalty.RemainingTime}"); + session.SendInformationChatMessage($"Reason: {penalty.Reason}"); + if (!string.IsNullOrEmpty(penalty.UnlockReason)) + { + session.SendInformationChatMessage($"Unlock reason: {penalty.UnlockReason}"); + } + + session.SendErrorChatMessage("----------------"); + } + + return new SaltyCommandResult(true); + } + + [Command("kick-id")] + [Description("Kick player by player id")] + public async Task KickAsync(long playerId) + { + IClientSession session = _sessionManager.GetSessionByCharacterId(playerId); + if (session != null) + { + session.ForceDisconnect(); + return new SaltyCommandResult(true, $"Player [{session.PlayerEntity.Name}] has been kicked."); + } + + if (!_sessionManager.IsOnline(playerId)) + { + return new SaltyCommandResult(false, "Player is offline"); + } + + await _kickMessage.PublishAsync(new PlayerKickMessage + { + PlayerId = playerId + }); + + return new SaltyCommandResult(true, $"Kicking player with ID: {playerId} on different channel..."); + } + + [Command("kick")] + [Description("Kick player by player name")] + public async Task KickAsync(string playerName) + { + IClientSession session = _sessionManager.GetSessionByCharacterName(playerName); + if (session != null) + { + session.ForceDisconnect(); + return new SaltyCommandResult(true, $"Player [{playerName}] has been kicked."); + } + + if (!_sessionManager.IsOnline(playerName)) + { + return new SaltyCommandResult(false, "Player is offline"); + } + + await _kickMessage.PublishAsync(new PlayerKickMessage + { + PlayerName = playerName + }); + + return new SaltyCommandResult(true, $"Kicking player [{playerName}] on different channel..."); + } + + [Command("mute")] + [Description("Mute player (duration in minutes)")] + public async Task Mute(IClientSession target, short minutes, [Remainder] string reason) + { + if (target.PlayerEntity.MuteRemainingTime.HasValue) + { + string timeLeft = target.PlayerEntity.MuteRemainingTime.Value.ToString(@"hh\:mm\:ss"); + return new SaltyCommandResult(false, $"Player is already muted - time left: {timeLeft}"); + } + + DateTime now = DateTime.UtcNow; + var time = TimeSpan.FromMinutes(minutes); + int? seconds = (int?)time.TotalSeconds; + + IClientSession session = Context.Player; + AccountPenaltyDto newPenalty = new() + { + JudgeName = session.PlayerEntity.Name, + TargetName = target.PlayerEntity.Name, + AccountId = target.Account.Id, + Start = now, + RemainingTime = seconds, + PenaltyType = PenaltyType.Muted, + Reason = reason + }; + + target.Account.Logs.Add(newPenalty); + target.PlayerEntity.MuteRemainingTime = TimeSpan.FromSeconds(time.TotalSeconds); + target.PlayerEntity.LastChatMuteMessage = DateTime.MinValue; + target.PlayerEntity.LastMuteTick = now; + return new SaltyCommandResult(true, $"Player [{target.PlayerEntity.Name}] has been muted for [{reason}]."); + } + + [Command("unmute")] + [Description("Unmute player")] + public async Task Unmute(IClientSession target, [Remainder] string reason) + { + TimeSpan? muteTime = target.PlayerEntity.MuteRemainingTime; + if (muteTime == null) + { + return new SaltyCommandResult(false, $"Player [{target.PlayerEntity.Name}] isn't muted."); + } + + target.PlayerEntity.LastChatMuteMessage = null; + target.PlayerEntity.MuteRemainingTime = null; + + AccountPenaltyDto penalty = target.Account.Logs.FirstOrDefault(x => x.PenaltyType == PenaltyType.Muted && x.RemainingTime.HasValue); + if (penalty != null) + { + penalty.UnlockReason = reason; + penalty.RemainingTime = null; + } + + return new SaltyCommandResult(true, $"Player [{target.PlayerEntity.Name}] has been unmuted."); + } + + [Command("ban")] + [Description("Ban player by player id (duration in hours - max 1 week)")] + public async Task Ban(long playerId, short hours, [Remainder] string reason) + { + IClientSession session = Context.Player; + IClientSession target = _sessionManager.GetSessionByCharacterId(playerId); + + // Max 1 week + var time = TimeSpan.FromHours(hours > 168 ? 168 : hours); + DateTime utcNow = DateTime.UtcNow; + + AccountBanDto newBan = new() + { + JudgeName = session.PlayerEntity.Name, + Start = utcNow, + End = utcNow + time, + Reason = reason + }; + + if (target != null) + { + newBan.AccountId = target.Account.Id; + newBan.TargetName = target.PlayerEntity.Name; + AccountBanSaveResponse response = null; + try + { + response = await _accountService.SaveAccountBan(new AccountBanSaveRequest + { + AccountBanDto = newBan + }); + } + catch (Exception e) + { + Log.Error("[PUNISHMENT_MODULE][Command: 'ban'] Unexpected error: ", e); + } + + if (response?.ResponseType != RpcResponseType.SUCCESS) + { + return new SaltyCommandResult(false, "Couldn't save the AccountBan through AccountService"); + } + + target.ForceDisconnect(); + return new SaltyCommandResult(true, $"Player [{target.PlayerEntity.Name}] has been banned for [{reason}] with duration of [{hours}] hours."); + } + + DbServerGetCharacterResponse targetResponse = null; + try + { + targetResponse = await _characterService.GetCharacterById(new DbServerGetCharacterByIdRequest + { + CharacterId = playerId + }); + } + catch (Exception e) + { + Log.Error("[PUNISHMENT_MODULE][Command: 'ban'] Unexpected error: ", e); + } + + if (targetResponse?.RpcResponseType != RpcResponseType.SUCCESS) + { + return new SaltyCommandResult(false, "Couldn't retrieve the CharacterDTO from CharacterService"); + } + + CharacterDTO targetCharacter = targetResponse.CharacterDto; + + await _kickMessage.PublishAsync(new PlayerKickMessage + { + PlayerId = playerId + }); + + newBan.AccountId = targetCharacter.AccountId; + newBan.TargetName = targetCharacter.Name; + AccountBanSaveResponse response2 = null; + try + { + response2 = await _accountService.SaveAccountBan(new AccountBanSaveRequest + { + AccountBanDto = newBan + }); + } + catch (Exception e) + { + Log.Error("[PUNISHMENT_MODULE][Command: 'ban'] Unexpected error: ", e); + } + + return response2?.ResponseType != RpcResponseType.SUCCESS + ? new SaltyCommandResult(false, "Couldn't save the AccountBan through AccountService") + : new SaltyCommandResult(true, $"Banning player with ID: [{playerId}] for [{reason}] with duration of [{hours}] hours."); + } + + [Command("ban")] + [Description("Ban player by player name (duration in hours)")] + public async Task Ban(string playerName, short hours, [Remainder] string reason) + { + IClientSession session = Context.Player; + IClientSession target = _sessionManager.GetSessionByCharacterName(playerName); + + // Max 1 week + var time = TimeSpan.FromHours(hours > 168 ? 168 : hours); + DateTime utcNow = DateTime.UtcNow; + + AccountBanDto newBan = new() + { + JudgeName = session.PlayerEntity.Name, + Start = utcNow, + End = utcNow + time, + Reason = reason + }; + + if (target != null) + { + newBan.AccountId = target.Account.Id; + newBan.TargetName = target.PlayerEntity.Name; + + AccountBanSaveResponse response = null; + try + { + response = await _accountService.SaveAccountBan(new AccountBanSaveRequest + { + AccountBanDto = newBan + }); + } + catch (Exception e) + { + Log.Error("[PUNISHMENT_MODULE][Command: 'ban'] Unexpected error: ", e); + } + + if (response?.ResponseType != RpcResponseType.SUCCESS) + { + return new SaltyCommandResult(false, "Couldn't save the AccountBan through AccountService"); + } + + target.ForceDisconnect(); + return new SaltyCommandResult(true, $"Player [{playerName}] has been banned for [{reason}] with duration of [{hours}] hours."); + } + + DbServerGetCharacterResponse targetResponse = null; + try + { + targetResponse = await _characterService.GetCharacterByName(new DbServerGetCharacterRequestByName + { + CharacterName = playerName + }); + } + catch (Exception e) + { + Log.Error("[PUNISHMENT_MODULE][Command: 'ban'] Unexpected error: ", e); + } + + if (targetResponse?.RpcResponseType != RpcResponseType.SUCCESS) + { + return new SaltyCommandResult(false, "Couldn't retrieve the CharacterDTO from CharacterService"); + } + + CharacterDTO targetCharacter = targetResponse.CharacterDto; + + await _kickMessage.PublishAsync(new PlayerKickMessage + { + PlayerName = playerName + }); + + newBan.AccountId = targetCharacter.AccountId; + newBan.TargetName = targetCharacter.Name; + + AccountBanSaveResponse response2 = null; + try + { + response2 = await _accountService.SaveAccountBan(new AccountBanSaveRequest + { + AccountBanDto = newBan + }); + } + catch (Exception e) + { + Log.Error("[PUNISHMENT_MODULE][Command: 'ban'] Unexpected error: ", e); + } + + return response2?.ResponseType != RpcResponseType.SUCCESS + ? new SaltyCommandResult(false, "Couldn't save the AccountBan through AccountService") + : new SaltyCommandResult(true, $"Banning [{playerName}] for [{reason}] with duration of [{hours}] hours."); + } + + [Command("unban")] + [Description("Unban player by player id")] + public async Task Unban(long playerId, [Remainder] string reason) + { + DbServerGetCharacterResponse targetResponse = null; + try + { + targetResponse = await _characterService.GetCharacterById(new DbServerGetCharacterByIdRequest + { + CharacterId = playerId + }); + } + catch (Exception e) + { + Log.Error("[PUNISHMENT_MODULE][Command: 'unban'] Unexpected error: ", e); + } + + if (targetResponse?.RpcResponseType != RpcResponseType.SUCCESS) + { + return new SaltyCommandResult(false, "Couldn't retrieve the CharacterDTO from CharacterService"); + } + + CharacterDTO target = targetResponse.CharacterDto; + + AccountBanGetResponse response = null; + try + { + response = await _accountService.GetAccountBan(new AccountBanGetRequest + { + AccountId = target.AccountId + }); + } + catch (Exception e) + { + Log.Error("[PUNISHMENT_MODULE][Command: 'unban'] Unexpected error: ", e); + } + + if (response?.ResponseType != RpcResponseType.SUCCESS) + { + return new SaltyCommandResult(false, "Couldn't get the AccountBan through AccountService"); + } + + AccountBanDto ban = response.AccountBanDto; + + ban.End = DateTime.UtcNow; + ban.UnlockReason = reason; + + AccountBanSaveResponse saveResponse = null; + try + { + saveResponse = await _accountService.SaveAccountBan(new AccountBanSaveRequest + { + AccountBanDto = ban + }); + } + catch (Exception e) + { + Log.Error("[PUNISHMENT_MODULE][Command: 'unban'] Unexpected error: ", e); + } + + return saveResponse?.ResponseType != RpcResponseType.SUCCESS + ? new SaltyCommandResult(false, "Couldn't save the AccountBan through AccountService") + : new SaltyCommandResult(true, $"Player [{target.Name}] has been unbanned with the reason [{reason}]"); + } + + [Command("unban")] + [Description("Unban player by player name")] + public async Task Unban(string playerName, [Remainder] string reason) + { + DbServerGetCharacterResponse targetResponse = null; + try + { + targetResponse = await _characterService.GetCharacterByName(new DbServerGetCharacterRequestByName + { + CharacterName = playerName + }); + } + catch (Exception e) + { + Log.Error("[PUNISHMENT_MODULE][Command: 'unban'] Unexpected error: ", e); + } + + if (targetResponse?.RpcResponseType != RpcResponseType.SUCCESS) + { + return new SaltyCommandResult(false, "Couldn't retrieve the CharacterDTO from CharacterService"); + } + + CharacterDTO target = targetResponse.CharacterDto; + + AccountBanGetResponse response = null; + try + { + response = await _accountService.GetAccountBan(new AccountBanGetRequest + { + AccountId = target.AccountId + }); + } + catch (Exception e) + { + Log.Error("[PUNISHMENT_MODULE][Command: 'unban'] Unexpected error: ", e); + } + + if (response?.ResponseType != RpcResponseType.SUCCESS) + { + return new SaltyCommandResult(false, "Couldn't get the AccountBan through AccountService"); + } + + AccountBanDto ban = response.AccountBanDto; + ban.End = DateTime.UtcNow; + ban.UnlockReason = reason; + AccountBanSaveResponse saveResponse = null; + try + { + saveResponse = await _accountService.SaveAccountBan(new AccountBanSaveRequest + { + AccountBanDto = ban + }); + } + catch (Exception e) + { + Log.Error("[PUNISHMENT_MODULE][Command: 'ban'] Unexpected error: ", e); + } + + return saveResponse?.ResponseType != RpcResponseType.SUCCESS + ? new SaltyCommandResult(false, "Couldn't save the AccountBan through AccountService") + : new SaltyCommandResult(true, $"Player [{playerName}] has been unbanned with the reason [{reason}]"); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.Essentials/GameMaster/SearchDataModule.cs b/srcs/_plugins/WingsEmu.Plugins.Essentials/GameMaster/SearchDataModule.cs new file mode 100644 index 0000000..d3f08ad --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.Essentials/GameMaster/SearchDataModule.cs @@ -0,0 +1,151 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Qmmands; +using WingsEmu.Commands.Checks; +using WingsEmu.Commands.Entities; +using WingsEmu.DTOs.Account; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Quests; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.Essentials.Administrator; + +[Name("Administrator")] +[Description("Module related to Administrator commands.")] +[RequireAuthority(AuthorityType.GameMaster)] +public class SearchDataModule : SaltyModuleBase +{ + private readonly ICardsManager _cardManager; + private readonly IItemsManager _itemManager; + private readonly IGameLanguageService _language; + private readonly INpcMonsterManager _npcMonsterManager; + private readonly IQuestManager _questManager; + private readonly ISkillsManager _skillManager; + + public SearchDataModule(INpcMonsterManager npcMonsterManager, IGameLanguageService language, IItemsManager itemManager, ISkillsManager skillManager, ICardsManager cardManager, + IQuestManager questManager) + { + _npcMonsterManager = npcMonsterManager; + _language = language; + _itemManager = itemManager; + _skillManager = skillManager; + _cardManager = cardManager; + _questManager = questManager; + } + + private void GetMatchingNames(GameDataType dataType, IReadOnlyDictionary dataNames, string name) + { + List> idName = new(); + switch (dataType) + { + case GameDataType.Item: + idName = dataNames + .Where(s => s.Value.Contains(name, StringComparison.InvariantCultureIgnoreCase)) + .SelectMany(s => _itemManager.GetItem(s.Key)) + .Select(s => new KeyValuePair(s.Id, dataNames[s.Name])) + .OrderBy(s => s.Key) + .ToList(); + break; + case GameDataType.NpcMonster: + idName = dataNames + .Where(s => s.Value.Contains(name, StringComparison.InvariantCultureIgnoreCase)) + .SelectMany(s => _npcMonsterManager.GetNpc(s.Key)) + .Select(s => new KeyValuePair(s.MonsterVNum, dataNames[s.Name])) + .OrderBy(s => s.Key) + .ToList(); + break; + case GameDataType.Skill: + idName = dataNames + .Where(s => s.Value.Contains(name, StringComparison.InvariantCultureIgnoreCase)) + .SelectMany(s => _skillManager.GetSkill(s.Key)) + .Select(s => new KeyValuePair(s.Id, dataNames[s.Name])) + .OrderBy(s => s.Key) + .ToList(); + break; + case GameDataType.Card: + idName = dataNames + .Where(s => s.Value.Contains(name, StringComparison.InvariantCultureIgnoreCase)) + .SelectMany(s => _cardManager.GetCardByName(s.Key)) + .Select(s => new KeyValuePair(s.Id, dataNames[s.Name])) + .OrderBy(s => s.Key) + .ToList(); + break; + case GameDataType.QuestName: + idName = dataNames + .Where(s => s.Value.Contains(name, StringComparison.InvariantCultureIgnoreCase)) + .SelectMany(s => _questManager.GetQuestByName(s.Key)) + .Select(s => new KeyValuePair(s.Id, dataNames[s.Name])) + .OrderBy(s => s.Key) + .ToList(); + break; + } + + foreach ((int key, string value) in idName) + { + Context.Player.SendChatMessage($"[{key}] {value}", ChatMessageColorType.Green); + } + } + + [Command("searchitem", "findItem")] + public async Task SearchItem([Remainder] string search) + { + Dictionary languagesAsync = _language.GetDataTranslations(GameDataType.Item, Context.Player.UserLanguage); + + Context.Player.SendChatMessage($"[SEARCH_ITEM] : {search}", ChatMessageColorType.Green); + Context.Player.SendChatMessage("===============================", ChatMessageColorType.Green); + GetMatchingNames(GameDataType.Item, languagesAsync, search); + Context.Player.SendChatMessage("===============================", ChatMessageColorType.Green); + return new SaltyCommandResult(true); + } + + [Command("searchmonster", "findMonster", "searchmob")] + public async Task SearchMonster([Remainder] string search) + { + Dictionary languagesAsync = _language.GetDataTranslations(GameDataType.NpcMonster, Context.Player.UserLanguage); + Context.Player.SendChatMessage($"[SEARCH_MONSTER] : {search}", ChatMessageColorType.Green); + Context.Player.SendChatMessage("===============================", ChatMessageColorType.Green); + GetMatchingNames(GameDataType.NpcMonster, languagesAsync, search); + Context.Player.SendChatMessage("===============================", ChatMessageColorType.Green); + return new SaltyCommandResult(true); + } + + + [Command("searchSkill", "findSkill")] + public async Task SearchSkill([Remainder] string search) + { + Dictionary tmp = _language.GetDataTranslations(GameDataType.Skill, Context.Player.UserLanguage); + Context.Player.SendChatMessage($"[SEARCH_SKILL] : {search}", ChatMessageColorType.Green); + Context.Player.SendChatMessage("===============================", ChatMessageColorType.Green); + + GetMatchingNames(GameDataType.Skill, tmp, search); + Context.Player.SendChatMessage("===============================", ChatMessageColorType.Green); + return new SaltyCommandResult(true); + } + + [Command("searchBuff", "findBuff")] + public async Task SearchBuff([Remainder] string search) + { + Dictionary tmp = _language.GetDataTranslations(GameDataType.Card, Context.Player.UserLanguage); + Context.Player.SendChatMessage($"[SEARCH_BUFF] : {search}", ChatMessageColorType.Green); + Context.Player.SendChatMessage("===============================", ChatMessageColorType.Green); + + GetMatchingNames(GameDataType.Card, tmp, search); + Context.Player.SendChatMessage("===============================", ChatMessageColorType.Green); + return new SaltyCommandResult(true); + } + + [Command("searchQuest", "findQuests")] + public async Task SearchQuests([Remainder] string search) + { + Dictionary tmp = _language.GetDataTranslations(GameDataType.QuestName, Context.Player.UserLanguage); + Context.Player.SendChatMessage($"[SEARCH_QUESTS] : {search}", ChatMessageColorType.Green); + Context.Player.SendChatMessage("===============================", ChatMessageColorType.Green); + GetMatchingNames(GameDataType.QuestName, tmp, search); + Context.Player.SendChatMessage("===============================", ChatMessageColorType.Green); + return new SaltyCommandResult(true); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.Essentials/GameMaster/SkillsModule.cs b/srcs/_plugins/WingsEmu.Plugins.Essentials/GameMaster/SkillsModule.cs new file mode 100644 index 0000000..add7a40 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.Essentials/GameMaster/SkillsModule.cs @@ -0,0 +1,94 @@ +using System; +using System.Threading.Tasks; +using PhoenixLib.Events; +using Qmmands; +using WingsEmu.Commands.Checks; +using WingsEmu.Commands.Entities; +using WingsEmu.DTOs.Account; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Extensions.Mates; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Skills; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.Essentials.GameMaster; + +[Name("Super Game Master")] +[Description("Module related to skills Super Game Master commands.")] +[RequireAuthority(AuthorityType.SuperGameMaster)] +public class SkillsModule : SaltyModuleBase +{ + private readonly IAsyncEventPipeline _eventPipeline; + private readonly ISkillsManager _skillsManager; + + public SkillsModule(IAsyncEventPipeline eventPipeline, ISkillsManager skillsManager) + { + _eventPipeline = eventPipeline; + _skillsManager = skillsManager; + } + + [Command("spcd")] + [Description("Remove SP cooldown.")] + public async Task SpcdAsync() + { + IClientSession session = Context.Player; + + session.PlayerEntity.SpCooldownEnd = DateTime.UtcNow; + session.ResetSpCooldownUi(); + return new SaltyCommandResult(true, "Specialist Cooldown transform has been removed."); + } + + [Command("kill")] + [Description("Kill yourself.")] + public async Task Kill() => AnonymousKill(Context.Player.PlayerEntity); + + private SaltyCommandResult AnonymousKill(IBattleEntity suicidalEntity) + { + if (suicidalEntity == default) + { + return new SaltyCommandResult(false, "The target provided is not valid"); + } + + var algorithmResult = new DamageAlgorithmResult(int.MaxValue, HitType.Critical, true, false); + _eventPipeline.ProcessEventAsync(new ApplyHitEvent(suicidalEntity, algorithmResult, new HitInformation(suicidalEntity, _skillsManager.GetSkill(299).GetInfo()))); + + return new SaltyCommandResult(true); + } + + [Command("skillreset", "resetcd", "resetCooldown", "sr")] + [Description("Resets all the skills cooldown")] + public async Task ResetCooldown() + { + Context.Player.PlayerEntity.ClearSkillCooldowns(); + + foreach (IBattleEntitySkill skill in Context.Player.PlayerEntity.Skills) + { + skill.LastUse = DateTime.MinValue; + Context.Player.SendSkillCooldownReset(skill.Skill.CastId); + } + + return new SaltyCommandResult(true, "All skills cooldown have been reset."); + } + + [Command("partnercd")] + [Description("Remove Partner SP cooldown.")] + public async Task PartnerSpcdAsync() + { + IClientSession session = Context.Player; + IMateEntity mateEntity = session.PlayerEntity.MateComponent.GetMate(m => m.IsTeamMember && m.MateType == MateType.Partner); + + if (mateEntity == null) + { + return new SaltyCommandResult(false); + } + + mateEntity.SpCooldownEnd = null; + session.SendMateSpCooldown(mateEntity, 0); + return new SaltyCommandResult(true, "Partner Specialist Cooldown has been removed."); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.Essentials/GameMaster/SpecialistModule.cs b/srcs/_plugins/WingsEmu.Plugins.Essentials/GameMaster/SpecialistModule.cs new file mode 100644 index 0000000..7612c63 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.Essentials/GameMaster/SpecialistModule.cs @@ -0,0 +1,52 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Threading.Tasks; +using Qmmands; +using WingsEmu.Commands.Checks; +using WingsEmu.Commands.Entities; +using WingsEmu.DTOs.Account; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Items; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Plugins.Essentials.GameMaster; + +[Name("SpecialistModule")] +[Description("Module related to specialist commands.")] +[RequireAuthority(AuthorityType.GameMaster)] +[Group("sp", "specialist")] +public class SpecialistModule : SaltyModuleBase +{ + [Command("perf", "perfection")] + [RequireAuthority(AuthorityType.GameAdmin)] + public async Task PerfectSpecialist(IClientSession session, byte atk, byte def, byte elem, byte hpmp, byte fireRes, byte waterRes, byte lightRes, byte darkRes) + { + IPlayerEntity targetEntity = session.PlayerEntity; + + if (targetEntity.Specialist == null) + { + return new SaltyCommandResult(false, "SP not equipped"); + } + + GameItemInstance specialist = targetEntity.Specialist; + + specialist.SpDamage = atk; + specialist.SpDefence = def; + specialist.SpElement = elem; + specialist.SpHP = hpmp; + specialist.SpFire = fireRes; + specialist.SpWater = waterRes; + specialist.SpLight = lightRes; + specialist.SpDark = darkRes; + + targetEntity.SpecialistComponent.RefreshSlStats(); + + return new SaltyCommandResult(true, "SP perfection updated"); + } + + [Command("perf", "perfection")] + public async Task PerfectSpecialist(byte atk, byte def, byte elem, byte hpmp, byte fireRes, byte waterRes, byte lightRes, byte darkRes) => + await PerfectSpecialist(Context.Player, atk, def, elem, hpmp, fireRes, waterRes, lightRes, darkRes); +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.Essentials/God/GodSetRankModule.cs b/srcs/_plugins/WingsEmu.Plugins.Essentials/God/GodSetRankModule.cs new file mode 100644 index 0000000..435edc3 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.Essentials/God/GodSetRankModule.cs @@ -0,0 +1,103 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Threading.Tasks; +using Qmmands; +using WingsAPI.Communication; +using WingsAPI.Communication.DbServer.CharacterService; +using WingsAPI.Data.Character; +using WingsEmu.Commands.Checks; +using WingsEmu.Commands.Entities; +using WingsEmu.DTOs.Account; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Plugins.Essentials.God; + +[Name("Administrator")] +[Description("Module related to God commands.")] +[RequireAuthority(AuthorityType.Root)] +public class GodSetRankModule : SaltyModuleBase +{ + private readonly ICharacterService _characterService; + private readonly IPlayerEntityFactory _playerEntityFactory; + + public GodSetRankModule(ICharacterService characterService, IPlayerEntityFactory playerEntityFactory) + { + _characterService = characterService; + _playerEntityFactory = playerEntityFactory; + } + + + [Command("setrank")] + [Description("Sets the rank of the given player")] + public async Task SetRank(IClientSession character, int authority) + { + var targetAuthority = (AuthorityType)authority; + if (targetAuthority > Context.Player.PlayerEntity.Authority) + { + return new SaltyCommandResult(false, "You can't set someone's rights to higher than your own rights"); + } + + character.Account.Authority = targetAuthority; + + Context.Player.SendInformationChatMessage($"{character.PlayerEntity.Name}'s new rank is {character.Account.Authority.ToString()}"); + character.ChangeMap(character.CurrentMapInstance.Id); + return new SaltyCommandResult(true); + } + + [Command("ranks")] + [Description("Gives the list of available ranks")] + public async Task SetRank() + { + Context.Player.SendInformationChatMessage("---- RANKS ----"); + foreach (object? rank in Enum.GetValues(typeof(AuthorityType))) + { + Context.Player.SendInformationChatMessage($"{rank}: {(int)rank}"); + } + + return new SaltyCommandResult(true); + } + + [Command("force-cache")] + [Description("Gives the list of available ranks")] + public async Task ForceRemoveCharacterFromCache([Description("characterName")] string characterName) + { + DbServerGetCharacterResponse response = await _characterService.ForceRemoveCharacterFromCache(new DbServerGetCharacterRequestByName + { + CharacterName = characterName + }); + string successMessage = $"[CACHE_CLEAN] {characterName} has been removed from cache"; + string errorMessage = $"[CACHE_CLEAN] {characterName} couldn't be found"; + bool isSuccess = response.RpcResponseType == RpcResponseType.SUCCESS; + return new SaltyCommandResult(isSuccess, isSuccess ? successMessage : errorMessage); + } + + [Command("change-name")] + [Description("Changes the name of the target to new name")] + public async Task ForceRemoveCharacterFromCache([Description("targetSession")] IClientSession target, [Description("New expected name")] string newName) + { + CharacterDTO newCharacter = _playerEntityFactory.CreateCharacterDto(target.PlayerEntity); + newCharacter.Name = newName; + + // force flush to db method + DbServerSaveCharacterResponse response = await _characterService.CreateCharacter(new DbServerSaveCharacterRequest + { + Character = newCharacter, + IgnoreSlotCheck = true + }); + + if (response.RpcResponseType != RpcResponseType.SUCCESS) + { + return new SaltyCommandResult(false, $"[CHANGE_NAME] {newName} already existed"); + } + + string oldName = target.PlayerEntity.Name; + target.PlayerEntity.Name = newName; + target.ChangeMap(target.CurrentMapInstance, target.PlayerEntity.PositionX, target.PlayerEntity.PositionY); + return new SaltyCommandResult(true, $"[CHANGE_NAME] {oldName} has changed to {newName}"); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.Essentials/Help/HelpModule.cs b/srcs/_plugins/WingsEmu.Plugins.Essentials/Help/HelpModule.cs new file mode 100644 index 0000000..c9c95a2 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.Essentials/Help/HelpModule.cs @@ -0,0 +1,136 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Qmmands; +using WingsEmu.Commands.Entities; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.Essentials.Help; + +[Name("help")] //todo: replace skip/take by proper 48 char limit paginator +[Description("Module related to help.")] +public class HelpModule : SaltyModuleBase +{ + private readonly IGameLanguageService _gamelanguage; + + public HelpModule(IGameLanguageService gamelanguage) => _gamelanguage = gamelanguage; + + [Command("help")] + public async Task HelpAsync() + { + var modules = Context.CommandService.GetAllModules().Where(x => x.Aliases.Count > 0 && x.Commands.Count > 0 && x.Parent == null).ToList(); + IEnumerable modulelessCommands = Context.CommandService.GetAllModules().Where(x => x.Aliases.Count == 0).SelectMany(x => x.Commands); + + foreach (Module module in modules.ToList()) + { + IResult result = await module.RunChecksAsync(Context); + if (!result.IsSuccessful) + { + modules.Remove(module); + } + } + + var commands = new List(); + foreach (Command command in modulelessCommands.ToList().Where(command => commands.All(x => x.FullAliases[0] != command.FullAliases[0])).OrderBy(s => s.Name[0])) + { + IResult result = await command.RunChecksAsync(Context); + if (result.IsSuccessful) + { + commands.Add(command); + } + } + + commands = commands.GroupBy(s => s.Name).Select(s => s.First()).ToList(); + Context.Player.SendChatMessage(_gamelanguage.GetLanguage(GameDialogKey.COMMANDS_CHATMESSAGE_HELP, Context.Player.UserLanguage), ChatMessageColorType.Red); + Context.Player.SendChatMessage(_gamelanguage.GetLanguage(GameDialogKey.COMMANDS_CHATMESSAGE_AVAILABLE, Context.Player.UserLanguage), ChatMessageColorType.Red); + for (int i = 0; i < commands.Count / 6 + 1; i++) + { + Context.Player.SendChatMessage(" -> " + string.Join(", ", commands.Skip(i * 6).Take(6).Select(x => x.Name)), ChatMessageColorType.Green); + } + } + + [Command("help")] + public async Task HelpAsync([Remainder] string command) + { + if (string.IsNullOrWhiteSpace(command)) + { + await HelpAsync(); + return; + } + + var cmds = Context.CommandService.FindCommands(command).ToList(); + + foreach (CommandMatch cmd in cmds.ToList()) + { + IResult result = await cmd.Command.RunChecksAsync(Context); + if (!result.IsSuccessful) + { + cmds.Remove(cmd); + } + } + + if (cmds.Count == 0) + { + Module module = Context.CommandService.TopLevelModules.FirstOrDefault(x => x.Name.Equals(command, StringComparison.OrdinalIgnoreCase)); + //Module module = Context.CommandService.FindModules(command).FirstOrDefault()?.Module; + + IResult passCheck = await module?.RunChecksAsync(Context); + + if (module is null || !passCheck.IsSuccessful) + { + var cmdArgs = command.Split(' ').ToList(); + cmdArgs.RemoveAt(cmdArgs.Count - 1); + + await HelpAsync(string.Join(" ", cmdArgs)); + return; + } + + Context.Player.SendChatMessage($"Help: ({command})", ChatMessageColorType.Red); + if (module.Submodules.Count > 0) + { + Context.Player.SendChatMessage("Submodules:", ChatMessageColorType.Red); + for (int i = 0; i < module.Submodules.Count / 6 + 1; i++) + { + Context.Player.SendChatMessage(" -> " + string.Join(", ", module.Submodules.Skip(i * 6).Take(6).Select(x => $"{x.Aliases[0]}")), + ChatMessageColorType.Green); + } + } + + if (module.Commands.Count > 0) + { + Context.Player.SendChatMessage("Commands:", ChatMessageColorType.Red); + for (int i = 0; i < module.Commands.Count / 6 + 1; i++) + { + Context.Player.SendChatMessage(" -> " + string.Join(", ", module.Commands.Skip(i * 6).Take(6).Select(x => $"{x.Aliases[0]}")), ChatMessageColorType.Green); + } + } + } + + Context.Player.SendChatMessage(_gamelanguage.GetLanguage(GameDialogKey.COMMANDS_CHATMESSAGE_USAGES, Context.Player.UserLanguage), ChatMessageColorType.Red); + foreach (CommandMatch cmd in cmds) + { + Context.Player.SendChatMessage( + $"${cmd.Command.Name} {string.Join(" ", cmd.Command.Parameters.Select(x => x.IsOptional ? $"<{x.Name}>" : $"[{x.Name}]"))}".ToLowerInvariant(), ChatMessageColorType.Green); + foreach (Parameter param in cmd.Command.Parameters) + { + string str = ""; + + str = param.IsOptional ? $"<{param.Name}>:" : $"[{param.Name}]:"; + + str += $" {param.Description ?? _gamelanguage.GetLanguage(GameDialogKey.COMMANDS_CHATMESSAGE_UNDOCUMENTED, Context.Player.UserLanguage)}"; + Context.Player.SendChatMessage(str, ChatMessageColorType.Green); + } + + Context.Player.SendChatMessage(cmd.Command.Description ?? _gamelanguage.GetLanguage(GameDialogKey.COMMANDS_CHATMESSAGE_UNDOCUMENTED, Context.Player.UserLanguage), + ChatMessageColorType.Red); + Context.Player.SendChatMessage(" ", ChatMessageColorType.Red); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.Essentials/NPC/BuffPack.cs b/srcs/_plugins/WingsEmu.Plugins.Essentials/NPC/BuffPack.cs new file mode 100644 index 0000000..e718a08 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.Essentials/NPC/BuffPack.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using YamlDotNet.Serialization; + +namespace WingsEmu.Plugins.Essentials.NPC; + +public class BuffPack +{ + [YamlMember(Alias = "name")] + public string Name { get; set; } + + [YamlMember(Alias = "price")] + public int Price { get; set; } + + [YamlMember(Alias = "minimum_level")] + public int MinimumLevel { get; set; } + + [YamlMember(Alias = "maximum_level")] + public int MaximumLevel { get; set; } + + [YamlMember(Alias = "buffs")] + public List Buffs { get; set; } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.Essentials/NPC/BuffPackConfiguration.cs b/srcs/_plugins/WingsEmu.Plugins.Essentials/NPC/BuffPackConfiguration.cs new file mode 100644 index 0000000..bb5003d --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.Essentials/NPC/BuffPackConfiguration.cs @@ -0,0 +1,7 @@ +using System.Collections.Generic; + +namespace WingsEmu.Plugins.Essentials.NPC; + +public class BuffPackConfiguration : List +{ +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.Essentials/NPC/BuffPackElement.cs b/srcs/_plugins/WingsEmu.Plugins.Essentials/NPC/BuffPackElement.cs new file mode 100644 index 0000000..8460822 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.Essentials/NPC/BuffPackElement.cs @@ -0,0 +1,19 @@ +using System; +using YamlDotNet.Serialization; + +namespace WingsEmu.Plugins.Essentials.NPC; + +public class BuffPackElement +{ + [YamlMember(Alias = "card_id")] + public int CardId { get; set; } + + [YamlMember(Alias = "level")] + public int Level { get; set; } + + [YamlMember(Alias = "duration")] + public TimeSpan Duration { get; set; } + + [YamlMember(Alias = "keep_on_death")] + public bool KeepOnDeath { get; set; } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.Essentials/NPC/MateCreationModule.cs b/srcs/_plugins/WingsEmu.Plugins.Essentials/NPC/MateCreationModule.cs new file mode 100644 index 0000000..2266edb --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.Essentials/NPC/MateCreationModule.cs @@ -0,0 +1,96 @@ +using System.Threading.Tasks; +using Qmmands; +using WingsEmu.Commands.Checks; +using WingsEmu.Commands.Entities; +using WingsEmu.DTOs.Account; +using WingsEmu.Game; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage.Configuration; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Mates.Events; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Npcs; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.Essentials.NPC; + +[Name("Mate Creation")] +[Description("Module related to mate creation commands.")] +[RequireAuthority(AuthorityType.GameMaster)] +public class MateCreationModule : SaltyModuleBase +{ + private readonly IGameLanguageService _gameLanguage; + private readonly IMateEntityFactory _mateEntityFactory; + private readonly INpcMonsterManager _npcMonsterManager; + private readonly IRandomGenerator _randomGenerator; + private readonly ISpPartnerConfiguration _spPartner; + + public MateCreationModule(INpcMonsterManager npcMonsterManager, IGameLanguageService gameLanguage, ISpPartnerConfiguration spPartner, IRandomGenerator randomGenerator, + IMateEntityFactory mateEntityFactory) + { + _npcMonsterManager = npcMonsterManager; + _gameLanguage = gameLanguage; + _spPartner = spPartner; + _randomGenerator = randomGenerator; + _mateEntityFactory = mateEntityFactory; + } + + [Command("addmate")] + [Description("Add mate")] + public async Task AddMateAsync( + [Description("Mob VNUM")] short vnum, + [Description("Level")] byte level, byte? attack = null, byte? defence = null) + { + IClientSession session = Context.Player; + IMonsterData data = _npcMonsterManager.GetNpc(vnum); + + if (data == null) + { + return new SaltyCommandResult(false, "Monster doesn't exist!"); + } + + var npcMate = new MonsterData(data); + + IMateEntity mateEntity = _mateEntityFactory.CreateMateEntity(session.PlayerEntity, npcMate, MateType.Pet, level); + mateEntity.Attack = attack ?? 0; + mateEntity.Defence = defence ?? 0; + + await session.EmitEventAsync(new MateInitializeEvent + { + MateEntity = mateEntity + }); + + await session.EmitEventAsync(new MateJoinTeamEvent { MateEntity = mateEntity }); + return new SaltyCommandResult(true, "NosMate has been added."); + } + + [Command("addpartner")] + [Description("Add partner")] + public async Task AddPartnerAsync( + [Description("Mob VNUM")] short vnum, + [Description("Level")] byte level) + { + IClientSession session = Context.Player; + + IMonsterData data = _npcMonsterManager.GetNpc(vnum); + + if (data == null) + { + return new SaltyCommandResult(false, "Monster doesn't exist!"); + } + + var npcPartner = new MonsterData(data); + + IMateEntity partner = _mateEntityFactory.CreateMateEntity(session.PlayerEntity, npcPartner, MateType.Partner, level); + await session.EmitEventAsync(new MateInitializeEvent + { + MateEntity = partner + }); + + await session.EmitEventAsync(new MateJoinTeamEvent { MateEntity = partner }); + + return new SaltyCommandResult(true, "Partner has been added."); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.Essentials/NPC/NPCModule.cs b/srcs/_plugins/WingsEmu.Plugins.Essentials/NPC/NPCModule.cs new file mode 100644 index 0000000..fb0784e --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.Essentials/NPC/NPCModule.cs @@ -0,0 +1,123 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading.Tasks; +using Qmmands; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.Commands.Checks; +using WingsEmu.Commands.Entities; +using WingsEmu.DTOs.Account; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Entities.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.Essentials.NPC; + +[Name("NPC")] +[Description("Module related to NPC commands.")] +[RequireAuthority(AuthorityType.GameAdmin)] +public class NPCModule : SaltyModuleBase +{ + private readonly IBuffFactory _buffFactory; + private readonly IEnumerable _buffPackConfiguration; + private readonly ICardsManager _cardManager; + private readonly IGameLanguageService _gameLanguage; + private readonly INpcEntityFactory _npcEntityFactory; + private readonly INpcMonsterManager _npcMonsterManager; + + public NPCModule(INpcMonsterManager npcMonsterManager, IEnumerable buffPackConfiguration, IGameLanguageService gameLanguage, ICardsManager cardManager, IBuffFactory buffFactory, + INpcEntityFactory npcEntityFactory) + { + _npcMonsterManager = npcMonsterManager; + _buffPackConfiguration = buffPackConfiguration; + _gameLanguage = gameLanguage; + _cardManager = cardManager; + _buffFactory = buffFactory; + _npcEntityFactory = npcEntityFactory; + } + + [Command("buffpack", "buybuffpack")] + [Description("List buff packs")] + public async Task BuffPackList() + { + IClientSession session = Context.Player; + + IEnumerable kits = _buffPackConfiguration; + + session.SendChatMessage("[========= BUFF PACKS =========]", ChatMessageColorType.Yellow); + foreach (BuffPack kit in kits) + { + string kitDetails = $"[{kit.Name}]: {kit.Price.ToString()} golds"; + session.SendChatMessage(kitDetails, ChatMessageColorType.Yellow); + foreach (BuffPackElement buffPackElement in kit.Buffs) + { + string buffName = _gameLanguage.GetLanguage(GameDataType.Card, _cardManager.GetCardByCardId(buffPackElement.CardId).Name, session.UserLanguage); + session.SendChatMessage($"{new string('-', kit.Name.Length + 2)}: {buffPackElement.Duration} {buffName} ", ChatMessageColorType.Yellow); + } + } + + session.SendChatMessage("[========================]", ChatMessageColorType.Yellow); + return new SaltyCommandResult(true); + } + + [Command("buffpack", "buybuffpack")] + [Description("Buy a buff pack")] + public async Task BuffPackBuy([NotNull] string packName) + { + IClientSession session = Context.Player; + + BuffPack kit = _buffPackConfiguration.FirstOrDefault(s => s.Name == packName); + + if (kit == null) + { + session.SendErrorChatMessage($"Kit: {packName}"); + return new SaltyCommandResult(false); + } + + if (!session.HasEnoughGold(kit.Price)) + { + session.SendErrorChatMessage($"Not enough gold: {packName}:{kit.Price.ToString()}"); + return new SaltyCommandResult(false); + } + + session.PlayerEntity.Gold -= kit.Price; + session.RefreshGold(); + var buffs = new List(); + foreach (BuffPackElement buff in kit.Buffs) + { + buffs.Add(_buffFactory.CreateBuff(buff.CardId, session.PlayerEntity, buff.Level, buff.Duration, buff.KeepOnDeath ? BuffFlag.BIG : BuffFlag.NORMAL)); + } + + session.BroadcastEffectInRange(54); + session.BroadcastEffectInRange(55); + session.BroadcastEffectInRange(56); + session.BroadcastSoundInRange(1517); + + await session.PlayerEntity.AddBuffAsync(buffs.ToArray()); + + return new SaltyCommandResult(true); + } + + [Command("npcadd")] + [Description("Add NPC")] + public async Task NpcAddAsync( + [Description("NPC VNum")] short vnum) + { + IClientSession session = Context.Player; + INpcEntity monster = _npcEntityFactory.CreateNpc(vnum, session.CurrentMapInstance); + if (monster == null) + { + return new SaltyCommandResult(false, "Monster doesn't exist."); + } + + await monster.EmitEventAsync(new MapJoinNpcEntityEvent(monster, session.PlayerEntity.PositionX, session.PlayerEntity.PositionY)); + return new SaltyCommandResult(true, "NPC has been created."); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.Essentials/Skills/AdministratorCheatModule_Skills.cs b/srcs/_plugins/WingsEmu.Plugins.Essentials/Skills/AdministratorCheatModule_Skills.cs new file mode 100644 index 0000000..20bab8c --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.Essentials/Skills/AdministratorCheatModule_Skills.cs @@ -0,0 +1,144 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Qmmands; +using WingsAPI.Game.Extensions.Quicklist; +using WingsEmu.Commands.Checks; +using WingsEmu.Commands.Entities; +using WingsEmu.DTOs.Account; +using WingsEmu.DTOs.Skills; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Algorithm; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Skills; +using WingsEmu.Packets.Enums.Character; + +namespace WingsEmu.Plugins.Essentials.Skills; + +[Name("Admin-SkillsCheat")] +[Description("Module related to Administrator commands.")] +[RequireAuthority(AuthorityType.GameAdmin)] +public class AdministratorCheatModule_Skills : SaltyModuleBase +{ + private readonly ICharacterAlgorithm _characterAlgorithm; + private readonly IGameLanguageService _gameLanguage; + private readonly ISkillsManager _skillsManager; + + public AdministratorCheatModule_Skills(ISkillsManager skillsManager, IGameLanguageService gameLanguage, ICharacterAlgorithm characterAlgorithm) + { + _skillsManager = skillsManager; + _gameLanguage = gameLanguage; + _characterAlgorithm = characterAlgorithm; + } + + [Command("unlockallclassskills")] + public async Task UnlockAllSkills() + { + IClientSession session = Context.Player; + IEnumerable skills = _skillsManager.GetSkills().Where(x => (ClassType)x.Class == session.PlayerEntity.Class && x.SkillType == SkillType.NormalPlayerSkill); + + foreach (SkillDTO skill in skills) + { + if (session.PlayerEntity.CharacterSkills.ContainsKey(skill.Id)) + { + continue; + } + + var newSkill = new CharacterSkill + { + SkillVNum = skill.Id + }; + + session.PlayerEntity.CharacterSkills.TryAdd(skill.Id, newSkill); + session.PlayerEntity.Skills.Add(newSkill); + } + + session.RefreshSkillList(); + session.RefreshQuicklist(); + session.RefreshLevel(_characterAlgorithm); + + return new SaltyCommandResult(true); + } + + [Command("unlockallpassives", "allbasicskills")] + public async Task UnlockAllBasicSkills() + { + IEnumerable skills = _skillsManager.GetSkills().Where(s => s.IsPassiveSkill()); + + var passiveSkills = Context.Player.PlayerEntity.CharacterSkills + .Where(s => s.Value.Skill.IsPassiveSkill()) + .Select(s => s.Value.SkillVNum) + .ToHashSet(); + + foreach (SkillDTO skill in skills) + { + if (passiveSkills.Contains(skill.Id)) + { + continue; + } + + Context.Player.PlayerEntity.CharacterSkills.TryAdd(skill.Id, new CharacterSkill + { + SkillVNum = skill.Id + }); + } + + Context.Player.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.SKILL_SHOUTMESSAGE_LEARNED, Context.Player.UserLanguage), 0); + Context.Player.RefreshSkillList(); + Context.Player.RefreshQuicklist(); + Context.Player.RefreshPassiveBCards(); + return new SaltyCommandResult(true, "All passive skills unlocked !"); + } + + [Command("clear-buff", "remove-card", "remove-buff")] + [Description("remove buff on map")] + public async Task ClearMapFromBuff(int cardId) + { + var tmp = Context.Player.CurrentMapInstance.Sessions.ToList(); + + foreach (IClientSession player in tmp) + { + foreach (IMateEntity pet in player.PlayerEntity.MateComponent.GetMates()) + { + Buff petBuff = pet.BuffComponent.GetBuff(cardId); + if (petBuff == null) + { + continue; + } + + await pet.RemoveBuffAsync(false, petBuff); + } + + Buff buff = player.PlayerEntity.BuffComponent.GetBuff(cardId); + if (buff == null) + { + continue; + } + + await player.PlayerEntity.RemoveBuffAsync(false, buff); + } + + foreach (INpcEntity npc in Context.Player.CurrentMapInstance.GetAliveNpcs()) + { + Buff buff = npc.BuffComponent.GetBuff(cardId); + if (buff == null) + { + continue; + } + + await npc.RemoveBuffAsync(false, buff); + } + + return new SaltyCommandResult(true, "All buff"); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.Essentials/SuperGameMaster/BetaGameTester.cs b/srcs/_plugins/WingsEmu.Plugins.Essentials/SuperGameMaster/BetaGameTester.cs new file mode 100644 index 0000000..3d81471 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.Essentials/SuperGameMaster/BetaGameTester.cs @@ -0,0 +1,417 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Qmmands; +using WingsAPI.Game.Extensions.Families; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsAPI.Game.Extensions.Quicklist; +using WingsEmu.Commands.Checks; +using WingsEmu.Commands.Entities; +using WingsEmu.DTOs.Account; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Algorithm; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Families; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Networking.Broadcasting; +using WingsEmu.Game.Skills; +using WingsEmu.Packets.Enums.Character; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.Essentials.GameMaster; + +[Name("Beta Game Tester")] +[Description("Module related to Beta Game Tester commands.")] +[RequireAuthority(AuthorityType.GameMaster)] +public class BetaGameTester : SaltyModuleBase +{ + private readonly IBattleEntityAlgorithmService _algorithm; + private readonly ICharacterAlgorithm _characterAlgorithm; + private readonly IGameItemInstanceFactory _gameItemInstance; + private readonly IItemsManager _itemManager; + private readonly IGameLanguageService _language; + private readonly IRankingManager _rankingManager; + private readonly IReputationConfiguration _reputationConfiguration; + private readonly IServerManager _serverManager; + private readonly ISkillsManager _skillsManager; + + public BetaGameTester(IItemsManager itemsManager, IGameLanguageService language, ISkillsManager skillsManager, IBattleEntityAlgorithmService algorithm, ICharacterAlgorithm characterAlgorithm, + IReputationConfiguration reputationConfiguration, IServerManager serverManager, IGameItemInstanceFactory gameItemInstance, IRankingManager rankingManager) + { + _language = language; + _itemManager = itemsManager; + _skillsManager = skillsManager; + _algorithm = algorithm; + _characterAlgorithm = characterAlgorithm; + _reputationConfiguration = reputationConfiguration; + _serverManager = serverManager; + _gameItemInstance = gameItemInstance; + _rankingManager = rankingManager; + } + + [Command("gold")] + [Description("Set player gold")] + public async Task SetGold( + [Description("Amount of gold.")] long gold) + { + IClientSession session = Context.Player; + if (gold < 0) + { + return new SaltyCommandResult(false, "Wrong value!"); + } + + if (gold > _serverManager.MaxGold) + { + return new SaltyCommandResult(false, "Wrong value!"); + } + + session.PlayerEntity.Gold = gold; + session.RefreshGold(); + return new SaltyCommandResult(true, $"Your gold: {gold}"); + } + + [Command("item")] + [Description("Create an Item")] + public async Task CreateitemAsync( + [Description("Item VNUM.")] short itemvnum, + [Description("Amount.")] short amount) + { + IClientSession session = Context.Player; + GameItemInstance newItem = _gameItemInstance.CreateItem(itemvnum, amount); + await session.AddNewItemToInventory(newItem); + return new SaltyCommandResult(true, $"Created item: {_language.GetLanguage(GameDataType.Item, newItem.GameItem.Name, Context.Player.UserLanguage)}"); + } + + [Command("item")] + [Description("Create an Item with rare and upgrade.")] + public async Task CreateitemAsync( + [Description("Item VNUM.")] short itemvnum, + [Description("Amount.")] short amount, + [Description("Item's rare.")] sbyte rare, + [Description("Item's upgrade.")] byte upgrade) + { + IClientSession session = Context.Player; + + GameItemInstance newItem = _gameItemInstance.CreateItem(itemvnum, amount, upgrade, rare); + await session.AddNewItemToInventory(newItem); + return new SaltyCommandResult(true, $"Created item: {_language.GetLanguage(GameDataType.Item, newItem.GameItem.Name, Context.Player.UserLanguage)}"); + } + + [Command("position", "pos")] + [Description("Outputs your current position")] + public async Task WhereAmI() => new SaltyCommandResult(true, + $"MapId: {Context.Player.CurrentMapInstance?.MapId} | X: {Context.Player.PlayerEntity.PositionX} | Y: {Context.Player.PlayerEntity.PositionY}"); + + [Command("splevel", "splvl")] + [Description("Set player job level")] + public async Task SetSpLevel( + [Description("SP job.")] byte spLevel) + { + if (spLevel == 0) + { + return new SaltyCommandResult(false, "Wrong value!"); + } + + IClientSession session = Context.Player; + if (session.PlayerEntity.Specialist == null) + { + return new SaltyCommandResult(false, "You need to wear Specialist Card!"); + } + + session.PlayerEntity.Specialist.SpLevel = spLevel; + session.PlayerEntity.Specialist.Xp = 0; + session.RefreshLevel(_characterAlgorithm); + session.LearnSpSkill(_skillsManager, _language); + foreach (IBattleEntitySkill skill in session.PlayerEntity.Skills) + { + skill.LastUse = DateTime.UtcNow.AddDays(-1); + } + + session.BroadcastIn(_reputationConfiguration, _rankingManager.TopReputation, new ExceptSessionBroadcast(session)); + session.BroadcastGidx(session.PlayerEntity.Family, _language); + session.BroadcastEffectInRange(EffectType.JobLevelUp); + return new SaltyCommandResult(true, "Specialist Card SP Level has been updated."); + } + + [Command("jlevel", "joblvl", "joblevel", "jlvl")] + [Description("Set player job level")] + public async Task SetJobLevel( + [Description("Joblevel.")] byte jobLevel) + { + if (jobLevel == 0) + { + return new SaltyCommandResult(false, "Wrong value!"); + } + + IClientSession session = Context.Player; + + if ((session.PlayerEntity.Class != 0 || jobLevel > 20) && (session.PlayerEntity.Class == 0 || jobLevel > 255) || + jobLevel <= 0) + { + return new SaltyCommandResult(false, "Wrong value!"); + } + + session.PlayerEntity.JobLevel = jobLevel; + session.PlayerEntity.JobLevelXp = 0; + session.PlayerEntity.CharacterSkills.Clear(); + + session.RefreshLevel(_characterAlgorithm); + + session.BroadcastIn(_reputationConfiguration, _rankingManager.TopReputation, new ExceptSessionBroadcast(session)); + session.BroadcastGidx(session.PlayerEntity.Family, _language); + session.BroadcastEffectInRange(EffectType.JobLevelUp); + + if (session.PlayerEntity.Class == ClassType.Wrestler) + { + session.PlayerEntity.CharacterSkills[1525] = new CharacterSkill + { + SkillVNum = 1525 + }; + + session.PlayerEntity.CharacterSkills[1529] = new CharacterSkill + { + SkillVNum = 1529 + }; + + session.PlayerEntity.CharacterSkills[1565] = new CharacterSkill + { + SkillVNum = 1565 + }; + } + else + { + session.PlayerEntity.CharacterSkills[(short)(200 + 20 * (byte)session.PlayerEntity.Class)] = new CharacterSkill + { + SkillVNum = (short)(200 + 20 * (byte)session.PlayerEntity.Class) + }; + session.PlayerEntity.CharacterSkills[(short)(201 + 20 * (byte)session.PlayerEntity.Class)] = new CharacterSkill + { + SkillVNum = (short)(201 + 20 * (byte)session.PlayerEntity.Class) + }; + session.PlayerEntity.CharacterSkills[236] = new CharacterSkill + { + SkillVNum = 236 + }; + } + + session.PlayerEntity.SkillComponent.SkillUpgrades.Clear(); + + session.RefreshSkillList(); + session.RefreshQuicklist(); + session.LearnAdventurerSkill(_skillsManager, _language); + + return new SaltyCommandResult(true, "Job Level has been updated."); + } + + [Command("speed")] + [Description("Set player speed")] + public async Task SetSpeed( + [Description("Amount of speed (0-59).")] + byte speed) + { + if (speed > 59 || speed == 0) + { + return new SaltyCommandResult(false, "Wrong value!"); + } + + IClientSession session = Context.Player; + + session.PlayerEntity.Speed = speed; + session.SendCondPacket(); + session.PlayerEntity.IsCustomSpeed = true; + return new SaltyCommandResult(true, $"Speed: {speed}"); + } + + [Command("speed")] + [Description("Turn off your custom speed")] + public async Task SetSpeed() + { + Context.Player.PlayerEntity.IsCustomSpeed = false; + Context.Player.PlayerEntity.RefreshCharacterStats(); + Context.Player.SendCondPacket(); + return new SaltyCommandResult(true); + } + + [Command("reput")] + [Description("Set reputation to the session")] + public async Task SetReput( + [Description("Amount of reputation.")] long reput) + { + if (reput < 0) + { + return new SaltyCommandResult(false, "Wrong value!"); + } + + IClientSession session = Context.Player; + + session.PlayerEntity.Reput = reput; + session.RefreshReputation(_reputationConfiguration, _rankingManager.TopReputation); + return new SaltyCommandResult(true, $"Reputation: {reput}"); + } + + [Command("class", "changeclass")] + [Description("Set character class.")] + public async Task SetClass( + [Description("0 - Adv, 1 - Sword, 2 - Archer, 3 - Mage, 4 - MA.")] + string classType) + { + if (!Enum.TryParse(classType, out ClassType classt)) + { + return new SaltyCommandResult(false, "Wrong value!"); + } + + if (classt >= ClassType.Wrestler) + { + return new SaltyCommandResult(false, "This Class doesn't exist!"); + } + + IClientSession session = Context.Player; + + session.EmitEvent(new ChangeClassEvent { NewClass = classt, ShouldObtainBasicItems = false, ShouldObtainNewFaction = false }); + return new SaltyCommandResult(true, "Class has been changed."); + } + + [Command("level", "lvl")] + [Description("Set player level")] + public async Task SetLvl( + [Description("Level.")] byte level, bool mates = false) + { + IClientSession session = Context.Player; + + if (level == 0) + { + return new SaltyCommandResult(false, "Wrong value!"); + } + + if (level == 150) + { + return new SaltyCommandResult(false, "Wrong value!"); + } + + session.PlayerEntity.Level = level; + session.PlayerEntity.LevelXp = 0; + session.PlayerEntity.RefreshCharacterStats(); + session.PlayerEntity.RefreshMaxHpMp(_algorithm); + session.PlayerEntity.Hp = session.PlayerEntity.MaxHp; + session.PlayerEntity.Mp = session.PlayerEntity.MaxMp; + + session.RefreshStat(); + session.RefreshStatInfo(); + session.RefreshStatChar(); + session.RefreshLevel(_characterAlgorithm); + + IFamily family = session.PlayerEntity.Family; + + session.BroadcastIn(_reputationConfiguration, _rankingManager.TopReputation, new ExceptSessionBroadcast(session)); + session.BroadcastGidx(family, _language); + session.BroadcastEffectInRange(EffectType.NormalLevelUp); + session.BroadcastEffectInRange(EffectType.NormalLevelUpSubEffect); + + if (mates) + { + foreach (IMateEntity mateEntity in session.PlayerEntity.MateComponent.GetMates()) + { + mateEntity.Level = level; + mateEntity.Hp = mateEntity.MaxHp; + mateEntity.Mp = mateEntity.MaxMp; + } + + session.RefreshMateStats(); + } + + return new SaltyCommandResult(true, "Level has been updated."); + } + + [Command("sex")] + [Description("Change sex of character")] + public async Task ChangeGenderAsync() + { + IClientSession session = Context.Player; + + session.PlayerEntity.Gender = session.PlayerEntity.Gender == GenderType.Female ? GenderType.Male : GenderType.Female; + session.SendMsg(_language.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_SEX_CHANGED, session.UserLanguage), MsgMessageType.Middle); + + session.SendEqPacket(); + session.SendGenderPacket(); + session.BroadcastIn(_reputationConfiguration, _rankingManager.TopReputation, new ExceptSessionBroadcast(session)); + session.BroadcastGidx(session.PlayerEntity.Family, _language); + session.BroadcastCMode(); + session.BroadcastEffect(EffectType.Transform, new RangeBroadcast(session.PlayerEntity.PositionX, session.PlayerEntity.PositionY)); + return new SaltyCommandResult(true, "Gender has been changed."); + } + + [Command("heal")] + [Description("Heal yourself.")] + public async Task HealAsync() + { + IClientSession session = Context.Player; + + session.PlayerEntity.Hp = session.PlayerEntity.MaxHp; + session.PlayerEntity.Mp = session.PlayerEntity.MaxMp; + + session.RefreshStat(); + + return new SaltyCommandResult(true, "You have been healed."); + } + + [Command("godmode")] + [Description("Enable or disable godmode.")] + public async Task GodmodeAsync() + { + IClientSession session = Context.Player; + + session.PlayerEntity.CheatComponent.HasGodMode = !session.PlayerEntity.CheatComponent.HasGodMode; + session.SendChatMessage($"GODMODE: {(session.PlayerEntity.CheatComponent.HasGodMode ? "ON" : "OFF")}", ChatMessageColorType.Yellow); + return new SaltyCommandResult(true); + } + + [Command("zoom")] + [Description("Camera zoom.")] + public async Task ZoomAsync( + [Description("Zoom value.")] byte valueZoom) + { + IClientSession session = Context.Player; + + session.PlayerEntity.SkillComponent.Zoom = valueZoom; + session.RefreshZoom(); + return new SaltyCommandResult(true, $"Zoom updated: {valueZoom}"); + } + + [Command("clearchat")] + [Description("Clear your chat")] + public async Task ClearchatAsync() + { + IClientSession session = Context.Player; + + for (int i = 0; i < 50; i++) + { + session.SendChatMessage(" ", ChatMessageColorType.Red); + } + + return new SaltyCommandResult(true); + } + + [Command("completed-ts")] + [Description("Check completed Time-Spaces done by player")] + public async Task CompletedTs(IClientSession target) + { + if (!target.PlayerEntity.CompletedTimeSpaces.Any()) + { + return new SaltyCommandResult(true, "Player didn't completed any Time-Space"); + } + + foreach (long tsId in target.PlayerEntity.CompletedTimeSpaces) + { + Context.Player.SendChatMessage($"Completed Time-Space: {tsId}", ChatMessageColorType.Yellow); + } + + return new SaltyCommandResult(true); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.Essentials/SuperGameMaster/MinilandModule.cs b/srcs/_plugins/WingsEmu.Plugins.Essentials/SuperGameMaster/MinilandModule.cs new file mode 100644 index 0000000..e2e548e --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.Essentials/SuperGameMaster/MinilandModule.cs @@ -0,0 +1,56 @@ +using System.Threading.Tasks; +using Qmmands; +using WingsEmu.Commands.Checks; +using WingsEmu.Commands.Entities; +using WingsEmu.DTOs.Account; +using WingsEmu.Game.Configurations.Miniland; +using WingsEmu.Game.Miniland.Minigames; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Plugins.Essentials.GameMaster; + +[Name("Miniland")] +[Description("Module related to Miniland commands.")] +[Group("miniland", "minigame")] +[RequireAuthority(AuthorityType.SuperGameMaster)] +public class MinilandModule : SaltyModuleBase +{ + private readonly MinigameConfiguration _minigameConfiguration; + + public MinilandModule(MinigameConfiguration minigameConfiguration) => _minigameConfiguration = minigameConfiguration; + + + [Command("refresh-p", "refresh-points", "rp")] + [Description("Refresh your daily minigame points.")] + public async Task MinigameRefreshPoints([Description("Force refresh")] bool force = false) + { + Context.Player.EmitEvent(new MinigameRefreshProductionEvent(force)); + return new SaltyCommandResult(true); + } + + [Command("points", "mgp")] + [Description("Sets your minigame points.")] + public async Task MinigamePoints([Description("Minigame Points to set")] short minigamePoints) + { + if (minigamePoints > _minigameConfiguration.Configuration.MaxmimumMinigamePoints || minigamePoints < 0) + { + return new SaltyCommandResult(false); + } + + Context.Player.PlayerEntity.MinilandPoint = minigamePoints; + return new SaltyCommandResult(true); + } + + [Command("points", "mgp")] + [Description("Sets your minigame points.")] + public async Task MinigamePoints(IClientSession target, [Description("Minigame Points to set")] short minigamePoints) + { + if (minigamePoints > _minigameConfiguration.Configuration.MaxmimumMinigamePoints || minigamePoints < 0) + { + return new SaltyCommandResult(false); + } + + target.PlayerEntity.MinilandPoint = minigamePoints; + return new SaltyCommandResult(true); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.Essentials/SuperGameMaster/SuperGameMasterMonsterModule.cs b/srcs/_plugins/WingsEmu.Plugins.Essentials/SuperGameMaster/SuperGameMasterMonsterModule.cs new file mode 100644 index 0000000..f7abd09 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.Essentials/SuperGameMaster/SuperGameMasterMonsterModule.cs @@ -0,0 +1,104 @@ +using System.Threading.Tasks; +using Qmmands; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.Commands.Checks; +using WingsEmu.Commands.Entities; +using WingsEmu.DTOs.Account; +using WingsEmu.DTOs.Mails; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Entities.Event; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Items; +using WingsEmu.Game.Mails.Events; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Plugins.Essentials.GameMaster; + +[Name("QuestHelpingModule")] +[Description("Module related to Miniland commands.")] +[RequireAuthority(AuthorityType.SuperGameMaster)] +public class SuperGameMasterMonsterModule : SaltyModuleBase +{ + private readonly IMonsterEntityFactory _entity; + private readonly IGameItemInstanceFactory _gameItemInstanceFactory; + private readonly IGameLanguageService _language; + private readonly INpcMonsterManager _npcMonsterManager; + + public SuperGameMasterMonsterModule(IMonsterEntityFactory entity, INpcMonsterManager npcMonsterManager, IGameLanguageService language, IGameItemInstanceFactory gameItemInstanceFactory) + { + _entity = entity; + _npcMonsterManager = npcMonsterManager; + _language = language; + _gameItemInstanceFactory = gameItemInstanceFactory; + } + + + [Command("quest-mob")] + [Description("Spawn monster with given vnum")] + public async Task SpawnMob( + [Description("Mob vnum")] short vnum, + [Description("Amount")] byte amount, + [Description("Can move (false, true)")] + bool canMove = false, bool isHostile = false) + { + IClientSession session = Context.Player; + + if (amount > 20) + { + return new SaltyCommandResult(false, "Amount can not exceed 20"); + } + + IMonsterData monster = _npcMonsterManager.GetNpc(vnum); + + if (monster == null) + { + return new SaltyCommandResult(false, "Monster doesn't exist!"); + } + + for (int i = 0; i < amount; i++) + { + IMonsterEntity monsterEntity = _entity.CreateMonster(monster, Context.Player.CurrentMapInstance, new MonsterEntityBuilder + { + IsWalkingAround = canMove, + IsHostile = isHostile + }); + monsterEntity.ChangePosition(Context.Player.PlayerEntity.Position); + monsterEntity.FirstX = Context.Player.PlayerEntity.Position.X; + monsterEntity.FirstY = Context.Player.PlayerEntity.Position.Y; + await monsterEntity.EmitEventAsync(new MapJoinMonsterEntityEvent(monsterEntity)); + } + + return new SaltyCommandResult(true, $"Created monster: {_language.GetLanguage(GameDataType.NpcMonster, monster.Name, Context.Player.UserLanguage)}"); + } + + [Command("quest-item")] + [Description("Create an Item")] + public async Task CreateitemAsync( + [Description("Item VNUM.")] short itemvnum, + [Description("Amount.")] short amount) + { + IClientSession session = Context.Player; + GameItemInstance newItem = _gameItemInstanceFactory.CreateItem(itemvnum, amount); + await session.AddNewItemToInventory(newItem); + return new SaltyCommandResult(true, $"Created item: {_language.GetLanguage(GameDataType.Item, newItem.GameItem.Name, Context.Player.UserLanguage)}"); + } + + [Command("quest-mail")] + [Description("Send gift mail to someone")] + public async Task ParcelAsync(IClientSession target, + [Description("Item VNUM.")] int itemVnum, + [Description("Amount.")] short amount) + { + IClientSession session = Context.Player; + if (target == null) + { + return new SaltyCommandResult(false, "Player is offline."); + } + + GameItemInstance item = _gameItemInstanceFactory.CreateItem(itemVnum, amount); + await session.EmitEventAsync(new MailCreateEvent(session.PlayerEntity.Name, target.PlayerEntity.Id, MailGiftType.Normal, item)); + return new SaltyCommandResult(true, "Parcel has been sent."); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.Essentials/Teleport/TeleportModule.cs b/srcs/_plugins/WingsEmu.Plugins.Essentials/Teleport/TeleportModule.cs new file mode 100644 index 0000000..921fa38 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.Essentials/Teleport/TeleportModule.cs @@ -0,0 +1,113 @@ +using System.Threading.Tasks; +using Qmmands; +using WingsEmu.Commands.Checks; +using WingsEmu.Commands.Entities; +using WingsEmu.DTOs.Account; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Plugins.Essentials.Teleport; + +[Name("Teleport")] +[Description("Module related to teleport commands.")] +[RequireAuthority(AuthorityType.GameMaster)] +public class TeleportModule : SaltyModuleBase +{ + private readonly IGameLanguageService _language; + private readonly IMapManager _mapManager; + + public TeleportModule(IGameLanguageService languageService, IMapManager mapManager) + { + _mapManager = mapManager; + _language = languageService; + } + + [Command("tp", "go")] + [Description("Teleport yourself to given MapId.")] + public async Task TeleportAsync( + [Description("MapId.")] short MapId) + { + IClientSession session = Context.Player; + await _mapManager.TeleportOnRandomPlaceInMapAsync(session, _mapManager.GetBaseMapInstanceByMapId(MapId)); + return new SaltyCommandResult(true); + } + + [Command("tp", "go")] + [Description("Teleport yourself to given x and y on current map.")] + public async Task TeleportAsync( + [Description("Coordinates: X and y")] short x, short y) + { + IClientSession session = Context.Player; + session.PlayerEntity.TeleportOnMap(x, y, true); + return new SaltyCommandResult(true); + } + + [Command("tp", "go")] + [Description("Teleport yourself to given MapId on x and y.")] + public async Task TeleportAsync( + [Description("MapId, x and y.")] short MapId, short x, short y) + { + IClientSession session = Context.Player; + session.ChangeMap(MapId, x, y); + return new SaltyCommandResult(true); + } + + [Command("goto")] + [Description("Teleport yourself to given player.")] + public async Task GotoAsync( + [Description("Player's name.")] IClientSession target) + { + IClientSession session = Context.Player; + session.ChangeMap(target.CurrentMapInstance, target.PlayerEntity.PositionX, target.PlayerEntity.PositionY); + return new SaltyCommandResult(true); + } + + [Command("summon")] + [Description("Teleport given player to yourself.")] + public async Task SummonAsync( + [Description("Player's name.")] IClientSession target) + { + IClientSession session = Context.Player; + target.ChangeMap(session.CurrentMapInstance, session.PlayerEntity.PositionX, session.PlayerEntity.PositionY); + return new SaltyCommandResult(true); + } + + [Command("act4")] + [Description("Brings you to act4")] + public async Task GotoAct4Async() + { + await Context.Player.EmitEventAsync(new PlayerChangeChannelAct4Event()); + return new SaltyCommandResult(true); + } + + [Command("act4leave")] + [Description("Brings you to alveus")] + public async Task LeaveoAct4Async() + { + await Context.Player.EmitEventAsync(new PlayerReturnFromAct4Event()); + return new SaltyCommandResult(true); + } + + [Command("mfield")] + [Description("Teleport yourself to your Mini-Land.")] + public async Task MfieldAsync() + { + IClientSession session = Context.Player; + + session.ChangeMap(session.PlayerEntity.Miniland); + return new SaltyCommandResult(true); + } + + [Command("mjoin")] + [Description("Teleport yourself to player's Mini-Land.")] + public async Task MjoinAsync( + [Description("Player's name.")] IClientSession target) + { + IClientSession session = Context.Player; + session.ChangeMap(target.PlayerEntity.Miniland); + return new SaltyCommandResult(true); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.Essentials/WingsEmu.Plugins.Essentials.csproj b/srcs/_plugins/WingsEmu.Plugins.Essentials/WingsEmu.Plugins.Essentials.csproj new file mode 100644 index 0000000..5e87518 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.Essentials/WingsEmu.Plugins.Essentials.csproj @@ -0,0 +1,19 @@ + + + + net5.0 + latest + disable + + + + + + + + + + + + + diff --git a/srcs/_plugins/WingsEmu.Plugins.GameEvents/CommandModules/GameEventsBasicModule.cs b/srcs/_plugins/WingsEmu.Plugins.GameEvents/CommandModules/GameEventsBasicModule.cs new file mode 100644 index 0000000..f01b580 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.GameEvents/CommandModules/GameEventsBasicModule.cs @@ -0,0 +1,82 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using PhoenixLib.Events; +using PhoenixLib.ServiceBus; +using Qmmands; +using WingsAPI.Communication.InstantBattle; +using WingsEmu.Commands.Checks; +using WingsEmu.Commands.Entities; +using WingsEmu.DTOs.Account; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.GameEvent; +using WingsEmu.Game.Networking; +using WingsEmu.Plugins.GameEvents.DataHolder; +using WingsEmu.Plugins.GameEvents.Event.Global; +using WingsEmu.Plugins.GameEvents.Event.InstantBattle; + +namespace WingsEmu.Plugins.GameEvents.CommandModules +{ + [Name("GameEventsBasic")] + [Group("ic", "instant-combat", "instant-battle")] + [Description("Module related to GameEvents commands.")] + [RequireAuthority(AuthorityType.GameMaster)] + public class GameEventsBasicModule : SaltyModuleBase + { + private readonly IAsyncEventPipeline _eventPipeline; + private readonly IGameEventInstanceManager _gameEventInstanceManager; + private readonly IMessagePublisher _messagePublisher; + + public GameEventsBasicModule(IAsyncEventPipeline eventPipeline, IMessagePublisher messagePublisher, IGameEventInstanceManager gameEventInstanceManager) + { + _eventPipeline = eventPipeline; + _messagePublisher = messagePublisher; + _gameEventInstanceManager = gameEventInstanceManager; + } + + [Command("start")] + [Description("Starts an instant combat")] + public async Task StartGameEvent(bool noDelay = true) + { + Context.Player.SendSuccessChatMessage("Instant combat started"); + await _messagePublisher.PublishAsync(new InstantBattleStartMessage + { + HasNoDelay = noDelay + }); + await _eventPipeline.ProcessEventAsync(new GameEventPrepareEvent(GameEventType.InstantBattle) + { + NoDelay = noDelay + }); + + return new SaltyCommandResult(true); + } + + [Command("finish")] + [Description("Finish instant-combat")] + public async Task Finish() + { + IClientSession session = Context.Player; + + IReadOnlyCollection instantBattles = _gameEventInstanceManager.GetGameEventsByType(GameEventType.InstantBattle); + if (instantBattles == null || !instantBattles.Any()) + { + return new SaltyCommandResult(false, "There is no Instant Battles now."); + } + + IGameEventInstance mapInstance = instantBattles.FirstOrDefault(x => x?.MapInstance != null && x.MapInstance.Id == session.CurrentMapInstance.Id); + if (mapInstance == null) + { + return new SaltyCommandResult(false, "Not in Instant Battle map."); + } + + if (mapInstance is not InstantBattleInstance instance) + { + return new SaltyCommandResult(false, "Not in Instant Battle map."); + } + + instance.AvailableWaves.Clear(); + await _eventPipeline.ProcessEventAsync(new InstantBattleCompleteEvent(instance)); + return new SaltyCommandResult(true); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.GameEvents/Configuration/InstantBattle/GlobalInstantBattleConfiguration.cs b/srcs/_plugins/WingsEmu.Plugins.GameEvents/Configuration/InstantBattle/GlobalInstantBattleConfiguration.cs new file mode 100644 index 0000000..34b339a --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.GameEvents/Configuration/InstantBattle/GlobalInstantBattleConfiguration.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using System.Linq; +using WingsEmu.Core; +using WingsEmu.Game.Characters; +using WingsEmu.Game.GameEvent.Configuration; + +namespace WingsEmu.Plugins.GameEvents.Configuration.InstantBattle +{ + public class GlobalInstantBattleConfiguration : IGlobalInstantBattleConfiguration + { + public List Configurations { get; set; } + + public InstantBattleConfiguration GetInternalConfiguration(IPlayerEntity character) + { + InstantBattleConfiguration config; + if (character.HeroLevel > 0) + { + config = GetInternalConfiguration(character.HeroLevel, true); + } + else + { + config = GetInternalConfiguration(character.Level, false); + } + + return config; + } + + public IGameEventConfiguration GetConfiguration(IPlayerEntity character) => GetInternalConfiguration(character); + + public uint GetRegistrationCost(IPlayerEntity character) => 0; + + public IEnumerable> GetNormalRanges() + { + return Configurations.Select(x => x.Requirements.Level).Where(x => x != null); + } + + public IEnumerable> GetHeroicRanges() + { + return Configurations.Select(x => x.Requirements.HeroicLevel).Where(x => x != null); + } + + public IGameEventConfiguration GetConfiguration(byte level, bool heroic) => GetInternalConfiguration(level, heroic); + + private InstantBattleConfiguration GetInternalConfiguration(byte level, bool heroic) + { + return heroic + ? Configurations.Find(x => x.Requirements.HeroicLevel != null && level >= x.Requirements.HeroicLevel.Minimum && level <= x.Requirements.HeroicLevel.Maximum) + : Configurations.Find(x => x.Requirements.Level != null && level >= x.Requirements.Level.Minimum && level <= x.Requirements.Level.Maximum); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.GameEvents/Configuration/InstantBattle/IGlobalInstantBattleConfiguration.cs b/srcs/_plugins/WingsEmu.Plugins.GameEvents/Configuration/InstantBattle/IGlobalInstantBattleConfiguration.cs new file mode 100644 index 0000000..60937be --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.GameEvents/Configuration/InstantBattle/IGlobalInstantBattleConfiguration.cs @@ -0,0 +1,10 @@ +using WingsEmu.Game.Characters; +using WingsEmu.Game.GameEvent.Configuration; + +namespace WingsEmu.Plugins.GameEvents.Configuration.InstantBattle +{ + public interface IGlobalInstantBattleConfiguration : IGlobalGameEventConfiguration + { + InstantBattleConfiguration GetInternalConfiguration(IPlayerEntity sessionCharacter); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.GameEvents/Configuration/InstantBattle/InstantBattleConfiguration.cs b/srcs/_plugins/WingsEmu.Plugins.GameEvents/Configuration/InstantBattle/InstantBattleConfiguration.cs new file mode 100644 index 0000000..7d5cfc3 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.GameEvents/Configuration/InstantBattle/InstantBattleConfiguration.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using WingsEmu.Game.GameEvent; +using WingsEmu.Game.GameEvent.Configuration; +using WingsEmu.Game.Maps; + +namespace WingsEmu.Plugins.GameEvents.Configuration.InstantBattle +{ + public class InstantBattleConfiguration : IGameEventConfiguration + { + private List _timeLeftWarnings; + + private List _waves; + + public TimeSpan ClosingTime { get; set; } + public TimeSpan PreWaveLongWarningTime { get; set; } + public TimeSpan PreWaveSoonWarningTime { get; set; } + public short ReturnPortalX { get; set; } + public short ReturnPortalY { get; set; } + public InstantBattleRequirement Requirements { get; set; } + public InstantBattleReward Reward { get; set; } + + public List TimeLeftWarnings + { + get => _timeLeftWarnings; + set => _timeLeftWarnings = value.OrderBy(x => x).ToList(); + } + + public List Waves + { + get => _waves; + set => _waves = value.OrderBy(x => x.TimeStart).ToList(); + } + + public GameEventType GameEventType => GameEventType.InstantBattle; + + public short MapId { get; set; } // 2004 + public MapInstanceType MapInstanceType => MapInstanceType.EventGameInstance; + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.GameEvents/Configuration/InstantBattle/InstantBattleRequirement.cs b/srcs/_plugins/WingsEmu.Plugins.GameEvents/Configuration/InstantBattle/InstantBattleRequirement.cs new file mode 100644 index 0000000..a24399b --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.GameEvents/Configuration/InstantBattle/InstantBattleRequirement.cs @@ -0,0 +1,11 @@ +using WingsEmu.Core; + +namespace WingsEmu.Plugins.GameEvents.Configuration.InstantBattle +{ + public class InstantBattleRequirement + { + public Range Players { get; set; } + public Range Level { get; set; } + public Range HeroicLevel { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.GameEvents/Configuration/InstantBattle/InstantBattleReward.cs b/srcs/_plugins/WingsEmu.Plugins.GameEvents/Configuration/InstantBattle/InstantBattleReward.cs new file mode 100644 index 0000000..968ab13 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.GameEvents/Configuration/InstantBattle/InstantBattleReward.cs @@ -0,0 +1,11 @@ +namespace WingsEmu.Plugins.GameEvents.Configuration.InstantBattle +{ + public class InstantBattleReward + { + public int GoldMultiplier { get; set; } + public int ReputationMultiplier { get; set; } + public int SpPointsMultiplier { get; set; } + public int FamilyExperience { get; set; } + public int Dignity { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.GameEvents/Configuration/InstantBattle/InstantBattleWave.cs b/srcs/_plugins/WingsEmu.Plugins.GameEvents/Configuration/InstantBattle/InstantBattleWave.cs new file mode 100644 index 0000000..f007871 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.GameEvents/Configuration/InstantBattle/InstantBattleWave.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; + +namespace WingsEmu.Plugins.GameEvents.Configuration.InstantBattle +{ + public class InstantBattleWave + { + public TimeSpan TimeStart { get; set; } + public TimeSpan TimeEnd { get; set; } + public string TitleKey { get; set; } + public List Monsters { get; set; } + public List Drops { get; set; } + } + + public class InstantBattleMonster + { + public short MonsterVnum { get; set; } + public int Amount { get; set; } + } + + public class InstantBattleDrop + { + public short ItemVnum { get; set; } + public short BunchCount { get; set; } + public int AmountPerBunch { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.GameEvents/Consumers/ComplimentsMonthlyRefreshMessageConsumer.cs b/srcs/_plugins/WingsEmu.Plugins.GameEvents/Consumers/ComplimentsMonthlyRefreshMessageConsumer.cs new file mode 100644 index 0000000..d3a1fef --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.GameEvents/Consumers/ComplimentsMonthlyRefreshMessageConsumer.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; +using WingsAPI.Communication.Compliments; +using WingsEmu.Game.Compliments; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Plugins.GameEvents.Consumers +{ + public class ComplimentsMonthlyRefreshMessageConsumer : IMessageConsumer + { + private readonly ISessionManager _sessionManager; + + public ComplimentsMonthlyRefreshMessageConsumer(ISessionManager sessionManager) => _sessionManager = sessionManager; + + public async Task HandleAsync(ComplimentsMonthlyRefreshMessage notification, CancellationToken token) + { + IReadOnlyList sessions = _sessionManager.Sessions; + + foreach (IClientSession session in sessions) + { + await session.EmitEventAsync(new ComplimentsMonthlyRefreshEvent + { + Force = notification.Force + }); + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.GameEvents/Consumers/InstantBattleStartMessageConsumer.cs b/srcs/_plugins/WingsEmu.Plugins.GameEvents/Consumers/InstantBattleStartMessageConsumer.cs new file mode 100644 index 0000000..c96f055 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.GameEvents/Consumers/InstantBattleStartMessageConsumer.cs @@ -0,0 +1,29 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using PhoenixLib.ServiceBus; +using WingsAPI.Communication.InstantBattle; +using WingsEmu.Game.GameEvent; +using WingsEmu.Plugins.GameEvents.Event.Global; + +namespace WingsEmu.Plugins.GameEvents.Consumers +{ + public class InstantBattleStartMessageConsumer : IMessageConsumer + { + private readonly IAsyncEventPipeline _eventPipeline; + + public InstantBattleStartMessageConsumer(IAsyncEventPipeline eventPipeline) => _eventPipeline = eventPipeline; + + public async Task HandleAsync(InstantBattleStartMessage notification, CancellationToken token) + { + await _eventPipeline.ProcessEventAsync(new GameEventPrepareEvent(GameEventType.InstantBattle) + { + NoDelay = notification.HasNoDelay + }); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.GameEvents/Consumers/MinigameRefreshProductionPointsMessageConsumer.cs b/srcs/_plugins/WingsEmu.Plugins.GameEvents/Consumers/MinigameRefreshProductionPointsMessageConsumer.cs new file mode 100644 index 0000000..1ea8f1e --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.GameEvents/Consumers/MinigameRefreshProductionPointsMessageConsumer.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; +using WingsAPI.Communication.Miniland; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Miniland.Minigames; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Plugins.GameEvents.Consumers +{ + public class MinigameRefreshProductionPointsMessageConsumer : IMessageConsumer + { + private readonly ISessionManager _sessionManager; + + public MinigameRefreshProductionPointsMessageConsumer(ISessionManager sessionManager) => _sessionManager = sessionManager; + + public async Task HandleAsync(MinigameRefreshProductionPointsMessage msg, CancellationToken token) + { + IReadOnlyList sessions = _sessionManager.Sessions; + + foreach (IClientSession session in sessions) + { + await session.EmitEventAsync(new MinigameRefreshProductionEvent(msg.Force)); + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.GameEvents/Consumers/QuestDailyRefreshMessageConsumer.cs b/srcs/_plugins/WingsEmu.Plugins.GameEvents/Consumers/QuestDailyRefreshMessageConsumer.cs new file mode 100644 index 0000000..235423f --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.GameEvents/Consumers/QuestDailyRefreshMessageConsumer.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; +using WingsAPI.Communication.Quests; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Quests.Event; + +namespace WingsEmu.Plugins.GameEvents.Consumers +{ + public class QuestDailyRefreshMessageConsumer : IMessageConsumer + { + private readonly ISessionManager _sessionManager; + + public QuestDailyRefreshMessageConsumer(ISessionManager sessionManager) => _sessionManager = sessionManager; + + public async Task HandleAsync(QuestDailyRefreshMessage notification, CancellationToken token) + { + IReadOnlyList sessions = _sessionManager.Sessions; + + foreach (IClientSession session in sessions) + { + await session.EmitEventAsync(new QuestDailyRefreshEvent + { + Force = notification.Force + }); + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.GameEvents/Consumers/RaidRestrictionRefreshMessageConsumer.cs b/srcs/_plugins/WingsEmu.Plugins.GameEvents/Consumers/RaidRestrictionRefreshMessageConsumer.cs new file mode 100644 index 0000000..a3cbc7c --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.GameEvents/Consumers/RaidRestrictionRefreshMessageConsumer.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; +using WingsAPI.Communication.Raid; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Raids.Events; + +namespace WingsEmu.Plugins.GameEvents.Consumers +{ + public class RaidRestrictionRefreshMessageConsumer : IMessageConsumer + { + private readonly ISessionManager _sessionManager; + + public RaidRestrictionRefreshMessageConsumer(ISessionManager sessionManager) => _sessionManager = sessionManager; + + public async Task HandleAsync(RaidRestrictionRefreshMessage notification, CancellationToken token) + { + IReadOnlyList sessions = _sessionManager.Sessions; + + foreach (IClientSession session in sessions) + { + await session.EmitEventAsync(new RaidResetRestrictionEvent()); + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.GameEvents/Consumers/RankingRefreshMessageConsumer.cs b/srcs/_plugins/WingsEmu.Plugins.GameEvents/Consumers/RankingRefreshMessageConsumer.cs new file mode 100644 index 0000000..9e9b034 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.GameEvents/Consumers/RankingRefreshMessageConsumer.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; +using WingsAPI.Communication.Player; +using WingsAPI.Data.Character; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Plugins.GameEvents.Consumers +{ + public class RankingRefreshMessageConsumer : IMessageConsumer + { + private readonly IRankingManager _rankingManager; + private readonly ISessionManager _sessionManager; + + public RankingRefreshMessageConsumer(ISessionManager sessionManager, IRankingManager rankingManager) + { + _sessionManager = sessionManager; + _rankingManager = rankingManager; + } + + public async Task HandleAsync(RankingRefreshMessage notification, CancellationToken token) + { + IReadOnlyList topCompliment = notification.TopCompliment; + IReadOnlyList topReputation = notification.TopReputation; + IReadOnlyList topPoints = notification.TopPoints; + + _rankingManager.RefreshRanking(topCompliment, topPoints, topReputation); + + foreach (IClientSession session in _sessionManager.Sessions) + { + session.SendClinitPacket(topCompliment); + session.SendFlinitPacket(topReputation); + session.SendKdlinitPacket(topPoints); + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.GameEvents/Consumers/SpecialistPointsRefreshMessageConsumer.cs b/srcs/_plugins/WingsEmu.Plugins.GameEvents/Consumers/SpecialistPointsRefreshMessageConsumer.cs new file mode 100644 index 0000000..97c3167 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.GameEvents/Consumers/SpecialistPointsRefreshMessageConsumer.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; +using WingsAPI.Communication.Player; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Plugins.GameEvents.Consumers +{ + public class SpecialistPointsRefreshMessageConsumer : IMessageConsumer + { + private readonly ISessionManager _sessionManager; + + public SpecialistPointsRefreshMessageConsumer(ISessionManager sessionManager) => _sessionManager = sessionManager; + + public async Task HandleAsync(SpecialistPointsRefreshMessage notification, CancellationToken token) + { + IReadOnlyList sessions = _sessionManager.Sessions; + + foreach (IClientSession session in sessions) + { + await session.EmitEventAsync(new SpecialistRefreshEvent + { + Force = notification.Force + }); + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.GameEvents/Consumers/TranslationsRefreshMessageConsumer.cs b/srcs/_plugins/WingsEmu.Plugins.GameEvents/Consumers/TranslationsRefreshMessageConsumer.cs new file mode 100644 index 0000000..870f484 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.GameEvents/Consumers/TranslationsRefreshMessageConsumer.cs @@ -0,0 +1,27 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.ServiceBus; +using WingsAPI.Communication.Translations; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Managers; + +namespace WingsEmu.Plugins.GameEvents.Consumers +{ + public class TranslationsRefreshMessageConsumer : IMessageConsumer + { + private readonly IForbiddenNamesManager _forbiddenNamesManager; + private readonly IGameLanguageService _languageService; + + public TranslationsRefreshMessageConsumer(IGameLanguageService languageService, IForbiddenNamesManager forbiddenNamesManager) + { + _languageService = languageService; + _forbiddenNamesManager = forbiddenNamesManager; + } + + public async Task HandleAsync(TranslationsRefreshMessage notification, CancellationToken token) + { + await _forbiddenNamesManager.Reload(); + await _languageService.Reload(notification.IsFullReload); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.GameEvents/DataHolder/InstantBattleInstance.cs b/srcs/_plugins/WingsEmu.Plugins.GameEvents/DataHolder/InstantBattleInstance.cs new file mode 100644 index 0000000..59c1dc0 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.GameEvents/DataHolder/InstantBattleInstance.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using WingsEmu.Game.GameEvent; +using WingsEmu.Game.GameEvent.Configuration; +using WingsEmu.Game.Maps; +using WingsEmu.Plugins.GameEvents.Configuration.InstantBattle; + +namespace WingsEmu.Plugins.GameEvents.DataHolder +{ + public class InstantBattleInstance : IGameEventInstance + { + public InstantBattleInstance(IMapInstance mapInstance, InstantBattleConfiguration configuration) + { + MapInstance = mapInstance; + InternalConfiguration = configuration; + ClosingTimeWarnings = configuration.TimeLeftWarnings.ToList(); + AvailableWaves = configuration.Waves.Select(x => new InstantBattleInstanceWave(x)).ToList(); + StartDate = DateTime.UtcNow; + DestroyDate = StartDate + configuration.ClosingTime; + } + + public DateTime StartDate { get; } + public bool Finished { get; set; } + public List ClosingTimeWarnings { get; } + public List AvailableWaves { get; } + public InstantBattleConfiguration InternalConfiguration { get; } + public GameEventType GameEventType => GameEventType.InstantBattle; + public DateTime DestroyDate { get; set; } + public IMapInstance MapInstance { get; } + public IGameEventConfiguration Configuration => InternalConfiguration; + } + + public class InstantBattleInstanceWave + { + public InstantBattleInstanceWave(InstantBattleWave configuration) + { + Configuration = configuration; + MonsterSpawn = DateTime.UtcNow; + } + + public InstantBattleWave Configuration { get; } + + public bool PreWaveLongWarningDone { get; set; } + public bool PreWaveSoonWarningDone { get; set; } + public bool StartedWave { get; set; } + public DateTime MonsterSpawn { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.GameEvents/Event/Global/GameEvent.cs b/srcs/_plugins/WingsEmu.Plugins.GameEvents/Event/Global/GameEvent.cs new file mode 100644 index 0000000..3bde650 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.GameEvents/Event/Global/GameEvent.cs @@ -0,0 +1,12 @@ +using PhoenixLib.Events; +using WingsEmu.Game.GameEvent; + +namespace WingsEmu.Plugins.GameEvents.Event.Global +{ + public abstract class GameEvent : IAsyncEvent + { + protected GameEvent(GameEventType type) => Type = type; + + public GameEventType Type { get; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.GameEvents/Event/Global/GameEventLockRegistrationEvent.cs b/srcs/_plugins/WingsEmu.Plugins.GameEvents/Event/Global/GameEventLockRegistrationEvent.cs new file mode 100644 index 0000000..6855319 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.GameEvents/Event/Global/GameEventLockRegistrationEvent.cs @@ -0,0 +1,11 @@ +using WingsEmu.Game.GameEvent; + +namespace WingsEmu.Plugins.GameEvents.Event.Global +{ + public class GameEventLockRegistrationEvent : GameEvent + { + public GameEventLockRegistrationEvent(GameEventType type) : base(type) + { + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.GameEvents/Event/Global/GameEventMatchmakeEvent.cs b/srcs/_plugins/WingsEmu.Plugins.GameEvents/Event/Global/GameEventMatchmakeEvent.cs new file mode 100644 index 0000000..27e3a2d --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.GameEvents/Event/Global/GameEventMatchmakeEvent.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using WingsEmu.Game.GameEvent; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Plugins.GameEvents.Event.Global +{ + public class GameEventMatchmakeEvent : GameEvent + { + public GameEventMatchmakeEvent(GameEventType gameEventType, List sessions) : base(gameEventType) => Sessions = sessions; + + public List Sessions { get; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.GameEvents/Event/Global/GameEventPrepareEvent.cs b/srcs/_plugins/WingsEmu.Plugins.GameEvents/Event/Global/GameEventPrepareEvent.cs new file mode 100644 index 0000000..cb0f0a2 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.GameEvents/Event/Global/GameEventPrepareEvent.cs @@ -0,0 +1,13 @@ +using WingsEmu.Game.GameEvent; + +namespace WingsEmu.Plugins.GameEvents.Event.Global +{ + public class GameEventPrepareEvent : GameEvent + { + public GameEventPrepareEvent(GameEventType type) : base(type) + { + } + + public bool NoDelay { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.GameEvents/Event/Global/GameEventUnlockRegistrationEvent.cs b/srcs/_plugins/WingsEmu.Plugins.GameEvents/Event/Global/GameEventUnlockRegistrationEvent.cs new file mode 100644 index 0000000..80bc4c7 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.GameEvents/Event/Global/GameEventUnlockRegistrationEvent.cs @@ -0,0 +1,11 @@ +using WingsEmu.Game.GameEvent; + +namespace WingsEmu.Plugins.GameEvents.Event.Global +{ + public class GameEventUnlockRegistrationEvent : GameEvent + { + public GameEventUnlockRegistrationEvent(GameEventType type) : base(type) + { + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.GameEvents/Event/InstantBattle/InstantBattleDestroyEvent.cs b/srcs/_plugins/WingsEmu.Plugins.GameEvents/Event/InstantBattle/InstantBattleDestroyEvent.cs new file mode 100644 index 0000000..0f2dafe --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.GameEvents/Event/InstantBattle/InstantBattleDestroyEvent.cs @@ -0,0 +1,11 @@ +using WingsEmu.Plugins.GameEvents.DataHolder; + +namespace WingsEmu.Plugins.GameEvents.Event.InstantBattle +{ + public class InstantBattleDestroyEvent : InstantBattleEvent + { + public InstantBattleDestroyEvent(InstantBattleInstance instance) : base(instance) + { + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.GameEvents/Event/InstantBattle/InstantBattleDropEvent.cs b/srcs/_plugins/WingsEmu.Plugins.GameEvents/Event/InstantBattle/InstantBattleDropEvent.cs new file mode 100644 index 0000000..2602587 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.GameEvents/Event/InstantBattle/InstantBattleDropEvent.cs @@ -0,0 +1,12 @@ +using WingsEmu.Plugins.GameEvents.Configuration.InstantBattle; +using WingsEmu.Plugins.GameEvents.DataHolder; + +namespace WingsEmu.Plugins.GameEvents.Event.InstantBattle +{ + public class InstantBattleDropEvent : InstantBattleEvent + { + public InstantBattleDropEvent(InstantBattleInstance instance, InstantBattleWave wave) : base(instance) => Wave = wave; + + public InstantBattleWave Wave { get; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.GameEvents/Event/InstantBattle/InstantBattleEvent.cs b/srcs/_plugins/WingsEmu.Plugins.GameEvents/Event/InstantBattle/InstantBattleEvent.cs new file mode 100644 index 0000000..f1a27c7 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.GameEvents/Event/InstantBattle/InstantBattleEvent.cs @@ -0,0 +1,12 @@ +using PhoenixLib.Events; +using WingsEmu.Plugins.GameEvents.DataHolder; + +namespace WingsEmu.Plugins.GameEvents.Event.InstantBattle +{ + public abstract class InstantBattleEvent : IAsyncEvent + { + protected InstantBattleEvent(InstantBattleInstance instance) => Instance = instance; + + public InstantBattleInstance Instance { get; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.GameEvents/Event/InstantBattle/InstantBattleRewardEvent.cs b/srcs/_plugins/WingsEmu.Plugins.GameEvents/Event/InstantBattle/InstantBattleRewardEvent.cs new file mode 100644 index 0000000..27f312c --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.GameEvents/Event/InstantBattle/InstantBattleRewardEvent.cs @@ -0,0 +1,11 @@ +using WingsEmu.Plugins.GameEvents.DataHolder; + +namespace WingsEmu.Plugins.GameEvents.Event.InstantBattle +{ + public class InstantBattleCompleteEvent : InstantBattleEvent + { + public InstantBattleCompleteEvent(InstantBattleInstance instance) : base(instance) + { + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.GameEvents/Event/InstantBattle/InstantBattleStartWaveEvent.cs b/srcs/_plugins/WingsEmu.Plugins.GameEvents/Event/InstantBattle/InstantBattleStartWaveEvent.cs new file mode 100644 index 0000000..bcb2bcd --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.GameEvents/Event/InstantBattle/InstantBattleStartWaveEvent.cs @@ -0,0 +1,11 @@ +using WingsEmu.Plugins.GameEvents.DataHolder; + +namespace WingsEmu.Plugins.GameEvents.Event.InstantBattle +{ + public class InstantBattleStartWaveEvent : InstantBattleEvent + { + public InstantBattleStartWaveEvent(InstantBattleInstance instance, InstantBattleInstanceWave wave) : base(instance) => Wave = wave; + + public InstantBattleInstanceWave Wave { get; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.GameEvents/EventHandler/Global/GameEventLockRegistrationEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.GameEvents/EventHandler/Global/GameEventLockRegistrationEventHandler.cs new file mode 100644 index 0000000..88939db --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.GameEvents/EventHandler/Global/GameEventLockRegistrationEventHandler.cs @@ -0,0 +1,76 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using PhoenixLib.Logging; +using WingsEmu.Core.Generics; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.GameEvent; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums.Chat; +using WingsEmu.Plugins.GameEvents.Event.Global; + +namespace WingsEmu.Plugins.GameEvents.EventHandler.Global +{ + public class GameEventLockRegistrationEventHandler : IAsyncEventProcessor + { + private readonly IAsyncEventPipeline _eventPipeline; + private readonly IGameEventRegistrationManager _gameEventRegistrationManager; + private readonly IGameLanguageService _languageService; + private readonly ISessionManager _sessionManager; + + public GameEventLockRegistrationEventHandler(IGameEventRegistrationManager gameEventRegistrationManager, ISessionManager sessionManager, IGameLanguageService languageService, + IAsyncEventPipeline eventPipeline) + { + _gameEventRegistrationManager = gameEventRegistrationManager; + _sessionManager = sessionManager; + _languageService = languageService; + _eventPipeline = eventPipeline; + } + + public async Task HandleAsync(GameEventLockRegistrationEvent e, CancellationToken cancellation) + { + _gameEventRegistrationManager.RemoveGameEventRegistration(e.Type); + + GameDialogKey? gameEventKey = e.Type switch + { + GameEventType.InstantBattle => GameDialogKey.INSTANT_COMBAT_NAME, + _ => null + }; + + Log.Debug("[GameEvent] Locked Registration for Event: " + e.Type); + + _sessionManager.Broadcast(x => + { + string gameEventName = gameEventKey != null ? _languageService.GetLanguage(gameEventKey.Value, x.UserLanguage) : "?"; + string message = _languageService.GetLanguageFormat(GameDialogKey.GAMEEVENT_MESSAGE_START, x.UserLanguage, gameEventName); + return x.GenerateMsgPacket(message, MsgMessageType.BottomCard); + }); + + _sessionManager.Broadcast(x => x.GenerateEsfPacket(4)); + + ThreadSafeHashSet registeredCharacters = _gameEventRegistrationManager.GetAndRemoveCharactersByGameEventInclination(e.Type); + if (registeredCharacters == null) + { + return; + } + + var list = new List(); + + foreach (long character in registeredCharacters) + { + IClientSession session = _sessionManager.GetSessionByCharacterId(character); + if (session == null) + { + continue; + } + + list.Add(session); + } + + await _eventPipeline.ProcessEventAsync(new GameEventMatchmakeEvent(e.Type, list), cancellation); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.GameEvents/EventHandler/Global/GameEventMatchmakeEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.GameEvents/EventHandler/Global/GameEventMatchmakeEventHandler.cs new file mode 100644 index 0000000..87417f9 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.GameEvents/EventHandler/Global/GameEventMatchmakeEventHandler.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using PhoenixLib.Logging; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.GameEvent.Configuration; +using WingsEmu.Game.GameEvent.Event; +using WingsEmu.Game.GameEvent.Matchmaking; +using WingsEmu.Game.GameEvent.Matchmaking.Filter; +using WingsEmu.Game.GameEvent.Matchmaking.Result; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums.Chat; +using WingsEmu.Plugins.GameEvents.Event.Global; +using WingsEmu.Plugins.GameEvents.Matchmaking.Filter; + +namespace WingsEmu.Plugins.GameEvents.EventHandler.Global +{ + public class GameEventMatchmakeEventHandler : IAsyncEventProcessor + { + private readonly IAsyncEventPipeline _eventPipeline; + private readonly IGameLanguageService _languageService; + private readonly IMatchmaking _matchmaking; + + public GameEventMatchmakeEventHandler(IAsyncEventPipeline eventPipeline, IMatchmaking matchmaking, IGameLanguageService languageService) + { + _eventPipeline = eventPipeline; + _matchmaking = matchmaking; + _languageService = languageService; + } + + public async Task HandleAsync(GameEventMatchmakeEvent e, CancellationToken cancellation) + { + Log.Debug("[GameEvent] Processing matchmaking for Event: " + e.Type); + FilterResult filterResult = _matchmaking.Filter(e.Sessions, new InBaseMapFilter()); + TellToRefusedSessions(filterResult.RefusedSessions); + + IMatchmakingResult matchmakingResult = _matchmaking.Matchmake(filterResult.AcceptedSessions, e.Type); + + if (matchmakingResult == null) + { + return; + } + + await ProcessMatchmakingResult(matchmakingResult); + } + + private void TellToRefusedSessions(IEnumerable refusedSessions) + { + foreach (IClientSession session in refusedSessions) + { + session.SendMsg(_languageService.GetLanguage(GameDialogKey.GAMEEVENT_MESSAGE_NOT_IN_GENERAL_MAP, session.UserLanguage), MsgMessageType.SmallMiddle); + } + } + + private void TellToRefusedSessions(Dictionary> refusedSessions) + { + foreach ((GameDialogKey gameDialogKey, List clientSessions) in refusedSessions) + { + foreach (IClientSession session in clientSessions) + { + session.SendMsg(_languageService.GetLanguage(gameDialogKey, session.UserLanguage), MsgMessageType.SmallMiddle); + } + } + } + + private async Task ProcessMatchmakingResult(IMatchmakingResult matchmakingResult) + { + foreach (Tuple> pair in matchmakingResult.Sessions) + { + await ProcessGameEventStart(pair); + } + + TellToRefusedSessions(matchmakingResult.RefusedSessions); + } + + private async Task ProcessGameEventStart(Tuple> pair) + { + (IGameEventConfiguration gameEventConfiguration, List clientSessions) = pair; + + await _eventPipeline.ProcessEventAsync(new GameEventInstanceStartEvent(clientSessions, gameEventConfiguration)); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.GameEvents/EventHandler/Global/GameEventPrepareEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.GameEvents/EventHandler/Global/GameEventPrepareEventHandler.cs new file mode 100644 index 0000000..972614e --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.GameEvents/EventHandler/Global/GameEventPrepareEventHandler.cs @@ -0,0 +1,121 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using PhoenixLib.Logging; +using PhoenixLib.Scheduler; +using WingsAPI.Communication.ServerApi.Protocol; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.GameEvent; +using WingsEmu.Game.Managers; +using WingsEmu.Packets.Enums.Chat; +using WingsEmu.Plugins.GameEvents.Event.Global; + +namespace WingsEmu.Plugins.GameEvents.EventHandler.Global +{ + public class GameEventPrepareEventHandler : IAsyncEventProcessor + { + private static readonly Step[] Steps = + { + new(new TimeSpan(0, 0, 0), 5, false), + new(new TimeSpan(0, 2, 0), 3, false), + new(new TimeSpan(0, 4, 0), 1, false), + new(new TimeSpan(0, 4, 30), 30, true), + new(new TimeSpan(0, 4, 50), 10, true) + }; + + private static readonly TimeSpan UnlockDelayAfterLastStep = TimeSpan.FromSeconds(10); + private readonly IAsyncEventPipeline _eventPipeline; + private readonly IGameLanguageService _gameLanguageService; + private readonly IScheduler _scheduler; + + private readonly SerializableGameServer _serializableGameServer; + private readonly ISessionManager _sessionManager; + + public GameEventPrepareEventHandler(ISessionManager sessionManager, IGameLanguageService gameLanguageService, IScheduler scheduler, IAsyncEventPipeline eventPipeline, + SerializableGameServer serializableGameServer) + { + _sessionManager = sessionManager; + _gameLanguageService = gameLanguageService; + _scheduler = scheduler; + _eventPipeline = eventPipeline; + _serializableGameServer = serializableGameServer; + } + + public async Task HandleAsync(GameEventPrepareEvent e, CancellationToken cancellation) + { + if (_serializableGameServer.ChannelType == GameChannelType.ACT_4) + { + return; + } + + GameEventType type = e.Type; + Log.Debug("[GameEvent] Preparing Event: " + type); + + if (e.NoDelay) + { + switch (type) + { + case GameEventType.InstantBattle: + await _eventPipeline.ProcessEventAsync(new GameEventUnlockRegistrationEvent(type)); + break; + } + + return; + } + + for (int i = 0; i < Steps.Length; i++) + { + Step step = Steps[i]; + + int local = i; + GameDialogKey isSecMessage = step.IsSecond ? GameDialogKey.GAMEEVENT_SHOUTMESSAGE_PREPARATION_SECONDS : GameDialogKey.GAMEEVENT_SHOUTMESSAGE_PREPARATION_MINUTES; + + _scheduler.Schedule(step.Delay, () => + { + switch (type) + { + case GameEventType.InstantBattle: + _sessionManager.BroadcastAsync(async x => + { + string instantBattleName = _gameLanguageService.GetLanguage(GameDialogKey.INSTANT_COMBAT_NAME, x.UserLanguage); + string message = _gameLanguageService.GetLanguageFormat(isSecMessage, x.UserLanguage, instantBattleName, step.DisplayTime); + + return x.GenerateMsgPacket(message, MsgMessageType.Middle); + }); + + _sessionManager.BroadcastAsync(async x => + { + string instantBattleName = _gameLanguageService.GetLanguage(GameDialogKey.INSTANT_COMBAT_NAME, x.UserLanguage); + string message = _gameLanguageService.GetLanguageFormat(isSecMessage, x.UserLanguage, instantBattleName, step.DisplayTime); + + return x.GenerateMsgPacket(string.Format(message, step.DisplayTime), MsgMessageType.BottomCard); + }); + + if (local == (Steps.Length - 1)) + { + _scheduler.Schedule(UnlockDelayAfterLastStep, async () => await _eventPipeline.ProcessEventAsync(new GameEventUnlockRegistrationEvent(e.Type))); + } + + break; + } + }); + } + } + + private class Step + { + public Step(TimeSpan delay, int displayTime, bool isSecond) + { + Delay = delay; + DisplayTime = displayTime; + IsSecond = isSecond; + } + + public TimeSpan Delay { get; } + public int DisplayTime { get; } + public bool IsSecond { get; } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.GameEvents/EventHandler/Global/GameEventUnlockRegistrationEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.GameEvents/EventHandler/Global/GameEventUnlockRegistrationEventHandler.cs new file mode 100644 index 0000000..c7ed83a --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.GameEvents/EventHandler/Global/GameEventUnlockRegistrationEventHandler.cs @@ -0,0 +1,55 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using PhoenixLib.Logging; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.GameEvent; +using WingsEmu.Game.GameEvent.Configuration; +using WingsEmu.Game.Managers; +using WingsEmu.Plugins.GameEvents.Configuration.InstantBattle; +using WingsEmu.Plugins.GameEvents.Event.Global; + +namespace WingsEmu.Plugins.GameEvents.EventHandler.Global +{ + public class GameEventUnlockRegistrationEventHandler : IAsyncEventProcessor + { + private static readonly TimeSpan RegistrationTime = TimeSpan.FromSeconds(15); + private readonly IGameEventRegistrationManager _gameEventRegistrationManager; + private readonly IGlobalInstantBattleConfiguration _instantBattleConfiguration; + private readonly ISessionManager _sessionManager; + + public GameEventUnlockRegistrationEventHandler(ISessionManager sessionManager, IGameEventRegistrationManager gameEventRegistrationManager, + IGlobalInstantBattleConfiguration instantBattleConfiguration) + { + _sessionManager = sessionManager; + _gameEventRegistrationManager = gameEventRegistrationManager; + _instantBattleConfiguration = instantBattleConfiguration; + } + + public async Task HandleAsync(GameEventUnlockRegistrationEvent e, CancellationToken cancellation) + { + DateTime currentTime = DateTime.UtcNow; + if (!_gameEventRegistrationManager.AddGameEventRegistration(e.Type, currentTime, currentTime + RegistrationTime)) + { + Log.Debug($"[GameEvent] Already Unlocked Registration (cancelling re-unlocking event) for Event: {e.Type.ToString()}"); + return; + } + + Log.Debug($"[GameEvent] Unlocked Registration for Event: {e.Type.ToString()}"); + + IGlobalGameEventConfiguration gameEventConfiguration; + switch (e.Type) + { + case GameEventType.InstantBattle: + gameEventConfiguration = _instantBattleConfiguration; + break; + default: + Log.Debug($"[GameEvent] GameEventConfiguration not defined for: {e.Type.ToString()} | In: {GetType().Name}"); + return; + } + + _sessionManager.BroadcastGameEventAsk(e.Type, gameEventConfiguration); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.GameEvents/EventHandler/InstantBattle/GameEventInstanceProcessEventInstantBattleHandler.cs b/srcs/_plugins/WingsEmu.Plugins.GameEvents/EventHandler/InstantBattle/GameEventInstanceProcessEventInstantBattleHandler.cs new file mode 100644 index 0000000..aa3c218 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.GameEvents/EventHandler/InstantBattle/GameEventInstanceProcessEventInstantBattleHandler.cs @@ -0,0 +1,145 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.GameEvent; +using WingsEmu.Game.GameEvent.Event; +using WingsEmu.Packets.Enums.Chat; +using WingsEmu.Plugins.GameEvents.Configuration.InstantBattle; +using WingsEmu.Plugins.GameEvents.DataHolder; +using WingsEmu.Plugins.GameEvents.Event.InstantBattle; + +namespace WingsEmu.Plugins.GameEvents.EventHandler.InstantBattle +{ + public class GameEventInstanceProcessEventInstantBattleHandler : IAsyncEventProcessor + { + private readonly IAsyncEventPipeline _asyncEventPipeline; + private readonly IGameEventInstanceManager _gameEventInstanceManager; + private readonly IGameLanguageService _languageService; + + public GameEventInstanceProcessEventInstantBattleHandler(IGameEventInstanceManager gameEventInstanceManager, IAsyncEventPipeline asyncEventPipeline, IGameLanguageService languageService) + { + _gameEventInstanceManager = gameEventInstanceManager; + _asyncEventPipeline = asyncEventPipeline; + _languageService = languageService; + } + + public async Task HandleAsync(GameEventInstanceProcessEvent e, CancellationToken cancellation) + { + IReadOnlyCollection gameEventInstances = _gameEventInstanceManager.GetGameEventsByType(GameEventType.InstantBattle); + if (gameEventInstances == null || gameEventInstances.Count < 1) + { + return; + } + + DateTime currentTime = e.CurrentTime; + + foreach (IGameEventInstance gameEventInstance in gameEventInstances.ToArray()) + { + var instantBattleInstance = (InstantBattleInstance)gameEventInstance; + await ProcessGameEventInstance(instantBattleInstance, currentTime); + } + } + + private async Task ProcessGameEventInstance(InstantBattleInstance gameEventInstance, DateTime currentTime) + { + if (gameEventInstance.DestroyDate < currentTime || gameEventInstance.MapInstance.Sessions.Count < 1) + { + await _asyncEventPipeline.ProcessEventAsync(new InstantBattleDestroyEvent(gameEventInstance)); + return; + } + + await ProcessWarnings(gameEventInstance, currentTime); + await ProcessWaves(gameEventInstance, currentTime); + } + + private async Task ProcessWarnings(InstantBattleInstance gameEventInstance, DateTime currentTime) + { + if (gameEventInstance.ClosingTimeWarnings.Count < 1) + { + return; + } + + TimeSpan currentWarning = gameEventInstance.ClosingTimeWarnings.First(); + + if (currentTime < gameEventInstance.StartDate + currentWarning) + { + return; + } + + TimeSpan timeLeft = gameEventInstance.DestroyDate - currentTime; + gameEventInstance.ClosingTimeWarnings.Remove(currentWarning); + bool isSeconds = timeLeft.TotalMinutes < 1; + GameDialogKey key = isSeconds ? GameDialogKey.INSTANT_COMBAT_SHOUTMESSAGE_SECONDS_REMAINING : GameDialogKey.INSTANT_COMBAT_SHOUTMESSAGE_MINUTES_REMAINING; + + gameEventInstance.MapInstance.Broadcast(x => + x.GenerateMsgPacket(_languageService.GetLanguageFormat(key, x.UserLanguage, (isSeconds ? timeLeft.Seconds : timeLeft.Minutes).ToString()), MsgMessageType.Middle)); + } + + private async Task ProcessWaves(InstantBattleInstance gameEventInstance, DateTime currentTime) + { + InstantBattleConfiguration configuration = gameEventInstance.InternalConfiguration; + if (gameEventInstance.AvailableWaves.Count < 1) + { + return; + } + + InstantBattleInstanceWave prioritizedWave = gameEventInstance.AvailableWaves.First(); + DateTime waveStartDate = gameEventInstance.StartDate + prioritizedWave.Configuration.TimeStart; + DateTime waveEndDate = gameEventInstance.StartDate + prioritizedWave.Configuration.TimeEnd; + + if (!prioritizedWave.PreWaveLongWarningDone && waveStartDate - configuration.PreWaveLongWarningTime < currentTime) + { + prioritizedWave.PreWaveLongWarningDone = true; + + gameEventInstance.MapInstance.Broadcast(x => + x.GenerateMsgPacket( + _languageService.GetLanguageFormat(GameDialogKey.INSTANT_COMBAT_SHOUTMESSAGE_WAVE_SECONDS_REMAINING, x.UserLanguage, configuration.PreWaveLongWarningTime.TotalSeconds), + MsgMessageType.Middle)); + } + + if (!prioritizedWave.PreWaveSoonWarningDone && waveStartDate - configuration.PreWaveSoonWarningTime < currentTime) + { + prioritizedWave.PreWaveSoonWarningDone = true; + + gameEventInstance.MapInstance.Broadcast(x => + x.GenerateMsgPacket(_languageService.GetLanguage(GameDialogKey.INSTANT_COMBAT_SHOUTMESSAGE_MONSTERS_INCOMING, x.UserLanguage), MsgMessageType.Middle)); + } + + if (!prioritizedWave.StartedWave && waveStartDate < currentTime) + { + prioritizedWave.StartedWave = true; + + await _asyncEventPipeline.ProcessEventAsync(new InstantBattleStartWaveEvent(gameEventInstance, prioritizedWave)); + } + + int currentInstantMonsters = gameEventInstance.MapInstance.GetAliveMonsters(x => x.IsInstantBattle).Count; + + if (prioritizedWave.StartedWave && waveEndDate < currentTime && currentInstantMonsters > 0) + { + gameEventInstance.MapInstance.Broadcast(x => + x.GenerateMsgPacket(_languageService.GetLanguage(GameDialogKey.INSTANT_COMBAT_SHOUTMESSAGE_WAVE_FAILED, x.UserLanguage), MsgMessageType.Middle)); + } + + if (prioritizedWave.StartedWave && gameEventInstance.AvailableWaves.Count > 0 && waveEndDate < currentTime) + { + gameEventInstance.AvailableWaves.Remove(prioritizedWave); + await _asyncEventPipeline.ProcessEventAsync(new InstantBattleDropEvent(gameEventInstance, prioritizedWave.Configuration)); + return; + } + + // Add some delay, because server needs to process monster spawn + if (prioritizedWave.StartedWave && prioritizedWave.MonsterSpawn.AddSeconds(5) < currentTime && gameEventInstance.AvailableWaves.Count == 1 + && !gameEventInstance.Finished && currentInstantMonsters < 1) + { + gameEventInstance.AvailableWaves.Remove(prioritizedWave); + await _asyncEventPipeline.ProcessEventAsync(new InstantBattleDropEvent(gameEventInstance, prioritizedWave.Configuration)); + await _asyncEventPipeline.ProcessEventAsync(new InstantBattleCompleteEvent(gameEventInstance)); + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.GameEvents/EventHandler/InstantBattle/GameEventInstanceStartEventInstantBattleHandler.cs b/srcs/_plugins/WingsEmu.Plugins.GameEvents/EventHandler/InstantBattle/GameEventInstanceStartEventInstantBattleHandler.cs new file mode 100644 index 0000000..ffbe947 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.GameEvents/EventHandler/InstantBattle/GameEventInstanceStartEventInstantBattleHandler.cs @@ -0,0 +1,50 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using PhoenixLib.Logging; +using WingsEmu.Game.GameEvent; +using WingsEmu.Game.GameEvent.Event; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Networking; +using WingsEmu.Plugins.GameEvents.Configuration.InstantBattle; +using WingsEmu.Plugins.GameEvents.DataHolder; + +namespace WingsEmu.Plugins.GameEvents.EventHandler.InstantBattle +{ + public class GameEventInstanceStartEventInstantBattleHandler : IAsyncEventProcessor + { + private readonly IGameEventInstanceManager _gameEventInstanceManager; + private readonly IMapManager _mapManager; + + public GameEventInstanceStartEventInstantBattleHandler(IGameEventInstanceManager gameEventInstanceManager, IMapManager mapManager) + { + _gameEventInstanceManager = gameEventInstanceManager; + _mapManager = mapManager; + } + + public async Task HandleAsync(GameEventInstanceStartEvent e, CancellationToken cancellation) + { + if (e.GameEventConfiguration.GameEventType != GameEventType.InstantBattle) + { + return; + } + + IMapInstance mapInstance = _mapManager.GenerateMapInstanceByMapId(e.GameEventConfiguration.MapId, e.GameEventConfiguration.MapInstanceType); + if (mapInstance == null) + { + Log.Warn($"[INSTANT_BATTLE_GAME_EVENT] Couldn't generate the desired map with mapId: '{e.GameEventConfiguration.MapId.ToString()}'"); + } + + var instance = new InstantBattleInstance(mapInstance, (InstantBattleConfiguration)e.GameEventConfiguration); + + Log.Debug($"[GameEvent] Teleporting {e.Sessions.Count().ToString()} users"); + foreach (IClientSession session in e.Sessions) + { + await _mapManager.TeleportOnRandomPlaceInMapAsync(session, instance.MapInstance); + } + + _gameEventInstanceManager.AddGameEvent(instance); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.GameEvents/EventHandler/InstantBattle/InstantBattleCompleteEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.GameEvents/EventHandler/InstantBattle/InstantBattleCompleteEventHandler.cs new file mode 100644 index 0000000..2b4ceff --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.GameEvents/EventHandler/InstantBattle/InstantBattleCompleteEventHandler.cs @@ -0,0 +1,106 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsAPI.Game.Extensions.PacketGeneration; +using WingsEmu.Game; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Families.Enum; +using WingsEmu.Game.Families.Event; +using WingsEmu.Game.GameEvent.InstantBattle; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Portals; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; +using WingsEmu.Plugins.GameEvents.Configuration.InstantBattle; +using WingsEmu.Plugins.GameEvents.DataHolder; +using WingsEmu.Plugins.GameEvents.Event.InstantBattle; + +namespace WingsEmu.Plugins.GameEvents.EventHandler.InstantBattle +{ + public class InstantBattleCompleteEventHandler : IAsyncEventProcessor + { + private readonly IGameLanguageService _gameLanguage; + private readonly GameMinMaxConfiguration _minMaxConfiguration; + private readonly IPortalFactory _portalFactory; + private readonly IRankingManager _rankingManager; + private readonly IReputationConfiguration _reputationConfiguration; + + public InstantBattleCompleteEventHandler(IGameLanguageService gameLanguage, GameMinMaxConfiguration minMaxConfiguration, IReputationConfiguration reputationConfiguration, + IPortalFactory portalFactory, IRankingManager rankingManager) + { + _gameLanguage = gameLanguage; + _minMaxConfiguration = minMaxConfiguration; + _reputationConfiguration = reputationConfiguration; + _portalFactory = portalFactory; + _rankingManager = rankingManager; + } + + public async Task HandleAsync(InstantBattleCompleteEvent e, CancellationToken cancellation) + { + IMapInstance map = e.Instance.MapInstance; + InstantBattleReward reward = e.Instance.InternalConfiguration.Reward; + InstantBattleInstance instance = e.Instance; + + e.Instance.Finished = true; + + await map.BroadcastAsync(async x => + { + string message = _gameLanguage.GetLanguage(GameDialogKey.INSTANT_COMBAT_SHOUTMESSAGE_SUCCEEDED, x.UserLanguage); + return x.GenerateMsgPacket(message, MsgMessageType.Middle); + }); + + foreach (IClientSession session in map.Sessions) + { + IPlayerEntity character = session.PlayerEntity; + bool isHeroic = 0 < instance.InternalConfiguration.Requirements.HeroicLevel?.Minimum; + + long gold = reward.GoldMultiplier * (isHeroic ? character.HeroLevel : character.Level); + long reputation = reward.ReputationMultiplier * (isHeroic ? character.HeroLevel : character.Level); + int specialistPoint = reward.SpPointsMultiplier * (isHeroic ? character.HeroLevel : character.Level); + int familyExperience = reward.FamilyExperience; + int dignity = reward.Dignity; + + await session.EmitEventAsync(new InstantBattleWonEvent()); + await session.EmitEventAsync(new GenerateGoldEvent(gold)); + + await session.EmitEventAsync(new GenerateReputationEvent + { + Amount = (int)reputation, + SendMessage = true + }); + + character.SpPointsBonus += specialistPoint; + if (character.Family != null) + { + await character.Session.EmitEventAsync(new FamilyAddExperienceEvent(familyExperience, FamXpObtainedFromType.InstantCombat)); + } + + character.AddDignity(dignity, _minMaxConfiguration, _gameLanguage, _reputationConfiguration, _rankingManager.TopReputation); + session.SendChatMessage(session.GetLanguageFormat(GameDialogKey.INFORMATION_CHATMESSAGE_WIN_SP_POINT, specialistPoint), ChatMessageColorType.Green); + + character.Hp = session.PlayerEntity.MaxHp; + character.Mp = session.PlayerEntity.MaxMp; + + if (character.SpPointsBonus > _minMaxConfiguration.MaxSpAdditionalPoints) + { + character.SpPointsBonus = _minMaxConfiguration.MaxSpAdditionalPoints; + } + + session.RefreshSpPoint(); + session.RefreshStat(); + session.RefreshStatInfo(); + } + + var pos = new Position(e.Instance.InternalConfiguration.ReturnPortalX, e.Instance.InternalConfiguration.ReturnPortalY); + IPortalEntity portal = _portalFactory.CreatePortal(PortalType.TSNormal, map, pos, map, pos); + map.AddPortalToMap(portal); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.GameEvents/EventHandler/InstantBattle/InstantBattleDestroyEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.GameEvents/EventHandler/InstantBattle/InstantBattleDestroyEventHandler.cs new file mode 100644 index 0000000..c5e1844 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.GameEvents/EventHandler/InstantBattle/InstantBattleDestroyEventHandler.cs @@ -0,0 +1,35 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.GameEvent; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Networking; +using WingsEmu.Plugins.GameEvents.Event.InstantBattle; + +namespace WingsEmu.Plugins.GameEvents.EventHandler.InstantBattle +{ + public class InstantBattleDestroyEventHandler : IAsyncEventProcessor + { + private readonly IGameEventInstanceManager _gameEventInstanceManager; + private readonly IMapManager _mapManager; + + public InstantBattleDestroyEventHandler(IMapManager mapManager, IGameEventInstanceManager gameEventInstanceManager) + { + _mapManager = mapManager; + _gameEventInstanceManager = gameEventInstanceManager; + } + + public async Task HandleAsync(InstantBattleDestroyEvent e, CancellationToken cancellation) + { + foreach (IClientSession session in e.Instance.MapInstance.Sessions.ToList()) + { + session.ChangeToLastBaseMap(); + } + + _gameEventInstanceManager.RemoveGameEvent(e.Instance); + _mapManager.RemoveMapInstance(e.Instance.MapInstance.Id); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.GameEvents/EventHandler/InstantBattle/InstantBattleDropEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.GameEvents/EventHandler/InstantBattle/InstantBattleDropEventHandler.cs new file mode 100644 index 0000000..ec68d29 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.GameEvents/EventHandler/InstantBattle/InstantBattleDropEventHandler.cs @@ -0,0 +1,46 @@ +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using PhoenixLib.Scheduler; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Inventory.Event; +using WingsEmu.Game.Maps; +using WingsEmu.Plugins.GameEvents.Configuration.InstantBattle; +using WingsEmu.Plugins.GameEvents.Event.InstantBattle; + +namespace WingsEmu.Plugins.GameEvents.InstantBattle +{ + public class InstantBattleDropEventHandler : IAsyncEventProcessor + { + private readonly IAsyncEventPipeline _eventPipeline; + private readonly IGameLanguageService _gameLanguage; + private readonly IScheduler _scheduler; + + public InstantBattleDropEventHandler(IAsyncEventPipeline eventPipeline, IGameLanguageService gameLanguage, IScheduler scheduler) + { + _eventPipeline = eventPipeline; + _gameLanguage = gameLanguage; + _scheduler = scheduler; + } + + public async Task HandleAsync(InstantBattleDropEvent e, CancellationToken cancellation) + { + IMapInstance map = e.Instance.MapInstance; + + if (0 < e.Instance.MapInstance.GetAliveMonsters(x => x.IsInstantBattle).Count || e.Wave.Drops == null || e.Wave.Drops.Count < 1) + { + return; + } + + foreach (InstantBattleDrop drop in e.Wave.Drops) + { + for (int i = 0; i < drop.BunchCount; i++) + { + Position position = map.GetRandomPosition(); + await _eventPipeline.ProcessEventAsync(new DropMapItemEvent(map, position, drop.ItemVnum, drop.AmountPerBunch), cancellation); + } + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.GameEvents/EventHandler/InstantBattle/InstantBattleStartWaveEventHandler.cs b/srcs/_plugins/WingsEmu.Plugins.GameEvents/EventHandler/InstantBattle/InstantBattleStartWaveEventHandler.cs new file mode 100644 index 0000000..840d783 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.GameEvents/EventHandler/InstantBattle/InstantBattleStartWaveEventHandler.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Monster.Event; +using WingsEmu.Packets.Enums.Chat; +using WingsEmu.Plugins.GameEvents.Configuration.InstantBattle; +using WingsEmu.Plugins.GameEvents.DataHolder; +using WingsEmu.Plugins.GameEvents.Event.InstantBattle; + +namespace WingsEmu.Plugins.GameEvents.InstantBattle +{ + public class InstantBattleStartWaveEventHandler : IAsyncEventProcessor + { + private readonly IAsyncEventPipeline _eventPipeline; + + public InstantBattleStartWaveEventHandler(IAsyncEventPipeline eventPipeline) => _eventPipeline = eventPipeline; + + public async Task HandleAsync(InstantBattleStartWaveEvent e, CancellationToken cancellation) + { + IMapInstance map = e.Instance.MapInstance; + InstantBattleInstanceWave wave = e.Wave; + + if (Enum.TryParse(wave.Configuration.TitleKey, out GameDialogKey key)) + { + map.Broadcast(x => x.GenerateMsgPacket(x.GetLanguage(key), MsgMessageType.Middle)); + } + + var summons = new List(); + foreach (InstantBattleMonster monster in wave.Configuration.Monsters) + { + for (int i = 0; i < monster.Amount; i++) + { + Position position = map.GetRandomPosition(); + var summon = new ToSummon + { + VNum = monster.MonsterVnum, + SpawnCell = position, + IsMoving = true, + IsHostile = true, + IsInstantBattle = true + }; + + summons.Add(summon); + } + } + + wave.MonsterSpawn = DateTime.UtcNow; + await _eventPipeline.ProcessEventAsync(new MonsterSummonEvent(map, summons), cancellation); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.GameEvents/GameEventInstanceManager.cs b/srcs/_plugins/WingsEmu.Plugins.GameEvents/GameEventInstanceManager.cs new file mode 100644 index 0000000..12693e2 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.GameEvents/GameEventInstanceManager.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using WingsEmu.Game.GameEvent; + +namespace WingsEmu.Plugins.GameEvents +{ + public class GameEventInstanceManager : IGameEventInstanceManager + { + private readonly Dictionary> _gameEventInstances = new(); + + public IReadOnlyCollection GetGameEventsByType(GameEventType gameEventType) => + _gameEventInstances.TryGetValue(gameEventType, out List gameEventInstance) ? gameEventInstance : null; + + public void AddGameEvent(IGameEventInstance gameEventInstance) + { + if (_gameEventInstances.TryGetValue(gameEventInstance.GameEventType, out List gameEventInstances)) + { + gameEventInstances.Add(gameEventInstance); + return; + } + + _gameEventInstances[gameEventInstance.GameEventType] = new List + { + gameEventInstance + }; + } + + public void RemoveGameEvent(IGameEventInstance gameEventInstance) + { + if (_gameEventInstances.TryGetValue(gameEventInstance.GameEventType, out List gameEventInstances)) + { + gameEventInstances.Remove(gameEventInstance); + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.GameEvents/GameEventRegistrationManager.cs b/srcs/_plugins/WingsEmu.Plugins.GameEvents/GameEventRegistrationManager.cs new file mode 100644 index 0000000..6e589c9 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.GameEvents/GameEventRegistrationManager.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using WingsEmu.Core.Generics; +using WingsEmu.Game.GameEvent; + +namespace WingsEmu.Plugins.GameEvents +{ + public class GameEventRegistrationManager : IGameEventRegistrationManager + { + private readonly ConcurrentDictionary> _characterIdsByGameEventInclination = new(); + private readonly ConcurrentDictionary _gameEventRegistrations = new(); + private readonly ConcurrentDictionary _gameEventTypeByCharacterId = new(); + + public IReadOnlyCollection> GameEventRegistrations => _gameEventRegistrations; + + public bool AddGameEventRegistration(GameEventType gameEventType, DateTime currentTime, DateTime expiryDate) + { + if (_gameEventRegistrations.TryGetValue(gameEventType, out DateTime unlockDate)) + { + if (unlockDate < currentTime) + { + return false; + } + } + + _gameEventRegistrations[gameEventType] = expiryDate; + return true; + } + + public bool IsGameEventRegistrationOpen(GameEventType gameEventType, DateTime currentTime) => + _gameEventRegistrations.TryGetValue(gameEventType, out DateTime unlockDate) && currentTime < unlockDate; + + public void RemoveGameEventRegistration(GameEventType gameEventType) + { + _gameEventRegistrations[gameEventType] = DateTime.MaxValue; + } + + public void SetCharacterGameEventInclination(long id, GameEventType gameEventType) + { + if (_gameEventTypeByCharacterId.TryGetValue(id, out GameEventType registeredGameEventType)) + { + _characterIdsByGameEventInclination[registeredGameEventType].Remove(id); + } + + if (_characterIdsByGameEventInclination.TryGetValue(gameEventType, out ThreadSafeHashSet list)) + { + list.Add(id); + } + else + { + _characterIdsByGameEventInclination.TryAdd(gameEventType, new ThreadSafeHashSet + { + id + }); + } + + if (!_gameEventTypeByCharacterId.TryAdd(id, gameEventType)) + { + _gameEventTypeByCharacterId[id] = gameEventType; + } + } + + public ThreadSafeHashSet GetAndRemoveCharactersByGameEventInclination(GameEventType gameEventType) + { + ThreadSafeHashSet valueToReturn = _characterIdsByGameEventInclination.TryGetValue(gameEventType, out ThreadSafeHashSet characterIds) ? characterIds : null; + RemoveCharactersByGameEventInclination(gameEventType, valueToReturn); + return valueToReturn; + } + + public void RemoveCharactersByGameEventInclination(GameEventType gameEventType) + { + RemoveCharactersByGameEventInclination(gameEventType, _characterIdsByGameEventInclination.TryGetValue(gameEventType, out ThreadSafeHashSet characterIds) ? characterIds : null); + } + + private void RemoveCharactersByGameEventInclination(GameEventType gameEventType, ThreadSafeHashSet characters) + { + _characterIdsByGameEventInclination.TryRemove(gameEventType, out _); + + if (characters == null) + { + return; + } + + foreach (long character in characters) + { + _gameEventTypeByCharacterId.TryRemove(character, out _); + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.GameEvents/GameEventsPlugin.cs b/srcs/_plugins/WingsEmu.Plugins.GameEvents/GameEventsPlugin.cs new file mode 100644 index 0000000..b2f55ab --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.GameEvents/GameEventsPlugin.cs @@ -0,0 +1,20 @@ +using WingsAPI.Plugins; +using WingsEmu.Commands.Interfaces; +using WingsEmu.Plugins.GameEvents.CommandModules; + +namespace WingsEmu.Plugins.GameEvents +{ + public class GameEventsPlugin : IGamePlugin + { + private readonly ICommandContainer _commandContainer; + + public GameEventsPlugin(ICommandContainer commandContainer) => _commandContainer = commandContainer; + + public string Name => nameof(GameEventsPlugin); + + public void OnLoad() + { + _commandContainer.AddModule(); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.GameEvents/GameEventsPluginCore.cs b/srcs/_plugins/WingsEmu.Plugins.GameEvents/GameEventsPluginCore.cs new file mode 100644 index 0000000..d36a440 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.GameEvents/GameEventsPluginCore.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using PhoenixLib.Configuration; +using PhoenixLib.Events; +using PhoenixLib.ServiceBus.Extensions; +using WingsAPI.Communication.Compliments; +using WingsAPI.Communication.InstantBattle; +using WingsAPI.Communication.Miniland; +using WingsAPI.Communication.Player; +using WingsAPI.Communication.Quests; +using WingsAPI.Communication.Raid; +using WingsAPI.Communication.ServerApi.Protocol; +using WingsAPI.Communication.Translations; +using WingsAPI.Plugins; +using WingsEmu.Game.GameEvent; +using WingsEmu.Game.GameEvent.Matchmaking; +using WingsEmu.Game.GameEvent.Matchmaking.Matchmaker; +using WingsEmu.Plugins.GameEvents.Configuration.InstantBattle; +using WingsEmu.Plugins.GameEvents.Consumers; +using WingsEmu.Plugins.GameEvents.Matchmaking.Matchmaker; +using WingsEmu.Plugins.GameEvents.RecurrentJob; + +namespace WingsEmu.Plugins.GameEvents +{ + public class GameEventsPluginCore : IGameServerPlugin + { + public string Name => nameof(GameEventsPluginCore); + + public void AddDependencies(IServiceCollection services, GameServerLoader gameServer) + { + services.AddEventHandlersInAssembly(); + + services.AddMessagePublisher(); + services.AddMessageSubscriber(); + services.AddMessageSubscriber(); + services.AddMessageSubscriber(); + services.AddMessageSubscriber(); + services.AddMessageSubscriber(); + services.AddMessageSubscriber(); + services.AddMessageSubscriber(); + services.AddMessagePublisher(); + services.AddMessageSubscriber(); + + services.AddSingleton(); + services.AddSingleton(); + + if (gameServer.Type != GameChannelType.ACT_4) + { + services.AddHostedService(); + } + + services.AddConfigurationsFromDirectory("gameevents/instant_combats"); + services.AddSingleton(s => new GlobalInstantBattleConfiguration + { + Configurations = s.GetRequiredService>().ToList() + }); + services.AddSingleton(s => new Matchmaking.Matchmaking(new Dictionary + { + [GameEventType.InstantBattle] = new InstantBattleMatchmaker(s.GetService()) + })); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.GameEvents/Matchmaking/Filter/InBaseMapFilter.cs b/srcs/_plugins/WingsEmu.Plugins.GameEvents/Matchmaking/Filter/InBaseMapFilter.cs new file mode 100644 index 0000000..4602ceb --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.GameEvents/Matchmaking/Filter/InBaseMapFilter.cs @@ -0,0 +1,11 @@ +using WingsEmu.DTOs.Maps; +using WingsEmu.Game.GameEvent.Matchmaking.Filter; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Plugins.GameEvents.Matchmaking.Filter +{ + public class InBaseMapFilter : IMatchmakingFilter + { + public bool IsAccepted(IClientSession session) => session.PlayerEntity.MapInstance != null && session.PlayerEntity.MapInstance.HasMapFlag(MapFlags.IS_BASE_MAP); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.GameEvents/Matchmaking/Matchmaker/InstantBattleMatchmaker.cs b/srcs/_plugins/WingsEmu.Plugins.GameEvents/Matchmaking/Matchmaker/InstantBattleMatchmaker.cs new file mode 100644 index 0000000..bcf239a --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.GameEvents/Matchmaking/Matchmaker/InstantBattleMatchmaker.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using WingsEmu.Game._i18n; +using WingsEmu.Game.GameEvent.Configuration; +using WingsEmu.Game.GameEvent.Matchmaking.Matchmaker; +using WingsEmu.Game.GameEvent.Matchmaking.Result; +using WingsEmu.Game.Networking; +using WingsEmu.Plugins.GameEvents.Configuration.InstantBattle; +using WingsEmu.Plugins.GameEvents.Matchmaking.Result; + +namespace WingsEmu.Plugins.GameEvents.Matchmaking.Matchmaker +{ + public class InstantBattleMatchmaker : IMatchmaker + { + private readonly IGlobalInstantBattleConfiguration _configuration; + + public InstantBattleMatchmaker(IGlobalInstantBattleConfiguration configuration) => _configuration = configuration; + + public IMatchmakingResult Matchmake(List sessions) + { + var games = new List>>(); + var refusedSessions = new Dictionary>(); + + foreach (IClientSession session in sessions) + { + InstantBattleConfiguration eventConfiguration = _configuration.GetInternalConfiguration(session.PlayerEntity); + + if (eventConfiguration == default) + { + continue; + } + + Tuple> game = games.FirstOrDefault( + x => x.Item1 == eventConfiguration && x.Item2.Count < eventConfiguration.Requirements.Players.Maximum); + + if (game == default) + { + game = new Tuple>(eventConfiguration, new List()); + games.Add(game); + } + + game.Item2.Add(session); + } + + var gamesToSend = new List>>(); + + foreach (Tuple> game in games) + { + if (game.Item2.Count < game.Item1.Requirements.Players.Minimum) + { + foreach (IClientSession session in game.Item2) + { + DictionaryAdd(refusedSessions, GameDialogKey.GAMEEVENT_SHOUTMESSAGE_NOT_ENOUGH_PLAYERS, session); + } + + continue; + } + + gamesToSend.Add(new Tuple>(game.Item1, game.Item2)); + } + + return new InstantBattleMatchmakingResult(gamesToSend, refusedSessions); + } + + private void DictionaryAdd(Dictionary> dictionary, GameDialogKey dialogKey, IClientSession clientSession) + { + if (dictionary.ContainsKey(dialogKey)) + { + dictionary[dialogKey].Add(clientSession); + return; + } + + dictionary.Add(dialogKey, new List + { + clientSession + }); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.GameEvents/Matchmaking/Matchmaking.cs b/srcs/_plugins/WingsEmu.Plugins.GameEvents/Matchmaking/Matchmaking.cs new file mode 100644 index 0000000..d699c70 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.GameEvents/Matchmaking/Matchmaking.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using System.Linq; +using WingsEmu.Game.GameEvent; +using WingsEmu.Game.GameEvent.Matchmaking; +using WingsEmu.Game.GameEvent.Matchmaking.Filter; +using WingsEmu.Game.GameEvent.Matchmaking.Matchmaker; +using WingsEmu.Game.GameEvent.Matchmaking.Result; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Plugins.GameEvents.Matchmaking +{ + public class Matchmaking : IMatchmaking + { + private readonly Dictionary _matchmakers; + + public Matchmaking(Dictionary matchmakers) => _matchmakers = matchmakers; + + public IMatchmakingResult Matchmake(List sessions, GameEventType type) => _matchmakers[type].Matchmake(sessions); + + public FilterResult Filter(List sessions, params IMatchmakingFilter[] filters) + { + var accepted = sessions.Where(session => filters.All(x => x.IsAccepted(session))).ToList(); + + return new FilterResult(accepted, sessions.Except(accepted).ToList()); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.GameEvents/Matchmaking/Result/IceBreakerMatchmakingResult.cs b/srcs/_plugins/WingsEmu.Plugins.GameEvents/Matchmaking/Result/IceBreakerMatchmakingResult.cs new file mode 100644 index 0000000..36c93c4 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.GameEvents/Matchmaking/Result/IceBreakerMatchmakingResult.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using WingsEmu.Game._i18n; +using WingsEmu.Game.GameEvent.Configuration; +using WingsEmu.Game.GameEvent.Matchmaking.Result; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Plugins.GameEvents.Matchmaking.Result +{ + public class IceBreakerMatchmakingResult : IMatchmakingResult + { + public IceBreakerMatchmakingResult(List>> sessions, Dictionary> refusedSessions) + { + Sessions = sessions; + RefusedSessions = refusedSessions; + } + + public List>> Sessions { get; } + + public Dictionary> RefusedSessions { get; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.GameEvents/Matchmaking/Result/InstantBattleMatchmakingResult.cs b/srcs/_plugins/WingsEmu.Plugins.GameEvents/Matchmaking/Result/InstantBattleMatchmakingResult.cs new file mode 100644 index 0000000..4146b44 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.GameEvents/Matchmaking/Result/InstantBattleMatchmakingResult.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using WingsEmu.Game._i18n; +using WingsEmu.Game.GameEvent.Configuration; +using WingsEmu.Game.GameEvent.Matchmaking.Result; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Plugins.GameEvents.Matchmaking.Result +{ + public class InstantBattleMatchmakingResult : IMatchmakingResult + { + public InstantBattleMatchmakingResult(List>> sessions, Dictionary> refusedSessions) + { + Sessions = sessions; + RefusedSessions = refusedSessions; + } + + public List>> Sessions { get; } + + public Dictionary> RefusedSessions { get; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.GameEvents/RecurrentJob/GameEventSystem.cs b/srcs/_plugins/WingsEmu.Plugins.GameEvents/RecurrentJob/GameEventSystem.cs new file mode 100644 index 0000000..dbc11f6 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.GameEvents/RecurrentJob/GameEventSystem.cs @@ -0,0 +1,60 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using PhoenixLib.Events; +using PhoenixLib.Logging; +using WingsEmu.Game.GameEvent; +using WingsEmu.Game.GameEvent.Event; +using WingsEmu.Plugins.GameEvents.Event.Global; + +namespace WingsEmu.Plugins.GameEvents.RecurrentJob +{ + public class GameEventSystem : BackgroundService + { + private static readonly TimeSpan Interval = TimeSpan.FromSeconds(2); + private readonly IAsyncEventPipeline _asyncEventPipeline; + private readonly IGameEventRegistrationManager _gameEventRegistrationManager; + + public GameEventSystem(IAsyncEventPipeline asyncEventPipeline, IGameEventRegistrationManager gameEventRegistrationManager) + { + _asyncEventPipeline = asyncEventPipeline; + _gameEventRegistrationManager = gameEventRegistrationManager; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + Log.Info("[GAME_EVENT_SYSTEM] Started!"); + while (!stoppingToken.IsCancellationRequested) + { + try + { + DateTime currentTime = DateTime.UtcNow; + + foreach ((GameEventType gameEventType, DateTime expiryDate) in _gameEventRegistrationManager.GameEventRegistrations) + { + await ProcessGameEventRegistration(gameEventType, currentTime, expiryDate, stoppingToken); + } + + await _asyncEventPipeline.ProcessEventAsync(new GameEventInstanceProcessEvent(currentTime), stoppingToken); + } + catch (Exception e) + { + Log.Error("[GAME_EVENT_SYSTEM]", e); + } + + await Task.Delay(Interval, stoppingToken); + } + } + + private async Task ProcessGameEventRegistration(GameEventType gameEventType, DateTime currentTime, DateTime expiryDate, CancellationToken cancellationToken) + { + if (currentTime < expiryDate) + { + return; + } + + await _asyncEventPipeline.ProcessEventAsync(new GameEventLockRegistrationEvent(gameEventType), cancellationToken); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.GameEvents/WingsEmu.Plugins.GameEvents.csproj b/srcs/_plugins/WingsEmu.Plugins.GameEvents/WingsEmu.Plugins.GameEvents.csproj new file mode 100644 index 0000000..1784246 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.GameEvents/WingsEmu.Plugins.GameEvents.csproj @@ -0,0 +1,20 @@ + + + + Library + net5.0 + + + + + + + + + + + + + + + diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/CharScreen/CreateBrawlerPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/CharScreen/CreateBrawlerPacketHandler.cs new file mode 100644 index 0000000..957e354 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/CharScreen/CreateBrawlerPacketHandler.cs @@ -0,0 +1,17 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Threading.Tasks; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.CharScreen; + +public class CreateBrawlerPacketHandler : GenericCharScreenPacketHandlerBase +{ + protected override async Task HandlePacketAsync(IClientSession session, BrawlerCreatePacket packet) + { + // Sorry, not today :c + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/CharScreen/CreateCharacterPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/CharScreen/CreateCharacterPacketHandler.cs new file mode 100644 index 0000000..d394c65 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/CharScreen/CreateCharacterPacketHandler.cs @@ -0,0 +1,239 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using CloneExtensions; +using Mapster; +using PhoenixLib.Logging; +using WingsAPI.Communication; +using WingsAPI.Communication.DbServer.CharacterService; +using WingsAPI.Data.Character; +using WingsEmu.Customization.NewCharCustomisation; +using WingsEmu.DTOs.Account; +using WingsEmu.DTOs.Inventory; +using WingsEmu.Game; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; +using WingsEmu.Packets.Enums; +using WingsEmu.Plugins.PacketHandling.Customization; + +namespace WingsEmu.Plugins.PacketHandling.CharScreen; + +public class CreateCharacterPacketHandler : GenericCharScreenPacketHandlerBase +{ + private readonly BaseCharacter _baseCharacter; + private readonly BaseInventory _baseInventory; + private readonly BaseQuicklist _baseQuicklist; + private readonly BaseSkill _baseSkill; + private readonly ICharacterService _characterService; + private readonly EntryPointPacketHandler _entrypoint; + private readonly IForbiddenNamesManager _forbiddenNamesManager; + private readonly IGameItemInstanceFactory _gameItemInstanceFactory; + private readonly IGameLanguageService _gameLanguage; + private readonly IGameItemInstanceFactory _itemInstanceFactory; + private readonly IMapManager _mapManager; + private readonly IRandomGenerator _randomGenerator; + private readonly IRespawnDefaultConfiguration _respawnDefaultConfiguration; + + public CreateCharacterPacketHandler(EntryPointPacketHandler entrypoint, IGameLanguageService gameLanguage, BaseCharacter baseCharacter, BaseSkill baseSkill, BaseQuicklist baseQuicklist, + BaseInventory baseInventory, IGameItemInstanceFactory gameItemInstanceFactory, ICharacterService characterService, IMapManager mapManager, + IRespawnDefaultConfiguration respawnDefaultConfiguration, IRandomGenerator randomGenerator, IGameItemInstanceFactory itemInstanceFactory, IForbiddenNamesManager forbiddenNamesManager) + { + _entrypoint = entrypoint; + _gameLanguage = gameLanguage; + _baseCharacter = baseCharacter; + _baseSkill = baseSkill; + _baseQuicklist = baseQuicklist; + _baseInventory = baseInventory; + _gameItemInstanceFactory = gameItemInstanceFactory; + _characterService = characterService; + _mapManager = mapManager; + _respawnDefaultConfiguration = respawnDefaultConfiguration; + _randomGenerator = randomGenerator; + _itemInstanceFactory = itemInstanceFactory; + _forbiddenNamesManager = forbiddenNamesManager; + } + + protected override async Task HandlePacketAsync(IClientSession session, CharacterCreatePacket packet) + { + if (session.HasCurrentMapInstance) + { + Log.Warn("HAS_CURRENTMAP_INSTANCE"); + return; + } + + // TODO: Hold Account Information in Authorized object + long accountId = session.Account.Id; + byte slot = packet.Slot; + string characterName = packet.Name; + DbServerGetCharacterResponse response = await _characterService.GetCharacterBySlot(new DbServerGetCharacterFromSlotRequest + { + AccountId = accountId, + Slot = slot + }); + + if (response.RpcResponseType == RpcResponseType.SUCCESS) + { + Log.Warn($"[CREATE_CHARACTER_PACKET_HANDLER] Character slot is already busy. Slot: '{slot.ToString()}'"); + return; + } + + if (slot > 3) + { + Log.Info("SLOTS > 3"); + return; + } + + if (characterName.Length is < 3 or >= 15 && session.Account.Authority < AuthorityType.GameMaster) + { + session.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.CHARACTER_CREATION_INFO_INVALID_CHARNAME, session.UserLanguage)); + return; + } + + if ((byte)packet.HairColor > 9) + { + Log.Info("COLOR NOT VALID FOR A NEW CHARACTER"); + return; + } + + if (packet.HairStyle != HairStyleType.A && packet.HairStyle != HairStyleType.B) + { + Log.Info("HAIRSTYLE NOT VALID FOR A NEW CHARACTER"); + return; + } + + var rg = new Regex(@"^[a-zA-Z0-9_\-\*]*$"); + if (rg.Matches(characterName).Count != 1) + { + session.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.CHARACTER_CREATION_INFO_INVALID_CHARNAME, session.UserLanguage)); + return; + } + + if (session.Account.Authority <= AuthorityType.GameMaster) + { + string lowerCharName = characterName.ToLower(); + if (_forbiddenNamesManager.IsBanned(lowerCharName, out string bannedName)) + { + session.SendInfo(_gameLanguage.GetLanguageFormat(GameDialogKey.CHARACTER_CREATION_INFO_BANNED_CHARNAME, session.UserLanguage, bannedName)); + return; + } + } + + DbServerGetCharacterResponse response2 = await _characterService.GetCharacterByName(new DbServerGetCharacterRequestByName + { + CharacterName = characterName + }); + + if (response2.RpcResponseType == RpcResponseType.SUCCESS) + { + session.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.CHARACTER_CREATION_INFO_ALREADY_TAKEN, session.UserLanguage)); + return; + } + + CharacterDTO newCharacter = _baseCharacter.GetCharacter(); + + newCharacter.AccountId = accountId; + newCharacter.Gender = packet.Gender; + newCharacter.HairColor = packet.HairColor; + newCharacter.HairStyle = packet.HairStyle; + newCharacter.Name = characterName; + newCharacter.Slot = slot; + newCharacter.QuickGetUp = true; + newCharacter.UiBlocked = true; + newCharacter.IsPartnerAutoRelive = true; + newCharacter.IsPetAutoRelive = true; + newCharacter.Dignity = 100; + newCharacter.MinilandPoint = 2000; + + RespawnDefault getRespawn = _respawnDefaultConfiguration.GetReturn(RespawnType.NOSVILLE_SPAWN); + if (getRespawn != null) + { + IMapInstance mapInstance = _mapManager.GetBaseMapInstanceByMapId(getRespawn.MapId); + if (mapInstance != null) + { + int randomX = getRespawn.MapX + _randomGenerator.RandomNumber(getRespawn.Radius, -getRespawn.Radius); + int randomY = getRespawn.MapY + _randomGenerator.RandomNumber(getRespawn.Radius, -getRespawn.Radius); + + if (mapInstance.IsBlockedZone(randomX, randomY)) + { + randomX = getRespawn.MapX; + randomY = getRespawn.MapY; + } + + newCharacter.MapX = (short)randomX; + newCharacter.MapY = (short)randomY; + } + } + + BaseSkill skills = _baseSkill.GetClone(); + if (skills != null) + { + newCharacter.LearnedSkills.AddRange(skills.Skills); + } + + BaseQuicklist quicklist = _baseQuicklist.GetClone(); + if (quicklist != null) + { + newCharacter.Quicklist.AddRange(quicklist.Quicklist); + } + + BaseInventory startupInventory = _baseInventory.GetClone(); + var listOfItems = new List(); + if (startupInventory != null) + { + foreach (BaseInventory.StartupInventoryItem item in startupInventory.Items) + { + GameItemInstance newItem = _gameItemInstanceFactory.CreateItem(item.Vnum, item.Quantity); + var inventoryItem = new InventoryItem + { + InventoryType = item.InventoryType, + IsEquipped = false, + ItemInstance = newItem, + CharacterId = newCharacter.Id, + Slot = item.Slot + }; + + listOfItems.Add(inventoryItem); + } + } + + newCharacter.EquippedStuffs = listOfItems.Where(s => s is { IsEquipped: true }).Select(s => + { + CharacterInventoryItemDto tmp = s.Adapt(); + tmp.ItemInstance = _itemInstanceFactory.CreateDto(s.ItemInstance); + return tmp; + }).ToList(); + newCharacter.Inventory = listOfItems.Where(s => s is { IsEquipped: false }).Select(s => + { + CharacterInventoryItemDto tmp = s.Adapt(); + tmp.ItemInstance = _itemInstanceFactory.CreateDto(s.ItemInstance); + return tmp; + }).ToList(); + newCharacter.LifetimeStats = new CharacterLifetimeStatsDto(); + + DbServerSaveCharacterResponse response3 = await _characterService.CreateCharacter(new DbServerSaveCharacterRequest + { + Character = newCharacter + }); + + if (response3.RpcResponseType != RpcResponseType.SUCCESS) + { + session.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.CHARACTER_CREATION_INFO_ALREADY_TAKEN, session.UserLanguage)); + return; + } + + await _entrypoint.EntryPointAsync(session, null); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/CharScreen/CrossServerEntryPointPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/CharScreen/CrossServerEntryPointPacketHandler.cs new file mode 100644 index 0000000..abbff9b --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/CharScreen/CrossServerEntryPointPacketHandler.cs @@ -0,0 +1,163 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Threading.Tasks; +using PhoenixLib.Logging; +using WingsAPI.Communication; +using WingsAPI.Communication.DbServer.AccountService; +using WingsAPI.Communication.Sessions; +using WingsAPI.Communication.Sessions.Model; +using WingsAPI.Communication.Sessions.Request; +using WingsAPI.Communication.Sessions.Response; +using WingsAPI.Data.Account; +using WingsAPI.Game.Extensions.PacketGeneration; +using WingsEmu.DTOs.Account; +using WingsEmu.Game; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Health; +using WingsEmu.Packets.ClientPackets; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.PacketHandling.CharScreen; + +public class CrossServerEntryPointPacketHandler : GenericCharScreenPacketHandlerBase +{ + private readonly IAccountService _accountService; + private readonly IMaintenanceManager _maintenanceManager; + private readonly IServerManager _serverManager; + private readonly ISessionService _sessionService; + + public CrossServerEntryPointPacketHandler(IServerManager serverManager, ISessionService sessionService, IMaintenanceManager maintenanceManager, IAccountService accountService) + { + _sessionService = sessionService; + _maintenanceManager = maintenanceManager; + _accountService = accountService; + _serverManager = serverManager; + } + + protected override async Task HandlePacketAsync(IClientSession session, CrossServerEntrypointPacket packet) + { + if (session.Account != null) + { + Log.Warn("CrossServerEntryPointPacketHandler session.Account != null"); + return; + } + + SessionResponse sessionResponse = await _sessionService.GetSessionByAccountName(new GetSessionByAccountNameRequest { AccountName = packet.AccountName }); + if (sessionResponse.ResponseType != RpcResponseType.SUCCESS) + { + Log.Info("Session response incorrect"); + session.SendPacket(session.GenerateFailcPacket(LoginFailType.CantConnect)); + session.ForceDisconnect(); + return; + } + + Session sharedSession = sessionResponse.Session; + if (sharedSession.State != SessionState.CrossChannelAuthentication) + { + Log.Debug($"Incorrect session state: {sharedSession.State}"); + session.SendPacket(session.GenerateFailcPacket(LoginFailType.CantConnect)); + session.ForceDisconnect(); + return; + } + + AccountLoadResponse accountLoadResponse = null; + try + { + accountLoadResponse = await _accountService.LoadAccountById(new AccountLoadByIdRequest + { + AccountId = sharedSession.AccountId + }); + } + catch (Exception e) + { + Log.Error("[CROSS_SERVER_AUTH] Unexpected error: ", e); + } + + if (accountLoadResponse?.ResponseType != RpcResponseType.SUCCESS) + { + Log.Warn($"[CROSS_SERVER_AUTH][SESSION_ID: '{session.SessionId.ToString()}'] Failed to load account for accountId: {sharedSession.AccountId}'"); + session.SendPacket(session.GenerateFailcPacket(LoginFailType.UnhandledError)); + session.ForceDisconnect(); + return; + } + + AccountDTO account = accountLoadResponse.AccountDto; + if (_maintenanceManager.IsMaintenanceActive && account.Authority < AuthorityType.GameMaster) + { + session.SendPacket(session.GenerateFailcPacket(LoginFailType.Maintenance)); + session.ForceDisconnect(); + return; + } + + AccountBanGetResponse banResponse = null; + try + { + banResponse = await _accountService.GetAccountBan(new AccountBanGetRequest + { + AccountId = account.Id + }); + } + catch (Exception e) + { + Log.Error($"[CROSS_SERVER_AUTH][SESSION_ID: '{session.SessionId.ToString()}'] Unexpected error: ", e); + } + + if (banResponse?.ResponseType != RpcResponseType.SUCCESS) + { + Log.Warn($"[CROSS_SERVER_AUTH][SESSION_ID: '{session.SessionId.ToString()}'] Failed to get account ban for accountId: '{account.Id.ToString()}'"); + session.SendPacket(session.GenerateFailcPacket(LoginFailType.UnhandledError)); + session.ForceDisconnect(); + return; + } + + AccountBanDto characterPenalty = banResponse.AccountBanDto; + if (characterPenalty != null) + { + session.SendPacket(session.GenerateFailcPacket(LoginFailType.Banned)); + Log.Info($"[CROSS_SERVER_AUTH][SESSION_ID: '{session.SessionId.ToString()}'] {account.Name} connected from {session.IpAddress} while being banned"); + session.ForceDisconnect(); + return; + } + + var accountObj = new Account + { + Id = account.Id, + Name = account.Name, + MasterAccountId = account.MasterAccountId, + Password = account.Password.ToLower(), + Authority = account.Authority, + BankMoney = account.BankMoney, + Language = account.Language + }; + + Log.Warn($"INITIALIZE_ACCOUNT : {account.Name}"); + + SessionResponse response = await _sessionService.ConnectToWorldServer(new ConnectToWorldServerRequest + { + AccountId = account.Id, + ChannelId = _serverManager.ChannelId, + ServerGroup = _serverManager.ServerGroup + }); + + if (response is not { ResponseType: RpcResponseType.SUCCESS }) + { + session.ForceDisconnect(); + return; + } + + if (response.Session == null) + { + session.ForceDisconnect(); + return; + } + + session.InitializeAccount(accountObj, sessionResponse.Session); + + await session.EmitEventAsync(new CharacterPreLoadEvent(packet.CharacterSlot)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/CharScreen/DeleteCharacterPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/CharScreen/DeleteCharacterPacketHandler.cs new file mode 100644 index 0000000..a49abb7 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/CharScreen/DeleteCharacterPacketHandler.cs @@ -0,0 +1,175 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Threading.Tasks; +using PhoenixLib.Extensions; +using PhoenixLib.Logging; +using WingsAPI.Communication; +using WingsAPI.Communication.Bazaar; +using WingsAPI.Communication.DbServer.AccountService; +using WingsAPI.Communication.DbServer.CharacterService; +using WingsAPI.Communication.Families; +using WingsAPI.Communication.Sessions; +using WingsAPI.Communication.Sessions.Model; +using WingsAPI.Communication.Sessions.Request; +using WingsAPI.Communication.Sessions.Response; +using WingsAPI.Data.Account; +using WingsAPI.Data.Character; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.CharScreen; + +public class DeleteCharacterPacketHandler : GenericCharScreenPacketHandlerBase +{ + private readonly IAccountService _accountService; + private readonly IBazaarService _bazaarService; + private readonly ICharacterService _characterService; + private readonly EntryPointPacketHandler _entryPoint; + private readonly IFamilyService _familyService; + private readonly IGameLanguageService _gameLanguage; + private readonly ISessionService _sessionService; + + public DeleteCharacterPacketHandler(EntryPointPacketHandler entryPoint, IGameLanguageService gameLanguage, IFamilyService familyService, ICharacterService characterService, + IBazaarService bazaarService, IAccountService accountService, ISessionService sessionService) + { + _entryPoint = entryPoint; + _gameLanguage = gameLanguage; + _familyService = familyService; + _characterService = characterService; + _bazaarService = bazaarService; + _accountService = accountService; + _sessionService = sessionService; + } + + protected override async Task HandlePacketAsync(IClientSession session, CharacterDeletePacket packet) + { + if (session.Account == null) + { + return; + } + + if (session.HasCurrentMapInstance) + { + return; + } + + SessionResponse sessionResponse = await _sessionService.GetSessionByAccountName(new GetSessionByAccountNameRequest + { + AccountName = session.Account.Name + }); + + if (sessionResponse.ResponseType != RpcResponseType.SUCCESS) + { + return; + } + + if (sessionResponse.Session.State != SessionState.CharacterSelection) + { + return; + } + + AccountLoadResponse accountLoadResponse = null; + try + { + accountLoadResponse = await _accountService.LoadAccountById(new AccountLoadByIdRequest + { + AccountId = session.Account.Id + }); + } + catch (Exception e) + { + Log.Error("[DELETE_CHARACTER] Unexpected error: ", e); + } + + if (accountLoadResponse?.ResponseType != RpcResponseType.SUCCESS) + { + Log.Warn($"[DELETE_CHARACTER] Failed to load the account with id: '{session.Account.Id.ToString()}'"); + return; + } + + AccountDTO account = accountLoadResponse.AccountDto; + if (session.Account.Id != account.Id) + { + return; + } + + if (!string.Equals(account.Password, packet.AccountPassword.ToSha512(), StringComparison.Ordinal)) + { + session.SendInfo(GameDialogKey.ACCOUNT_INFO_BAD_ID); + return; + } + + DbServerGetCharacterResponse response = await _characterService.GetCharacterBySlot(new DbServerGetCharacterFromSlotRequest + { + AccountId = account.Id, + Slot = packet.Slot + }); + + if (response.RpcResponseType != RpcResponseType.SUCCESS || response.CharacterDto == null) + { + session.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.CHARACTER_DELETION_INFO_UNABLE_TO_RETRIEVE_CHARACTER, session.UserLanguage)); + Log.Warn($"[DELETE_CHARACTER] Failed to retrieve the targeted character. AccountId: '{account.Id.ToString()}' TargetedSlot: '{packet.Slot.ToString()}'"); + return; + } + + CharacterDTO character = response.CharacterDto; + + BazaarRemoveItemsByCharIdResponse bazaarResponse = await _bazaarService.RemoveItemsByCharacterIdFromBazaar(new BazaarRemoveItemsByCharIdRequest + { + CharacterId = character.Id + }); + + if (bazaarResponse.ResponseType == RpcResponseType.MAINTENANCE_MODE) + { + session.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.BAZAAR_INFO_MAINTENANCE_MODE, session.UserLanguage)); + Log.Warn("[DELETE_CHARACTER] Failed deleting Character's Bazaar Items because the BazaarServer is in Maintenance mode."); + } + + if (bazaarResponse.ResponseType != RpcResponseType.SUCCESS) + { + session.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.CHARACTER_DELETION_INFO_FAILED, session.UserLanguage)); + Log.Warn($"[DELETE_CHARACTER] Failed deleting Character's Bazaar Items. CharacterId: '{character.Id.ToString()}'"); + return; + } + + + BasicRpcResponse removeMemberFromFamilyResponse = null; + try + { + removeMemberFromFamilyResponse = await _familyService.RemoveMemberByCharIdAsync(new FamilyRemoveMemberByCharIdRequest + { + CharacterId = character.Id + }); + } + catch (Exception e) + { + Log.Error("[DELETE_CHARACTER] ", e); + } + + if (removeMemberFromFamilyResponse == null) + { + session.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.CHARACTER_DELETION_INFO_FAILED, session.UserLanguage)); + Log.Warn($"[DELETE_CHARACTER] Failed to contact with FamilyService to request RemoveMemberByCharId. CharacterId: '{character.Id.ToString()}'"); + return; + } + + DbServerDeleteCharacterResponse response2 = await _characterService.DeleteCharacter(new DbServerDeleteCharacterRequest + { + CharacterDto = character + }); + + if (response2.RpcResponseType != RpcResponseType.SUCCESS) + { + session.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.CHARACTER_DELETION_INFO_FAILED, session.UserLanguage)); + Log.Warn($"[DELETE_CHARACTER] For unknown reasons the character couldn't be deleted. AccountId: '{account.Id.ToString()}' TargetedSlot: '{character.Slot.ToString()}'"); + return; + } + + await _entryPoint.EntryPointAsync(session, null); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/CharScreen/EntryPointPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/CharScreen/EntryPointPacketHandler.cs new file mode 100644 index 0000000..df769d9 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/CharScreen/EntryPointPacketHandler.cs @@ -0,0 +1,272 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using PhoenixLib.Extensions; +using PhoenixLib.Logging; +using WingsAPI.Communication; +using WingsAPI.Communication.DbServer.AccountService; +using WingsAPI.Communication.DbServer.CharacterService; +using WingsAPI.Communication.Sessions; +using WingsAPI.Communication.Sessions.Model; +using WingsAPI.Communication.Sessions.Request; +using WingsAPI.Communication.Sessions.Response; +using WingsAPI.Data.Account; +using WingsAPI.Data.Character; +using WingsEmu.Core.Extensions; +using WingsEmu.DTOs.Account; +using WingsEmu.DTOs.Inventory; +using WingsEmu.DTOs.Mates; +using WingsEmu.Game; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Health; +using WingsEmu.Packets.ClientPackets; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.PacketHandling.CharScreen; + +public class EntryPointPacketHandler : GenericCharScreenPacketHandlerBase +{ + private static readonly SemaphoreSlim _semaphoreSlim = new(1, 1); + private readonly IAccountService _accountService; + private readonly ICharacterService _characterService; + private readonly IGameItemInstanceFactory _itemInstanceFactory; + private readonly IMaintenanceManager _maintenanceManager; + private readonly IServerManager _serverManager; + private readonly ISessionManager _sessionManager; + private readonly ISessionService _sessionService; + + public EntryPointPacketHandler(ISessionService sessionService, ICharacterService characterService, IMaintenanceManager maintenanceManager, IAccountService accountService, + ISessionManager sessionManager, IServerManager serverManager, IGameItemInstanceFactory itemInstanceFactory) + { + _sessionService = sessionService; + _characterService = characterService; + _maintenanceManager = maintenanceManager; + _accountService = accountService; + _sessionManager = sessionManager; + _serverManager = serverManager; + _itemInstanceFactory = itemInstanceFactory; + } + + public async Task EntryPointAsync(IClientSession session, EntryPointPacket packet) + { + await _semaphoreSlim.WaitAsync(); + try + { + if (session.Account == null) + { + SessionResponse sessionResponse = await _sessionService.GetSessionByAccountName(new GetSessionByAccountNameRequest() + { + AccountName = packet.SessionName + }); + + if (sessionResponse.ResponseType != RpcResponseType.SUCCESS) + { + Log.Debug($"Can't get session with ID {packet.SessionName}"); + session.ForceDisconnect(); + return; + } + + Session sharedSession = sessionResponse.Session; + if (sharedSession.State != SessionState.ServerSelection) + { + Log.Debug($"Incorrect state for {sharedSession.Id}"); + session.ForceDisconnect(); + return; + } + + if (session.SessionId != sessionResponse.Session.EncryptionKey) + { + session.ForceDisconnect(); + return; + } + + SessionResponse sessionResponseAccount = await _sessionService.GetSessionByAccountId(new GetSessionByAccountIdRequest + { + AccountId = sharedSession.AccountId + }); + + if (sessionResponseAccount is null) + { + session.ForceDisconnect(); + return; + } + + if (sessionResponseAccount.Session.EncryptionKey != session.SessionId) + { + session.ForceDisconnect(); + return; + } + + if (sessionResponseAccount.Session.State != SessionState.ServerSelection) + { + session.ForceDisconnect(); + return; + } + + if (_maintenanceManager.IsMaintenanceActive && sharedSession.Authority < AuthorityType.GameMaster) + { + Log.Debug("[ENTRY_POINT] Maintenance is active"); + session.ForceDisconnect(); + return; + } + + if (_sessionManager.SessionsCount >= _serverManager.AccountLimit && sharedSession.Authority < AuthorityType.Moderator) + { + Log.Debug("[ENTRY_POINT] Account limit reached"); + session.ForceDisconnect(); + return; + } + + AccountBanGetResponse banResponse = null; + try + { + banResponse = await _accountService.GetAccountBan(new AccountBanGetRequest + { + AccountId = sharedSession.AccountId + }); + } + catch (Exception e) + { + Log.Error($"[ENTRY_POINT][SESSION_ID: '{session.SessionId.ToString()}'] Unexpected error: ", e); + } + + if (banResponse?.ResponseType != RpcResponseType.SUCCESS) + { + Log.Warn($"[ENTRY_POINT][SESSION_ID: '{session.SessionId.ToString()}'] Failed to get account ban for accountId: '{sharedSession.AccountId}'"); + session.ForceDisconnect(); + return; + } + + AccountBanDto characterPenalty = banResponse.AccountBanDto; + if (characterPenalty != null) + { + Log.Info($"[ENTRY_POINT][SESSION_ID: '{session.SessionId.ToString()}'] connected from {session.IpAddress} while being banned"); + session.ForceDisconnect(); + return; + } + + AccountLoadResponse accountResponse = await _accountService.LoadAccountById(new AccountLoadByIdRequest + { + AccountId = sharedSession.AccountId + }); + + if (accountResponse.ResponseType != RpcResponseType.SUCCESS) + { + Log.Debug("[ENTRY_POINT] Failed to load account"); + session.ForceDisconnect(); + return; + } + + AccountDTO account = accountResponse.AccountDto; + if (!string.Equals(account.Password, packet.Password.ToSha512(), StringComparison.Ordinal)) + { + Log.Debug("[ENTRY_POINT] Incorrect password"); + session.ForceDisconnect(); + return; + } + + var accountObject = new Account + { + Id = account.Id, + Name = account.Name, + MasterAccountId = account.MasterAccountId, + Authority = account.Authority, + BankMoney = account.BankMoney, + Password = account.Password, + Language = account.Language + }; + + Log.Warn($"INITIALIZE_ACCOUNT : {account.Name}"); + + SessionResponse response = await _sessionService.ConnectToWorldServer(new ConnectToWorldServerRequest + { + AccountId = account.Id, + ChannelId = _serverManager.ChannelId, + ServerGroup = _serverManager.ServerGroup + }); + + if (response is not { ResponseType: RpcResponseType.SUCCESS }) + { + session.ForceDisconnect(); + return; + } + + if (response.Session == null) + { + session.ForceDisconnect(); + return; + } + + + session.InitializeAccount(accountObject, response.Session); + } + + DbServerGetCharactersResponse response2 = await _characterService.GetCharacters(new DbServerGetCharactersRequest + { + AccountId = session.Account.Id + }); + Log.Info($"[ACCOUNT_ARRIVED] {session.Account.Name}"); + + // load characterlist packet for each character in CharacterDTO + session.SendPacket("clist_start 0"); + + if (response2.Characters == null) + { + session.SendPacket("clist_end"); + return; + } + + foreach (CharacterDTO character in response2.Characters) + { + List inventory = character.EquippedStuffs; + + var equipment = new Dictionary(); + foreach (CharacterInventoryItemDto equipmentEntry in inventory) + { + if (equipmentEntry.InventoryType != InventoryType.EquippedItems) + { + continue; + } + + GameItemInstance instance = _itemInstanceFactory.CreateItem(equipmentEntry.ItemInstance); + if (instance == null) + { + continue; + } + + equipment[instance.GameItem.EquipmentSlot] = instance; + } + + string petlist = string.Empty; + List mates = character.NosMates; + for (int i = 0; i < 26; i++) + { + //0.2105.1102.319.0.632.0.333.0.318.0.317.0.9.-1.-1.-1.-1.-1.-1.-1.-1.-1.-1.-1.-1 + petlist += $"{(i != 0 ? "." : "")}{(mates.Count > i ? $"{mates[i].Skin}.{mates[i].NpcMonsterVNum}" : "-1")}"; + } + + // 1 1 before long string of -1.-1 = act completion + session.SendPacket( + $"clist {character.Slot} {character.Name} 0 {(byte)character.Gender} {(byte)character.HairStyle} {(byte)character.HairColor} 0 {(byte)character.Class} {character.Level} {character.HeroLevel} {(character.HideHat ? 0 : equipment.GetOrDefault(EquipmentType.Hat)?.ItemVNum ?? -1)}.{equipment.GetOrDefault(EquipmentType.Armor)?.ItemVNum ?? -1}.{equipment.GetOrDefault(EquipmentType.WeaponSkin)?.ItemVNum ?? equipment.GetOrDefault(EquipmentType.MainWeapon)?.ItemVNum ?? -1}.{equipment.GetOrDefault(EquipmentType.SecondaryWeapon)?.ItemVNum ?? -1}.{equipment.GetOrDefault(EquipmentType.Mask)?.ItemVNum ?? -1}.{equipment.GetOrDefault(EquipmentType.Fairy)?.ItemVNum ?? -1}.{equipment.GetOrDefault(EquipmentType.CostumeSuit)?.ItemVNum ?? -1}.{(character.HideHat ? 0 : equipment.GetOrDefault(EquipmentType.CostumeHat)?.ItemVNum ?? -1)} {character.JobLevel} 1 1 {petlist} {(equipment.GetOrDefault(EquipmentType.Hat)?.GameItem.IsColorable == true ? equipment.GetOrDefault(EquipmentType.Hat).Design : 0)} 0"); + } + + session.SendPacket("clist_end"); + } + finally + { + _semaphoreSlim.Release(); + } + } + + protected override async Task HandlePacketAsync(IClientSession session, EntryPointPacket packet) + { + await EntryPointAsync(session, packet); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/CharScreen/SelectPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/CharScreen/SelectPacketHandler.cs new file mode 100644 index 0000000..cbb8442 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/CharScreen/SelectPacketHandler.cs @@ -0,0 +1,30 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Threading.Tasks; +using WingsEmu.DTOs.Account; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Networking; +using WingsEmu.Health; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.CharScreen; + +public class SelectPacketHandler : GenericCharScreenPacketHandlerBase +{ + private readonly IMaintenanceManager _maintenanceManager; + + public SelectPacketHandler(IMaintenanceManager maintenanceManager) => _maintenanceManager = maintenanceManager; + + protected override async Task HandlePacketAsync(IClientSession session, SelectPacket packet) + { + if (_maintenanceManager.IsMaintenanceActive && session.Account.Authority < AuthorityType.GameMaster) + { + session.ForceDisconnect(); + return; + } + + await session.EmitEventAsync(new CharacterPreLoadEvent(packet.Slot)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/CharScreenPacketHandlerCorePlugin.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/CharScreenPacketHandlerCorePlugin.cs new file mode 100644 index 0000000..ad4784e --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/CharScreenPacketHandlerCorePlugin.cs @@ -0,0 +1,26 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using Microsoft.Extensions.DependencyInjection; +using PhoenixLib.Extensions; +using WingsAPI.Plugins; + +namespace WingsEmu.Plugins.PacketHandling; + +public class CharScreenPacketHandlerCorePlugin : IGameServerPlugin +{ + public string Name => nameof(CharScreenPacketHandlerCorePlugin); + + + public void AddDependencies(IServiceCollection services, GameServerLoader gameServer) + { + Type[] handlerTypes = typeof(CharScreenPacketHandlerCorePlugin).Assembly.GetTypesImplementingGenericClass(typeof(GenericCharScreenPacketHandlerBase<>)); + + foreach (Type handlerType in handlerTypes) + { + services.AddTransient(handlerType); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/CharScreenPacketHandlerGamePlugin.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/CharScreenPacketHandlerGamePlugin.cs new file mode 100644 index 0000000..0a97280 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/CharScreenPacketHandlerGamePlugin.cs @@ -0,0 +1,45 @@ +using System; +using PhoenixLib.Extensions; +using PhoenixLib.Logging; +using WingsAPI.Plugins; +using WingsEmu.Game._packetHandling; + +namespace WingsEmu.Plugins.PacketHandling; + +public class CharScreenPacketHandlerGamePlugin : IGamePlugin +{ + private readonly IServiceProvider _container; + private readonly IPacketHandlerContainer _handlers; + + public CharScreenPacketHandlerGamePlugin(IPacketHandlerContainer handlers, IServiceProvider container) + { + _handlers = handlers; + _container = container; + } + + public string Name => nameof(CharScreenPacketHandlerGamePlugin); + + public void OnLoad() + { + foreach (Type handlerType in typeof(CharScreenPacketHandlerGamePlugin).Assembly.GetTypesImplementingGenericClass(typeof(GenericCharScreenPacketHandlerBase<>))) + { + try + { + object tmp = _container.GetService(handlerType); + if (!(tmp is ICharacterScreenPacketHandler handler)) + { + continue; + } + + Type type = handlerType.BaseType.GenericTypeArguments[0]; + + Log.Info($"[CHARSCREEN_HANDLERS][ADD_HANDLER] {handlerType}"); + _handlers.Register(type, handler); + } + catch (Exception e) + { + Log.Error("[CHARSCREEN_HANDLERS][FAIL_ADD]", e); + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Customization/BaseCharacter.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Customization/BaseCharacter.cs new file mode 100644 index 0000000..88d0e71 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Customization/BaseCharacter.cs @@ -0,0 +1,41 @@ +// WingsEmu +// +// Developed by NosWings Team + +using Mapster; +using WingsAPI.Data.Character; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Character; + +namespace WingsEmu.Plugins.PacketHandling.Customization; + +public class BaseCharacter +{ + public BaseCharacter() => Character = new CharacterDTO + { + Class = ClassType.Adventurer, + Gender = GenderType.Male, + HairColor = HairColorType.Black, + HairStyle = HairStyleType.A, + Hp = 221, + JobLevel = 1, + Level = 1, + MapId = 1, + MapX = 78, + MapY = 109, + Mp = 221, + MaxPetCount = 10, + MaxPartnerCount = 3, + Gold = 0, + SpPointsBasic = 10000, + SpPointsBonus = 0, + Name = "template", + Slot = 0, + AccountId = 0, + MinilandMessage = string.Empty + }; + + public CharacterDTO Character { get; set; } + + public CharacterDTO GetCharacter() => Character.Adapt(); +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Customization/BaseInventory.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Customization/BaseInventory.cs new file mode 100644 index 0000000..9f4d96d --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Customization/BaseInventory.cs @@ -0,0 +1,60 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Customization.NewCharCustomisation; + +public class BaseInventory +{ + public BaseInventory() => Items = new List + { + new() + { + InventoryType = InventoryType.EquippedItems, + Slot = (short)EquipmentType.MainWeapon, + Quantity = 1, + Vnum = 1 + }, + new() + { + InventoryType = InventoryType.EquippedItems, + Slot = (short)EquipmentType.Armor, + Quantity = 1, + Vnum = 12 + }, + new() + { + InventoryType = InventoryType.EquippedItems, + Slot = (short)EquipmentType.SecondaryWeapon, + Quantity = 1, + Vnum = 8 + }, + new() + { + InventoryType = InventoryType.Etc, + Slot = 0, + Quantity = 10, + Vnum = 2024 + }, + new() + { + Vnum = 2081, + Slot = 1, + Quantity = 1, + InventoryType = InventoryType.Etc + } + }; + + public List Items { get; set; } + + public class StartupInventoryItem + { + public short Vnum { get; set; } + public short Quantity { get; set; } + public short Slot { get; set; } + public InventoryType InventoryType { get; set; } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Customization/BaseQuicklist.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Customization/BaseQuicklist.cs new file mode 100644 index 0000000..7583dc4 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Customization/BaseQuicklist.cs @@ -0,0 +1,43 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using WingsEmu.DTOs.Quicklist; + +namespace WingsEmu.Customization.NewCharCustomisation; + +public class BaseQuicklist +{ + public BaseQuicklist() => Quicklist = new List + { + new() + { + Type = QuicklistType.SKILLS, + InventoryTypeOrSkillTab = 1, + InvSlotOrSkillSlotOrSkillVnum = 1 + }, + new() + { + Type = QuicklistType.ITEM, + QuicklistSlot = 1, + InventoryTypeOrSkillTab = 2 + }, + new() + { + QuicklistSlot = 8, + Type = QuicklistType.SKILLS, + InventoryTypeOrSkillTab = 1, + InvSlotOrSkillSlotOrSkillVnum = 16 + }, + new() + { + QuicklistSlot = 9, + Type = QuicklistType.SKILLS, + InventoryTypeOrSkillTab = 3, + InvSlotOrSkillSlotOrSkillVnum = 1 + } + }; + + public List Quicklist { get; set; } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Customization/BaseSkill.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Customization/BaseSkill.cs new file mode 100644 index 0000000..8c9d8f0 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Customization/BaseSkill.cs @@ -0,0 +1,20 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Collections.Generic; +using WingsEmu.DTOs.Skills; + +namespace WingsEmu.Customization.NewCharCustomisation; + +public class BaseSkill +{ + public BaseSkill() => Skills = new List + { + new() { SkillVNum = 200 }, + new() { SkillVNum = 201 }, + new() { SkillVNum = 209 } + }; + + public List Skills { get; set; } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Customization/CustomizationCorePlugin.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Customization/CustomizationCorePlugin.cs new file mode 100644 index 0000000..262f871 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Customization/CustomizationCorePlugin.cs @@ -0,0 +1,43 @@ +// WingsEmu +// +// Developed by NosWings Team + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using PhoenixLib.Configuration; +using WingsAPI.Plugins; +using WingsEmu.Customization.NewCharCustomisation; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Configurations; +using WingsEmu.Plugins.BasicImplementations.Warehouse; +using DependencyInjectionExtensions = WingsEmu.Plugins.BasicImplementations.Miniland.DependencyInjectionExtensions; + +namespace WingsEmu.Plugins.PacketHandling.Customization; + +public class CustomizationCorePlugin : IGameServerPlugin +{ + public string Name => nameof(CustomizationCorePlugin); + + public void AddDependencies(IServiceCollection services, GameServerLoader gameServer) + { + services.AddFileConfiguration(new BaseCharacter()); + services.AddFileConfiguration(); + services.AddFileConfiguration(); + services.AddFileConfiguration(); + services.AddFileConfiguration(); + services.AddFileConfiguration(); + services.AddFileConfiguration(); + services.AddFileConfiguration(); + services.AddFileConfiguration(); + services.AddFileConfiguration("bank_reputation_configuration"); + services.TryAddSingleton(); + services.AddMultipleConfigurationOneFile("reputation_configuration"); + services.TryAddSingleton(); + services.AddMultipleConfigurationOneFile("buffs_duration_configuration"); + services.TryAddSingleton(); + services.AddMultipleConfigurationOneFile("return_default_configuration"); + services.TryAddSingleton(); + DependencyInjectionExtensions.AddMinilandModule(services); + services.AddWarehouseModule(); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Banks/BankManagementPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Banks/BankManagementPacketHandler.cs new file mode 100644 index 0000000..9a2f81a --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Banks/BankManagementPacketHandler.cs @@ -0,0 +1,144 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.PacketHandling.Game.Banks; + +public class BankManagementPacketHandler : GenericGamePacketHandlerBase +{ + private readonly IBankReputationConfiguration _bankReputationConfiguration; + private readonly IGameLanguageService _gameLanguage; + private readonly IRankingManager _rankingManager; + private readonly IReputationConfiguration _reputationConfiguration; + private readonly IServerManager _serverManager; + + public BankManagementPacketHandler(IGameLanguageService gameLanguage, IServerManager serverManager, IReputationConfiguration reputationConfiguration, + IBankReputationConfiguration bankReputationConfiguration, IRankingManager rankingManager) + { + _gameLanguage = gameLanguage; + _serverManager = serverManager; + _reputationConfiguration = reputationConfiguration; + _bankReputationConfiguration = bankReputationConfiguration; + _rankingManager = rankingManager; + } + + protected override async Task HandlePacketAsync(IClientSession session, GboxPacket packet) + { + if (session.PlayerEntity.IsInExchange()) + { + return; + } + + if (session.PlayerEntity.HasShopOpened) + { + return; + } + + if (!session.PlayerEntity.IsBankOpen) + { + await session.NotifyStrangeBehavior(StrangeBehaviorSeverity.ABUSING, "Tried to withdraw/deposit gold without opened bank."); + return; + } + + switch (packet.Type) + { + case BankActionType.Deposit: + if (packet.Option == 0) + { + session.SendQnaPacket($"gbox 1 {packet.Amount} 1", _gameLanguage.GetLanguageFormat(GameDialogKey.BANK_DIALOG_ASK_DEPOSIT, session.UserLanguage, packet.Amount)); + return; + } + + if (packet.Option == 1) + { + if (packet.Amount <= 0) + { + return; + } + + if (session.Account.BankMoney + packet.Amount * 1000 > _serverManager.MaxBankGold) + { + session.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.BANK_MESSAGE_MAX_GOLD_REACHED, session.UserLanguage)); + session.SendSMemo(SmemoType.BankError, _gameLanguage.GetLanguage(GameDialogKey.BANK_MESSAGE_MAX_GOLD_REACHED, session.UserLanguage)); + return; + } + + if (session.PlayerEntity.Gold < packet.Amount * 1000) + { + session.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.INTERACTION_MESSAGE_NOT_ENOUGH_GOLD, session.UserLanguage)); + session.SendSMemo(SmemoType.BankError, _gameLanguage.GetLanguage(GameDialogKey.INTERACTION_MESSAGE_NOT_ENOUGH_GOLD, session.UserLanguage)); + return; + } + + session.PlayerEntity.Gold -= packet.Amount * 1000; + session.Account.BankMoney += packet.Amount * 1000; + session.RefreshGold(); + session.SendGbPacket(BankType.Deposit, _reputationConfiguration, _bankReputationConfiguration, _rankingManager.TopReputation); + session.SendSMemo(SmemoType.BankInfo, _gameLanguage.GetLanguageFormat(GameDialogKey.BANK_MESSAGE_DEPOSIT, session.UserLanguage, $"{packet.Amount}000")); + session.SendChatMessage(_gameLanguage.GetLanguageFormat(GameDialogKey.BANK_MESSAGE_BALANCE, session.UserLanguage, session.Account.BankMoney), ChatMessageColorType.Green); + session.SendSMemo(SmemoType.BankBalance, _gameLanguage.GetLanguageFormat(GameDialogKey.BANK_MESSAGE_BALANCE, session.UserLanguage, session.Account.BankMoney)); + } + + break; + case BankActionType.Withdraw: + if (packet.Option == 0) + { + session.SendQnaPacket($"gbox 2 {packet.Amount} 1", _gameLanguage.GetLanguageFormat(GameDialogKey.BANK_DIALOG_ASK_WITHDRAW, session.UserLanguage, packet.Amount)); + return; + } + + if (packet.Option == 1) + { + if (packet.Amount <= 0) + { + return; + } + + if (session.PlayerEntity.Gold + packet.Amount * 1000 > _serverManager.MaxGold) + { + session.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.BANK_INFO_TOO_MUCH_GOLD, session.UserLanguage)); + session.SendSMemo(SmemoType.BankError, _gameLanguage.GetLanguage(GameDialogKey.BANK_INFO_TOO_MUCH_GOLD, session.UserLanguage)); + return; + } + + if (session.Account.BankMoney < packet.Amount * 1000) + { + session.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.BANK_INFO_NOT_ENOUGH_FUNDS, session.UserLanguage)); + session.SendSMemo(SmemoType.BankError, _gameLanguage.GetLanguage(GameDialogKey.BANK_INFO_NOT_ENOUGH_FUNDS, session.UserLanguage)); + return; + } + + if (!session.HasEnoughGold(session.PlayerEntity.GetBankPenalty(_reputationConfiguration, _bankReputationConfiguration, _rankingManager.TopReputation))) + { + session.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.INTERACTION_MESSAGE_NOT_ENOUGH_GOLD, session.UserLanguage)); + session.SendSMemo(SmemoType.BankError, _gameLanguage.GetLanguage(GameDialogKey.INTERACTION_MESSAGE_NOT_ENOUGH_GOLD, session.UserLanguage)); + return; + } + + session.PlayerEntity.Gold -= session.PlayerEntity.GetBankPenalty(_reputationConfiguration, _bankReputationConfiguration, _rankingManager.TopReputation); + session.Account.BankMoney -= packet.Amount * 1000; + session.PlayerEntity.Gold += packet.Amount * 1000; + session.RefreshGold(); + session.SendGbPacket(BankType.Withdraw, _reputationConfiguration, _bankReputationConfiguration, _rankingManager.TopReputation); + session.SendSMemo(SmemoType.BankInfo, _gameLanguage.GetLanguageFormat(GameDialogKey.BANK_LOG_WITHDRAW_BANK, session.UserLanguage, packet.Amount)); + session.SendChatMessage(_gameLanguage.GetLanguageFormat(GameDialogKey.BANK_MESSAGE_BALANCE, session.UserLanguage, session.Account.BankMoney), ChatMessageColorType.Green); + session.SendSMemo(SmemoType.BankBalance, _gameLanguage.GetLanguageFormat(GameDialogKey.BANK_MESSAGE_BALANCE, session.UserLanguage, session.Account.BankMoney)); + } + + break; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/ArenaPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/ArenaPacketHandler.cs new file mode 100644 index 0000000..9013030 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/ArenaPacketHandler.cs @@ -0,0 +1,81 @@ +using System; +using System.Threading.Tasks; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Arena; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.PacketHandling.Game.Basic; + +public class ArenaPacketHandler : GenericGamePacketHandlerBase +{ + private readonly IArenaManager _arenaManager; + private readonly IGameLanguageService _gameLanguage; + private readonly IMapManager _mapManager; + + public ArenaPacketHandler(IGameLanguageService gameLanguage, IMapManager mapManager, IArenaManager arenaManager) + { + _gameLanguage = gameLanguage; + _mapManager = mapManager; + _arenaManager = arenaManager; + } + + protected override async Task HandlePacketAsync(IClientSession session, ArenaPacket packet) + { + byte arenaType = packet.ArenaType; + + if (arenaType > 1) + { + return; + } + + double timeSpanSinceLastPortal = (DateTime.UtcNow - session.PlayerEntity.LastPortal).TotalSeconds; + + if (timeSpanSinceLastPortal < 4 || !session.HasCurrentMapInstance) + { + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.PORTAL_CHATMESSAGE_TOO_EARLY, session.UserLanguage), ChatMessageColorType.Yellow); + return; + } + + if (session.CantPerformActionOnAct4()) + { + return; + } + + if (!session.CurrentMapInstance.HasMapFlag(MapFlags.IS_BASE_MAP)) + { + return; + } + + if (session.PlayerEntity.IsInRaidParty) + { + return; + } + + if (session.PlayerEntity.TimeSpaceComponent.IsInTimeSpaceParty) + { + return; + } + + if (!session.PlayerEntity.IsAlive()) + { + return; + } + + if (session.PlayerEntity.Gold < 500 * (1 + arenaType)) + { + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.INTERACTION_MESSAGE_NOT_ENOUGH_GOLD, session.UserLanguage), ChatMessageColorType.Yellow); + return; + } + + session.PlayerEntity.LastPortal = DateTime.UtcNow; + session.PlayerEntity.Gold -= 500 * (1 + arenaType); + session.RefreshGold(); + await _mapManager.TeleportOnRandomPlaceInMapAsync(session, arenaType == 0 ? _arenaManager.ArenaInstance : _arenaManager.FamilyArenaInstance); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/BtkPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/BtkPacketHandler.cs new file mode 100644 index 0000000..8072f26 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/BtkPacketHandler.cs @@ -0,0 +1,18 @@ +using System.Threading.Tasks; +using WingsEmu.Game.Chat; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.Game.Basic; + +public class BtkPacketHandler : GenericGamePacketHandlerBase +{ + protected override async Task HandlePacketAsync(IClientSession session, BtkPacket btkPacket) + { + await session.EmitEventAsync(new ChatSendFriendMessageEvent + { + Message = btkPacket.Message, + TargetId = btkPacket.CharacterId + }); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/CharacterOptionPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/CharacterOptionPacketHandler.cs new file mode 100644 index 0000000..f54e7e4 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/CharacterOptionPacketHandler.cs @@ -0,0 +1,189 @@ +using System.Threading.Tasks; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Groups; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Networking.Broadcasting; +using WingsEmu.Packets.ClientPackets; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Character; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.PacketHandling.Game.Basic; + +public class CharacterOptionPacketHandler : GenericGamePacketHandlerBase +{ + private readonly IGameLanguageService _language; + private readonly ISessionManager _sessionManager; + + public CharacterOptionPacketHandler(IGameLanguageService language, ISessionManager sessionManager) + { + _language = language; + _sessionManager = sessionManager; + } + + protected override async Task HandlePacketAsync(IClientSession session, CharacterOptionPacket characterOptionPacket) + { + if (characterOptionPacket == null) + { + return; + } + + switch (characterOptionPacket.Option) + { + case CharacterOption.BuffBlocked: + session.PlayerEntity.BuffBlocked = characterOptionPacket.IsActive; + session.SendMsg(_language.GetLanguage( + session.PlayerEntity.BuffBlocked ? GameDialogKey.OPTIONS_SHOUTMESSAGE_BUFF_DISABLED : GameDialogKey.OPTIONS_SHOUTMESSAGE_BUFF_ENABLED, session.UserLanguage), + MsgMessageType.Middle); + break; + + case CharacterOption.EmoticonsBlocked: + session.PlayerEntity.EmoticonsBlocked = characterOptionPacket.IsActive; + session.SendMsg(_language.GetLanguage( + session.PlayerEntity.EmoticonsBlocked ? GameDialogKey.OPTIONS_SHOUTMESSAGE_EMOTE_DISABLED : GameDialogKey.OPTIONS_SHOUTMESSAGE_EMOTE_ENABLED, session.UserLanguage), + MsgMessageType.Middle); + break; + + case CharacterOption.ExchangeBlocked: + session.PlayerEntity.ExchangeBlocked = !characterOptionPacket.IsActive; + session.SendMsg(_language.GetLanguage( + session.PlayerEntity.ExchangeBlocked ? GameDialogKey.OPTIONS_SHOUTMESSAGE_TRADE_DISABLED : GameDialogKey.OPTIONS_SHOUTMESSAGE_TRADE_ENABLED, session.UserLanguage), + MsgMessageType.Middle); + break; + + case CharacterOption.FriendRequestBlocked: + session.PlayerEntity.FriendRequestBlocked = !characterOptionPacket.IsActive; + session.SendMsg(_language.GetLanguage( + session.PlayerEntity.FriendRequestBlocked ? GameDialogKey.OPTIONS_SHOUTMESSAGE_FRIEND_REQUESTS_DISABLED : GameDialogKey.OPTIONS_SHOUTMESSAGE_FRIEND_REQUESTS_ENABLED, + session.UserLanguage), MsgMessageType.Middle); + break; + + case CharacterOption.GroupRequestBlocked: + session.PlayerEntity.GroupRequestBlocked = !characterOptionPacket.IsActive; + session.SendMsg(_language.GetLanguage( + session.PlayerEntity.GroupRequestBlocked ? GameDialogKey.OPTIONS_SHOUTMESSAGE_GROUP_REQUESTS_DISABLED : GameDialogKey.OPTIONS_SHOUTMESSAGE_GROUP_REQUESTS_ENABLED, + session.UserLanguage), + MsgMessageType.Middle); + break; + + case CharacterOption.PetAutoRelive: + session.PlayerEntity.IsPetAutoRelive = characterOptionPacket.IsActive; + session.SendMsg(_language.GetLanguage( + session.PlayerEntity.IsPetAutoRelive ? GameDialogKey.OPTIONS_SHOUTMESSAGE_PET_AUTO_RELIVE_ENABLED : GameDialogKey.OPTIONS_SHOUTMESSAGE_PET_AUTO_RELIVE_DISABLED, + session.UserLanguage), MsgMessageType.Middle); + break; + + case CharacterOption.PartnerAutoRelive: + session.PlayerEntity.IsPartnerAutoRelive = characterOptionPacket.IsActive; + session.SendMsg(_language.GetLanguage( + session.PlayerEntity.IsPartnerAutoRelive ? GameDialogKey.OPTIONS_SHOUTMESSAGE_PARTNER_AUTO_RELIVE_ENABLED : GameDialogKey.OPTIONS_SHOUTMESSAGE_PARTNER_AUTO_RELIVE_DISABLED, + session.UserLanguage), MsgMessageType.Middle); + break; + + case CharacterOption.HeroChatBlocked: + session.PlayerEntity.HeroChatBlocked = characterOptionPacket.IsActive; + session.SendMsg(_language.GetLanguage( + session.PlayerEntity.HeroChatBlocked ? GameDialogKey.OPTIONS_SHOUTMESSAGE_SPEAKERS_DISABLED : GameDialogKey.OPTIONS_SHOUTMESSAGE_SPEAKERS_ENABLED, session.UserLanguage), + MsgMessageType.Middle); + break; + + case CharacterOption.HpBlocked: + session.PlayerEntity.HpBlocked = characterOptionPacket.IsActive; + session.SendMsg(_language.GetLanguage( + session.PlayerEntity.HpBlocked ? GameDialogKey.OPTIONS_SHOUTMESSAGE_HP_DISABLED : GameDialogKey.OPTIONS_SHOUTMESSAGE_HP_ENABLED, session.UserLanguage), MsgMessageType.Middle); + break; + + case CharacterOption.MinilandInviteBlocked: + session.PlayerEntity.MinilandInviteBlocked = characterOptionPacket.IsActive; + session.SendMsg(_language.GetLanguage( + session.PlayerEntity.MinilandInviteBlocked ? GameDialogKey.OPTIONS_SHOUTMESSAGE_MINILAND_INVITES_DISABLED : GameDialogKey.OPTIONS_SHOUTMESSAGE_MINILAND_INVITES_ENABLED, + session.UserLanguage), + MsgMessageType.Middle); + break; + + case CharacterOption.MouseAimLock: + session.PlayerEntity.MouseAimLock = characterOptionPacket.IsActive; + session.SendMsg(_language.GetLanguage( + session.PlayerEntity.MouseAimLock ? GameDialogKey.OPTIONS_SHOUTMESSAGE_MOUSE_ENABLED : GameDialogKey.OPTIONS_SHOUTMESSAGE_MOUSE_DISABLED, session.UserLanguage), + MsgMessageType.Middle); + break; + + case CharacterOption.QuickGetUp: + session.PlayerEntity.QuickGetUp = characterOptionPacket.IsActive; + session.SendMsg(_language.GetLanguage( + session.PlayerEntity.QuickGetUp ? GameDialogKey.OPTIONS_SHOUTMESSAGE_QUICK_GET_UP_ENABLED : GameDialogKey.OPTIONS_SHOUTMESSAGE_QUICK_GET_UP_DISABLED, session.UserLanguage), + MsgMessageType.Middle); + break; + + case CharacterOption.WhisperBlocked: + session.PlayerEntity.WhisperBlocked = !characterOptionPacket.IsActive; + session.SendMsg(_language.GetLanguage( + session.PlayerEntity.WhisperBlocked ? GameDialogKey.OPTIONS_SHOUTMESSAGE_WHISPER_DISABLED : GameDialogKey.OPTIONS_SHOUTMESSAGE_WHISPER_ENABLED, session.UserLanguage), + MsgMessageType.Middle); + break; + + case CharacterOption.FamilyRequestBlocked: + session.PlayerEntity.FamilyRequestBlocked = !characterOptionPacket.IsActive; + session.SendMsg(_language.GetLanguage( + session.PlayerEntity.FamilyRequestBlocked ? GameDialogKey.OPTIONS_SHOUTMESSAGE_FAMILY_REQUESTS_DISABLED : GameDialogKey.OPTIONS_SHOUTMESSAGE_FAMILY_REQUESTS_ENABLED, + session.UserLanguage), MsgMessageType.Middle); + break; + + case CharacterOption.HideHat: + session.PlayerEntity.HideHat = !characterOptionPacket.IsActive; + session.SendMsg( + _language.GetLanguage(session.PlayerEntity.HideHat ? GameDialogKey.OPTIONS_SHOUTMESSAGE_HIDE_HAT_ENABLED : GameDialogKey.OPTIONS_SHOUTMESSAGE_HIDE_HAT_DISABLED, + session.UserLanguage), MsgMessageType.Middle); + session.BroadcastEq(); + break; + + case CharacterOption.UiBlocked: + session.PlayerEntity.UiBlocked = !characterOptionPacket.IsActive; + session.SendMsg( + _language.GetLanguage(session.PlayerEntity.UiBlocked ? GameDialogKey.OPTIONS_SHOUTMESSAGE_UI_DISABLED : GameDialogKey.OPTIONS_SHOUTMESSAGE_UI_ENABLED, session.UserLanguage), + MsgMessageType.Middle); + break; + + case CharacterOption.GroupSharing: + if (!session.PlayerEntity.IsInGroup()) + { + return; + } + + PlayerGroup grp = session.PlayerEntity.GetGroup(); + + if (!session.PlayerEntity.IsLeaderOfGroup(session.PlayerEntity.Id)) + { + session.SendMsg(_language.GetLanguage(GameDialogKey.GROUP_SHOUTMESSAGE_YOU_ARE_NOT_LEADER, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (characterOptionPacket.IsActive == false) + { + grp.SharingMode = GroupSharingType.Everyone; + + await _sessionManager.BroadcastAsync(async g => + { + string message = _language.GetLanguage(GameDialogKey.GROUP_SHOUTMESSAGE_SHARING, g.UserLanguage); + return g.GenerateMsgPacket(message, MsgMessageType.Middle); + }, new GroupBroadcast(grp)); + } + else + { + grp.SharingMode = GroupSharingType.ByOrder; + + await _sessionManager.BroadcastAsync(async g => + { + string message = _language.GetLanguage(GameDialogKey.GROUP_SHOUTMESSAGE_SHARING_BY_ORDER, g.UserLanguage); + return g.GenerateMsgPacket(message, MsgMessageType.Middle); + }, new GroupBroadcast(grp)); + } + + break; + } + + session.RefreshStat(); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/ComplimentPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/ComplimentPacketHandler.cs new file mode 100644 index 0000000..f860bef --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/ComplimentPacketHandler.cs @@ -0,0 +1,72 @@ +using System; +using System.Threading.Tasks; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Compliments; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.PacketHandling.Game.Basic; + +public class ComplimentPacketHandler : GenericGamePacketHandlerBase +{ + private readonly IComplimentsManager _complimentsManager; + private readonly IGameLanguageService _language; + private readonly ISessionManager _sessionManager; + + public ComplimentPacketHandler(IGameLanguageService language, ISessionManager sessionManager, IComplimentsManager complimentsManager) + { + _sessionManager = sessionManager; + _complimentsManager = complimentsManager; + _language = language; + } + + protected override async Task HandlePacketAsync(IClientSession session, ComplimentPacket complimentPacket) + { + if (complimentPacket == null) + { + return; + } + + if (session.CantPerformActionOnAct4()) + { + return; + } + + long complimentedCharacterId = complimentPacket.CharacterId; + if (session.PlayerEntity.Level <= 30) + { + session.SendChatMessage(_language.GetLanguage(GameDialogKey.COMMEND_CHATMESSAGE_NOT_MINLVL, session.UserLanguage), ChatMessageColorType.Red); + return; + } + + if (session.PlayerEntity.GameStartDate.AddMinutes(60) > DateTime.UtcNow) + { + session.SendChatMessage( + _language.GetLanguageFormat(GameDialogKey.COMMEND_CHATMESSAGE_LOGIN_COOLDOWN, session.UserLanguage, (session.PlayerEntity.GameStartDate.AddMinutes(60) - DateTime.UtcNow).Minutes), + ChatMessageColorType.Red); + + return; + } + + + IClientSession complimentedSession = _sessionManager.GetSessionByCharacterId(complimentedCharacterId); + if (complimentedSession?.PlayerEntity == null) + { + return; + } + + bool canCompliment = await _complimentsManager.CanCompliment(session.Account.Id); + if (!canCompliment) + { + session.SendChatMessage(_language.GetLanguage(GameDialogKey.COMMEND_CHATMESSAGE_COOLDOWN, session.UserLanguage), ChatMessageColorType.Red); + return; + } + + complimentedSession.PlayerEntity.Compliment += 1; + session.SendChatMessage(session.GetLanguageFormat(GameDialogKey.COMMEND_CHATMESSAGE_GIVEN, complimentedSession.PlayerEntity.Name), ChatMessageColorType.Green); + complimentedSession.SendChatMessage(complimentedSession.GetLanguageFormat(GameDialogKey.COMMEND_CHATMESSAGE_RECEIVED, session.PlayerEntity.Name), ChatMessageColorType.LightPurple); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/CspPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/CspPacketHandler.cs new file mode 100644 index 0000000..7956803 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/CspPacketHandler.cs @@ -0,0 +1,32 @@ +using System.Threading.Tasks; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.Game.Basic; + +public class CspPacketHandler : GenericGamePacketHandlerBase +{ + protected override async Task HandlePacketAsync(IClientSession session, CspServerPacket packet) + { + if (session.PlayerEntity.Id != packet.CharacterId) + { + return; + } + + if (!session.PlayerEntity.IsUsingBubble()) + { + return; + } + + if (session.PlayerEntity.GetMessage() != packet.Message) + { + session.PlayerEntity.RemoveBubble(); + await session.NotifyStrangeBehavior(StrangeBehaviorSeverity.NORMAL, "Tried to change bubble message"); + return; + } + + session.BroadcastBubbleMessage(packet.Message); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/CspePacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/CspePacketHandler.cs new file mode 100644 index 0000000..84f7522 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/CspePacketHandler.cs @@ -0,0 +1,15 @@ +using System; +using System.Threading.Tasks; +using WingsAPI.Packets.ClientPackets; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Plugins.PacketHandling.Game.Basic; + +public class CspePacketHandler : GenericGamePacketHandlerBase +{ + protected override async Task HandlePacketAsync(IClientSession session, CspePacket packet) + { + session.PlayerEntity.Bubble = DateTime.MinValue; + session.PlayerEntity.RemoveBubble(); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/DirectionPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/DirectionPacketHandler.cs new file mode 100644 index 0000000..f1682a5 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/DirectionPacketHandler.cs @@ -0,0 +1,64 @@ +using System.Threading.Tasks; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.PacketHandling.Game.Basic; + +public class DirectionPacketHandler : GenericGamePacketHandlerBase +{ + private readonly ISessionManager _sessionManager; + + public DirectionPacketHandler(ISessionManager sessionManager) => _sessionManager = sessionManager; + + protected override async Task HandlePacketAsync(IClientSession session, DirectionPacket packet) + { + if (packet.Direction >= 8) + { + _sessionManager.BroadcastToGameMaster(session, "DirectionPacketHandler dir > 8"); + return; + } + + if (session.PlayerEntity.CheatComponent.IsInvisible) + { + return; + } + + switch (packet.VisualType) + { + case VisualType.Player: + if (packet.Id != session.PlayerEntity.Id) + { + _sessionManager.BroadcastToGameMaster(session, "DirectionPacketHandler id != charId"); + return; + } + + session.PlayerEntity.Direction = packet.Direction; + session.CurrentMapInstance?.Broadcast(session.PlayerEntity.GenerateDir()); + break; + case VisualType.Npc: + IMateEntity mate = session.PlayerEntity.MateComponent.GetMate(m => m.Id == packet.Id); + + if (mate == null) + { + return; + } + + mate.Direction = packet.Direction; + session.CurrentMapInstance?.Broadcast(mate.GenerateDir()); + return; + case VisualType.Monster: + _sessionManager.BroadcastToGameMaster(session, "DirectionPacketHandler VisualType.Monster"); + break; + case VisualType.Object: + _sessionManager.BroadcastToGameMaster(session, "DirectionPacketHandler VisualType.Object"); + break; + default: + _sessionManager.BroadcastToGameMaster(session, "DirectionPacketHandler default"); + break; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/FlPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/FlPacketHandler.cs new file mode 100644 index 0000000..a6e7816 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/FlPacketHandler.cs @@ -0,0 +1,35 @@ +using System.Threading.Tasks; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.Game.Basic; + +public class FlPacketHandler : GenericGamePacketHandlerBase +{ + private readonly FInsPacketHandler _fInsPacketHandler; + private readonly ISessionManager _sessionManager; + + public FlPacketHandler(FInsPacketHandler fInsPacketHandler, ISessionManager sessionManager) + { + _sessionManager = sessionManager; + _fInsPacketHandler = fInsPacketHandler; + } + + protected override async Task HandlePacketAsync(IClientSession session, FlPacket packet) + { + IClientSession otherSession = _sessionManager.GetSessionByCharacterName(packet.CharName); + + if (otherSession == null) + { + return; + } + + if (otherSession == session) + { + return; + } + + await _fInsPacketHandler.HandleAsync(session, new FInsPacket { Type = 0, CharacterId = otherSession.PlayerEntity.Id }); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/GameStartPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/GameStartPacketHandler.cs new file mode 100644 index 0000000..58a0e21 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/GameStartPacketHandler.cs @@ -0,0 +1,14 @@ +using System.Threading.Tasks; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.Game.Basic; + +public class GameStartPacketHandler : GenericCharScreenPacketHandlerBase +{ + protected override async Task HandlePacketAsync(IClientSession session, GameStartPacket packet) + { + await session.EmitEventAsync(new CharacterLoadEvent()); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/GetGiftPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/GetGiftPacketHandler.cs new file mode 100644 index 0000000..63cab24 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/GetGiftPacketHandler.cs @@ -0,0 +1,28 @@ +using System; +using System.Threading.Tasks; +using WingsEmu.Game.Mails.Events; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.Game.Basic; + +public class GetGiftPacketHandler : GenericGamePacketHandlerBase +{ + protected override async Task HandlePacketAsync(IClientSession session, GetGiftPacket getGiftPacket) + { + if (!Enum.TryParse(getGiftPacket.Type.ToString(), out GetGiftType type)) + { + return; + } + + switch (type) + { + case GetGiftType.OpenMail: + await session.EmitEventAsync(new MailOpenEvent(getGiftPacket.GiftId)); + break; + case GetGiftType.RemoveMail: + await session.EmitEventAsync(new MailRemoveEvent(getGiftPacket.GiftId)); + break; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/GuriPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/GuriPacketHandler.cs new file mode 100644 index 0000000..5bcc3ef --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/GuriPacketHandler.cs @@ -0,0 +1,27 @@ +using System.Threading.Tasks; +using WingsEmu.Game._Guri.Event; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.Game.Basic; + +public class GuriPacketHandler : GenericGamePacketHandlerBase +{ + protected override async Task HandlePacketAsync(IClientSession session, GuriPacket packet) + { + if (session.PlayerEntity.CheatComponent.IsInvisible) + { + return; + } + + string[] split = packet.OriginalContent.Split(' ', '^'); + await session.EmitEventAsync(new GuriEvent + { + EffectId = packet.Type, + Data = (int)(split[1][0] == '#' ? packet.Argument : packet.Data ?? 0), + User = packet.User ?? session.PlayerEntity.Id, + Value = packet.Value, + Packet = split + }); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/HeroPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/HeroPacketHandler.cs new file mode 100644 index 0000000..711569e --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/HeroPacketHandler.cs @@ -0,0 +1,53 @@ +using System.Threading.Tasks; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Chat; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Networking.Broadcasting; +using WingsEmu.Packets.ClientPackets; +using WingsEmu.Packets.Enums.Chat; +using ChatType = WingsEmu.Game._playerActionLogs.ChatType; + +namespace WingsEmu.Plugins.PacketHandling.Game.Basic; + +public class HeroPacketHandler : GenericGamePacketHandlerBase +{ + private readonly IGameLanguageService _language; + private readonly ISessionManager _sessionManager; + + public HeroPacketHandler(ISessionManager sessionManager, IGameLanguageService language) + { + _sessionManager = sessionManager; + _language = language; + } + + protected override async Task HandlePacketAsync(IClientSession session, HeroPacket packet) + { + string message = packet.Message; + if (string.IsNullOrEmpty(message)) + { + return; + } + + if (!session.IsGameMaster()) + { + session.SendChatMessage(_language.GetLanguage(GameDialogKey.INFORMATION_CHATMESSAGE_USER_NOT_HERO, session.UserLanguage), ChatMessageColorType.Red); + return; + } + + message = message.Trim(); + if (message.Length > 60) + { + message = message.Substring(0, 60); + } + + await _sessionManager.BroadcastAsync(async x => { return x.GenerateMsgPacket($"[{session.PlayerEntity.Name}]: {message}", MsgMessageType.BottomRed); }, new SpeakerHeroBroadcast()); + + await session.EmitEventAsync(new ChatGenericEvent + { + Message = message, + ChatType = ChatType.HeroChat + }); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/ISortPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/ISortPacketHandler.cs new file mode 100644 index 0000000..d023d3a --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/ISortPacketHandler.cs @@ -0,0 +1,25 @@ +using System; +using System.Threading.Tasks; +using WingsAPI.Packets.ClientPackets; +using WingsEmu.Game.Inventory.Event; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.PacketHandling.Game.Basic; + +public class ISortPacketHandler : GenericGamePacketHandlerBase +{ + protected override async Task HandlePacketAsync(IClientSession session, ISortPacket packet) + { + if (!Enum.TryParse(packet.InventoryType.ToString(), out InventoryType inventoryType)) + { + return; + } + + await session.EmitEventAsync(new InventorySortItemEvent + { + InventoryType = inventoryType, + Confirm = packet.Confirm + }); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/NcifPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/NcifPacketHandler.cs new file mode 100644 index 0000000..7f0dbcb --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/NcifPacketHandler.cs @@ -0,0 +1,40 @@ +using System.Threading.Tasks; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.PacketHandling.Game.Basic; + +public class NcifPacketHandler : GenericGamePacketHandlerBase +{ + protected override async Task HandlePacketAsync(IClientSession session, NcifPacket ncifPacket) + { + if (!session.HasCurrentMapInstance) + { + return; + } + + IBattleEntity entity = session.PlayerEntity.MapInstance.GetBattleEntity((VisualType)ncifPacket.Type, ncifPacket.TargetId); + + if (entity == null) + { + return; + } + + if (entity.Id == session.PlayerEntity.Id) + { + return; + } + + if (!entity.IsAlive()) + { + return; + } + + session.PlayerEntity.LastEntity = (entity.Type, entity.Id); + session.SendStPacket(entity); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/NpinfoPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/NpinfoPacketHandler.cs new file mode 100644 index 0000000..b707cec --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/NpinfoPacketHandler.cs @@ -0,0 +1,16 @@ +using System.Threading.Tasks; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.Game.Basic; + +public class NpinfoPacketHandler : GenericGamePacketHandlerBase +{ + protected override async Task HandlePacketAsync(IClientSession session, NpinfoPacket npInfoPacket) + { + session.SendPClearPacket(); + session.SendScpPackets(npInfoPacket.Page); + session.SendScnPackets(); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/PreqPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/PreqPacketHandler.cs new file mode 100644 index 0000000..0f16b29 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/PreqPacketHandler.cs @@ -0,0 +1,64 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using PhoenixLib.Logging; +using WingsAPI.Communication.ServerApi.Protocol; +using WingsEmu.Game; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Maps.Event; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.Game.Basic; + +public class PreqPacketHandler : GenericGamePacketHandlerBase +{ + private readonly IGameLanguageService _languageService; + private readonly SerializableGameServer _serializableGameServer; + + public PreqPacketHandler(IGameLanguageService languageService, SerializableGameServer serializableGameServer) + { + _languageService = languageService; + _serializableGameServer = serializableGameServer; + } + + protected override async Task HandlePacketAsync(IClientSession session, PreqPacket packet) + { + bool isAct4 = _serializableGameServer.ChannelType == GameChannelType.ACT_4; + if (session.PlayerEntity.LastMapChange + TimeSpan.FromSeconds(isAct4 ? 5 : 2) > DateTime.UtcNow) + { + session.SendInformationChatMessage(_languageService.GetLanguage(GameDialogKey.PORTAL_CHATMESSAGE_TOO_EARLY, session.UserLanguage)); + return; + } + + if (session.PlayerEntity.IsSeal) + { + return; + } + + if (!session.PlayerEntity.IsAlive()) + { + return; + } + + IPortalEntity portal = session.CurrentMapInstance.Portals.Concat(session.PlayerEntity.GetExtraPortal()).FirstOrDefault(s => + session.PlayerEntity.PositionY >= s.PositionY - 1 && + session.PlayerEntity.PositionY <= s.PositionY + 1 && + session.PlayerEntity.PositionX >= s.PositionX - 1 && + session.PlayerEntity.PositionX <= s.PositionX + 1); + + if (portal == null) + { + Log.Debug("Portal not found"); + return; + } + + await session.EmitEventAsync(new PortalTriggerEvent + { + Portal = portal, + Confirmed = packet.Confirmed + }); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/PstPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/PstPacketHandler.cs new file mode 100644 index 0000000..82634a7 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/PstPacketHandler.cs @@ -0,0 +1,49 @@ +using System; +using System.Threading.Tasks; +using WingsEmu.Game.Mails.Events; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.Game.Basic; + +public class PstPacketHandler : GenericGamePacketHandlerBase +{ + protected override async Task HandlePacketAsync(IClientSession session, PstPacket pstPacket) + { + if (!Enum.TryParse(pstPacket.Argument.ToString(), out PstPacketType type)) + { + return; + } + + bool isSenderCopy = pstPacket.Type == 2; + + switch (type) + { + case PstPacketType.SendNote: + string data = pstPacket.Data; + string receiverName = pstPacket.Receiver; + if (string.IsNullOrEmpty(data)) + { + return; + } + + if (string.IsNullOrEmpty(receiverName)) + { + return; + } + + string[] split = data.Split(' '); + string title = split[0]; + string message = split[1]; + + await session.EmitEventAsync(new NoteCreateEvent(receiverName, title, message)); + break; + case PstPacketType.RemoveNote: + await session.EmitEventAsync(new NoteRemoveEvent(pstPacket.Id, isSenderCopy)); + break; + case PstPacketType.ReadNote: + await session.EmitEventAsync(new NoteOpenEvent(pstPacket.Id, isSenderCopy)); + break; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/PulsePacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/PulsePacketHandler.cs new file mode 100644 index 0000000..703ea29 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/PulsePacketHandler.cs @@ -0,0 +1,50 @@ +using System; +using System.Threading.Tasks; +using WingsAPI.Communication; +using WingsAPI.Communication.Sessions; +using WingsAPI.Communication.Sessions.Request; +using WingsAPI.Communication.Sessions.Response; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.Game.Basic; + +public class PulsePacketHandler : GenericGamePacketHandlerBase +{ + private readonly ISessionService _sessionService; + + public PulsePacketHandler(ISessionService sessionService) => _sessionService = sessionService; + + protected override async Task HandlePacketAsync(IClientSession session, PulsePacket packet) + { + // if player used Speed Hack in CE and client sent pulse packet too quickly + double seconds = (DateTime.UtcNow - session.PlayerEntity.LastPulseTick).TotalSeconds; + if (seconds < 50 && !session.IsGameMaster()) + { + await session.NotifyStrangeBehavior(StrangeBehaviorSeverity.ABUSING, "Player used Speed Hack in Cheat Engine, because client sent pulse packet too quickly"); + session.ForceDisconnect(); + return; + } + + session.PlayerEntity.LastPulse += 60; + if (packet.Tick != session.PlayerEntity.LastPulse) + { + session.ForceDisconnect(); + return; + } + + session.PlayerEntity.LastPulseTick = DateTime.UtcNow; + + SessionResponse response = await _sessionService.Pulse(new PulseRequest + { + AccountId = session.Account.Id + }); + + if (response.ResponseType != RpcResponseType.SUCCESS) + { + session.ForceDisconnect(); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/QSetPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/QSetPacketHandler.cs new file mode 100644 index 0000000..435cb22 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/QSetPacketHandler.cs @@ -0,0 +1,72 @@ +using System.Threading.Tasks; +using WingsEmu.DTOs.Quicklist; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Quicklist; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.Game.Basic; + +public class QSetPacketHandler : GenericGamePacketHandlerBase +{ + protected override async Task HandlePacketAsync(IClientSession session, QSetPacket qSetPacket) + { + short secondParam = 0; + + // first => secondParam + // item => inventorySlot + // skill => + // 0 => passiveSkill => ?? + // 1 => skill => skillTabSlot (ordered by castId) + // 2 => skill upgrade => upgradeSlot (its ordered by parent skill priority and then how much you have) + // 3 => emote/motion => vnum + + short inventoryOrSkillSlot = 0; + short quicklistTab = qSetPacket.QuicklistTab; + short quicklistSlot = qSetPacket.QuicklistSlot; + QsetPacketType type = qSetPacket.Type; + + + if (qSetPacket.DestinationType.HasValue) + { + secondParam = qSetPacket.DestinationType.Value; + } + + if (qSetPacket.DestinationSlotOrVnum.HasValue) + { + inventoryOrSkillSlot = qSetPacket.DestinationSlotOrVnum.Value; + } + + switch (type) + { + case QsetPacketType.SET_ITEM: + case QsetPacketType.SET_SKILL: + + session.EmitEvent(new QuicklistAddEvent + { + Tab = quicklistTab, + Slot = quicklistSlot, + Type = type switch { QsetPacketType.SET_ITEM => QuicklistType.ITEM, QsetPacketType.SET_SKILL => QuicklistType.SKILLS }, + DestinationType = secondParam, + DestinationSlotOrVnum = inventoryOrSkillSlot + }); + break; + + case QsetPacketType.SWAP: + session.EmitEvent(new QuicklistSwapEvent + { + Tab = quicklistTab, + FromSlot = inventoryOrSkillSlot, + ToSlot = quicklistSlot + }); + + break; + + case QsetPacketType.REMOVE: + session.EmitEvent(new QuicklistRemoveEvent { Tab = quicklistTab, Slot = quicklistSlot }); + break; + + default: + return; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/QtPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/QtPacketHandler.cs new file mode 100644 index 0000000..b1a6330 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/QtPacketHandler.cs @@ -0,0 +1,87 @@ +using System.Threading.Tasks; +using PhoenixLib.Logging; +using WingsAPI.Game.Extensions.Quests; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Quests; +using WingsEmu.Game.Quests.Event; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.Game.Basic; + +public class QtPacketHandler : GenericGamePacketHandlerBase +{ + private readonly IQuestManager _questManager; + + public QtPacketHandler(IQuestManager questManager) => _questManager = questManager; + + protected override async Task HandlePacketAsync(IClientSession session, QtPacket packet) + { + int packetSlot = packet.Slot; + int action = packet.Action; + + + CharacterQuest quest = session.GetQuestByActionSlot(action, packetSlot); + if (quest == null && action != (short)QtAction.START_QUEST) + { + Log.Debug($"[ERROR] PACKET QT: Not a valid quest found for the slot: {packetSlot.ToString()}"); + return; + } + + switch (action) + { + case (short)QtAction.CHARACTER_IN_TARGET: + await session.EmitEventAsync(new QuestCompletedEvent(quest, true)); + break; + case (short)QtAction.START_QUEST: + if (session.PlayerEntity.GetPendingSoundFlowerQuests() == 0) + { + await session.NotifyStrangeBehavior(StrangeBehaviorSeverity.ABUSING, "Player tried to start a sound flower quest without having any."); + return; + } + + await session.EmitEventAsync(new AddSoundFlowerQuestEvent + { + SoundFlowerType = SoundFlowerType.SOUND_FLOWER + }); + break; + case (short)QtAction.LEAVE_QUEST: + + if (quest == null) + { + break; + } + + if (_questManager.IsNpcBlueAlertQuest(quest.QuestId)) + { + session.SendInfo(session.GetLanguage(GameDialogKey.QUEST_INFO_CANT_GIVE_UP)); + break; + } + + await session.EmitEventAsync(new QuestRemoveEvent(quest, false)); + break; + case (short)QtAction.QUEST_REWARD_CLICKED: + if (!session.PlayerEntity.IsQuestCompleted(quest)) + { + return; + } + + await session.EmitEventAsync(new QuestCompletedEvent(quest, true, false)); + break; + default: + Log.Debug($"[ERROR] PACKET QT: Invalid action: {action.ToString()}"); + return; + } + } + + private enum QtAction + { + CHARACTER_IN_TARGET = 1, + START_QUEST = 2, + LEAVE_QUEST = 3, + QUEST_REWARD_CLICKED = 4 + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/RStartPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/RStartPacketHandler.cs new file mode 100644 index 0000000..c242774 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/RStartPacketHandler.cs @@ -0,0 +1,19 @@ +using System.Threading.Tasks; +using WingsEmu.Game.Networking; +using WingsEmu.Game.TimeSpaces.Events; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.Game.Basic; + +public class RStartPacketHandler : GenericGamePacketHandlerBase +{ + protected override async Task HandlePacketAsync(IClientSession session, RStartPacket packet) + { + if (packet.Type != 1) + { + return; + } + + await session.EmitEventAsync(new TimeSpaceStartPortalEvent()); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/RankSkPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/RankSkPacketHandler.cs new file mode 100644 index 0000000..b7632e5 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/RankSkPacketHandler.cs @@ -0,0 +1,28 @@ +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.Game.Basic; + +public class RankSkPacketHandler : GenericGamePacketHandlerBase +{ + private readonly IBuffFactory _buffFactory; + private readonly IAsyncEventPipeline _eventPipeline; + + public RankSkPacketHandler(IBuffFactory buffFactory, IAsyncEventPipeline eventPipeline) + { + _eventPipeline = eventPipeline; + _buffFactory = buffFactory; + } + + protected override async Task HandlePacketAsync(IClientSession session, RankSkPacket packet) + { + /* TODO + if (session.PlayerEntity.IsReputHero() <= 3) + { + return; + }*/ + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/ReqInfoPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/ReqInfoPacketHandler.cs new file mode 100644 index 0000000..fb0cc6e --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/ReqInfoPacketHandler.cs @@ -0,0 +1,137 @@ +using System.Threading.Tasks; +using WingsEmu.DTOs.Items; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage.Configuration; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Extensions.Mates; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.PacketHandling.Game.Basic; + +public class ReqInfoPacketHandler : GenericGamePacketHandlerBase +{ + private readonly IGameLanguageService _gameLanguage; + private readonly INpcMonsterManager _npcMonsterManager; + private readonly IRankingManager _rankingManager; + private readonly IReputationConfiguration _reputationConfiguration; + private readonly ISpPartnerConfiguration _spPartnerConfiguration; + + public ReqInfoPacketHandler(INpcMonsterManager npcMonsterManager, IGameLanguageService gameLanguage, + IReputationConfiguration reputationConfiguration, ISpPartnerConfiguration spPartnerConfiguration, IRankingManager rankingManager) + { + _npcMonsterManager = npcMonsterManager; + _gameLanguage = gameLanguage; + _reputationConfiguration = reputationConfiguration; + _spPartnerConfiguration = spPartnerConfiguration; + _rankingManager = rankingManager; + } + + protected override async Task HandlePacketAsync(IClientSession session, ReqInfoPacket packet) + { + switch (packet.Type) + { + case 12: + InventoryItem item = session.PlayerEntity.GetItemBySlotAndType((short)packet.TargetVNum, InventoryType.Equipment); + if (item == null) + { + return; + } + + if (item.ItemInstance.Type != ItemInstanceType.WearableInstance) + { + return; + } + + if (item.ItemInstance.Rarity <= 0) + { + session.SendPacket($"r_info {item.ItemInstance.ItemVNum} 0"); + return; + } + + if (item.ItemInstance.BoundCharacterId == session.PlayerEntity.Id) + { + session.SendPacket($"r_info {item.ItemInstance.ItemVNum} 1"); + return; + } + + session.SendPacket($"r_info {item.ItemInstance.ItemVNum} 2"); + break; + + case 6: + if (!packet.MateVNum.HasValue) + { + return; + } + + IMonsterData npcPartner = session.CurrentMapInstance.GetNpcById(packet.MateVNum.Value); + if (npcPartner != null) + { + session.SendNpcInfo(npcPartner, _gameLanguage); + return; + } + + IMateEntity targetMate = session.CurrentMapInstance.GetMateById(packet.MateVNum.Value); + + if (targetMate == null) + { + return; + } + + if (session.CurrentMapInstance.HasMapFlag(MapFlags.ACT_4)) + { + if (session.PlayerEntity.Faction != targetMate.Faction) + { + return; + } + } + + session.SendPacket(targetMate.GenerateEInfo(_gameLanguage, session.UserLanguage, _spPartnerConfiguration)); + break; + case 5: + IMonsterData npc = _npcMonsterManager.GetNpc((short)packet.TargetVNum); + if (npc == null) + { + return; + } + + session.SendNpcInfo(npc, _gameLanguage); + + break; + default: + IPlayerEntity target = session.CurrentMapInstance.GetCharacterById(packet.TargetVNum); + if (target == null) + { + return; + } + + if (session.CurrentMapInstance.HasMapFlag(MapFlags.ACT_4)) + { + if (session.PlayerEntity.Faction != target.Faction) + { + return; + } + } + + if (session.PlayerEntity.RainbowBattleComponent.IsInRainbowBattle && target.RainbowBattleComponent.IsInRainbowBattle) + { + if (session.PlayerEntity.RainbowBattleComponent.Team != target.RainbowBattleComponent.Team) + { + return; + } + } + + session.SendPacket(target.Session.GenerateReqInfo(_reputationConfiguration, _rankingManager.TopReputation)); + break; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/RevivalPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/RevivalPacketHandler.cs new file mode 100644 index 0000000..a27abdd --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/RevivalPacketHandler.cs @@ -0,0 +1,26 @@ +using System; +using System.Threading.Tasks; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Revival; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.Game.Basic; + +public class RevivalPacketHandler : GenericGamePacketHandlerBase +{ + protected override async Task HandlePacketAsync(IClientSession session, RevivalPacket packet) + { + if (session.PlayerEntity.IsAlive()) + { + return; + } + + if (!Enum.IsDefined(typeof(RevivalType), (int)packet.Type)) + { + throw new ArgumentOutOfRangeException("", $"The RevivalType that was requested isn't defined -> 'ValueRecieved': {packet.Type.ToString()}"); + } + + await session.EmitEventAsync(new RevivalReviveEvent((RevivalType)packet.Type)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/SayPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/SayPacketHandler.cs new file mode 100644 index 0000000..b0e05b8 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/SayPacketHandler.cs @@ -0,0 +1,79 @@ +using System.Threading.Tasks; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Chat; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Networking.Broadcasting; +using WingsEmu.Packets.ClientPackets; +using WingsEmu.Packets.Enums.Chat; +using ChatType = WingsEmu.Game._playerActionLogs.ChatType; + +namespace WingsEmu.Plugins.PacketHandling.Game.Basic; + +public class SayPacketHandler : GenericGamePacketHandlerBase +{ + private readonly ISessionManager _sessionManager; + + public SayPacketHandler(ISessionManager sessionManager) => _sessionManager = sessionManager; + + protected override async Task HandlePacketAsync(IClientSession session, SayPacket packet) + { + if (string.IsNullOrEmpty(packet.Message)) + { + return; + } + + if (session.CurrentMapInstance == null) + { + return; + } + + if (session.PlayerEntity.CheatComponent.IsInvisible) + { + return; + } + + if (session.IsMuted()) + { + session.SendMuteMessage(); + return; + } + + string message = packet.Message; + + if (message.Length > 60) + { + message = message.Substring(0, 60); + } + + if (message.StartsWith("!")) + { + ProcessTimeSpaceMessage(session, message); + return; + } + + await session.EmitEventAsync(new NormalChatEvent + { + Message = message + }); + + await session.EmitEventAsync(new ChatGenericEvent + { + Message = message.Trim(), + ChatType = ChatType.General + }); + } + + private void ProcessTimeSpaceMessage(IClientSession session, string message) + { + if (!session.PlayerEntity.TimeSpaceComponent.IsInTimeSpaceParty) + { + return; + } + + message = message[1..]; + session.SendChatMessage(message, ChatMessageColorType.LightYellow); + _sessionManager.Broadcast(session.GenerateSpkPacket(message, SpeakType.TimeSpace), new TimeSpaceBroadcast(session), new ExpectBlockedPlayerBroadcast(session.PlayerEntity.Id)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/SitPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/SitPacketHandler.cs new file mode 100644 index 0000000..aeb69eb --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/SitPacketHandler.cs @@ -0,0 +1,53 @@ +using System.Threading.Tasks; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Mates.Events; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.PacketHandling.Game.Basic; + +public class SitPacketHandler : GenericGamePacketHandlerBase +{ + private readonly IMeditationManager _meditationManager; + + public SitPacketHandler(IMeditationManager meditationManager) => _meditationManager = meditationManager; + + protected override async Task HandlePacketAsync(IClientSession session, SitPacket packet) + { + if (_meditationManager.HasMeditation(session.PlayerEntity)) + { + _meditationManager.RemoveAllMeditation(session.PlayerEntity); + } + + if (packet?.Users == null) + { + return; + } + + bool syncWithPlayer = false; + foreach (SitSubPacket subPacket in packet.Users) + { + if (subPacket.VisualType == VisualType.Player) + { + await session.RestAsync(); + syncWithPlayer = true; + continue; + } + + IMateEntity mateEntity = session.PlayerEntity.MateComponent.GetMate(x => x.Id == subPacket.UserId); + if (mateEntity == null) + { + continue; + } + + await session.EmitEventAsync(new MateRestEvent + { + MateEntity = mateEntity, + Rest = syncWithPlayer ? session.PlayerEntity.IsSitting : !mateEntity.IsSitting + }); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/TitleEquipPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/TitleEquipPacketHandler.cs new file mode 100644 index 0000000..4283375 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/TitleEquipPacketHandler.cs @@ -0,0 +1,72 @@ +using System.Threading.Tasks; +using WingsEmu.DTOs.Titles; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; +using WingsEmu.Packets.Enums.Titles; + +namespace WingsEmu.Plugins.PacketHandling.Game.Basic; + +public sealed class TitleEquipPacketHandler : GenericGamePacketHandlerBase +{ + private readonly IBCardEffectHandlerContainer _bcardHandler; + private readonly IItemsManager _itemsManager; + private readonly IGameLanguageService _languageService; + + public TitleEquipPacketHandler(IGameLanguageService languageService, IItemsManager itemsManager, IBCardEffectHandlerContainer bcardHandler) + { + _languageService = languageService; + _itemsManager = itemsManager; + _bcardHandler = bcardHandler; + } + + protected override async Task HandlePacketAsync(IClientSession session, TitEqPacket packet) + { + CharacterTitleDto targetTitle = session.PlayerEntity.Titles.Find(x => x.ItemVnum == packet.ItemVnum); + if (targetTitle == null) + { + return; + } + + switch (packet.Type) + { + case TitEqPacketType.EquipAsEffect: + { + CharacterTitleDto effectTitle = session.PlayerEntity.Titles.Find(x => x.IsEquipped); + if (effectTitle != null && targetTitle != effectTitle) + { + effectTitle.IsEquipped = false; + session.PlayerEntity.RefreshTitleBCards(_itemsManager, effectTitle, _bcardHandler, true); + } + + targetTitle.IsEquipped = !targetTitle.IsEquipped; + + session.PlayerEntity.RefreshTitleBCards(_itemsManager, targetTitle, _bcardHandler, !targetTitle.IsEquipped); + + session.SendInfo(_languageService.GetLanguage(targetTitle.IsEquipped ? GameDialogKey.TITLE_INFO_EFFECT_ENABLED : GameDialogKey.TITLE_INFO_EFFECT_DISABLED, session.UserLanguage)); + break; + } + case TitEqPacketType.EquipAsVisible: + { + CharacterTitleDto visibleTitle = session.PlayerEntity.Titles.Find(x => x.IsVisible); + if (visibleTitle != null && targetTitle != visibleTitle) + { + visibleTitle.IsVisible = false; + } + + targetTitle.IsVisible = !targetTitle.IsVisible; + session.SendInfo(_languageService.GetLanguage(targetTitle.IsVisible ? GameDialogKey.TITLE_INFO_VISIBLE_ENABLED : GameDialogKey.TITLE_INFO_VISIBLE_DISABLED, session.UserLanguage)); + break; + } + } + + session.RefreshStatChar(); + session.RefreshStat(); + session.SendCondPacket(); + session.BroadcastTitleInfo(); + session.SendTitlePacket(); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/WalkPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/WalkPacketHandler.cs new file mode 100644 index 0000000..f60233f --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/WalkPacketHandler.cs @@ -0,0 +1,189 @@ +using System; +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.CharacterExtensions; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Networking.Broadcasting; +using WingsEmu.Packets.ClientPackets; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.PacketHandling.Game.Basic; + +public class WalkPacketHandler : GenericGamePacketHandlerBase +{ + private readonly IGameLanguageService _gameLanguage; + private readonly IMeditationManager _meditationManager; + private readonly ISacrificeManager _sacrificeManager; + + public WalkPacketHandler(ISacrificeManager sacrificeManager, IGameLanguageService gameLanguage, IMeditationManager meditationManager) + { + _sacrificeManager = sacrificeManager; + _gameLanguage = gameLanguage; + _meditationManager = meditationManager; + } + + protected override async Task HandlePacketAsync(IClientSession session, WalkPacket walkPacket) + { + DateTime actualTime = DateTime.UtcNow; + + if (!session.HasCurrentMapInstance) + { + return; + } + + if (walkPacket.Speed != session.PlayerEntity.Speed) + { + session.SendCondPacket(); + } + + if (session.CurrentMapInstance.IsBlockedZone(walkPacket.XCoordinate, walkPacket.YCoordinate) || session.PlayerEntity.HasShopOpened || session.PlayerEntity.IsInExchange()) + { + BeforeReturnLogic(session); + return; + } + + if (!session.PlayerEntity.CanPerformMove()) + { + session.RefreshStat(); + } + + (bool IsValid, Position Position) calculatedPosition = GetExpectedClientSidePosition(session, actualTime); + Position expectedClientsidePosition = calculatedPosition.IsValid ? calculatedPosition.Position : session.PlayerEntity.Position; + + double distance = expectedClientsidePosition.GetDoubleDistance(walkPacket.XCoordinate, walkPacket.YCoordinate); + int speed = session.PlayerEntity.Speed < 1 ? 1 : session.PlayerEntity.Speed; + double expectedMaximumDistance = speed * 0.4 + 3 + 2; // we sum 3 for basic distance and 2 for error margin + double waitingtime = distance / speed * 2.5; + if (distance > expectedMaximumDistance + (session.PlayerEntity.IsOnVehicle ? 3 : 0)) // margin error for vehicles + { + BeforeReturnLogic(session); + return; + } + + session.PlayerEntity.RemoveMeditation(_meditationManager); + + session.PlayerEntity.LastWalk = new LastWalk + { + MapId = session.PlayerEntity.MapId, + StartPosition = expectedClientsidePosition, + EndPosition = new Position(walkPacket.XCoordinate, walkPacket.YCoordinate), + WalkTimeStart = actualTime, + WalkTimeEnd = actualTime.AddSeconds(waitingtime) + }; + + if (session.PlayerEntity.MapInstance.HasMapFlag(MapFlags.IS_BASE_MAP) && session.PlayerEntity.MapInstance.MapInstanceType != MapInstanceType.TimeSpaceInstance) + { + session.PlayerEntity.MapX = walkPacket.XCoordinate; + session.PlayerEntity.MapY = walkPacket.YCoordinate; + } + + session.PlayerEntity.ChangePosition(new Position(walkPacket.XCoordinate, walkPacket.YCoordinate)); + + await CheckNobleGesture(session); + await CheckSpiritOfSacrifice(session); + + if (!session.PlayerEntity.CheatComponent.IsInvisible) + { + session.BroadcastMovement(session.PlayerEntity, new ExceptSessionBroadcast(session)); + } + + session.SendCondPacket(); + session.PlayerEntity.LastMove = actualTime; + } + + private (bool IsValid, Position Position) GetExpectedClientSidePosition(IClientSession session, DateTime actualTime) + { + if (session.PlayerEntity.LastSitting.AddSeconds(1) < actualTime || session.PlayerEntity.LastWalk.WalkTimeEnd < actualTime || session.PlayerEntity.LastWalk.MapId != session.PlayerEntity.MapId + || session.PlayerEntity.PositionX != session.PlayerEntity.LastWalk.EndPosition.X || session.PlayerEntity.PositionY != session.PlayerEntity.LastWalk.EndPosition.Y) + { + return (false, default); + } + + double expectedPositionRatio = (actualTime - session.PlayerEntity.LastWalk.WalkTimeStart) / (session.PlayerEntity.LastWalk.WalkTimeEnd - session.PlayerEntity.LastWalk.WalkTimeStart); + + short x = GetExpectedClientSideCoordinate(session.PlayerEntity.LastWalk.StartPosition.X, session.PlayerEntity.LastWalk.EndPosition.X, expectedPositionRatio); + short y = GetExpectedClientSideCoordinate(session.PlayerEntity.LastWalk.StartPosition.Y, session.PlayerEntity.LastWalk.EndPosition.Y, expectedPositionRatio); + + return (true, new Position(x, y)); + } + + private short GetExpectedClientSideCoordinate(short xStart, short xEnd, double ratio) => Convert.ToInt16((xEnd - xStart) * ratio + xStart); + + private void BeforeReturnLogic(IClientSession session) + { + session.SendCondPacket(); + session.BroadcastTeleportPacket(); + } + + private async Task CheckSpiritOfSacrifice(IClientSession session) + { + if (!session.PlayerEntity.HasBuff(BuffVnums.SPIRIT_OF_SACRIFICE)) + { + return; + } + + IBattleEntity target = _sacrificeManager.GetTarget(session.PlayerEntity); + if (target == null || target.GetDistance(session.PlayerEntity) <= 14) + { + return; + } + + // WARNING + if (session.PlayerEntity.GetDistance(target) == 15) + { + string message; + if (target is IPlayerEntity character) + { + message = _gameLanguage.GetLanguage(GameDialogKey.SKILL_SHOUTMESSAGE_SACRIFICE_WARNING, character.Session.UserLanguage); + character.Session.SendMsg(message, MsgMessageType.SmallMiddle); + } + + message = _gameLanguage.GetLanguage(GameDialogKey.SKILL_SHOUTMESSAGE_SACRIFICE_WARNING, session.UserLanguage); + session.SendMsg(message, MsgMessageType.SmallMiddle); + } + else + { + await session.PlayerEntity.RemoveSacrifice(target, _sacrificeManager, _gameLanguage); + } + } + + private async Task CheckNobleGesture(IClientSession session) + { + if (!session.PlayerEntity.HasBuff(BuffVnums.NOBLE_GESTURE)) + { + return; + } + + IBattleEntity caster = _sacrificeManager.GetCaster(session.PlayerEntity); + if (caster == null || caster.GetDistance(session.PlayerEntity) <= 14) + { + return; + } + + // WARNING + if (caster.GetDistance(session.PlayerEntity) == 15) + { + string message; + if (caster is IPlayerEntity character) + { + message = _gameLanguage.GetLanguage(GameDialogKey.SKILL_SHOUTMESSAGE_SACRIFICE_WARNING, character.Session.UserLanguage); + character.Session.SendMsg(message, MsgMessageType.SmallMiddle); + } + + message = _gameLanguage.GetLanguage(GameDialogKey.SKILL_SHOUTMESSAGE_SACRIFICE_WARNING, session.UserLanguage); + session.SendMsg(message, MsgMessageType.SmallMiddle); + } + else + { + await caster.RemoveSacrifice(session.PlayerEntity, _sacrificeManager, _gameLanguage); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/WhisperPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/WhisperPacketHandler.cs new file mode 100644 index 0000000..8662354 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Basic/WhisperPacketHandler.cs @@ -0,0 +1,31 @@ +using System.Linq; +using System.Threading.Tasks; +using WingsEmu.Game.InterChannel; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.Game.Basic; + +public class WhisperPacketHandler : GenericGamePacketHandlerBase +{ + protected override async Task HandlePacketAsync(IClientSession session, WhisperPacket whisperPacket) + { + if (string.IsNullOrEmpty(whisperPacket.Message) || whisperPacket.Message.Length < 2) + { + return; + } + + string[] messageSplit = whisperPacket.Message.Split(' '); + string characterName = messageSplit[0]; + string message = string.Join(" ", messageSplit.Skip(1)); + + if (message.Length > 60) + { + message = message.Substring(0, 60); + } + + message = message.Trim(); + + await session.EmitEventAsync(new InterChannelSendWhisperEvent(characterName, message)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Battle/MultitargetListPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Battle/MultitargetListPacketHandler.cs new file mode 100644 index 0000000..4ff0266 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Battle/MultitargetListPacketHandler.cs @@ -0,0 +1,51 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Linq; +using System.Threading.Tasks; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; +using WingsEmu.Packets.Enums.Battle; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.PacketHandling.Game.Battle; + +public class MultiTargetListPacketHandler : GenericGamePacketHandlerBase +{ + private readonly IGameLanguageService _gameLanguage; + private readonly ISkillUsageManager _skillUsageManager; + + public MultiTargetListPacketHandler(IGameLanguageService gameLanguage, ISkillUsageManager skillUsageManager) + { + _gameLanguage = gameLanguage; + _skillUsageManager = skillUsageManager; + } + + protected override async Task HandlePacketAsync(IClientSession session, MultiTargetListPacket packet) + { + if (session.IsMuted()) + { + session.SendMuteMessage(); + return; + } + + if ((DateTime.UtcNow - session.PlayerEntity.LastTransform).TotalSeconds < 3) + { + session.SendCancelPacket(CancelType.NotInCombatMode); + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.SPECIALIST_SHOUTMESSAGE_CANT_ATTACK_YET, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (packet.TargetsAmount <= 0 || packet.TargetsAmount != packet.Targets.Count || packet.Targets == null) + { + return; + } + + _skillUsageManager.SetMultiTargets(session.PlayerEntity.Id, packet.Targets.Select(x => (x.TargetType, (long)x.TargetId)).ToList()); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Battle/ObaPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Battle/ObaPacketHandler.cs new file mode 100644 index 0000000..509b21c --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Battle/ObaPacketHandler.cs @@ -0,0 +1,100 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Threading.Tasks; +using WingsEmu.DTOs.Skills; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Skills; +using WingsEmu.Packets.ClientPackets; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.PacketHandling.Game.Battle; + +public class ObaPacketHandler : GenericGamePacketHandlerBase +{ + private readonly IGameLanguageService _gameLanguage; + private readonly ISkillsManager _skillsManager; + private readonly ISpyOutManager _spyOutManager; + + public ObaPacketHandler(IGameLanguageService gameLanguage, ISkillsManager skillsManager, ISpyOutManager spyOutManager) + { + _gameLanguage = gameLanguage; + _skillsManager = skillsManager; + _spyOutManager = spyOutManager; + } + + protected override async Task HandlePacketAsync(IClientSession session, ObaPacket packet) + { + if (!session.PlayerEntity.UseSp) + { + return; + } + + if (session.PlayerEntity.Specialist == null) + { + return; + } + + if (!session.PlayerEntity.IsAlive()) + { + return; + } + + if (!_spyOutManager.ContainsSpyOut(session.PlayerEntity.Id)) + { + return; + } + + _spyOutManager.RemoveSpyOutSkill(session.PlayerEntity.Id); + + if (!session.PlayerEntity.CanPerformAttack()) + { + return; + } + + if (session.IsMuted()) + { + session.SendMuteMessage(); + return; + } + + if (session.PlayerEntity.IsOnVehicle) + { + return; + } + + session.SendObArPacket(); + (long targetId, VisualType targetType) = _spyOutManager.GetSpyOutTarget(session.PlayerEntity.Id); + IBattleEntity targetEntity = session.CurrentMapInstance.GetBattleEntity(targetType, targetId); + if (targetEntity == null) + { + return; + } + + if (!targetEntity.IsAlive()) + { + return; + } + + if (session.CurrentMapInstance.IsPvp && targetEntity.IsInPvpZone()) + { + return; + } + + SkillDTO skill = _skillsManager.GetSkill((short)SkillsVnums.SPY_OUT_SKILL); + SkillInfo skillInfo = skill.GetInfo(); + skillInfo.Vnum = -1; + skillInfo.CastId = -1; + session.SendEffectObject(targetEntity, false, EffectType.Sp6ArcherTargetFalcon); + await session.PlayerEntity.EmitEventAsync(new BattleExecuteSkillEvent(session.PlayerEntity, targetEntity, skillInfo, DateTime.UtcNow.AddSeconds(-10))); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Battle/UseAtSkillPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Battle/UseAtSkillPacketHandler.cs new file mode 100644 index 0000000..9edf674 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Battle/UseAtSkillPacketHandler.cs @@ -0,0 +1,232 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.Quicklist; +using WingsEmu.DTOs.Skills; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Skills; +using WingsEmu.Packets.ClientPackets; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Battle; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.PacketHandling.Game.Battle; + +public class UseAtSkillPacketHandler : GenericGamePacketHandlerBase +{ + private readonly ICardsManager _cardsManager; + private readonly IGameLanguageService _gameLanguage; + private readonly RainbowBattleConfiguration _rainbowBattleConfiguration; + private readonly ISkillsManager _skillsManager; + + public UseAtSkillPacketHandler(IGameLanguageService gameLanguage, ICardsManager cardsManager, ISkillsManager skillsManager, RainbowBattleConfiguration rainbowBattleConfiguration) + { + _gameLanguage = gameLanguage; + _cardsManager = cardsManager; + _skillsManager = skillsManager; + _rainbowBattleConfiguration = rainbowBattleConfiguration; + } + + protected override async Task HandlePacketAsync(IClientSession session, UseAtSkillPacket packet) + { + IPlayerEntity character = session.PlayerEntity; + + if (!character.CanFight() || packet == null) + { + session.SendCancelPacket(CancelType.NotInCombatMode); + session.SendDebugMessage("[U_AS] !canFight, packet null"); + return; + } + + if (session.CurrentMapInstance.IsPvp && session.CurrentMapInstance.PvpZone(session.PlayerEntity.PositionX, session.PlayerEntity.PositionY) && + session.CurrentMapInstance.PvpZone(packet.MapX, packet.MapY)) + { + session.SendCancelPacket(CancelType.NotInCombatMode); + session.SendDebugMessage("[U_AS] Character no-PvP zone / !map.IsPvp"); + return; + } + + if (session.CurrentMapInstance.IsBlockedZone(packet.MapX, packet.MapY)) + { + session.SendDebugMessage("[U_AS] character.IsBlockedZone()"); + session.SendCancelPacket(CancelType.NotInCombatMode); + return; + } + + if (session.IsMuted()) + { + session.SendMuteMessage(); + session.SendCancelPacket(CancelType.NotInCombatMode); + return; + } + + if (session.PlayerEntity.IsOnVehicle || session.PlayerEntity.CheatComponent.IsInvisible) + { + session.SendDebugMessage("[U_AS] character.IsVehicled, InvisibleGm"); + session.SendCancelPacket(CancelType.NotInCombatMode); + return; + } + + if (!session.PlayerEntity.CanPerformAttack()) + { + session.SendDebugMessage("[U_AS] !CanPerformAttack"); + session.SendCancelPacket(CancelType.NotInCombatMode); + return; + } + + if ((DateTime.UtcNow - session.PlayerEntity.LastTransform).TotalSeconds < 3) + { + session.SendDebugMessage("[U_AS] Under transformation cooldown"); + session.SendCancelPacket(CancelType.NotInCombatMode); + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.SPECIALIST_SHOUTMESSAGE_CANT_ATTACK_YET, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (character.IsCastingSkill) + { + session.SendCancelPacket(CancelType.NotInCombatMode); + session.SendDebugMessage("[U_AS] Already using a skill"); + return; + } + + List skills = session.PlayerEntity.Skills; + var characterSkill = skills.FirstOrDefault(s => s.Skill?.CastId == packet.CastId && + (s.Skill?.UpgradeSkill == 0 || s.Skill?.SkillType == SkillType.NormalPlayerSkill) && s.Skill.TargetType == TargetType.NonTarget) as CharacterSkill; + SkillDTO skill = characterSkill?.Skill; + + if (skill == null) + { + session.SendDebugMessage("[U_AS] Skill does not exist"); + session.SendCancelPacket(CancelType.NotInCombatMode); + return; + } + + SkillInfo skillInfo = session.PlayerEntity.GetUpgradedSkill(skill, _cardsManager, _skillsManager) ?? skill.GetInfo(); + + bool canBeUsed = session.PlayerEntity.CharacterCanCastOrCancel(characterSkill, _gameLanguage, skillInfo, false); + if (!canBeUsed) + { + session.SendDebugMessage("[U_AS] !skill.canBeUsed"); + return; + } + + if (!session.PlayerEntity.CanPerformAttack(skillInfo)) + { + session.SendCancelPacket(CancelType.NotInCombatMode); + return; + } + + if (session.PlayerEntity.IsSitting) + { + await session.RestAsync(); + } + + var position = new Position(packet.MapX, packet.MapY); + + if (!session.PlayerEntity.Position.IsInRange(position, skillInfo.Range + 3)) + { + session.SendDebugMessage("[U_AS] !character.IsInRange"); + session.SendCancelPacket(CancelType.NotInCombatMode); + return; + } + + if (session.PlayerEntity.RainbowBattleComponent.IsFrozen) + { + session.SendCancelPacket(CancelType.NotInCombatMode); + return; + } + + if (!session.PlayerEntity.CheatComponent.HasGodMode) + { + skillInfo.ManaCost = session.PlayerEntity.BCardComponent.HasBCard(BCardType.TimeCircleSkills, (byte)AdditionalTypes.TimeCircleSkills.DisableMPConsumption) ? 0 : skillInfo.ManaCost; + if (session.PlayerEntity.AdditionalMp > 0) + { + int removedAdditionalMp; + if (session.PlayerEntity.AdditionalMp > skillInfo.ManaCost) + { + removedAdditionalMp = skillInfo.ManaCost; + } + else + { + removedAdditionalMp = session.PlayerEntity.AdditionalMp; + + int overflow = Math.Abs(session.PlayerEntity.AdditionalMp - skillInfo.ManaCost); + session.PlayerEntity.Mp -= overflow; + } + + await session.EmitEventAsync(new RemoveAdditionalHpMpEvent + { + Mp = removedAdditionalMp + }); + } + else + { + session.PlayerEntity.RemoveEntityMp((short)skillInfo.ManaCost, skill); + } + + (int firstDataNegative, int _) = session.PlayerEntity.BCardComponent.GetAllBCardsInformation(BCardType.HealingBurningAndCasting, + (byte)AdditionalTypes.HealingBurningAndCasting.HPDecreasedByConsumingMP, session.PlayerEntity.Level); + + (int firstDataPositive, int _) = session.PlayerEntity.BCardComponent.GetAllBCardsInformation(BCardType.HealingBurningAndCasting, + (byte)AdditionalTypes.HealingBurningAndCasting.HPIncreasedByConsumingMP, session.PlayerEntity.Level); + + int hpRemoved = (int)(firstDataPositive / 100.0 * skillInfo.ManaCost - firstDataNegative / 100.0 * skillInfo.ManaCost); + + if (hpRemoved > 0) + { + await session.PlayerEntity.EmitEventAsync(new BattleEntityHealEvent + { + Entity = session.PlayerEntity, + HpHeal = hpRemoved + }); + } + else + { + if (session.PlayerEntity.Hp - hpRemoved <= 0) + { + session.PlayerEntity.BroadcastDamage(session.PlayerEntity.Hp - 1); + session.PlayerEntity.Hp = 1; + } + else + { + session.PlayerEntity.BroadcastDamage(hpRemoved); + session.PlayerEntity.Hp -= hpRemoved; + } + } + + session.RefreshStat(); + } + + session.SendDebugMessage($"Hit {skill.HitType} / Target {skill.TargetType} / Attack Type {skill.AttackType} / Affected entities {skill.TargetAffectedEntities}"); + session.SendCancelPacket(CancelType.NotInCombatMode); + + session.PlayerEntity.CleanComboState(); + session.SendMsCPacket(0); + session.RefreshQuicklist(); + session.RefreshStat(); + session.PlayerEntity.ClearFoodBuffer(); + + if (session.PlayerEntity.RainbowBattleComponent.IsInRainbowBattle) + { + session.PlayerEntity.RainbowBattleComponent.ActivityPoints += _rainbowBattleConfiguration.UsingSkillActivityPoints; + } + + character.WeaponLoaded(characterSkill, _gameLanguage, true); + character.LastSkillUse = DateTime.UtcNow; + DateTime castTime = character.GenerateSkillCastTime(skillInfo); + character.SkillComponent.CanBeInterrupted = character.CanBeInterrupted(skillInfo); + + await character.EmitEventAsync(new BattleExecuteSkillEvent(character, null, skillInfo, castTime, position)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Battle/UseSkillPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Battle/UseSkillPacketHandler.cs new file mode 100644 index 0000000..9333e9d --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Battle/UseSkillPacketHandler.cs @@ -0,0 +1,571 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsAPI.Game.Extensions.ItemExtension.Item; +using WingsAPI.Game.Extensions.Quicklist; +using WingsEmu.DTOs.Skills; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Skills; +using WingsEmu.Packets.ClientPackets; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Battle; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.PacketHandling.Game.Battle; + +public class UseSkillPacketHandler : GenericGamePacketHandlerBase +{ + private readonly ICardsManager _cardsManager; + private readonly IDelayManager _delayManager; + private readonly IGameLanguageService _gameLanguage; + private readonly IItemsManager _items; + private readonly RainbowBattleConfiguration _rainbowBattleConfiguration; + private readonly ISkillsManager _skillsManager; + + public UseSkillPacketHandler(IGameLanguageService gameLanguage, IDelayManager delayManager, IItemsManager items, ICardsManager cardsManager, ISkillsManager skillsManager, + RainbowBattleConfiguration rainbowBattleConfiguration) + { + _gameLanguage = gameLanguage; + _delayManager = delayManager; + _items = items; + _cardsManager = cardsManager; + _skillsManager = skillsManager; + _rainbowBattleConfiguration = rainbowBattleConfiguration; + } + + protected override async Task HandlePacketAsync(IClientSession session, UseSkillPacket packet) + { + session.SendDebugMessage("[U_S] Start u_s"); + IPlayerEntity character = session.PlayerEntity; + + if (!BasicChecks(session, packet)) + { + return; + } + + List skills = session.PlayerEntity.Skills; + var characterSkill = skills.FirstOrDefault(s => s.Skill?.CastId == packet.CastId + && (s.Skill?.UpgradeSkill == 0 || s.Skill?.SkillType == SkillType.NormalPlayerSkill) && s.Skill.TargetType != TargetType.NonTarget) as CharacterSkill; + SkillDTO skill = characterSkill?.Skill; + + if (skill == null) + { + session.SendCancelPacket(CancelType.NotInCombatMode); + session.SendDebugMessage("[U_S] Skill does not exist"); + return; + } + + if (skill.ItemVNum != 0 && !session.PlayerEntity.HasItem(skill.ItemVNum)) + { + session.SendCancelPacket(CancelType.NotInCombatMode); + IGameItem gameItem = _items.GetItem(skill.ItemVNum); + if (gameItem == null) + { + return; + } + + string itemName = gameItem.GetItemName(_gameLanguage, session.UserLanguage); + session.SendInformationChatMessage(_gameLanguage.GetLanguageFormat(GameDialogKey.INVENTORY_SHOUTMESSAGE_NOT_ENOUGH_ITEMS, session.UserLanguage, 1, itemName)); + return; + } + + SkillInfo skillInfo = session.PlayerEntity.GetUpgradedSkill(skill, _cardsManager, _skillsManager) ?? skill.GetInfo(); + + if (skillInfo.AoERange != 0 && skill.CastId != 0 && skill.CastId != 1 && session.PlayerEntity.HasBuff(BuffVnums.EXPLOSIVE_ENCHACMENT)) + { + (int firstData, int secondData) buff = session.PlayerEntity.BCardComponent.GetAllBCardsInformation(BCardType.FireCannoneerRangeBuff, + (byte)AdditionalTypes.FireCannoneerRangeBuff.AOEIncreased, session.PlayerEntity.Level); + skillInfo.AoERange += (byte)buff.firstData; + skillInfo.HitEffect = session.PlayerEntity.GetCannoneerHitEffect(skill.CastId); + } + + skillInfo.Range += (byte)session.PlayerEntity.BCardComponent.GetAllBCardsInformation(BCardType.FearSkill, (byte)AdditionalTypes.FearSkill.AttackRangedIncreased, session.PlayerEntity.Level) + .Item1; + + bool canBeUsed = character.CharacterCanCastOrCancel(characterSkill, _gameLanguage, skillInfo, false); + bool comboSkill = skill.CastId > 10 && character.UseSp && skill.SpecialCost == 999; + + if (!canBeUsed) + { + session.SendCancelPacket(CancelType.NotInCombatMode); + session.SendDebugMessage("[U_S] !canBeUsed"); + return; + } + + if (!session.PlayerEntity.CanPerformAttack(skillInfo)) + { + session.SendCancelPacket(CancelType.NotInCombatMode); + return; + } + + if (comboSkill) + { + ComboSkillState comboSkillState = session.PlayerEntity.GetComboState(); + if (comboSkillState == null) + { + session.SendCancelPacket(CancelType.NotInCombatMode); + session.SendDebugMessage("[U_S] comboSkills == null"); + return; + } + + bool canCastComboSkill = comboSkillState.LastSkillByCastId == skillInfo.CastId; + if (!canCastComboSkill) + { + session.SendCancelPacket(CancelType.NotInCombatMode); + session.SendDebugMessage("[U_S] comboSkill && comboSkill == null"); + return; + } + + if (session.PlayerEntity.AngelElement.HasValue) + { + if (!character.HasBuff(BuffVnums.MAGIC_SPELL)) + { + session.SendCancelPacket(CancelType.NotInCombatMode); + session.SendDebugMessage("[U_S] Character without MAGIC_SPELL"); + return; + } + + ElementType? elementType = character.GetBuffElementType((short)skill.Id); + + if (!elementType.HasValue) + { + session.SendCancelPacket(CancelType.NotInCombatMode); + session.SendDebugMessage("[U_S] ElementType == null"); + return; + } + + if (session.PlayerEntity.AngelElement.Value != elementType.Value) + { + session.SendCancelPacket(CancelType.NotInCombatMode); + session.SendDebugMessage("[U_S] Element != skill.Element"); + return; + } + } + } + + IBattleEntity target = character.MapInstance.GetBattleEntity(packet.VisualType, packet.MapMonsterId); + + if (await TargetChecks(session, target, skillInfo)) + { + return; + } + + if (session.PlayerEntity.RainbowBattleComponent.IsFrozen) + { + session.SendCancelPacket(CancelType.NotInCombatMode); + return; + } + + if (skillInfo.TargetType == TargetType.Self || skillInfo.TargetType == TargetType.SelfOrTarget && target.Id == character.Id) + { + session.SendDebugMessage("[U_S] Target = character"); + target = character; + } + + if (skillInfo.TargetAffectedEntities == TargetAffectedEntities.BuffForAllies && character.IsEnemyWith(target)) + { + target = character; + } + + int cellSizeBonus = target switch + { + IPlayerEntity => 7, + _ => 3 + }; + + if (target is INpcMonsterEntity npcMonsterEntity) + { + cellSizeBonus += npcMonsterEntity.CellSize; + } + + if (!character.Position.IsInRange(target.Position, skillInfo.Range + cellSizeBonus) && skillInfo.AttackType != AttackType.Dash) + { + session.SendCancelPacket(CancelType.NotInCombatMode); + session.SendDebugMessage($"[U_S] Out of range {character.Position.GetDistance(target.Position)} - {skill.Range}"); + return; + } + + if (target is IMonsterEntity mob) + { + if (!session.PlayerEntity.CanMonsterBeAttacked(mob) && !mob.IsMateTrainer) + { + session.SendCancelPacket(CancelType.NotInCombatMode); + return; + } + + if (mob.MonsterRaceType == MonsterRaceType.Fixed) + { + session.SendCancelPacket(CancelType.NotInCombatMode); + return; + } + } + + // it should check before taking Mp/Hp + if (character.IsAllyWith(target) && skillInfo.TargetType != TargetType.Self && skillInfo.TargetType != TargetType.SelfOrTarget && target.Id != character.Id && + skillInfo.TargetAffectedEntities != TargetAffectedEntities.BuffForAllies) + { + character.CancelCastingSkill(); + character.Session.SendDebugMessage("[U_S] !caster.IsEnemyWith(target) && caster.IsPlayer() && skill.TargetType != TargetType.Self"); + return; + } + + if (!character.IsEnemyWith(target) && skillInfo.TargetType != TargetType.Self && skillInfo.TargetType != TargetType.SelfOrTarget) + { + character.CancelCastingSkill(); + return; + } + + Position positionAfterDash = default; + + if (packet.MapX.HasValue && packet.MapY.HasValue) + { + if (skillInfo.AttackType != AttackType.Dash) + { + session.SendDebugMessage("[U_S] Skill.AttackType != Dash"); + session.SendCancelPacket(CancelType.NotInCombatMode); + return; + } + + if (character.MapInstance.IsBlockedZone((int)packet.MapX, (int)packet.MapY)) + { + session.SendCancelPacket(CancelType.NotInCombatMode); + return; + } + + var newPosition = new Position((short)packet.MapX, (short)packet.MapY); + if (!character.Position.IsInRange(newPosition, skillInfo.Range + 2)) + { + session.SendDebugMessage("[U_S] newPosition !IsInRange"); + session.SendCancelPacket(CancelType.NotInCombatMode); + return; + } + + positionAfterDash = newPosition; + } + + await FinalChecks(session, skill, character, skillInfo, target, comboSkill); + + session.SendDebugMessage($"Hit {skillInfo.HitType} / Target {skillInfo.TargetType} / Attack Type {skillInfo.AttackType} / Affected entities {skillInfo.TargetAffectedEntities}"); + if (target is IPlayerEntity characterTarget) + { + session.SendDebugMessage($"Sender: {session.PlayerEntity.Name} -> Target: {characterTarget.Name} "); + } + + character.SkillComponent.CanBeInterrupted = false; + character.SkillComponent.IsSkillInterrupted = false; + character.SkillComponent.CanBeInterrupted = character.CanBeInterrupted(skillInfo); + character.WeaponLoaded(characterSkill, _gameLanguage, true); + DateTime castTime = character.GenerateSkillCastTime(skillInfo); + session.SendDebugMessage("[U_S] IsCasting = true"); + await character.EmitEventAsync(new BattleExecuteSkillEvent(character, target, skillInfo, castTime, positionAfterDash)); + } + + private async Task TargetChecks(IClientSession session, IBattleEntity target, SkillInfo skillInfo) + { + if (target == null) + { + session.SendCancelPacket(CancelType.NotInCombatMode); + session.SendDebugMessage("[U_S] No target"); + return true; + } + + if (target.MapInstance.IsPvp && session.CurrentMapInstance.PvpZone(target.PositionX, target.PositionY)) + { + session.SendDebugMessage("[U_S] Target no-PvP zone / !map.IsPvp"); + session.SendCancelPacket(CancelType.NotInCombatMode); + return true; + } + + switch (session.CurrentMapInstance.MapInstanceType) + { + case MapInstanceType.RainbowBattle: + + if (target is not IPlayerEntity rainbowFrozen) + { + break; + } + + if (session.PlayerEntity.IsEnemyWith(rainbowFrozen)) + { + break; + } + + if (!rainbowFrozen.RainbowBattleComponent.IsFrozen) + { + break; + } + + if (rainbowFrozen.RainbowBattleComponent.Team != session.PlayerEntity.RainbowBattleComponent.Team) + { + break; + } + + if (target.Position.GetDistance(session.PlayerEntity.Position) > 5) + { + break; + } + + session.SendCancelPacket(CancelType.NotInCombatMode); + + DateTime now = DateTime.UtcNow; + if (session.PlayerEntity.LastUnfreezedPlayer > now) + { + return true; + } + + session.PlayerEntity.LastUnfreezedPlayer = now.AddSeconds(5); + DateTime wait = await _delayManager.RegisterAction(session.PlayerEntity, DelayedActionType.RainbowBattleUnfreeze); + session.SendDelay((int)(wait - DateTime.UtcNow).TotalMilliseconds, GuriType.Unfreezing, $"guri 505 {rainbowFrozen.Id}"); + return true; + } + + if (target is INpcEntity && skillInfo.Vnum == (short)SkillsVnums.SACRIFICE) + { + session.SendCancelPacket(CancelType.NotInCombatMode); + session.SendDebugMessage("[U_S] MapNpc && Sacrifice"); + return true; + } + + if (!target.IsAlive()) + { + session.SendCancelPacket(CancelType.NotInCombatMode); + session.SendDebugMessage("[U_S] Target is dead"); + return true; + } + + if (target is not IMonsterEntity mob || mob.SummonerId == 0) + { + return false; + } + + if (mob.SummonerType is not VisualType.Player) + { + return false; + } + + if (session.PlayerEntity.CanMonsterBeAttacked(mob) && !mob.IsMateTrainer) + { + return false; + } + + session.SendDebugMessage("[U_S] mob.SummonerId != 0"); + session.SendCancelPacket(CancelType.NotInCombatMode); + return true; + } + + private async Task FinalChecks(IClientSession session, SkillDTO skill, IPlayerEntity character, SkillInfo skillInfo, IBattleEntity target, bool comboSkill) + { + if (!session.PlayerEntity.CheatComponent.HasGodMode) + { + skillInfo.ManaCost = session.PlayerEntity.BCardComponent.HasBCard(BCardType.TimeCircleSkills, (byte)AdditionalTypes.TimeCircleSkills.DisableMPConsumption) ? 0 : skillInfo.ManaCost; + if (session.PlayerEntity.AdditionalMp > 0) + { + int removedAdditionalMp; + if (session.PlayerEntity.AdditionalMp > skillInfo.ManaCost) + { + removedAdditionalMp = skillInfo.ManaCost; + } + else + { + removedAdditionalMp = session.PlayerEntity.AdditionalMp; + + int overflow = Math.Abs(session.PlayerEntity.AdditionalMp - skillInfo.ManaCost); + session.PlayerEntity.Mp -= overflow; + } + + await session.EmitEventAsync(new RemoveAdditionalHpMpEvent + { + Mp = removedAdditionalMp + }); + } + else + { + session.PlayerEntity.RemoveEntityMp((short)skillInfo.ManaCost, skill); + } + + (int firstDataNegative, int _) = session.PlayerEntity.BCardComponent.GetAllBCardsInformation(BCardType.HealingBurningAndCasting, + (byte)AdditionalTypes.HealingBurningAndCasting.HPDecreasedByConsumingMP, session.PlayerEntity.Level); + + (int firstDataPositive, int _) = session.PlayerEntity.BCardComponent.GetAllBCardsInformation(BCardType.HealingBurningAndCasting, + (byte)AdditionalTypes.HealingBurningAndCasting.HPIncreasedByConsumingMP, session.PlayerEntity.Level); + + int hpRemoved = (int)(firstDataPositive / 100.0 * skillInfo.ManaCost - firstDataNegative / 100.0 * skillInfo.ManaCost); + + if (hpRemoved > 0) + { + await session.PlayerEntity.EmitEventAsync(new BattleEntityHealEvent + { + Entity = session.PlayerEntity, + HpHeal = hpRemoved + }); + } + else + { + if (session.PlayerEntity.Hp - hpRemoved <= 0) + { + session.PlayerEntity.BroadcastDamage(session.PlayerEntity.Hp - 1); + session.PlayerEntity.Hp = 1; + } + else + { + session.PlayerEntity.BroadcastDamage(hpRemoved); + session.PlayerEntity.Hp -= hpRemoved; + } + } + + session.RefreshStat(); + session.SendDebugMessage($"[U_S] MpCost: {skillInfo.ManaCost}"); + } + + if (skill.Id == (short)SkillsVnums.SPY_OUT) + { + session.SendEffectObject(target, true, EffectType.Sp6ArcherTargetFalcon); + } + + if (skill.ItemVNum != 0) + { + await session.RemoveItemFromInventory(skill.ItemVNum); + } + + if (session.PlayerEntity.RainbowBattleComponent.IsInRainbowBattle) + { + session.PlayerEntity.RainbowBattleComponent.ActivityPoints += _rainbowBattleConfiguration.UsingSkillActivityPoints; + } + + character.LastSkillUse = DateTime.UtcNow; + character.ClearFoodBuffer(); + ComboSkillState newState = null; + if (!comboSkill && skill.CastId != 0) + { + newState = new ComboSkillState + { + State = 0 + }; + } + + if (skill.CastId < 11 && skill.CastId != 0 && newState != null) + { + newState.OriginalSkillCastId = (byte)skill.CastId; + } + + session.RefreshStat(); + if (newState != null && !session.PlayerEntity.AngelElement.HasValue) + { + session.PlayerEntity.SaveComboSkill(newState); + } + + if (skill.CastId == 0) + { + return; + } + + ComboSkillState state = session.PlayerEntity.GetComboState(); + bool sendBuffIconWindow = session.PlayerEntity.AngelElement.HasValue && character.Specialist is { SpLevel: > 19 } && character.HasBuff(BuffVnums.MAGIC_SPELL); + + if (state != null) + { + if (state.State >= 10) + { + session.PlayerEntity.CleanComboState(); + } + else + { + if (comboSkill) + { + session.PlayerEntity.IncreaseComboState((byte)skill.CastId); + } + else if (newState == null) + { + session.PlayerEntity.CleanComboState(); + } + } + + if (sendBuffIconWindow) + { + character.Session.SendMSlotPacket(state.OriginalSkillCastId); + } + } + + if (sendBuffIconWindow) + { + session.RefreshQuicklist(); + return; + } + + session.SendMsCPacket(0); + session.RefreshQuicklist(); + } + + private bool BasicChecks(IClientSession session, UseSkillPacket packet) + { + IPlayerEntity character = session.PlayerEntity; + + if (!character.CanFight() || packet == null) + { + session.SendCancelPacket(CancelType.NotInCombatMode); + session.SendDebugMessage("[U_S] !canFight, packet null"); + return false; + } + + if (!session.PlayerEntity.CanPerformAttack()) + { + session.SendDebugMessage("[U_S] !CanPerformAttack"); + session.SendCancelPacket(CancelType.NotInCombatMode); + return false; + } + + if (session.CurrentMapInstance.IsPvp && session.CurrentMapInstance.PvpZone(session.PlayerEntity.PositionX, session.PlayerEntity.PositionY)) + { + session.SendCancelPacket(CancelType.NotInCombatMode); + session.SendDebugMessage("[U_S] Character no-PvP zone / !map.IsPvp"); + return false; + } + + if (session.IsMuted()) + { + session.SendMuteMessage(); + session.SendCancelPacket(CancelType.NotInCombatMode); + return false; + } + + if (session.PlayerEntity.IsOnVehicle || session.PlayerEntity.CheatComponent.IsInvisible) + { + session.SendDebugMessage("[U_S] IsVehicled, InvisibleGm"); + session.SendCancelPacket(CancelType.NotInCombatMode); + return false; + } + + if ((DateTime.UtcNow - session.PlayerEntity.LastTransform).TotalSeconds < 3) + { + session.SendCancelPacket(CancelType.NotInCombatMode); + session.SendDebugMessage("[U_S] Under transformation cooldown"); + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.SPECIALIST_SHOUTMESSAGE_CANT_ATTACK_YET, session.UserLanguage), MsgMessageType.Middle); + return false; + } + + if (!character.IsCastingSkill) + { + return true; + } + + session.SendCancelPacket(CancelType.NotInCombatMode); + session.SendDebugMessage("[U_S] Already using a skill"); + return false; + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Bazaar/CScaclcPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Bazaar/CScaclcPacketHandler.cs new file mode 100644 index 0000000..9cc2c20 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Bazaar/CScaclcPacketHandler.cs @@ -0,0 +1,14 @@ +using System.Threading.Tasks; +using WingsEmu.Game.Bazaar.Events; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.Game.Bazaar; + +public class CScaclcPacketHandler : GenericGamePacketHandlerBase +{ + protected override async Task HandlePacketAsync(IClientSession session, CScalcPacket packet) + { + await session.EmitEventAsync(new BazaarItemRemoveEvent(packet.BazaarId)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Bazaar/CSkillPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Bazaar/CSkillPacketHandler.cs new file mode 100644 index 0000000..f48e745 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Bazaar/CSkillPacketHandler.cs @@ -0,0 +1,41 @@ +using System.Threading.Tasks; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Bazaar.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.PacketHandling.Game.Bazaar; + +public class CSkillPacketHandler : GenericGamePacketHandlerBase +{ + private readonly IGameLanguageService _gameLanguage; + + public CSkillPacketHandler(IGameLanguageService gameLanguage) => _gameLanguage = gameLanguage; + + protected override async Task HandlePacketAsync(IClientSession session, CSkillPacket packet) + { + if (session.PlayerEntity.IsInExchange()) + { + return; + } + + if (session.PlayerEntity.HasShopOpened) + { + return; + } + + IMapInstance mapInstanceType = session.CurrentMapInstance; + + if (!mapInstanceType.HasMapFlag(MapFlags.IS_BASE_MAP) && !mapInstanceType.HasMapFlag(MapFlags.ACT_4)) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_MUST_BE_IN_CLASSIC_MAP, session.UserLanguage), MsgMessageType.Middle); + return; + } + + await session.EmitEventAsync(new BazaarOpenUiEvent(true)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Bazaar/CbListPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Bazaar/CbListPacketHandler.cs new file mode 100644 index 0000000..c42199a --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Bazaar/CbListPacketHandler.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using WingsEmu.DTOs.Bonus; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Bazaar.Configuration; +using WingsEmu.Game.Bazaar.Events; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.Game.Bazaar; + +public class CbListPacketHandler : GenericGamePacketHandlerBase +{ + private readonly BazaarConfiguration _bazaarConfiguration; + private readonly IGameLanguageService _languageService; + + public CbListPacketHandler(IGameLanguageService languageService, BazaarConfiguration bazaarConfiguration) + { + _languageService = languageService; + _bazaarConfiguration = bazaarConfiguration; + } + + protected override async Task HandlePacketAsync(IClientSession session, CbListPacket packet) + { + if (session.IsActionForbidden()) + { + session.CloseNosBazaarUi(); + return; + } + + if (session.PlayerEntity.IsInExchange()) + { + session.CloseNosBazaarUi(); + return; + } + + if (session.PlayerEntity.HasShopOpened) + { + session.CloseNosBazaarUi(); + return; + } + + if (session.PlayerEntity.IsShopping) + { + session.CloseNosBazaarUi(); + return; + } + + INpcEntity getNosBazaarNpc = session.CurrentMapInstance.GetPassiveNpcs().FirstOrDefault(x => x.ShopNpc is { ShopType: (byte)NpcShopType.NOS_BAZAAR }); + if (!session.PlayerEntity.HaveStaticBonus(StaticBonusType.BazaarMedalSilver) && !session.PlayerEntity.HaveStaticBonus(StaticBonusType.BazaarMedalGold) && getNosBazaarNpc == null) + { + session.CloseNosBazaarUi(); + await session.NotifyStrangeBehavior(StrangeBehaviorSeverity.DANGER, "Tried to open NosBazaar without a medal."); + return; + } + + DateTime currentDate = DateTime.UtcNow; + if (session.PlayerEntity.LastBuySearchBazaarRefresh > currentDate) + { + return; + } + + session.PlayerEntity.LastBuySearchBazaarRefresh = currentDate.AddSeconds(_bazaarConfiguration.DelayServerBetweenRequestsInSecs); + + if (packet.ItemVNumFilter == null) + { + return; + } + + string[] splitedString = packet.ItemVNumFilter.Split(' '); + + List list = null; + + for (int i = 0; i < splitedString.Length; i++) + { + short value = Convert.ToInt16(splitedString[i]); + if (i == 0) + { + i += value + 1; + continue; + } + + list ??= new List(); + list.Add(value); + } + + await session.EmitEventAsync(new BazaarSearchItemsEvent(packet.Index, packet.CategoryFilterType, packet.SubTypeFilter, packet.LevelFilter, packet.RareFilter, packet.UpgradeFilter, + packet.OrderFilter, list)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Bazaar/CbuyPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Bazaar/CbuyPacketHandler.cs new file mode 100644 index 0000000..64f955e --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Bazaar/CbuyPacketHandler.cs @@ -0,0 +1,36 @@ +using System; +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.Bazaar; +using WingsEmu.Game.Bazaar.Configuration; +using WingsEmu.Game.Bazaar.Events; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.Game.Bazaar; + +public class CbuyPacketHandler : GenericGamePacketHandlerBase +{ + private readonly BazaarConfiguration _bazaarConfiguration; + + public CbuyPacketHandler(BazaarConfiguration bazaarConfiguration) => _bazaarConfiguration = bazaarConfiguration; + + protected override async Task HandlePacketAsync(IClientSession session, CBuyPacket cBuyPacket) + { + DateTime currentDate = DateTime.UtcNow; + if (session.PlayerEntity.LastBuyBazaarRefresh > currentDate) + { + return; + } + + session.PlayerEntity.LastBuyBazaarRefresh = currentDate.AddSeconds(_bazaarConfiguration.DelayServerBetweenRequestsInSecs); + + if (BazaarExtensions.PriceOrAmountExceeds(true, cBuyPacket.Price, cBuyPacket.Amount)) + { + await session.NotifyStrangeBehavior(StrangeBehaviorSeverity.ABUSING, "[BAZAAR] Exceeding the price or amount limit"); + return; + } + + await session.EmitEventAsync(new BazaarItemBuyEvent(cBuyPacket.BazaarItemId, cBuyPacket.Amount, cBuyPacket.Price)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Bazaar/CmodPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Bazaar/CmodPacketHandler.cs new file mode 100644 index 0000000..cad04aa --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Bazaar/CmodPacketHandler.cs @@ -0,0 +1,14 @@ +using System.Threading.Tasks; +using WingsEmu.Game.Bazaar.Events; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.Game.Bazaar; + +public class CmodPacketHandler : GenericGamePacketHandlerBase +{ + protected override async Task HandlePacketAsync(IClientSession session, CmodPacket packet) + { + await session.EmitEventAsync(new BazaarItemChangePriceEvent(packet.BazaarId, packet.NewPricePerItem, packet.Confirmed != 0)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Bazaar/CregPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Bazaar/CregPacketHandler.cs new file mode 100644 index 0000000..833e0c5 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Bazaar/CregPacketHandler.cs @@ -0,0 +1,115 @@ +using System; +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.Bazaar; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.DTOs.Bonus; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Bazaar.Configuration; +using WingsEmu.Game.Bazaar.Events; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.PacketHandling.Game.Bazaar; + +public class CregPacketHandler : GenericGamePacketHandlerBase +{ + private readonly BazaarConfiguration _bazaarConfiguration; + private readonly IGameLanguageService _languageService; + + public CregPacketHandler(IGameLanguageService languageService, BazaarConfiguration bazaarConfiguration) + { + _languageService = languageService; + _bazaarConfiguration = bazaarConfiguration; + } + + protected override async Task HandlePacketAsync(IClientSession session, CRegPacket cRegPacket) + { + if (session.IsActionForbidden()) + { + return; + } + + if (session.PlayerEntity.IsShopping) + { + return; + } + + DateTime currentDate = DateTime.UtcNow; + if (session.PlayerEntity.LastListItemBazaar > currentDate) + { + return; + } + + session.PlayerEntity.LastListItemBazaar = currentDate.AddSeconds(_bazaarConfiguration.DelayServerBetweenRequestsInSecs); + + bool hasMedal = session.PlayerEntity.HaveStaticBonus(StaticBonusType.BazaarMedalGold) || session.PlayerEntity.HaveStaticBonus(StaticBonusType.BazaarMedalSilver); + + if (BazaarExtensions.PriceOrAmountExceeds(hasMedal, cRegPacket.PricePerItem, cRegPacket.Amount)) + { + await session.NotifyStrangeBehavior(StrangeBehaviorSeverity.ABUSING, "[BAZAAR] Exceeding the price or amount limit"); + return; + } + + long totalPrice = cRegPacket.PricePerItem * cRegPacket.Amount; + + short days = cRegPacket.Durability switch + { + 1 => 1, + 2 when hasMedal => 7, + 3 when hasMedal => 15, + 4 when hasMedal => 30, + _ => -1 + }; + + if (days == -1) + { + await session.NotifyStrangeBehavior(StrangeBehaviorSeverity.ABUSING, "[BAZAAR] Invalid day amount"); + return; + } + + long tax = hasMedal ? BazaarExtensions.MedalTax(totalPrice, days) : BazaarExtensions.NormalTax(totalPrice); + + if (tax != cRegPacket.Taxe || !session.HasEnoughGold(tax)) + { + session.SendInfo(_languageService.GetLanguage(GameDialogKey.BAZAAR_INFO_RESYNC, session.UserLanguage)); + return; + } + + cRegPacket.Inventory = cRegPacket.Inventory == 4 ? (byte)0 : + cRegPacket.Inventory > 7 ? (byte)(cRegPacket.Inventory - 8) : cRegPacket.Inventory; + + if (!Enum.IsDefined(typeof(InventoryType), cRegPacket.Inventory)) + { + await session.NotifyStrangeBehavior(StrangeBehaviorSeverity.ABUSING, "[BAZAAR] Received an InventoryType that is not defined in our enum"); + return; + } + + var inventoryType = (InventoryType)cRegPacket.Inventory; + + if (inventoryType != InventoryType.Equipment && inventoryType != InventoryType.Etc && inventoryType != InventoryType.Main) + { + await session.NotifyStrangeBehavior(StrangeBehaviorSeverity.ABUSING, "[BAZAAR] Trying to put an item whose InventoryType is not allowed."); + return; + } + + DateTime expiryDate = DateTime.UtcNow.AddDays(days); + + InventoryItem inventoryItem = session.PlayerEntity.GetItemBySlotAndType(cRegPacket.Slot, inventoryType); + + await session.EmitEventAsync(new BazaarItemAddEvent + { + InventoryItem = inventoryItem, + Amount = cRegPacket.Amount, + ExpiryDate = expiryDate, + DayExpiryAmount = days, + PricePerItem = cRegPacket.PricePerItem, + UsedMedal = hasMedal, + IsPackage = cRegPacket.IsPackage == 1, + Tax = tax + }); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Bazaar/CsListPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Bazaar/CsListPacketHandler.cs new file mode 100644 index 0000000..df3eb8e --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Bazaar/CsListPacketHandler.cs @@ -0,0 +1,34 @@ +using System; +using System.Threading.Tasks; +using WingsAPI.Packets.Enums.Bazaar; +using WingsEmu.Game.Bazaar.Configuration; +using WingsEmu.Game.Bazaar.Events; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.Game.Bazaar; + +public class CsListPacketHandler : GenericGamePacketHandlerBase +{ + private readonly BazaarConfiguration _bazaarConfiguration; + + public CsListPacketHandler(BazaarConfiguration bazaarConfiguration) => _bazaarConfiguration = bazaarConfiguration; + + protected override async Task HandlePacketAsync(IClientSession session, CsListPacket packet) + { + if (!Enum.IsDefined(typeof(BazaarListedItemType), packet.Filter)) + { + return; + } + + DateTime currentDate = DateTime.UtcNow; + if (session.PlayerEntity.LastAdministrationBazaarRefresh > currentDate) + { + return; + } + + session.PlayerEntity.LastAdministrationBazaarRefresh = currentDate.AddSeconds(_bazaarConfiguration.DelayClientBetweenRequestsInSecs); + + await session.EmitEventAsync(new BazaarGetListedItemsEvent(packet.Index, (BazaarListedItemType)packet.Filter)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Chat/FamilyChatPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Chat/FamilyChatPacketHandler.cs new file mode 100644 index 0000000..bab6cde --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Chat/FamilyChatPacketHandler.cs @@ -0,0 +1,79 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Threading.Tasks; +using WingsEmu.Game.InterChannel; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.Game.Chat; + +public class FamilyChatPacketHandler : GenericGamePacketHandlerBase +{ + private readonly ISessionManager _sessionManager; + + public FamilyChatPacketHandler(ISessionManager sessionManager) => _sessionManager = sessionManager; + + protected override async Task HandlePacketAsync(IClientSession session, FamilyChatPacket packet) + { + if (string.IsNullOrEmpty(packet.Message)) + { + return; + } + + string message = packet.Message; + + if (message.Length > 60) + { + message = message.Substring(0, 60); + } + + await session.EmitEventAsync(new FamilyChatMessageEvent(message)); + + /*foreach (IClientSession targetSession in _sessionManager.Sessions.ToList()) + { + if (!targetSession.HasSelectedCharacter) + { + return; + } + + if (!session.Character.IsInFamily()) + { + return; + } + + if (!session.Character.IsInFamily()) + { + return; + } + + if (targetSession.Character.Family?.Id != session.Character.Family?.Id) + { + return; + } + + if (session.HasCurrentMapInstance && targetSession.HasCurrentMapInstance && session.CurrentMapInstance == targetSession.CurrentMapInstance) + { + if (session.Account.Authority != AuthorityType.Moderator && !session.Character.CheatComponent.IsInvisible) + { + targetSession.SendChatMessage(msg, ChatMessageColorType.Blue); + } + else + { + targetSession.SendChatMessage(ccmsg, ChatMessageColorType.Blue); + } + } + else + { + targetSession.SendChatMessage(ccmsg, ChatMessageColorType.Blue); + } + + if (!session.Character.CheatComponent.IsInvisible) + { + targetSession.SendSpeak(msg, SpeakType.Family); + } + }*/ + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Families/CreateFamilyPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Families/CreateFamilyPacketHandler.cs new file mode 100644 index 0000000..a504486 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Families/CreateFamilyPacketHandler.cs @@ -0,0 +1,31 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Threading.Tasks; +using WingsEmu.Game.Families.Event; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.Game.Families; + +public class CreateFamilyPacketHandler : GenericGamePacketHandlerBase +{ + protected override async Task HandlePacketAsync(IClientSession session, CreateFamilyPacket packet) + { + if (string.IsNullOrEmpty(packet.FamilyName)) + { + return; + } + + if (string.IsNullOrWhiteSpace(packet.FamilyName)) + { + return; + } + + await session.EmitEventAsync(new FamilyCreateEvent + { + Name = packet.FamilyName + }); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Families/FDepositPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Families/FDepositPacketHandler.cs new file mode 100644 index 0000000..498ba33 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Families/FDepositPacketHandler.cs @@ -0,0 +1,32 @@ +using System.Threading.Tasks; +using WingsEmu.Game.Families.Event; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.PacketHandling.Game.Families; + +public class FDepositPacketHandler : GenericGamePacketHandlerBase +{ + protected override async Task HandlePacketAsync(IClientSession session, FDepositPacket packet) + { + if (packet.Inventory == InventoryType.EquippedItems) + { + return; + } + + InventoryItem item = session.PlayerEntity.GetItemBySlotAndType(packet.SourceSlot, packet.Inventory); + if (item == null || packet.Amount < 1 || 999 < packet.Amount) + { + return; + } + + await session.EmitEventAsync(new FamilyWarehouseAddItemEvent + { + Item = item, + Amount = packet.Amount, + DestinationSlot = packet.DestinationSlot + }); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Families/FReposPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Families/FReposPacketHandler.cs new file mode 100644 index 0000000..0add3ee --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Families/FReposPacketHandler.cs @@ -0,0 +1,24 @@ +using System.Threading.Tasks; +using WingsEmu.Game.Families.Event; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.Game.Families; + +public class FReposPacketHandler : GenericGamePacketHandlerBase +{ + protected override async Task HandlePacketAsync(IClientSession session, FReposPacket fReposPacket) + { + if (fReposPacket.Amount < 1 || 999 < fReposPacket.Amount) + { + return; + } + + await session.EmitEventAsync(new FamilyWarehouseMoveItemEvent + { + OldSlot = fReposPacket.OldSlot, + Amount = fReposPacket.Amount, + NewSlot = fReposPacket.NewSlot + }); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Families/FWithdrawPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Families/FWithdrawPacketHandler.cs new file mode 100644 index 0000000..c58a060 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Families/FWithdrawPacketHandler.cs @@ -0,0 +1,23 @@ +using System.Threading.Tasks; +using WingsEmu.Game.Families.Event; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.Game.Families; + +public class FWithdrawPacketHandler : GenericGamePacketHandlerBase +{ + protected override async Task HandlePacketAsync(IClientSession session, FWithdrawPacket packet) + { + if (packet.Amount < 1 || 999 < packet.Amount) + { + return; + } + + await session.EmitEventAsync(new FamilyWarehouseWithdrawItemEvent + { + Slot = packet.Slot, + Amount = packet.Amount + }); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Families/FamilyDisbandPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Families/FamilyDisbandPacketHandler.cs new file mode 100644 index 0000000..47208ae --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Families/FamilyDisbandPacketHandler.cs @@ -0,0 +1,18 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Threading.Tasks; +using WingsEmu.Game.Families.Event; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.Game.Families; + +public class FamilyDisbandPacketHandler : GenericGamePacketHandlerBase +{ + protected override async Task HandlePacketAsync(IClientSession session, FamilyDisbandPacket packet) + { + await session.EmitEventAsync(new FamilyDisbandEvent()); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Families/FamilyFAuthPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Families/FamilyFAuthPacketHandler.cs new file mode 100644 index 0000000..0972c72 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Families/FamilyFAuthPacketHandler.cs @@ -0,0 +1,30 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Threading.Tasks; +using WingsAPI.Packets.Enums.Families; +using WingsEmu.Game.Families.Event; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.Game.Families; + +public class FamilyFAuthPacketHandler : GenericGamePacketHandlerBase +{ + protected override async Task HandlePacketAsync(IClientSession session, FAuthPacket packet) + { + if (!Enum.TryParse(packet.AuthorityId.ToString(), out FamilyActionType actionType)) + { + return; + } + + await session.EmitEventAsync(new FamilyChangeSettingsEvent + { + Authority = packet.MemberType, + FamilyActionType = actionType, + Value = packet.Value + }); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Families/FhistCtsPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Families/FhistCtsPacketHandler.cs new file mode 100644 index 0000000..7ee7c0b --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Families/FhistCtsPacketHandler.cs @@ -0,0 +1,26 @@ +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.Families; +using WingsAPI.Packets.ClientPackets; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Plugins.PacketHandling.Game.Families; + +public class FhistCtsPacketHandler : GenericGamePacketHandlerBase +{ + private readonly IGameLanguageService _gameLanguage; + + public FhistCtsPacketHandler(IGameLanguageService gameLanguage) => _gameLanguage = gameLanguage; + + protected override async Task HandlePacketAsync(IClientSession session, FhistCtsPacket packet) + { + if (!session.PlayerEntity.IsInFamily()) + { + session.SendInfo(_gameLanguage.GetLanguage(GameDialogKey.FAMILY_INFO_NO_FAMILY, session.UserLanguage)); + return; + } + + session.SendFamilyLogsToMember(session.PlayerEntity.Family); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Families/FmgPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Families/FmgPacketHandler.cs new file mode 100644 index 0000000..5d60357 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Families/FmgPacketHandler.cs @@ -0,0 +1,18 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Threading.Tasks; +using WingsEmu.Game.Families.Event; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.Game.Families; + +public class FmgPacketHandler : GenericGamePacketHandlerBase +{ + protected override async Task HandlePacketAsync(IClientSession session, FamilyManagementPacket packet) + { + await session.EmitEventAsync(new FamilyChangeAuthorityEvent(packet.FamilyAuthorityType, packet.TargetId, packet.Confirmed.GetValueOrDefault())); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Families/FrankCtsPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Families/FrankCtsPacketHandler.cs new file mode 100644 index 0000000..6a1564a --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Families/FrankCtsPacketHandler.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.Game.Families; + +public class FrankCtsPacketHandler : GenericGamePacketHandlerBase +{ + protected override async Task HandlePacketAsync(IClientSession session, FrankCtsPacket packet) + { + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Families/FsLogCtsPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Families/FsLogCtsPacketHandler.cs new file mode 100644 index 0000000..a8fe452 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Families/FsLogCtsPacketHandler.cs @@ -0,0 +1,17 @@ +using System.Threading.Tasks; +using WingsAPI.Packets.ClientPackets; +using WingsEmu.Game.Families.Event; +using WingsEmu.Game.Networking; + +namespace WingsEmu.Plugins.PacketHandling.Game.Families; + +public class FsLogCtsPacketHandler : GenericGamePacketHandlerBase +{ + protected override async Task HandlePacketAsync(IClientSession session, FsLogCtsPacket packet) + { + await session.EmitEventAsync(new FamilyWarehouseLogsOpenEvent + { + Refresh = true + }); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Families/GLeavePacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Families/GLeavePacketHandler.cs new file mode 100644 index 0000000..53ac8fc --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Families/GLeavePacketHandler.cs @@ -0,0 +1,14 @@ +using System.Threading.Tasks; +using WingsEmu.Game.Families.Event; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.Game.Families; + +public class GLeavePacketHandler : GenericGamePacketHandlerBase +{ + protected override async Task HandlePacketAsync(IClientSession session, GLeavePacket packet) + { + await session.EmitEventAsync(new FamilyLeaveEvent()); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Families/GListPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Families/GListPacketHandler.cs new file mode 100644 index 0000000..5acccb0 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Families/GListPacketHandler.cs @@ -0,0 +1,49 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.Families; +using WingsEmu.Game.Families; +using WingsEmu.Game.Families.Configuration; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.Game.Families; + +public class GListPacketHandler : GenericGamePacketHandlerBase +{ + private readonly FamilyConfiguration _familyConfiguration; + private readonly ISessionManager _sessionService; + + public GListPacketHandler(ISessionManager sessionService, FamilyConfiguration familyConfiguration) + { + _sessionService = sessionService; + _familyConfiguration = familyConfiguration; + } + + protected override async Task HandlePacketAsync(IClientSession session, GListPacket packet) + { + if (!session.PlayerEntity.IsInFamily()) + { + return; + } + + IFamily family = session.PlayerEntity.Family; + + switch (packet.Type) + { + case GListPacketType.RefreshFamilyInfo: + session.RefreshFamilyInfo(family, _familyConfiguration); + break; + case GListPacketType.RefreshFamilyMembers: + session.RefreshFamilyMembers(_sessionService, family); + session.RefreshFamilyMembersExp(family); + session.RefreshFamilyMembersMessages(family); + break; + default: + return; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Families/JoinFamilyPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Families/JoinFamilyPacketHandler.cs new file mode 100644 index 0000000..88d5ed8 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Families/JoinFamilyPacketHandler.cs @@ -0,0 +1,25 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Threading.Tasks; +using WingsAPI.Packets.Enums.Families; +using WingsEmu.Game.Families.Event; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.Game.Families; + +public class JoinFamilyPacketHandler : GenericGamePacketHandlerBase +{ + protected override async Task HandlePacketAsync(IClientSession session, JoinFamilyPacket joinFamilyPacket) + { + if (!Enum.TryParse(joinFamilyPacket.Type.ToString(), out FamilyJoinType type)) + { + return; + } + + await session.EmitEventAsync(new FamilyInviteResponseEvent(type, joinFamilyPacket.CharacterId)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Families/TodayPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Families/TodayPacketHandler.cs new file mode 100644 index 0000000..db87eb0 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Families/TodayPacketHandler.cs @@ -0,0 +1,38 @@ +using System.Threading.Tasks; +using PhoenixLib.DAL.Redis.Locks; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.Game.Families; + +public class TodayPacketHandler : GenericGamePacketHandlerBase +{ + private readonly IExpirableLockService _lock; + + public TodayPacketHandler(IExpirableLockService @lock) => _lock = @lock; + + protected override async Task HandlePacketAsync(IClientSession session, TodayPacket packet) + { + if (!session.PlayerEntity.IsInFamily()) + { + session.SendInfo(session.GetLanguage(GameDialogKey.FAMILY_INFO_NO_FAMILY)); + return; + } + + if (session.PlayerEntity.Level < 30) + { + session.SendInfo(session.GetLanguage(GameDialogKey.FAMILY_INFO_TODAY_LOW_LEVEL)); + return; + } + + if (await _lock.ExistsTemporaryLock($"game:locks:family:{session.PlayerEntity.Family.Id}:{session.PlayerEntity.Id}:quote-of-the-day")) + { + session.SendInfo(session.GetLanguage(GameDialogKey.FAMILY_INFO_USED_DAILY_MESSAGE)); + return; + } + + session.SendPacket("today_stc"); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Group/GroupSayPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Group/GroupSayPacketHandler.cs new file mode 100644 index 0000000..54a3738 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Group/GroupSayPacketHandler.cs @@ -0,0 +1,55 @@ +using System.Linq; +using System.Threading.Tasks; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Chat; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Groups; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; +using WingsEmu.Packets.Enums.Chat; +using ChatType = WingsEmu.Game._playerActionLogs.ChatType; + +namespace WingsEmu.Plugins.PacketHandling.Game.Group; + +public class GroupSayPacketHandler : GenericGamePacketHandlerBase +{ + protected override async Task HandlePacketAsync(IClientSession session, GroupSayPacket groupSayPacket) + { + if (string.IsNullOrEmpty(groupSayPacket.Message)) + { + return; + } + + if (session.PlayerEntity.IsInRaidParty) + { + foreach (IClientSession target in session.PlayerEntity.Raid.Members) + { + session.SendSpeakToTarget(target, groupSayPacket.Message, SpeakType.Group); + } + + await session.EmitEventAsync(new ChatGenericEvent + { + Message = groupSayPacket.Message, + ChatType = ChatType.GroupChat + }); + return; + } + + if (!session.PlayerEntity.IsInGroup()) + { + return; + } + + PlayerGroup group = session.PlayerEntity.GetGroup(); + foreach (IPlayerEntity member in group.Members.ToArray()) + { + session.SendSpeakToTarget(member.Session, groupSayPacket.Message, SpeakType.Group); + } + + await session.EmitEventAsync(new ChatGenericEvent + { + Message = groupSayPacket.Message, + ChatType = ChatType.GroupChat + }); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Group/PJoinPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Group/PJoinPacketHandler.cs new file mode 100644 index 0000000..a1a2be0 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Group/PJoinPacketHandler.cs @@ -0,0 +1,24 @@ +using System.Threading.Tasks; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Groups.Events; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.Game.Group; + +public class PjoinPacketHandler : GenericGamePacketHandlerBase +{ + protected override async Task HandlePacketAsync(IClientSession session, PJoinPacket packet) + { + if (session.IsActionForbidden()) + { + return; + } + + await session.EmitEventAsync(new GroupActionEvent + { + CharacterId = packet.CharacterId, + RequestType = packet.RequestType + }); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Group/PLeavePacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Group/PLeavePacketHandler.cs new file mode 100644 index 0000000..d42b0bf --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Group/PLeavePacketHandler.cs @@ -0,0 +1,14 @@ +using System.Threading.Tasks; +using WingsEmu.Game.Groups.Events; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.Game.Group; + +public class PLeavePacketHandler : GenericGamePacketHandlerBase +{ + protected override async Task HandlePacketAsync(IClientSession session, PLeavePacket packet) + { + await session.EmitEventAsync(new LeaveGroupEvent()); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Group/RdPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Group/RdPacketHandler.cs new file mode 100644 index 0000000..3f7ef0c --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Group/RdPacketHandler.cs @@ -0,0 +1,63 @@ +using System.Threading.Tasks; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Raids.Events; +using WingsEmu.Game.Relations; +using WingsEmu.Packets.ClientPackets; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.PacketHandling.Game.Group; + +public class RdPacketHandler : GenericGamePacketHandlerBase +{ + private readonly IInvitationManager _invitationManager; + + public RdPacketHandler(IInvitationManager invitationManager) => _invitationManager = invitationManager; + + protected override async Task HandlePacketAsync(IClientSession session, RdPacket rdPacket) + { + if (rdPacket == null) + { + return; + } + + switch (rdPacket.Type) + { + case 1: + switch (rdPacket.Parameter) + { + case null: + await session.EmitEventAsync(new RaidPartyInvitePlayerEvent(rdPacket.CharacterId)); + return; + case 1: + await session.EmitEventAsync(new RaidPartyJoinEvent(rdPacket.CharacterId, false)); + return; + case 2: + if (!_invitationManager.ContainsPendingInvitation(rdPacket.CharacterId, session.PlayerEntity.Id, InvitationType.Raid)) + { + return; + } + + _invitationManager.RemovePendingInvitation(rdPacket.CharacterId, session.PlayerEntity.Id, InvitationType.Raid); + return; + } + + break; + + case 2: + + if (session.PlayerEntity.Raid is { Finished: true }) + { + return; + } + + await session.EmitEventAsync(new RaidPartyLeaveEvent(false)); + break; + case 3: + await session.EmitEventAsync(new RaidPartyKickPlayerEvent(rdPacket.CharacterId)); + break; + case 4: + await session.EmitEventAsync(new RaidPartyDisbandEvent(true)); + break; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Group/RlPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Group/RlPacketHandler.cs new file mode 100644 index 0000000..1989fa9 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Group/RlPacketHandler.cs @@ -0,0 +1,47 @@ +using System; +using System.Threading.Tasks; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Raids; +using WingsEmu.Game.Raids.Events; +using WingsEmu.Packets.ClientPackets; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.PacketHandling.Game.Group; + +public class RlPacketHandler : GenericGamePacketHandlerBase +{ + private readonly IGameLanguageService _language; + + public RlPacketHandler(IGameLanguageService language) => _language = language; + + protected override async Task HandlePacketAsync(IClientSession session, RlPacket rlPacket) + { + if (session.PlayerEntity.Level < 20) + { + session.SendChatMessage(_language.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_TOO_LOW_LVL, session.UserLanguage), ChatMessageColorType.Red); + return; + } + + if (!Enum.TryParse(rlPacket.Type.ToString(), out RlClientPacketType type)) + { + return; + } + + switch (type) + { + case RlClientPacketType.REGISTER: + await session.EmitEventAsync(new RaidListRegisterEvent()); + break; + case RlClientPacketType.UNREGISTER: + await session.EmitEventAsync(new RaidListUnregisterEvent()); + break; + case RlClientPacketType.JOIN: + await session.EmitEventAsync(new RaidListJoinEvent(rlPacket.CharacterName)); + break; + } + + await session.EmitEventAsync(new RaidListOpenEvent()); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/BiPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/BiPacketHandler.cs new file mode 100644 index 0000000..49a7fdd --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/BiPacketHandler.cs @@ -0,0 +1,51 @@ +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.Game.Inventory; + +public class BiPacketHandler : GenericGamePacketHandlerBase +{ + private readonly IGameLanguageService _language; + + public BiPacketHandler(IGameLanguageService language) => _language = language; + + protected override async Task HandlePacketAsync(IClientSession session, BiPacket packet) + { + InventoryItem itemToRemove = session.PlayerEntity.GetItemBySlotAndType(packet.Slot, packet.InventoryType); + if (itemToRemove == null) + { + return; + } + + switch (packet.Option) + { + case null: + session.SendDialog($"b_i {(byte)packet.InventoryType} {packet.Slot} 1", "b_i 0 0 5", _language.GetLanguage(GameDialogKey.INVENTORY_DIALOG_ASK_TO_DELETE, session.UserLanguage)); + + break; + case 1: + session.SendDialog($"b_i {(byte)packet.InventoryType} {packet.Slot} 2", $"b_i {(byte)packet.InventoryType} {packet.Slot} 5", + _language.GetLanguage(GameDialogKey.INVENTORY_DIALOG_SURE_TO_DELETE, session.UserLanguage)); + + break; + case 2: + if (session.PlayerEntity.IsInExchange()) + { + return; + } + + if (session.PlayerEntity.HasShopOpened) + { + return; + } + + await session.RemoveItemFromInventory(amount: (short)itemToRemove.ItemInstance.Amount, item: itemToRemove); + break; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/EquipmentInfoPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/EquipmentInfoPacketHandler.cs new file mode 100644 index 0000000..1ee5e8b --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/EquipmentInfoPacketHandler.cs @@ -0,0 +1,241 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.ItemExtension.Item; +using WingsEmu.DTOs.Items; +using WingsEmu.Game.Algorithm; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Exchange; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Families.Event; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Shops; +using WingsEmu.Game.Warehouse; +using WingsEmu.Game.Warehouse.Events; +using WingsEmu.Packets.ClientPackets; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.PacketHandling.Game.Inventory; + +public class EquipmentInfoPacketHandler : GenericGamePacketHandlerBase +{ + private readonly ICharacterAlgorithm _algorithm; + private readonly IGameItemInstanceFactory _instanceFactory; + private readonly IItemsManager _itemManager; + private readonly ISessionManager _sessionManager; + + public EquipmentInfoPacketHandler(IItemsManager itemsManager, ISessionManager sessionManager, + ICharacterAlgorithm algorithm, IGameItemInstanceFactory instanceFactory) + { + _itemManager = itemsManager; + _sessionManager = sessionManager; + _algorithm = algorithm; + _instanceFactory = instanceFactory; + } + + protected override async Task HandlePacketAsync(IClientSession session, EquipmentInfoPacket equipmentInfoPacket) + { + if (!session.HasSelectedCharacter) + { + return; + } + + if (!session.HasCurrentMapInstance) + { + return; + } + + InventoryItem inventory = null; + switch (equipmentInfoPacket.Type) + { + case 0: + if (!Enum.TryParse(equipmentInfoPacket.Slot.ToString(), out EquipmentType type)) + { + return; + } + + inventory = session.PlayerEntity.GetInventoryItemFromEquipmentSlot(type); + break; + + case 1: + inventory = session.PlayerEntity.GetItemBySlotAndType(equipmentInfoPacket.Slot, InventoryType.Equipment); + break; + + case 2: + IGameItem npcGameItem = _itemManager.GetItem(equipmentInfoPacket.Slot); + if (npcGameItem == null) + { + return; + } + + inventory = _instanceFactory.CreateInventoryItem(npcGameItem.Id); + break; + + case 5: + PlayerExchange playerExchange = session.PlayerEntity.GetExchange(); + if (playerExchange == null) + { + return; + } + + IClientSession targetSession = _sessionManager.GetSessionByCharacterId(playerExchange.TargetId); + if (targetSession == null) + { + break; + } + + PlayerExchange targetExchange = targetSession.PlayerEntity.GetExchange(); + if (targetExchange == null) + { + return; + } + + if (!targetExchange.Items.Any()) + { + return; + } + + inventory = targetExchange.Items.ElementAt(equipmentInfoPacket.Slot).Item1; + break; + + case 6: + if (!equipmentInfoPacket.ShopOwnerId.HasValue) + { + return; + } + + IPlayerEntity owner = session.CurrentMapInstance.GetCharacterById(equipmentInfoPacket.ShopOwnerId.Value); + ShopPlayerItem shopPlayerItem = owner?.ShopComponent.GetItem(equipmentInfoPacket.Slot); + if (shopPlayerItem != null) + { + inventory = shopPlayerItem.InventoryItem; + } + + break; + + case 7: + if (equipmentInfoPacket.Slot == 0) + { + return; + } + + short partnerSlot = (short)(equipmentInfoPacket.Slot - 1); + IMateEntity entity = session.PlayerEntity.MateComponent.GetMate(x => x.MateType == MateType.Partner && x.PetSlot == partnerSlot); + if (entity == null) + { + return; + } + + if (!Enum.TryParse(equipmentInfoPacket.PartnerEqSlot.ToString(), out EquipmentType eqType)) + { + return; + } + + PartnerInventoryItem partnerItem = session.PlayerEntity.PartnerGetEquippedItem(eqType, partnerSlot); + + if (partnerItem == null) + { + return; + } + + if (partnerItem.ItemInstance.Type != ItemInstanceType.WearableInstance && partnerItem.ItemInstance.Type != ItemInstanceType.SpecialistInstance) + { + return; + } + + GameItemInstance partnerInstance = partnerItem.ItemInstance; + + if (partnerInstance.GameItem.EquipmentSlot == EquipmentType.Sp) + { + if (partnerInstance.GameItem.IsPartnerSpecialist) + { + session.SendPartnerSpecialistInfo(partnerInstance); + } + else + { + session.SendSpecialistCardInfo(partnerInstance, _algorithm); + } + + return; + } + + session.SendEInfoPacket(partnerInstance, _itemManager, _algorithm); + return; + + case 8: + await session.EmitEventAsync(new AccountWarehouseShowItemEvent + { + Slot = equipmentInfoPacket.Slot + }); + return; + + case 9: + PartnerWarehouseItem partnerWarehouseItem = session.PlayerEntity.GetPartnerWarehouseItem(equipmentInfoPacket.Slot); + + if (partnerWarehouseItem == null) + { + return; + } + + GameItemInstance partnerWarehouseInstance = partnerWarehouseItem.ItemInstance; + + if (partnerWarehouseInstance.GameItem.EquipmentSlot == EquipmentType.Sp) + { + if (partnerWarehouseInstance.GameItem.IsPartnerSpecialist) + { + session.SendPartnerSpecialistInfo(partnerWarehouseInstance); + } + else + { + session.SendSpecialistCardInfo(partnerWarehouseInstance, _algorithm); + } + + return; + } + + session.SendEInfoPacket(partnerWarehouseInstance, _itemManager, _algorithm); + return; + + case 10: + inventory = session.PlayerEntity.GetItemBySlotAndType(equipmentInfoPacket.Slot, InventoryType.Specialist); + break; + + case 11: + inventory = session.PlayerEntity.GetItemBySlotAndType(equipmentInfoPacket.Slot, InventoryType.Costume); + break; + + case 12: + await session.EmitEventAsync(new FamilyWarehouseShowItemEvent + { + Slot = equipmentInfoPacket.Slot + }); + return; + } + + if (inventory == null) + { + return; + } + + if (inventory.ItemInstance.GameItem.EquipmentSlot == EquipmentType.Sp) + { + if (inventory.ItemInstance.GameItem.IsPartnerSpecialist) + { + session.SendPartnerSpecialistInfo(inventory.ItemInstance); + } + else + { + session.SendSpecialistCardInfo(inventory.ItemInstance, _algorithm); + } + + return; + } + + session.SendEInfoPacket(inventory.ItemInstance, _itemManager, _algorithm); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/ExcListPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/ExcListPacketHandler.cs new file mode 100644 index 0000000..84c3f8d --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/ExcListPacketHandler.cs @@ -0,0 +1,192 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Exchange; +using WingsEmu.Game.Exchange.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.PacketHandling.Game.Inventory; + +public class ExcListPacketHandler : GenericGamePacketHandlerBase +{ + private readonly IBankReputationConfiguration _bankReputationConfiguration; + private readonly IGameLanguageService _language; + private readonly IRankingManager _rankingManager; + private readonly IReputationConfiguration _reputationConfiguration; + private readonly ISessionManager _sessionManager; + + public ExcListPacketHandler(IGameLanguageService language, ISessionManager sessionManager, IReputationConfiguration reputationConfiguration, + IBankReputationConfiguration bankReputationConfiguration, IRankingManager rankingManager) + { + _sessionManager = sessionManager; + _reputationConfiguration = reputationConfiguration; + _bankReputationConfiguration = bankReputationConfiguration; + _rankingManager = rankingManager; + _language = language; + } + + protected override async Task HandlePacketAsync(IClientSession session, ExcListPacket packet) + { + if (packet == null) + { + return; + } + + if (session.IsActionForbidden()) + { + return; + } + + if (!session.PlayerEntity.IsInExchange()) + { + return; + } + + if (string.IsNullOrWhiteSpace(packet.PacketData)) + { + return; + } + + string[] packetSplit = packet.PacketData.Split(' '); + if (packetSplit.Length < 2) + { + return; + } + + if (!int.TryParse(packetSplit[0], out int gold)) + { + return; + } + + if (!long.TryParse(packetSplit[1], out long bankGold)) + { + return; + } + + if (bankGold < 0 || bankGold * 1000 > session.Account.BankMoney) + { + return; + } + + if (gold < 0 || gold > session.PlayerEntity.Gold) + { + return; + } + + PlayerExchange playerExchange = session.PlayerEntity.GetExchange(); + if (playerExchange == null) + { + return; + } + + IClientSession exchangeTarget = _sessionManager.GetSessionByCharacterId(playerExchange.TargetId); + if (exchangeTarget == null) + { + return; + } + + if (playerExchange.RegisteredItems) + { + return; + } + + if (bankGold != 0 && !session.HasEnoughGold(session.PlayerEntity.GetBankPenalty(_reputationConfiguration, _bankReputationConfiguration, _rankingManager.TopReputation) + gold)) + { + await session.CloseExchange(); + session.SendMsg(_language.GetLanguage(GameDialogKey.INTERACTION_MESSAGE_NOT_ENOUGH_GOLD, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (session.PlayerEntity.HasShopOpened || exchangeTarget.PlayerEntity.HasShopOpened) + { + await session.CloseExchange(); + return; + } + + string packetList = string.Empty; + + var itemList = new List<(InventoryItem, short)>(); + for (int i = 2, j = 0; i < packetSplit.Length && j < 10; i += 3, j++) + { + if (i + 2 > packetSplit.Length) + { + return; + } + + if (!Enum.TryParse(packetSplit[i], out InventoryType inventoryType)) + { + return; + } + + if (!short.TryParse(packetSplit[i + 1], out short slot)) + { + return; + } + + if (!short.TryParse(packetSplit[i + 2], out short itemAmount)) + { + return; + } + + InventoryItem item = session.PlayerEntity.GetItemBySlotAndType(slot, inventoryType); + if (item == null) + { + await session.CloseExchange(); + return; + } + + if (itemAmount <= 0) + { + await session.CloseExchange(); + return; + } + + if (itemAmount > item.ItemInstance.Amount) + { + await session.CloseExchange(); + return; + } + + if (!item.ItemInstance.GameItem.IsTradable) + { + await session.CloseExchange(); + session.SendMsg(_language.GetLanguage(GameDialogKey.TRADE_SHOUTMESSAGE_ITEM_NOT_TRADABLE, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (item.ItemInstance.IsBound) + { + await session.CloseExchange(); + session.SendMsg(_language.GetLanguage(GameDialogKey.TRADE_SHOUTMESSAGE_ITEM_NOT_TRADABLE, session.UserLanguage), MsgMessageType.Middle); + return; + } + + itemList.Add((item, itemAmount)); + if (inventoryType != InventoryType.Equipment) + { + packetList += $"{j}.{(byte)inventoryType}.{item.ItemInstance.ItemVNum}.{itemAmount}.0 "; + } + else + { + packetList += $"{j}.{(byte)inventoryType}.{item.ItemInstance.ItemVNum}.{item.ItemInstance.Rarity}.{item.ItemInstance.Upgrade}.{item.ItemInstance.GetRunesCount()} "; + } + } + + await session.EmitEventAsync(new ExchangeRegisterEvent + { + InventoryItems = itemList, + BankGold = bankGold * 1000, + Gold = gold, + Packets = packetList + }); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/ExchangeRequestPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/ExchangeRequestPacketHandler.cs new file mode 100644 index 0000000..6b4f6f8 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/ExchangeRequestPacketHandler.cs @@ -0,0 +1,306 @@ +using System; +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsAPI.Game.Extensions.PacketGeneration; +using WingsAPI.Packets.Enums; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Exchange; +using WingsEmu.Game.Exchange.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Relations; +using WingsEmu.Packets.ClientPackets; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.PacketHandling.Game.Inventory; + +public class ExchangeRequestPacketHandler : GenericGamePacketHandlerBase +{ + private readonly IInvitationManager _invitation; + private readonly IGameLanguageService _language; + private readonly IServerManager _serverManager; + + public ExchangeRequestPacketHandler(IServerManager serverManager, IGameLanguageService language, IInvitationManager invitation) + { + _invitation = invitation; + _serverManager = serverManager; + _language = language; + } + + protected override async Task HandlePacketAsync(IClientSession session, ExchangeRequestPacket exchangeRequestPacket) + { + long targetId = exchangeRequestPacket.CharacterId; + RequestExchangeType requestType = exchangeRequestPacket.RequestType; + + if (!session.HasCurrentMapInstance || !session.HasSelectedCharacter) + { + return; + } + + if (session.IsActionForbidden()) + { + return; + } + + if (!session.CurrentMapInstance.HasMapFlag(MapFlags.IS_BASE_MAP) && session.CurrentMapInstance.MapInstanceType != MapInstanceType.Miniland) + { + return; + } + + IClientSession target; + switch (requestType) + { + case RequestExchangeType.Requested: + if (session.PlayerEntity.Id == targetId) + { + return; + } + + target = session.CurrentMapInstance.GetCharacterById(targetId)?.Session; + + if (target == null) + { + return; + } + + if (target.IsActionForbidden()) + { + return; + } + + if (session.CurrentMapInstance.Id != target.CurrentMapInstance.Id) + { + return; + } + + if (session.CurrentMapInstance.HasMapFlag(MapFlags.ACT_4)) + { + if (target.PlayerEntity.Faction != session.PlayerEntity.Faction) + { + return; + } + } + + if (target.PlayerEntity.HasRaidStarted) + { + session.SendMsg(_language.GetLanguage(GameDialogKey.TRADE_SHOUTMESSAGE_NOT_ALLOWED_IN_RAID, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (session.PlayerEntity.HasRaidStarted) + { + session.SendMsg(_language.GetLanguage(GameDialogKey.TRADE_SHOUTMESSAGE_NOT_ALLOWED_WITH_RAID_MEMBER, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (session.PlayerEntity.IsBlocking(targetId)) + { + session.SendInfo(_language.GetLanguage(GameDialogKey.BLACKLIST_INFO_BLOCKING, session.UserLanguage)); + return; + } + + if (target.PlayerEntity.IsBlocking(session.PlayerEntity.Id)) + { + session.SendInfo(_language.GetLanguage(GameDialogKey.BLACKLIST_INFO_BLOCKED, session.UserLanguage)); + return; + } + + if (session.PlayerEntity.IsInExchange()) + { + session.SendModal(_language.GetLanguage(GameDialogKey.TRADE_MODAL_ALREADY_IN_EXCHANGE, session.UserLanguage), ModalType.Cancel); + return; + } + + if (target.PlayerEntity.IsInExchange()) + { + session.SendModal(_language.GetLanguage(GameDialogKey.TRADE_MODAL_ALREADY_IN_EXCHANGE, session.UserLanguage), ModalType.Cancel); + return; + } + + if (target.PlayerEntity.LastSkillUse.AddSeconds(10) > DateTime.UtcNow || target.PlayerEntity.LastDefence.AddSeconds(10) > DateTime.UtcNow) + { + session.SendInfo(_language.GetLanguageFormat(GameDialogKey.INFORMATION_INFO_PLAYER_IN_BATTLE, session.UserLanguage, target.PlayerEntity.Name)); + return; + } + + if (session.PlayerEntity.LastSkillUse.AddSeconds(10) > DateTime.UtcNow || session.PlayerEntity.LastDefence.AddSeconds(10) > DateTime.UtcNow) + { + session.SendInfo(_language.GetLanguage(GameDialogKey.TRADE_INFO_PLAYER_IN_BATTLE, session.UserLanguage)); + return; + } + + if (session.PlayerEntity.HasShopOpened || target.PlayerEntity.HasShopOpened) + { + session.SendMsg(_language.GetLanguage(GameDialogKey.TRADE_SHOUTMESSAGE_HAS_SHOP_OPEN, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (target.PlayerEntity.ExchangeBlocked) + { + session.SendChatMessage(_language.GetLanguage(GameDialogKey.TRADE_CHATMESSAGE_BLOCKED, session.UserLanguage), ChatMessageColorType.Red); + return; + } + + session.SendModal(_language.GetLanguageFormat(GameDialogKey.TRADE_DIALOG_ASK_FOR_TRADE, session.UserLanguage, target.PlayerEntity.Name), ModalType.Cancel); + await session.EmitEventAsync(new InvitationEvent(targetId, InvitationType.Exchange)); + break; + + case RequestExchangeType.Confirmed: + PlayerExchange exchange = session.PlayerEntity.GetExchange(); + if (exchange == null) + { + return; + } + + target = session.CurrentMapInstance.GetCharacterById(exchange.TargetId)?.Session; + + PlayerExchange targetExchange = target?.PlayerEntity.GetExchange(); + if (targetExchange == null) + { + return; + } + + if (session.PlayerEntity.HasRaidStarted) + { + session.SendMsg(_language.GetLanguage(GameDialogKey.TRADE_SHOUTMESSAGE_NOT_ALLOWED_IN_RAID, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (target.PlayerEntity.HasRaidStarted) + { + session.SendMsg(_language.GetLanguage(GameDialogKey.TRADE_SHOUTMESSAGE_NOT_ALLOWED_WITH_RAID_MEMBER, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (session.IsDisposing || target.IsDisposing) + { + await session.CloseExchange(); + return; + } + + if (!exchange.RegisteredItems || !targetExchange.RegisteredItems) + { + return; + } + + if (!exchange.AcceptedTrade) + { + exchange.AcceptedTrade = true; + } + + if (exchange.AcceptedTrade && !targetExchange.AcceptedTrade) + { + session.SendInfo(_language.GetLanguageFormat(GameDialogKey.TRADE_INFO_WAITING_FOR_TARGET, session.UserLanguage, target.PlayerEntity.Name)); + return; + } + + await session.EmitEventAsync(new ExchangeTransferItemsEvent + { + Target = target, + SenderGold = exchange.Gold, + SenderBankGold = exchange.BankGold, + SenderItems = exchange.Items, + TargetItems = targetExchange.Items, + TargetGold = targetExchange.Gold, + TargetBankGold = targetExchange.BankGold + }); + + break; + + case RequestExchangeType.Cancelled: + if (!session.PlayerEntity.IsInExchange()) + { + return; + } + + PlayerExchange playerExchange = session.PlayerEntity.GetExchange(); + if (playerExchange == null) + { + return; + } + + target = session.CurrentMapInstance.GetCharacterById(playerExchange.TargetId)?.Session; + if (target == null) + { + return; + } + + await session.CloseExchange(); + break; + + case RequestExchangeType.AcceptExchangeInvitation: + if (session.PlayerEntity.Id == targetId) + { + return; + } + + target = session.CurrentMapInstance.GetCharacterById(targetId)?.Session; + if (target == null) + { + return; + } + + if (session.CurrentMapInstance.Id != target.CurrentMapInstance.Id) + { + return; + } + + if (!_invitation.ContainsPendingInvitation(targetId, session.PlayerEntity.Id, InvitationType.Exchange)) + { + return; + } + + if (session.PlayerEntity.IsInExchange()) + { + return; + } + + if (target.PlayerEntity.IsInExchange()) + { + session.SendModal(_language.GetLanguage(GameDialogKey.TRADE_MODAL_ALREADY_IN_EXCHANGE, session.UserLanguage), ModalType.Cancel); + return; + } + + _invitation.RemovePendingInvitation(targetId, session.PlayerEntity.Id, InvitationType.Exchange); + + await session.EmitEventAsync(new ExchangeJoinEvent + { + Target = target + }); + break; + + case RequestExchangeType.Declined: + if (session.PlayerEntity.Id == targetId) + { + return; + } + + target = session.CurrentMapInstance.GetCharacterById(targetId)?.Session; + + if (target == null) + { + return; + } + + if (session.CurrentMapInstance.Id != target.CurrentMapInstance.Id) + { + return; + } + + if (!_invitation.ContainsPendingInvitation(targetId, session.PlayerEntity.Id, InvitationType.Exchange)) + { + return; + } + + _invitation.RemovePendingInvitation(targetId, session.PlayerEntity.Id, InvitationType.Exchange); + target.SendChatMessage(_language.GetLanguageFormat(GameDialogKey.TRADE_CHATMESSAGE_REFUSED, target.UserLanguage, session.PlayerEntity.Name), ChatMessageColorType.Yellow); + session.SendChatMessage(_language.GetLanguage(GameDialogKey.TRADE_CHATMESSAGE_YOU_REFUSED, session.UserLanguage), ChatMessageColorType.Yellow); + break; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/GetPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/GetPacketHandler.cs new file mode 100644 index 0000000..bc1e1d1 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/GetPacketHandler.cs @@ -0,0 +1,21 @@ +using System; +using System.Threading.Tasks; +using WingsEmu.Game.Inventory.Event; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.PacketHandling.Game.Inventory; + +public class GetPacketHandler : GenericGamePacketHandlerBase +{ + protected override async Task HandlePacketAsync(IClientSession session, GetPacket getPacket) + { + if (!Enum.TryParse(getPacket.PickerType.ToString(), out VisualType type)) + { + return; + } + + await session.EmitEventAsync(new InventoryPickUpItemEvent(type, getPacket.PickerId, getPacket.TransportId)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/MvePacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/MvePacketHandler.cs new file mode 100644 index 0000000..ca1103c --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/MvePacketHandler.cs @@ -0,0 +1,58 @@ +using System.Threading.Tasks; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Inventory.Event; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.PacketHandling.Game.Inventory; + +public class MvePacketHandler : GenericGamePacketHandlerBase +{ + protected override async Task HandlePacketAsync(IClientSession session, MvePacket mvePacket) + { + if (mvePacket.Slot == mvePacket.DestinationSlot && mvePacket.InventoryType == mvePacket.DestinationInventoryType) + { + return; + } + + if (mvePacket.DestinationSlot > session.PlayerEntity.GetInventorySlots(inventoryType: mvePacket.DestinationInventoryType)) + { + return; + } + + if (session.PlayerEntity.IsInExchange()) + { + return; + } + + if (session.PlayerEntity.HasShopOpened) + { + return; + } + + if (mvePacket.InventoryType != InventoryType.Costume && + mvePacket.InventoryType != InventoryType.Specialist && + mvePacket.InventoryType != InventoryType.Equipment) + { + return; + } + + if (mvePacket.DestinationInventoryType != InventoryType.Costume && + mvePacket.DestinationInventoryType != InventoryType.Specialist && + mvePacket.DestinationInventoryType != InventoryType.Equipment) + { + return; + } + + InventoryItem sourceItem = session.PlayerEntity.GetItemBySlotAndType(mvePacket.Slot, mvePacket.InventoryType); + if ((sourceItem == null || sourceItem.ItemInstance.GameItem.ItemType != ItemType.Specialist) && (sourceItem == null || sourceItem.ItemInstance.GameItem.ItemType != ItemType.Fashion)) + { + return; + } + + await session.EmitEventAsync(new InventoryMoveItemEvent(mvePacket.InventoryType, mvePacket.Slot, (short)sourceItem.ItemInstance.Amount, + mvePacket.DestinationSlot, mvePacket.DestinationInventoryType)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/MviPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/MviPacketHandler.cs new file mode 100644 index 0000000..24bc4c5 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/MviPacketHandler.cs @@ -0,0 +1,48 @@ +using System.Threading.Tasks; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Inventory.Event; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.PacketHandling.Game.Inventory; + +public class MviPacketHandler : GenericGamePacketHandlerBase +{ + protected override async Task HandlePacketAsync(IClientSession session, MviPacket mviPacket) + { + if (mviPacket.Amount <= 0) + { + return; + } + + if (mviPacket.Slot == mviPacket.DestinationSlot) + { + return; + } + + if (mviPacket.InventoryType == InventoryType.EquippedItems || mviPacket.InventoryType == InventoryType.Miniland) + { + return; + } + + // check if the destination slot is out of range + if (mviPacket.DestinationSlot > session.PlayerEntity.GetInventorySlots(inventoryType: mviPacket.InventoryType)) + { + return; + } + + // check if the character is allowed to move the item + if (session.PlayerEntity.IsInExchange()) + { + return; + } + + if (session.PlayerEntity.HasShopOpened) + { + return; + } + + await session.EmitEventAsync(new InventoryMoveItemEvent(mviPacket.InventoryType, mviPacket.Slot, mviPacket.Amount, mviPacket.DestinationSlot, mviPacket.InventoryType)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/PutPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/PutPacketHandler.cs new file mode 100644 index 0000000..b9e722c --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/PutPacketHandler.cs @@ -0,0 +1,14 @@ +using System.Threading.Tasks; +using WingsEmu.Game.Inventory.Event; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.Game.Inventory; + +public class PutPacketHandler : GenericGamePacketHandlerBase +{ + protected override async Task HandlePacketAsync(IClientSession session, PutPacket putPacket) + { + await session.EmitEventAsync(new InventoryDropItemEvent(putPacket.InventoryType, putPacket.Slot, putPacket.Amount)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/RemovePacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/RemovePacketHandler.cs new file mode 100644 index 0000000..f40db27 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/RemovePacketHandler.cs @@ -0,0 +1,25 @@ +using System.Threading.Tasks; +using WingsEmu.Game.Inventory.Event; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.Game.Inventory; + +public class RemovePacketHandler : GenericGamePacketHandlerBase +{ + protected override async Task HandlePacketAsync(IClientSession session, RemovePacket removePacket) + { + if (session.PlayerEntity.IsSeal) + { + return; + } + + if (removePacket.PartnerSlot == 0) + { + await session.EmitEventAsync(new InventoryTakeOffItemEvent(removePacket.InventorySlot)); + return; + } + + await session.EmitEventAsync(new PartnerInventoryTakeOffItemEvent(removePacket.PartnerSlot, removePacket.InventorySlot)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/ReposPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/ReposPacketHandler.cs new file mode 100644 index 0000000..5bf8b66 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/ReposPacketHandler.cs @@ -0,0 +1,20 @@ +using System.Threading.Tasks; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Warehouse.Events; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.Game.Inventory; + +public class ReposPacketHandler : GenericGamePacketHandlerBase +{ + protected override async Task HandlePacketAsync(IClientSession session, ReposPacket reposPacket) + { + if (reposPacket.IsPartnerBackpack) + { + await session.EmitEventAsync(new PartnerWarehouseMoveEvent(reposPacket.OldSlot, reposPacket.Amount, reposPacket.NewSlot)); + return; + } + + await session.EmitEventAsync(new AccountWarehouseMoveEvent(reposPacket.OldSlot, reposPacket.Amount, reposPacket.NewSlot)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/SortOpenPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/SortOpenPacketHandler.cs new file mode 100644 index 0000000..220a42f --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/SortOpenPacketHandler.cs @@ -0,0 +1,46 @@ +using System.Linq; +using System.Threading.Tasks; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Inventory.Event; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.PacketHandling.Game.Inventory; + +public class SortOpenPacketHandler : GenericGamePacketHandlerBase +{ + protected override async Task HandlePacketAsync(IClientSession session, SortOpenPacket packet) + { + InventoryItem[] specialists = session.PlayerEntity.GetItemsByInventoryType(InventoryType.Specialist).OrderBy(x => x?.ItemInstance.ItemVNum).ToArray(); + InventoryItem[] costumes = session.PlayerEntity.GetItemsByInventoryType(InventoryType.Costume).OrderBy(x => x?.ItemInstance.ItemVNum).ToArray(); + + if (!costumes.Any() && !specialists.Any()) + { + return; + } + + if (specialists.Any()) + { + foreach (InventoryItem specialist in specialists) + { + short slot = session.PlayerEntity.GetNextInventorySlot(InventoryType.Specialist); + await session.EmitEventAsync(new InventoryMoveItemEvent(InventoryType.Specialist, specialist.Slot, 1, slot, InventoryType.Specialist, false)); + } + + session.SendSortedItems(InventoryType.Specialist); + } + + if (costumes.Any()) + { + foreach (InventoryItem costume in costumes) + { + short slot = session.PlayerEntity.GetNextInventorySlot(InventoryType.Costume); + await session.EmitEventAsync(new InventoryMoveItemEvent(InventoryType.Costume, costume.Slot, 1, slot, InventoryType.Costume, false)); + } + + session.SendSortedItems(InventoryType.Costume); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/SpTransformPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/SpTransformPacketHandler.cs new file mode 100644 index 0000000..25bf448 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/SpTransformPacketHandler.cs @@ -0,0 +1,164 @@ +using System; +using System.Threading.Tasks; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Algorithm; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Networking.Broadcasting; +using WingsEmu.Packets.ClientPackets; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.PacketHandling.Game.Inventory; + +public class SpTransformPacketHandler : GenericGamePacketHandlerBase +{ + private readonly ICharacterAlgorithm _algorithm; + private readonly IDelayManager _delayManager; + private readonly IGameLanguageService _language; + + public SpTransformPacketHandler(IGameLanguageService language, IDelayManager delayManager, ICharacterAlgorithm algorithm) + { + _language = language; + _delayManager = delayManager; + _algorithm = algorithm; + } + + protected override async Task HandlePacketAsync(IClientSession session, SpTransformPacket spTransformPacket) + { + GameItemInstance specialistInstance = session.PlayerEntity.Specialist; + + if (specialistInstance == null) + { + session.SendMsg(_language.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_NO_SPECIALIST_CARD, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (!session.PlayerEntity.IsAlive()) + { + return; + } + + if (session.PlayerEntity.IsSeal) + { + return; + } + + if (spTransformPacket.Type == 10) + { + short specialistDamage = spTransformPacket.SpecialistDamage; + short specialistDefense = spTransformPacket.SpecialistDefense; + short specialistElement = spTransformPacket.SpecialistElement; + short specialistHealth = spTransformPacket.SpecialistHp; + int transportId = spTransformPacket.TransportId; + if (!session.PlayerEntity.UseSp || transportId != specialistInstance.TransportId) + { + session.SendMsg(_language.GetLanguage(GameDialogKey.SPECIALIST_SHOUTMESSAGE_USE_NEEDED, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (specialistDamage < 0 || specialistDefense < 0 || specialistElement < 0 || specialistHealth < 0) + { + return; + } + + if (specialistInstance.SlDamage + specialistDamage + specialistInstance.SlElement + specialistElement + + specialistInstance.SlHP + specialistHealth + specialistInstance.SlDefence + specialistDefense > specialistInstance.SpPointsBasic()) + { + return; + } + + specialistInstance.SlDamage += specialistDamage; + specialistInstance.SlDefence += specialistDefense; + specialistInstance.SlElement += specialistElement; + specialistInstance.SlHP += specialistHealth; + + session.PlayerEntity.SpecialistComponent.RefreshSlStats(); + + session.RefreshStatChar(); + session.RefreshStat(); + session.SendSpecialistCardInfo(specialistInstance, _algorithm); + session.SendMsg(_language.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_SP_POINTS_SET, session.UserLanguage), MsgMessageType.Middle); + + return; + } + + if (session.PlayerEntity.IsSitting) + { + await session.RestAsync(); + } + + if (session.PlayerEntity.BuffComponent.HasBuff(BuffGroup.Bad)) + { + session.SendMsg(_language.GetLanguage(GameDialogKey.SPECIALIST_SHOUTMESSAGE_NO_REMOVE_DEBUFFS, session.UserLanguage), MsgMessageType.Middle); + session.PlayerEntity.BroadcastEndDancingGuriPacket(); + return; + } + + if (session.PlayerEntity.IsOnVehicle) + { + session.SendMsg(_language.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_REMOVE_VEHICLE, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (session.PlayerEntity.IsMorphed) + { + session.SendChatMessage(_language.GetLanguage(GameDialogKey.ITEM_MESSAGE_CANT_USE, session.UserLanguage), ChatMessageColorType.Yellow); + session.PlayerEntity.BroadcastEndDancingGuriPacket(); + return; + } + + if (session.PlayerEntity.UseSp) + { + if (session.PlayerEntity.IsCastingSkill) + { + return; + } + + if (session.PlayerEntity.LastSkillUse.AddSeconds(3) > DateTime.UtcNow) + { + return; + } + + await session.EmitEventAsync(new SpUntransformEvent()); + return; + } + + if (!session.PlayerEntity.IsSpCooldownElapsed()) + { + session.SendMsg(_language.GetLanguageFormat(GameDialogKey.SPECIALIST_SHOUTMESSAGE_IN_COOLDOWN, session.UserLanguage, session.PlayerEntity.GetSpCooldown()), MsgMessageType.Middle); + return; + } + + if (session.PlayerEntity.LastSkillUse.AddSeconds(2) >= DateTime.UtcNow) + { + return; + } + + if (spTransformPacket.Type == 1) + { + bool canWearSp = await _delayManager.CanPerformAction(session.PlayerEntity, DelayedActionType.WearSp); + if (!canWearSp) + { + return; + } + + await _delayManager.CompleteAction(session.PlayerEntity, DelayedActionType.WearSp); + await session.EmitEventAsync(new SpTransformEvent + { + Specialist = specialistInstance + }); + } + else + { + DateTime waitUntil = await _delayManager.RegisterAction(session.PlayerEntity, DelayedActionType.WearSp); + session.SendDelay((int)(waitUntil - DateTime.UtcNow).TotalMilliseconds, GuriType.Transforming, "sl 1"); + session.BroadcastGuri(2, 1, 0, new RangeBroadcast(session.PlayerEntity.PositionX, session.PlayerEntity.PositionY)); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/SpecialistHolderPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/SpecialistHolderPacketHandler.cs new file mode 100644 index 0000000..248b7a9 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/SpecialistHolderPacketHandler.cs @@ -0,0 +1,95 @@ +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsEmu.DTOs.Items; +using WingsEmu.Game._enum; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Items; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.PacketHandling.Game.Inventory; + +public class SpecialistHolderPacketHandler : GenericGamePacketHandlerBase +{ + protected override async Task HandlePacketAsync(IClientSession session, SpecialistHolderPacket specialistHolderPacket) + { + InventoryItem specialist = session.PlayerEntity.GetItemBySlotAndType(specialistHolderPacket.Slot, InventoryType.Equipment); + InventoryItem holder = session.PlayerEntity.GetItemBySlotAndType(specialistHolderPacket.HolderSlot, InventoryType.Equipment); + if (specialist == null || holder == null) + { + return; + } + + if (specialist.ItemInstance.Type != ItemInstanceType.SpecialistInstance) + { + return; + } + + GameItemInstance spItem = specialist.ItemInstance; + + if (holder.ItemInstance.Type != ItemInstanceType.BoxInstance) + { + return; + } + + GameItemInstance box = holder.ItemInstance; + + switch (specialistHolderPacket.HolderType) + { + case 0: + if (box.ItemVNum != (short)ItemVnums.GOLDEN_SP_HOLDER) + { + return; + } + + if (spItem.GameItem.IsPartnerSpecialist) + { + return; + } + + box.HoldingVNum = spItem.ItemVNum; + box.SlDamage = spItem.SlDamage; + box.SlDefence = spItem.SlDefence; + box.SlElement = spItem.SlElement; + box.SlHP = spItem.SlHP; + box.SpDamage = spItem.SpDamage; + box.SpDark = spItem.SpDark; + box.SpDefence = spItem.SpDefence; + box.SpElement = spItem.SpElement; + box.SpFire = spItem.SpFire; + box.SpHP = spItem.SpHP; + box.SpLevel = spItem.SpLevel; + box.SpLight = spItem.SpLight; + box.SpStoneUpgrade = spItem.SpStoneUpgrade; + box.SpWater = spItem.SpWater; + box.Upgrade = spItem.Upgrade; + box.Xp = spItem.Xp; + break; + case 1: + if (box.ItemVNum != (short)ItemVnums.PSP_HOLDER) + { + return; + } + + if (!spItem.GameItem.IsPartnerSpecialist) + { + return; + } + + box.HoldingVNum = spItem.ItemVNum; + box.PartnerSkill1 = spItem.PartnerSkill1; + box.PartnerSkill2 = spItem.PartnerSkill2; + box.PartnerSkill3 = spItem.PartnerSkill3; + box.SkillRank1 = spItem.SkillRank1; + box.SkillRank2 = spItem.SkillRank2; + box.SkillRank3 = spItem.SkillRank3; + box.PartnerSkills = spItem.PartnerSkills; + break; + } + + session.SendShopEndPacket(ShopEndType.SpecialistHolder); + await session.RemoveItemFromInventory(item: specialist); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/UpgradePacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/UpgradePacketHandler.cs new file mode 100644 index 0000000..87953e6 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/UpgradePacketHandler.cs @@ -0,0 +1,431 @@ +using System; +using System.Threading.Tasks; +using WingsEmu.DTOs.Items; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Inventory.Event; +using WingsEmu.Game.Items; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; +using WingsEmu.Plugins.BasicImplementations.Event.Characters; + +namespace WingsEmu.Plugins.PacketHandling.Game.Inventory; + +public class UpgradePacketHandler : GenericGamePacketHandlerBase +{ + private readonly IGameLanguageService _language; + + public UpgradePacketHandler(IGameLanguageService language) => _language = language; + + protected override async Task HandlePacketAsync(IClientSession session, UpgradePacket upgradePacket) + { + if (session.PlayerEntity.IsInExchange()) + { + return; + } + + if (session.PlayerEntity.HasShopOpened) + { + return; + } + + if (session.PlayerEntity.LastItemUpgrade.AddSeconds(4) > DateTime.UtcNow) + { + session.PlayerEntity.BroadcastEndDancingGuriPacket(); + return; + } + + InventoryType inventoryType = upgradePacket.InventoryType; + if (!Enum.TryParse(upgradePacket.UpgradeType.ToString(), out UpgradePacketType upType)) + { + session.PlayerEntity.BroadcastEndDancingGuriPacket(); + return; + } + + byte slot = upgradePacket.Slot; + InventoryItem inventory; + InventoryItem specialist2 = session.PlayerEntity.GetItemBySlotAndType(slot, inventoryType); + session.PlayerEntity.LastItemUpgrade = DateTime.UtcNow; + switch (upType) + { + case UpgradePacketType.FREE_CHICKEN_UPGRADE: + // chicken SP + inventory = session.PlayerEntity.GetItemBySlotAndType(slot, inventoryType); + + if (inventory?.ItemInstance.ItemVNum != (short)ItemVnums.CHICKEN_SP) + { + session.PlayerEntity.BroadcastEndDancingGuriPacket(); + return; + } + + if (specialist2 == null) + { + session.PlayerEntity.BroadcastEndDancingGuriPacket(); + return; + } + + if (specialist2.ItemInstance.GameItem.EquipmentSlot != EquipmentType.Sp) + { + session.PlayerEntity.BroadcastEndDancingGuriPacket(); + return; + } + + if (specialist2.ItemInstance.Rarity == -2) + { + session.SendMsg(_language.GetLanguage(GameDialogKey.ITEM_SHOUTMESSAGE_CANT_UPGRADE_DESTROYED_SP, session.UserLanguage), MsgMessageType.Middle); + session.PlayerEntity.BroadcastEndDancingGuriPacket(); + return; + } + + await session.EmitEventAsync(new SpUpgradeEvent(UpgradeProtection.Protected, specialist2, true)); + break; + case UpgradePacketType.FREE_PAJAMA_UPGRADE: + // sp pyj + inventory = session.PlayerEntity.GetItemBySlotAndType(slot, inventoryType); + + if (inventory?.ItemInstance.ItemVNum != (short)ItemVnums.PYJAMA_SP) + { + session.PlayerEntity.BroadcastEndDancingGuriPacket(); + return; + } + + if (specialist2 == null) + { + session.PlayerEntity.BroadcastEndDancingGuriPacket(); + return; + } + + if (specialist2.ItemInstance.GameItem.EquipmentSlot != EquipmentType.Sp) + { + session.PlayerEntity.BroadcastEndDancingGuriPacket(); + return; + } + + if (specialist2.ItemInstance.Rarity == -2) + { + session.SendMsg(_language.GetLanguage(GameDialogKey.ITEM_SHOUTMESSAGE_CANT_UPGRADE_DESTROYED_SP, session.UserLanguage), MsgMessageType.Middle); + session.PlayerEntity.BroadcastEndDancingGuriPacket(); + return; + } + + await session.EmitEventAsync(new SpUpgradeEvent(UpgradeProtection.Protected, specialist2, true)); + break; + case UpgradePacketType.FREE_PIRATE_UPGRADFE: + inventory = session.PlayerEntity.GetItemBySlotAndType(slot, inventoryType); + + if (inventory?.ItemInstance.ItemVNum != (short)ItemVnums.PIRATE_SP) + { + session.PlayerEntity.BroadcastEndDancingGuriPacket(); + return; + } + + if (specialist2 == null) + { + session.PlayerEntity.BroadcastEndDancingGuriPacket(); + return; + } + + if (specialist2.ItemInstance.GameItem.EquipmentSlot != EquipmentType.Sp) + { + session.PlayerEntity.BroadcastEndDancingGuriPacket(); + return; + } + + if (specialist2.ItemInstance.Rarity == -2) + { + session.SendMsg(_language.GetLanguage(GameDialogKey.ITEM_SHOUTMESSAGE_CANT_UPGRADE_DESTROYED_SP, session.UserLanguage), MsgMessageType.Middle); + session.PlayerEntity.BroadcastEndDancingGuriPacket(); + return; + } + + await session.EmitEventAsync(new SpUpgradeEvent(UpgradeProtection.Protected, specialist2, true)); + break; + case UpgradePacketType.PLAYER_ITEM_TO_PARTNER: + await session.EmitEventAsync(new PlayerItemToPartnerItemEvent(slot, inventoryType)); + break; + case UpgradePacketType.ITEM_UPGRADE: + inventory = session.PlayerEntity.GetItemBySlotAndType(slot, inventoryType); + + if (inventory == null) + { + session.PlayerEntity.BroadcastEndDancingGuriPacket(); + return; + } + + if (inventory.ItemInstance.GameItem.EquipmentSlot != EquipmentType.Armor && inventory.ItemInstance.GameItem.EquipmentSlot != EquipmentType.MainWeapon && + inventory.ItemInstance.GameItem.EquipmentSlot != EquipmentType.SecondaryWeapon) + { + session.PlayerEntity.BroadcastEndDancingGuriPacket(); + return; + } + + GameItemInstance amulet1 = session.PlayerEntity.Amulet; + FixedUpMode hasAmulet1 = amulet1?.GameItem.Effect == 793 ? FixedUpMode.HasAmulet : FixedUpMode.None; + + await session.EmitEventAsync(new UpgradeItemEvent + { + Inv = inventory, + Mode = UpgradeMode.Normal, + Protection = UpgradeProtection.None, + HasAmulet = hasAmulet1 + }); + + break; + case UpgradePacketType.CELLON_UPGRADE: + if (upgradePacket.InventoryType2 == null) + { + session.PlayerEntity.BroadcastEndDancingGuriPacket(); + return; + } + + inventory = session.PlayerEntity.GetItemBySlotAndType((byte)upgradePacket.InventoryType2.Value, upgradePacket.InventoryType); + + if (inventory == null) + { + session.PlayerEntity.BroadcastEndDancingGuriPacket(); + return; + } + + if (inventory.ItemInstance.GameItem.EquipmentSlot != EquipmentType.Necklace + && inventory.ItemInstance.GameItem.EquipmentSlot != EquipmentType.Ring + && inventory.ItemInstance.GameItem.EquipmentSlot != EquipmentType.Bracelet) + { + session.PlayerEntity.BroadcastEndDancingGuriPacket(); + return; + } + + if (upgradePacket.CellonSlot == null) + { + session.PlayerEntity.BroadcastEndDancingGuriPacket(); + return; + } + + if (upgradePacket.CellonInventoryType == null) + { + session.PlayerEntity.BroadcastEndDancingGuriPacket(); + return; + } + + if (upgradePacket.CellonInventoryType.Value != InventoryType.Main) + { + session.PlayerEntity.BroadcastEndDancingGuriPacket(); + return; + } + + InventoryItem cellon = session.PlayerEntity.GetItemBySlotAndType(upgradePacket.CellonSlot.Value, upgradePacket.CellonInventoryType.Value); + if (cellon == null) + { + session.PlayerEntity.BroadcastEndDancingGuriPacket(); + return; + } + + if (cellon.ItemInstance.GameItem.Effect != 100) + { + session.PlayerEntity.BroadcastEndDancingGuriPacket(); + return; + } + + if (inventory.ItemInstance.Type != ItemInstanceType.WearableInstance) + { + session.PlayerEntity.BroadcastEndDancingGuriPacket(); + return; + } + + await session.EmitEventAsync(new CellonUpgradeEvent(cellon, inventory.ItemInstance)); + break; + case UpgradePacketType.ITEM_RARITY: + inventory = session.PlayerEntity.GetItemBySlotAndType(slot, inventoryType); + + if (inventory == null) + { + session.PlayerEntity.BroadcastEndDancingGuriPacket(); + return; + } + + if (inventory.ItemInstance.GameItem.EquipmentSlot != EquipmentType.Armor && inventory.ItemInstance.GameItem.EquipmentSlot != EquipmentType.MainWeapon && + inventory.ItemInstance.GameItem.EquipmentSlot != EquipmentType.SecondaryWeapon) + { + session.PlayerEntity.BroadcastEndDancingGuriPacket(); + return; + } + + InventoryItem amulet7 = session.PlayerEntity.GetInventoryItemFromEquipmentSlot(EquipmentType.Amulet); + + await session.EmitRarifyEvent(inventory, amulet7); + session.SendShopEndPacket(ShopEndType.Npc); + break; + case UpgradePacketType.ITEM_SUM: + inventory = session.PlayerEntity.GetItemBySlotAndType(slot, inventoryType); + if (upgradePacket.InventoryType2 == null) + { + session.PlayerEntity.BroadcastEndDancingGuriPacket(); + return; + } + + if (upgradePacket.Slot2 == null) + { + session.PlayerEntity.BroadcastEndDancingGuriPacket(); + return; + } + + if (inventory == null) + { + session.PlayerEntity.BroadcastEndDancingGuriPacket(); + return; + } + + InventoryItem inventory2 = session.PlayerEntity.GetItemBySlotAndType((byte)upgradePacket.Slot2, (InventoryType)upgradePacket.InventoryType2); + if (inventory2 == null) + { + session.PlayerEntity.BroadcastEndDancingGuriPacket(); + return; + } + + await session.EmitEventAsync(new ItemSumEvent(inventory, inventory2)); + break; + + case UpgradePacketType.SP_UPGRADE: + InventoryItem specialist = session.PlayerEntity.GetItemBySlotAndType(slot, inventoryType); + + if (specialist == null) + { + session.PlayerEntity.BroadcastEndDancingGuriPacket(); + return; + } + + if (specialist.ItemInstance.GameItem.EquipmentSlot != EquipmentType.Sp) + { + session.PlayerEntity.BroadcastEndDancingGuriPacket(); + return; + } + + await session.EmitEventAsync(new SpUpgradeEvent(UpgradeProtection.None, specialist)); + break; + + case UpgradePacketType.ITEM_UPGRADE_SCROLL: + inventory = session.PlayerEntity.GetItemBySlotAndType(slot, inventoryType); + + if (inventory == null) + { + session.PlayerEntity.BroadcastEndDancingGuriPacket(); + return; + } + + GameItemInstance amulet9 = session.PlayerEntity.Amulet; + FixedUpMode hasAmulet9 = amulet9?.GameItem.Effect == 793 ? FixedUpMode.HasAmulet : FixedUpMode.None; + + if (inventory.ItemInstance.GameItem.EquipmentSlot != EquipmentType.Armor + && inventory.ItemInstance.GameItem.EquipmentSlot != EquipmentType.MainWeapon + && inventory.ItemInstance.GameItem.EquipmentSlot != EquipmentType.SecondaryWeapon) + { + session.PlayerEntity.BroadcastEndDancingGuriPacket(); + return; + } + + await session.EmitEventAsync(new UpgradeItemEvent + { + Inv = inventory, + Mode = UpgradeMode.Normal, + Protection = UpgradeProtection.Protected, + HasAmulet = hasAmulet9 + }); + break; + + case UpgradePacketType.ITEM_RARITY_SCROLL: + inventory = session.PlayerEntity.GetItemBySlotAndType(slot, inventoryType); + + if (inventory == null) + { + session.PlayerEntity.BroadcastEndDancingGuriPacket(); + return; + } + + if (inventory.ItemInstance.GameItem.EquipmentSlot != EquipmentType.Armor + && inventory.ItemInstance.GameItem.EquipmentSlot != EquipmentType.MainWeapon + && inventory.ItemInstance.GameItem.EquipmentSlot != EquipmentType.SecondaryWeapon) + { + session.PlayerEntity.BroadcastEndDancingGuriPacket(); + return; + } + + await session.EmitRarifyEvent(inventory, null, isScroll: true); + break; + + case UpgradePacketType.SP_UPGRADE_SCROLL_BLUE: + case UpgradePacketType.SP_UPGRADE_SCROLL_RED: + specialist = session.PlayerEntity.GetItemBySlotAndType(slot, inventoryType); + + if (specialist == null) + { + session.PlayerEntity.BroadcastEndDancingGuriPacket(); + return; + } + + if (specialist.ItemInstance.GameItem.EquipmentSlot != EquipmentType.Sp) + { + session.PlayerEntity.BroadcastEndDancingGuriPacket(); + return; + } + + await session.EmitEventAsync(new SpUpgradeEvent(UpgradeProtection.Protected, specialist)); + break; + + case UpgradePacketType.SP_PERFECTION: + specialist = session.PlayerEntity.GetItemBySlotAndType(slot, inventoryType); + if (specialist == null) + { + session.PlayerEntity.BroadcastEndDancingGuriPacket(); + return; + } + + if (specialist.ItemInstance.GameItem.EquipmentSlot != EquipmentType.Sp) + { + session.PlayerEntity.BroadcastEndDancingGuriPacket(); + return; + } + + if (specialist.ItemInstance.Rarity == -2) + { + session.SendMsg(_language.GetLanguage(GameDialogKey.ITEM_SHOUTMESSAGE_CANT_UPGRADE_DESTROYED_SP, session.UserLanguage), MsgMessageType.Middle); + session.PlayerEntity.BroadcastEndDancingGuriPacket(); + return; + } + + await session.EmitEventAsync(new SpPerfectEvent(specialist)); + break; + + case UpgradePacketType.ITEM_UPGRADE_GOLD_SCROLL: + inventory = session.PlayerEntity.GetItemBySlotAndType(slot, inventoryType); + if (inventory == null) + { + session.PlayerEntity.BroadcastEndDancingGuriPacket(); + return; + } + + GameItemInstance amulet43 = session.PlayerEntity.Amulet; + FixedUpMode hasAmulet43 = amulet43?.GameItem.Effect == 793 ? FixedUpMode.HasAmulet : FixedUpMode.None; + + if (inventory.ItemInstance.GameItem.EquipmentSlot != EquipmentType.Armor + && inventory.ItemInstance.GameItem.EquipmentSlot != EquipmentType.MainWeapon + && inventory.ItemInstance.GameItem.EquipmentSlot != EquipmentType.SecondaryWeapon) + { + session.PlayerEntity.BroadcastEndDancingGuriPacket(); + return; + } + + await session.EmitEventAsync(new UpgradeItemEvent + { + Inv = inventory, + Mode = UpgradeMode.Reduced, + Protection = UpgradeProtection.Protected, + HasAmulet = hasAmulet43 + }); + break; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/UseItemPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/UseItemPacketHandler.cs new file mode 100644 index 0000000..2fd6239 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/UseItemPacketHandler.cs @@ -0,0 +1,247 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsAPI.Game.Extensions.Quests; +using WingsEmu.DTOs.BCards; +using WingsEmu.DTOs.Items; +using WingsEmu.DTOs.Quests; +using WingsEmu.DTOs.ServerDatas; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game._ItemUsage.Configuration; +using WingsEmu.Game._ItemUsage.Event; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Buffs; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Inventory.Event; +using WingsEmu.Game.Items; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Quests; +using WingsEmu.Game.Quests.Event; +using WingsEmu.Packets.ClientPackets; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; +using WingsEmu.Plugins.BasicImplementations.Vehicles; + +namespace WingsEmu.Plugins.PacketHandling.Game.Inventory; + +public class UseItemPacketHandler : GenericGamePacketHandlerBase +{ + private readonly IBCardEffectHandlerContainer _bCardEffectHandlerContainer; + private readonly ICostumeScrollConfiguration _costumeScrollConfiguration; + private readonly IGameLanguageService _gameLanguage; + private readonly IItemBoxManager _itemBoxManager; + private readonly IVehicleConfigurationProvider _vehicleConfiguration; + + public UseItemPacketHandler(IGameLanguageService gameLanguage, IItemBoxManager itemBoxManager, IVehicleConfigurationProvider vehicleConfiguration, + ICostumeScrollConfiguration costumeScrollConfiguration, IBCardEffectHandlerContainer bCardEffectHandlerContainer) + { + _gameLanguage = gameLanguage; + _itemBoxManager = itemBoxManager; + _vehicleConfiguration = vehicleConfiguration; + _costumeScrollConfiguration = costumeScrollConfiguration; + _bCardEffectHandlerContainer = bCardEffectHandlerContainer; + } + + protected override async Task HandlePacketAsync(IClientSession session, UseItemPacket useItemPacket) + { + if (session.PlayerEntity.IsInExchange()) + { + await session.NotifyStrangeBehavior(StrangeBehaviorSeverity.NORMAL, "Tried to use item while being in exchange"); + return; + } + + if (session.PlayerEntity.HasShopOpened) + { + await session.NotifyStrangeBehavior(StrangeBehaviorSeverity.NORMAL, "Tried to use item while having shop open"); + return; + } + + if (session.PlayerEntity.IsSeal) + { + return; + } + + if (session.PlayerEntity.CheatComponent.IsInvisible) + { + return; + } + + if (!session.PlayerEntity.IsAlive()) + { + return; + } + + if ((byte)useItemPacket.Type >= 9) + { + return; + } + + InventoryItem inv = session.PlayerEntity.GetItemBySlotAndType(useItemPacket.Slot, useItemPacket.Type); + string[] split = useItemPacket.OriginalContent.Split(' ', '^'); + + if (inv == null) + { + return; + } + + /* + * You can use only potions and snack being on vehicle / be morphed and: + * Speed Booster / Limited and vehicle + * Costume scroll + */ + if (inv.ItemInstance.Type == ItemInstanceType.NORMAL_ITEM) + { + if (!CanUseItemInVehicle(session, inv)) + { + session.SendChatMessage(session.GetLanguage(GameDialogKey.ITEM_CHATMESSAGE_ON_MOUNT), ChatMessageColorType.Yellow); + return; + } + + if (session.PlayerEntity.IsMorphed) + { + IGameItem gameItem = inv.ItemInstance.GameItem; + IReadOnlyList morphs = _costumeScrollConfiguration.GetScrollMorphs((short)gameItem.Id); + if (gameItem.ItemType is not ItemType.Potion and ItemType.Snack && (morphs == null || !morphs.Any())) + { + return; + } + } + } + + if (inv.ItemInstance.GameItem.ItemType == ItemType.Special) + { + IGameItem gameItem = inv.ItemInstance.GameItem; + foreach (BCardDTO c in gameItem.BCards) + { + _bCardEffectHandlerContainer.Execute(session.PlayerEntity, session.PlayerEntity, c); + } + + ItemBoxDto itemBox = _itemBoxManager.GetItemBoxByItemVnumAndDesign(inv.ItemInstance.ItemVNum); + + if (itemBox != null && itemBox.Items.Count > 0) + { + if (split.Length == 9 || gameItem.ItemSubType == 3) + { + session.SendQnaPacket($"guri 4999 8023 {inv.Slot}", _gameLanguage.GetLanguage(GameDialogKey.ITEM_DIALOG_ASK_OPEN_BOX, session.UserLanguage)); + return; + } + } + } + + CheckQuests(session, inv.ItemInstance); + switch (inv.ItemInstance.Type) + { + case ItemInstanceType.SpecialistInstance when !inv.ItemInstance.GameItem.IsPartnerSpecialist: + await session.EmitEventAsync(new InventoryEquipItemEvent(inv.Slot, true, InventoryType.Specialist)); + return; + case ItemInstanceType.WearableInstance when inv.ItemInstance.GameItem.ItemType == ItemType.Fashion && inv.InventoryType == InventoryType.Costume: + await session.EmitEventAsync(new InventoryEquipItemEvent(inv.Slot, true, InventoryType.Costume)); + return; + default: + await session.EmitEventAsync(new InventoryUseItemEvent + { + Item = inv, + Option = split[1].ElementAt(0) == '#' ? (byte)255 : (byte)0, + Packet = split + }); + break; + } + } + + private bool CanUseItemInVehicle(IClientSession session, InventoryItem item) + { + if (!session.PlayerEntity.IsOnVehicle) + { + return true; + } + + if (item.ItemInstance.GameItem.ItemType is ItemType.Potion or ItemType.Snack) + { + return true; + } + + if (item.ItemInstance.GameItem.Id is (short)ItemVnums.SPEED_BOOSTER or (short)ItemVnums.SPEED_BOOSTER_LIMITED) + { + return true; + } + + if (item.ItemInstance.GameItem.ItemType is ItemType.Box && item.ItemInstance.GameItem.ItemSubType == 7) + { + return true; + } + + return _vehicleConfiguration.GetByVehicleVnum(item.ItemInstance.GameItem.Id) != null; + } + + private void CheckQuests(IClientSession session, GameItemInstance inv) + { + IEnumerable characterQuests = session.PlayerEntity.GetCurrentQuestsByType(QuestType.USE_ITEM_ON_TARGET); + foreach (CharacterQuest quest in characterQuests) + { + IReadOnlyCollection objectives = quest.Quest.Objectives; + foreach (QuestObjectiveDto objective in objectives) + { + if (inv.ItemVNum != objective.Data0) + { + continue; + } + + + IEntity entity = session.CurrentMapInstance.GetBattleEntity(session.PlayerEntity.LastEntity.Item1, session.PlayerEntity.LastEntity.Item2); + if (entity == null) + { + continue; + } + + CharacterQuestObjectiveDto questObjectiveDto = quest.ObjectiveAmount[objective.ObjectiveIndex]; + if (entity.IsMonster()) + { + var monster = (IMonsterEntity)entity; + if (monster.MonsterVNum != objective.Data1) + { + continue; + } + + if (questObjectiveDto.CurrentAmount == 0) + { + questObjectiveDto.CurrentAmount++; + session.EmitEventAsync(new QuestObjectiveUpdatedEvent + { + CharacterQuest = quest + }); + session.RemoveItemFromInventory(inv.ItemVNum); + } + } + + else if (entity.IsNpc()) + { + var npc = (INpcEntity)entity; + if (npc.NpcVNum != objective.Data1) + { + continue; + } + + if (questObjectiveDto.CurrentAmount == 0) + { + questObjectiveDto.CurrentAmount++; + session.EmitEventAsync(new QuestObjectiveUpdatedEvent + { + CharacterQuest = quest + }); + session.RemoveItemFromInventory(inv.ItemVNum); + } + } + + if (session.PlayerEntity.IsQuestCompleted(quest)) + { + session.EmitEvent(new QuestCompletedEvent(quest)); + } + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/WarehouseAddPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/WarehouseAddPacketHandler.cs new file mode 100644 index 0000000..547005f --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/WarehouseAddPacketHandler.cs @@ -0,0 +1,38 @@ +using System.Threading.Tasks; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Warehouse.Events; +using WingsEmu.Packets.ClientPackets; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.PacketHandling.Game.Inventory; + +public class WarehouseAddPacketHandler : GenericGamePacketHandlerBase +{ + protected override async Task HandlePacketAsync(IClientSession session, DepositPacket packet) + { + if (packet.Inventory == InventoryType.EquippedItems) + { + return; + } + + InventoryItem item = session.PlayerEntity.GetItemBySlotAndType(packet.Slot, packet.Inventory); + if (item == null || packet.Amount is < 1 or > 999) + { + return; + } + + if (packet.PartnerBackpack) + { + await session.EmitEventAsync(new PartnerWarehouseDepositEvent(packet.Inventory, packet.Slot, packet.Amount, packet.NewSlot)); + return; + } + + await session.EmitEventAsync(new AccountWarehouseAddItemEvent + { + Item = item, + Amount = packet.Amount, + SlotDestination = packet.NewSlot + }); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/WearPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/WearPacketHandler.cs new file mode 100644 index 0000000..71ca1e1 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/WearPacketHandler.cs @@ -0,0 +1,25 @@ +using System.Threading.Tasks; +using WingsEmu.Game.Inventory.Event; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.Game.Inventory; + +public class WearPacketHandler : GenericGamePacketHandlerBase +{ + protected override async Task HandlePacketAsync(IClientSession session, WearPacket wearPacket) + { + if (session.PlayerEntity.IsSeal) + { + return; + } + + if (wearPacket.PetId == 0) + { + await session.EmitEventAsync(new InventoryEquipItemEvent(wearPacket.InventorySlot, boundItem: wearPacket.BoundItem)); + return; + } + + await session.EmitEventAsync(new PartnerInventoryEquipItemEvent(wearPacket.PetId, wearPacket.InventorySlot)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/WearPartnerCardPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/WearPartnerCardPacketHandler.cs new file mode 100644 index 0000000..7f52d83 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/WearPartnerCardPacketHandler.cs @@ -0,0 +1,23 @@ +using System.Threading.Tasks; +using WingsEmu.Game.Inventory.Event; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.PacketHandling.Game.Inventory; + +public class WearPartnerCardPacketHandler : GenericGamePacketHandlerBase +{ + protected override async Task HandlePacketAsync(IClientSession session, WearPartnerCardPacket packet) + { + byte petSlot = packet.PetId; + byte itemSlot = packet.InventorySlot; + + if (petSlot == 0) + { + return; + } + + await session.EmitEventAsync(new PartnerInventoryEquipItemEvent(petSlot, itemSlot, InventoryType.Specialist)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/WithdrawPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/WithdrawPacketHandler.cs new file mode 100644 index 0000000..f93cb53 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Inventory/WithdrawPacketHandler.cs @@ -0,0 +1,20 @@ +using System.Threading.Tasks; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Warehouse.Events; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.Game.Inventory; + +public class WithdrawPacketHandler : GenericGamePacketHandlerBase +{ + protected override async Task HandlePacketAsync(IClientSession session, WithdrawPacket withdrawPacket) + { + if (withdrawPacket.PetBackpack) + { + await session.EmitEventAsync(new PartnerWarehouseWithdrawEvent(withdrawPacket.Slot, withdrawPacket.Amount)); + return; + } + + await session.EmitEventAsync(new AccountWarehouseWithdrawItemEvent(withdrawPacket.Slot, withdrawPacket.Amount)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Mate/PsOpPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Mate/PsOpPacketHandler.cs new file mode 100644 index 0000000..c4eb035 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Mate/PsOpPacketHandler.cs @@ -0,0 +1,19 @@ +using System.Threading.Tasks; +using WingsEmu.Game.Inventory.Event; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.Game.Mate; + +public class PsOpPacketHandler : GenericGamePacketHandlerBase +{ + protected override async Task HandlePacketAsync(IClientSession session, PsopServerPacket packet) + { + await session.EmitEventAsync(new PartnerSpecialistSkillEvent + { + PartnerSlot = packet.PetSlot, + SkillSlot = packet.SkillSlot, + Roll = packet.Option == 1 + }); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Mate/PslPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Mate/PslPacketHandler.cs new file mode 100644 index 0000000..eef6360 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Mate/PslPacketHandler.cs @@ -0,0 +1,102 @@ +using System; +using System.Threading.Tasks; +using PhoenixLib.Events; +using WingsEmu.Core.Extensions; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Extensions.Mates; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Mates.Events; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Networking.Broadcasting; +using WingsEmu.Packets.ClientPackets; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.PacketHandling.Game.Mate; + +public class PslPacketHandler : GenericGamePacketHandlerBase +{ + private readonly IDelayManager _delayManager; + private readonly IAsyncEventPipeline _eventPipeline; + private readonly IGameLanguageService _gameLanguage; + + public PslPacketHandler(IAsyncEventPipeline eventPipeline, IDelayManager delayManager, IGameLanguageService languageService) + { + _eventPipeline = eventPipeline; + _delayManager = delayManager; + _gameLanguage = languageService; + } + + protected override async Task HandlePacketAsync(IClientSession session, PslPacket packet) + { + if (session.PlayerEntity.IsOnVehicle) + { + return; + } + + IMateEntity mateEntity = session.PlayerEntity.MateComponent.GetMate(x => x.IsTeamMember && x.MateType == MateType.Partner); + if (mateEntity == null) + { + return; + } + + if (mateEntity.Specialist == null) + { + session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.PARTNER_MESSAGE_NO_SP_EQUIPPED, session.UserLanguage), MsgMessageType.Middle); + return; + } + + if (!mateEntity.IsAlive()) + { + return; + } + + if (!mateEntity.IsSpCooldownElapsed()) + { + session.SendMsg(_gameLanguage.GetLanguageFormat(GameDialogKey.PARTNER_SHOUTMESSAGE_SP_IN_COOLDOWN, session.UserLanguage, mateEntity.GetSpCooldown()), MsgMessageType.Middle); + return; + } + + if (mateEntity.IsSitting) + { + await mateEntity.Owner.Session.EmitEventAsync(new MateRestEvent + { + MateEntity = mateEntity, + Force = true + }); + } + + if (packet.Type == 0) + { + if (!mateEntity.IsUsingSp) + { + DateTime time = await _delayManager.RegisterAction(mateEntity, DelayedActionType.PartnerWearSp); + session.SendMateDelay(mateEntity, time.GetTotalMillisecondUntilNow(), GuriType.Transforming, "#psl^1"); + session.Broadcast(mateEntity.GenerateMateDance(), new RangeBroadcast(mateEntity.PositionX, mateEntity.PositionY)); + return; + } + + await session.EmitEventAsync(new MateSpUntransformEvent + { + MateEntity = mateEntity + }); + return; + } + + bool canPerformAction = await _delayManager.CanPerformAction(mateEntity, DelayedActionType.PartnerWearSp); + if (!canPerformAction) + { + return; + } + + await _delayManager.CompleteAction(session.PlayerEntity, DelayedActionType.PartnerWearSp); + + await session.EmitEventAsync(new MateSpTransformEvent + { + MateEntity = mateEntity + }); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Mate/PtctlPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Mate/PtctlPacketHandler.cs new file mode 100644 index 0000000..a88221f --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Mate/PtctlPacketHandler.cs @@ -0,0 +1,170 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Extensions.Mates; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Networking.Broadcasting; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.Game.Mate; + +public class PtCtlPacketHandler : GenericGamePacketHandlerBase +{ + protected override async Task HandlePacketAsync(IClientSession session, PtCtlPacket packet) + { + if (packet == null) + { + return; + } + + if (session?.CurrentMapInstance == null) + { + return; + } + + if (string.IsNullOrEmpty(packet.PacketEnd)) + { + return; + } + + string[] packetSplit = packet.PacketEnd.Split(' '); + for (int i = 0; i < packet.Amount * 3; i += 3) + { + if (!int.TryParse(packetSplit[i], out int petId)) + { + continue; + } + + if (!short.TryParse(packetSplit[i + 1], out short positionX)) + { + continue; + } + + if (!short.TryParse(packetSplit[i + 2], out short positionY)) + { + continue; + } + + INpcEntity npc = session.PlayerEntity.MapInstance.GetNpcById(petId); + if (npc != null) + { + CheckNpcMovement(session, npc, positionX, positionY); + continue; + } + + IMateEntity mateEntity = session.PlayerEntity.MateComponent.GetMate(s => s.Id == petId); + if (mateEntity == null) + { + continue; + } + + if (!mateEntity.IsAlive()) + { + continue; + } + + if (mateEntity.Owner.IsOnVehicle) + { + continue; + } + + session.SendCondMate(mateEntity); + + if (!mateEntity.CanMove()) + { + session.SendCondMate(mateEntity); + continue; + } + + if (session.CurrentMapInstance.IsBlockedZone(positionX, positionY)) + { + continue; + } + + if (mateEntity.MapInstance.IsBlockedZone(positionX, positionY)) + { + continue; + } + + int distance = mateEntity.Position.GetDistance(positionX, positionY); + if (distance > 10) + { + continue; + } + + if (mateEntity.MapInstance.HasMapFlag(MapFlags.IS_BASE_MAP)) + { + mateEntity.MapX = positionX; + mateEntity.MapY = positionY; + } + + if (mateEntity.MapInstance.MapInstanceType == MapInstanceType.Miniland && mateEntity.MapInstance.Id == mateEntity.Owner.Miniland.Id) + { + mateEntity.MinilandX = positionX; + mateEntity.MinilandY = positionY; + } + + mateEntity.ChangePosition(new Position(positionX, positionY)); + session.BroadcastMovement(mateEntity, new ExceptSessionBroadcast(session)); + } + } + + private void CheckNpcMovement(IClientSession session, INpcEntity npc, short positionX, short positionY) + { + if (npc.MapInstance.MapInstanceType != MapInstanceType.TimeSpaceInstance) + { + return; + } + + if (!npc.IsTimeSpaceMate) + { + return; + } + + List partners = session.PlayerEntity.TimeSpaceComponent.Partners; + INpcEntity partner = partners.FirstOrDefault(x => x.MonsterVNum == npc.MonsterVNum); + if (partner == null || !npc.CharacterPartnerId.HasValue || npc.Id != partner.Id) + { + return; + } + + if (!npc.IsAlive()) + { + return; + } + + session.SendCondMate(npc); + + if (!npc.CanPerformMove()) + { + session.SendCondMate(npc); + return; + } + + if (session.CurrentMapInstance.IsBlockedZone(positionX, positionY)) + { + return; + } + + if (npc.MapInstance.IsBlockedZone(positionX, positionY)) + { + return; + } + + int distance = npc.Position.GetDistance(positionX, positionY); + if (distance > 10) + { + return; + } + + npc.ChangePosition(new Position(positionX, positionY)); + session.BroadcastMovement(npc, new ExceptSessionBroadcast(session)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Mate/SayPPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Mate/SayPPacketHandler.cs new file mode 100644 index 0000000..4029319 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Mate/SayPPacketHandler.cs @@ -0,0 +1,55 @@ +using System.Threading.Tasks; +using WingsEmu.DTOs.Maps; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Entities.Extensions; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Networking.Broadcasting; +using WingsEmu.Packets.ClientPackets; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.PacketHandling.Game.Mate; + +public class SayPPacketHandler : GenericGamePacketHandlerBase +{ + private readonly IGameLanguageService _gameLanguage; + + public SayPPacketHandler(IGameLanguageService gameLanguage) => _gameLanguage = gameLanguage; + + protected override async Task HandlePacketAsync(IClientSession session, SayPPacket packet) + { + if (string.IsNullOrEmpty(packet.Message)) + { + return; + } + + if (session.IsMuted()) + { + session.SendMuteMessage(); + return; + } + + string message = packet.Message; + + if (message.Length > 60) + { + message = message.Substring(0, 60); + } + + IMateEntity mateEntity = session.PlayerEntity.MateComponent.GetMate(s => s.Id == packet.PetId); + if (mateEntity == null) + { + return; + } + + if (session.CurrentMapInstance.HasMapFlag(MapFlags.ACT_4)) + { + session.CurrentMapInstance.Broadcast(mateEntity.GenerateSayPacket(message.Trim(), ChatMessageColorType.Mate), + new FactionBroadcast(session.PlayerEntity.Faction)); + return; + } + + session.CurrentMapInstance.Broadcast(mateEntity.GenerateSayPacket(message, ChatMessageColorType.Mate)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Mate/SuctlPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Mate/SuctlPacketHandler.cs new file mode 100644 index 0000000..d26de4e --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Mate/SuctlPacketHandler.cs @@ -0,0 +1,383 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Foundatio.Utility; +using WingsEmu.DTOs.Skills; +using WingsEmu.Game; +using WingsEmu.Game._enum; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Extensions.Mates; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Skills; +using WingsEmu.Packets.ClientPackets; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.PacketHandling.Game.Mate; + +public class SuctlPacketHandler : GenericGamePacketHandlerBase +{ + private readonly IPartnerSpecialistBasicConfig _partnerSpecialistBasic; + private readonly IRandomGenerator _randomGenerator; + + public SuctlPacketHandler(IRandomGenerator randomGenerator, IPartnerSpecialistBasicConfig partnerSpecialistBasic) + { + _randomGenerator = randomGenerator; + _partnerSpecialistBasic = partnerSpecialistBasic; + } + + protected override async Task HandlePacketAsync(IClientSession session, SuctlPacket packet) + { + if (session.IsMuted()) + { + session.SendMuteMessage(); + return; + } + + if (packet == null) + { + session.SendDebugMessage("packet == null"); + return; + } + + // check, if owner of mate is in PvP cell + if (session.CurrentMapInstance.IsPvp && session.CurrentMapInstance.PvpZone(session.PlayerEntity.PositionX, session.PlayerEntity.PositionY)) + { + session.SendDebugMessage("Character in No-PvP zone"); + return; + } + + if (session.PlayerEntity.IsOnVehicle) + { + session.SendDebugMessage("Mate - session.IsVehicled"); + return; + } + + if (session.PlayerEntity.Invisible) + { + session.SendDebugMessage("Mate - session.PlayerEntity.Invisible"); + return; + } + + if (session.PlayerEntity.IsInvisible()) + { + session.SendDebugMessage("Mate - session.PlayerEntity.IsInvisible()"); + return; + } + + if (session.PlayerEntity.CheatComponent.IsInvisible) + { + session.SendDebugMessage("Mate - session.IsVehicled, InvisibleGm"); + return; + } + + if (session.PlayerEntity.IsInvisible()) + { + return; + } + + INpcEntity npc = session.PlayerEntity.MapInstance.GetNpcById(packet.MateTransportId); + if (npc != null) + { + await CheckNpcAttack(session, npc, packet); + return; + } + + IMateEntity attacker = session.PlayerEntity.MateComponent.GetMate(x => x.Id == packet.MateTransportId); + + if (attacker == null) + { + session.SendDebugMessage("Mate == null"); + return; + } + + if (!attacker.IsAlive()) + { + session.SendDebugMessage("Mate is dead"); + return; + } + + if (attacker.IsCastingSkill) + { + session.SendDebugMessage("Mate IsCasting"); + return; + } + + if (attacker.IsSitting) + { + session.SendDebugMessage("Mate IsSitting"); + return; + } + + if (!attacker.CanAttack()) + { + return; + } + + // check, if mate is in PvP cell + if (attacker.MapInstance.IsPvp && attacker.MapInstance.PvpZone(attacker.PositionX, attacker.PositionY)) + { + session.SendDebugMessage("Mate in No-PvP zone"); + return; + } + + IBattleEntity target = session.PlayerEntity.MapInstance.GetBattleEntity(packet.TargetType, packet.TargetId); + + switch (target) + { + case null: + session.SendDebugMessage("Target == null"); + return; + case IMonsterEntity { MonsterRaceType: MonsterRaceType.Fixed }: + case IPlayerEntity { IsSeal: true }: + return; + } + + // check, if target is in PvP cell + if (target.MapInstance.IsPvp && target.MapInstance.PvpZone(target.PositionX, target.PositionY)) + { + session.SendDebugMessage("Target in No-PvP zone"); + return; + } + + // check, if target owner is in PvP cell + IPlayerEntity targetMateOwner = (target as IMateEntity)?.Owner; + if (targetMateOwner != null && target.MapInstance.PvpZone(targetMateOwner.PositionX, targetMateOwner.PositionY) && target.MapInstance.IsPvp) + { + session.SendDebugMessage("Target Mate Owner in No-PvP zone"); + return; + } + + if (!target.IsAlive()) + { + session.SendDebugMessage("Target is dead"); + return; + } + + if (target is IMonsterEntity mob && mob.SummonerId != 0 && mob.SummonerType == VisualType.Player && !mob.IsMateTrainer) + { + return; + } + + IEnumerable mateSkills = attacker.Skills; + + if (mateSkills == null) + { + session.SendDebugMessage("mateSkills == null"); + return; + } + + IBattleEntitySkill ski = mateSkills.FirstOrDefault(s => s.Skill.SkillType == SkillType.MonsterSkill); + SkillDTO skill = null; + if (ski is NpcMonsterSkill npcMonsterSkill) + { + if (npcMonsterSkill.IsBasicAttack) + { + skill = npcMonsterSkill.Skill; + } + else if (npcMonsterSkill.Rate == 0 || _randomGenerator.RandomNumber() <= npcMonsterSkill.Rate) + { + skill = ski.Skill; + } + } + + SkillInfo skillInfo = skill == null ? attacker.BasicSkill.DeepClone() : skill.GetInfo(battleEntity: attacker); + + if (attacker.IsUsingSp && attacker.Specialist != null) + { + skillInfo.Element = attacker.Specialist.GameItem.Element; + } + + if (skillInfo.Vnum != 0 && !attacker.SkillCanBeUsed(ski, DateTime.UtcNow)) + { + skill = null; + skillInfo = attacker.BasicSkill; + } + + if (session.PlayerEntity.IsAllyWith(target)) + { + return; + } + + if (attacker.Mp < skill?.MpCost) + { + skill = null; + skillInfo = attacker.BasicSkill; + } + + if (skillInfo.Vnum == 0 && attacker.LastBasicSkill > DateTime.UtcNow) + { + return; + } + + if (!session.PlayerEntity.CheatComponent.HasGodMode && skill != null) + { + attacker.RemoveEntityMp(skill.MpCost, skill); + } + + int cellSizeBonus = 3; + + if (target is INpcMonsterEntity npcMonsterEntity) + { + cellSizeBonus += npcMonsterEntity.CellSize; + } + + if (!attacker.Position.IsInRange(target.Position, skillInfo.Range + cellSizeBonus)) + { + return; + } + + session.SendMateEffect(attacker, EffectType.PetAttack); + session.SendMateLife(attacker); + DateTime castTime = attacker.GenerateSkillCastTime(skillInfo); + attacker.LastSkillUse = DateTime.UtcNow; + if (skillInfo.Vnum == 0) + { + if (skillInfo.Cooldown > 10) // Cooldown is > 1 second + { + skillInfo.Cooldown = 10; + } + + attacker.LastBasicSkill = DateTime.UtcNow.AddMilliseconds(attacker.ApplyCooldownReduction(skillInfo) * 100); + + if (attacker.IsUsingSp && attacker.Specialist != null) + { + skillInfo.HitEffect = _partnerSpecialistBasic.GetAttackEffect(attacker.Specialist.GameItem.Morph); + } + } + + await attacker.EmitEventAsync(new BattleExecuteSkillEvent(attacker, target, skillInfo, castTime)); + } + + private async Task CheckNpcAttack(IClientSession session, INpcEntity npc, SuctlPacket packet) + { + if (npc.MapInstance.MapInstanceType != MapInstanceType.TimeSpaceInstance) + { + return; + } + + if (!npc.IsTimeSpaceMate) + { + return; + } + + List partners = session.PlayerEntity.TimeSpaceComponent.Partners; + INpcEntity partner = partners.FirstOrDefault(x => x.MonsterVNum == npc.MonsterVNum); + if (partner == null || !npc.CharacterPartnerId.HasValue || npc.Id != partner.Id) + { + return; + } + + if (!npc.IsAlive()) + { + session.SendDebugMessage("Mate is dead"); + return; + } + + if (npc.IsCastingSkill) + { + session.SendDebugMessage("Mate IsCasting"); + return; + } + + if (npc.IsSitting) + { + session.SendDebugMessage("Mate IsSitting"); + return; + } + + if (!npc.CanPerformAttack()) + { + return; + } + + IBattleEntity target = session.PlayerEntity.MapInstance.GetBattleEntity(packet.TargetType, packet.TargetId); + + switch (target) + { + case null: + session.SendDebugMessage("Target == null"); + return; + case IMonsterEntity { MonsterRaceType: MonsterRaceType.Fixed }: + return; + } + + if (!target.IsAlive()) + { + session.SendDebugMessage("Target is dead"); + return; + } + + if (target is IMonsterEntity mob && mob.SummonerId != 0 && mob.SummonerType == VisualType.Player) + { + return; + } + + IBattleEntitySkill skillToUse = null; + DateTime now = DateTime.UtcNow; + + foreach (IBattleEntitySkill skill in npc.Skills) + { + if (skill is not NpcMonsterSkill npcMonsterSkill) + { + continue; + } + + if (npcMonsterSkill.IsBasicAttack && npc.SkillCanBeUsed(npcMonsterSkill, now)) + { + skillToUse = npcMonsterSkill; + break; + } + + if (!npc.SkillCanBeUsed(npcMonsterSkill, now)) + { + continue; + } + + skillToUse = npcMonsterSkill; + break; + } + + SkillInfo skillInfo = skillToUse?.Skill.GetInfo() ?? npc.BasicSkill; + + if (session.PlayerEntity.IsAllyWith(target)) + { + return; + } + + if (skillInfo.Vnum == 0 && npc.Skills.Any(x => x is NpcMonsterSkill { IsBasicAttack: true })) + { + return; + } + + if (npc.LastBasicAttack > DateTime.UtcNow && skillInfo.Vnum == 0) + { + return; + } + + int cellSizeBonus = 3; + + if (target is INpcMonsterEntity npcMonsterEntity) + { + cellSizeBonus += npcMonsterEntity.CellSize; + } + + if (!npc.Position.IsInRange(target.Position, skillInfo.Range + cellSizeBonus)) + { + return; + } + + session.SendNpcEffect(npc, EffectType.PetAttack); + DateTime castTime = npc.GenerateSkillCastTime(skillInfo); + + npc.LastBasicAttack = DateTime.UtcNow.AddMilliseconds(npc.ApplyCooldownReduction(npc.BasicSkill) * 100); + await npc.EmitEventAsync(new BattleExecuteSkillEvent(npc, target, skillInfo, castTime)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Mate/UpetPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Mate/UpetPacketHandler.cs new file mode 100644 index 0000000..a80226c --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Mate/UpetPacketHandler.cs @@ -0,0 +1,189 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using WingsEmu.DTOs.Skills; +using WingsEmu.Game._enum; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Extensions.Mates; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Skills; +using WingsEmu.Packets.ClientPackets; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.PacketHandling.Game.Mate; + +public class UpetPacketHandler : GenericGamePacketHandlerBase +{ + protected override async Task HandlePacketAsync(IClientSession session, UpetPacket packet) + { + if (session.IsMuted()) + { + session.SendMuteMessage(); + return; + } + + if (!session.PlayerEntity.CanFight() || packet == null) + { + session.SendDebugMessage("[U_PET] !CanFight or packet == null"); + return; + } + + // check, if owner of mate is in PvP cell + if (session.CurrentMapInstance.IsPvp && session.CurrentMapInstance.PvpZone(session.PlayerEntity.PositionX, session.PlayerEntity.PositionY)) + { + session.SendDebugMessage("[U_PET] Character in No-PvP zone"); + return; + } + + IMateEntity attacker = session.PlayerEntity.MateComponent.GetMate(x => x.Id == packet.MateTransportId && x.MateType == MateType.Pet); + + if (attacker == null) + { + session.SendDebugMessage("[U_PET] Mate == null"); + return; + } + + if (!attacker.IsAlive()) + { + session.SendDebugMessage("[U_PET] Mate is dead"); + return; + } + + if (attacker.IsCastingSkill) + { + session.SendDebugMessage("[U_PET] Mate IsCasting"); + return; + } + + if (attacker.IsSitting) + { + session.SendDebugMessage("[U_PET] Mate IsSitting"); + return; + } + + if (!attacker.CanAttack()) + { + session.SendDebugMessage("[U_PET] !canAttack()"); + return; + } + + // check, if mate is in PvP cell + if (attacker.MapInstance.IsPvp && attacker.MapInstance.PvpZone(attacker.PositionX, attacker.PositionY)) + { + session.SendDebugMessage("[U_PET] Mate in No-PvP zone"); + return; + } + + if (session.PlayerEntity.IsOnVehicle || session.PlayerEntity.CheatComponent.IsInvisible) + { + session.SendDebugMessage("[U_PET] Mate - session.IsVehicled, InvisibleGm"); + return; + } + + if (session.PlayerEntity.IsInvisible()) + { + return; + } + + IBattleEntity target = session.PlayerEntity.MapInstance.GetBattleEntity(packet.TargetType, packet.TargetId); + + switch (target) + { + case null: + session.SendDebugMessage("[U_PET] Target == null"); + return; + case IMonsterEntity { MonsterRaceType: MonsterRaceType.Fixed }: + case IPlayerEntity { IsSeal: true }: + return; + } + + // check, if target is in PvP cell + if (target.MapInstance.IsPvp && target.MapInstance.PvpZone(target.PositionX, target.PositionY)) + { + session.SendDebugMessage("[U_PET] Target in No-PvP zone"); + return; + } + + // check, if target owner is in PvP cell + IPlayerEntity targetMateOwner = (target as IMateEntity)?.Owner; + if (targetMateOwner != null && target.MapInstance.PvpZone(targetMateOwner.PositionX, targetMateOwner.PositionY) && target.MapInstance.IsPvp) + { + session.SendDebugMessage("[U_PET] Target Mate Owner in No-PvP zone"); + return; + } + + if (!target.IsAlive()) + { + session.SendDebugMessage("[U_PET] Target is dead"); + return; + } + + IEnumerable mateSkills = attacker.Skills; + + if (mateSkills == null) + { + session.SendDebugMessage("[U_PET] mateSkills == null"); + return; + } + + IBattleEntitySkill ski = mateSkills.FirstOrDefault(s => s.Skill.SkillType == SkillType.PartnerSkill); + + if (ski == null) + { + session.SendDebugMessage("[U_PET] skill == null"); + return; + } + + SkillDTO skill = ski.Skill; + SkillInfo skillInfo = skill.GetInfo(battleEntity: attacker); + + if (!attacker.SkillCanBeUsed(ski, DateTime.UtcNow)) + { + session.SendDebugMessage("[U_PET] !skill.CanBeUsed()"); + return; + } + + if (!attacker.IsEnemyWith(target) && attacker.IsMate() && skillInfo.TargetType != TargetType.Self) + { + session.SendDebugMessage("[U_PET] !attacker.IsEnemyWith(target)"); + return; + } + + if (attacker.Mp < skill.MpCost) + { + session.SendDebugMessage("[U_PET] attacker.Mp < MpCost"); + return; + } + + if (!session.PlayerEntity.CheatComponent.HasGodMode) + { + attacker.RemoveEntityMp(skill.MpCost, skill); + } + + int cellSizeBonus = 3; + + if (target is INpcMonsterEntity npcMonsterEntity) + { + cellSizeBonus += npcMonsterEntity.CellSize; + } + + if (!attacker.Position.IsInRange(target.Position, skillInfo.Range + cellSizeBonus)) + { + session.SendDebugMessage("[U_PET] !attacker.IsInRange"); + return; + } + + attacker.LastSkillUse = DateTime.UtcNow; + DateTime castTime = attacker.GenerateSkillCastTime(skillInfo); + session.SendDebugMessage("[U_PET] Casting u_pet"); + + await attacker.EmitEventAsync(new BattleExecuteSkillEvent(attacker, target, skillInfo, castTime)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Mate/UpsPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Mate/UpsPacketHandler.cs new file mode 100644 index 0000000..20e63e3 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Mate/UpsPacketHandler.cs @@ -0,0 +1,214 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using WingsEmu.DTOs.Skills; +using WingsEmu.Game._enum; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Extensions.Mates; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Mates; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Skills; +using WingsEmu.Packets.ClientPackets; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Battle; + +namespace WingsEmu.Plugins.PacketHandling.Game.Mate; + +public class UpsPacketHandler : GenericGamePacketHandlerBase +{ + protected override async Task HandlePacketAsync(IClientSession session, UpsPacket packet) + { + if (session.IsMuted()) + { + session.SendMuteMessage(); + return; + } + + if (!session.PlayerEntity.CanFight() || packet == null) + { + session.SendDebugMessage("[U_PS] !CanFight or packet == null"); + return; + } + + // check, if owner of mate is in PvP cell + if (session.CurrentMapInstance.IsPvp && session.CurrentMapInstance.PvpZone(session.PlayerEntity.PositionX, session.PlayerEntity.PositionY)) + { + session.SendDebugMessage("[U_PS] Character in No-PvP zone"); + return; + } + + if (packet.SkillSlot > 2) + { + session.SendDebugMessage("[U_PS] skillSlot > 2"); + return; + } + + IMateEntity attacker = session.PlayerEntity.MateComponent.GetMate(x => x.Id == packet.MateTransportId && x.MateType == MateType.Partner); + + if (attacker == null) + { + session.SendDebugMessage("[U_PS] attacker == null"); + return; + } + + if (!attacker.IsUsingSp) + { + session.SendDebugMessage("[U_PS] !attacker.IsUsingSp"); + return; + } + + if (!attacker.IsAlive()) + { + session.SendDebugMessage("[U_PS] attacker is dead"); + return; + } + + if (attacker.IsCastingSkill) + { + session.SendDebugMessage("[U_PS] Mate IsCasting"); + return; + } + + if (attacker.IsSitting) + { + session.SendDebugMessage("[U_PS] Mate IsSitting"); + return; + } + + if (!attacker.CanAttack()) + { + session.SendDebugMessage("[U_PS] !attacker.CanAttack"); + return; + } + + // check, if mate is in PvP cell + if (attacker.MapInstance.IsPvp && attacker.MapInstance.PvpZone(attacker.PositionX, attacker.PositionY)) + { + session.SendDebugMessage("[U_PS] Mate in No-PvP zone"); + return; + } + + if (session.PlayerEntity.IsOnVehicle || session.PlayerEntity.CheatComponent.IsInvisible) + { + session.SendDebugMessage("[U_PS] Mate - session.IsVehicled, InvisibleGm"); + return; + } + + if (session.PlayerEntity.IsInvisible()) + { + return; + } + + IBattleEntity target = session.PlayerEntity.MapInstance.GetBattleEntity(packet.TargetType, packet.TargetId); + + switch (target) + { + case null: + session.SendDebugMessage("[U_PS] Target == null"); + return; + case IMonsterEntity { MonsterRaceType: MonsterRaceType.Fixed }: + case IPlayerEntity { IsSeal: true }: + return; + } + + // check, if target is in PvP cell + if (target.MapInstance.IsPvp && target.MapInstance.PvpZone(target.PositionX, target.PositionY)) + { + session.SendDebugMessage("[U_PS] Target in No-PvP zone"); + return; + } + + // check, if target owner is in PvP cell + IPlayerEntity targetMateOwner = (target as IMateEntity)?.Owner; + if (targetMateOwner != null && target.MapInstance.PvpZone(targetMateOwner.PositionX, targetMateOwner.PositionY) && target.MapInstance.IsPvp) + { + session.SendDebugMessage("[U_PS] Target Mate Owner in No-PvP zone"); + return; + } + + if (!target.IsAlive()) + { + session.SendDebugMessage("[U_PS] Target is dead"); + return; + } + + List mateSkills = attacker.Specialist.PartnerSkills; + PartnerSkill skill = mateSkills?.FirstOrDefault(s => s.Skill.CastId == packet.SkillSlot); + + if (skill == null) + { + session.SendDebugMessage("[U_PS] skill == null"); + return; + } + + if (!attacker.SkillCanBeUsed(skill)) + { + session.SendDebugMessage("[U_PS] !skill.CanBeUsed()"); + return; + } + + SkillInfo skillInfo = skill.Skill.GetInfo(skill, attacker); + if (attacker.IsUsingSp && attacker.Specialist != null) + { + skillInfo.Element = attacker.Specialist.GameItem.Element; + } + + if (!attacker.IsEnemyWith(target) && attacker.IsMate() && skillInfo.TargetType != TargetType.Self) + { + session.SendDebugMessage("[U_PS] !attacker.IsEnemyWith(target)"); + return; + } + + if (attacker.Mp < skill.Skill.MpCost) + { + session.SendDebugMessage("[U_PS] attacker.Mp < MpCost"); + return; + } + + Position newPosition = default; + if (skillInfo.AttackType == AttackType.Dash) + { + if (attacker.MapInstance.IsBlockedZone(packet.MapX, packet.MapY)) + { + return; + } + + newPosition = new Position(packet.MapX, packet.MapY); + if (!attacker.Position.IsInRange(newPosition, skillInfo.Range + 3)) + { + session.SendDebugMessage("[U_PS] newPosition !IsInRange"); + return; + } + } + + if (!session.PlayerEntity.CheatComponent.HasGodMode) + { + attacker.RemoveEntityMp(skill.Skill.MpCost, skill.Skill); + } + + int cellSizeBonus = 3; + + if (target is INpcMonsterEntity npcMonsterEntity) + { + cellSizeBonus += npcMonsterEntity.CellSize; + } + + if (!attacker.Position.IsInRange(target.Position, skillInfo.Range + cellSizeBonus)) + { + session.SendDebugMessage("[U_PS] !attacker.IsInRange"); + return; + } + + DateTime castTime = attacker.GenerateSkillCastTime(skillInfo, true); + attacker.LastUsedPartnerSkill = skill; + session.SendDebugMessage("[U_PS] Casting u_ps"); + + await attacker.EmitEventAsync(new BattleExecuteSkillEvent(attacker, target, skillInfo, castTime, newPosition)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Miniland/AddobjPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Miniland/AddobjPacketHandler.cs new file mode 100644 index 0000000..e8ad85f --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Miniland/AddobjPacketHandler.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; +using WingsEmu.Game.Miniland.Events; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.Game.Miniland; + +public class AddobjPacketHandler : GenericGamePacketHandlerBase +{ + protected override Task HandlePacketAsync(IClientSession session, AddobjPacket packet) => + packet == null ? Task.CompletedTask : session.EmitEventAsync(new AddObjMinilandEvent(packet.Slot, packet.PositionX, packet.PositionY)); +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Miniland/MiniGamePacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Miniland/MiniGamePacketHandler.cs new file mode 100644 index 0000000..ddbce70 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Miniland/MiniGamePacketHandler.cs @@ -0,0 +1,99 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using WingsEmu.Game.Configurations.Miniland; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Miniland; +using WingsEmu.Game.Miniland.Events; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.PacketHandling.Game.Miniland; + +public class MiniGamePacketHandler : GenericGamePacketHandlerBase +{ + protected override async Task HandlePacketAsync(IClientSession session, MinigamePacket packet) + { + if (packet == null + || session.CurrentMapInstance.MapInstanceType != MapInstanceType.Miniland) + { + return; + } + + MapDesignObject mapObject = session.CurrentMapInstance?.MapDesignObjects.FirstOrDefault(s => s.InventoryItem.Slot == packet.Id); + if (mapObject == null + || mapObject.InventoryItem.ItemInstance.GameItem.ItemType != ItemType.Minigame) + { + return; + } + + RewardLevel rewardLevel; + switch (packet.Type) + { + case 1: + await session.EmitEventAsync(new MinigamePlayEvent(mapObject, false)); + break; + case 2: + await session.EmitEventAsync(new MinigameStopEvent(mapObject)); + break; + case 3: + if (packet.Point == null || packet.Point2 == null) + { + return; + } + + await session.EmitEventAsync(new MinigameScoreEvent(mapObject, packet.Point.Value, packet.Point2.Value)); + break; + case 4: + if (packet.Point == null) + { + return; + } + + if (!Enum.IsDefined(typeof(RewardLevel), packet.Point)) + { + throw new ArgumentOutOfRangeException("", "The RewardLevel (Minigame) that was reclaimed doesn't exist in the Enum."); + } + + rewardLevel = (RewardLevel)packet.Point; + + bool coupon = packet.Point2 != null; + await session.EmitEventAsync(new MinigameRewardEvent(rewardLevel, mapObject, coupon)); + break; + case 5: + await session.EmitEventAsync(new MinigameDurabilityInfoEvent(mapObject)); + break; + case 6: + if (packet.Point == null) + { + return; + } + + await session.EmitEventAsync(new MinigameRepairDurabilityEvent(mapObject, Math.Abs(packet.Point.Value))); + break; + case 7: + await session.EmitEventAsync(new MinigameGetYieldInfoEvent(mapObject)); + break; + case 8: + if (packet.Point == null) + { + return; + } + + if (!Enum.IsDefined(typeof(RewardLevel), packet.Point)) + { + throw new ArgumentOutOfRangeException("", "The RewardLevel (Minigame) that was reclaimed doesn't exist in the Enum."); + } + + session.EmitEvent(new MinigameGetYieldRewardEvent(mapObject, (RewardLevel)packet.Point)); + break; + case 9: + await session.EmitEventAsync(new MinigameDurabilityCouponEvent(mapObject)); + break; + case 10: + await session.EmitEventAsync(new MinigamePlayEvent(mapObject, true)); + break; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Miniland/MinilandEditPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Miniland/MinilandEditPacketHandler.cs new file mode 100644 index 0000000..b160de3 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Miniland/MinilandEditPacketHandler.cs @@ -0,0 +1,40 @@ +using System; +using System.Threading.Tasks; +using WingsEmu.Game.Miniland.Events; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.PacketHandling.Game.Miniland; + +public class MinilandEditPacketHandler : GenericGamePacketHandlerBase +{ + protected override async Task HandlePacketAsync(IClientSession session, MlEditPacket packet) + { + if (packet == null) + { + return; + } + + switch (packet.Type) + { + case 1: + if (packet.Parameters == default) + { + return; + } + + await session.EmitEventAsync(new MinilandIntroEvent(packet.Parameters)); + break; + + case 2: + if (!Enum.TryParse(packet.Parameters, out MinilandState state)) + { + throw new ArgumentOutOfRangeException("", $"Miniland State Type received doesn't equal to any known enum value -> 'OutOfRangeValue': {packet.Parameters}"); + } + + await session.EmitEventAsync(new MinilandStateEvent(state)); + break; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Miniland/MjoinPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Miniland/MjoinPacketHandler.cs new file mode 100644 index 0000000..ee3e3c2 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Miniland/MjoinPacketHandler.cs @@ -0,0 +1,71 @@ +using System.Threading.Tasks; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Miniland; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.Game.Miniland; + +public class MjoinPacketHandler : GenericGamePacketHandlerBase +{ + private readonly IMinilandManager _miniland; + private readonly ISessionManager _sessionManager; + + public MjoinPacketHandler(ISessionManager sessionManager, IMinilandManager miniland) + { + _sessionManager = sessionManager; + _miniland = miniland; + } + + protected override async Task HandlePacketAsync(IClientSession session, MJoinPacket packet) + { + switch (packet.Type) + { + case 0: + IClientSession target = _sessionManager.GetSessionByCharacterId(packet.CharacterId); + if (target == null) + { + return; + } + + if (!_miniland.ContainsMinilandInvite(target.PlayerEntity.Id)) + { + return; + } + + if (!_miniland.ContainsTargetInvite(target.PlayerEntity.Id, session.PlayerEntity.Id)) + { + return; + } + + if (packet.OptionType != 1) + { + _miniland.RemoveMinilandInvite(target.PlayerEntity.Id, session.PlayerEntity.Id); + return; + } + + await target.EmitEventAsync(new InviteJoinMinilandEvent(session.CharacterName(), false)); + break; + case 1: + if (packet.OptionType == 1) + { + await session.EmitEventAsync(new MinilandSignPostJoinEvent + { + PlayerId = packet.CharacterId + }); + return; + } + + target = _sessionManager.GetSessionByCharacterId(packet.CharacterId); + if (target == null) + { + return; + } + + await session.EmitEventAsync(new InviteJoinMinilandEvent(target.CharacterName(), false, true)); + break; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Miniland/RmvobjPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Miniland/RmvobjPacketHandler.cs new file mode 100644 index 0000000..952a357 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Miniland/RmvobjPacketHandler.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; +using WingsEmu.Game.Miniland.Events; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.Game.Miniland; + +public class RmvobjPacketHandler : GenericGamePacketHandlerBase +{ + protected override Task HandlePacketAsync(IClientSession session, RmvobjPacket packet) => + session.EmitEventAsync(new RmvObjMinilandEvent(packet.Slot)); +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Miniland/UseobjPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Miniland/UseobjPacketHandler.cs new file mode 100644 index 0000000..bc38a2f --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Miniland/UseobjPacketHandler.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; +using WingsEmu.Game.Miniland.Events; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.Game.Miniland; + +public class UseobjPacketHandler : GenericGamePacketHandlerBase +{ + protected override Task HandlePacketAsync(IClientSession session, UseobjPacket packet) + => packet == null ? Task.CompletedTask : session.EmitEventAsync(new UseObjMinilandEvent(packet.CharacterName, packet.Slot)); +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Npc/BuyPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Npc/BuyPacketHandler.cs new file mode 100644 index 0000000..3c540ca --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Npc/BuyPacketHandler.cs @@ -0,0 +1,48 @@ +using System.Threading.Tasks; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Shops.Event; +using WingsEmu.Packets.ClientPackets; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.PacketHandling.Game.Npc; + +public class BuyPacketHandler : GenericGamePacketHandlerBase +{ + protected override async Task HandlePacketAsync(IClientSession session, BuyPacket buyPacket) + { + if (session.PlayerEntity.IsInExchange()) + { + return; + } + + if (!session.HasCurrentMapInstance) + { + return; + } + + switch (buyPacket.Type) + { + case BuyShopType.CharacterShop: + await session.EmitEventAsync(new ShopPlayerBuyItemEvent + { + OwnerId = buyPacket.OwnerId, + Amount = buyPacket.Amount, + Slot = buyPacket.Slot + }); + break; + + case BuyShopType.ItemShop: + await session.EmitEventAsync(new BuyItemNpcShopEvent + { + OwnerId = buyPacket.OwnerId, + Amount = buyPacket.Amount, + Slot = buyPacket.Slot, + Accept = buyPacket.Amount == 1 + }); + break; + + default: + return; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Npc/MShopPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Npc/MShopPacketHandler.cs new file mode 100644 index 0000000..2866089 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Npc/MShopPacketHandler.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Shops; +using WingsEmu.Game.Shops.Event; +using WingsEmu.Packets.ClientPackets; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.PacketHandling.Game.Npc; + +public class MShopPacketHandler : GenericGamePacketHandlerBase +{ + private readonly IGameLanguageService _language; + + public MShopPacketHandler(IGameLanguageService language) => _language = language; + + protected override async Task HandlePacketAsync(IClientSession session, MShopPacket packet) + { + if (session.IsActionForbidden()) + { + return; + } + + switch (packet.Type) + { + case MShopPacketType.CloseShop: + await session.EmitEventAsync(new ShopPlayerCloseEvent()); + break; + case MShopPacketType.OpenDialog: + session.SendPacket("ishop"); + break; + + case MShopPacketType.OpenShop: + await ProcessPacketStructure(session, packet.PacketData); + break; + + default: + return; + } + } + + private async Task ProcessPacketStructure(IClientSession session, string packet) + { + string[] packetsplit = packet.Split(' '); + + if (packetsplit.Length <= 80) + { + return; + } + + if (!session.HasCurrentMapInstance || session.PlayerEntity.HasShopOpened || session.PlayerEntity.IsInExchange()) + { + return; + } + + var list = new List(); + const int amountPersonalShopItems = 20; + int nonNullItemsCount = 0; + + for (short i = 0; i < amountPersonalShopItems; i++) + { + int packetIndexGuide = i * 4; + if (!(Enum.TryParse(packetsplit[packetIndexGuide], out InventoryType inventoryType) + && short.TryParse(packetsplit[packetIndexGuide + 1], out short inventorySlot) + && short.TryParse(packetsplit[packetIndexGuide + 2], out short sellAmount) + && long.TryParse(packetsplit[packetIndexGuide + 3], out long price))) + { + list.Add(null); + continue; + } + + if (inventorySlot < 0 || sellAmount < 1 || price < 1) + { + list.Add(null); + continue; + } + + if (inventoryType != InventoryType.Equipment && inventoryType != InventoryType.Etc && inventoryType != InventoryType.Main) + { + await session.NotifyStrangeBehavior(StrangeBehaviorSeverity.ABUSING, + $"[PLAYER_SHOP_OPEN] Tried to add an object from a non allowed InventoryType. InventoryType: '{inventoryType.ToString()}'"); + return; + } + + InventoryItem inv = session.PlayerEntity.GetItemBySlotAndType(inventorySlot, inventoryType); + if (inv == null || inv.ItemInstance.Amount < sellAmount) + { + await session.NotifyStrangeBehavior(StrangeBehaviorSeverity.ABUSING, "[PLAYER_SHOP_OPEN] Tried to add a nonexistent object."); + return; + } + + if (!inv.ItemInstance.GameItem.IsTradable || inv.ItemInstance.IsBound) + { + session.SendChatMessage(_language.GetLanguage(GameDialogKey.SHOP_CHATMESSAGE_ONLY_TRADABLE_ITEMS, session.UserLanguage), ChatMessageColorType.Yellow); + session.SendShopEndPacket(ShopEndType.Player); + return; + } + + list.Add(new ShopPlayerItem + { + ShopSlot = i, + PricePerUnit = price, + InventoryItem = inv, + SellAmount = sellAmount + }); + nonNullItemsCount++; + } + + if (nonNullItemsCount < 1) + { + session.SendShopEndPacket(ShopEndType.Player); + session.SendChatMessage(_language.GetLanguage(GameDialogKey.SHOP_CHATMESSAGE_EMPTY, session.UserLanguage), ChatMessageColorType.Yellow); + return; + } + + var shopNameBuilder = new StringBuilder(); + + const int maxShopNameLength = 20; + for (int j = 80; j < packetsplit.Length; j++) + { + if (maxShopNameLength < shopNameBuilder.Length) + { + break; + } + + shopNameBuilder.Append(packetsplit[j]); + shopNameBuilder.Append(' '); + } + + string shopName = shopNameBuilder.ToString(0, maxShopNameLength < shopNameBuilder.Length ? maxShopNameLength : shopNameBuilder.Length); + + if (string.IsNullOrWhiteSpace(shopName) || string.IsNullOrEmpty(shopName)) + { + shopName = _language.GetLanguageFormat(GameDialogKey.SHOP_DEFAULT_NAME, session.UserLanguage, session.PlayerEntity.Name); + } + + await session.EmitEventAsync(new ShopPlayerOpenEvent + { + Items = list, + ShopTitle = shopName + }); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Npc/NrunPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Npc/NrunPacketHandler.cs new file mode 100644 index 0000000..1653d7c --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Npc/NrunPacketHandler.cs @@ -0,0 +1,31 @@ +using System.Threading.Tasks; +using WingsEmu.Game._NpcDialog.Event; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.Game.Npc; + +public class NrunPacketHandler : GenericGamePacketHandlerBase +{ + private readonly IItemUsageManager _itemUsageManager; + + public NrunPacketHandler(IItemUsageManager itemUsageManager) => _itemUsageManager = itemUsageManager; + + protected override async Task HandlePacketAsync(IClientSession session, NRunPacket packet) + { + session.PlayerEntity.LastNRunId = packet.NpcId; + _itemUsageManager.SetLastItemUsed(session.PlayerEntity.Id, 0); + if (session.PlayerEntity.Hp > 0 && session.HasCurrentMapInstance) + { + await session.EmitEventAsync(new NpcDialogEvent + { + NpcRunType = packet.Type, + Argument = packet.Argument, + VisualType = packet.Value, + NpcId = packet.NpcId, + Confirmation = packet.Confirmation + }); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Npc/PdtsePacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Npc/PdtsePacketHandler.cs new file mode 100644 index 0000000..56cc195 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Npc/PdtsePacketHandler.cs @@ -0,0 +1,325 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsAPI.Game.Extensions.ItemExtension.Item; +using WingsAPI.Game.Extensions.Quests; +using WingsEmu.DTOs.Maps; +using WingsEmu.DTOs.Quests; +using WingsEmu.DTOs.Recipes; +using WingsEmu.Game; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Configurations; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Entities.Event; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Items; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Managers.ServerData; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Npcs.Event; +using WingsEmu.Game.Portals; +using WingsEmu.Game.Quests; +using WingsEmu.Game.Quests.Event; +using WingsEmu.Packets.ClientPackets; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.PacketHandling.Game.Npc; + +public class PdtsePacketHandler : GenericGamePacketHandlerBase +{ + private readonly IGameItemInstanceFactory _gameItemInstanceFactory; + private readonly IItemsManager _itemsManager; + private readonly IItemUsageManager _itemUsageManager; + private readonly IGameLanguageService _language; + private readonly INpcEntityFactory _npcEntityFactory; + private readonly INpcMonsterManager _npcMonsterManager; + private readonly IRandomGenerator _randomGenerator; + private readonly IRecipeManager _recipeManager; + private readonly ITimeSpaceConfiguration _timeSpaceConfiguration; + + public PdtsePacketHandler(IGameLanguageService language, IRecipeManager recipeManager, IItemUsageManager itemUsageManager, + IGameItemInstanceFactory gameItemInstanceFactory, IItemsManager itemsManager, IRandomGenerator randomGenerator, + ITimeSpaceConfiguration timeSpaceConfiguration, INpcEntityFactory npcEntityFactory, + INpcMonsterManager npcMonsterManager) + { + _language = language; + _recipeManager = recipeManager; + _itemUsageManager = itemUsageManager; + _gameItemInstanceFactory = gameItemInstanceFactory; + _itemsManager = itemsManager; + _randomGenerator = randomGenerator; + _timeSpaceConfiguration = timeSpaceConfiguration; + _npcEntityFactory = npcEntityFactory; + _npcMonsterManager = npcMonsterManager; + } + + protected override async Task HandlePacketAsync(IClientSession session, PdtsePacket packet) + { + if (!session.HasCurrentMapInstance) + { + return; + } + + short vNum = packet.VNum; + int lastItem = session.PlayerEntity.LastMinilandProducedItem ?? _itemUsageManager.GetLastItemUsed(session.PlayerEntity.Id); + int lastNpcId = session.PlayerEntity.LastNRunId; + short? eqItemSlot = packet.EqItemSlot; + + IReadOnlyList producerItemRecipes = _recipeManager.GetRecipesByProducerItemVnum(lastItem); + Recipe recipe = producerItemRecipes?.FirstOrDefault(x => x.ProducedItemVnum == vNum); + + bool isNpcRecipe = false; + if (recipe == null) + { + IReadOnlyList npcRecipes = _recipeManager.GetRecipesByNpcId(lastNpcId) ?? + _recipeManager.GetRecipesByNpcMonsterVnum(session.CurrentMapInstance.GetNpcById(lastNpcId)?.MonsterVNum ?? 0); + recipe = npcRecipes?.FirstOrDefault(x => x.ProducedItemVnum == vNum); + isNpcRecipe = true; + } + + if (recipe is not { Amount: > 0 }) + { + session.SendEmptyRecipeCraftItem(); + session.PlayerEntity.IsCraftingItem = false; + return; + } + + if (packet.Type == 1) + { + session.SendRecipeCraftItemList(recipe); + session.PlayerEntity.IsCraftingItem = true; + return; + } + + if (!recipe.Items.Any() || recipe.Items.Any(ite => !session.PlayerEntity.HasItem(ite.ItemVNum, ite.Amount))) + { + session.PlayerEntity.IsCraftingItem = false; + return; + } + + IGameItem producedItem = _itemsManager.GetItem(recipe.ProducedItemVnum); + if (producedItem == null) + { + return; + } + + if (!session.PlayerEntity.HasSpaceFor(recipe.ProducedItemVnum, (short)recipe.Amount) && !producedItem.IsTimeSpaceStone()) + { + session.SendChatMessage(_language.GetLanguage(GameDialogKey.INTERACTION_MESSAGE_NOT_ENOUGH_PLACE, session.UserLanguage), ChatMessageColorType.Yellow); + session.PlayerEntity.IsCraftingItem = false; + return; + } + + bool close = false; + short rarity = 0; + short upgrade = 0; + + if (!CanCraftTimeSpace(session, producedItem, _timeSpaceConfiguration)) + { + session.SendShopEndPacket(ShopEndType.Player); + return; + } + + foreach (RecipeItemDTO recipeItem in recipe.Items) + { + InventoryItem getFirstItem = session.PlayerEntity.GetFirstItemByVnum(recipeItem.ItemVNum); + IGameItem eqItem = _itemsManager.GetItem(recipeItem.ItemVNum); + bool isEquipment = false; + + if (eqItem.Type == InventoryType.Equipment && recipeItem.Slot == 0) + { + close = true; + isEquipment = true; + getFirstItem = eqItemSlot.HasValue ? session.PlayerEntity.GetItemBySlotAndType(eqItemSlot.Value, InventoryType.Equipment) : null; + getFirstItem ??= session.PlayerEntity.GetFirstItemByVnum(recipeItem.ItemVNum); + } + + if (!isEquipment) + { + await session.RemoveItemFromInventory(recipeItem.ItemVNum, recipeItem.Amount); + } + else + { + await session.RemoveItemFromInventory(amount: recipeItem.Amount, item: getFirstItem); + } + + if (session.PlayerEntity.HasItem(recipeItem.ItemVNum, recipeItem.Amount)) + { + continue; + } + + close = true; + } + + GameItemInstance newItem = _gameItemInstanceFactory.CreateItem(recipe.ProducedItemVnum, recipe.Amount, (byte)upgrade, (sbyte)rarity); + + if (newItem.GameItem.IsTimeSpaceStone()) + { + await CreateTimeSpaceNpc(session, newItem.GameItem); + return; + } + + await ProcessCraftingQuest(session, newItem); + + if (!isNpcRecipe && close || !session.PlayerEntity.IsCraftingItem) + { + session.SendShopEndPacket(ShopEndType.Player); + } + + await session.EmitEventAsync(new ItemProducedEvent + { + ItemInstance = newItem, + ItemAmount = recipe.Amount + }); + + session.SendSound(SoundType.CRAFTING_SUCCESS); + string itemName = _language.GetLanguage(GameDataType.Item, newItem.GameItem.Name, session.UserLanguage); + session.SendMsg(_language.GetLanguageFormat(GameDialogKey.ITEM_SHOUTMESSAGE_CRAFTED_OBJECT, session.UserLanguage, itemName, recipe.Amount.ToString()), MsgMessageType.Middle); + _itemUsageManager.SetLastItemUsed(session.PlayerEntity.Id, 0); + } + + private bool CanCraftTimeSpace(IClientSession session, IGameItem item, ITimeSpaceConfiguration timeSpaceConfiguration) + { + // If it's not TS crafted stone + if (!item.IsTimeSpaceStone()) + { + return true; + } + + if (!session.CurrentMapInstance.HasMapFlag(MapFlags.IS_BASE_MAP)) + { + session.SendMsg(session.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_MUST_BE_IN_CLASSIC_MAP), MsgMessageType.Middle); + return false; + } + + int mapVnum = item.Data[3]; + + if (mapVnum == -1 && session.CurrentMapInstance.HasMapFlag(MapFlags.ACT_4)) + { + session.SendMsg(session.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_MUST_BE_IN_CLASSIC_MAP), MsgMessageType.Middle); + return false; + } + + if (mapVnum != -1 && session.CurrentMapInstance.MapVnum != mapVnum) + { + session.SendMsg(session.GetLanguage(GameDialogKey.TIMESPACE_SHOUTMESSAGE_WRONG_MAP), MsgMessageType.Middle); + return false; + } + + // Can't create portal close to another one + IPortalEntity portal = session.CurrentMapInstance.Portals.Concat(session.PlayerEntity.GetExtraPortal()).FirstOrDefault(s => + session.PlayerEntity.PositionY >= s.PositionY - 3 && + session.PlayerEntity.PositionY <= s.PositionY + 3 && + session.PlayerEntity.PositionX >= s.PositionX - 3 && + session.PlayerEntity.PositionX <= s.PositionX + 3); + + ITimeSpacePortalEntity tsPortal = session.CurrentMapInstance.TimeSpacePortals.FirstOrDefault(s => + session.PlayerEntity.PositionY >= s.Position.Y - 3 && + session.PlayerEntity.PositionY <= s.Position.Y + 3 && + session.PlayerEntity.PositionX >= s.Position.X - 3 && + session.PlayerEntity.PositionX <= s.Position.X + 3); + + INpcEntity anotherTimeSpaceNpc = session.CurrentMapInstance.GetPassiveNpcs().FirstOrDefault(s => + session.PlayerEntity.PositionY >= s.Position.Y - 3 && + session.PlayerEntity.PositionY <= s.Position.Y + 3 && + session.PlayerEntity.PositionX >= s.Position.X - 3 && + session.PlayerEntity.PositionX <= s.Position.X + 3 && s.TimeSpaceOwnerId.HasValue); + + if (portal != null || tsPortal != null || anotherTimeSpaceNpc != null) + { + session.SendMsg(session.GetLanguage(GameDialogKey.TIMESPACE_SHOUTMESSAGE_CLOSE_PORTAL), MsgMessageType.Middle); + return false; + } + + // Check if player created already Time-Space npc + INpcEntity timeSpaceNpc = session.CurrentMapInstance.GetPassiveNpcs().FirstOrDefault(x => x.TimeSpaceOwnerId.HasValue && session.PlayerEntity.Id == x.TimeSpaceOwnerId.Value); + if (timeSpaceNpc != null) + { + return false; + } + + TimeSpaceFileConfiguration timeSpace = _timeSpaceConfiguration.GetTimeSpaceConfiguration(item.Data[2]); + return timeSpace != null; + } + + private async Task CreateTimeSpaceNpc(IClientSession session, IGameItem item) + { + session.SendSound(SoundType.CRAFTING_SUCCESS); + session.SendShopEndPacket(ShopEndType.Player); + _itemUsageManager.SetLastItemUsed(session.PlayerEntity.Id, 0); + + TimeSpaceFileConfiguration timeSpace = _timeSpaceConfiguration.GetTimeSpaceConfiguration(item.Data[2]); + if (timeSpace == null) + { + return; + } + + IMonsterData timeSpaceMonster = _npcMonsterManager.GetNpc((short)MonsterVnum.TIME_SPACE_NPC); + if (timeSpaceMonster == null) + { + return; + } + + INpcEntity newNpc = _npcEntityFactory.CreateNpc(timeSpaceMonster, session.CurrentMapInstance, null, new NpcAdditionalData + { + TimeSpaceInfo = timeSpace, + TimeSpaceOwnerId = session.PlayerEntity.Id + }); + + await newNpc.EmitEventAsync(new MapJoinNpcEntityEvent(newNpc, session.PlayerEntity.Position.X, session.PlayerEntity.Position.Y)); + string packet = newNpc.GenerateEffectGround(EffectType.BlueTimeSpace, newNpc.PositionX, newNpc.PositionY, false); + newNpc.MapInstance.Broadcast(x => packet); + } + + private async Task ProcessCraftingQuest(IClientSession session, GameItemInstance itemCrafted) + { + IReadOnlyCollection characterQuests = session.PlayerEntity.GetCurrentQuests() + .Where(s => s.Quest.QuestType == QuestType.CRAFT_WITHOUT_KEEPING && s.Quest.Objectives.Any(o => o.Data0 == itemCrafted.ItemVNum)).ToList(); + + if (!characterQuests.Any()) + { + InventoryItem invItem = await session.AddNewItemToInventory(itemCrafted, sendGiftIsFull: true); + GameItemInstance item = invItem.ItemInstance; + if (item.GameItem.EquipmentSlot is EquipmentType.Armor or EquipmentType.MainWeapon or EquipmentType.SecondaryWeapon) + { + item.SetRarityPoint(_randomGenerator); + } + + session.SendPdtiPacket(PdtiType.ItemHasBeenProduced, itemCrafted.ItemVNum, (short)itemCrafted.Amount, invItem.Slot, itemCrafted.Upgrade, itemCrafted.Rarity); + return; + } + + foreach (CharacterQuest quest in characterQuests) + { + IEnumerable objectives = quest.Quest.Objectives; + foreach (QuestObjectiveDto objective in objectives) + { + if (objective.Data0 != itemCrafted.ItemVNum) + { + continue; + } + + CharacterQuestObjectiveDto questObjectiveDto = quest.ObjectiveAmount[objective.ObjectiveIndex]; + + int objectiveAmount = Math.Min(itemCrafted.Amount, quest.ObjectiveAmount[objective.ObjectiveIndex].RequiredAmount); + questObjectiveDto.CurrentAmount += objectiveAmount; + + await session.EmitEventAsync(new QuestObjectiveUpdatedEvent + { + CharacterQuest = quest + }); + + if (session.PlayerEntity.IsQuestCompleted(quest)) + { + await session.EmitEventAsync(new QuestCompletedEvent(quest)); + } + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Npc/RequestNpcPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Npc/RequestNpcPacketHandler.cs new file mode 100644 index 0000000..cd9a737 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Npc/RequestNpcPacketHandler.cs @@ -0,0 +1,211 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.ItemExtension.Item; +using WingsAPI.Game.Extensions.MinilandExtensions; +using WingsAPI.Game.Extensions.PacketGeneration; +using WingsAPI.Game.Extensions.Quests; +using WingsEmu.Core.Extensions; +using WingsEmu.DTOs.ServerDatas; +using WingsEmu.Game; +using WingsEmu.Game._enum; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Managers.ServerData; +using WingsEmu.Game.Managers.StaticData; +using WingsEmu.Game.Monster; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Quests; +using WingsEmu.Game.Quests.Event; +using WingsEmu.Game.RainbowBattle.Event; +using WingsEmu.Packets.ClientPackets; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.PacketHandling.Game.Npc; + +public class RequestNpcPacketHandler : GenericGamePacketHandlerBase +{ + private static readonly QuestType[] DialogQuests = + { + QuestType.DIALOG, + QuestType.DIALOG_2, + QuestType.DELIVER_ITEM_TO_NPC, + QuestType.GIVE_ITEM_TO_NPC, + QuestType.GIVE_ITEM_TO_NPC_2, + QuestType.GIVE_NPC_GOLD, + QuestType.DIALOG_WHILE_WEARING, + QuestType.DIALOG_WHILE_HAVING_ITEM, + QuestType.WIN_RAID_AND_TALK_TO_NPC + }; + + private readonly IDelayManager _delayManager; + private readonly IGameLanguageService _gameLanguageService; + private readonly IItemsManager _itemsManager; + private readonly IQuestManager _questManager; + private readonly IRecipeManager _recipeManager; + private readonly ITeleporterManager _teleporterManager; + + public RequestNpcPacketHandler(IDelayManager delayManager, ITeleporterManager teleporterManager, IRecipeManager recipeManager, IGameLanguageService gameLanguageService, IItemsManager itemsManager, + IQuestManager questManager) + { + _delayManager = delayManager; + _teleporterManager = teleporterManager; + _recipeManager = recipeManager; + _gameLanguageService = gameLanguageService; + _itemsManager = itemsManager; + _questManager = questManager; + } + + protected override async Task HandlePacketAsync(IClientSession session, RequestNpcPacket packet) + { + long owner = packet.TargetNpcId; + if (!session.HasCurrentMapInstance) + { + return; + } + + if (session.PlayerEntity.IsSeal) + { + return; + } + + if (packet.VisualType == VisualType.Player) + { + // User Shop + session.SendTargetNpcDialog(owner, (int)DialogVnums.SHOP_PLAYER); + return; + } + + // Npc Shop , ignore if has drop + INpcEntity npcEntity = session.CurrentMapInstance.GetNpcById(packet.TargetNpcId); + if (npcEntity == null) + { + return; + } + + if (npcEntity.MinilandOwner != null) + { + if (npcEntity.MinilandOwner.Id == session.PlayerEntity.Id) + { + session.ChangeMap(session.PlayerEntity.Miniland); + return; + } + + IPlayerEntity minilandOwner = npcEntity.MinilandOwner; + string message = $"{minilandOwner.Name} - {minilandOwner.Session.GetMinilandCleanMessage(_gameLanguageService)}"; + session.SendQnaPacket($"mjoin 1 {minilandOwner.Id} 1", message); + return; + } + + #region Quest + + bool showNpcDialog = session.ShowNpcDialog(npcEntity, _questManager); + IEnumerable npcTalkQuests = session.PlayerEntity.GetCurrentQuestsByTypes(DialogQuests); + + foreach (CharacterQuest npcTalkQuest in npcTalkQuests) + { + await session.EmitEventAsync(new QuestNpcTalkEvent(npcTalkQuest, npcEntity, _questManager.IsNpcBlueAlertQuest(npcTalkQuest.QuestId))); + } + + #endregion + + if (npcEntity.MonsterRaceType == MonsterRaceType.Fixed) + { + switch (npcEntity.MonsterRaceSubType) + { + case (byte)MonsterSubRace.Fixed.CannonBall: + + await session.EmitEventAsync(new RainbowBattleCaptureFlagEvent + { + NpcEntity = npcEntity + }); + + return; + case (byte)MonsterSubRace.Fixed.Unknown3: // collect + + if (npcEntity.Drops.All(s => s?.MonsterVNum == null)) + { + return; + } + + if (npcEntity.VNumRequired != 0 && npcEntity.AmountRequired != 0) + { + if (!session.PlayerEntity.HasItem(npcEntity.VNumRequired, npcEntity.AmountRequired)) + { + string itemName = _itemsManager.GetItem(npcEntity.VNumRequired).GetItemName(_gameLanguageService, session.UserLanguage); + session.SendMsg(_gameLanguageService.GetLanguageFormat(GameDialogKey.INVENTORY_SHOUTMESSAGE_NOT_ENOUGH_ITEMS, session.UserLanguage, npcEntity.AmountRequired, itemName), + MsgMessageType.Middle); + return; + } + } + + DateTime actionDate = await _delayManager.RegisterAction(session.PlayerEntity, DelayedActionType.Mining, TimeSpan.FromSeconds(npcEntity.CollectionDanceTime)); + + session.SendDelay(actionDate.GetTotalMillisecondUntilNow(), npcEntity.NpcVNum is (short)MonsterVnum.ROBBER_GANG_CHEST ? GuriType.OpeningTreasureChest : GuriType.Mining, + $"guri 400 {npcEntity.Id}"); + + break; + case (byte)MonsterSubRace.Fixed.Unknown1: // teleport + { + if (npcEntity.VNumRequired != 0 && npcEntity.AmountRequired != 0) + { + if (!session.PlayerEntity.HasItem(npcEntity.VNumRequired, npcEntity.AmountRequired)) + { + string itemName = _itemsManager.GetItem(npcEntity.VNumRequired).GetItemName(_gameLanguageService, session.UserLanguage); + session.SendMsg(_gameLanguageService.GetLanguageFormat(GameDialogKey.INVENTORY_SHOUTMESSAGE_NOT_ENOUGH_ITEMS, session.UserLanguage, npcEntity.AmountRequired, itemName), + MsgMessageType.Middle); + return; + } + } + + TeleporterDTO tp = _teleporterManager.GetTeleportByNpcId(npcEntity.Id)?.FirstOrDefault(t => t?.Type == TeleporterType.TELEPORT_ON_MAP); + if (tp == null) + { + return; + } + + actionDate = await _delayManager.RegisterAction(session.PlayerEntity, DelayedActionType.UseTeleporter); + session.SendDelay(actionDate.GetTotalMillisecondUntilNow(), GuriType.ButtonSwitch, $"guri 710 {tp.MapX} {tp.MapY} {npcEntity.Id}"); + break; + } + case (byte)MonsterSubRace.Fixed.MiniLandStructure: + IReadOnlyList recipes = _recipeManager.GetRecipesByNpcMonsterVnum(npcEntity.NpcVNum); + if (recipes == null) + { + if (showNpcDialog) + { + session.SendNpcDialog(npcEntity); + } + + return; + } + + session.SendWopenPacket(WindowType.CRAFTING_ITEMS); + session.SendRecipeNpcList(recipes); + break; + default: + if (showNpcDialog) + { + session.SendNpcDialog(npcEntity); + } + + break; + } + + return; + } + + if (!showNpcDialog) + { + return; + } + + session.SendNpcDialog(npcEntity); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Npc/SellPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Npc/SellPacketHandler.cs new file mode 100644 index 0000000..c6822f8 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Npc/SellPacketHandler.cs @@ -0,0 +1,198 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.ItemExtension.Inventory; +using WingsAPI.Game.Extensions.Quicklist; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Algorithm; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Inventory; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Shops.Event; +using WingsEmu.Game.Skills; +using WingsEmu.Packets.ClientPackets; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.PacketHandling.Game.Npc; + +public class SellPacketHandler : GenericGamePacketHandlerBase +{ + private static readonly HashSet Basics = new() { 200, 201, 220, 221, 240, 241, 260, 261, 1525, 1529 }; + private static readonly HashSet Capture = new() { 209, 235, 236, 237, 1565 }; + private readonly ICharacterAlgorithm _characterAlgorithm; + private readonly IGameLanguageService _language; + private readonly IServerManager _serverManager; + + public SellPacketHandler(IServerManager serverManager, ICharacterAlgorithm characterAlgorithm, IGameLanguageService language) + { + _characterAlgorithm = characterAlgorithm; + _serverManager = serverManager; + _language = language; + } + + protected override async Task HandlePacketAsync(IClientSession session, SellPacket sellPacket) + { + if (session.PlayerEntity.IsInExchange()) + { + return; + } + + if (session.PlayerEntity.IsShopping) + { + return; + } + + if (sellPacket.Amount.HasValue && sellPacket.Slot.HasValue) + { + await HandleItemPacket(session, sellPacket); + return; + } + + HandleSkillPacket(session, sellPacket); + } + + private void HandleSkillPacket(IClientSession session, SellPacket sellPacket) + { + if (session.PlayerEntity.UseSp) + { + session.SendMsg(session.GetLanguage(GameDialogKey.INFORMATION_SHOUTMESSAGE_REMOVE_SP), MsgMessageType.Middle); + return; + } + + if (session.PlayerEntity.IsOnVehicle) + { + session.SendChatMessage(session.GetLanguage(GameDialogKey.SKILL_CHATMESSAGE_CANT_LEARN_MORPHED), ChatMessageColorType.Yellow); + return; + } + + if (session.PlayerEntity.IsInExchange()) + { + return; + } + + if (session.PlayerEntity.HasShopOpened) + { + return; + } + + if (session.PlayerEntity.Skills.Any(s => !session.PlayerEntity.SkillCanBeUsed(s, DateTime.UtcNow))) + { + session.SendMsg(session.GetLanguage(GameDialogKey.SKILL_SHOUTMESSAGE_CANT_LEARN_COOLDOWN), MsgMessageType.Middle); + return; + } + + short vnum = sellPacket.Data; + + if (!session.PlayerEntity.CharacterSkills.TryGetValue(vnum, out CharacterSkill skill)) + { + return; + } + + if (skill == null || IsBasicSkillOrCapture(vnum)) + { + session.SendChatMessage(_language.GetLanguage(GameDialogKey.SKILL_CHATMESSAGE_NOT_TO_REFUND, session.UserLanguage), ChatMessageColorType.Yellow); + return; + } + + if (skill.Skill.IsPassiveSkill()) + { + session.SendChatMessage(_language.GetLanguage(GameDialogKey.SKILL_CHATMESSAGE_NOT_TO_REFUND, session.UserLanguage), ChatMessageColorType.Yellow); + return; + } + + if (skill.Skill.UpgradeSkill != 0) + { + if (session.PlayerEntity.SkillComponent.SkillUpgrades.TryGetValue(skill.Skill.UpgradeSkill, out HashSet hashSet)) + { + hashSet.Remove(skill); + } + } + + foreach (CharacterSkill loadedSkill in session.PlayerEntity.CharacterSkills.Values) + { + if (skill.SkillVNum != loadedSkill.Skill.UpgradeSkill) + { + continue; + } + + session.PlayerEntity.CharacterSkills.TryRemove(loadedSkill.SkillVNum, out CharacterSkill value); + session.PlayerEntity.Skills.Remove(value); + } + + session.PlayerEntity.CharacterSkills.TryRemove(skill.SkillVNum, out CharacterSkill _); + session.PlayerEntity.Skills.Remove(skill); + + if (session.PlayerEntity.SkillComponent.SkillUpgrades.TryGetValue(vnum, out HashSet upgrades)) + { + upgrades.Clear(); + } + + session.RefreshSkillList(); + session.RefreshQuicklist(); + session.RefreshLevel(_characterAlgorithm); + + session.EmitEventAsync(new ShopSkillSoldEvent + { + SkillVnum = skill.SkillVNum + }); + } + + private async Task HandleItemPacket(IClientSession session, SellPacket sellPacket) + { + var type = (InventoryType)sellPacket.Data; + byte slot = sellPacket.Slot.Value; + ushort amount = sellPacket.Amount.Value; + + InventoryItem inv = session.PlayerEntity.GetItemBySlotAndType(slot, type); + if (inv == null) + { + return; + } + + if (amount > inv.ItemInstance.Amount) + { + return; + } + + if (inv.ItemInstance.GameItem.Type == InventoryType.Miniland && + session.PlayerEntity.Miniland != null && session.PlayerEntity.Miniland.MapDesignObjects.Any(s => s.InventorySlot == inv.Slot)) + { + return; + } + + if (inv.ItemInstance.GameItem.ReputPrice != 0) + { + return; + } + + if (!inv.ItemInstance.GameItem.IsSoldable) + { + session.SendSMemo(SmemoType.Error, _language.GetLanguage(GameDialogKey.INTERACTION_LOG_ITEM_NOT_SELLABLE, session.UserLanguage)); + return; + } + + long price = inv.ItemInstance.GameItem.ItemType == ItemType.Sell ? inv.ItemInstance.GameItem.Price : inv.ItemInstance.GameItem.Price / 20 <= 0 ? 1 : inv.ItemInstance.GameItem.Price / 20; + + if (session.PlayerEntity.Gold + price * amount > _serverManager.MaxGold) + { + session.SendSMemo(SmemoType.Error, _language.GetLanguage(GameDialogKey.INFORMATION_MESSAGE_MAX_GOLD, session.UserLanguage)); + return; + } + + session.PlayerEntity.Gold += price * amount; + session.RefreshGold(); + session.SendSMemo(SmemoType.Balance, _language.GetLanguage(GameDialogKey.SHOP_LOG_SELL_ITEM_VALID, session.UserLanguage)); + await session.RemoveItemFromInventory(item: inv, amount: (short)amount); + await session.EmitEventAsync(new ShopNpcSoldItemEvent + { + ItemInstance = inv.ItemInstance, + Amount = (short)amount, + PricePerItem = price + }); + } + + private static bool IsBasicSkillOrCapture(short vnum) => Basics.Contains(vnum) || Capture.Contains(vnum); +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Npc/ShoppingPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Npc/ShoppingPacketHandler.cs new file mode 100644 index 0000000..665fee8 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Npc/ShoppingPacketHandler.cs @@ -0,0 +1,33 @@ +using System.Threading.Tasks; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Shops.Event; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.Game.Npc; + +public class ShoppingPacketHandler : GenericGamePacketHandlerBase +{ + protected override async Task HandlePacketAsync(IClientSession session, ShoppingPacket packet) + { + if (session.PlayerEntity.IsShopping || !session.HasCurrentMapInstance) + { + return; + } + + byte type = packet.Type; + int npcId = packet.NpcId; + + INpcEntity npcEntity = session.CurrentMapInstance.GetNpcById(npcId); + if (npcEntity?.ShopNpc == null) + { + return; + } + + session.EmitEvent(new ShopNpcListItemsEvent + { + NpcId = npcId, + ShopType = type + }); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Relations/BlDelpacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Relations/BlDelpacketHandler.cs new file mode 100644 index 0000000..93b2f3d --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Relations/BlDelpacketHandler.cs @@ -0,0 +1,27 @@ +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.RelationsExtensions; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; +using WingsEmu.Packets.Enums.Relations; + +namespace WingsEmu.Plugins.PacketHandling.Game.Basic; + +public class BlDelPacketHandler : GenericGamePacketHandlerBase +{ + private readonly IGameLanguageService _language; + + public BlDelPacketHandler(IGameLanguageService language) => _language = language; + + protected override async Task HandlePacketAsync(IClientSession session, BlDelPacket packet) + { + if (!session.PlayerEntity.IsBlocking(packet.CharacterId)) + { + return; + } + + await session.RemoveRelationAsync(packet.CharacterId, CharacterRelationType.Blocked); + session.SendInfo(_language.GetLanguage(GameDialogKey.BLACKLIST_INFO_DELETED, session.UserLanguage)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Relations/BlinsPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Relations/BlinsPacketHandler.cs new file mode 100644 index 0000000..b4c9a06 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Relations/BlinsPacketHandler.cs @@ -0,0 +1,17 @@ +using System.Threading.Tasks; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Relations; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.Game.Basic; + +public class BlInsPacketHandler : GenericGamePacketHandlerBase +{ + protected override async Task HandlePacketAsync(IClientSession session, BlInsPacket packet) + { + await session.EmitEventAsync(new RelationBlockEvent + { + CharacterId = packet.CharacterId + }); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Relations/FDelPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Relations/FDelPacketHandler.cs new file mode 100644 index 0000000..a7cf39d --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Relations/FDelPacketHandler.cs @@ -0,0 +1,32 @@ +using System.Threading.Tasks; +using WingsAPI.Game.Extensions.RelationsExtensions; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; +using WingsEmu.Packets.Enums.Relations; + +namespace WingsEmu.Plugins.PacketHandling.Game.Basic; + +public class FDelPacketHandler : GenericGamePacketHandlerBase +{ + private readonly IGameLanguageService _language; + + public FDelPacketHandler(IGameLanguageService language) => _language = language; + + protected override async Task HandlePacketAsync(IClientSession session, FDelPacket packet) + { + if (session.PlayerEntity.IsMarried(packet.CharacterId)) + { + return; + } + + if (!session.PlayerEntity.IsFriend(packet.CharacterId)) + { + return; + } + + await session.RemoveRelationAsync(packet.CharacterId, CharacterRelationType.Friend); + session.SendInfo(_language.GetLanguage(GameDialogKey.FRIEND_INFO_DELETED, session.UserLanguage)); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Relations/FInsPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Relations/FInsPacketHandler.cs new file mode 100644 index 0000000..743e428 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Relations/FInsPacketHandler.cs @@ -0,0 +1,24 @@ +using System; +using System.Threading.Tasks; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Relations; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.Game.Basic; + +public class FInsPacketHandler : GenericGamePacketHandlerBase +{ + protected override async Task HandlePacketAsync(IClientSession session, FInsPacket packet) + { + if (!Enum.TryParse(packet.Type.ToString(), out FInsPacketType type)) + { + return; + } + + await session.EmitEventAsync(new RelationFriendEvent + { + RequestType = type, + CharacterId = packet.CharacterId + }); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/ScriptPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/ScriptPacketHandler.cs new file mode 100644 index 0000000..85a968d --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/ScriptPacketHandler.cs @@ -0,0 +1,143 @@ +using System; +using System.Threading.Tasks; +using PhoenixLib.Logging; +using WingsAPI.Game.Extensions.Quests; +using WingsEmu.DTOs.Quests; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Characters.Events; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Quests; +using WingsEmu.Game.Quests.Event; +using WingsEmu.Packets.ClientPackets; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.PacketHandling.Game; + +public class ScriptPacketHandler : GenericGamePacketHandlerBase +{ + private readonly IQuestManager _questManager; + + public ScriptPacketHandler(IQuestManager questManager) => _questManager = questManager; + + protected override async Task HandlePacketAsync(IClientSession session, ScriptPacket packet) + { + short type = packet.Type; + string data = packet.Data; + + Log.Debug($"Type: {type.ToString()} ; Data: {data}"); + int[] parameters = Array.ConvertAll(data.Split(), int.Parse); + + // Depending on the script you are sending, it follows a few formats: + // script 0 -> finishes the script and starts the next one + // script 4 -> script made for starting quests. + TutorialDto actualScript; + switch (type) + { + case 0: + if (parameters.Length != 2) + { + break; + } + + actualScript = _questManager.GetScriptTutorialByIndex(parameters[0], parameters[1]); + if (actualScript == null) + { + break; + } + + CompletedScriptsDto lastCompletedScript = session.PlayerEntity.GetLastCompletedScript(); + if (lastCompletedScript == null && actualScript.ScriptId != 1 && actualScript.ScriptIndex != 10) // First script + { + await session.NotifyStrangeBehavior(StrangeBehaviorSeverity.SEVERE_ABUSE, + $"Tried to send a script different to the first one on a new character | ScriptId: {actualScript.ScriptId}, ScriptIndex: {actualScript.ScriptIndex}"); + return; + } + + if (lastCompletedScript != null) + { + TutorialDto lastCompletedTutorial = _questManager.GetScriptTutorialByIndex(lastCompletedScript.ScriptId, lastCompletedScript.ScriptIndex); + if (Math.Abs(lastCompletedTutorial.Id - actualScript.Id) > 1) + { + // We check first it it's a bugged WAIT_FOR_QUEST_COMPLETION that was not sent to complete + TutorialDto expectedTutorial = _questManager.GetScriptTutorialById(lastCompletedTutorial.Id + 1); + if (expectedTutorial.Type == TutorialActionType.WAIT_FOR_QUEST_COMPLETION && session.PlayerEntity.HasCompletedQuest(expectedTutorial.Data)) + { + session.PlayerEntity.SaveScript(expectedTutorial.ScriptId, expectedTutorial.ScriptIndex, expectedTutorial.Type, DateTime.UtcNow); + await ProcessNextScriptLogic(session, expectedTutorial); + return; + } + + await session.NotifyStrangeBehavior(StrangeBehaviorSeverity.SEVERE_ABUSE, + $"Tried to send a script bigger than it should be | ExpectedId: {lastCompletedTutorial.Id + 1} | SentId: {actualScript.Id}"); + return; + } + + // Checking if it's trying to manually send a script 0 packet + if (actualScript.Type == TutorialActionType.WAIT_FOR_QUEST_COMPLETION && !session.PlayerEntity.IsQuestCompleted(session.PlayerEntity.GetQuestById(actualScript.Data))) + { + CharacterQuest activeQuest = session.PlayerEntity.GetQuestById(actualScript.Data); + if (activeQuest != null && !session.PlayerEntity.IsQuestCompleted(activeQuest) && activeQuest?.Quest.QuestType != QuestType.DIALOG && + activeQuest?.Quest.QuestType != QuestType.DIALOG_2) + { + await session.NotifyStrangeBehavior(StrangeBehaviorSeverity.SEVERE_ABUSE, + $"Tried to send manually a script 0 for its completion | ScriptId: {actualScript.ScriptId} | ScriptIndex. {actualScript.ScriptIndex}"); + return; + } + } + } + + + session.PlayerEntity.SaveScript(actualScript.ScriptId, actualScript.ScriptIndex, actualScript.Type, DateTime.UtcNow); + await ProcessNextScriptLogic(session, actualScript); + break; + case 1: + if (parameters.Length != 1) + { + break; + } + + await session.EmitEventAsync(new RunScriptEvent + { + RunId = parameters[0] + }); + break; + case 4: + if (parameters.Length != 3) + { + break; + } + + actualScript = _questManager.GetScriptTutorialByIndex(parameters[1], parameters[2]); + if (actualScript == null) + { + break; + } + + await session.EmitEventAsync(new AddQuestEvent(parameters[0], QuestSlotType.MAIN)); + break; + } + } + + private async Task ProcessNextScriptLogic(IClientSession session, TutorialDto actualScript) + { + TutorialDto nextScript = _questManager.GetScriptTutorialById(actualScript.Id + 1); + if (nextScript == null) + { + return; + } + + // If it's the end of the script, the player has to take the next quest from an NPC + if (nextScript.ScriptId != actualScript.ScriptId) + { + QuestNpcDto questNpc = _questManager.GetQuestNpcByScriptId(nextScript.ScriptId); + session.SendQnpcPacket(questNpc.Level, questNpc.NpcVnum, questNpc.MapId); + session.SendChatMessage(session.GetLanguage(GameDialogKey.QUEST_CHATMESSAGE_NEW_MISSION_FROM_NPC), ChatMessageColorType.Green); + } + else + { + session.SendScriptPacket(nextScript.ScriptId, nextScript.ScriptIndex); + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/ScriptedInstance/BscPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/ScriptedInstance/BscPacketHandler.cs new file mode 100644 index 0000000..21c40c8 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/ScriptedInstance/BscPacketHandler.cs @@ -0,0 +1,49 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Networking; +using WingsEmu.Game.RainbowBattle; +using WingsEmu.Packets.ClientPackets; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.PacketHandling.Game.ScriptedInstance; + +public class BscPacketHandler : GenericGamePacketHandlerBase +{ + private readonly IGameLanguageService _language; + private readonly IRainbowBattleManager _rainbowBattleManager; + private readonly IServerManager _serverManager; + + public BscPacketHandler(IServerManager serverManager, IGameLanguageService language, IRainbowBattleManager rainbowBattleManager) + { + _serverManager = serverManager; + _language = language; + _rainbowBattleManager = rainbowBattleManager; + } + + protected override async Task HandlePacketAsync(IClientSession session, BscPacket packet) + { + if (!Enum.TryParse(packet.Type.ToString(), out GameType type)) + { + return; + } + + switch (type) + { + case GameType.ArenaOfTalents: + break; + case GameType.RainbowBattle: + if (!_rainbowBattleManager.RegisteredPlayers.Contains(session.PlayerEntity.Id)) + { + return; + } + + _rainbowBattleManager.UnregisterPlayer(session.PlayerEntity.Id); + session.SendBsInfoPacket(BsInfoType.CloseWindow, GameType.RainbowBattle, 0, QueueWindow.WaitForEntry); + break; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/ScriptedInstance/EscapePacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/ScriptedInstance/EscapePacketHandler.cs new file mode 100644 index 0000000..ed1c275 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/ScriptedInstance/EscapePacketHandler.cs @@ -0,0 +1,34 @@ +using System.Threading.Tasks; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Raids.Events; +using WingsEmu.Game.RainbowBattle.Event; +using WingsEmu.Game.TimeSpaces.Events; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.Game.ScriptedInstance; + +public class EscapePacketHandler : GenericGamePacketHandlerBase +{ + protected override async Task HandlePacketAsync(IClientSession session, EscapePacket packet) + { + switch (session.CurrentMapInstance.MapInstanceType) + { + case MapInstanceType.RainbowBattle: + await session.EmitEventAsync(new RainbowBattleLeaveEvent + { + CheckIfFinished = true + }); + break; + case MapInstanceType.RaidInstance: + await session.EmitEventAsync(new RaidPartyLeaveEvent(false)); + break; + case MapInstanceType.TimeSpaceInstance: + await session.EmitEventAsync(new TimeSpaceLeavePartyEvent + { + CheckFinished = true + }); + break; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/ScriptedInstance/FbPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/ScriptedInstance/FbPacketHandler.cs new file mode 100644 index 0000000..5147d4c --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/ScriptedInstance/FbPacketHandler.cs @@ -0,0 +1,22 @@ +using System.Threading.Tasks; +using WingsAPI.Packets.ClientPackets; +using WingsEmu.Game.Networking; +using WingsEmu.Game.RainbowBattle.Event; + +namespace WingsEmu.Plugins.PacketHandling.Game.ScriptedInstance; + +public class FbPacketHandler : GenericGamePacketHandlerBase +{ + protected override async Task HandlePacketAsync(IClientSession session, FbPacket packet) + { + if (!session.PlayerEntity.RainbowBattleComponent.IsInRainbowBattle) + { + return; + } + + await session.EmitEventAsync(new RainbowBattleLeaveEvent + { + SendMessage = true + }); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/ScriptedInstance/GitPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/ScriptedInstance/GitPacketHandler.cs new file mode 100644 index 0000000..829268b --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/ScriptedInstance/GitPacketHandler.cs @@ -0,0 +1,50 @@ +using System.Threading.Tasks; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Raids; +using WingsEmu.Game.Raids.Events; +using WingsEmu.Game.TimeSpaces.Events; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.Game.ScriptedInstance; + +public class GitPacketHandler : GenericGamePacketHandlerBase +{ + private readonly IDelayManager _delayManager; + + public GitPacketHandler(IDelayManager delayManager) => _delayManager = delayManager; + + protected override async Task HandlePacketAsync(IClientSession session, GitPacket packet) + { + MapItem mapItem = session.CurrentMapInstance.GetDrop(packet.ButtonId); + if (mapItem == null) + { + return; + } + + if (!await _delayManager.CanPerformAction(session.PlayerEntity, DelayedActionType.ButtonSwitch)) + { + return; + } + + switch (mapItem) + { + case ButtonMapItem buttonMapItem: + + if (buttonMapItem.CanBeMovedOnlyOnce.HasValue && buttonMapItem.CanBeMovedOnlyOnce.Value) + { + return; + } + + await session.EmitEventAsync(new RaidPlayerSwitchButtonEvent(buttonMapItem)); + break; + case TimeSpaceMapItem timeSpaceMapItem: + await session.EmitEventAsync(new TimeSpacePickUpItemEvent + { + TimeSpaceMapItem = timeSpaceMapItem + }); + break; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/ScriptedInstance/MkRaidPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/ScriptedInstance/MkRaidPacketHandler.cs new file mode 100644 index 0000000..d10b646 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/ScriptedInstance/MkRaidPacketHandler.cs @@ -0,0 +1,59 @@ +using System.Linq; +using System.Threading.Tasks; +using WingsAPI.Packets.Enums; +using WingsEmu.Game; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Raids.Events; +using WingsEmu.Packets.ClientPackets; +using WingsEmu.Packets.Enums; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.PacketHandling.Game.ScriptedInstance; + +public class MkRaidPacketHandler : GenericGamePacketHandlerBase +{ + private readonly IGameLanguageService _gameLanguage; + + public MkRaidPacketHandler(IGameLanguageService gameLanguage) => _gameLanguage = gameLanguage; + + protected override async Task HandlePacketAsync(IClientSession session, MkraidPacket packet) + { + if (!session.PlayerEntity.IsInRaidParty) + { + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.RAID_CHATMESSAGE_START_NOT_IN_RAID_PARTY, session.UserLanguage), ChatMessageColorType.PlayerSay); + return; + } + + if (!session.PlayerEntity.IsRaidLeader(session.PlayerEntity.Id)) + { + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.RAID_CHATMESSAGE_START_IS_NOT_LEADER, session.UserLanguage), ChatMessageColorType.PlayerSay); + return; + } + + if (session.PlayerEntity.Raid.Started) + { + return; + } + + IPortalEntity portal = session.CurrentMapInstance.Portals.FirstOrDefault(s => + session.PlayerEntity.PositionY >= s.PositionY - 1 && + session.PlayerEntity.PositionY <= s.PositionY + 1 && + session.PlayerEntity.PositionX >= s.PositionX - 1 && + session.PlayerEntity.PositionX <= s.PositionX + 1); + + if (portal == null || portal.Type != PortalType.Raid || portal.RaidType == null) + { + return; + } + + if (session.PlayerEntity.Raid.Type != (RaidType)portal.RaidType.Value) + { + session.SendChatMessage(_gameLanguage.GetLanguage(GameDialogKey.RAID_CHATMESSAGE_START_RAID_TYPE_IS_WRONG, session.UserLanguage), ChatMessageColorType.PlayerSay); + return; + } + + await session.EmitEventAsync(new RaidInstanceStartEvent()); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/ScriptedInstance/RselPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/ScriptedInstance/RselPacketHandler.cs new file mode 100644 index 0000000..81da0d5 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/ScriptedInstance/RselPacketHandler.cs @@ -0,0 +1,17 @@ +using System.Threading.Tasks; +using WingsEmu.Game.Networking; +using WingsEmu.Game.TimeSpaces.Events; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.Game.ScriptedInstance; + +public class RSelPacketHandler : GenericGamePacketHandlerBase +{ + protected override async Task HandlePacketAsync(IClientSession session, RSelPacket packet) + { + await session.EmitEventAsync(new TimeSpaceSelectRewardEvent + { + SendRepayPacket = true + }); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/ScriptedInstance/RxitPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/ScriptedInstance/RxitPacketHandler.cs new file mode 100644 index 0000000..8f9ce98 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/ScriptedInstance/RxitPacketHandler.cs @@ -0,0 +1,51 @@ +using System.Threading.Tasks; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Managers; +using WingsEmu.Game.Maps; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Raids.Events; +using WingsEmu.Game.TimeSpaces.Events; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.Game.ScriptedInstance; + +public class RxitPacketHandler : GenericGamePacketHandlerBase +{ + private readonly IGameLanguageService _language; + private readonly IMapManager _mapManager; + private readonly ISessionManager _sessionManager; + + public RxitPacketHandler(IGameLanguageService language, IMapManager mapManager, ISessionManager sessionManager) + { + _sessionManager = sessionManager; + _mapManager = mapManager; + _language = language; + } + + protected override async Task HandlePacketAsync(IClientSession session, RxitPacket packet) + { + if (packet?.State != 1) + { + return; + } + + switch (session.CurrentMapInstance?.MapInstanceType) + { + case MapInstanceType.TimeSpaceInstance: + await session.EmitEventAsync(new TimeSpaceLeavePartyEvent + { + RemoveLive = true, + CheckForSeeds = true + }); + break; + case MapInstanceType.RaidInstance when session.PlayerEntity.IsInRaidParty: + if (session.PlayerEntity.Raid is { Finished: true }) + { + return; + } + + await session.EmitEventAsync(new RaidPartyLeaveEvent(false)); + break; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/ScriptedInstance/TaCallPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/ScriptedInstance/TaCallPacketHandler.cs new file mode 100644 index 0000000..7834397 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/ScriptedInstance/TaCallPacketHandler.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.Game.ScriptedInstance; + +public class TaCallPacketHandler : GenericGamePacketHandlerBase +{ + protected override async Task HandlePacketAsync(IClientSession session, TaCallPacket packet) + { + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/ScriptedInstance/TawPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/ScriptedInstance/TawPacketHandler.cs new file mode 100644 index 0000000..c952e47 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/ScriptedInstance/TawPacketHandler.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.Game.ScriptedInstance; + +public class TawPacketHandler : GenericGamePacketHandlerBase +{ + protected override async Task HandlePacketAsync(IClientSession session, TawPacket packet) + { + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/ScriptedInstance/TreqPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/ScriptedInstance/TreqPacketHandler.cs new file mode 100644 index 0000000..d2ee4b3 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/ScriptedInstance/TreqPacketHandler.cs @@ -0,0 +1,207 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using PhoenixLib.Logging; +using WingsAPI.Communication.DbServer.TimeSpaceService; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Portals; +using WingsEmu.Game.TimeSpaces; +using WingsEmu.Game.TimeSpaces.Events; +using WingsEmu.Packets.ClientPackets; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.PacketHandling.Game.ScriptedInstance; + +public class TReqPacketHandler : GenericGamePacketHandlerBase +{ + private readonly ITimeSpaceService _timeSpaceService; + + public TReqPacketHandler(ITimeSpaceService timeSpaceService) => _timeSpaceService = timeSpaceService; + + protected override async Task HandlePacketAsync(IClientSession session, TreqClientPacket clientPacket) + { + if (!session.HasCurrentMapInstance) + { + return; + } + + if (session.PlayerEntity.IsSeal) + { + return; + } + + if (!session.PlayerEntity.IsAlive()) + { + return; + } + + if (session.PlayerEntity.TimeSpaceComponent.IsInTimeSpaceParty) + { + return; + } + + INpcEntity npcTimeSpacePortal = session.CurrentMapInstance.GetPassiveNpcs().FirstOrDefault(x => x.TimeSpaceInfo != null && + clientPacket.X == x.PositionX && clientPacket.Y == x.PositionY); + if (npcTimeSpacePortal != null) + { + await HandleNpcTimeSpacePortal(session, npcTimeSpacePortal, clientPacket); + return; + } + + ITimeSpacePortalEntity portal = session.CurrentMapInstance.TimeSpacePortals.FirstOrDefault(s => + clientPacket.X == s.Position.X && clientPacket.Y == s.Position.Y); + + if (portal == null) + { + return; + } + + if (portal.Position.GetDistance(session.PlayerEntity.Position) > 5) + { + return; + } + + switch (clientPacket.StartPress) + { + case 0: + IPlayerEntity findMemberInTimeSpace = session.PlayerEntity.GetGroup()?.Members + .FirstOrDefault(x => x.TimeSpaceComponent.TimeSpace?.Instance != null && x.TimeSpaceComponent.TimeSpace.TimeSpaceId == portal.TimeSpaceId); + + if (findMemberInTimeSpace != null && clientPacket.RecordPressAndCharacterId == 0) + { + session.SendDialog($"treq {portal.Position.X} {portal.Position.Y} 3 {findMemberInTimeSpace.Id}", $"treq {portal.Position.X} {portal.Position.Y} 0 1", + session.GetLanguage(GameDialogKey.TIMESPACE_DIALOG_ASK_JOIN_TO_PLAYER)); + return; + } + + TimeSpaceRecordResponse record = await _timeSpaceService.GetTimeSpaceRecord(new TimeSpaceRecordRequest + { + TimeSpaceId = portal.TimeSpaceId + }); + + session.SendTimeSpaceInfo(portal, record?.TimeSpaceRecordDto); + break; + case 1: + if (clientPacket.RecordPressAndCharacterId == 1 && !clientPacket.RecordPressConfirm) + { + session.SendQnaPacket($"treq {portal.Position.X} {portal.Position.Y} 1 0 1", session.GetLanguageFormat(GameDialogKey.TIMESPACE_DIALOG_ASK_START_RECORD, portal.MinLevel * 50)); + return; + } + + await session.EmitEventAsync(new TimeSpacePartyCreateEvent(portal.TimeSpaceId, null, false, clientPacket.RecordPressConfirm)); + if (!session.PlayerEntity.TimeSpaceComponent.IsInTimeSpaceParty) + { + return; + } + + await session.EmitEventAsync(new TimeSpaceInstanceStartEvent()); + break; + case 3: + if (clientPacket.RecordPressAndCharacterId == 0) + { + return; + } + + await session.EmitEventAsync(new TimeSpaceGroupTryJoinEvent + { + CharacterId = clientPacket.RecordPressAndCharacterId, + PortalEntity = portal + }); + break; + case 5: + await session.EmitEventAsync(new TimeSpacePartyCreateEvent(portal.TimeSpaceId, null, true)); + if (!session.PlayerEntity.TimeSpaceComponent.IsInTimeSpaceParty) + { + return; + } + + await session.EmitEventAsync(new TimeSpaceInstanceStartEvent()); + break; + } + } + + private async Task HandleNpcTimeSpacePortal(IClientSession session, INpcEntity npcTimeSpacePortal, TreqClientPacket clientPacket) + { + if (npcTimeSpacePortal.Position.GetDistance(session.PlayerEntity.Position) > 5) + { + return; + } + + if (npcTimeSpacePortal.TimeSpaceInfo == null) + { + Log.Error("TimeSpaceInfo couldn't be find in this TimeSpacePortal", new Exception()); + return; + } + + if (npcTimeSpacePortal.TimeSpaceOwnerId == null) + { + Log.Error("TimeSpacePortal doesn't have any owner", new Exception()); + return; + } + + bool isSoloTimeSpace = npcTimeSpacePortal.TimeSpaceInfo.MinPlayers == 1 && npcTimeSpacePortal.TimeSpaceInfo.MaxPlayers == 1; + switch (isSoloTimeSpace) + { + // Solo Time-Space and player is in group + case true when session.PlayerEntity.IsInGroup(): + session.SendMsg(session.GetLanguage(GameDialogKey.TIMESPACE_SHOUTMESSAGE_CANT_BE_IN_GROUP), MsgMessageType.Middle); + return; + // Group Time-Space and player is not in group + case false when !session.PlayerEntity.IsInGroup(): + session.SendMsg(session.GetLanguage(GameDialogKey.TIMESPACE_SHOUTMESSAGE_MUST_BE_IN_GROUP), MsgMessageType.Middle); + return; + } + + if (session.PlayerEntity.Id != npcTimeSpacePortal.TimeSpaceOwnerId.Value) + { + if (!session.PlayerEntity.IsInGroup()) + { + return; + } + + bool timeSpaceOwnerInGroup = session.PlayerEntity.GetGroup().Members.Any(member => member.Id == npcTimeSpacePortal.TimeSpaceOwnerId.Value); + if (!timeSpaceOwnerInGroup) + { + return; + } + } + + switch (clientPacket.StartPress) + { + case 0: + TimeSpaceRecordResponse record = await _timeSpaceService.GetTimeSpaceRecord(new TimeSpaceRecordRequest + { + TimeSpaceId = npcTimeSpacePortal.TimeSpaceInfo.TsId + }); + + session.SendTimeSpaceInfo(npcTimeSpacePortal, record?.TimeSpaceRecordDto); + break; + case 1: + if (session.PlayerEntity.Id != npcTimeSpacePortal.TimeSpaceOwnerId.Value) + { + session.SendMsg(session.GetLanguage(GameDialogKey.TIMESPACE_SHOUTMESSAGE_NOT_OWNER), MsgMessageType.Middle); + return; + } + + if (clientPacket.RecordPressAndCharacterId == 1 && !clientPacket.RecordPressConfirm) + { + session.SendQnaPacket($"treq {npcTimeSpacePortal.Position.X} {npcTimeSpacePortal.Position.Y} 1 0 1", + session.GetLanguageFormat(GameDialogKey.TIMESPACE_DIALOG_ASK_START_RECORD, npcTimeSpacePortal.TimeSpaceInfo.MinLevel * 50)); + return; + } + + await session.EmitEventAsync(new TimeSpaceTryStartHiddenEvent + { + TimeSpacePortal = npcTimeSpacePortal, + IsChallengeMode = clientPacket.RecordPressConfirm + }); + break; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/ScriptedInstance/WreqPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/ScriptedInstance/WreqPacketHandler.cs new file mode 100644 index 0000000..334a507 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/ScriptedInstance/WreqPacketHandler.cs @@ -0,0 +1,211 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using WingsAPI.Communication.DbServer.TimeSpaceService; +using WingsEmu.Game._i18n; +using WingsEmu.Game.Battle; +using WingsEmu.Game.Characters; +using WingsEmu.Game.Entities; +using WingsEmu.Game.Extensions; +using WingsEmu.Game.Helpers.Damages; +using WingsEmu.Game.Networking; +using WingsEmu.Game.Portals; +using WingsEmu.Game.TimeSpaces; +using WingsEmu.Game.TimeSpaces.Events; +using WingsEmu.Packets.ClientPackets; +using WingsEmu.Packets.Enums.Chat; + +namespace WingsEmu.Plugins.PacketHandling.Game.ScriptedInstance; + +public class WreqPacketHandler : GenericGamePacketHandlerBase +{ + private readonly ITimeSpaceService _timeSpaceService; + + public WreqPacketHandler(ITimeSpaceService timeSpaceService) => _timeSpaceService = timeSpaceService; + + protected override async Task HandlePacketAsync(IClientSession session, WreqPacket packet) + { + if (!session.HasCurrentMapInstance) + { + return; + } + + if (session.PlayerEntity.IsSeal) + { + return; + } + + if (!session.PlayerEntity.IsAlive()) + { + return; + } + + if (session.PlayerEntity.TimeSpaceComponent.IsInTimeSpaceParty) + { + return; + } + + INpcEntity npcTimeSpacePortal = session.CurrentMapInstance.GetPassiveNpcs().FirstOrDefault(s => + session.PlayerEntity.PositionY >= s.Position.Y - 1 && + session.PlayerEntity.PositionY <= s.Position.Y + 1 && + session.PlayerEntity.PositionX >= s.Position.X - 1 && + session.PlayerEntity.PositionX <= s.Position.X + 1 && s.TimeSpaceOwnerId.HasValue); + + if (npcTimeSpacePortal != null) + { + await HandleNpcTimeSpacePortal(session, npcTimeSpacePortal, packet); + return; + } + + ITimeSpacePortalEntity portal = session.CurrentMapInstance.TimeSpacePortals.FirstOrDefault(s => + session.PlayerEntity.PositionY >= s.Position.Y - 1 && + session.PlayerEntity.PositionY <= s.Position.Y + 1 && + session.PlayerEntity.PositionX >= s.Position.X - 1 && + session.PlayerEntity.PositionX <= s.Position.X + 1); + + if (portal == null) + { + return; + } + + if (session.PlayerEntity.LastAttack.AddSeconds(5) > DateTime.UtcNow) + { + session.SendChatMessage(session.GetLanguage(GameDialogKey.TIMESPACE_CHATMESSAGE_ATTACK_COOLDOWN), ChatMessageColorType.Yellow); + return; + } + + switch (packet.Value) + { + case 0: + IPlayerEntity findMemberInTimeSpace = session.PlayerEntity.GetGroup()?.Members + .FirstOrDefault(x => x.TimeSpaceComponent.TimeSpace?.Instance != null && x.TimeSpaceComponent.TimeSpace.TimeSpaceId == portal.TimeSpaceId); + + if (findMemberInTimeSpace != null && !packet.Param.HasValue) + { + session.SendDialog($"wreq 3 {findMemberInTimeSpace.Id}", "wreq 0 1", session.GetLanguage(GameDialogKey.TIMESPACE_DIALOG_ASK_JOIN_TO_PLAYER)); + return; + } + + TimeSpaceRecordResponse record = await _timeSpaceService.GetTimeSpaceRecord(new TimeSpaceRecordRequest + { + TimeSpaceId = portal.TimeSpaceId + }); + + session.SendTimeSpaceInfo(portal, record?.TimeSpaceRecordDto); + break; + case 1: + if (packet.Param is 1) + { + session.SendQnaPacket("wreq 1 0", session.GetLanguageFormat(GameDialogKey.TIMESPACE_DIALOG_ASK_START_RECORD, portal.MinLevel * 50)); + return; + } + + await session.EmitEventAsync(new TimeSpacePartyCreateEvent(portal.TimeSpaceId, null, false, packet.Param is 0)); + if (!session.PlayerEntity.TimeSpaceComponent.IsInTimeSpaceParty) + { + return; + } + + await session.EmitEventAsync(new TimeSpaceInstanceStartEvent()); + break; + case 3: + if (!packet.Param.HasValue) + { + return; + } + + await session.EmitEventAsync(new TimeSpaceGroupTryJoinEvent + { + CharacterId = packet.Param.Value, + PortalEntity = portal + }); + break; + case 5: + if (portal.IsHidden || portal.IsSpecial) + { + return; + } + + await session.EmitEventAsync(new TimeSpacePartyCreateEvent(portal.TimeSpaceId, null, true)); + if (!session.PlayerEntity.TimeSpaceComponent.IsInTimeSpaceParty) + { + return; + } + + await session.EmitEventAsync(new TimeSpaceInstanceStartEvent()); + break; + } + } + + private async Task HandleNpcTimeSpacePortal(IClientSession session, INpcEntity npcTimeSpacePortal, WreqPacket clientPacket) + { + if (npcTimeSpacePortal.Position.GetDistance(session.PlayerEntity.Position) > 5) + { + return; + } + + if (npcTimeSpacePortal.TimeSpaceInfo == null || npcTimeSpacePortal.TimeSpaceOwnerId == null) + { + return; + } + + bool isSoloTimeSpace = npcTimeSpacePortal.TimeSpaceInfo.MinPlayers == 1 && npcTimeSpacePortal.TimeSpaceInfo.MaxPlayers == 1; + switch (isSoloTimeSpace) + { + // Solo Time-Space and player is in group + case true when session.PlayerEntity.IsInGroup(): + session.SendMsg(session.GetLanguage(GameDialogKey.TIMESPACE_SHOUTMESSAGE_CANT_BE_IN_GROUP), MsgMessageType.Middle); + return; + // Group Time-Space and player is not in group + case false when !session.PlayerEntity.IsInGroup(): + session.SendMsg(session.GetLanguage(GameDialogKey.TIMESPACE_SHOUTMESSAGE_MUST_BE_IN_GROUP), MsgMessageType.Middle); + return; + } + + if (session.PlayerEntity.Id != npcTimeSpacePortal.TimeSpaceOwnerId.Value) + { + if (!session.PlayerEntity.IsInGroup()) + { + return; + } + + bool timeSpaceOwnerInGroup = session.PlayerEntity.GetGroup().Members.Any(member => member.Id == npcTimeSpacePortal.TimeSpaceOwnerId.Value); + if (!timeSpaceOwnerInGroup) + { + return; + } + } + + switch (clientPacket.Value) + { + case 0: + TimeSpaceRecordResponse record = await _timeSpaceService.GetTimeSpaceRecord(new TimeSpaceRecordRequest + { + TimeSpaceId = npcTimeSpacePortal.TimeSpaceInfo.TsId + }); + + session.SendTimeSpaceInfo(npcTimeSpacePortal, record?.TimeSpaceRecordDto); + break; + case 1: + if (session.PlayerEntity.Id != npcTimeSpacePortal.TimeSpaceOwnerId.Value) + { + session.SendMsg(session.GetLanguage(GameDialogKey.TIMESPACE_SHOUTMESSAGE_NOT_OWNER), MsgMessageType.Middle); + return; + } + + if (clientPacket.Param is 1) + { + session.SendQnaPacket("wreq 1 0", + session.GetLanguageFormat(GameDialogKey.TIMESPACE_DIALOG_ASK_START_RECORD, npcTimeSpacePortal.TimeSpaceInfo.MinLevel * 50)); + return; + } + + await session.EmitEventAsync(new TimeSpaceTryStartHiddenEvent + { + TimeSpacePortal = npcTimeSpacePortal, + IsChallengeMode = clientPacket.Param is 0 + }); + break; + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Useless/CClosePacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Useless/CClosePacketHandler.cs new file mode 100644 index 0000000..920476a --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Useless/CClosePacketHandler.cs @@ -0,0 +1,18 @@ +using System.Threading.Tasks; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.Game.Useless; + +public class CClosePacketHandler : GenericGamePacketHandlerBase +{ + protected override async Task HandlePacketAsync(IClientSession session, CClosePacket packet) + { + if (!session.HasSelectedCharacter) + { + return; + } + + session.PlayerEntity.IsBankOpen = false; + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Useless/FStashEndPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Useless/FStashEndPacketHandler.cs new file mode 100644 index 0000000..45a0b37 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Useless/FStashEndPacketHandler.cs @@ -0,0 +1,14 @@ +using System.Threading.Tasks; +using WingsEmu.Game.Families.Event; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.Game.Useless; + +public class FStashEndPacketHandler : GenericGamePacketHandlerBase +{ + protected override async Task HandlePacketAsync(IClientSession session, FStashEndPacket packet) + { + await session.EmitEventAsync(new FamilyWarehouseCloseEvent()); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Useless/LbsPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Useless/LbsPacketHandler.cs new file mode 100644 index 0000000..0aa149a --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Useless/LbsPacketHandler.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.Game.Useless; + +public class LbsPacketHandler : GenericGamePacketHandlerBase +{ + protected override async Task HandlePacketAsync(IClientSession session, LbsPacket packet) + { + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Useless/PdtClosePacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Useless/PdtClosePacketHandler.cs new file mode 100644 index 0000000..284a359 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Useless/PdtClosePacketHandler.cs @@ -0,0 +1,14 @@ +using System.Threading.Tasks; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.Game.Useless; + +public class PdtClosePacketHandler : GenericGamePacketHandlerBase +{ + protected override async Task HandlePacketAsync(IClientSession session, PdtClosePacket packet) + { + session.PlayerEntity.IsCraftingItem = false; + session.PlayerEntity.LastMinilandProducedItem = null; + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Useless/ScpCtsPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Useless/ScpCtsPacketHandler.cs new file mode 100644 index 0000000..7b7b86a --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Useless/ScpCtsPacketHandler.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.Game.Useless; + +public class ScpCtsPacketHandler : GenericGamePacketHandlerBase +{ + protected override async Task HandlePacketAsync(IClientSession session, ScpCtsPacket packet) + { + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Useless/ShopClosePacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Useless/ShopClosePacketHandler.cs new file mode 100644 index 0000000..c126188 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Useless/ShopClosePacketHandler.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.Game.Useless; + +public class ShopClosePacketHandler : GenericGamePacketHandlerBase +{ + protected override async Task HandlePacketAsync(IClientSession session, ShopClosePacket packet) + { + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Useless/SnapPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Useless/SnapPacketHandler.cs new file mode 100644 index 0000000..4c64398 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Useless/SnapPacketHandler.cs @@ -0,0 +1,13 @@ +using System.Threading.Tasks; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.Game.Useless; + +public class SnapPacketHandler : GenericGamePacketHandlerBase +{ + protected override async Task HandlePacketAsync(IClientSession session, SnapPacket packet) + { + //we can log when people are taking screenshots ingame so that we can determine if they are legit (at least in some scale) or fake + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Useless/StashEndPacketHandler.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Useless/StashEndPacketHandler.cs new file mode 100644 index 0000000..23d896f --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/Game/Useless/StashEndPacketHandler.cs @@ -0,0 +1,14 @@ +using System.Threading.Tasks; +using WingsEmu.Game.Networking; +using WingsEmu.Packets.ClientPackets; + +namespace WingsEmu.Plugins.PacketHandling.Game.Useless; + +public class StashEndPacketHandler : GenericGamePacketHandlerBase +{ + protected override async Task HandlePacketAsync(IClientSession session, StashEndPacket packet) + { + session.PlayerEntity.IsWarehouseOpen = false; + session.PlayerEntity.IsPartnerWarehouseOpen = false; + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/GamePacketHandlersCorePlugin.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/GamePacketHandlersCorePlugin.cs new file mode 100644 index 0000000..f1937b6 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/GamePacketHandlersCorePlugin.cs @@ -0,0 +1,30 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using PhoenixLib.Extensions; +using WingsAPI.Packets; +using WingsAPI.Packets.Handling; +using WingsAPI.Plugins; +using WingsEmu.Game._packetHandling; +using WingsEmu.Game.Battle; +using WingsEmu.Packets; + +namespace WingsEmu.Plugins.PacketHandling; + +public class GamePacketHandlersCorePlugin : IGameServerPlugin +{ + public string Name => nameof(GamePacketHandlersCorePlugin); + + public void AddDependencies(IServiceCollection services, GameServerLoader gameServer) + { + services.AddSingleton(typeof(IPacketHandlerContainer<>), typeof(GenericPacketHandlerContainer<>)); + services.AddClientPacketsInAssembly(); + + + Type[] types = typeof(GamePacketHandlersCorePlugin).Assembly.GetTypesImplementingGenericClass(typeof(GenericGamePacketHandlerBase<>)); + services.AddGamePacketHandlersInAssembly(types); + + + services.AddTransient(); + services.AddSingleton(); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/GamePacketHandlersGamePlugin.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/GamePacketHandlersGamePlugin.cs new file mode 100644 index 0000000..7202210 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/GamePacketHandlersGamePlugin.cs @@ -0,0 +1,50 @@ +using System; +using System.Reflection; +using Microsoft.Extensions.DependencyInjection; +using PhoenixLib.Logging; +using WingsAPI.Packets.Handling; +using WingsAPI.Plugins; +using WingsEmu.Game._packetHandling; +using WingsEmu.Packets; + +namespace WingsEmu.Plugins.PacketHandling; + +public class GamePacketHandlersGamePlugin : IGamePlugin +{ + private readonly IServiceProvider _container; + private readonly IPacketHandlerContainer _handlers; + + public GamePacketHandlersGamePlugin(IPacketHandlerContainer handlers, IServiceProvider container) + { + _handlers = handlers; + _container = container; + } + + public string Name => nameof(GamePacketHandlersGamePlugin); + + public void OnLoad() + { + foreach (RegisteredPacketHandler registeredPacketHandler in _container.GetServices()) + { + try + { + Type handlerType = registeredPacketHandler.HandlerType; + object tmp = _container.GetService(handlerType); + if (!(tmp is IGamePacketHandler handler)) + { + continue; + } + + Type type = handlerType.BaseType.GenericTypeArguments[0]; + + _handlers.Register(type, handler); + Log.Info($"[GAME_HANDLERS][ADD_HANDLER] {type.GetCustomAttribute().Identification}"); + } + catch (Exception e) + { + Log.Error("[GAME_HANDLERS][FAIL_ADD]", e); + // ignored + } + } + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/GenericCharScreenPacketHandlerBase.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/GenericCharScreenPacketHandlerBase.cs new file mode 100644 index 0000000..6faeb99 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/GenericCharScreenPacketHandlerBase.cs @@ -0,0 +1,28 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System.Threading.Tasks; +using WingsEmu.Game._packetHandling; +using WingsEmu.Game.Networking; +using WingsEmu.Packets; + +namespace WingsEmu.Plugins.PacketHandling; + +public abstract class GenericCharScreenPacketHandlerBase : ICharacterScreenPacketHandler where T : IPacket +{ + public async Task HandleAsync(IClientSession session, IPacket packet) + { + if (packet is T typedPacket && !session.HasSelectedCharacter) + { + await HandlePacketAsync(session, typedPacket); + } + } + + public void Handle(IClientSession session, IPacket packet) + { + HandleAsync(session, packet).ConfigureAwait(false).GetAwaiter().GetResult(); + } + + protected abstract Task HandlePacketAsync(IClientSession session, T packet); +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/GenericGamePacketHandlerBase.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/GenericGamePacketHandlerBase.cs new file mode 100644 index 0000000..27c1c94 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/GenericGamePacketHandlerBase.cs @@ -0,0 +1,24 @@ +using System.Threading.Tasks; +using WingsEmu.Game._packetHandling; +using WingsEmu.Game.Networking; +using WingsEmu.Packets; + +namespace WingsEmu.Plugins.PacketHandling; + +public abstract class GenericGamePacketHandlerBase : IGamePacketHandler where T : IPacket +{ + public async Task HandleAsync(IClientSession session, IPacket packet) + { + if (packet is T typedPacket && session.IsAuthenticated) + { + await HandlePacketAsync(session, typedPacket); + } + } + + public void Handle(IClientSession session, IPacket packet) + { + HandleAsync(session, packet).ConfigureAwait(false).GetAwaiter().GetResult(); + } + + protected abstract Task HandlePacketAsync(IClientSession session, T packet); +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/GenericPacketHandlerContainer.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/GenericPacketHandlerContainer.cs new file mode 100644 index 0000000..f35e64b --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/GenericPacketHandlerContainer.cs @@ -0,0 +1,47 @@ +// WingsEmu +// +// Developed by NosWings Team + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using PhoenixLib.Logging; +using WingsEmu.Game._packetHandling; +using WingsEmu.Game.Networking; +using WingsEmu.Packets; + +namespace WingsEmu.Plugins.PacketHandling; + +public class GenericPacketHandlerContainer : IPacketHandlerContainer where T : IPacketHandler +{ + private readonly Dictionary _handlers = new(); + + public void Register(Type packetType, T handler) + { + _handlers.Add(packetType, handler); + } + + public void Unregister(Type packetType) + { + _handlers.Remove(packetType); + } + + public void Execute(IClientSession session, IClientPacket packet, Type packetType) + { + ExecuteAsync(session, packet, packetType).ConfigureAwait(false).GetAwaiter().GetResult(); + } + + public async Task ExecuteAsync(IClientSession session, IClientPacket packet, Type packetType) + { + if (!_handlers.TryGetValue(packetType, out T handler)) + { + Log.Info($"[PACKET_HANDLER] {packetType.Name} NO_HANDLER"); + return; + } + + Log.Info(session.HasSelectedCharacter == false + ? $"[PACKET_HANDLER] [Id: {session.Account?.Id} - {(session.Account == null ? "None" : "account: " + session.Account?.Name)}] Handling {packet.OriginalHeader}" + : $"[PACKET_HANDLER] [Id: {session.Account?.Id} - {session.PlayerEntity?.Name}] Handling {packet.OriginalHeader}"); + await handler.HandleAsync(session, packet); + } +} \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/WingsEmu.Plugins.PacketHandling.csproj b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/WingsEmu.Plugins.PacketHandling.csproj new file mode 100644 index 0000000..f69a3d7 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/WingsEmu.Plugins.PacketHandling.csproj @@ -0,0 +1,18 @@ + + + + net5.0 + latest + + + + + + + + + + + + +