Compare commits

..

19 Commits

Author SHA1 Message Date
Kei-Luna
7c41d84a08 Update LICENSE 2026-06-05 06:33:48 +09:00
Kei-Luna
1c9eb7ef14 License 2026-06-05 06:30:16 +09:00
Kei-Luna
3c045a79a9 Update version.txt 2026-05-31 05:42:02 +09:00
Kei-Luna
ecf2446598 Weapon_OneKeyToMax 2026-05-31 05:17:08 +09:00
Kei-Luna
75d840974b Item_Recycle 2026-05-31 05:02:04 +09:00
Kei-Luna
d6f57053dd Update README_jp.md 2026-05-30 17:52:15 +09:00
Kei-Luna
518c04fdb4 Fixed an issue where clicking on a feature not yet implemented on the server side would cause an infinite loading loop. 2026-05-27 07:12:31 +09:00
Kei-Luna
a9b57fc1b7 MoneySync 2026-05-27 06:09:29 +09:00
Kei-Luna
63dc993614 IBLogic 2026-05-26 09:02:03 +09:00
Kei-Luna
26e4d6fd0f BattlePass 2026-05-26 08:49:39 +09:00
Kei-Luna
5de3551ef3 DLCLogic_CheckOpenAct 2026-05-26 07:47:49 +09:00
Kei-Luna
5f81b5f6ec Update version.txt 2026-05-25 14:03:00 +09:00
Kei-Luna
6063a3a0cd DreamCard_LevelSettlement DreamCard_UpdateData 2026-05-25 13:45:14 +09:00
Kei-Luna
60101e75e2 DreamCard can Enter 2026-05-25 13:25:35 +09:00
Kei-Luna
6497bb1c66 VirCapture_GetLevelAward 2026-05-25 13:25:04 +09:00
Kei-Luna
e4fb4d7722 Update version.txt 2026-05-25 13:08:11 +09:00
Kei-Luna
6bc9090c89 VirCaptureTower_EnterLevel 2026-05-25 13:07:34 +09:00
Kei-Luna
bc69a072e1 FishingServer_ConvertFood 2026-05-25 12:58:34 +09:00
Kei-Luna
2f1b6d35da VirCapture fix EXP 2026-05-25 10:20:19 +09:00
38 changed files with 3192 additions and 38 deletions

View File

@@ -0,0 +1,23 @@
using Newtonsoft.Json;
namespace MikuSB.Data.Excel;
[ResourceEntity("battlepass/timelist.json")]
public class BattlePassTimeExcel : ExcelResource
{
[JsonProperty("ID")] public uint Id { get; set; }
[JsonProperty("StartTime")] public string StartTime { get; set; } = "";
[JsonProperty("EndTime")] public string EndTime { get; set; } = "";
[JsonProperty("BuyStartTime")] public string BuyStartTime { get; set; } = "";
[JsonProperty("BuyEndTime")] public string BuyEndTime { get; set; } = "";
[JsonProperty("Condition")] public string Condition { get; set; } = "";
[JsonProperty("ExpStep")] public uint ExpStep { get; set; }
[JsonProperty("MaxExPerWeek")] public uint MaxExPerWeek { get; set; }
public override uint GetId() => Id;
public override void Loaded()
{
GameData.BattlePassTimeData[Id] = this;
}
}

View File

@@ -0,0 +1,21 @@
using Newtonsoft.Json;
namespace MikuSB.Data.Excel;
[ResourceEntity("dlc/dlc_activities.json")]
public class DlcActivityExcel : ExcelResource
{
[JsonProperty("ID")] public uint Id { get; set; }
[JsonProperty("StartTime")] public string StartTime { get; set; } = "";
[JsonProperty("EndTime")] public string EndTime { get; set; } = "";
[JsonProperty("EnterStartTime")] public string EnterStartTime { get; set; } = "";
[JsonProperty("CloseEndTime")] public string CloseEndTime { get; set; } = "";
[JsonProperty("Condition")] public string Condition { get; set; } = "";
public override uint GetId() => Id;
public override void Loaded()
{
GameData.DlcActivityData[Id] = this;
}
}

View File

@@ -0,0 +1,20 @@
using Newtonsoft.Json;
namespace MikuSB.Data.Excel;
[ResourceEntity("dlc/DreamCard/activity.json")]
public class DreamCardActivityExcel : ExcelResource
{
[JsonProperty("ID")] public uint ID { get; set; }
[JsonProperty("StartTime")] public string StartTime { get; set; } = "";
[JsonProperty("EndTime")] public string EndTime { get; set; } = "";
[JsonProperty("Condition")] public string Condition { get; set; } = "";
[JsonProperty("LevelListID")] public List<uint> LevelListID { get; set; } = [];
public override uint GetId() => ID;
public override void Loaded()
{
GameData.DreamCardActivityData[ID] = this;
}
}

View File

@@ -0,0 +1,89 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace MikuSB.Data.Excel;
[ResourceEntity("dlc/fishing/food.json")]
public class FishingFoodExcel : ExcelResource
{
[JsonProperty("ID")] public uint Id { get; set; }
[JsonProperty("FoodType")] public JToken? FoodTypeRaw { get; set; }
[JsonProperty("NeedItem")] public JToken? NeedItemRaw { get; set; }
[JsonProperty("CreateItems")] public JToken? CreateItemsRaw { get; set; }
[JsonProperty("EffectTime")] public JToken? EffectTimeRaw { get; set; }
[JsonProperty("FishingLevel")] public JToken? FishingLevelRaw { get; set; }
[JsonProperty("SeasonId")] public JToken? SeasonIdRaw { get; set; }
[JsonProperty("BaitNum")] public JToken? BaitNumRaw { get; set; }
[JsonProperty("FoodArea")] public JToken? FoodAreaRaw { get; set; }
[JsonIgnore] public uint FoodType => ReadUInt(FoodTypeRaw);
[JsonIgnore] public uint EffectTime => ReadUInt(EffectTimeRaw);
[JsonIgnore] public uint FishingLevel => ReadUInt(FishingLevelRaw);
[JsonIgnore] public uint SeasonId => ReadUInt(SeasonIdRaw);
[JsonIgnore] public List<List<uint>> NeedItem => ReadNestedUIntList(NeedItemRaw);
[JsonIgnore] public List<uint> CreateItems => ReadUIntList(CreateItemsRaw);
[JsonIgnore] public List<uint> BaitNum => ReadUIntList(BaitNumRaw);
[JsonIgnore] public List<uint> FoodArea => ReadUIntList(FoodAreaRaw);
public override uint GetId() => Id;
public override void Loaded()
{
GameData.FishingFoodData[Id] = this;
}
private static int ReadInt(JToken? token)
{
if (token == null)
return 0;
return token.Type switch
{
JTokenType.Integer => token.Value<int>(),
JTokenType.Float => (int)token.Value<decimal>(),
JTokenType.String when int.TryParse(token.Value<string>(), out var value) => value,
_ => 0
};
}
private static uint ReadUInt(JToken? token)
{
var value = ReadInt(token);
return value > 0 ? (uint)value : 0;
}
private static List<uint> ReadUIntList(JToken? token)
{
if (token is not JArray array)
return [];
var result = new List<uint>(array.Count);
foreach (var item in array)
{
var value = ReadUInt(item);
if (value > 0)
result.Add(value);
}
return result;
}
private static List<List<uint>> ReadNestedUIntList(JToken? token)
{
if (token is not JArray array)
return [];
var result = new List<List<uint>>(array.Count);
foreach (var row in array.OfType<JArray>())
{
var values = new List<uint>(row.Count);
foreach (var item in row)
{
values.Add(ReadUInt(item));
}
result.Add(values);
}
return result;
}
}

View File

@@ -0,0 +1,76 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace MikuSB.Data.Excel;
[ResourceEntity("purchase/ibgoods.json")]
public class IbGoodsExcel : ExcelResource
{
[JsonProperty("GoodsId")] private JToken? GoodsIdRaw { get; set; }
[JsonProperty("Type")] private JToken? TypeRaw { get; set; }
[JsonProperty("PreId")] private JToken? PreIdRaw { get; set; }
[JsonProperty("LimitTimes")] private JToken? LimitTimesRaw { get; set; }
[JsonProperty("Item")] private JToken? ItemRaw { get; set; }
[JsonProperty("Cost")] private JToken? CostRaw { get; set; }
[JsonProperty("Cost2")] private JToken? Cost2Raw { get; set; }
[JsonProperty("PcId")] public string PcId { get; set; } = "";
[JsonProperty("IosId")] public string IosId { get; set; } = "";
[JsonProperty("AndroidId")] public string AndroidId { get; set; } = "";
public override uint GetId() => GoodsId;
public override void Loaded()
{
GameData.IbGoodsData[GoodsId] = this;
}
[JsonIgnore]
public uint GoodsId => ReadUInt(GoodsIdRaw);
[JsonIgnore]
public int Type => (int)ReadUInt(TypeRaw);
[JsonIgnore]
public uint PreId => ReadUInt(PreIdRaw);
[JsonIgnore]
public uint LimitTimes => ReadUInt(LimitTimesRaw);
[JsonIgnore]
public List<uint> Item => ReadUIntList(ItemRaw);
[JsonIgnore]
public List<uint> Cost => ReadUIntList(CostRaw);
[JsonIgnore]
public List<uint> Cost2 => ReadUIntList(Cost2Raw);
public string GetProductId() =>
!string.IsNullOrWhiteSpace(PcId) ? PcId :
!string.IsNullOrWhiteSpace(AndroidId) ? AndroidId :
IosId;
private static uint ReadUInt(JToken? token)
{
if (token == null || token.Type is JTokenType.Null or JTokenType.Undefined)
return 0;
if (token.Type == JTokenType.Integer)
return token.Value<uint>();
if (token.Type == JTokenType.String && uint.TryParse(token.Value<string>(), out var value))
return value;
return 0;
}
private static List<uint> ReadUIntList(JToken? token)
{
if (token is not JArray array)
return [];
return array
.Select(entry => entry.Type == JTokenType.Integer ? entry.Value<uint>() : 0u)
.ToList();
}
}

View File

@@ -12,6 +12,7 @@ public class MonsterCardExcel : ExcelResource
[JsonProperty("Color")] public uint Color { get; set; }
[JsonProperty("RikiId")] public uint RikiId { get; set; }
[JsonProperty("CostValue")] public uint CostValue { get; set; }
[JsonProperty("Exp")] public uint Exp { get; set; }
[JsonIgnore]
public ulong TemplateId => GameResourceTemplateId.FromGdpl(Genre, Detail, Particular, Level);

View File

@@ -10,8 +10,17 @@ public class OtherItemExcel : ExcelResource
public uint Detail { get; set; }
public uint Particular { get; set; }
public uint Level { get; set; }
public string LuaType { get; set; } = "";
[JsonProperty("UseMode")] public JToken? UseModeRaw { get; set; }
[JsonProperty("Param1")] public JToken? Param1Raw { get; set; }
[JsonProperty("GMnum")] public JToken? GMnumRaw { get; set; }
[JsonIgnore]
public uint UseMode => ReadUInt(UseModeRaw);
[JsonIgnore]
public uint Param1 => ReadUInt(Param1Raw);
[JsonIgnore]
public uint GMnum => ReadUInt(GMnumRaw);

View File

@@ -6,6 +6,7 @@ namespace MikuSB.Data.Excel;
public class RecycleExcel : ExcelResource
{
public int ID { get; set; }
public JToken? RecycleReward { get; set; }
public JToken? RecycleBase { get; set; }
public JToken? RecycleRatio { get; set; }

View File

@@ -13,6 +13,7 @@ public class SupportCardExcel : ExcelResource
public uint Icon { get; set; }
public uint ProvideExp { get; set; }
public uint Color { get; set; }
[JsonProperty("RecycleID")] public int RecycleID { get; set; }
[JsonProperty("LevelLimitID")] public int LevelLimitId { get; set; }
[JsonProperty("AffixPool")] public List<int> AffixPool { get; set; } = [];
[JsonProperty("AffixCost")] public JToken? AffixCostRaw { get; set; }

View File

@@ -6,8 +6,11 @@ namespace MikuSB.Data.Excel;
public class VirCaptureLevelListExcel : ExcelResource
{
[JsonProperty("Level")] public uint Level { get; set; }
[JsonProperty("Exp")] public uint Exp { get; set; }
[JsonProperty("Num")] public uint Num { get; set; }
[JsonProperty("MaxCost")] public uint MaxCost { get; set; }
[JsonProperty("Rewards")] public List<List<uint>> Rewards { get; set; } = [];
[JsonProperty("ExpUp")] public double ExpUp { get; set; }
public override uint GetId() => Level;

View File

@@ -9,6 +9,7 @@ public class VirCaptureTimeExcel : ExcelResource
[JsonProperty("StartTime")] public string StartTime { get; set; } = "";
[JsonProperty("EndTime")] public string EndTime { get; set; } = "";
[JsonProperty("CaptureRegionId")] public List<uint> CaptureRegionId { get; set; } = [];
[JsonProperty("MaxExp")] public uint MaxExp { get; set; }
public override uint GetId() => Id;

View File

@@ -0,0 +1,44 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace MikuSB.Data.Excel;
[ResourceEntity("dlc/vircapture/tower.json")]
public class VirCaptureTowerExcel : ExcelResource
{
[JsonProperty("ID")] public uint Id { get; set; }
[JsonProperty("Condition")] public JToken? ConditionRaw { get; set; }
[JsonProperty("MapID")] public uint MapId { get; set; }
[JsonProperty("TrialCard")] public List<uint> TrialCard { get; set; } = [];
[JsonProperty("TaskPath")] public string TaskPath { get; set; } = "";
[JsonIgnore]
public Dictionary<int, uint> Condition { get; } = [];
public override uint GetId() => Id;
public override void Loaded()
{
Condition.Clear();
if (ConditionRaw is JObject obj)
{
foreach (var property in obj.Properties())
{
if (!int.TryParse(property.Name, out var key))
continue;
uint value = 0;
if (property.Value.Type == JTokenType.Integer)
value = property.Value.Value<uint>();
else if (property.Value.Type == JTokenType.String &&
uint.TryParse(property.Value.Value<string>(), out var parsed))
value = parsed;
if (value > 0)
Condition[key] = value;
}
}
GameData.VirCaptureTowerData[Id] = this;
}
}

View File

@@ -56,6 +56,12 @@ public static class GameData
public static Dictionary<uint, VirCaptureCaptureRegionExcel> VirCaptureCaptureRegionData { get; private set; } = [];
public static Dictionary<uint, VirCaptureLevelListExcel> VirCaptureLevelListData { get; private set; } = [];
public static Dictionary<ulong, MonsterCardExcel> MonsterCardData { get; private set; } = [];
public static Dictionary<uint, FishingFoodExcel> FishingFoodData { get; private set; } = [];
public static Dictionary<uint, VirCaptureTowerExcel> VirCaptureTowerData { get; private set; } = [];
public static Dictionary<uint, DreamCardActivityExcel> DreamCardActivityData { get; private set; } = [];
public static Dictionary<uint, DlcActivityExcel> DlcActivityData { get; private set; } = [];
public static Dictionary<uint, BattlePassTimeExcel> BattlePassTimeData { get; private set; } = [];
public static Dictionary<uint, IbGoodsExcel> IbGoodsData { get; private set; } = [];
}
public static class GameResourceTemplateId

View File

@@ -205,6 +205,8 @@ public class PlayerInstance(PlayerGameData data)
Pid = (ulong)Data.Uid,
Account = displayName,
Provider = displayName,
Channel = "gm",
Subchannel = "gm",
Name = displayName,
Level = Data.Level,
Sex = Data.Gender,
@@ -242,6 +244,11 @@ public class PlayerInstance(PlayerGameData data)
proto.StrAttrs[ToShiftedAttrKey(x.Gid, x.Sid)] = x.Val;
}
foreach (var (key, value) in BuildMoneySync())
{
proto.Money[key] = value;
}
proto.ShowItems.AddRange(Data.ShowItems);
return proto;
@@ -295,6 +302,24 @@ public class PlayerInstance(PlayerGameData data)
return (gid << 16) | sid;
}
public Dictionary<string, int> BuildMoneySync()
{
var currentMoney = (int)Math.Min(int.MaxValue, GetAttrValue(1, 3));
var sync = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase)
{
["."] = currentMoney,
["gm.gm"] = currentMoney,
["jinshan.jinshan"] = currentMoney,
["pc_jinshan.pc_jinshan"] = currentMoney
};
return sync;
}
private uint GetAttrValue(uint gid, uint sid)
{
return Data.Attrs.FirstOrDefault(x => x.Gid == gid && x.Sid == sid)?.Val ?? 0;
}
public void BuildPlayerAttr(bool additional = false)
{
var bootstrapAttrs = BuildLobbyBootstrapAttrs().ToList();
@@ -445,4 +470,4 @@ public class PlayerInstance(PlayerGameData data)
yield return (132, 1, 0);
}
#endregion
}
}

