World drops, group based spawning and a pile of fixes
Drops now land on the ground as gadgets and are picked up with F instead of appearing in the bag out of nowhere. Chests, ore, crystals and plants all spawn and can be interacted with. Monsters load by scene group rather than by a radius around each one, so a camp shows up as a camp and nothing pops into existence mid fight. The group anchors come out of the scene Lua, which also killed four invented tuning constants. The gateway was creating a whole new session for every handshake datagram, and the client retransmits those until it gets an answer. That is where the piles of duplicated monsters and the chests coming back from the dead were coming from. Also: monster level scales to the party now, path queries return an actual two point path instead of an empty one, NpcTalk closes properly, and the ability and combat invocations are no longer echoed back to the client that sent them.
This commit is contained in:
parent
a4b3b669a8
commit
49450270db
21 changed files with 377 additions and 162 deletions
|
|
@ -16,6 +16,7 @@ public sealed class MaterialExcel : ExcelItem
|
||||||
|
|
||||||
[JsonPropertyName("id")] public uint Id { get; set; }
|
[JsonPropertyName("id")] public uint Id { get; set; }
|
||||||
[JsonPropertyName("materialType")] public string MaterialType { get; set; } = string.Empty;
|
[JsonPropertyName("materialType")] public string MaterialType { get; set; } = string.Empty;
|
||||||
|
[JsonPropertyName("gadgetId")] public uint GadgetId { get; set; }
|
||||||
[JsonPropertyName("itemUse")] public List<MaterialItemUse> ItemUse { get; set; } = new();
|
[JsonPropertyName("itemUse")] public List<MaterialItemUse> ItemUse { get; set; } = new();
|
||||||
|
|
||||||
public override uint ExcelId => Id;
|
public override uint ExcelId => Id;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
using Yuanshen.Gameserver.Controllers.Attributes;
|
using Yuanshen.Gameserver.Controllers.Attributes;
|
||||||
using Yuanshen.Gameserver.Controllers.Result;
|
using Yuanshen.Gameserver.Controllers.Result;
|
||||||
using Yuanshen.Gameserver.Game;
|
using Yuanshen.Gameserver.Game;
|
||||||
using Yuanshen.Gameserver.Network.Session;
|
|
||||||
using Yuanshen.Protocol;
|
using Yuanshen.Protocol;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
|
@ -11,62 +10,27 @@ namespace Yuanshen.Gameserver.Controllers;
|
||||||
internal sealed class AbilityController : ControllerBase
|
internal sealed class AbilityController : ControllerBase
|
||||||
{
|
{
|
||||||
[NetCommand(CmdType.AbilityInvocationsNotify)]
|
[NetCommand(CmdType.AbilityInvocationsNotify)]
|
||||||
public async ValueTask<IResult> OnAbilityInvocationsNotify(NetSession session)
|
public ValueTask<IResult> OnAbilityInvocationsNotify() => ValueTask.FromResult(Ok());
|
||||||
{
|
|
||||||
AbilityInvocationsNotify notify = Packet!.DecodeBody<AbilityInvocationsNotify>();
|
[NetCommand(CmdType.ClientAbilityChangeNotify)]
|
||||||
await ForwardAsync(session, notify.Invokes);
|
public ValueTask<IResult> OnClientAbilityChangeNotify() => ValueTask.FromResult(Ok());
|
||||||
return Ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
[NetCommand(CmdType.ClientAbilityInitFinishNotify)]
|
[NetCommand(CmdType.ClientAbilityInitFinishNotify)]
|
||||||
public async ValueTask<IResult> OnClientAbilityInitFinishNotify(NetSession session, Player player, SceneManager scene, ILogger<AbilityController> logger)
|
public async ValueTask<IResult> OnClientAbilityInitFinishNotify(Player player, SceneManager scene)
|
||||||
{
|
{
|
||||||
ClientAbilityInitFinishNotify notify = Packet!.DecodeBody<ClientAbilityInitFinishNotify>();
|
ClientAbilityInitFinishNotify notify = Packet!.DecodeBody<ClientAbilityInitFinishNotify>();
|
||||||
|
|
||||||
foreach (AbilityInvokeEntry entry in notify.Invokes)
|
GameMonster? monster = player.Monsters.FirstOrDefault(candidate => candidate.EntityId == notify.EntityId);
|
||||||
{
|
|
||||||
logger.LogDebug("AbilityInit entity={e} arg={arg} fwd={fwd} instAbility={ia} instModifier={im} localId={l} target={t}",
|
|
||||||
notify.EntityId, entry.ArgumentType, entry.ForwardType,
|
|
||||||
entry.Head?.InstancedAbilityId, entry.Head?.InstancedModifierId, entry.Head?.LocalId, entry.Head?.TargetId);
|
|
||||||
}
|
|
||||||
|
|
||||||
await ForwardAsync(session, notify.Invokes);
|
|
||||||
|
|
||||||
GameMonster? monster = player.Monsters.FirstOrDefault(m => m.EntityId == notify.EntityId);
|
|
||||||
if (monster is not null)
|
if (monster is not null)
|
||||||
await scene.SyncMonsterLevelAsync(monster);
|
await scene.SyncMonsterLevelAsync(monster);
|
||||||
|
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
[NetCommand(CmdType.ClientAbilityChangeNotify)]
|
|
||||||
public async ValueTask<IResult> OnClientAbilityChangeNotify(NetSession session)
|
|
||||||
{
|
|
||||||
ClientAbilityChangeNotify notify = Packet!.DecodeBody<ClientAbilityChangeNotify>();
|
|
||||||
await ForwardAsync(session, notify.Invokes);
|
|
||||||
return Ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
[NetCommand(CmdType.EvtDoSkillSuccNotify)]
|
[NetCommand(CmdType.EvtDoSkillSuccNotify)]
|
||||||
public ValueTask<IResult> OnEvtDoSkillSuccNotify(ILogger<AbilityController> logger)
|
public ValueTask<IResult> OnEvtDoSkillSuccNotify(ILogger<AbilityController> logger)
|
||||||
{
|
{
|
||||||
logger.LogDebug("DoSkillSucc raw={body}", Convert.ToHexString(Packet!.Body.Span));
|
logger.LogDebug("DoSkillSucc raw={body}", Convert.ToHexString(Packet!.Body.Span));
|
||||||
return ValueTask.FromResult(Ok());
|
return ValueTask.FromResult(Ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static async ValueTask ForwardAsync(NetSession session, IEnumerable<AbilityInvokeEntry> invokes)
|
|
||||||
{
|
|
||||||
AbilityInvocationsNotify forward = new();
|
|
||||||
|
|
||||||
foreach (AbilityInvokeEntry entry in invokes)
|
|
||||||
{
|
|
||||||
if (entry.ForwardType is ForwardType.ForwardLocal)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
forward.Invokes.Add(entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (forward.Invokes.Count != 0)
|
|
||||||
await session.NotifyAsync(CmdType.AbilityInvocationsNotify, forward);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,6 @@ internal sealed class CombatController : ControllerBase
|
||||||
{
|
{
|
||||||
if (cmd.MessageId == (uint)CmdType.CombatInvocationsNotify)
|
if (cmd.MessageId == (uint)CmdType.CombatInvocationsNotify)
|
||||||
await ProcessCombatInvocationsAsync(CombatInvocationsNotify.Parser.ParseFrom(cmd.Body), state, player, combat, worldSpawner, logger, session);
|
await ProcessCombatInvocationsAsync(CombatInvocationsNotify.Parser.ParseFrom(cmd.Body), state, player, combat, worldSpawner, logger, session);
|
||||||
else if (cmd.MessageId == (uint)CmdType.AbilityInvocationsNotify)
|
|
||||||
await AbilityController.ForwardAsync(session, AbilityInvocationsNotify.Parser.ParseFrom(cmd.Body).Invokes);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Ok();
|
return Ok();
|
||||||
|
|
@ -61,7 +59,6 @@ internal sealed class CombatController : ControllerBase
|
||||||
private static async ValueTask ProcessCombatInvocationsAsync(CombatInvocationsNotify notify, GameState state, Player player, CombatManager combat, WorldSpawner worldSpawner, ILogger logger, NetSession session)
|
private static async ValueTask ProcessCombatInvocationsAsync(CombatInvocationsNotify notify, GameState state, Player player, CombatManager combat, WorldSpawner worldSpawner, ILogger logger, NetSession session)
|
||||||
{
|
{
|
||||||
uint activeEntityId = player.ActiveAvatar?.EntityId ?? 0;
|
uint activeEntityId = player.ActiveAvatar?.EntityId ?? 0;
|
||||||
CombatInvocationsNotify forward = new();
|
|
||||||
|
|
||||||
foreach (CombatInvokeEntry entry in notify.InvokeList)
|
foreach (CombatInvokeEntry entry in notify.InvokeList)
|
||||||
{
|
{
|
||||||
|
|
@ -88,12 +85,6 @@ internal sealed class CombatController : ControllerBase
|
||||||
logger.LogDebug("combat arg={arg} fwd={f}", entry.ArgumentType, entry.ForwardType);
|
logger.LogDebug("combat arg={arg} fwd={f}", entry.ArgumentType, entry.ForwardType);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entry.ForwardType is ForwardType.ForwardToAll or ForwardType.ForwardToHost or ForwardType.ForwardToPeer)
|
|
||||||
forward.InvokeList.Add(entry);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (forward.InvokeList.Count != 0)
|
|
||||||
await session.NotifyAsync(CmdType.CombatInvocationsNotify, forward);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
using Yuanshen.Common.Data.Excel;
|
|
||||||
using Yuanshen.Gameserver.Controllers.Attributes;
|
using Yuanshen.Gameserver.Controllers.Attributes;
|
||||||
using Yuanshen.Gameserver.Controllers.Result;
|
using Yuanshen.Gameserver.Controllers.Result;
|
||||||
using Yuanshen.Gameserver.Database;
|
using Yuanshen.Gameserver.Database;
|
||||||
|
|
@ -20,7 +19,7 @@ internal sealed class GadgetController : ControllerBase
|
||||||
|
|
||||||
[NetCommand(CmdType.GadgetInteractReq)]
|
[NetCommand(CmdType.GadgetInteractReq)]
|
||||||
public async ValueTask<IResult> OnGadgetInteractReq(NetSession session, Player player, SceneManager scene,
|
public async ValueTask<IResult> OnGadgetInteractReq(NetSession session, Player player, SceneManager scene,
|
||||||
DropTable drops, ExcelTableCollection excel, PlayerPersistence persistence, ILogger<GadgetController> logger)
|
DropTable drops, PlayerPersistence persistence, ILogger<GadgetController> logger)
|
||||||
{
|
{
|
||||||
GadgetInteractReq req = Packet!.DecodeBody<GadgetInteractReq>();
|
GadgetInteractReq req = Packet!.DecodeBody<GadgetInteractReq>();
|
||||||
|
|
||||||
|
|
@ -28,14 +27,32 @@ internal sealed class GadgetController : ControllerBase
|
||||||
if (gadget is null)
|
if (gadget is null)
|
||||||
return Reply(req, Retcode.RET_GADGET_NOT_EXIST);
|
return Reply(req, Retcode.RET_GADGET_NOT_EXIST);
|
||||||
|
|
||||||
if (gadget.IsChest)
|
if (gadget.IsDrop)
|
||||||
return await OpenChestAsync(session, player, scene, drops, excel, persistence, logger, gadget, req);
|
return await PickUpAsync(session, player, scene, drops, persistence, logger, gadget, req);
|
||||||
|
|
||||||
return await GatherAsync(session, player, scene, drops, excel, persistence, logger, gadget, req);
|
if (gadget.IsChest)
|
||||||
|
return await OpenChestAsync(session, player, scene, drops, persistence, logger, gadget, req);
|
||||||
|
|
||||||
|
return await GatherAsync(session, player, scene, drops, persistence, logger, gadget, req);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ValueTask<IResult> PickUpAsync(NetSession session, Player player, SceneManager scene,
|
||||||
|
DropTable drops, PlayerPersistence persistence, ILogger logger, GameGadget gadget, GadgetInteractReq req)
|
||||||
|
{
|
||||||
|
await session.NotifyAsync(CmdType.GadgetInteractRsp, Build(req, Retcode.RET_SUCC, InteractType.InteractPickItem));
|
||||||
|
await scene.DespawnGadgetAsync(gadget, VisionType.VisionMiss);
|
||||||
|
|
||||||
|
Dictionary<uint, uint> picked = new() { [gadget.DropItemId] = gadget.DropCount };
|
||||||
|
await GrantAsync(session, player, drops, gadget, picked, (uint)ActionReasonType.ActionReasonTrifle);
|
||||||
|
|
||||||
|
await persistence.SaveAsync();
|
||||||
|
|
||||||
|
logger.LogInformation("Picked up {count}x {itemId}", gadget.DropCount, gadget.DropItemId);
|
||||||
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async ValueTask<IResult> OpenChestAsync(NetSession session, Player player, SceneManager scene,
|
private async ValueTask<IResult> OpenChestAsync(NetSession session, Player player, SceneManager scene,
|
||||||
DropTable drops, ExcelTableCollection excel, PlayerPersistence persistence, ILogger logger,
|
DropTable drops, PlayerPersistence persistence, ILogger logger,
|
||||||
GameGadget gadget, GadgetInteractReq req)
|
GameGadget gadget, GadgetInteractReq req)
|
||||||
{
|
{
|
||||||
if (req.OpType == InterOpType.InterOpStart)
|
if (req.OpType == InterOpType.InterOpStart)
|
||||||
|
|
@ -53,7 +70,7 @@ internal sealed class GadgetController : ControllerBase
|
||||||
await scene.SetGadgetStateAsync(gadget, ChestOpenedState, false);
|
await scene.SetGadgetStateAsync(gadget, ChestOpenedState, false);
|
||||||
|
|
||||||
Dictionary<uint, uint> rolled = drops.Roll(gadget.Node.DropId, Random);
|
Dictionary<uint, uint> rolled = drops.Roll(gadget.Node.DropId, Random);
|
||||||
await GrantAsync(session, player, drops, excel, gadget, rolled, (uint)ActionReasonType.ActionReasonOpenChest);
|
await GrantAsync(session, player, drops, gadget, rolled, (uint)ActionReasonType.ActionReasonOpenChest);
|
||||||
|
|
||||||
await persistence.SaveAsync();
|
await persistence.SaveAsync();
|
||||||
|
|
||||||
|
|
@ -64,7 +81,7 @@ internal sealed class GadgetController : ControllerBase
|
||||||
}
|
}
|
||||||
|
|
||||||
private async ValueTask<IResult> GatherAsync(NetSession session, Player player, SceneManager scene,
|
private async ValueTask<IResult> GatherAsync(NetSession session, Player player, SceneManager scene,
|
||||||
DropTable drops, ExcelTableCollection excel, PlayerPersistence persistence, ILogger logger,
|
DropTable drops, PlayerPersistence persistence, ILogger logger,
|
||||||
GameGadget gadget, GadgetInteractReq req)
|
GameGadget gadget, GadgetInteractReq req)
|
||||||
{
|
{
|
||||||
if (req.OpType != InterOpType.InterOpStart)
|
if (req.OpType != InterOpType.InterOpStart)
|
||||||
|
|
@ -87,7 +104,7 @@ internal sealed class GadgetController : ControllerBase
|
||||||
if (rolled.Count == 0 && gadget.Node.GatherItemId != 0)
|
if (rolled.Count == 0 && gadget.Node.GatherItemId != 0)
|
||||||
rolled[gadget.Node.GatherItemId] = 1;
|
rolled[gadget.Node.GatherItemId] = 1;
|
||||||
|
|
||||||
await GrantAsync(session, player, drops, excel, gadget, rolled, (uint)ActionReasonType.ActionReasonGather);
|
await GrantAsync(session, player, drops, gadget, rolled, (uint)ActionReasonType.ActionReasonGather);
|
||||||
await scene.DespawnGadgetAsync(gadget, VisionType.VisionGatherEscape);
|
await scene.DespawnGadgetAsync(gadget, VisionType.VisionGatherEscape);
|
||||||
await persistence.SaveAsync();
|
await persistence.SaveAsync();
|
||||||
|
|
||||||
|
|
@ -98,7 +115,7 @@ internal sealed class GadgetController : ControllerBase
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async ValueTask GrantAsync(NetSession session, Player player, DropTable drops,
|
private static async ValueTask GrantAsync(NetSession session, Player player, DropTable drops,
|
||||||
ExcelTableCollection excel, GameGadget gadget, Dictionary<uint, uint> rolled, uint reason)
|
GameGadget gadget, Dictionary<uint, uint> rolled, uint reason)
|
||||||
{
|
{
|
||||||
if (rolled.Count == 0)
|
if (rolled.Count == 0)
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,9 @@ namespace Yuanshen.Gameserver.Controllers;
|
||||||
[NetController]
|
[NetController]
|
||||||
internal sealed class StubController : ControllerBase
|
internal sealed class StubController : ControllerBase
|
||||||
{
|
{
|
||||||
|
private const int PathComplete = 1;
|
||||||
|
private const int PathFailed = 3;
|
||||||
|
|
||||||
[NetCommand(CmdType.ClientFpsStatusNotify)]
|
[NetCommand(CmdType.ClientFpsStatusNotify)]
|
||||||
public ValueTask<IResult> OnClientFpsStatusNotify() => ValueTask.FromResult(Ok());
|
public ValueTask<IResult> OnClientFpsStatusNotify() => ValueTask.FromResult(Ok());
|
||||||
|
|
||||||
|
|
@ -82,11 +85,50 @@ internal sealed class StubController : ControllerBase
|
||||||
{
|
{
|
||||||
QueryPathReq req = Packet!.DecodeBody<QueryPathReq>();
|
QueryPathReq req = Packet!.DecodeBody<QueryPathReq>();
|
||||||
|
|
||||||
return ValueTask.FromResult(Response(CmdType.QueryPathRsp, new QueryPathRsp
|
QueryPathRsp rsp = new()
|
||||||
{
|
{
|
||||||
Retcode = Retcode.RET_SUCC,
|
Retcode = Retcode.RET_SUCC,
|
||||||
QueryId = req.QueryId,
|
QueryId = req.QueryId,
|
||||||
QueryStatus = 1
|
QueryStatus = PathFailed
|
||||||
|
};
|
||||||
|
|
||||||
|
Vector? destination = req.DestinationPos.LastOrDefault();
|
||||||
|
|
||||||
|
if (req.SourcePos is not null && destination is not null)
|
||||||
|
{
|
||||||
|
rsp.QueryStatus = PathComplete;
|
||||||
|
rsp.Corners.Add(req.SourcePos);
|
||||||
|
rsp.Corners.Add(destination);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ValueTask.FromResult(Response(CmdType.QueryPathRsp, rsp));
|
||||||
|
}
|
||||||
|
|
||||||
|
[NetCommand(CmdType.NpcTalkReq)]
|
||||||
|
public ValueTask<IResult> OnNpcTalkReq()
|
||||||
|
{
|
||||||
|
NpcTalkReq req = Packet!.DecodeBody<NpcTalkReq>();
|
||||||
|
|
||||||
|
return ValueTask.FromResult(Response(CmdType.NpcTalkRsp, new NpcTalkRsp
|
||||||
|
{
|
||||||
|
Retcode = Retcode.RET_SUCC,
|
||||||
|
NpcEntityId = req.NpcEntityId,
|
||||||
|
CurTalkId = req.TalkId
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[NetCommand(CmdType.ClientLockGameTimeNotify)]
|
||||||
|
public ValueTask<IResult> OnClientLockGameTimeNotify() => ValueTask.FromResult(Ok());
|
||||||
|
|
||||||
|
[NetCommand(CmdType.EnterTransPointRegionNotify)]
|
||||||
|
public ValueTask<IResult> OnEnterTransPointRegionNotify() => ValueTask.FromResult(Ok());
|
||||||
|
|
||||||
|
[NetCommand(CmdType.ExitTransPointRegionNotify)]
|
||||||
|
public ValueTask<IResult> OnExitTransPointRegionNotify() => ValueTask.FromResult(Ok());
|
||||||
|
|
||||||
|
[NetCommand(CmdType.EntityAiSyncNotify)]
|
||||||
|
public ValueTask<IResult> OnEntityAiSyncNotify() => ValueTask.FromResult(Ok());
|
||||||
|
|
||||||
|
[NetCommand(CmdType.ObstacleModifyNotify)]
|
||||||
|
public ValueTask<IResult> OnObstacleModifyNotify() => ValueTask.FromResult(Ok());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Yuanshen.Common.Data.Excel;
|
||||||
using Yuanshen.Gameserver.Database;
|
using Yuanshen.Gameserver.Database;
|
||||||
using Yuanshen.Gameserver.Network.Session;
|
using Yuanshen.Gameserver.Network.Session;
|
||||||
using Yuanshen.Protocol;
|
using Yuanshen.Protocol;
|
||||||
|
|
@ -18,15 +19,19 @@ internal sealed class CombatManager
|
||||||
private readonly Player _player;
|
private readonly Player _player;
|
||||||
private readonly PlayerPersistence _persistence;
|
private readonly PlayerPersistence _persistence;
|
||||||
private readonly DropTable _drops;
|
private readonly DropTable _drops;
|
||||||
|
private readonly SceneManager _scene;
|
||||||
|
private readonly ExcelTableCollection _excel;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
public CombatManager(NetSession session, Player player, PlayerPersistence persistence, DropTable drops,
|
public CombatManager(NetSession session, Player player, PlayerPersistence persistence, DropTable drops,
|
||||||
ILogger<CombatManager> logger)
|
SceneManager scene, ExcelTableCollection excel, ILogger<CombatManager> logger)
|
||||||
{
|
{
|
||||||
_session = session;
|
_session = session;
|
||||||
_player = player;
|
_player = player;
|
||||||
_persistence = persistence;
|
_persistence = persistence;
|
||||||
_drops = drops;
|
_drops = drops;
|
||||||
|
_scene = scene;
|
||||||
|
_excel = excel;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -89,6 +94,46 @@ internal sealed class CombatManager
|
||||||
_player.Monsters.Remove(monster);
|
_player.Monsters.Remove(monster);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Vector Scatter(Vector origin, int index)
|
||||||
|
{
|
||||||
|
double angle = index * 2.39996;
|
||||||
|
double radius = 0.6 + index * 0.35;
|
||||||
|
|
||||||
|
return new Vector
|
||||||
|
{
|
||||||
|
X = origin.X + (float)(Math.Cos(angle) * radius),
|
||||||
|
Y = origin.Y + 0.4f,
|
||||||
|
Z = origin.Z + (float)(Math.Sin(angle) * radius)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ValueTask GrantDirectAsync(uint itemId, uint count)
|
||||||
|
{
|
||||||
|
if (itemId == MoraItemId)
|
||||||
|
{
|
||||||
|
_player.Scoin += count;
|
||||||
|
}
|
||||||
|
else if (itemId == PrimogemItemId)
|
||||||
|
{
|
||||||
|
_player.Hcoin += count;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_player.GiveMaterial(itemId, count);
|
||||||
|
StoreItemChangeNotify store = new() { StoreType = StoreType.StorePack };
|
||||||
|
store.ItemList.Add(new Item
|
||||||
|
{
|
||||||
|
ItemId = itemId,
|
||||||
|
Guid = _player.MaterialGuid(itemId),
|
||||||
|
Material = new Material { Count = _player.Materials.GetValueOrDefault(itemId) }
|
||||||
|
});
|
||||||
|
await _session.NotifyAsync(CmdType.StoreItemChangeNotify, store);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _session.NotifyAsync(CmdType.PlayerPropNotify, _player.BuildCurrencyProps());
|
||||||
|
}
|
||||||
|
|
||||||
private async ValueTask GrantDropsAsync(GameMonster monster)
|
private async ValueTask GrantDropsAsync(GameMonster monster)
|
||||||
{
|
{
|
||||||
uint dropId = _drops.DropIdFor(monster.MonsterId, monster.Level);
|
uint dropId = _drops.DropIdFor(monster.MonsterId, monster.Level);
|
||||||
|
|
@ -99,74 +144,27 @@ internal sealed class CombatManager
|
||||||
if (rolled.Count == 0)
|
if (rolled.Count == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
StoreItemChangeNotify store = new() { StoreType = StoreType.StorePack };
|
|
||||||
ItemAddHintNotify hint = new()
|
|
||||||
{
|
|
||||||
Reason = monster.IsWildlife ? (uint)ActionReasonType.ActionReasonMonsterEnvAnimal : (uint)ActionReasonType.ActionReasonMonsterDie,
|
|
||||||
Position = monster.Pos,
|
|
||||||
IsPositionValid = true
|
|
||||||
};
|
|
||||||
|
|
||||||
List<string> granted = new();
|
List<string> granted = new();
|
||||||
bool currencyChanged = false;
|
int scattered = 0;
|
||||||
|
|
||||||
foreach ((uint itemId, uint count) in rolled)
|
foreach ((uint itemId, uint count) in rolled)
|
||||||
{
|
{
|
||||||
switch (_drops.TypeOf(itemId))
|
uint gadgetId = _excel.GetTable(ExcelType.Material).Items.OfType<MaterialExcel>()
|
||||||
|
.FirstOrDefault(candidate => candidate.Id == itemId)?.GadgetId ?? 0;
|
||||||
|
|
||||||
|
if (gadgetId == 0)
|
||||||
{
|
{
|
||||||
case "weapon":
|
await GrantDirectAsync(itemId, count);
|
||||||
for (uint made = 0; made < Math.Min(count, 10u); made++)
|
}
|
||||||
store.ItemList.Add(_player.CreateWeapon(itemId).AsItem());
|
else
|
||||||
break;
|
{
|
||||||
|
GameGadget drop = _player.SpawnDrop(gadgetId, itemId, count, Scatter(monster.Pos, scattered++));
|
||||||
case "virtual" when itemId == MoraItemId:
|
await _scene.AppearGadgetAsync(drop);
|
||||||
_player.Scoin += count;
|
|
||||||
currencyChanged = true;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "virtual" when itemId == PrimogemItemId:
|
|
||||||
_player.Hcoin += count;
|
|
||||||
currencyChanged = true;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "virtual":
|
|
||||||
continue;
|
|
||||||
|
|
||||||
case "reliquary":
|
|
||||||
for (uint made = 0; made < Math.Min(count, 5u); made++)
|
|
||||||
{
|
|
||||||
GameReliquary? reliquary = _player.CreateReliquary(itemId, Random);
|
|
||||||
if (reliquary is not null)
|
|
||||||
store.ItemList.Add(reliquary.AsItem());
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
_player.GiveMaterial(itemId, count);
|
|
||||||
store.ItemList.Add(new Item
|
|
||||||
{
|
|
||||||
ItemId = itemId,
|
|
||||||
Guid = _player.MaterialGuid(itemId),
|
|
||||||
Material = new Material { Count = _player.Materials.GetValueOrDefault(itemId) }
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hint.ItemList.Add(new ItemHint { ItemId = itemId, Count = count, IsNew = false });
|
|
||||||
granted.Add($"{itemId}x{count}");
|
granted.Add($"{itemId}x{count}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hint.ItemList.Count == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (store.ItemList.Count != 0)
|
|
||||||
await _session.NotifyAsync(CmdType.StoreItemChangeNotify, store);
|
|
||||||
|
|
||||||
if (currencyChanged)
|
|
||||||
await _session.NotifyAsync(CmdType.PlayerPropNotify, _player.BuildCurrencyProps());
|
|
||||||
|
|
||||||
await _session.NotifyAsync(CmdType.ItemAddHintNotify, hint);
|
|
||||||
|
|
||||||
_logger.LogInformation("Monster {monsterId} lvl {level} dropped {items}",
|
_logger.LogInformation("Monster {monsterId} lvl {level} dropped {items}",
|
||||||
monster.MonsterId, monster.Level, string.Join(", ", granted));
|
monster.MonsterId, monster.Level, string.Join(", ", granted));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@ internal readonly record struct GadgetNode(
|
||||||
uint MarkFlag)
|
uint MarkFlag)
|
||||||
{
|
{
|
||||||
public bool IsChest => Kind == "chest";
|
public bool IsChest => Kind == "chest";
|
||||||
|
|
||||||
|
public bool IsDrop => Kind == "drop";
|
||||||
}
|
}
|
||||||
|
|
||||||
internal sealed class GadgetSpawnTable
|
internal sealed class GadgetSpawnTable
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,10 @@ internal sealed class GameGadget
|
||||||
public uint GadgetId => Node.GadgetId;
|
public uint GadgetId => Node.GadgetId;
|
||||||
public uint NodeId => Node.NodeId;
|
public uint NodeId => Node.NodeId;
|
||||||
public bool IsChest => Node.IsChest;
|
public bool IsChest => Node.IsChest;
|
||||||
|
public bool IsDrop => Node.IsDrop;
|
||||||
|
public uint DropItemId { get; init; }
|
||||||
|
public uint DropCount { get; init; }
|
||||||
|
public ulong DropGuid { get; init; }
|
||||||
|
|
||||||
public GameGadget(GadgetNode node, uint entityId)
|
public GameGadget(GadgetNode node, uint entityId)
|
||||||
{
|
{
|
||||||
|
|
@ -45,17 +49,25 @@ internal sealed class GameGadget
|
||||||
GroupId = Node.GroupId,
|
GroupId = Node.GroupId,
|
||||||
ConfigId = Node.ConfigId,
|
ConfigId = Node.ConfigId,
|
||||||
OwnerEntityId = 0,
|
OwnerEntityId = 0,
|
||||||
BornType = GadgetBornType.GadgetBornNone,
|
|
||||||
GadgetState = Node.State,
|
GadgetState = Node.State,
|
||||||
GadgetType = Node.GadgetType,
|
GadgetType = Node.GadgetType,
|
||||||
IsShowCutscene = false,
|
IsShowCutscene = false,
|
||||||
AuthorityPeerId = Player.PeerId,
|
AuthorityPeerId = Player.PeerId,
|
||||||
IsEnableInteract = true,
|
IsEnableInteract = true,
|
||||||
InteractId = 0,
|
InteractId = 0,
|
||||||
MarkFlag = Node.MarkFlag
|
MarkFlag = Node.MarkFlag,
|
||||||
|
BornType = GadgetBornType.GadgetBornNone
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (IsDrop)
|
||||||
|
entity.Gadget.TrifleItem = new Item
|
||||||
|
{
|
||||||
|
ItemId = DropItemId,
|
||||||
|
Guid = DropGuid,
|
||||||
|
Material = new Material { Count = DropCount }
|
||||||
|
};
|
||||||
|
|
||||||
entity.PropList.Add(new PropPair
|
entity.PropList.Add(new PropPair
|
||||||
{
|
{
|
||||||
Type = 4001,
|
Type = 4001,
|
||||||
|
|
|
||||||
|
|
@ -175,6 +175,21 @@ internal sealed class Player
|
||||||
return monster;
|
return monster;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public GameGadget SpawnDrop(uint gadgetId, uint itemId, uint count, Yuanshen.Protocol.Vector pos)
|
||||||
|
{
|
||||||
|
GadgetNode node = new(0, 0, 0, gadgetId, 1, pos, new Yuanshen.Protocol.Vector(), "drop", 0, 0, 0, 0, 0, 0);
|
||||||
|
|
||||||
|
GameGadget drop = new(node, 0x04000000 + ++_gadgetEntitySeq)
|
||||||
|
{
|
||||||
|
DropItemId = itemId,
|
||||||
|
DropCount = count,
|
||||||
|
DropGuid = NextGuid()
|
||||||
|
};
|
||||||
|
|
||||||
|
Gadgets.Add(drop);
|
||||||
|
return drop;
|
||||||
|
}
|
||||||
|
|
||||||
public GameGadget SpawnGadget(GadgetNode node)
|
public GameGadget SpawnGadget(GadgetNode node)
|
||||||
{
|
{
|
||||||
GameGadget gadget = new(node, 0x04000000 + ++_gadgetEntitySeq);
|
GameGadget gadget = new(node, 0x04000000 + ++_gadgetEntitySeq);
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ namespace Yuanshen.Gameserver.Game;
|
||||||
|
|
||||||
internal sealed class SceneManager
|
internal sealed class SceneManager
|
||||||
{
|
{
|
||||||
private const int LevelSyncAttempts = 3;
|
private const int LevelSyncAttempts = 6;
|
||||||
|
|
||||||
private readonly NetSession _session;
|
private readonly NetSession _session;
|
||||||
private readonly Player _player;
|
private readonly Player _player;
|
||||||
|
|
@ -109,6 +109,20 @@ internal sealed class SceneManager
|
||||||
SceneEntityAppearNotify notify = new() { AppearType = VisionType.VisionBorn };
|
SceneEntityAppearNotify notify = new() { AppearType = VisionType.VisionBorn };
|
||||||
notify.EntityList.Add(monster.AsSceneEntity());
|
notify.EntityList.Add(monster.AsSceneEntity());
|
||||||
await _session.NotifyAsync(CmdType.SceneEntityAppearNotify, notify);
|
await _session.NotifyAsync(CmdType.SceneEntityAppearNotify, notify);
|
||||||
|
await SyncMonsterLevelAsync(monster);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask ClearGadgetsAsync()
|
||||||
|
{
|
||||||
|
if (_player.Gadgets.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
SceneEntityDisappearNotify notify = new() { DisappearType = VisionType.VisionRemove };
|
||||||
|
foreach (GameGadget gadget in _player.Gadgets)
|
||||||
|
notify.EntityList.Add(gadget.EntityId);
|
||||||
|
|
||||||
|
_player.Gadgets.Clear();
|
||||||
|
await _session.NotifyAsync(CmdType.SceneEntityDisappearNotify, notify);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask AppearGadgetAsync(GameGadget gadget)
|
public async ValueTask AppearGadgetAsync(GameGadget gadget)
|
||||||
|
|
|
||||||
42
Yuanshen.Gameserver/Game/SpawnGroupTable.cs
Normal file
42
Yuanshen.Gameserver/Game/SpawnGroupTable.cs
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace Yuanshen.Gameserver.Game;
|
||||||
|
|
||||||
|
internal readonly record struct SpawnGroup(uint GroupId, float X, float Z, float Extent)
|
||||||
|
{
|
||||||
|
public bool IsCompact => Extent <= CompactExtent;
|
||||||
|
|
||||||
|
private const float CompactExtent = 60f;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class SpawnGroupTable
|
||||||
|
{
|
||||||
|
private readonly Dictionary<uint, SpawnGroup> _groups = new();
|
||||||
|
|
||||||
|
public SpawnGroupTable()
|
||||||
|
{
|
||||||
|
Load("scene3_groups.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGet(uint groupId, out SpawnGroup group) => _groups.TryGetValue(groupId, out group);
|
||||||
|
|
||||||
|
private void Load(string fileName)
|
||||||
|
{
|
||||||
|
string path = Path.Combine(AppContext.BaseDirectory, "assets", "scene", fileName);
|
||||||
|
if (!File.Exists(path))
|
||||||
|
return;
|
||||||
|
|
||||||
|
using JsonDocument document = JsonDocument.Parse(File.ReadAllText(path));
|
||||||
|
|
||||||
|
foreach (JsonElement row in document.RootElement.EnumerateArray())
|
||||||
|
{
|
||||||
|
uint groupId = row.GetProperty("g").GetUInt32();
|
||||||
|
|
||||||
|
_groups[groupId] = new SpawnGroup(
|
||||||
|
groupId,
|
||||||
|
(float)row.GetProperty("x").GetDouble(),
|
||||||
|
(float)row.GetProperty("z").GetDouble(),
|
||||||
|
(float)row.GetProperty("e").GetDouble());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,32 +4,41 @@ namespace Yuanshen.Gameserver.Game;
|
||||||
|
|
||||||
internal sealed class WorldSpawner
|
internal sealed class WorldSpawner
|
||||||
{
|
{
|
||||||
private const float SpawnRadiusSq = 120f * 120f;
|
private const float GroupLoadSq = 80f * 80f;
|
||||||
private const float DespawnRadiusSq = 150f * 150f;
|
private const float GroupUnloadSq = 110f * 110f;
|
||||||
|
private const float LooseLoadSq = 80f * 80f;
|
||||||
|
private const float WildlifeLoadSq = 50f * 50f;
|
||||||
|
private const float LooseUnloadSq = 110f * 110f;
|
||||||
private const float StreamStepSq = 20f * 20f;
|
private const float StreamStepSq = 20f * 20f;
|
||||||
private const int ConcurrentCap = 25;
|
private const int MonsterCap = 60;
|
||||||
private const int WildlifeCap = 6;
|
|
||||||
private const int GadgetCap = 40;
|
private const int GadgetCap = 40;
|
||||||
|
private const int LevelBand = 2;
|
||||||
|
private const int LevelSpread = 6;
|
||||||
|
private const int TopNodeLevel = 36;
|
||||||
|
private const int MaxMonsterLevel = 90;
|
||||||
|
|
||||||
private readonly Player _player;
|
private readonly Player _player;
|
||||||
private readonly SceneManager _scene;
|
private readonly SceneManager _scene;
|
||||||
private readonly MonsterSpawnTable _table;
|
private readonly MonsterSpawnTable _table;
|
||||||
private readonly GadgetSpawnTable _gadgets;
|
private readonly GadgetSpawnTable _gadgets;
|
||||||
|
private readonly SpawnGroupTable _groups;
|
||||||
|
|
||||||
private Vector? _lastStream;
|
private Vector? _lastStream;
|
||||||
|
|
||||||
public WorldSpawner(Player player, SceneManager scene, MonsterSpawnTable table, GadgetSpawnTable gadgets)
|
public WorldSpawner(Player player, SceneManager scene, MonsterSpawnTable table, GadgetSpawnTable gadgets,
|
||||||
|
SpawnGroupTable groups)
|
||||||
{
|
{
|
||||||
_player = player;
|
_player = player;
|
||||||
_scene = scene;
|
_scene = scene;
|
||||||
_table = table;
|
_table = table;
|
||||||
_gadgets = gadgets;
|
_gadgets = gadgets;
|
||||||
|
_groups = groups;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask EnterSceneAsync(Vector pos)
|
public async ValueTask EnterSceneAsync(Vector pos)
|
||||||
{
|
{
|
||||||
_player.Monsters.Clear();
|
await _scene.ClearMonstersAsync();
|
||||||
_player.Gadgets.Clear();
|
await _scene.ClearGadgetsAsync();
|
||||||
_lastStream = null;
|
_lastStream = null;
|
||||||
await StreamAsync(pos, force: true);
|
await StreamAsync(pos, force: true);
|
||||||
}
|
}
|
||||||
|
|
@ -52,42 +61,54 @@ internal sealed class WorldSpawner
|
||||||
|
|
||||||
_lastStream = pos;
|
_lastStream = pos;
|
||||||
|
|
||||||
|
await DespawnFarAsync(pos);
|
||||||
|
await SpawnNearAsync(pos);
|
||||||
|
await StreamGadgetsAsync(pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ValueTask DespawnFarAsync(Vector pos)
|
||||||
|
{
|
||||||
|
foreach (GameMonster monster in _player.Monsters.Where(monster => monster.NodeId != 0 && IsOutOfRange(pos, monster)).ToList())
|
||||||
|
await _scene.DespawnMonsterAsync(monster);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsOutOfRange(Vector pos, GameMonster monster)
|
||||||
|
{
|
||||||
|
if (_groups.TryGet(monster.GroupId, out SpawnGroup group) && group.IsCompact)
|
||||||
|
return AnchorSq(pos, group) > GroupUnloadSq;
|
||||||
|
|
||||||
|
return HorizontalSq(pos, monster.Pos) > LooseUnloadSq;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ValueTask SpawnNearAsync(Vector pos)
|
||||||
|
{
|
||||||
HashSet<uint> active = new();
|
HashSet<uint> active = new();
|
||||||
foreach (GameMonster monster in _player.Monsters)
|
foreach (GameMonster monster in _player.Monsters)
|
||||||
if (monster.NodeId != 0)
|
active.Add(monster.NodeId);
|
||||||
active.Add(monster.NodeId);
|
|
||||||
|
|
||||||
foreach (GameMonster monster in _player.Monsters
|
foreach (SpawnNode node in _table.Nodes)
|
||||||
.Where(monster => monster.NodeId != 0 && HorizontalSq(pos, monster.Pos) > DespawnRadiusSq)
|
|
||||||
.ToList())
|
|
||||||
{
|
{
|
||||||
active.Remove(monster.NodeId);
|
if (active.Count >= MonsterCap)
|
||||||
await _scene.DespawnMonsterAsync(monster);
|
|
||||||
}
|
|
||||||
|
|
||||||
int wildlife = _player.Monsters.Count(monster => monster.IsWildlife);
|
|
||||||
|
|
||||||
foreach (SpawnNode node in _table.Nodes.OrderBy(node => node.IsWildlife))
|
|
||||||
{
|
|
||||||
if (active.Count >= ConcurrentCap)
|
|
||||||
break;
|
break;
|
||||||
if (active.Contains(node.NodeId) || _player.KilledNodeIds.Contains(node.NodeId))
|
if (active.Contains(node.NodeId) || _player.KilledNodeIds.Contains(node.NodeId))
|
||||||
continue;
|
continue;
|
||||||
if (HorizontalSq(pos, node.Pos) > SpawnRadiusSq)
|
if (!ShouldLoad(pos, node))
|
||||||
continue;
|
|
||||||
if (node.IsWildlife && wildlife >= WildlifeCap)
|
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (node.IsWildlife)
|
GameMonster monster = _player.SpawnMonster(node.MonsterId, node.Pos, ScaleLevel(node.Level), node.Hp,
|
||||||
wildlife++;
|
node.Attack, node.Defense, node.NodeId, node.ConfigId, node.GroupId);
|
||||||
|
|
||||||
GameMonster monster = _player.SpawnMonster(node.MonsterId, node.Pos, node.Level, node.Hp, node.Attack, node.Defense, node.NodeId, node.ConfigId, node.GroupId);
|
|
||||||
|
|
||||||
active.Add(node.NodeId);
|
active.Add(node.NodeId);
|
||||||
await _scene.AppearMonsterAsync(monster);
|
await _scene.AppearMonsterAsync(monster);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await StreamGadgetsAsync(pos);
|
private bool ShouldLoad(Vector pos, SpawnNode node)
|
||||||
|
{
|
||||||
|
if (_groups.TryGet(node.GroupId, out SpawnGroup group) && group.IsCompact)
|
||||||
|
return AnchorSq(pos, group) <= GroupLoadSq;
|
||||||
|
|
||||||
|
return HorizontalSq(pos, node.Pos) <= (node.IsWildlife ? WildlifeLoadSq : LooseLoadSq);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async ValueTask StreamGadgetsAsync(Vector pos)
|
private async ValueTask StreamGadgetsAsync(Vector pos)
|
||||||
|
|
@ -97,7 +118,7 @@ internal sealed class WorldSpawner
|
||||||
live.Add(gadget.NodeId);
|
live.Add(gadget.NodeId);
|
||||||
|
|
||||||
foreach (GameGadget gadget in _player.Gadgets
|
foreach (GameGadget gadget in _player.Gadgets
|
||||||
.Where(gadget => HorizontalSq(pos, gadget.Node.Pos) > DespawnRadiusSq)
|
.Where(gadget => HorizontalSq(pos, gadget.Node.Pos) > LooseUnloadSq)
|
||||||
.ToList())
|
.ToList())
|
||||||
{
|
{
|
||||||
live.Remove(gadget.NodeId);
|
live.Remove(gadget.NodeId);
|
||||||
|
|
@ -112,7 +133,7 @@ internal sealed class WorldSpawner
|
||||||
continue;
|
continue;
|
||||||
if (node.IsChest && _player.OpenedGadgetIds.Contains(node.NodeId))
|
if (node.IsChest && _player.OpenedGadgetIds.Contains(node.NodeId))
|
||||||
continue;
|
continue;
|
||||||
if (HorizontalSq(pos, node.Pos) > SpawnRadiusSq)
|
if (HorizontalSq(pos, node.Pos) > LooseLoadSq)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
GameGadget gadget = _player.SpawnGadget(node);
|
GameGadget gadget = _player.SpawnGadget(node);
|
||||||
|
|
@ -121,6 +142,25 @@ internal sealed class WorldSpawner
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private uint ScaleLevel(uint nodeLevel)
|
||||||
|
{
|
||||||
|
int reference = (int)(_player.ActiveAvatar?.Level ?? nodeLevel);
|
||||||
|
if (reference <= (int)nodeLevel)
|
||||||
|
return nodeLevel;
|
||||||
|
|
||||||
|
int top = reference - LevelBand;
|
||||||
|
int drop = (TopNodeLevel - Math.Min((int)nodeLevel, TopNodeLevel)) * LevelSpread / TopNodeLevel;
|
||||||
|
|
||||||
|
return (uint)Math.Clamp(top - drop, 1, MaxMonsterLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float AnchorSq(Vector pos, SpawnGroup group)
|
||||||
|
{
|
||||||
|
float dx = pos.X - group.X;
|
||||||
|
float dz = pos.Z - group.Z;
|
||||||
|
return dx * dx + dz * dz;
|
||||||
|
}
|
||||||
|
|
||||||
private static float HorizontalSq(Vector a, Vector b)
|
private static float HorizontalSq(Vector a, Vector b)
|
||||||
{
|
{
|
||||||
float dx = a.X - b.X;
|
float dx = a.X - b.X;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Sockets;
|
using System.Net.Sockets;
|
||||||
using Yuanshen.Gameserver.Options;
|
using Yuanshen.Gameserver.Options;
|
||||||
|
|
@ -14,6 +15,8 @@ internal sealed class KcpGateway : IGateway
|
||||||
private readonly IOptions<GatewayOptions> _options;
|
private readonly IOptions<GatewayOptions> _options;
|
||||||
private readonly NetSessionManager _sessionManager;
|
private readonly NetSessionManager _sessionManager;
|
||||||
|
|
||||||
|
private readonly ConcurrentDictionary<string, (uint ConvId, uint Token)> _handshakes = new();
|
||||||
|
|
||||||
private uint _sessionCounter;
|
private uint _sessionCounter;
|
||||||
private IKcpTransport<IKcpMultiplexConnection>? _transport;
|
private IKcpTransport<IKcpMultiplexConnection>? _transport;
|
||||||
|
|
||||||
|
|
@ -57,12 +60,29 @@ internal sealed class KcpGateway : IGateway
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
string key = clientEndPoint.ToString();
|
||||||
|
|
||||||
|
if (_handshakes.TryGetValue(key, out (uint ConvId, uint Token) existing))
|
||||||
|
{
|
||||||
|
await SendConversationCreatedPacket(clientEndPoint, existing.ConvId, existing.Token);
|
||||||
|
_logger.LogDebug("StartConversation retransmit from {endPoint}, reusing conv={conv}",
|
||||||
|
clientEndPoint, (long)existing.ConvId << 32 | existing.Token);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
uint convId = Interlocked.Increment(ref _sessionCounter);
|
uint convId = Interlocked.Increment(ref _sessionCounter);
|
||||||
uint token = (uint)_random.Next();
|
uint token = (uint)_random.Next();
|
||||||
|
|
||||||
|
if (!_handshakes.TryAdd(key, (convId, token)))
|
||||||
|
{
|
||||||
|
(uint ConvId, uint Token) winner = _handshakes[key];
|
||||||
|
await SendConversationCreatedPacket(clientEndPoint, winner.ConvId, winner.Token);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
long convId64 = (long)convId << 32 | token;
|
long convId64 = (long)convId << 32 | token;
|
||||||
KcpConversation conversation = _transport!.Connection.CreateConversation(convId64, clientEndPoint);
|
KcpConversation conversation = _transport!.Connection.CreateConversation(convId64, clientEndPoint);
|
||||||
_ = _sessionManager.RunSessionAsync(convId64, new KcpNetworkUnit(conversation, clientEndPoint));
|
_ = RunAndReleaseAsync(convId64, conversation, clientEndPoint, key);
|
||||||
|
|
||||||
await SendConversationCreatedPacket(clientEndPoint, convId, token);
|
await SendConversationCreatedPacket(clientEndPoint, convId, token);
|
||||||
_logger.LogInformation("StartConversation accepted from {endPoint}, conv={conv}", clientEndPoint, convId64);
|
_logger.LogInformation("StartConversation accepted from {endPoint}, conv={conv}", clientEndPoint, convId64);
|
||||||
|
|
@ -73,6 +93,18 @@ internal sealed class KcpGateway : IGateway
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task RunAndReleaseAsync(long convId64, KcpConversation conversation, IPEndPoint clientEndPoint, string key)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _sessionManager.RunSessionAsync(convId64, new KcpNetworkUnit(conversation, clientEndPoint));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_handshakes.TryRemove(key, out _);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async ValueTask SendConversationCreatedPacket(IPEndPoint clientEndPoint, uint convId, uint token)
|
private async ValueTask SendConversationCreatedPacket(IPEndPoint clientEndPoint, uint convId, uint token)
|
||||||
{
|
{
|
||||||
KcpHandshake handshakeResponse = new()
|
KcpHandshake handshakeResponse = new()
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ builder.Services.AddScoped<GmCommandService>();
|
||||||
builder.Services.AddSingleton<MonsterSpawnTable>();
|
builder.Services.AddSingleton<MonsterSpawnTable>();
|
||||||
builder.Services.AddSingleton<DropTable>();
|
builder.Services.AddSingleton<DropTable>();
|
||||||
builder.Services.AddSingleton<GadgetSpawnTable>();
|
builder.Services.AddSingleton<GadgetSpawnTable>();
|
||||||
|
builder.Services.AddSingleton<SpawnGroupTable>();
|
||||||
builder.Services.AddScoped<WorldSpawner>();
|
builder.Services.AddScoped<WorldSpawner>();
|
||||||
builder.Services.AddScoped<Yuanshen.Gameserver.Database.PlayerPersistence>();
|
builder.Services.AddScoped<Yuanshen.Gameserver.Database.PlayerPersistence>();
|
||||||
builder.Services.AddSingleton<Yuanshen.Gameserver.Database.PlayerSessionRegistry>();
|
builder.Services.AddSingleton<Yuanshen.Gameserver.Database.PlayerSessionRegistry>();
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
1
Yuanshen.Gameserver/assets/scene/scene3_groups.json
Normal file
1
Yuanshen.Gameserver/assets/scene/scene3_groups.json
Normal file
File diff suppressed because one or more lines are too long
8
Yuanshen.Proto/NpcTalkReq.proto
Normal file
8
Yuanshen.Proto/NpcTalkReq.proto
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
option csharp_namespace = "Yuanshen.Protocol";
|
||||||
|
|
||||||
|
message NpcTalkReq {
|
||||||
|
uint32 npcEntityId = 1;
|
||||||
|
uint32 talkId = 3;
|
||||||
|
}
|
||||||
9
Yuanshen.Proto/NpcTalkRsp.proto
Normal file
9
Yuanshen.Proto/NpcTalkRsp.proto
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
option csharp_namespace = "Yuanshen.Protocol";
|
||||||
|
|
||||||
|
message NpcTalkRsp {
|
||||||
|
int32 retcode = 1;
|
||||||
|
uint32 npcEntityId = 2;
|
||||||
|
uint32 curTalkId = 4;
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,12 @@ syntax = "proto3";
|
||||||
|
|
||||||
option csharp_namespace = "Yuanshen.Protocol";
|
option csharp_namespace = "Yuanshen.Protocol";
|
||||||
|
|
||||||
|
import "Vector.proto";
|
||||||
|
|
||||||
message QueryPathReq {
|
message QueryPathReq {
|
||||||
uint32 queryId = 1;
|
int32 queryType = 1;
|
||||||
|
int32 queryId = 2;
|
||||||
|
uint32 sceneId = 3;
|
||||||
|
Vector sourcePos = 4;
|
||||||
|
repeated Vector destinationPos = 5;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,11 @@ syntax = "proto3";
|
||||||
|
|
||||||
option csharp_namespace = "Yuanshen.Protocol";
|
option csharp_namespace = "Yuanshen.Protocol";
|
||||||
|
|
||||||
|
import "Vector.proto";
|
||||||
|
|
||||||
message QueryPathRsp {
|
message QueryPathRsp {
|
||||||
int32 retcode = 1;
|
int32 retcode = 1;
|
||||||
uint32 queryId = 2;
|
int32 queryId = 2;
|
||||||
int32 queryStatus = 3;
|
int32 queryStatus = 3;
|
||||||
|
repeated Vector corners = 4;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,29 @@ public static class TelemetryHandler
|
||||||
{
|
{
|
||||||
private const int PreviewLimit = 1200;
|
private const int PreviewLimit = 1200;
|
||||||
|
|
||||||
|
private static readonly object FileLock = new();
|
||||||
|
private static readonly string LogPath = Path.Combine(AppContext.BaseDirectory, "client-telemetry.log");
|
||||||
|
|
||||||
public static async Task<IResult> OnLog(HttpRequest request)
|
public static async Task<IResult> OnLog(HttpRequest request)
|
||||||
{
|
{
|
||||||
Log.Info($"telemetry {request.Path} {await BodyAsync(request)}");
|
string body = await BodyAsync(request);
|
||||||
|
Append(request.Path, body);
|
||||||
|
Log.Info($"telemetry {request.Path} {body}");
|
||||||
return TypedResults.Text("{\"code\":0}", "application/json");
|
return TypedResults.Text("{\"code\":0}", "application/json");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void Append(string path, string body)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
lock (FileLock)
|
||||||
|
File.AppendAllText(LogPath, $"{DateTime.Now:HH:mm:ss} {path} {body}{Environment.NewLine}");
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static async Task<IResult> OnUnhandled(HttpRequest request)
|
public static async Task<IResult> OnUnhandled(HttpRequest request)
|
||||||
{
|
{
|
||||||
string path = $"{request.Path}{request.QueryString}";
|
string path = $"{request.Path}{request.QueryString}";
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue