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("materialType")] public string MaterialType { get; set; } = string.Empty;
|
||||
[JsonPropertyName("gadgetId")] public uint GadgetId { get; set; }
|
||||
[JsonPropertyName("itemUse")] public List<MaterialItemUse> ItemUse { get; set; } = new();
|
||||
|
||||
public override uint ExcelId => Id;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
using Yuanshen.Gameserver.Controllers.Attributes;
|
||||
using Yuanshen.Gameserver.Controllers.Result;
|
||||
using Yuanshen.Gameserver.Game;
|
||||
using Yuanshen.Gameserver.Network.Session;
|
||||
using Yuanshen.Protocol;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
|
|
@ -11,62 +10,27 @@ namespace Yuanshen.Gameserver.Controllers;
|
|||
internal sealed class AbilityController : ControllerBase
|
||||
{
|
||||
[NetCommand(CmdType.AbilityInvocationsNotify)]
|
||||
public async ValueTask<IResult> OnAbilityInvocationsNotify(NetSession session)
|
||||
{
|
||||
AbilityInvocationsNotify notify = Packet!.DecodeBody<AbilityInvocationsNotify>();
|
||||
await ForwardAsync(session, notify.Invokes);
|
||||
return Ok();
|
||||
}
|
||||
public ValueTask<IResult> OnAbilityInvocationsNotify() => ValueTask.FromResult(Ok());
|
||||
|
||||
[NetCommand(CmdType.ClientAbilityChangeNotify)]
|
||||
public ValueTask<IResult> OnClientAbilityChangeNotify() => ValueTask.FromResult(Ok());
|
||||
|
||||
[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>();
|
||||
|
||||
foreach (AbilityInvokeEntry entry in notify.Invokes)
|
||||
{
|
||||
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);
|
||||
GameMonster? monster = player.Monsters.FirstOrDefault(candidate => candidate.EntityId == notify.EntityId);
|
||||
if (monster is not null)
|
||||
await scene.SyncMonsterLevelAsync(monster);
|
||||
|
||||
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)]
|
||||
public ValueTask<IResult> OnEvtDoSkillSuccNotify(ILogger<AbilityController> logger)
|
||||
{
|
||||
logger.LogDebug("DoSkillSucc raw={body}", Convert.ToHexString(Packet!.Body.Span));
|
||||
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)
|
||||
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();
|
||||
|
|
@ -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)
|
||||
{
|
||||
uint activeEntityId = player.ActiveAvatar?.EntityId ?? 0;
|
||||
CombatInvocationsNotify forward = new();
|
||||
|
||||
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);
|
||||
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.Result;
|
||||
using Yuanshen.Gameserver.Database;
|
||||
|
|
@ -20,7 +19,7 @@ internal sealed class GadgetController : ControllerBase
|
|||
|
||||
[NetCommand(CmdType.GadgetInteractReq)]
|
||||
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>();
|
||||
|
||||
|
|
@ -28,14 +27,32 @@ internal sealed class GadgetController : ControllerBase
|
|||
if (gadget is null)
|
||||
return Reply(req, Retcode.RET_GADGET_NOT_EXIST);
|
||||
|
||||
if (gadget.IsChest)
|
||||
return await OpenChestAsync(session, player, scene, drops, excel, persistence, logger, gadget, req);
|
||||
if (gadget.IsDrop)
|
||||
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,
|
||||
DropTable drops, ExcelTableCollection excel, PlayerPersistence persistence, ILogger logger,
|
||||
DropTable drops, PlayerPersistence persistence, ILogger logger,
|
||||
GameGadget gadget, GadgetInteractReq req)
|
||||
{
|
||||
if (req.OpType == InterOpType.InterOpStart)
|
||||
|
|
@ -53,7 +70,7 @@ internal sealed class GadgetController : ControllerBase
|
|||
await scene.SetGadgetStateAsync(gadget, ChestOpenedState, false);
|
||||
|
||||
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();
|
||||
|
||||
|
|
@ -64,7 +81,7 @@ internal sealed class GadgetController : ControllerBase
|
|||
}
|
||||
|
||||
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)
|
||||
{
|
||||
if (req.OpType != InterOpType.InterOpStart)
|
||||
|
|
@ -87,7 +104,7 @@ internal sealed class GadgetController : ControllerBase
|
|||
if (rolled.Count == 0 && gadget.Node.GatherItemId != 0)
|
||||
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 persistence.SaveAsync();
|
||||
|
||||
|
|
@ -98,7 +115,7 @@ internal sealed class GadgetController : ControllerBase
|
|||
}
|
||||
|
||||
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)
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@ namespace Yuanshen.Gameserver.Controllers;
|
|||
[NetController]
|
||||
internal sealed class StubController : ControllerBase
|
||||
{
|
||||
private const int PathComplete = 1;
|
||||
private const int PathFailed = 3;
|
||||
|
||||
[NetCommand(CmdType.ClientFpsStatusNotify)]
|
||||
public ValueTask<IResult> OnClientFpsStatusNotify() => ValueTask.FromResult(Ok());
|
||||
|
||||
|
|
@ -82,11 +85,50 @@ internal sealed class StubController : ControllerBase
|
|||
{
|
||||
QueryPathReq req = Packet!.DecodeBody<QueryPathReq>();
|
||||
|
||||
return ValueTask.FromResult(Response(CmdType.QueryPathRsp, new QueryPathRsp
|
||||
QueryPathRsp rsp = new()
|
||||
{
|
||||
Retcode = Retcode.RET_SUCC,
|
||||
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 Yuanshen.Common.Data.Excel;
|
||||
using Yuanshen.Gameserver.Database;
|
||||
using Yuanshen.Gameserver.Network.Session;
|
||||
using Yuanshen.Protocol;
|
||||
|
|
@ -18,15 +19,19 @@ internal sealed class CombatManager
|
|||
private readonly Player _player;
|
||||
private readonly PlayerPersistence _persistence;
|
||||
private readonly DropTable _drops;
|
||||
private readonly SceneManager _scene;
|
||||
private readonly ExcelTableCollection _excel;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public CombatManager(NetSession session, Player player, PlayerPersistence persistence, DropTable drops,
|
||||
ILogger<CombatManager> logger)
|
||||
SceneManager scene, ExcelTableCollection excel, ILogger<CombatManager> logger)
|
||||
{
|
||||
_session = session;
|
||||
_player = player;
|
||||
_persistence = persistence;
|
||||
_drops = drops;
|
||||
_scene = scene;
|
||||
_excel = excel;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
|
|
@ -89,6 +94,46 @@ internal sealed class CombatManager
|
|||
_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)
|
||||
{
|
||||
uint dropId = _drops.DropIdFor(monster.MonsterId, monster.Level);
|
||||
|
|
@ -99,74 +144,27 @@ internal sealed class CombatManager
|
|||
if (rolled.Count == 0)
|
||||
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();
|
||||
bool currencyChanged = false;
|
||||
int scattered = 0;
|
||||
|
||||
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":
|
||||
for (uint made = 0; made < Math.Min(count, 10u); made++)
|
||||
store.ItemList.Add(_player.CreateWeapon(itemId).AsItem());
|
||||
break;
|
||||
|
||||
case "virtual" when itemId == MoraItemId:
|
||||
_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());
|
||||
await GrantDirectAsync(itemId, count);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
_player.GiveMaterial(itemId, count);
|
||||
store.ItemList.Add(new Item
|
||||
else
|
||||
{
|
||||
ItemId = itemId,
|
||||
Guid = _player.MaterialGuid(itemId),
|
||||
Material = new Material { Count = _player.Materials.GetValueOrDefault(itemId) }
|
||||
});
|
||||
break;
|
||||
GameGadget drop = _player.SpawnDrop(gadgetId, itemId, count, Scatter(monster.Pos, scattered++));
|
||||
await _scene.AppearGadgetAsync(drop);
|
||||
}
|
||||
|
||||
hint.ItemList.Add(new ItemHint { ItemId = itemId, Count = count, IsNew = false });
|
||||
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}",
|
||||
monster.MonsterId, monster.Level, string.Join(", ", granted));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ internal readonly record struct GadgetNode(
|
|||
uint MarkFlag)
|
||||
{
|
||||
public bool IsChest => Kind == "chest";
|
||||
|
||||
public bool IsDrop => Kind == "drop";
|
||||
}
|
||||
|
||||
internal sealed class GadgetSpawnTable
|
||||
|
|
|
|||
|
|
@ -10,6 +10,10 @@ internal sealed class GameGadget
|
|||
public uint GadgetId => Node.GadgetId;
|
||||
public uint NodeId => Node.NodeId;
|
||||
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)
|
||||
{
|
||||
|
|
@ -45,17 +49,25 @@ internal sealed class GameGadget
|
|||
GroupId = Node.GroupId,
|
||||
ConfigId = Node.ConfigId,
|
||||
OwnerEntityId = 0,
|
||||
BornType = GadgetBornType.GadgetBornNone,
|
||||
GadgetState = Node.State,
|
||||
GadgetType = Node.GadgetType,
|
||||
IsShowCutscene = false,
|
||||
AuthorityPeerId = Player.PeerId,
|
||||
IsEnableInteract = true,
|
||||
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
|
||||
{
|
||||
Type = 4001,
|
||||
|
|
|
|||
|
|
@ -175,6 +175,21 @@ internal sealed class Player
|
|||
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)
|
||||
{
|
||||
GameGadget gadget = new(node, 0x04000000 + ++_gadgetEntitySeq);
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ namespace Yuanshen.Gameserver.Game;
|
|||
|
||||
internal sealed class SceneManager
|
||||
{
|
||||
private const int LevelSyncAttempts = 3;
|
||||
private const int LevelSyncAttempts = 6;
|
||||
|
||||
private readonly NetSession _session;
|
||||
private readonly Player _player;
|
||||
|
|
@ -109,6 +109,20 @@ internal sealed class SceneManager
|
|||
SceneEntityAppearNotify notify = new() { AppearType = VisionType.VisionBorn };
|
||||
notify.EntityList.Add(monster.AsSceneEntity());
|
||||
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)
|
||||
|
|
|
|||
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
|
||||
{
|
||||
private const float SpawnRadiusSq = 120f * 120f;
|
||||
private const float DespawnRadiusSq = 150f * 150f;
|
||||
private const float GroupLoadSq = 80f * 80f;
|
||||
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 int ConcurrentCap = 25;
|
||||
private const int WildlifeCap = 6;
|
||||
private const int MonsterCap = 60;
|
||||
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 SceneManager _scene;
|
||||
private readonly MonsterSpawnTable _table;
|
||||
private readonly GadgetSpawnTable _gadgets;
|
||||
private readonly SpawnGroupTable _groups;
|
||||
|
||||
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;
|
||||
_scene = scene;
|
||||
_table = table;
|
||||
_gadgets = gadgets;
|
||||
_groups = groups;
|
||||
}
|
||||
|
||||
public async ValueTask EnterSceneAsync(Vector pos)
|
||||
{
|
||||
_player.Monsters.Clear();
|
||||
_player.Gadgets.Clear();
|
||||
await _scene.ClearMonstersAsync();
|
||||
await _scene.ClearGadgetsAsync();
|
||||
_lastStream = null;
|
||||
await StreamAsync(pos, force: true);
|
||||
}
|
||||
|
|
@ -52,42 +61,54 @@ internal sealed class WorldSpawner
|
|||
|
||||
_lastStream = pos;
|
||||
|
||||
HashSet<uint> active = new();
|
||||
foreach (GameMonster monster in _player.Monsters)
|
||||
if (monster.NodeId != 0)
|
||||
active.Add(monster.NodeId);
|
||||
await DespawnFarAsync(pos);
|
||||
await SpawnNearAsync(pos);
|
||||
await StreamGadgetsAsync(pos);
|
||||
}
|
||||
|
||||
foreach (GameMonster monster in _player.Monsters
|
||||
.Where(monster => monster.NodeId != 0 && HorizontalSq(pos, monster.Pos) > DespawnRadiusSq)
|
||||
.ToList())
|
||||
private async ValueTask DespawnFarAsync(Vector pos)
|
||||
{
|
||||
active.Remove(monster.NodeId);
|
||||
foreach (GameMonster monster in _player.Monsters.Where(monster => monster.NodeId != 0 && IsOutOfRange(pos, monster)).ToList())
|
||||
await _scene.DespawnMonsterAsync(monster);
|
||||
}
|
||||
|
||||
int wildlife = _player.Monsters.Count(monster => monster.IsWildlife);
|
||||
|
||||
foreach (SpawnNode node in _table.Nodes.OrderBy(node => node.IsWildlife))
|
||||
private bool IsOutOfRange(Vector pos, GameMonster monster)
|
||||
{
|
||||
if (active.Count >= ConcurrentCap)
|
||||
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();
|
||||
foreach (GameMonster monster in _player.Monsters)
|
||||
active.Add(monster.NodeId);
|
||||
|
||||
foreach (SpawnNode node in _table.Nodes)
|
||||
{
|
||||
if (active.Count >= MonsterCap)
|
||||
break;
|
||||
if (active.Contains(node.NodeId) || _player.KilledNodeIds.Contains(node.NodeId))
|
||||
continue;
|
||||
if (HorizontalSq(pos, node.Pos) > SpawnRadiusSq)
|
||||
continue;
|
||||
if (node.IsWildlife && wildlife >= WildlifeCap)
|
||||
if (!ShouldLoad(pos, node))
|
||||
continue;
|
||||
|
||||
if (node.IsWildlife)
|
||||
wildlife++;
|
||||
|
||||
GameMonster monster = _player.SpawnMonster(node.MonsterId, node.Pos, node.Level, node.Hp, node.Attack, node.Defense, node.NodeId, node.ConfigId, node.GroupId);
|
||||
GameMonster monster = _player.SpawnMonster(node.MonsterId, node.Pos, ScaleLevel(node.Level), node.Hp,
|
||||
node.Attack, node.Defense, node.NodeId, node.ConfigId, node.GroupId);
|
||||
|
||||
active.Add(node.NodeId);
|
||||
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)
|
||||
|
|
@ -97,7 +118,7 @@ internal sealed class WorldSpawner
|
|||
live.Add(gadget.NodeId);
|
||||
|
||||
foreach (GameGadget gadget in _player.Gadgets
|
||||
.Where(gadget => HorizontalSq(pos, gadget.Node.Pos) > DespawnRadiusSq)
|
||||
.Where(gadget => HorizontalSq(pos, gadget.Node.Pos) > LooseUnloadSq)
|
||||
.ToList())
|
||||
{
|
||||
live.Remove(gadget.NodeId);
|
||||
|
|
@ -112,7 +133,7 @@ internal sealed class WorldSpawner
|
|||
continue;
|
||||
if (node.IsChest && _player.OpenedGadgetIds.Contains(node.NodeId))
|
||||
continue;
|
||||
if (HorizontalSq(pos, node.Pos) > SpawnRadiusSq)
|
||||
if (HorizontalSq(pos, node.Pos) > LooseLoadSq)
|
||||
continue;
|
||||
|
||||
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)
|
||||
{
|
||||
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.Sockets;
|
||||
using Yuanshen.Gameserver.Options;
|
||||
|
|
@ -14,6 +15,8 @@ internal sealed class KcpGateway : IGateway
|
|||
private readonly IOptions<GatewayOptions> _options;
|
||||
private readonly NetSessionManager _sessionManager;
|
||||
|
||||
private readonly ConcurrentDictionary<string, (uint ConvId, uint Token)> _handshakes = new();
|
||||
|
||||
private uint _sessionCounter;
|
||||
private IKcpTransport<IKcpMultiplexConnection>? _transport;
|
||||
|
||||
|
|
@ -57,12 +60,29 @@ internal sealed class KcpGateway : IGateway
|
|||
{
|
||||
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 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;
|
||||
KcpConversation conversation = _transport!.Connection.CreateConversation(convId64, clientEndPoint);
|
||||
_ = _sessionManager.RunSessionAsync(convId64, new KcpNetworkUnit(conversation, clientEndPoint));
|
||||
_ = RunAndReleaseAsync(convId64, conversation, clientEndPoint, key);
|
||||
|
||||
await SendConversationCreatedPacket(clientEndPoint, convId, token);
|
||||
_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)
|
||||
{
|
||||
KcpHandshake handshakeResponse = new()
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ builder.Services.AddScoped<GmCommandService>();
|
|||
builder.Services.AddSingleton<MonsterSpawnTable>();
|
||||
builder.Services.AddSingleton<DropTable>();
|
||||
builder.Services.AddSingleton<GadgetSpawnTable>();
|
||||
builder.Services.AddSingleton<SpawnGroupTable>();
|
||||
builder.Services.AddScoped<WorldSpawner>();
|
||||
builder.Services.AddScoped<Yuanshen.Gameserver.Database.PlayerPersistence>();
|
||||
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";
|
||||
|
||||
import "Vector.proto";
|
||||
|
||||
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";
|
||||
|
||||
import "Vector.proto";
|
||||
|
||||
message QueryPathRsp {
|
||||
int32 retcode = 1;
|
||||
uint32 queryId = 2;
|
||||
int32 queryId = 2;
|
||||
int32 queryStatus = 3;
|
||||
repeated Vector corners = 4;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,12 +4,29 @@ public static class TelemetryHandler
|
|||
{
|
||||
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)
|
||||
{
|
||||
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");
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
string path = $"{request.Path}{request.QueryString}";
|
||||
|
|
|
|||
Loading…
Reference in a new issue