View File

@@ -8,6 +8,7 @@ public static class CallGSRouter
{
private static readonly Logger Logger = new("CallGS");
private static readonly Dictionary<string, ICallGSHandler> Handlers = [];
private const string UnavailableTipKey = "ui.TxtNotOpen";
public static void Init()
{
@@ -32,11 +33,13 @@ public static class CallGSRouter
catch (Exception e)
{
Logger.Error($"[{req.Api}] {e.Message}", e);
await SendUnavailableResponse(connection, req.Api);
}
return;
}
Logger.Error($"No handler for CallGS API: {req.Api}");
await SendUnavailableResponse(connection, req.Api);
}
public static async Task SendScript(Connection connection, string api, string arg, NtfSyncPlayer extra = null!)
@@ -44,4 +47,11 @@ public static class CallGSRouter
var rsp = new NtfCallScript { Api = api, Arg = arg, ExtraSync = extra };
await connection.SendPacket(CmdIds.NtfScript, rsp);
}
private static Task SendUnavailableResponse(Connection connection, string api)
{
// Many client Lua handlers treat sErr/sError as a recoverable failure path,
// which is preferable to leaving the request hanging forever.
return SendScript(connection, api, $$"""{"sErr":"{{UnavailableTipKey}}","sError":"{{UnavailableTipKey}}"}""");
}
}

View File

@@ -0,0 +1,113 @@
using MikuSB.Data;
using MikuSB.Data.Excel;
using MikuSB.Database.Player;
using MikuSB.GameServer.Game.Player;
using MikuSB.Proto;
using System.Globalization;
using System.Text.Json.Nodes;
namespace MikuSB.GameServer.Server.CallGS.Handlers.BattlePass;
[CallGSApi("BattlePassLogic_ClientRefresh")]
public class BattlePassLogic_ClientRefresh : ICallGSHandler
{
private const uint GroupId = 25;
private const uint CurIdSid = 1;
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var now = DateTime.Now;
var battlePass = ResolveCurrent(GameData.BattlePassTimeData.Values, now);
var player = connection.Player!;
var sync = new NtfSyncPlayer();
if (battlePass == null)
{
SetAttr(player, CurIdSid, 0, sync);
await CallGSRouter.SendScript(connection, "BattlePassLogic_ClientRefresh", "{}", sync);
return;
}
SetAttr(player, CurIdSid, battlePass.Id, sync);
var response = new JsonObject
{
["nId"] = battlePass.Id,
["nStartTime"] = ToUnixSeconds(ParseConfigTime(battlePass.StartTime)),
["nEndTime"] = ToUnixSeconds(ParseConfigTime(battlePass.EndTime))
};
await CallGSRouter.SendScript(connection, "BattlePassLogic_ClientRefresh", response.ToJsonString(), sync);
}
private static BattlePassTimeExcel? ResolveCurrent(IEnumerable<BattlePassTimeExcel> configs, DateTime now)
{
var parsed = configs
.Select(x => new
{
Config = x,
Start = ParseConfigTime(x.StartTime),
End = ParseConfigTime(x.EndTime)
})
.Where(x => x.Start.HasValue && x.End.HasValue)
.OrderBy(x => x.Start)
.ToList();
var current = parsed.FirstOrDefault(x => x.Start <= now && now < x.End);
if (current != null)
return current.Config;
var latestStarted = parsed.LastOrDefault(x => x.Start <= now && x.End > x.Start);
return latestStarted?.Config;
}
private static DateTime? ParseConfigTime(string? raw)
{
if (string.IsNullOrWhiteSpace(raw))
return null;
var normalized = raw.Trim().Trim('[', ']');
if (normalized.Length != 12)
return null;
return DateTime.TryParseExact(
normalized,
"yyyyMMddHHmm",
CultureInfo.InvariantCulture,
DateTimeStyles.None,
out var value)
? value
: null;
}
private static long ToUnixSeconds(DateTime? value)
{
return value.HasValue ? new DateTimeOffset(value.Value).ToUnixTimeSeconds() : 0L;
}
private static void SetAttr(PlayerInstance player, uint sid, uint value, NtfSyncPlayer sync)
{
var attr = GetOrCreateAttr(player, sid);
if (attr.Val != value)
{
attr.Val = value;
sync.Custom[player.ToPackedAttrKey(GroupId, sid)] = value;
sync.Custom[player.ToShiftedAttrKey(GroupId, sid)] = value;
}
}
private static PlayerAttr GetOrCreateAttr(PlayerInstance player, uint sid)
{
var attr = player.Data.Attrs.FirstOrDefault(x => x.Gid == GroupId && x.Sid == sid);
if (attr != null)
return attr;
attr = new PlayerAttr
{
Gid = GroupId,
Sid = sid
};
player.Data.Attrs.Add(attr);
return attr;
}
}

View File

@@ -3,7 +3,9 @@ using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using MikuSB.GameServer.Game.BossPvp;
using MikuSB.Proto;
using MikuSB.GameServer.Server.CallGS.Handlers.DreamCard;
using MikuSB.GameServer.Server.CallGS.Handlers.Tower;
using MikuSB.GameServer.Server.CallGS.Handlers.VirCapture;
namespace MikuSB.GameServer.Server.CallGS.Handlers.Chapter;
@@ -72,6 +74,20 @@ public class Chapter_DealLevelSettlement : ICallGSHandler
return response;
}
if (string.Equals(sCmd, "VirCaptureTower_LevelSettlement", StringComparison.Ordinal))
{
var (response, sync) = VirCaptureTower_LevelSettlement.HandleSettlement(connection.Player!, tbParam);
extraSync = sync;
return response;
}
if (string.Equals(sCmd, "DreamCard_LevelSettlement", StringComparison.Ordinal))
{
var (response, sync) = DreamCard_LevelSettlement.HandleSettlement(connection.Player!, tbParam);
extraSync = sync;
return response;
}
return tbParam?.DeepClone() ?? new JsonObject();
}

View File

@@ -0,0 +1,112 @@
using MikuSB.Data;
using MikuSB.Data.Excel;
using MikuSB.Database.Player;
using MikuSB.GameServer.Game.Player;
using MikuSB.Proto;
using System.Globalization;
using System.Text.Json.Nodes;
namespace MikuSB.GameServer.Server.CallGS.Handlers.DLC;
[CallGSApi("DLCLogic_CheckOpenAct")]
public class DLCLogic_CheckOpenAct : ICallGSHandler
{
private const uint GroupId = 15;
private const uint ActIdSid = 1;
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var now = DateTime.Now;
var act = ResolveCurrent(GameData.DlcActivityData.Values, now);
if (act == null)
{
await CallGSRouter.SendScript(connection, "DLCLogic_CheckOpenAct", "{\"bOpen\":false}");
return;
}
var player = connection.Player!;
var sync = new NtfSyncPlayer();
SetAttr(player, ActIdSid, act.Id, sync);
var response = new JsonObject
{
["bOpen"] = true,
["nId"] = act.Id,
["nStartTime"] = ToUnixSeconds(ParseConfigTime(act.EnterStartTime)),
["nEndTime"] = ToUnixSeconds(ParseConfigTime(act.CloseEndTime))
};
await CallGSRouter.SendScript(connection, "DLCLogic_CheckOpenAct", response.ToJsonString(), sync);
}
private static DlcActivityExcel? ResolveCurrent(IEnumerable<DlcActivityExcel> configs, DateTime now)
{
var parsed = configs
.Select(x => new
{
Config = x,
Start = ParseConfigTime(x.EnterStartTime),
End = ParseConfigTime(x.CloseEndTime)
})
.Where(x => x.Start.HasValue && x.End.HasValue)
.OrderBy(x => x.Start)
.ToList();
var current = parsed.FirstOrDefault(x => x.Start <= now && now < x.End);
if (current != null)
return current.Config;
var latestStarted = parsed.LastOrDefault(x => x.Start <= now && x.End > x.Start);
return latestStarted?.Config;
}
private static DateTime? ParseConfigTime(string? raw)
{
if (string.IsNullOrWhiteSpace(raw))
return null;
var normalized = raw.Trim().Trim('[', ']');
if (normalized.Length != 12)
return null;
return DateTime.TryParseExact(
normalized,
"yyyyMMddHHmm",
CultureInfo.InvariantCulture,
DateTimeStyles.None,
out var value)
? value
: null;
}
private static long ToUnixSeconds(DateTime? value)
{
return value.HasValue ? new DateTimeOffset(value.Value).ToUnixTimeSeconds() : 0L;
}
private static void SetAttr(PlayerInstance player, uint sid, uint value, NtfSyncPlayer sync)
{
var attr = GetOrCreateAttr(player, sid);
if (attr.Val != value)
{
attr.Val = value;
sync.Custom[player.ToPackedAttrKey(GroupId, sid)] = value;
sync.Custom[player.ToShiftedAttrKey(GroupId, sid)] = value;
}
}
private static PlayerAttr GetOrCreateAttr(PlayerInstance player, uint sid)
{
var attr = player.Data.Attrs.FirstOrDefault(x => x.Gid == GroupId && x.Sid == sid);
if (attr != null)
return attr;
attr = new PlayerAttr
{
Gid = GroupId,
Sid = sid
};
player.Data.Attrs.Add(attr);
return attr;
}
}

View File

@@ -0,0 +1,59 @@
using MikuSB.Data;
using MikuSB.Data.Excel;
using System.Globalization;
using System.Text.Json.Nodes;
namespace MikuSB.GameServer.Server.CallGS.Handlers.DreamCard;
[CallGSApi("DreamCard_CheckOpen")]
public class DreamCard_CheckOpen : ICallGSHandler
{
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var now = DateTime.Now;
var ids = GameData.DreamCardActivityData.Values
.Where(x => IsOpen(x, now))
.OrderBy(x => x.ID)
.Select(x => JsonValue.Create(x.ID))
.ToArray();
var response = new JsonObject
{
["tbID"] = new JsonArray(ids)
};
await CallGSRouter.SendScript(connection, "DreamCard_CheckOpen", response.ToJsonString());
}
private static bool IsOpen(DreamCardActivityExcel config, DateTime now)
{
var start = ParseConfigTime(config.StartTime);
if (!start.HasValue || start > now)
return false;
var end = ParseConfigTime(config.EndTime);
if (end.HasValue && now >= end.Value)
return false;
return string.IsNullOrWhiteSpace(config.Condition);
}
private static DateTime? ParseConfigTime(string? raw)
{
if (string.IsNullOrWhiteSpace(raw))
return null;
var normalized = raw.Trim().Trim('[', ']');
if (normalized.Length != 12)
return null;
return DateTime.TryParseExact(
normalized,
"yyyyMMddHHmm",
CultureInfo.InvariantCulture,
DateTimeStyles.None,
out var value)
? value
: null;
}
}

View File

