Gadgets are streamed alongside monsters now, so chests and ore nodes show up in the world and can be interacted with. The two flows differ: for a chest the client sends the finish request itself and the loot is granted on that one, for ore it sends nothing more so the server has to close everything on the first request. Artifacts used to be silently dropped on the floor because the item ids that actually roll were not in the reliquary table at all. Added the missing rows and gave them real main stat and substat rolls instead of the fixed four. Dropped items survive a relogin. The inventory is still rebuilt from the tables on every login, so the trick is to remember where the default set ends and only persist what was found in the world. Gacha and shop weapons were disappearing the same way and are fixed by the same change.
186 lines
7.1 KiB
C#
186 lines
7.1 KiB
C#
using Yuanshen.Common.Data.Excel;
|
|
using Yuanshen.Gameserver.Controllers.Attributes;
|
|
using Yuanshen.Gameserver.Controllers.Result;
|
|
using Yuanshen.Gameserver.Database;
|
|
using Yuanshen.Gameserver.Game;
|
|
using Yuanshen.Gameserver.Network.Session;
|
|
using Yuanshen.Protocol;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace Yuanshen.Gameserver.Controllers;
|
|
|
|
[NetController]
|
|
internal sealed class GadgetController : ControllerBase
|
|
{
|
|
private const uint ChestOpenedState = 102;
|
|
private const uint ActionReasonOpenChest = 39;
|
|
private const uint ActionReasonGather = 11;
|
|
private const uint MoraItemId = 202;
|
|
private const uint PrimogemItemId = 201;
|
|
|
|
private static readonly Random Random = new();
|
|
|
|
[NetCommand(CmdType.GadgetInteractReq)]
|
|
public async ValueTask<IResult> OnGadgetInteractReq(NetSession session, Player player, SceneManager scene,
|
|
DropTable drops, ExcelTableCollection excel, PlayerPersistence persistence, ILogger<GadgetController> logger)
|
|
{
|
|
GadgetInteractReq req = Packet!.DecodeBody<GadgetInteractReq>();
|
|
|
|
GameGadget? gadget = player.Gadgets.FirstOrDefault(candidate => candidate.EntityId == req.GadgetEntityId);
|
|
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);
|
|
|
|
return await GatherAsync(session, player, scene, drops, excel, persistence, logger, gadget, req);
|
|
}
|
|
|
|
private async ValueTask<IResult> OpenChestAsync(NetSession session, Player player, SceneManager scene,
|
|
DropTable drops, ExcelTableCollection excel, PlayerPersistence persistence, ILogger logger,
|
|
GameGadget gadget, GadgetInteractReq req)
|
|
{
|
|
if (req.OpType == InterOpType.InterOpStart)
|
|
{
|
|
if (player.OpenedGadgetIds.Contains(gadget.NodeId))
|
|
return Reply(req, Retcode.RET_CHEST_IS_LOCKED);
|
|
|
|
return Reply(req, Retcode.RET_SUCC, InteractType.InteractOpenChest);
|
|
}
|
|
|
|
if (!player.OpenedGadgetIds.Add(gadget.NodeId))
|
|
return Reply(req, Retcode.RET_CHEST_IS_LOCKED);
|
|
|
|
await session.NotifyAsync(CmdType.GadgetInteractRsp, Build(req, Retcode.RET_SUCC, InteractType.InteractOpenChest));
|
|
await scene.SetGadgetStateAsync(gadget, ChestOpenedState, false);
|
|
|
|
Dictionary<uint, uint> rolled = drops.Roll(gadget.Node.DropId, Random);
|
|
await GrantAsync(session, player, drops, excel, gadget, rolled, ActionReasonOpenChest);
|
|
|
|
await persistence.SaveAsync();
|
|
|
|
logger.LogInformation("Chest {gadgetId} node {node} opened, drop {dropId} gave {count} stacks",
|
|
gadget.GadgetId, gadget.NodeId, gadget.Node.DropId, rolled.Count);
|
|
|
|
return Ok();
|
|
}
|
|
|
|
private async ValueTask<IResult> GatherAsync(NetSession session, Player player, SceneManager scene,
|
|
DropTable drops, ExcelTableCollection excel, PlayerPersistence persistence, ILogger logger,
|
|
GameGadget gadget, GadgetInteractReq req)
|
|
{
|
|
if (req.OpType != InterOpType.InterOpStart)
|
|
return Reply(req, Retcode.RET_SUCC, InteractType.InteractGather);
|
|
|
|
if (!player.OpenedGadgetIds.Add(gadget.NodeId))
|
|
return Reply(req, Retcode.RET_GADGET_NOT_EXIST);
|
|
|
|
await session.NotifyAsync(CmdType.GadgetInteractRsp, Build(req, Retcode.RET_SUCC, InteractType.InteractGather));
|
|
|
|
Dictionary<uint, uint> rolled = new();
|
|
|
|
if (gadget.Node.SubfieldId != 0)
|
|
{
|
|
uint dropId = drops.SubfieldDropId(gadget.Node.SubfieldId, gadget.Node.Level);
|
|
if (dropId != 0)
|
|
rolled = drops.Roll(dropId, Random);
|
|
}
|
|
|
|
if (rolled.Count == 0 && gadget.Node.GatherItemId != 0)
|
|
rolled[gadget.Node.GatherItemId] = 1;
|
|
|
|
await GrantAsync(session, player, drops, excel, gadget, rolled, ActionReasonGather);
|
|
await scene.DespawnGadgetAsync(gadget, VisionType.VisionGatherEscape);
|
|
await persistence.SaveAsync();
|
|
|
|
logger.LogInformation("Gathered {gadgetId} node {node}, gave {count} stacks",
|
|
gadget.GadgetId, gadget.NodeId, rolled.Count);
|
|
|
|
return Ok();
|
|
}
|
|
|
|
private static async ValueTask GrantAsync(NetSession session, Player player, DropTable drops,
|
|
ExcelTableCollection excel, GameGadget gadget, Dictionary<uint, uint> rolled, uint reason)
|
|
{
|
|
if (rolled.Count == 0)
|
|
return;
|
|
|
|
StoreItemChangeNotify store = new() { StoreType = StoreType.StorePack };
|
|
ItemAddHintNotify hint = new()
|
|
{
|
|
Reason = reason,
|
|
Position = gadget.Node.Pos,
|
|
IsPositionValid = true
|
|
};
|
|
|
|
bool currencyChanged = false;
|
|
|
|
foreach ((uint itemId, uint count) in rolled)
|
|
{
|
|
switch (drops.TypeOf(itemId))
|
|
{
|
|
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());
|
|
}
|
|
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 });
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
private IResult Reply(GadgetInteractReq req, int retcode, InteractType interactType = InteractType.InteractNone)
|
|
=> Response(CmdType.GadgetInteractRsp, Build(req, retcode, interactType));
|
|
|
|
private static GadgetInteractRsp Build(GadgetInteractReq req, int retcode, InteractType interactType) => new()
|
|
{
|
|
Retcode = retcode,
|
|
GadgetEntityId = req.GadgetEntityId,
|
|
GadgetId = req.GadgetId,
|
|
InteractType = interactType,
|
|
OpType = req.OpType
|
|
};
|
|
}
|