ddon-server/Arrowgene.Ddon.GameServer/Handler/InstanceEnemyKillHandler.cs

298 lines
15 KiB
C#

using Arrowgene.Ddon.GameServer.Characters;
using Arrowgene.Ddon.GameServer.GatheringItems.Generators;
using Arrowgene.Ddon.GameServer.Party;
using Arrowgene.Ddon.GameServer.Quests;
using Arrowgene.Ddon.GameServer.Scripting.Interfaces;
using Arrowgene.Ddon.Server;
using Arrowgene.Ddon.Server.Network;
using Arrowgene.Ddon.Shared.Entity.PacketStructure;
using Arrowgene.Ddon.Shared.Entity.Structure;
using Arrowgene.Ddon.Shared.Model;
using Arrowgene.Ddon.Shared.Model.Quest;
using Arrowgene.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Arrowgene.Ddon.GameServer.Handler
{
public class InstanceEnemyKillHandler : GameRequestPacketHandler<C2SInstanceEnemyKillReq, S2CInstanceEnemyKillRes>
{
private static readonly ServerLogger Logger = LogProvider.Logger<ServerLogger>(typeof(InstanceEnemyKillHandler));
private readonly DdonGameServer _gameServer;
private readonly HashSet<uint> _ignoreKillsInStageIds = new HashSet<uint>()
{
Stage.TrainingRoom.StageId,
};
public InstanceEnemyKillHandler(DdonGameServer server) : base(server)
{
_gameServer = server;
}
public override S2CInstanceEnemyKillRes Handle(GameClient client, C2SInstanceEnemyKillReq packet)
{
CDataStageLayoutId layoutId = packet.LayoutId;
StageLayoutId stageId = layoutId.AsStageLayoutId();
PacketQueue queuedPackets = new();
// The training room uses special handling to produce enemies that don't exist in the QuestState or InstanceEnemyManager.
// Return an empty response here to not break the rest of the handling.
if (_ignoreKillsInStageIds.Contains(stageId.Id))
{
return new();
}
InstancedEnemy enemyKilled = client.Party.InstanceEnemyManager.GetInstanceEnemy(stageId, (byte)packet.SetId);
if (enemyKilled is null)
{
Logger.Error(client, $"Enemy killed data missing; {layoutId}.{packet.SetId}");
throw new ResponseErrorException(ErrorCode.ERROR_CODE_INSTANCE_AREA_ENEMY_UNIT_DATA_NONE);
}
Quest quest = null;
bool isQuestControlled = false;
if (enemyKilled.QuestScheduleId != 0)
{
quest = QuestManager.GetQuestByScheduleId(enemyKilled.QuestScheduleId);
isQuestControlled = (quest != null);
}
if (enemyKilled.RepopCount > 0 && enemyKilled.RepopNum < enemyKilled.RepopCount)
{
enemyKilled.RepopNum += 1;
S2CInstanceEnemyRepopNtc repopNtc = new S2CInstanceEnemyRepopNtc()
{
LayoutId = layoutId,
WaitSecond = enemyKilled.RepopWaitSecond,
EnemyData = new CDataLayoutEnemyData()
{
PositionIndex = (byte)packet.SetId,
EnemyInfo = enemyKilled.AsCDataStageLayoutEnemyPresetEnemyInfoClient()
}
};
client.Party.EnqueueToAll(repopNtc, queuedPackets);
}
else
{
enemyKilled.IsKilled = true;
}
bool isEpitaphEnemy = false;
if (_gameServer.EpitaphRoadManager.TrialInProgress(client.Party))
{
isEpitaphEnemy = _gameServer.EpitaphRoadManager.TrialHasEnemies(client.Party, stageId, 0);
_gameServer.EpitaphRoadManager.EvaluateEnemyKilled(client.Party, stageId, packet.SetId, enemyKilled);
}
Server.Database.ExecuteInTransaction(connectionIn =>
{
List<InstancedEnemy> group = client.Party.InstanceEnemyManager.GetInstancedEnemies(stageId);
bool groupDestroyed;
if (isQuestControlled)
{
groupDestroyed = group.Where(x => x.IsRequired && x.QuestScheduleId == quest.QuestScheduleId).All(x => x.IsKilled);
}
else
{
groupDestroyed = group.Where(x => x.IsRequired).All(x => x.IsKilled);
}
if (groupDestroyed)
{
bool isAreaBoss = group.Any(x => x.IsAreaBoss);
bool isDungeon = StageManager.IsDungeon(stageId);
bool isCautionSpot = Server.ScriptManager.MonsterCautionSpotModule.IsEnabledCautionSpotGroup(Server, client.Party, stageId, client.Character.AreaId);
if (isQuestControlled)
{
var ntcs = QuestManager.GetQuestStateManager(client, quest).HandleDestroyGroupWorkNotice(client.Party, quest, stageId, enemyKilled, connectionIn);
queuedPackets.AddRange(ntcs);
}
// This is used for quests and things like key door monsters
S2CInstanceEnemyGroupDestroyNtc groupDestroyedNtc = new S2CInstanceEnemyGroupDestroyNtc()
{
LayoutId = packet.LayoutId,
IsAreaBoss = isCautionSpot && (client.GameMode == GameMode.Normal)
};
client.Party.EnqueueToAll(groupDestroyedNtc, queuedPackets);
if (isAreaBoss && client.GameMode == GameMode.BitterblackMaze)
{
foreach (var memberClient in client.Party.Clients)
{
var ntcs = BitterblackMazeManager.HandleTierClear(_gameServer, memberClient, memberClient.Character, stageId, connectionIn);
queuedPackets.AddRange(ntcs);
}
}
else if (isAreaBoss && isDungeon && client.GameMode == GameMode.Normal)
{
var boss = group.Where(x => x.IsAreaBoss).First();
client.Party.EnqueueToAll(new S2CInstanceEnemyStageBossAnnihilateNtc()
{
LayoutId = boss.StageLayoutId.ToCDataStageLayoutId(),
}, queuedPackets);
}
if (isAreaBoss && client.GameMode == GameMode.Normal)
{
foreach (var memberClient in client.Party.Clients)
{
queuedPackets.AddRange(Server.AreaRankManager.AddAreaPoint(memberClient, client.Character.AreaId, (Server.GameSettings.GameServerSettings.AreaBossApReward, 0), connectionIn));
}
}
}
if (!packet.IsNoBattleReward && !client.QuestState.IsQuestActive(QuestId.ResolutionsAndOmens))
{
foreach (var partyMemberClient in client.Party.Clients)
{
var instancedGatheringItems = partyMemberClient.InstanceDropItemManager.Generate(enemyKilled);
uint offsetSetId = partyMemberClient.InstanceDropItemManager.Assign(layoutId, packet.SetId, instancedGatheringItems.Values.SelectMany(x => x).ToList());
var dropItemNtc = new S2CInstancePopDropItemNtc()
{
LayoutId = packet.LayoutId,
SetId = offsetSetId,
MdlType = enemyKilled.DropsTable?.MdlType ?? 0,
PosX = packet.DropPosX,
PosY = packet.DropPosY,
PosZ = packet.DropPosZ
};
if (instancedGatheringItems[typeof(EnemyEpitaphRoadDropGenerator)].Any())
{
dropItemNtc.MdlType = 1; // Make the bag appear as golden
}
// If the roll was unlucky, there is a chance that no bag will show.
if (instancedGatheringItems.Any(x => x.Value.Any()))
{
partyMemberClient.Enqueue(dropItemNtc, queuedPackets);
}
}
// TODO: This will be revisited so we can properly handle EXP assigned by tool and
// TODO: EXP determined by the mixin. For now, the default behavior of the mixin
// TODO: is the same as the original server behavior.
var enemyExpMixin = Server.ScriptManager.MixinModule.Get<IExpMixin>("enemy_exp");
foreach (PartyMember member in client.Party.Members)
{
if (member.JoinState != JoinState.On) continue; // Only fully joined members get rewards.
GameClient memberClient;
CharacterCommon memberCharacter;
if (member is PlayerPartyMember playerMember)
{
var gainedExp = _gameServer.ExpManager.GetAdjustedPoints(client, RewardSource.Enemy, client.Character, client.Party, PointType.ExperiencePoints, enemyExpMixin.GetExpValue(playerMember.Client.Character, enemyKilled), enemyKilled);
var gainedPP = _gameServer.ExpManager.GetAdjustedPoints(client, RewardSource.Enemy, client.Character, client.Party, PointType.PlayPoints, enemyKilled.GetDroppedPlayPoints(), enemyKilled);
memberClient = playerMember.Client;
memberCharacter = memberClient.Character;
if (memberCharacter.Stage.Id != stageId.Id) continue; // Only nearby allies get XP.
if (memberClient.Character.ActiveCharacterPlayPointData.PlayPoint.ExpMode == ExpMode.Experience && !isQuestControlled && !isEpitaphEnemy)
{
gainedPP = (0, 0);
}
else if (!isQuestControlled && !isEpitaphEnemy)
{
gainedExp = (0, 0);
}
var huntPackets = playerMember.QuestState.HandleEnemyHuntRequests(enemyKilled, connectionIn);
queuedPackets.AddRange(huntPackets);
S2CItemUpdateCharacterItemNtc updateCharacterItemNtc = new S2CItemUpdateCharacterItemNtc();
if (enemyKilled.BloodOrbs > 0)
{
// Drop BO
uint gainedBo = (uint)(enemyKilled.BloodOrbs * _gameServer.GameSettings.GameServerSettings.BoModifier);
uint bonusBo = (uint)(gainedBo * _gameServer.GpCourseManager.EnemyBloodOrbBonus());
CDataUpdateWalletPoint boUpdateWalletPoint = _gameServer.WalletManager.AddToWallet(memberClient.Character, WalletType.BloodOrbs, gainedBo + bonusBo, bonusBo, connectionIn: connectionIn);
updateCharacterItemNtc.UpdateWalletList.Add(boUpdateWalletPoint);
}
if (enemyKilled.HighOrbs > 0)
{
// Drop HO
uint gainedHo = (uint)(enemyKilled.HighOrbs * _gameServer.GameSettings.GameServerSettings.HoModifier);
CDataUpdateWalletPoint hoUpdateWalletPoint = _gameServer.WalletManager.AddToWallet(memberClient.Character, WalletType.HighOrbs, gainedHo, connectionIn: connectionIn);
updateCharacterItemNtc.UpdateWalletList.Add(hoUpdateWalletPoint);
}
if (updateCharacterItemNtc.UpdateItemList.Count != 0 || updateCharacterItemNtc.UpdateWalletList.Count != 0)
{
memberClient.Enqueue(updateCharacterItemNtc, queuedPackets);
}
if ((gainedPP.BasePoints + gainedPP.BonusPoints) > 0)
{
var ntc = _gameServer.PPManager.AddPlayPoint(memberClient, gainedPP, type: 1, connectionIn: connectionIn);
memberClient.Enqueue(ntc, queuedPackets);
}
if ((gainedExp.BasePoints + gainedExp.BonusPoints) > 0)
{
var ntcs = _gameServer.ExpManager.AddExp(memberClient, memberCharacter, gainedExp, RewardSource.Enemy, connectionIn: connectionIn);
queuedPackets.AddRange(ntcs);
}
queuedPackets.AddRange(Server.AchievementManager.HandleKillEnemy(memberClient, enemyKilled, connectionIn: connectionIn));
queuedPackets.AddRange(Server.JobMasterManager.HandleEnemyKill(memberClient, enemyKilled, connectionIn));
}
else if (member is PawnPartyMember pawnMember)
{
Pawn pawn = pawnMember.Pawn;
memberClient = _gameServer.ClientLookup.GetClientByCharacterId(pawn.CharacterId);
memberCharacter = pawn;
if (memberClient is null || memberClient.Character.Stage.Id != stageId.Id || pawn.IsRented)
{
// Only nearby allies get XP
// and non-rented pawns
continue;
}
var pawnExp = _gameServer.ExpManager.GetAdjustedPoints(client, RewardSource.Enemy, pawn, client.Party, PointType.ExperiencePoints, enemyExpMixin.GetExpValue(memberCharacter, enemyKilled), enemyKilled);
if ((pawnExp.BasePoints + pawnExp.BonusPoints) > 0)
{
var ntcs = _gameServer.ExpManager.AddExp(memberClient, memberCharacter, pawnExp, RewardSource.Enemy, connectionIn: connectionIn);
queuedPackets.AddRange(ntcs);
}
if (pawn is RentalPawn rentalPawn)
{
Server.RentalPawnManager.HandleEnemyKill(rentalPawn, connectionIn);
}
}
else
{
throw new Exception("Unknown member type");
}
}
}
});
queuedPackets.Send();
// TODO: EnemyId and KillNum
return new S2CInstanceEnemyKillRes()
{
EnemyId = packet.IsNoBattleReward ? 0u : enemyKilled.EnemyId,
KillNum = packet.IsNoBattleReward ? 0u : 1u
};
}
}
}