@@ -0,0 +1,227 @@
using MikuSB.Data;
using MikuSB.Util;
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
namespace MikuSB.GameServer.Server.CallGS.Handlers.DreamCard;
[CallGSApi("DreamCard_EnterLevel")]
public class DreamCard_EnterLevel : ICallGSHandler
{
private static readonly Random Random = new();
private static readonly Lazy<DreamCardLevelIndex?> LevelIndex = new(LoadLevelIndex);
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var req = JsonSerializer.Deserialize<DreamCardEnterLevelParam>(param);
if (req == null || req.LevelId <= 0 || req.Diff <= 0 || req.Type is < 1 or > 3)
{
await CallGSRouter.SendScript(connection, "DreamCard_EnterLevel", "null");
return;
}
var now = DateTime.Now;
if (!IsAllowed(req, now))
{
await CallGSRouter.SendScript(connection, "DreamCard_EnterLevel", "null");
return;
}
var response = new JsonObject
{
["nSeed"] = Random.Next(1, 1_000_000_000),
["nID"] = req.LevelId,
["nDiff"] = req.Diff,
["nType"] = req.Type
};
await CallGSRouter.SendScript(connection, "DreamCard_EnterLevel", response.ToJsonString());
}
private static bool IsAllowed(DreamCardEnterLevelParam req, DateTime now)
{
var index = LevelIndex.Value;
if (index == null)
return true;
return req.Type switch
{
1 => index.OpenOrdinaryLevelIds(now).Contains((uint)req.LevelId),
2 => index.IsChallengeOpen((uint)req.LevelId, now),
3 => index.IsEndlessOpen((uint)req.LevelId, now),
_ => false
};
}
private static DreamCardLevelIndex? LoadLevelIndex()
{
try
{
var resourceRoot = ConfigManager.Config.Path.ResourcePath;
var dreamCardRoot = Path.Combine(resourceRoot, "dlc", "DreamCard");
var ordinaryLevels = LoadJson<List<DreamCardOrdinaryLevelEntry>>(Path.Combine(dreamCardRoot, "levellist.json")) ?? [];
var challengeLevels = LoadJson<List<DreamCardChallengeLevelEntry>>(Path.Combine(dreamCardRoot, "challenge.json")) ?? [];
var endlessLevels = LoadJson<List<DreamCardEndlessLevelEntry>>(Path.Combine(dreamCardRoot, "endless.json")) ?? [];
return new DreamCardLevelIndex(ordinaryLevels, challengeLevels, endlessLevels);
}
catch
{
return null;
}
}
private static T? LoadJson<T>(string path)
{
if (!File.Exists(path))
return default;
return JsonSerializer.Deserialize<T>(File.ReadAllText(path));
}
}
internal sealed class DreamCardEnterLevelParam
{
[JsonPropertyName("nID")]
public int LevelId { get; set; }
[JsonPropertyName("nDiff")]
public int Diff { get; set; }
[JsonPropertyName("nType")]
public int Type { get; set; }
[JsonPropertyName("nRoleId")]
public int RoleId { get; set; }
}
internal sealed class DreamCardLevelIndex
{
private readonly HashSet<uint> ordinaryLevelIds;
private readonly Dictionary<uint, DreamCardChallengeLevelEntry> challengeLevels;
private readonly Dictionary<uint, DreamCardEndlessLevelEntry> endlessLevels;
public DreamCardLevelIndex(
IEnumerable<DreamCardOrdinaryLevelEntry> ordinaryLevels,
IEnumerable<DreamCardChallengeLevelEntry> challengeLevels,
IEnumerable<DreamCardEndlessLevelEntry> endlessLevels)
{
ordinaryLevelIds = ordinaryLevels
.Where(x => x.LevelListId > 0)
.Select(x => x.LevelListId)
.ToHashSet();
this.challengeLevels = challengeLevels
.Where(x => x.ChallengeId > 0)
.GroupBy(x => x.ChallengeId)
.ToDictionary(x => x.Key, x => x.First());
this.endlessLevels = endlessLevels
.Where(x => x.EndlessId > 0)
.GroupBy(x => x.EndlessId)
.ToDictionary(x => x.Key, x => x.First());
}
public HashSet<uint> OpenOrdinaryLevelIds(DateTime now)
{
var ids = new HashSet<uint>();
foreach (var activity in GameData.DreamCardActivityData.Values)
{
if (!IsActivityOpen(activity, now))
continue;
foreach (var id in activity.LevelListID)
{
if (ordinaryLevelIds.Contains(id))
ids.Add(id);
}
}
return ids;
}
public bool IsChallengeOpen(uint id, DateTime now)
{
return challengeLevels.TryGetValue(id, out var entry) && IsWithin(entry.StartTime, entry.EndTime, now);
}
public bool IsEndlessOpen(uint id, DateTime now)
{
return endlessLevels.TryGetValue(id, out var entry) && IsWithin(entry.StartTime, entry.EndTime, now);
}
private static bool IsActivityOpen(Data.Excel.DreamCardActivityExcel config, DateTime now)
{
var start = ParseConfigTime(config.StartTime);
if (!start.HasValue || start > now)
return false;
var end = ParseConfigTime(config.EndTime);
if (end.HasValue && now >= end.Value)
return false;
return string.IsNullOrWhiteSpace(config.Condition);
}
private static bool IsWithin(string? startRaw, string? endRaw, DateTime now)
{
var start = ParseConfigTime(startRaw);
if (!start.HasValue || now < start.Value)
return false;
var end = ParseConfigTime(endRaw);
return !end.HasValue || now < end.Value;
}
private static DateTime? ParseConfigTime(string? raw)
{
if (string.IsNullOrWhiteSpace(raw))
return null;
var normalized = raw.Trim().Trim('[', ']');
if (normalized.Length != 12)
return null;
return DateTime.TryParseExact(
normalized,
"yyyyMMddHHmm",
CultureInfo.InvariantCulture,
DateTimeStyles.None,
out var value)
? value
: null;
}
}
internal sealed class DreamCardOrdinaryLevelEntry
{
[JsonPropertyName("LevelListID")]
public uint LevelListId { get; set; }
}
internal sealed class DreamCardChallengeLevelEntry
{
[JsonPropertyName("ChallengeId")]
public uint ChallengeId { get; set; }
[JsonPropertyName("StartTime")]
public string StartTime { get; set; } = "";
[JsonPropertyName("EndTime")]
public string EndTime { get; set; } = "";
}
internal sealed class DreamCardEndlessLevelEntry
{
[JsonPropertyName("EndlessID")]
public uint EndlessId { get; set; }
[JsonPropertyName("StartTime")]
public string StartTime { get; set; } = "";
[JsonPropertyName("EndTime")]
public string EndTime { get; set; } = "";
}

View File

@@ -0,0 +1,281 @@
using MikuSB.Database;
using MikuSB.Database.Player;
using MikuSB.GameServer.Game.Player;
using MikuSB.Proto;
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
namespace MikuSB.GameServer.Server.CallGS.Handlers.DreamCard;
[CallGSApi("DreamCard_LevelSettlement")]
public class DreamCard_LevelSettlement : ICallGSHandler
{
private const uint LevelGroupId = 152;
private const uint LevelSubNum = 10;
private const int OrdinaryType = 1;
private const int ChallengeType = 2;
private const int EndlessType = 3;
private static readonly Lazy<DreamCardSettlementIndex?> SettlementIndex = new(LoadIndex);
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var (response, sync) = HandleSettlement(connection.Player!, JsonNode.Parse(param));
await CallGSRouter.SendScript(connection, "DreamCard_LevelSettlement", response.ToJsonString(), sync);
}
public static (JsonObject Response, NtfSyncPlayer Sync) HandleSettlement(PlayerInstance player, JsonNode? tbParam)
{
var req = tbParam?.Deserialize<DreamCardLevelSettlementParam>();
if (req == null || req.LevelId <= 0 || req.Diff <= 0 || req.Type is < OrdinaryType or > EndlessType)
return (new JsonObject { ["sErr"] = "error.BadParam" }, new NtfSyncPlayer());
var sync = new NtfSyncPlayer();
var response = new JsonObject
{
["nID"] = req.LevelId,
["nDiff"] = req.Diff,
["nType"] = req.Type
};
switch (req.Type)
{
case OrdinaryType:
HandleOrdinary(player, sync, response, req);
break;
case ChallengeType:
HandleChallenge(player, sync, response, req);
break;
case EndlessType:
HandleEndless(response, req);
break;
}
DatabaseHelper.SaveDatabaseType(player.Data);
return (response, sync);
}
private static void HandleOrdinary(PlayerInstance player, NtfSyncPlayer sync, JsonObject response, DreamCardLevelSettlementParam req)
{
var baseSid = (uint)(LevelSubNum * req.LevelId);
var passAttr = GetOrCreateAttr(player.Data, LevelGroupId, baseSid + 1);
passAttr.Val += 1;
SyncAttr(sync, player, passAttr);
var diffAttr = GetOrCreateAttr(player.Data, LevelGroupId, baseSid + 2);
diffAttr.Val = Math.Max(diffAttr.Val, (uint)req.Diff);
SyncAttr(sync, player, diffAttr);
var starAttr = GetOrCreateAttr(player.Data, LevelGroupId, baseSid + 3);
starAttr.Val = MergeDifficultyBits(starAttr.Val, req.Diff, req.StarValue);
SyncAttr(sync, player, starAttr);
if (TryGetOrdinaryRewardId((uint)req.LevelId, (uint)req.Diff, out var rewardId) && rewardId > 0)
response["nRewardID"] = rewardId;
}
private static void HandleChallenge(PlayerInstance player, NtfSyncPlayer sync, JsonObject response, DreamCardLevelSettlementParam req)
{
var baseSid = (uint)(LevelSubNum * req.LevelId);
var scoreSid = baseSid + (uint)req.Diff + 4;
var currentScore = (uint)Math.Max(0, req.Score);
var scoreAttr = GetOrCreateAttr(player.Data, LevelGroupId, scoreSid);
var newRecord = currentScore > scoreAttr.Val;
scoreAttr.Val = Math.Max(scoreAttr.Val, currentScore);
SyncAttr(sync, player, scoreAttr);
var challengePeriodId = ResolveCurrentChallengePeriodId(DateTime.Now);
if (challengePeriodId > 0)
{
var periodAttr = GetOrCreateAttr(player.Data, LevelGroupId, 0);
periodAttr.Val = challengePeriodId;
SyncAttr(sync, player, periodAttr);
}
response["NewRecord"] = newRecord;
}
private static void HandleEndless(JsonObject response, DreamCardLevelSettlementParam req)
{
response["NewRecord"] = false;
}
private static uint MergeDifficultyBits(uint currentValue, int diff, int starMask)
{
var bitStart = Math.Max(0, diff - 1) * 3;
var result = currentValue;
for (var i = 0; i < 3; i++)
{
if (((starMask >> i) & 1) == 0)
continue;
result |= 1u << (bitStart + i);
}
return result;
}
private static bool TryGetOrdinaryRewardId(uint levelId, uint diff, out uint rewardId)
{
rewardId = 0;
var index = SettlementIndex.Value;
if (index == null)
return false;
return index.TryGetOrdinaryRewardId(levelId, diff, out rewardId);
}
private static uint ResolveCurrentChallengePeriodId(DateTime now)
{
var index = SettlementIndex.Value;
return index?.ResolveCurrentChallengePeriodId(now) ?? 0;
}
private static DreamCardSettlementIndex? LoadIndex()
{
try
{
var root = Path.Combine(MikuSB.Util.ConfigManager.Config.Path.ResourcePath, "dlc", "DreamCard");
var ordinaryLevels = LoadJson<List<DreamCardOrdinarySettlementEntry>>(Path.Combine(root, "levellist.json")) ?? [];
var challengeTimes = LoadJson<List<DreamCardChallengeTimeEntry>>(Path.Combine(root, "chall_time.json")) ?? [];
return new DreamCardSettlementIndex(ordinaryLevels, challengeTimes);
}
catch
{
return null;
}
}
private static T? LoadJson<T>(string path)
{
if (!File.Exists(path))
return default;
return JsonSerializer.Deserialize<T>(File.ReadAllText(path));
}
private static PlayerAttr GetOrCreateAttr(PlayerGameData data, uint gid, uint sid)
{
var attr = data.Attrs.FirstOrDefault(x => x.Gid == gid && x.Sid == sid);
if (attr != null)
return attr;
attr = new PlayerAttr
{
Gid = gid,
Sid = sid
};
data.Attrs.Add(attr);
return attr;
}
private static void SyncAttr(NtfSyncPlayer sync, PlayerInstance player, PlayerAttr attr)
{
sync.Custom[player.ToPackedAttrKey(attr.Gid, attr.Sid)] = attr.Val;
sync.Custom[player.ToShiftedAttrKey(attr.Gid, attr.Sid)] = attr.Val;
}
}
internal sealed class DreamCardLevelSettlementParam
{
[JsonPropertyName("nID")]
public int LevelId { get; set; }
[JsonPropertyName("nDiff")]
public int Diff { get; set; }
[JsonPropertyName("nType")]
public int Type { get; set; }
[JsonPropertyName("nStarValue")]
public int StarValue { get; set; }
[JsonPropertyName("nScore")]
public int Score { get; set; }
}
internal sealed class DreamCardSettlementIndex
{
private readonly Dictionary<(uint LevelId, uint Diff), uint> ordinaryRewardIds;
private readonly List<DreamCardChallengeTimeEntry> challengeTimes;
public DreamCardSettlementIndex(
IEnumerable<DreamCardOrdinarySettlementEntry> ordinaryLevels,
IEnumerable<DreamCardChallengeTimeEntry> challengeTimes)
{
ordinaryRewardIds = ordinaryLevels
.Where(x => x.LevelListId > 0 && x.HardStage > 0)
.GroupBy(x => (x.LevelListId, x.HardStage))
.ToDictionary(x => x.Key, x => x.First().RewardId);
this.challengeTimes = challengeTimes.ToList();
}
public bool TryGetOrdinaryRewardId(uint levelId, uint diff, out uint rewardId)
{
return ordinaryRewardIds.TryGetValue((levelId, diff), out rewardId);
}
public uint ResolveCurrentChallengePeriodId(DateTime now)
{
foreach (var entry in challengeTimes.OrderBy(x => x.ChallTimeId))
{
var start = ParseConfigTime(entry.StartTime);
var end = ParseConfigTime(entry.EndTime);
if (!start.HasValue || !end.HasValue)
continue;
if (start.Value <= now && now < end.Value)
return entry.ChallTimeId;
}
return 0;
}
private static DateTime? ParseConfigTime(string? raw)
{
if (string.IsNullOrWhiteSpace(raw))
return null;
var normalized = raw.Trim().Trim('[', ']');
if (normalized.Length != 12)
return null;
return DateTime.TryParseExact(
normalized,
"yyyyMMddHHmm",
CultureInfo.InvariantCulture,
DateTimeStyles.None,
out var value)
? value
: null;
}
}
internal sealed class DreamCardOrdinarySettlementEntry
{
[JsonPropertyName("LevelListID")]
public uint LevelListId { get; set; }
[JsonPropertyName("HardStage")]
public uint HardStage { get; set; }
[JsonPropertyName("RewardID")]
public uint RewardId { get; set; }
}
internal sealed class DreamCardChallengeTimeEntry
{
[JsonPropertyName("ChallTimeID")]
public uint ChallTimeId { get; set; }
[JsonPropertyName("StartTime")]
public string StartTime { get; set; } = "";
[JsonPropertyName("EndTime")]
public string EndTime { get; set; } = "";
}

View File

@@ -0,0 +1,59 @@
using MikuSB.Database;
using MikuSB.Proto;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace MikuSB.GameServer.Server.CallGS.Handlers.DreamCard;
[CallGSApi("DreamCard_UpdateData")]
public class DreamCard_UpdateData : ICallGSHandler
{
private const uint DataGroupId = 62;
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var player = connection.Player!;
var sync = new NtfSyncPlayer();
var dirty = false;
try
{
var entries = JsonSerializer.Deserialize<List<DreamCardUpdateDataEntry>>(param) ?? [];
foreach (var entry in entries)
{
if (entry.Id <= 0)
continue;
var value = NormalizeJson(entry.Data);
player.SetStrAttr(DataGroupId, (uint)entry.Id, value);
sync.CustomStr[player.ToShiftedAttrKey(DataGroupId, (uint)entry.Id)] = value;
dirty = true;
}
}
catch
{
// Ignore malformed payloads so the client-side save queue can continue.
}
if (dirty)
DatabaseHelper.SaveDatabaseType(player.Data);
await CallGSRouter.SendScript(connection, "DreamCard_UpdateData", "{}", sync);
}
private static string NormalizeJson(JsonElement data)
{
return data.ValueKind == JsonValueKind.Undefined
? "null"
: data.GetRawText();
}
}
internal sealed class DreamCardUpdateDataEntry
{
[JsonPropertyName("id")]
public int Id { get; set; }
[JsonPropertyName("data")]
public JsonElement Data { get; set; }
}

