From def4b8ae6898b0b5c3fba60ea72cb21f8c776f36 Mon Sep 17 00:00:00 2001 From: Kei-Luna Date: Tue, 19 May 2026 10:14:22 +0900 Subject: [PATCH] Gacha_UpSelect --- Common/Data/Excel/GachaExcel.cs | 2 + .../CallGS/Handlers/Gacha/Gacha_Launch.cs | 103 ++++++++++++++++++ .../CallGS/Handlers/Gacha/Gacha_UpSelect.cs | 82 ++++++++++++++ 3 files changed, 187 insertions(+) create mode 100644 GameServer/Server/CallGS/Handlers/Gacha/Gacha_UpSelect.cs diff --git a/Common/Data/Excel/GachaExcel.cs b/Common/Data/Excel/GachaExcel.cs index cb19bba..03f2715 100644 --- a/Common/Data/Excel/GachaExcel.cs +++ b/Common/Data/Excel/GachaExcel.cs @@ -16,6 +16,7 @@ public class GachaExcel : ExcelResource public uint? ProtectTag { get; set; } public uint? ProtectType { get; set; } public JToken? ProtectCount { get; set; } + public uint? UpSelect { get; set; } public override uint GetId() => ID; public override void Loaded() => GameData.GachaData[ID] = this; @@ -41,4 +42,5 @@ public class GachaPoolItem public List GDPL { get; set; } = []; public int Weight { get; set; } public int? UPTag { get; set; } + public int? UPSelectTag { get; set; } } diff --git a/GameServer/Server/CallGS/Handlers/Gacha/Gacha_Launch.cs b/GameServer/Server/CallGS/Handlers/Gacha/Gacha_Launch.cs index dc1399e..98d82a0 100644 --- a/GameServer/Server/CallGS/Handlers/Gacha/Gacha_Launch.cs +++ b/GameServer/Server/CallGS/Handlers/Gacha/Gacha_Launch.cs @@ -26,6 +26,8 @@ public class Gacha_Launch : ICallGSHandler private const uint SidAddTimeProb = 2; private const uint SidAddProtectType = 3; private const uint SidAddTotalTime = 7; + private const int UpSelectIndex = 0; + private const int UpSelectGetFlagIndex = 1; private static readonly Random Rng = new(); public async Task Handle(Connection connection, string param, ushort seqNo) @@ -58,6 +60,7 @@ public class Gacha_Launch : ICallGSHandler } var pityState = LoadPityState(player, gachaCfg); + var upSelectState = LoadUpSelectState(player, gachaCfg); var config = BuildRuntimeConfig(gachaCfg, poolNames); var awards = new List>(); var tbNew = new List(); @@ -94,6 +97,18 @@ public class Gacha_Launch : ICallGSHandler trigger = forceTopUp && item != null && config.UpTarget != null && item.Rarity == config.UpTarget.Rarity; } + if (item != null && upSelectState.SelectedItem != null && item.Rarity >= config.TopRarity) + { + bool forceSelected = upSelectState.GuaranteedNext; + bool shouldSelect = forceSelected || Rng.Next(100) < 50; + if (shouldSelect) + { + var selectedItem = FindExactItem(allPoolItems, upSelectState.SelectedItem); + if (selectedItem != null && selectedItem.Rarity >= config.TopRarity) + item = selectedItem; + } + } + if (item == null || item.GDPL.Count < 4) { tbTrigger.Add(false); @@ -109,6 +124,7 @@ public class Gacha_Launch : ICallGSHandler tbTrigger.Add(trigger); UpdatePityState(pityState, config, item); + UpdateUpSelectState(upSelectState, config, item); var itemType = (ItemTypeEnum)g; switch (itemType) @@ -149,6 +165,7 @@ public class Gacha_Launch : ICallGSHandler } SavePityState(player, gachaCfg, pityState, awards.Count, sync); + SaveUpSelectState(player, gachaCfg, upSelectState, sync); DatabaseHelper.SaveDatabaseType(player.Data); DatabaseHelper.SaveDatabaseType(player.InventoryManager.InventoryData); DatabaseHelper.SaveDatabaseType(player.CharacterManager.CharacterData); @@ -187,6 +204,14 @@ public class Gacha_Launch : ICallGSHandler x.GDPL[2] == gdpl[2] && x.GDPL[3] == gdpl[3]); + private static GachaPoolItem? FindExactItem(List pool, uint[] gdpl) => + pool.FirstOrDefault(x => + x.GDPL.Count >= 4 && + x.GDPL[0] == gdpl[0] && + x.GDPL[1] == gdpl[1] && + x.GDPL[2] == gdpl[2] && + x.GDPL[3] == gdpl[3]); + private static GachaRuntimeConfig BuildRuntimeConfig(GachaExcel gachaCfg, List poolNames) { var allPoolItems = poolNames.SelectMany(name => GameData.GachaPoolData[name]).ToList(); @@ -271,6 +296,56 @@ public class Gacha_Launch : ICallGSHandler SetAttr(player, sync, GachaGid, baseSid + SidAddTotalTime, (uint)(state.PoolTotalTime + drawCount)); } + private static GachaUpSelectState LoadUpSelectState(PlayerInstance player, GachaExcel gachaCfg) + { + if (gachaCfg.UpSelect != 1) + return new GachaUpSelectState(); + + var raw = player.Data.StrAttrs.FirstOrDefault(x => x.Gid == GachaSgid && x.Sid == gachaCfg.ID)?.Val; + if (string.IsNullOrWhiteSpace(raw)) + return new GachaUpSelectState(); + + try + { + var state = JArray.Parse(raw); + uint[]? selected = null; + if (state.Count > UpSelectIndex && state[UpSelectIndex] is JArray selectedArray && selectedArray.Count >= 4) + { + selected = + [ + selectedArray[0]?.Value() ?? 0, + selectedArray[1]?.Value() ?? 0, + selectedArray[2]?.Value() ?? 0, + selectedArray[3]?.Value() ?? 0 + ]; + } + + return new GachaUpSelectState + { + SelectedItem = selected, + GuaranteedNext = state.Count > UpSelectGetFlagIndex && (state[UpSelectGetFlagIndex]?.Value() ?? 0) == 1, + RawState = state + }; + } + catch + { + return new GachaUpSelectState(); + } + } + + private static void SaveUpSelectState(PlayerInstance player, GachaExcel gachaCfg, GachaUpSelectState state, NtfSyncPlayer sync) + { + if (gachaCfg.UpSelect != 1 || state.RawState == null) + return; + + EnsureArraySize(state.RawState, 2); + state.RawState[UpSelectGetFlagIndex] = state.GuaranteedNext ? 1 : 0; + + var value = state.RawState.ToString(Newtonsoft.Json.Formatting.None); + player.SetStrAttr(GachaSgid, gachaCfg.ID, value); + sync.CustomStr[player.ToShiftedAttrKey(GachaSgid, gachaCfg.ID)] = value; + } + private static uint GetBaseSid(GachaExcel gachaCfg) { if (gachaCfg.ProtectTag.HasValue) @@ -317,6 +392,27 @@ public class Gacha_Launch : ICallGSHandler } } + private static void UpdateUpSelectState(GachaUpSelectState state, GachaRuntimeConfig config, GachaPoolItem item) + { + if (state.SelectedItem == null || item.Rarity < config.TopRarity) + return; + + state.GuaranteedNext = !MatchesGdpl(item, state.SelectedItem); + } + + private static bool MatchesGdpl(GachaPoolItem item, uint[] gdpl) => + item.GDPL.Count >= 4 && + item.GDPL[0] == gdpl[0] && + item.GDPL[1] == gdpl[1] && + item.GDPL[2] == gdpl[2] && + item.GDPL[3] == gdpl[3]; + + private static void EnsureArraySize(JArray state, int size) + { + while (state.Count < size) + state.Add(JValue.CreateNull()); + } + private static bool IsFromPool(GachaPoolItem item, PoolRarityRef target) => item.Rarity == target.Rarity && GameData.GachaPoolData.TryGetValue(target.PoolName, out var pool) && @@ -446,4 +542,11 @@ internal sealed class GachaRuntimeConfig public int TenGuaranteeRarity { get; set; } } +internal sealed class GachaUpSelectState +{ + public uint[]? SelectedItem { get; set; } + public bool GuaranteedNext { get; set; } + public JArray? RawState { get; set; } = new(); +} + internal sealed record PoolRarityRef(string PoolName, int Rarity); diff --git a/GameServer/Server/CallGS/Handlers/Gacha/Gacha_UpSelect.cs b/GameServer/Server/CallGS/Handlers/Gacha/Gacha_UpSelect.cs new file mode 100644 index 0000000..eab74fa --- /dev/null +++ b/GameServer/Server/CallGS/Handlers/Gacha/Gacha_UpSelect.cs @@ -0,0 +1,82 @@ +using MikuSB.Data; +using MikuSB.Database; +using MikuSB.Proto; +using Newtonsoft.Json.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace MikuSB.GameServer.Server.CallGS.Handlers.Gacha; + +[CallGSApi("Gacha_UpSelect")] +public class Gacha_UpSelect : ICallGSHandler +{ + private const uint GachaStrGid = 42; + private const int UpSelectIndex = 0; + private const int UpSelectGetFlagIndex = 1; + private const int UpPickPoolIndex = 2; + + public async Task Handle(Connection connection, string param, ushort seqNo) + { + var req = JsonSerializer.Deserialize(param); + var player = connection.Player!; + if (req == null || req.NId == 0 || req.Gdpl == null || req.Gdpl.Count < 4) + { + await CallGSRouter.SendScript(connection, "Gacha_UpSelect", "{\"sErr\":\"error.BadParam\"}"); + return; + } + + if (!GameData.GachaData.TryGetValue((uint)req.NId, out var gachaCfg) || gachaCfg.UpSelect != 1) + { + await CallGSRouter.SendScript(connection, "Gacha_UpSelect", "{\"sErr\":\"error.BadParam\"}"); + return; + } + + var valid = (gachaCfg.Pool ?? []) + .Where(GameData.GachaPoolData.ContainsKey) + .SelectMany(name => GameData.GachaPoolData[name]) + .Any(item => + item.UPSelectTag == 1 && + item.GDPL.Count >= 4 && + item.GDPL[0] == req.Gdpl[0] && + item.GDPL[1] == req.Gdpl[1] && + item.GDPL[2] == req.Gdpl[2] && + item.GDPL[3] == req.Gdpl[3]); + + if (!valid) + { + await CallGSRouter.SendScript(connection, "Gacha_UpSelect", "{\"sErr\":\"error.BadParam\"}"); + return; + } + + var existing = player.Data.StrAttrs.FirstOrDefault(x => x.Gid == GachaStrGid && x.Sid == (uint)req.NId)?.Val; + var state = string.IsNullOrWhiteSpace(existing) ? new JArray() : JArray.Parse(existing); + + EnsureArraySize(state, 3); + state[UpSelectIndex] = new JArray(req.Gdpl); + state[UpSelectGetFlagIndex] = 0; + if (state[UpPickPoolIndex] == null) + state[UpPickPoolIndex] = 0; + + player.SetStrAttr(GachaStrGid, (uint)req.NId, state.ToString(Newtonsoft.Json.Formatting.None)); + DatabaseHelper.SaveDatabaseType(player.Data); + + var sync = new NtfSyncPlayer(); + sync.CustomStr[player.ToShiftedAttrKey(GachaStrGid, (uint)req.NId)] = state.ToString(Newtonsoft.Json.Formatting.None); + await CallGSRouter.SendScript(connection, "Gacha_UpSelect", "{}", sync); + } + + private static void EnsureArraySize(JArray state, int size) + { + while (state.Count < size) + state.Add(JValue.CreateNull()); + } +} + +internal sealed class GachaUpSelectParam +{ + [JsonPropertyName("nId")] + public int NId { get; set; } + + [JsonPropertyName("gdpl")] + public List? Gdpl { get; set; } +}