View File

@@ -0,0 +1,245 @@
using MikuSB.Data;
using MikuSB.Data.Excel;
using MikuSB.Database;
using MikuSB.Database.Inventory;
using MikuSB.Database.Player;
using MikuSB.Enums.Item;
using MikuSB.Proto;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
namespace MikuSB.GameServer.Server.CallGS.Handlers.Fishing;
[CallGSApi("FishingServer_ConvertFood")]
public class FishingServer_ConvertFood : ICallGSHandler
{
private const uint FishingGroupId = 32;
private const uint CashGroupId = 1;
private const uint FoodBaseSid = 30000;
private const uint FoodAvaTimeSubType = 1;
private const uint ExploreAvaTimeSubType = 2;
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var player = connection.Player!;
var req = JsonSerializer.Deserialize<FishingConvertFoodParam>(param);
if (req == null || req.FoodId <= 0 || req.Num <= 0)
{
await CallGSRouter.SendScript(connection, "FishingServer_ConvertFood", "{\"sError\":\"error.BadParam\"}");
return;
}
if (!GameData.FishingFoodData.TryGetValue((uint)req.FoodId, out var food))
{
await CallGSRouter.SendScript(connection, "FishingServer_ConvertFood", "{\"sError\":\"error.BadParam\"}");
return;
}
var count = Math.Max(1u, req.Num);
var sync = new NtfSyncPlayer();
if (!HasEnoughMaterials(player.InventoryManager.InventoryData, food.NeedItem, count) ||
!HasEnoughCash(player.Data, food.BaitNum, count))
{
await CallGSRouter.SendScript(connection, "FishingServer_ConvertFood", "{\"sError\":\"tip.girlcard_cmd_err\"}");
return;
}
ConsumeMaterials(player.InventoryManager.InventoryData, food.NeedItem, count, sync.Items);
ConsumeCash(player, food.BaitNum, count, sync);
var response = new JsonObject
{
["nFoodID"] = req.FoodId
};
switch (food.FoodType)
{
case 1:
ApplyFoodDuration(player, food, FoodAvaTimeSubType, count, sync);
break;
case 2:
{
var rewards = await CreateItemsAsync(player, sync, food.CreateItems, count);
response["tbBait"] = rewards;
break;
}
case 3:
ApplyFoodDuration(player, food, ExploreAvaTimeSubType, count, sync);
break;
default:
await CallGSRouter.SendScript(connection, "FishingServer_ConvertFood", "{\"sError\":\"error.BadParam\"}");
return;
}
DatabaseHelper.SaveDatabaseType(player.InventoryManager.InventoryData);
DatabaseHelper.SaveDatabaseType(player.Data);
await CallGSRouter.SendScript(connection, "FishingServer_ConvertFood", response.ToJsonString(), sync);
}
private static bool HasEnoughMaterials(InventoryData inventory, IEnumerable<List<uint>> costs, uint multiplier)
{
foreach (var cost in costs)
{
if (cost.Count < 5)
return false;
var templateId = GameResourceTemplateId.FromGdpl(cost[0], cost[1], cost[2], cost[3]);
var item = inventory.Items.Values.FirstOrDefault(x => x.TemplateId == templateId);
var needCount = checked(cost[4] * multiplier);
if (item == null || item.ItemCount < needCount)
return false;
}
return true;
}
private static void ConsumeMaterials(InventoryData inventory, IEnumerable<List<uint>> costs, uint multiplier, ICollection<Item> syncItems)
{
foreach (var cost in costs)
{
var templateId = GameResourceTemplateId.FromGdpl(cost[0], cost[1], cost[2], cost[3]);
var item = inventory.Items.Values.First(x => x.TemplateId == templateId);
var needCount = checked(cost[4] * multiplier);
item.ItemCount -= needCount;
if (item.ItemCount == 0)
{
inventory.Items.Remove(item.UniqueId);
var proto = item.ToProto();
proto.Count = 0;
syncItems.Add(proto);
}
else
{
syncItems.Add(item.ToProto());
}
}
}
private static bool HasEnoughCash(PlayerGameData data, IReadOnlyList<uint> baitNum, uint multiplier)
{
if (baitNum.Count < 2)
return true;
var moneyType = baitNum[0];
var need = checked(baitNum[1] * multiplier);
var sid = moneyType * 2 + 1;
var attr = data.Attrs.FirstOrDefault(x => x.Gid == CashGroupId && x.Sid == sid);
return (attr?.Val ?? 0) >= need;
}
private static void ConsumeCash(MikuSB.GameServer.Game.Player.PlayerInstance player, IReadOnlyList<uint> baitNum, uint multiplier, NtfSyncPlayer sync)
{
if (baitNum.Count < 2)
return;
var moneyType = baitNum[0];
var sid = moneyType * 2 + 1;
var need = checked(baitNum[1] * multiplier);
var attr = GetOrCreateAttr(player.Data, CashGroupId, sid);
attr.Val -= need;
SyncAttr(player, sync, attr);
}
private static void ApplyFoodDuration(MikuSB.GameServer.Game.Player.PlayerInstance player, FishingFoodExcel food, uint subType, uint count, NtfSyncPlayer sync)
{
var sid = FoodBaseSid + food.Id * 10 + subType;
var attr = GetOrCreateAttr(player.Data, FishingGroupId, sid);
var now = (uint)DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var startTime = Math.Max(attr.Val, now);
attr.Val = checked(startTime + food.EffectTime * count);
SyncAttr(player, sync, attr);
}
private static async Task<JsonArray> CreateItemsAsync(MikuSB.GameServer.Game.Player.PlayerInstance player, NtfSyncPlayer sync, IReadOnlyList<uint> createItem, uint multiplier)
{
var rewards = new JsonArray();
if (createItem.Count < 5)
return rewards;
var itemType = (ItemTypeEnum)createItem[0];
var detail = createItem[1];
var particular = createItem[2];
var level = createItem[3];
var totalCount = checked(createItem[4] * multiplier);
switch (itemType)
{
case ItemTypeEnum.TYPE_SUPPLIES:
{
var templateId = (uint)GameResourceTemplateId.FromGdpl(createItem[0], detail, particular, level);
if (GameData.SuppliesData.TryGetValue(templateId, out var supplies))
{
var item = await player.InventoryManager.AddSuppliesItem(supplies, totalCount, sendPacket: false);
if (item != null)
sync.Items.Add(item.ToProto());
}
break;
}
case ItemTypeEnum.TYPE_USEABLE:
{
var item = AddOtherItem(player.InventoryManager.InventoryData, detail, particular, level, totalCount);
if (item != null)
sync.Items.Add(item.ToProto());
break;
}
}
rewards.Add(new JsonArray((int)createItem[0], (int)detail, (int)particular, (int)level, (int)totalCount));
return rewards;
}
private static BaseGameItemInfo? AddOtherItem(InventoryData inventory, uint detail, uint particular, uint level, uint count)
{
var templateId = (uint)GameResourceTemplateId.FromGdpl((uint)ItemTypeEnum.TYPE_USEABLE, detail, particular, level);
if (!GameData.OtherItemData.TryGetValue(templateId, out var otherItem))
return null;
var maxCount = otherItem.GMnum > 0 ? otherItem.GMnum : 99999u;
var existing = inventory.Items.Values.FirstOrDefault(x => x.TemplateId == templateId);
if (existing != null)
{
existing.ItemCount = Math.Min(existing.ItemCount + count, maxCount);
return existing;
}
var item = new BaseGameItemInfo
{
TemplateId = templateId,
UniqueId = inventory.NextUniqueUid++,
ItemType = ItemTypeEnum.TYPE_USEABLE,
ItemCount = Math.Min(count, maxCount)
};
inventory.Items[item.UniqueId] = item;
return item;
}
private static PlayerAttr GetOrCreateAttr(PlayerGameData data, uint gid, uint sid)
{
var attr = data.Attrs.FirstOrDefault(x => x.Gid == gid && x.Sid == sid);
if (attr != null)
return attr;
attr = new PlayerAttr { Gid = gid, Sid = sid, Val = 0 };
data.Attrs.Add(attr);
return attr;
}
private static void SyncAttr(MikuSB.GameServer.Game.Player.PlayerInstance player, NtfSyncPlayer sync, PlayerAttr attr)
{
sync.Custom[player.ToPackedAttrKey(attr.Gid, attr.Sid)] = attr.Val;
sync.Custom[player.ToShiftedAttrKey(attr.Gid, attr.Sid)] = attr.Val;
}
}
internal sealed class FishingConvertFoodParam
{
[JsonPropertyName("nFoodID")]
public int FoodId { get; set; }
[JsonPropertyName("nNum")]
public uint Num { get; set; }
}

View File

@@ -0,0 +1,316 @@
using MikuSB.Data;
using MikuSB.Data.Excel;
using MikuSB.Database;
using MikuSB.Database.Inventory;
using MikuSB.Enums.Item;
using MikuSB.Proto;
using Newtonsoft.Json.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace MikuSB.GameServer.Server.CallGS.Handlers.Inventory;
[CallGSApi("Item_Recycle")]
public class Item_Recycle : ICallGSHandler
{
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var player = connection.Player!;
var req = JsonSerializer.Deserialize<ItemRecycleParam>(param);
if (req?.TbItems == null || req.TbItems.Count == 0)
{
await CallGSRouter.SendScript(connection, "Item_Recycle", "{\"sErr\":\"error.BadParam\"}");
return;
}
var config = RecycleConfig.Load();
var itemsToRecycle = new List<(BaseGameItemInfo Item, int RecycleId)>();
foreach (var uniqueId in req.TbItems)
{
BaseGameItemInfo? item = player.InventoryManager.GetWeaponItem((uint)uniqueId)
?? (BaseGameItemInfo?)player.InventoryManager.GetSupportCardItem((uint)uniqueId);
if (item == null)
{
await CallGSRouter.SendScript(connection, "Item_Recycle", "{\"sErr\":\"error.Recycle.ItemNotExists\"}");
return;
}
var recycleId = GetRecycleId(item);
if (recycleId <= 0 || !config.HasConfig(recycleId))
{
await CallGSRouter.SendScript(connection, "Item_Recycle", "{\"sErr\":\"error.Recycle.ItemCanNotRecycle\"}");
return;
}
itemsToRecycle.Add((item, recycleId));
}
var sync = new NtfSyncPlayer();
foreach (var (item, recycleId) in itemsToRecycle)
{
var rewards = config.CalcRewards(item, recycleId);
foreach (var reward in rewards)
await GrantRewardAsync(player, sync, reward);
RemoveItem(player.InventoryManager.InventoryData, item, sync);
}
DatabaseHelper.SaveDatabaseType(player.InventoryManager.InventoryData);
await CallGSRouter.SendScript(connection, "Item_Recycle", "{}", sync);
}
private static int GetRecycleId(BaseGameItemInfo item)
{
if (item.ItemType == ItemTypeEnum.TYPE_WEAPON)
{
var t = GameData.WeaponData.Values.FirstOrDefault(x =>
GameResourceTemplateId.FromGdpl(x.Genre, x.Detail, x.Particular, x.Level) == item.TemplateId);
return t?.RecycleID ?? 0;
}
if (item.ItemType == ItemTypeEnum.TYPE_SUPPORT)
{
var t = GameData.SupportCardData.FirstOrDefault(x => x.TemplateId == item.TemplateId);
return t?.RecycleID ?? 0;
}
return 0;
}
private static void RemoveItem(InventoryData inventory, BaseGameItemInfo item, NtfSyncPlayer sync)
{
var removed = item.ToProto();
removed.Count = 0;
sync.Items.Add(removed);
if (item.ItemType == ItemTypeEnum.TYPE_WEAPON)
inventory.Weapons.Remove(item.UniqueId);
else
inventory.SupportCards.Remove(item.UniqueId);
}
private static async Task GrantRewardAsync(GameServer.Game.Player.PlayerInstance player, NtfSyncPlayer sync, IReadOnlyList<uint> reward)
{
if (reward.Count < 5) return;
var itemType = (ItemTypeEnum)reward[0];
var detail = reward[1];
var particular = reward[2];
var level = reward[3];
var count = Math.Max(1u, reward[4]);
switch (itemType)
{
case ItemTypeEnum.TYPE_SUPPLIES:
{
var templateId = (uint)GameResourceTemplateId.FromGdpl(reward[0], detail, particular, level);
if (!GameData.SuppliesData.TryGetValue(templateId, out var supplies)) break;
var item = await player.InventoryManager.AddSuppliesItem(supplies, count, sendPacket: false);
if (item != null) sync.Items.Add(item.ToProto());
break;
}
}
}
}
internal sealed class RecycleConfig
{
private readonly Dictionary<int, RecycleEntry> _entries;
private readonly List<SupplyTemplate> _weaponSupplies;
private readonly List<SupplyTemplate> _supportSupplies;
private readonly Dictionary<int, ulong> _weaponLevelExp;
private readonly Dictionary<int, ulong> _supportLevelExp;
private readonly Dictionary<int, ulong> _weaponLevelExpSsr;
private readonly Dictionary<int, ulong> _supportLevelExpSsr;
private RecycleConfig(
Dictionary<int, RecycleEntry> entries,
List<SupplyTemplate> weaponSupplies,
List<SupplyTemplate> supportSupplies,
Dictionary<int, ulong> weaponLevelExp,
Dictionary<int, ulong> weaponLevelExpSsr,
Dictionary<int, ulong> supportLevelExp,
Dictionary<int, ulong> supportLevelExpSsr)
{
_entries = entries;
_weaponSupplies = weaponSupplies;
_supportSupplies = supportSupplies;
_weaponLevelExp = weaponLevelExp;
_weaponLevelExpSsr = weaponLevelExpSsr;
_supportLevelExp = supportLevelExp;
_supportLevelExpSsr = supportLevelExpSsr;
}
public static RecycleConfig Load()
{
var entries = new Dictionary<int, RecycleEntry>();
foreach (var row in GameData.RecycleData.Values)
{
var fixedRewards = ParseRewards(row.RecycleReward);
var recycleBase = GetUInt(row.RecycleBase);
var recycleRatio = GetDecimal(row.RecycleRatio);
entries[row.ID] = new RecycleEntry(fixedRewards, recycleBase, recycleRatio);
}
var weaponSupplies = new List<SupplyTemplate>();
var supportSupplies = new List<SupplyTemplate>();
foreach (var s in GameData.AllSuppliesData)
{
if (s.ProvideExp == 0) continue;
if (s.Genre == 5 && s.Detail == 2)
weaponSupplies.Add(new SupplyTemplate(s.Genre, s.Detail, s.Particular, s.Level, s.ProvideExp));
else if (s.Genre == 5 && s.Detail == 3)
supportSupplies.Add(new SupplyTemplate(s.Genre, s.Detail, s.Particular, s.Level, s.ProvideExp));
}
weaponSupplies.Sort((a, b) => b.ProvideExp.CompareTo(a.ProvideExp));
supportSupplies.Sort((a, b) => b.ProvideExp.CompareTo(a.ProvideExp));
var weaponLevelExp = BuildLevelExpTable(GameData.UpgradeExpData.Values.Select(x => (x.Lv, x.WeaponNeedExp)));
var weaponLevelExpSsr = BuildLevelExpTable(GameData.UpgradeExpData.Values.Select(x => (x.Lv, x.SSRWeaponNeedExp)));
var supportLevelExp = BuildLevelExpTable(GameData.UpgradeExpData.Values.Select(x => (x.Lv, x.SusNeedExp)));
var supportLevelExpSsr = BuildLevelExpTable(GameData.UpgradeExpData.Values.Select(x => (x.Lv, x.SSRSusNeedExp)));
return new RecycleConfig(entries, weaponSupplies, supportSupplies, weaponLevelExp, weaponLevelExpSsr, supportLevelExp, supportLevelExpSsr);
}
public bool HasConfig(int recycleId) => _entries.ContainsKey(recycleId);
public List<IReadOnlyList<uint>> CalcRewards(BaseGameItemInfo item, int recycleId)
{
if (!_entries.TryGetValue(recycleId, out var entry))
return [];
var rewards = new List<IReadOnlyList<uint>>(entry.FixedRewards);
var expRewards = CalcExpRewards(item, entry);
rewards.AddRange(expRewards);
return rewards;
}
private List<IReadOnlyList<uint>> CalcExpRewards(BaseGameItemInfo item, RecycleEntry entry)
{
if (entry.RecycleRatio == 0) return [];
List<SupplyTemplate> supplies;
Dictionary<int, ulong> levelExp;
if (item.ItemType == ItemTypeEnum.TYPE_WEAPON)
{
supplies = _weaponSupplies;
var color = GetItemColor(item);
levelExp = color == 5 ? _weaponLevelExpSsr : _weaponLevelExp;
}
else if (item.ItemType == ItemTypeEnum.TYPE_SUPPORT)
{
supplies = _supportSupplies;
var color = GetItemColor(item);
levelExp = color == 5 ? _supportLevelExpSsr : _supportLevelExp;
}
else
{
return [];
}
var baseExp = (ulong)entry.RecycleBase;
var levelAccum = levelExp.GetValueOrDefault((int)item.Level);
var totalExp = (ulong)Math.Floor((baseExp + levelAccum + item.Exp) * (double)entry.RecycleRatio);
if (totalExp == 0 || supplies.Count == 0) return [];
var rewards = new List<IReadOnlyList<uint>>();
var remaining = totalExp;
foreach (var supply in supplies)
{
if (remaining == 0) break;
var count = remaining / supply.ProvideExp;
if (count == 0) continue;
remaining -= count * supply.ProvideExp;
rewards.Add([supply.Genre, supply.Detail, supply.Particular, supply.Level, (uint)Math.Min(count, 99999)]);
}
return rewards;
}
private static List<IReadOnlyList<uint>> ParseRewards(JToken? token)
{
if (token == null) return [];
if (token is JArray outerArray)
{
var rewards = new List<IReadOnlyList<uint>>();
foreach (var element in outerArray)
{
if (element is JArray inner && inner.Count >= 4)
{
var reward = inner.Select(x => x.Value<uint>()).ToArray();
if (reward.Length < 5)
reward = [.. reward, 1];
rewards.Add(reward);
}
}
return rewards;
}
return [];
}
private static int GetItemColor(BaseGameItemInfo item)
{
if (item.ItemType == ItemTypeEnum.TYPE_WEAPON)
{
var t = GameData.WeaponData.Values.FirstOrDefault(x =>
GameResourceTemplateId.FromGdpl(x.Genre, x.Detail, x.Particular, x.Level) == item.TemplateId);
return t?.Color ?? 0;
}
if (item.ItemType == ItemTypeEnum.TYPE_SUPPORT)
{
var t = GameData.SupportCardData.FirstOrDefault(x => x.TemplateId == item.TemplateId);
return (int)(t?.Color ?? 0);
}
return 0;
}
private static Dictionary<int, ulong> BuildLevelExpTable(IEnumerable<(int Lv, uint NeedExp)> source)
{
var table = new Dictionary<int, ulong>();
ulong accumulated = 0;
foreach (var (lv, needExp) in source.OrderBy(x => x.Lv))
{
table[lv] = accumulated;
accumulated += needExp;
}
return table;
}
private static uint GetUInt(JToken? token) => token?.Type switch
{
JTokenType.Integer => token.Value<uint>(),
JTokenType.Float => (uint)Math.Max(0, token.Value<decimal>()),
JTokenType.String when uint.TryParse(token.Value<string>(), out var r) => r,
_ => 0
};
private static decimal GetDecimal(JToken? token) => token?.Type switch
{
JTokenType.Integer => token.Value<decimal>(),
JTokenType.Float => token.Value<decimal>(),
JTokenType.String when decimal.TryParse(token.Value<string>(), out var r) => r,
_ => 0m
};
}
internal readonly record struct RecycleEntry(
List<IReadOnlyList<uint>> FixedRewards,
uint RecycleBase,
decimal RecycleRatio);
internal readonly record struct SupplyTemplate(uint Genre, uint Detail, uint Particular, uint Level, uint ProvideExp);
internal sealed class ItemRecycleParam
{
[JsonPropertyName("tbItems")]
public List<int> TbItems { get; set; } = [];
}

View File

@@ -0,0 +1,59 @@
using MikuSB.Database;
using MikuSB.Database.Player;
using MikuSB.GameServer.Game.Player;
using MikuSB.Proto;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace MikuSB.GameServer.Server.CallGS.Handlers.Misc;
[CallGSApi("Adjust_Record")]
public class Adjust_Record : ICallGSHandler
{
private const uint GroupId = 107;
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var req = JsonSerializer.Deserialize<AdjustRecordParam>(param);
if (req == null || req.Type == 0)
{
await CallGSRouter.SendScript(connection, "Adjust_Record", "null");
return;
}
var player = connection.Player!;
var sync = new NtfSyncPlayer();
var attr = GetOrCreateAttr(player, req.Type);
if (attr.Val == 0)
{
attr.Val = 1;
sync.Custom[player.ToPackedAttrKey(GroupId, req.Type)] = 1;
sync.Custom[player.ToShiftedAttrKey(GroupId, req.Type)] = 1;
DatabaseHelper.SaveDatabaseType(player.Data);
}
await CallGSRouter.SendScript(connection, "Adjust_Record", "null", sync);
}
private static PlayerAttr GetOrCreateAttr(PlayerInstance player, uint sid)
{
var attr = player.Data.Attrs.FirstOrDefault(x => x.Gid == GroupId && x.Sid == sid);
if (attr != null)
return attr;
attr = new PlayerAttr
{
Gid = GroupId,
Sid = sid
};
player.Data.Attrs.Add(attr);
return attr;
}
}
internal sealed class AdjustRecordParam
{
[JsonPropertyName("nType")]
public uint Type { get; set; }
}

View File

@@ -0,0 +1,451 @@
using MikuSB.Data;
using MikuSB.Data.Excel;
using MikuSB.Database;
using MikuSB.Database.Inventory;
using MikuSB.Database.Player;
using MikuSB.Enums.Item;
using MikuSB.GameServer.Game.Player;
using MikuSB.Proto;
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
namespace MikuSB.GameServer.Server.CallGS.Handlers.Shop;
[CallGSApi("IBLogic_BuyGoods")]
public class IBLogic_BuyGoods : ICallGSHandler
{
private const uint BuyGroupId = 26;
private const uint RedGroupId = 113;
private const uint CashGroupId = 1;
private const uint BattlePassGroupId = 25;
private const uint BattlePassCurIdSid = 1;
private const uint BattlePassStatusSid = 2;
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var req = JsonSerializer.Deserialize<IbBuyGoodsParam>(param);
var player = connection.Player!;
if (req?.Type == 3 && req.GoodsId > 0 && req.Count > 0)
{
await HandleBattlePassPurchase(connection, player, req);
return;
}
if (req == null ||
req.GoodsId == 0 ||
req.Count == 0 ||
!GameData.IbGoodsData.TryGetValue(req.GoodsId, out var goods))
{
await CallGSRouter.SendScript(connection, "IBLogic_BuyGoods", "{\"sErr\":\"error.BadParam\"}");
return;
}
if (goods.LimitTimes > 0)
{
var buyAttr = GetOrCreateAttr(player, BuyGroupId, req.GoodsId);
if (buyAttr.Val >= goods.LimitTimes)
{
await CallGSRouter.SendScript(connection, "IBLogic_BuyGoods", "{\"sErr\":\"tip.Mall_Limit_Buy\"}");
return;
}
}
var rewardItems = BuildRewardItems(goods, req);
if (rewardItems.Count == 0)
{
await CallGSRouter.SendScript(connection, "IBLogic_BuyGoods", "{\"sErr\":\"error.BadParam\"}");
return;
}
var sync = new NtfSyncPlayer();
foreach (var reward in rewardItems)
await GrantRewardAsync(player, sync, reward);
var buyCountAttr = GetOrCreateAttr(player, BuyGroupId, req.GoodsId);
buyCountAttr.Val += req.Count;
SyncAttr(player, sync, buyCountAttr);
var redAttr = GetOrCreateAttr(player, RedGroupId, req.GoodsId);
if (redAttr.Val == 0)
{
redAttr.Val = 1;
SyncAttr(player, sync, redAttr);
}
DatabaseHelper.SaveDatabaseType(player.Data);
DatabaseHelper.SaveDatabaseType(player.InventoryManager.InventoryData);
DatabaseHelper.SaveDatabaseType(player.CharacterManager.CharacterData);
var responseGoods = new JsonArray();
foreach (var reward in rewardItems)
{
var row = new JsonArray();
foreach (var value in reward)
row.Add((int)value);
responseGoods.Add(row);
}
var rsp = new JsonObject
{
["nGoodsId"] = (int)req.GoodsId,
["tbGoods"] = responseGoods
};
var productId = goods.GetProductId();
if (!string.IsNullOrWhiteSpace(productId))
rsp["sProductId"] = productId;
var cost = req.Index == 2 ? goods.Cost2 : goods.Cost;
if (cost.Count >= 2)
rsp["nTotalPrice"] = (int)cost[1];
await CallGSRouter.SendScript(connection, "IBLogic_BuyGoods", rsp.ToJsonString(), sync);
}
private static async Task HandleBattlePassPurchase(Connection connection, PlayerInstance player, IbBuyGoodsParam req)
{
var sync = new NtfSyncPlayer();
var battlePassId = ResolveCurrentBattlePassId();
if (battlePassId > 0)
{
var curIdAttr = GetOrCreateAttr(player, BattlePassGroupId, BattlePassCurIdSid);
curIdAttr.Val = battlePassId;
SyncAttr(player, sync, curIdAttr);
}
var statusAttr = GetOrCreateAttr(player, BattlePassGroupId, BattlePassStatusSid);
if (statusAttr.Val < 2)
{
statusAttr.Val = 2;
SyncAttr(player, sync, statusAttr);
}
var buyCountAttr = GetOrCreateAttr(player, BuyGroupId, req.GoodsId);
buyCountAttr.Val += req.Count;
SyncAttr(player, sync, buyCountAttr);
var redAttr = GetOrCreateAttr(player, RedGroupId, req.GoodsId);
if (redAttr.Val == 0)
{
redAttr.Val = 1;
SyncAttr(player, sync, redAttr);
}
DatabaseHelper.SaveDatabaseType(player.Data);
var rsp = new JsonObject
{
["nGoodsId"] = (int)req.GoodsId,
["tbGoods"] = new JsonArray()
};
await CallGSRouter.SendScript(connection, "IBLogic_BuyGoods", rsp.ToJsonString(), sync);
}
private static List<List<uint>> BuildRewardItems(IbGoodsExcel goods, IbBuyGoodsParam req)
{
var rewards = new List<List<uint>>();
if (goods.Item.Count >= 4)
rewards.Add(WithCount(goods.Item, req.Count));
if (req.SelectItem1?.Count >= 4)
rewards.Add(WithCount(req.SelectItem1, req.Count));
if (req.SelectItem2?.Count >= 4)
rewards.Add(WithCount(req.SelectItem2, req.Count));
return rewards;
}
private static List<uint> WithCount(IReadOnlyList<uint> item, uint buyCount)
{
var reward = item.Take(5).ToList();
while (reward.Count < 5)
reward.Add(1);
reward[4] = Math.Max(1u, reward[4]) * Math.Max(1u, buyCount);
return reward;
}
private static async Task GrantRewardAsync(PlayerInstance player, NtfSyncPlayer sync, IReadOnlyList<uint> reward)
{
if (reward.Count < 5)
return;
var itemType = (ItemTypeEnum)reward[0];
var detail = reward[1];
var particular = reward[2];
var level = reward[3];
var count = Math.Max(1u, reward[4]);
switch (itemType)
{
case ItemTypeEnum.TYPE_CARD:
for (var i = 0u; i < count; i++)
{
var character = await player.CharacterManager.AddCharacter(itemType, detail, particular, level, sendPacket: false);
if (character != null)
sync.Items.Add(character.ToProto());
}
break;
case ItemTypeEnum.TYPE_WEAPON:
for (var i = 0u; i < count; i++)
{
var weapon = await player.InventoryManager.AddWeaponItem(itemType, detail, particular, level, sendPacket: false);
if (weapon != null)
sync.Items.Add(weapon.ToProto());
}
break;
case ItemTypeEnum.TYPE_SUPPORT:
for (var i = 0u; i < count; i++)
{
var support = await player.InventoryManager.AddSupportCardItem(detail, particular, level, sendPacket: false);
if (support != null)
sync.Items.Add(support.ToProto());
}
break;
case ItemTypeEnum.TYPE_SUPPLIES:
{
var templateId = (uint)GameResourceTemplateId.FromGdpl(reward[0], detail, particular, level);
if (!GameData.SuppliesData.TryGetValue(templateId, out var supplies))
break;
var item = await player.InventoryManager.AddSuppliesItem(supplies, count, sendPacket: false);
if (item != null)
sync.Items.Add(item.ToProto());
break;
}
case ItemTypeEnum.TYPE_USEABLE:
{
if (!TryGrantCashBox(player, sync, detail, particular, level, count))
{
var item = AddOtherItem(player.InventoryManager.InventoryData, reward[0], detail, particular, level, count);
if (item != null)
sync.Items.Add(item.ToProto());
}
break;
}
case ItemTypeEnum.TYPE_WEAPON_PART:
for (var i = 0u; i < count; i++)
{
var item = await player.InventoryManager.AddWeaponPartItem(itemType, detail, particular, level, sendPacket: false);
if (item != null)
sync.Items.Add(item.ToProto());
}
break;
case ItemTypeEnum.TYPE_CARD_SKIN:
for (var i = 0u; i < count; i++)
{
var item = await player.InventoryManager.AddSkinItem(itemType, detail, particular, level, sendPacket: false);
if (item != null)
sync.Items.Add(item.ToProto());
}
break;
case ItemTypeEnum.TYPE_HOUSE:
for (var i = 0u; i < count; i++)
{
var item = await player.InventoryManager.AddHouseFurnitureItem(itemType, detail, particular, level, sendPacket: false);
if (item != null)
sync.Items.Add(item.ToProto());
}
break;
case ItemTypeEnum.TYPE_PROFILE:
case ItemTypeEnum.TYPE_FRAME:
case ItemTypeEnum.TYPE_BADGE:
case ItemTypeEnum.TYPE_COVER:
case ItemTypeEnum.TYPE_NAMECARD:
case ItemTypeEnum.TYPE_EXPRESSION:
case ItemTypeEnum.TYPE_BUBBLE:
case ItemTypeEnum.TYPE_ANALYST:
for (var i = 0u; i < count; i++)
{
var item = await player.InventoryManager.AddProfileItem(itemType, detail, particular, level, sendPacket: false);
if (item != null)
sync.Items.Add(item.ToProto());
}
break;
case ItemTypeEnum.TYPE_WEAPON_SKIN:
for (var i = 0u; i < count; i++)
{
var item = await player.InventoryManager.AddWeaponSkinItem(itemType, detail, particular, level, sendPacket: false);
if (item != null)
sync.Items.Add(item.ToProto());
}
break;
case ItemTypeEnum.TYPE_MANIFESTATION:
for (var i = 0u; i < count; i++)
{
var item = await player.InventoryManager.AddManifestationItem(itemType, detail, particular, level, sendPacket: false);
if (item != null)
sync.Items.Add(item.ToProto());
}
break;
case ItemTypeEnum.TYPE_CARD_SKIN_PART:
for (var i = 0u; i < count; i++)
{
var item = await player.InventoryManager.AddSkinPartItem(itemType, detail, particular, level, sendPacket: false);
if (item != null)
sync.Items.Add(item.ToProto());
}
break;
case ItemTypeEnum.TYPE_AR:
for (var i = 0u; i < count; i++)
{
var item = await player.InventoryManager.AddArItem(itemType, detail, particular, level, sendPacket: false);
if (item != null)
sync.Items.Add(item.ToProto());
}
break;
case ItemTypeEnum.TYPE_CALL:
for (var i = 0u; i < count; i++)
{
var item = await player.InventoryManager.AddCallItem(itemType, detail, particular, level, sendPacket: false);
if (item != null)
sync.Items.Add(item.ToProto());
}
break;
}
}
private static BaseGameItemInfo? AddOtherItem(InventoryData inventory, uint genre, uint detail, uint particular, uint level, uint count)
{
var templateId = (uint)GameResourceTemplateId.FromGdpl(genre, detail, particular, level);
if (!GameData.OtherItemData.TryGetValue(templateId, out var otherItem))
return null;
var maxCount = otherItem.GMnum > 0 ? otherItem.GMnum : 99999u;
var existing = inventory.Items.Values.FirstOrDefault(x => x.TemplateId == templateId);
if (existing != null)
{
existing.ItemCount = Math.Min(existing.ItemCount + count, maxCount);
return existing;
}
var item = new BaseGameItemInfo
{
TemplateId = templateId,
UniqueId = inventory.NextUniqueUid++,
ItemType = ItemTypeEnum.TYPE_USEABLE,
ItemCount = Math.Min(count, maxCount)
};
inventory.Items[item.UniqueId] = item;
return item;
}
private static bool TryGrantCashBox(PlayerInstance player, NtfSyncPlayer sync, uint detail, uint particular, uint level, uint count)
{
var templateId = (uint)GameResourceTemplateId.FromGdpl((uint)ItemTypeEnum.TYPE_USEABLE, detail, particular, level);
if (!GameData.OtherItemData.TryGetValue(templateId, out var otherItem))
return false;
uint moneyType = otherItem.LuaType switch
{
"money_box" => 1,
"gold_box" => 2,
"silver_box" => 3,
"vigor_box" => 4,
_ => 0
};
if (moneyType == 0 || otherItem.Param1 == 0)
return false;
var amount = checked(otherItem.Param1 * count);
var sid = moneyType * 2 + 1;
var attr = GetOrCreateAttr(player, CashGroupId, sid);
attr.Val += amount;
SyncAttr(player, sync, attr);
if (moneyType == 1)
{
foreach (var (key, value) in player.BuildMoneySync())
sync.Money[key] = value;
}
return true;
}
private static uint ResolveCurrentBattlePassId()
{
var now = DateTime.Now;
var parsed = GameData.BattlePassTimeData.Values
.Select(x => new
{
Config = x,
Start = ParseConfigTime(x.StartTime),
End = ParseConfigTime(x.EndTime)
})
.Where(x => x.Start.HasValue && x.End.HasValue)
.OrderBy(x => x.Start)
.ToList();
var current = parsed.FirstOrDefault(x => x.Start <= now && now < x.End);
if (current != null)
return current.Config.Id;
var latestStarted = parsed.LastOrDefault(x => x.Start <= now && x.End > x.Start);
return latestStarted?.Config.Id ?? 0;
}
private static DateTime? ParseConfigTime(string? raw)
{
if (string.IsNullOrWhiteSpace(raw))
return null;
var normalized = raw.Trim().Trim('[', ']');
if (normalized.Length != 12)
return null;
return DateTime.TryParseExact(
normalized,
"yyyyMMddHHmm",
CultureInfo.InvariantCulture,
DateTimeStyles.None,
out var value)
? value
: null;
}
private static PlayerAttr GetOrCreateAttr(PlayerInstance player, uint gid, uint sid)
{
var attr = player.Data.Attrs.FirstOrDefault(x => x.Gid == gid && x.Sid == sid);
if (attr != null)
return attr;
attr = new PlayerAttr
{
Gid = gid,
Sid = sid
};
player.Data.Attrs.Add(attr);
return attr;
}
private static void SyncAttr(PlayerInstance player, NtfSyncPlayer sync, PlayerAttr attr)
{
sync.Custom[player.ToPackedAttrKey(attr.Gid, attr.Sid)] = attr.Val;
sync.Custom[player.ToShiftedAttrKey(attr.Gid, attr.Sid)] = attr.Val;
}
}
internal sealed class IbBuyGoodsParam
{
[JsonPropertyName("nType")]
public int Type { get; set; }
[JsonPropertyName("nGoodsId")]
public uint GoodsId { get; set; }
[JsonPropertyName("nCount")]
public uint Count { get; set; }
[JsonPropertyName("nIndex")]
public int Index { get; set; }
[JsonPropertyName("tbSelectItem1")]
public List<uint>? SelectItem1 { get; set; }
[JsonPropertyName("tbSelectItem2")]
public List<uint>? SelectItem2 { get; set; }
}

View File

@@ -0,0 +1,71 @@
using MikuSB.Database;
using MikuSB.Database.Player;
using MikuSB.GameServer.Game.Player;
using MikuSB.Proto;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace MikuSB.GameServer.Server.CallGS.Handlers.Shop;
[CallGSApi("IBLogic_GoodsRedDot")]
public class IBLogic_GoodsRedDot : ICallGSHandler
{
private const uint RedGroupId = 113;
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var req = JsonSerializer.Deserialize<IbGoodsRedDotParam>(param);
if (req?.GoodsIds == null || req.GoodsIds.Count == 0)
{
await CallGSRouter.SendScript(connection, "IBLogic_GoodsRedDot", "null");
return;
}
var player = connection.Player!;
var sync = new NtfSyncPlayer();
var changed = false;
foreach (var goodsId in req.GoodsIds.Where(x => x > 0).Distinct())
{
var attr = GetOrCreateAttr(player, RedGroupId, goodsId);
if (attr.Val > 0)
continue;
attr.Val = 1;
SyncAttr(player, sync, attr);
changed = true;
}
if (changed)
DatabaseHelper.SaveDatabaseType(player.Data);
await CallGSRouter.SendScript(connection, "IBLogic_GoodsRedDot", "null", sync);
}
private static PlayerAttr GetOrCreateAttr(PlayerInstance player, uint gid, uint sid)
{
var attr = player.Data.Attrs.FirstOrDefault(x => x.Gid == gid && x.Sid == sid);
if (attr != null)
return attr;
attr = new PlayerAttr
{
Gid = gid,
Sid = sid
};
player.Data.Attrs.Add(attr);
return attr;
}
private static void SyncAttr(PlayerInstance player, NtfSyncPlayer sync, PlayerAttr attr)
{
sync.Custom[player.ToPackedAttrKey(attr.Gid, attr.Sid)] = attr.Val;
sync.Custom[player.ToShiftedAttrKey(attr.Gid, attr.Sid)] = attr.Val;
}
}
internal sealed class IbGoodsRedDotParam
{
[JsonPropertyName("tbList")]
public List<uint> GoodsIds { get; set; } = [];
}

View File

@@ -11,6 +11,14 @@ namespace MikuSB.GameServer.Server.CallGS.Handlers.VirCapture;
[CallGSApi("VirCaptureLevel_SaveCapture")]
public class VirCaptureLevel_SaveCapture : ICallGSHandler
{
private const uint VirCaptureGroupId = 128;
private const uint CurExpSid = 2;
private const uint CurLevelSid = 3;
private const uint BagNumSid = 5;
private const uint DailyExpSid = 8;
private const uint ColorMaxStartSid = 11;
private const uint RikiGroupId = 135;
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var req = JsonSerializer.Deserialize<VirCaptureSaveCaptureParam>(param);
@@ -50,6 +58,7 @@ public class VirCaptureLevel_SaveCapture : ICallGSHandler
sync.Items.Add(grantedItem.ToProto());
SyncVirCaptureCounters(player, grantedItem.TemplateId, sync);
ApplyCaptureExp(player, grantedItem.TemplateId, sync);
DatabaseHelper.SaveDatabaseType(player.Data);
DatabaseHelper.SaveDatabaseType(player.InventoryManager.InventoryData);
@@ -68,17 +77,22 @@ public class VirCaptureLevel_SaveCapture : ICallGSHandler
private static void SyncVirCaptureCounters(MikuSB.GameServer.Game.Player.PlayerInstance player, ulong templateId, NtfSyncPlayer sync)
{
var bagCount = (uint)player.InventoryManager.InventoryData.Items.Values.Count(x => x.ItemType == ItemTypeEnum.TYPE_MONSTER_CARD);
VirCaptureStateHelper.SetUnsignedAttr(player, 5, bagCount, sync);
VirCaptureStateHelper.SetUnsignedAttr(player, BagNumSid, bagCount, sync);
if (!GameData.MonsterCardData.TryGetValue(templateId, out var monsterCard) || monsterCard.RikiId == 0)
return;
var rikiAttr = player.Data.Attrs.FirstOrDefault(x => x.Gid == 135 && x.Sid == monsterCard.RikiId);
var colorSid = ColorMaxStartSid + Math.Max(0u, monsterCard.Color - 1u);
var colorAttr = player.Data.Attrs.FirstOrDefault(x => x.Gid == VirCaptureGroupId && x.Sid == colorSid);
var nextColorValue = (colorAttr?.Val ?? 0) + 1;
VirCaptureStateHelper.SetUnsignedAttr(player, colorSid, nextColorValue, sync);
var rikiAttr = player.Data.Attrs.FirstOrDefault(x => x.Gid == RikiGroupId && x.Sid == monsterCard.RikiId);
if (rikiAttr == null)
{
rikiAttr = new Database.Player.PlayerAttr
{
Gid = 135,
Gid = RikiGroupId,
Sid = monsterCard.RikiId,
Val = 0
};
@@ -86,8 +100,115 @@ public class VirCaptureLevel_SaveCapture : ICallGSHandler
}
rikiAttr.Val += 1;
sync.Custom[player.ToPackedAttrKey(135, monsterCard.RikiId)] = rikiAttr.Val;
sync.Custom[player.ToShiftedAttrKey(135, monsterCard.RikiId)] = rikiAttr.Val;
sync.Custom[player.ToPackedAttrKey(RikiGroupId, monsterCard.RikiId)] = rikiAttr.Val;
sync.Custom[player.ToShiftedAttrKey(RikiGroupId, monsterCard.RikiId)] = rikiAttr.Val;
}
private static void ApplyCaptureExp(MikuSB.GameServer.Game.Player.PlayerInstance player, ulong templateId, NtfSyncPlayer sync)
{
if (!GameData.MonsterCardData.TryGetValue(templateId, out var monsterCard) || monsterCard.Exp == 0)
return;
var curLevelAttr = GetOrCreateVirCaptureAttr(player, CurLevelSid);
var curExpAttr = GetOrCreateVirCaptureAttr(player, CurExpSid);
var dailyExpAttr = GetOrCreateVirCaptureAttr(player, DailyExpSid);
var maxLevel = GameData.VirCaptureLevelListData.Count == 0 ? 1u : GameData.VirCaptureLevelListData.Keys.Max();
var curLevel = Math.Max(1u, curLevelAttr.Val);
if (curLevel >= maxLevel)
return;
var baseExp = monsterCard.Exp;
if (GameData.VirCaptureLevelListData.TryGetValue(curLevel, out var currentLevelCfg) && currentLevelCfg.ExpUp > 1d)
baseExp = (uint)Math.Floor(baseExp * currentLevelCfg.ExpUp);
var maxDailyExp = ResolveCurrentAct(player)?.MaxExp ?? 0u;
if (maxDailyExp > 0 && dailyExpAttr.Val >= maxDailyExp)
return;
var gainExp = baseExp;
if (maxDailyExp > 0)
gainExp = Math.Min(gainExp, maxDailyExp - dailyExpAttr.Val);
if (gainExp == 0)
return;
dailyExpAttr.Val += gainExp;
SyncVirCaptureAttr(player, DailyExpSid, dailyExpAttr.Val, sync);
var pendingExp = curExpAttr.Val + gainExp;
while (GameData.VirCaptureLevelListData.TryGetValue(curLevel, out var levelCfg) && curLevel < maxLevel)
{
if (pendingExp < levelCfg.Exp)
break;
pendingExp -= levelCfg.Exp;
curLevel++;
}
curLevelAttr.Val = curLevel;
curExpAttr.Val = curLevel >= maxLevel
? GameData.VirCaptureLevelListData.GetValueOrDefault(maxLevel)?.Exp ?? pendingExp
: pendingExp;
SyncVirCaptureAttr(player, CurLevelSid, curLevelAttr.Val, sync);
SyncVirCaptureAttr(player, CurExpSid, curExpAttr.Val, sync);
}
private static Database.Player.PlayerAttr GetOrCreateVirCaptureAttr(MikuSB.GameServer.Game.Player.PlayerInstance player, uint sid)
{
var attr = player.Data.Attrs.FirstOrDefault(x => x.Gid == VirCaptureGroupId && x.Sid == sid);
if (attr != null)
return attr;
attr = new Database.Player.PlayerAttr
{
Gid = VirCaptureGroupId,
Sid = sid,
Val = 0
};
player.Data.Attrs.Add(attr);
return attr;
}
private static void SyncVirCaptureAttr(MikuSB.GameServer.Game.Player.PlayerInstance player, uint sid, uint value, NtfSyncPlayer sync)
{
sync.Custom[player.ToPackedAttrKey(VirCaptureGroupId, sid)] = value;
sync.Custom[player.ToShiftedAttrKey(VirCaptureGroupId, sid)] = value;
}
private static MikuSB.Data.Excel.VirCaptureTimeExcel? ResolveCurrentAct(MikuSB.GameServer.Game.Player.PlayerInstance player)
{
var actId = player.Data.Attrs.FirstOrDefault(x => x.Gid == VirCaptureGroupId && x.Sid == 1)?.Val ?? 0;
if (actId > 0 && GameData.VirCaptureTimeData.TryGetValue(actId, out var act))
return act;
var now = DateTime.Now;
return GameData.VirCaptureTimeData.Values
.Select(x => new { Config = x, Start = ParseConfigTime(x.StartTime), End = ParseConfigTime(x.EndTime) })
.Where(x => x.Start.HasValue && x.End.HasValue && x.Start <= now && now < x.End)
.OrderBy(x => x.Start)
.Select(x => x.Config)
.FirstOrDefault();
}
private static DateTime? ParseConfigTime(string? raw)
{
if (string.IsNullOrWhiteSpace(raw))
return null;
var normalized = raw.Trim().Trim('[', ']');
if (normalized.Length != 12)
return null;
return DateTime.TryParseExact(
normalized,
"yyyyMMddHHmm",
System.Globalization.CultureInfo.InvariantCulture,
System.Globalization.DateTimeStyles.None,
out var value)
? value
: null;
}
}

View File

@@ -0,0 +1,79 @@
using MikuSB.Data;
using MikuSB.Database.Player;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace MikuSB.GameServer.Server.CallGS.Handlers.VirCapture;
[CallGSApi("VirCaptureTower_EnterLevel")]
public class VirCaptureTower_EnterLevel : ICallGSHandler
{
private const uint LaunchPassGroupId = 22;
private const uint VirCaptureGroupId = 128;
private const uint VirCaptureLevelSid = 3;
private static readonly Random Random = new();
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var req = JsonSerializer.Deserialize<VirCaptureTowerEnterLevelParam>(param);
if (req == null || req.LevelId <= 0 || req.TeamId <= 0)
{
await CallGSRouter.SendScript(connection, "VirCaptureTower_EnterLevel", "{\"sErr\":\"error.BadParam\"}");
return;
}
if (!GameData.VirCaptureTowerData.TryGetValue((uint)req.LevelId, out var levelCfg))
{
await CallGSRouter.SendScript(connection, "VirCaptureTower_EnterLevel", "{\"sErr\":\"error.BadParam\"}");
return;
}
var player = connection.Player!;
if (!CheckConditions(player.Data, levelCfg.Condition))
{
await CallGSRouter.SendScript(connection, "VirCaptureTower_EnterLevel", "{\"sErr\":\"tip.LevelLocked\"}");
return;
}
await CallGSRouter.SendScript(connection, "VirCaptureTower_EnterLevel", $"{{\"nSeed\":{Random.Next(1, 1_000_000_000)}}}");
}
private static bool CheckConditions(PlayerGameData data, IReadOnlyDictionary<int, uint> conditions)
{
foreach (var (key, value) in conditions)
{
switch (key)
{
case 1:
if (data.Level < value)
return false;
break;
case 2:
{
var pass = data.Attrs.FirstOrDefault(x => x.Gid == LaunchPassGroupId && x.Sid == value)?.Val ?? 0;
if (pass == 0)
return false;
break;
}
case 20:
{
var virLevel = data.Attrs.FirstOrDefault(x => x.Gid == VirCaptureGroupId && x.Sid == VirCaptureLevelSid)?.Val ?? 0;
if (virLevel < value)
return false;
break;
}
}
}
return true;
}
}
internal sealed class VirCaptureTowerEnterLevelParam
{
[JsonPropertyName("nID")]
public int LevelId { get; set; }
[JsonPropertyName("nTeamID")]
public int TeamId { get; set; }
}

View File

@@ -0,0 +1,94 @@
using MikuSB.Database;
using MikuSB.Database.Player;
using MikuSB.GameServer.Game.Player;
using MikuSB.Proto;
using MikuSB.Util;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
namespace MikuSB.GameServer.Server.CallGS.Handlers.VirCapture;
[CallGSApi("VirCaptureTower_LevelSettlement")]
public class VirCaptureTower_LevelSettlement : ICallGSHandler
{
private const uint LaunchLevelStateGroupId = 21;
private const uint LaunchPassGroupId = 22;
private const uint PassedFlagBit = 1u << 8;
private static readonly Logger Logger = new("VirCaptureTower");
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var (response, sync) = HandleSettlement(connection.Player!, JsonNode.Parse(param));
await CallGSRouter.SendScript(connection, "VirCaptureTower_LevelSettlement", response.ToJsonString(), sync);
}
public static (JsonNode Response, NtfSyncPlayer Sync) HandleSettlement(PlayerInstance player, JsonNode? tbParam)
{
var req = tbParam?.Deserialize<VirCaptureTowerSettlementParam>();
if (req == null || req.LevelId == 0)
{
Logger.Error($"Invalid vircapture tower settlement payload: {tbParam?.ToJsonString() ?? "null"}");
return (new JsonObject { ["sErr"] = "error.BadParam" }, new NtfSyncPlayer());
}
var sync = new NtfSyncPlayer();
var levelStateAttr = GetOrCreateAttr(player.Data, LaunchLevelStateGroupId, (uint)req.LevelId);
levelStateAttr.Val |= MergeStarMask(req.StarMask) | PassedFlagBit;
SyncAttr(sync, player, levelStateAttr);
var passAttr = GetOrCreateAttr(player.Data, LaunchPassGroupId, (uint)req.LevelId);
passAttr.Val = Math.Max(1u, passAttr.Val + 1);
SyncAttr(sync, player, passAttr);
Logger.Info(
$"VirCaptureTower settlement saved. uid={player.Uid} levelId={req.LevelId} starMask={req.StarMask} " +
$"levelStateVal={levelStateAttr.Val} passVal={passAttr.Val}");
DatabaseHelper.SaveDatabaseType(player.Data);
return (new JsonObject(), sync);
}
private static uint MergeStarMask(int starMask)
{
uint result = 0;
for (var i = 0; i < 3; i++)
{
if (((starMask >> i) & 1) != 0)
result |= 1u << i;
}
return result;
}
private static PlayerAttr GetOrCreateAttr(PlayerGameData data, uint gid, uint sid)
{
var attr = data.Attrs.FirstOrDefault(x => x.Gid == gid && x.Sid == sid);
if (attr != null)
return attr;
attr = new PlayerAttr
{
Gid = gid,
Sid = sid
};
data.Attrs.Add(attr);
return attr;
}
private static void SyncAttr(NtfSyncPlayer sync, PlayerInstance player, PlayerAttr attr)
{
sync.Custom[player.ToPackedAttrKey(attr.Gid, attr.Sid)] = attr.Val;
sync.Custom[player.ToShiftedAttrKey(attr.Gid, attr.Sid)] = attr.Val;
}
}
internal sealed class VirCaptureTowerSettlementParam
{
[JsonPropertyName("nID")]
public int LevelId { get; set; }
[JsonPropertyName("nStar")]
public int StarMask { get; set; }
}

View File

@@ -0,0 +1,292 @@
using MikuSB.Data;
using MikuSB.Data.Excel;
using MikuSB.Database;
using MikuSB.Database.Inventory;
using MikuSB.Database.Player;
using MikuSB.Enums.Item;
using MikuSB.GameServer.Game.Player;
using MikuSB.Proto;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
namespace MikuSB.GameServer.Server.CallGS.Handlers.VirCapture;
[CallGSApi("VirCapture_GetLevelAward")]
public class VirCapture_GetLevelAward : ICallGSHandler
{
private const uint VirCaptureGroupId = 128;
private const uint CurLevelSid = 3;
private const uint LevelAwardFlagStartSid = 101;
private const uint LevelAwardFlagEndSid = 120;
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var player = connection.Player!;
var req = JsonSerializer.Deserialize<VirCaptureGetLevelAwardParam>(param);
if (req == null || req.IdList == null || req.IdList.Count == 0)
{
await CallGSRouter.SendScript(connection, "VirCapture_GetLevelAward", "{\"tbAwardList\":[]}");
return;
}
var curLevel = player.Data.Attrs.FirstOrDefault(x => x.Gid == VirCaptureGroupId && x.Sid == CurLevelSid)?.Val ?? 0;
var requestedLevels = req.IdList
.Where(x => x > 0)
.Select(x => (uint)x)
.Distinct()
.OrderBy(x => x)
.ToList();
var claimLevels = requestedLevels
.Where(level => level <= curLevel && CanClaimLevel(player.Data, level))
.ToList();
var sync = new NtfSyncPlayer();
var responseAwards = new JsonArray();
foreach (var level in claimLevels)
{
if (!GameData.VirCaptureLevelListData.TryGetValue(level, out var levelCfg) ||
levelCfg.Rewards.Count == 0)
{
continue;
}
SetClaimed(player, sync, level);
foreach (var reward in levelCfg.Rewards)
{
if (reward.Count < 5)
continue;
await GrantRewardAsync(player, sync, reward);
responseAwards.Add(new JsonArray(
(int)reward[0],
(int)reward[1],
(int)reward[2],
(int)reward[3],
(int)reward[4]));
}
}
DatabaseHelper.SaveDatabaseType(player.Data);
DatabaseHelper.SaveDatabaseType(player.InventoryManager.InventoryData);
DatabaseHelper.SaveDatabaseType(player.CharacterManager.CharacterData);
var rsp = new JsonObject
{
["tbAwardList"] = responseAwards
};
await CallGSRouter.SendScript(connection, "VirCapture_GetLevelAward", rsp.ToJsonString(), sync);
}
private static bool CanClaimLevel(PlayerGameData data, uint level)
{
var sid = GetLevelAwardSid(level);
if (sid < LevelAwardFlagStartSid || sid > LevelAwardFlagEndSid)
return false;
var pos = GetLevelAwardBit(level);
var attr = data.Attrs.FirstOrDefault(x => x.Gid == VirCaptureGroupId && x.Sid == sid);
return ((attr?.Val ?? 0) & (1u << pos)) == 0;
}
private static void SetClaimed(PlayerInstance player, NtfSyncPlayer sync, uint level)
{
var sid = GetLevelAwardSid(level);
var pos = GetLevelAwardBit(level);
var attr = GetOrCreateAttr(player.Data, VirCaptureGroupId, sid);
attr.Val |= 1u << pos;
sync.Custom[player.ToPackedAttrKey(VirCaptureGroupId, sid)] = attr.Val;
sync.Custom[player.ToShiftedAttrKey(VirCaptureGroupId, sid)] = attr.Val;
}
private static uint GetLevelAwardSid(uint level) => LevelAwardFlagStartSid + (level / 30);
private static int GetLevelAwardBit(uint level) => (int)(level % 30);
private static PlayerAttr GetOrCreateAttr(PlayerGameData data, uint gid, uint sid)
{
var attr = data.Attrs.FirstOrDefault(x => x.Gid == gid && x.Sid == sid);
if (attr != null)
return attr;
attr = new PlayerAttr
{
Gid = gid,
Sid = sid,
Val = 0
};
data.Attrs.Add(attr);
return attr;
}
private static async Task GrantRewardAsync(PlayerInstance player, NtfSyncPlayer sync, IReadOnlyList<uint> reward)
{
var itemType = (ItemTypeEnum)reward[0];
var detail = reward[1];
var particular = reward[2];
var level = reward[3];
var count = Math.Max(1u, reward[4]);
switch (itemType)
{
case ItemTypeEnum.TYPE_CARD:
for (var i = 0u; i < count; i++)
{
var character = await player.CharacterManager.AddCharacter(itemType, detail, particular, level, sendPacket: false);
if (character != null)
sync.Items.Add(character.ToProto());
}
break;
case ItemTypeEnum.TYPE_WEAPON:
for (var i = 0u; i < count; i++)
{
var weapon = await player.InventoryManager.AddWeaponItem(itemType, detail, particular, level, sendPacket: false);
if (weapon != null)
sync.Items.Add(weapon.ToProto());
}
break;
case ItemTypeEnum.TYPE_SUPPORT:
for (var i = 0u; i < count; i++)
{
var support = await player.InventoryManager.AddSupportCardItem(detail, particular, level, sendPacket: false);
if (support != null)
sync.Items.Add(support.ToProto());
}
break;
case ItemTypeEnum.TYPE_SUPPLIES:
{
var templateId = (uint)GameResourceTemplateId.FromGdpl(reward[0], detail, particular, level);
if (GameData.SuppliesData.TryGetValue(templateId, out var supplies))
{
var item = await player.InventoryManager.AddSuppliesItem(supplies, count, sendPacket: false);
if (item != null)
sync.Items.Add(item.ToProto());
}
break;
}
case ItemTypeEnum.TYPE_USEABLE:
{
var item = AddOtherItem(player.InventoryManager.InventoryData, reward[0], detail, particular, level, count);
if (item != null)
sync.Items.Add(item.ToProto());
break;
}
case ItemTypeEnum.TYPE_WEAPON_PART:
for (var i = 0u; i < count; i++)
{
var item = await player.InventoryManager.AddWeaponPartItem(itemType, detail, particular, level, sendPacket: false);
if (item != null)
sync.Items.Add(item.ToProto());
}
break;
case ItemTypeEnum.TYPE_CARD_SKIN:
for (var i = 0u; i < count; i++)
{
var item = await player.InventoryManager.AddSkinItem(itemType, detail, particular, level, sendPacket: false);
if (item != null)
sync.Items.Add(item.ToProto());
}
break;
case ItemTypeEnum.TYPE_HOUSE:
for (var i = 0u; i < count; i++)
{
var item = await player.InventoryManager.AddHouseFurnitureItem(itemType, detail, particular, level, sendPacket: false);
if (item != null)
sync.Items.Add(item.ToProto());
}
break;
case ItemTypeEnum.TYPE_PROFILE:
case ItemTypeEnum.TYPE_FRAME:
case ItemTypeEnum.TYPE_BADGE:
case ItemTypeEnum.TYPE_COVER:
case ItemTypeEnum.TYPE_NAMECARD:
case ItemTypeEnum.TYPE_EXPRESSION:
case ItemTypeEnum.TYPE_BUBBLE:
case ItemTypeEnum.TYPE_ANALYST:
for (var i = 0u; i < count; i++)
{
var item = await player.InventoryManager.AddProfileItem(itemType, detail, particular, level, sendPacket: false);
if (item != null)
sync.Items.Add(item.ToProto());
}
break;
case ItemTypeEnum.TYPE_WEAPON_SKIN:
for (var i = 0u; i < count; i++)
{
var item = await player.InventoryManager.AddWeaponSkinItem(itemType, detail, particular, level, sendPacket: false);
if (item != null)
sync.Items.Add(item.ToProto());
}
break;
case ItemTypeEnum.TYPE_MANIFESTATION:
for (var i = 0u; i < count; i++)
{
var item = await player.InventoryManager.AddManifestationItem(itemType, detail, particular, level, sendPacket: false);
if (item != null)
sync.Items.Add(item.ToProto());
}
break;
case ItemTypeEnum.TYPE_CARD_SKIN_PART:
for (var i = 0u; i < count; i++)
{
var item = await player.InventoryManager.AddSkinPartItem(itemType, detail, particular, level, sendPacket: false);
if (item != null)
sync.Items.Add(item.ToProto());
}
break;
case ItemTypeEnum.TYPE_AR:
for (var i = 0u; i < count; i++)
{
var item = await player.InventoryManager.AddArItem(itemType, detail, particular, level, sendPacket: false);
if (item != null)
sync.Items.Add(item.ToProto());
}
break;
case ItemTypeEnum.TYPE_CALL:
for (var i = 0u; i < count; i++)
{
var item = await player.InventoryManager.AddCallItem(itemType, detail, particular, level, sendPacket: false);
if (item != null)
sync.Items.Add(item.ToProto());
}
break;
}
}
private static BaseGameItemInfo? AddOtherItem(InventoryData inventory, uint genre, uint detail, uint particular, uint level, uint count)
{
var templateId = (uint)GameResourceTemplateId.FromGdpl(genre, detail, particular, level);
if (!GameData.OtherItemData.TryGetValue(templateId, out var otherItem))
return null;
var maxCount = otherItem.GMnum > 0 ? otherItem.GMnum : 99999u;
var existing = inventory.Items.Values.FirstOrDefault(x => x.TemplateId == templateId);
if (existing != null)
{
existing.ItemCount = Math.Min(existing.ItemCount + count, maxCount);
return existing;
}
var item = new BaseGameItemInfo
{
TemplateId = templateId,
UniqueId = inventory.NextUniqueUid++,
ItemType = ItemTypeEnum.TYPE_USEABLE,
ItemCount = Math.Min(count, maxCount)
};
inventory.Items[item.UniqueId] = item;
return item;
}
}
internal sealed class VirCaptureGetLevelAwardParam
{
[JsonPropertyName("nId")]
public int ActId { get; set; }
[JsonPropertyName("tbIdList")]
public List<int> IdList { get; set; } = [];
}

View File

@@ -0,0 +1,183 @@
using MikuSB.Data;
using MikuSB.Database;
using MikuSB.Database.Inventory;
using MikuSB.Proto;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace MikuSB.GameServer.Server.CallGS.Handlers.Weapon;
[CallGSApi("Weapon_OneKeyToMax")]
public class Weapon_OneKeyToMax : ICallGSHandler
{
private const uint MaxBreak = 6;
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var player = connection.Player!;
var req = JsonSerializer.Deserialize<OneKeyToMaxParam>(param);
if (req == null || req.Id <= 0 || req.TbBreakUpgradeMat == null || req.TbBreakUpgradeMat.Count == 0)
{
await CallGSRouter.SendScript(connection, "Weapon_OneKeyToMax", "{\"sErr\":\"error.BadParam\"}");
return;
}
var weapon = player.InventoryManager.GetWeaponItem((uint)req.Id);
if (weapon == null)
{
await CallGSRouter.SendScript(connection, "Weapon_OneKeyToMax", "{\"sErr\":\"error.BadParam\"}");
return;
}
var config = WeaponUpgradeConfig.Load();
if (!config.TryGetWeaponTemplate(weapon.TemplateId, out var targetTemplate))
{
await CallGSRouter.SendScript(connection, "Weapon_OneKeyToMax", "{\"sErr\":\"error.BadParam\"}");
return;
}
var inventory = player.InventoryManager.InventoryData;
var equippedWeaponIds = player.CharacterManager.CharacterData.Characters
.Select(x => x.WeaponUniqueId)
.Where(x => x != 0)
.ToHashSet();
// Validate all materials upfront before making any changes
foreach (var stage in req.TbBreakUpgradeMat)
{
if (stage == null || stage.Count < 3) continue;
var matList = stage[1].Deserialize<List<List<int>>>();
if (matList == null) continue;
foreach (var entry in matList)
{
if (entry == null || entry.Count < 2) continue;
var itemId = (uint)Math.Max(0, entry[0]);
var count = (uint)Math.Max(0, entry[1]);
if (itemId == 0 || count == 0) continue;
if (itemId == weapon.UniqueId)
{
await CallGSRouter.SendScript(connection, "Weapon_OneKeyToMax", "{\"sErr\":\"tip.material_not_enough\"}");
return;
}
var material = FindInventoryItem(inventory, itemId);
if (material == null || material.ItemCount < count)
{
await CallGSRouter.SendScript(connection, "Weapon_OneKeyToMax", "{\"sErr\":\"tip.material_not_enough\"}");
return;
}
if (material is GameWeaponInfo materialWeapon &&
(materialWeapon.EquipAvatarId != 0 || equippedWeaponIds.Contains(materialWeapon.UniqueId)))
{
await CallGSRouter.SendScript(connection, "Weapon_OneKeyToMax", "{\"sErr\":\"tip.material_not_enough\"}");
return;
}
}
}
var syncItems = new List<Item>();
var weaponLevel = weapon.Level == 0 ? 1u : weapon.Level;
weapon.Level = weaponLevel;
// Process each break stage
foreach (var stage in req.TbBreakUpgradeMat)
{
if (stage == null || stage.Count < 3) continue;
var matList = stage[1].Deserialize<List<List<int>>>();
var doBreak = stage[2].GetInt32() == 1;
if (matList != null && matList.Count > 0)
{
// Aggregate materials, skip duplicates
var materialsUsed = new Dictionary<uint, uint>();
foreach (var entry in matList)
{
if (entry == null || entry.Count < 2) continue;
var itemId = (uint)Math.Max(0, entry[0]);
var count = (uint)Math.Max(0, entry[1]);
if (itemId == 0 || count == 0) continue;
materialsUsed[itemId] = materialsUsed.GetValueOrDefault(itemId) + count;
}
ulong totalExp = 0;
foreach (var (itemId, count) in materialsUsed)
{
var material = FindInventoryItem(inventory, itemId)!;
if (config.TryGetMaterialGain(material, out var gainExp))
totalExp += gainExp * count;
}
// Consume materials
foreach (var (itemId, count) in materialsUsed)
{
var material = FindInventoryItem(inventory, itemId)!;
material.ItemCount -= count;
if (material.ItemCount == 0)
{
RemoveInventoryItem(inventory, itemId);
var proto = material.ToProto();
proto.Count = 0;
syncItems.Add(proto);
}
else
{
syncItems.Add(material.ToProto());
}
}
// Apply exp to weapon
if (totalExp > 0)
{
var maxLevel = config.GetWeaponMaxLevel(targetTemplate.BreakLimitId, weapon.Break);
var (newLevel, newExp) = config.ApplyWeaponExp(weapon.Level, weapon.Exp, totalExp, targetTemplate.Color, maxLevel);
weapon.Level = newLevel;
weapon.Exp = newExp;
}
}
// Perform break
if (doBreak && weapon.Break < MaxBreak)
weapon.Break++;
}
syncItems.Add(weapon.ToProto());
DatabaseHelper.SaveDatabaseType(inventory);
var finalMaxLevel = config.GetWeaponMaxLevel(targetTemplate.BreakLimitId, weapon.Break);
var bMaxUnlock = finalMaxLevel > 0 && weapon.Level >= finalMaxLevel;
var sync = new NtfSyncPlayer();
sync.Items.AddRange(syncItems);
await CallGSRouter.SendScript(connection, "Weapon_OneKeyToMax",
$"{{\"bMaxUnLock\":{(bMaxUnlock ? "true" : "false")}}}", sync);
}
private static BaseGameItemInfo? FindInventoryItem(InventoryData inventory, uint itemId)
{
if (inventory.Weapons.TryGetValue(itemId, out var weapon)) return weapon;
if (inventory.Skins.TryGetValue(itemId, out var skin)) return skin;
if (inventory.Items.TryGetValue(itemId, out var item)) return item;
return null;
}
private static void RemoveInventoryItem(InventoryData inventory, uint itemId)
{
inventory.Weapons.Remove(itemId);
inventory.Skins.Remove(itemId);
inventory.Items.Remove(itemId);
}
}
internal sealed class OneKeyToMaxParam
{
[JsonPropertyName("Id")]
public int Id { get; set; }
[JsonPropertyName("tbBreakUpgradeMat")]
public List<List<System.Text.Json.JsonElement>> TbBreakUpgradeMat { get; set; } = [];
}

View File

@@ -258,9 +258,9 @@ internal sealed class WeaponUpgradeConfig
var weaponTemplates = GameData.WeaponData.Values.ToDictionary(
x => GameResourceTemplateId.FromGdpl(x.Genre, x.Detail, x.Particular, x.Level),
x => new MaterialTemplate(x.Color, x.ProvideExp, x.ConsumeGold, x.RecycleID, x.BreakLimitID));
var suppliesTemplates = GameData.SuppliesData.Values.ToDictionary(
x => GameResourceTemplateId.FromGdpl(x.Genre, x.Detail, x.Particular, x.Level),
x => new MaterialTemplate(x.Color, x.ProvideExp, x.ConsumeGold, 0, 0));
var suppliesTemplates = GameData.AllSuppliesData
.GroupBy(x => GameResourceTemplateId.FromGdpl(x.Genre, x.Detail, x.Particular, x.Level))
.ToDictionary(g => g.Key, g => new MaterialTemplate(g.First().Color, g.First().ProvideExp, g.First().ConsumeGold, 0, 0));
return new WeaponUpgradeConfig(normalExp, ssrExp, breakLimits, recycleById, weaponTemplates, suppliesTemplates);
}
@@ -271,6 +271,24 @@ internal sealed class WeaponUpgradeConfig
public bool TryGetSuppliesTemplate(ulong templateId, out MaterialTemplate template) =>
_suppliesTemplates.TryGetValue(templateId, out template!);
public bool TryGetMaterialGain(BaseGameItemInfo item, out ulong exp)
{
exp = 0;
if (TryGetWeaponTemplate(item.TemplateId, out var weaponTemplate))
{
exp = weaponTemplate.ProvideExp;
if (item is GameWeaponInfo weapon && weapon.Level > 1)
exp += GetWeaponRecycleExp(weaponTemplate, weapon.Level);
return true;
}
if (TryGetSuppliesTemplate(item.TemplateId, out var suppliesTemplate))
{
exp = suppliesTemplate.ProvideExp;
return true;
}
return false;
}
public ulong GetWeaponRecycleExp(MaterialTemplate template, uint level)
{
if (template.RecycleId <= 0 || !_recycleById.TryGetValue(template.RecycleId, out var recycle))

View File

@@ -126,6 +126,8 @@ public class PacketNtfCallScript : BasePacket
sync.Custom[Player.ToPackedAttrKey(gid, sid)] = val;
sync.Custom[Player.ToShiftedAttrKey(gid, sid)] = val;
}
foreach (var (key, value) in Player.BuildMoneySync())
sync.Money[key] = value;
proto.ExtraSync = sync;
SetData(proto);

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Kei-Luna, Naruse and contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -87,6 +87,9 @@ dotnet build
- [Naruse](https://github.com/DevilProMT)
- [Kei-Luna](https://github.com/Kei-Luna)
## License
This repository is licensed under the [MIT License](LICENSE).
## Notes on use
This software is intended for research and testing purposes in a local environment.
It is not intended for unauthorized access to, interference with, or commercial use of official services.

View File

@@ -53,40 +53,42 @@ dotnet build
## 機能一覧
* [x] ログインと基本的なアカウント入場
* [x] プレイヤーデータの読み込み
* [x] 所持品の読み込み
* [x] キャラクターの読み込み
* [x] スキンの読み込み
* [x] 武器の読み込み
* [x] ロビー表示キャラクターの変更
* [x] キャラクタースキンの変更
* [x] キャラクタースキン形態の変更
* [x] 武器の付け替え
* [x] 武器の強化
* [x] プレイヤー名の変更
* [x] 現在対応済みロビー状態の基本保存
* [✓] メイン章のステージ入場と関連フロ
* [✓] デイリーのステージ入場と関連フロー
* [✓] 基本的なプレイヤー設定同期
* [✓] 基本的なプロフィール同期
* [✓] イベント関連リクエスト
* [✓] 実績関連リクエスト
* [✓] 編成関連リクエスト
* [✓] プレビュー関連リクエスト
* [✓] 一部のショップ関連リクエスト
* [ ] 完全な戦闘フロー
* [ ] ミッション / クエスト進行
* [ ] ガチャ / 募集システム
* [ ] 完全なショップ挙動
* [x] ログインシステム
* [x] インベントリ
* [x] 戦闘
* [x] キャラクター
* [x] GMメニュー
* [x] 武器
* [x] 後方支援
* [x] アポカリプス
* [x] アウクトゥス
* [x] 神格神経
* [x] キャラスキン
* [x] 武器スキン
* [x] ガチャ
* [x] メインストーリーチャプタ
* [x] 社員寮
* [x] ミニゲーム
* [x] 地下清掃
* [x] ニューロンシュミレーション
* [x] 戦術評価
* [x] エピソードストーリー
* [x] ルーチン作戦
* [x] 逆説迷宮
* [x] ロビー画面の編集
* [✓] 惑星開拓
* [✓] 夢の絵本
* [ ] ショップ
* [ ] マルチプレイシステム
* [ ] 基地 / 宿舎システム
* [ ] クライアント API 全体の対応
* [ ] 名誉紛争
## 貢献者
- [Naruse](https://github.com/DevilProMT)
- [Kei-Luna](https://github.com/Kei-Luna)
## ライセンス
このリポジトリは [MIT License](LICENSE) の下で公開されています。
## 利用上の注意
本ソフトウェアはローカル環境での研究・検証用途を想定しています。

View File

@@ -1 +1 @@
v=4.2
v=4.6