Compare commits

...

54 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
Kei-Luna
2047758c18 Update version.txt 2026-05-24 09:17:45 +09:00
Kei-Luna
6b48c90783 Basic functionality of VirCapture has been implemented. 2026-05-24 09:17:32 +09:00
Kei-Luna
a50b0563be Create VirCaptureLevel_SaveFightData.cs 2026-05-24 08:31:52 +09:00
Kei-Luna
b78c709f76 VirCaptureLevel_SavePos VirCaptureLevel_SaveCapture VirCaptureLevel_ChangeFlag 2026-05-24 08:27:28 +09:00
Kei-Luna
12094f6dd1 VirCaptureLevel_EnterLevel 2026-05-24 08:18:41 +09:00
Kei-Luna
c3b675dc34 VirCapture can Enter 2026-05-24 08:14:19 +09:00
Kei-Luna
6f51e335de Update version.txt 2026-05-24 08:03:12 +09:00
Kei-Luna
e4398e17b4 Implement TowerEvent 2026-05-24 08:02:59 +09:00
Kei-Luna
bc399f6afe fix Chapter_DealLevelSettlement 2026-05-24 07:47:52 +09:00
Kei-Luna
4fdd093644 Lineups_Update 2026-05-24 07:44:10 +09:00
Kei-Luna
772c272fb4 ClimbTowerLogic_SetLevelDiff 2026-05-24 07:11:29 +09:00
Kei-Luna
55bfadbd3e ClimbTowerLogic_GetReward 2026-05-24 05:40:26 +09:00
Kei-Luna
589f7d6340 Update version.txt 2026-05-23 20:02:23 +09:00
Kei-Luna
196b03718c TowerLevel_LevelSettlement ClimbTowerLogic_RecordProgres 2026-05-23 20:00:15 +09:00
Kei-Luna
b916ab5dfc ExtendFightDynamicLog ExtendFightLog 2026-05-23 19:51:16 +09:00
Kei-Luna
1dca65f91a ClimbTowerLogic_CheckCycleLevel TowerLevel_EnterLevel 2026-05-23 19:03:52 +09:00
Kei-Luna
1051de8dcf Update version.txt 2026-05-23 18:46:54 +09:00
Kei-Luna
72126da9a5 GirlCard_UpBySpecialBreak 2026-05-23 18:42:44 +09:00
Kei-Luna
331d2dbcaa Update version.txt 2026-05-20 10:04:59 +09:00
Kei-Luna
85df98c0ae Added functionality to Rogue3D 2026-05-20 09:50:28 +09:00
Kei-Luna
a585232045 small fix 2026-05-19 17:46:53 +09:00
Kei-Luna
125dadd224 Update version.txt 2026-05-19 17:35:26 +09:00
Kei-Luna
cfca2f970c Implement BossPvP logic (I implemented this based on undownding's code. Thank you!) 2026-05-19 17:35:08 +09:00
Kei-Luna
9a9ae13da0 Character episode chapters are now playable. 2026-05-19 16:57:22 +09:00
Kei-Luna
1938095ea5 Update version.txt 2026-05-19 16:32:40 +09:00
Kei-Luna
132355d76b Fixed an issue where adding too many support cards could prevent users from logging in. 2026-05-19 16:32:25 +09:00
Kei-Luna
5a8e45a44c Update version.txt 2026-05-19 10:16:40 +09:00
Kei-Luna
def4b8ae68 Gacha_UpSelect 2026-05-19 10:14:22 +09:00
Kei-Luna
686794a68c Gacha 2026-05-19 10:05:28 +09:00
Kei-Luna
3bc30812aa Added a warning against scam. Die, scammers! 2026-05-18 07:32:25 +09:00
Kei-Luna
f8f7311997 SupporterCard Affix can now be changed. 2026-05-16 16:20:17 +09:00
Kei-Luna
e628a010be Update version.txt 2026-05-15 16:45:00 +09:00
Kei-Luna
738a7d4e14 Improved the system so that affixes are correctly applied when obtaining support cards using the give command. 2026-05-15 16:17:36 +09:00
Kei-Luna
0058ba0db6 Support Card Affix 2026-05-15 16:07:36 +09:00
Kei-Luna
46d945f3ce Fix SupporterCard_Upgrade 2026-05-15 15:11:53 +09:00
104 changed files with 8308 additions and 60 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,32 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.Globalization;
namespace MikuSB.Data.Excel;
[ResourceEntity("challenge/bosspvp/boss_challenge.json")]
public class BossPvpBossChallengeExcel : ExcelResource
{
public uint ID { get; set; }
public string StartTime { get; set; } = "";
public string EndTime { get; set; } = "";
public List<uint> tbTaskID { get; set; } = [];
[JsonExtensionData] public IDictionary<string, JToken> ExtraData { get; set; } = new Dictionary<string, JToken>();
[JsonIgnore] public List<uint> BossIds { get; private set; } = [];
public override uint GetId() => ID;
public override void Loaded()
{
BossIds = ExtraData
.Where(x => x.Key.StartsWith("Boss", StringComparison.Ordinal) && int.TryParse(x.Key[4..], out _))
.OrderBy(x => int.Parse(x.Key[4..], CultureInfo.InvariantCulture))
.Select(x => x.Value.Type == JTokenType.Integer ? x.Value.Value<uint>() : 0u)
.Where(x => x > 0)
.ToList();
GameData.BossPvpBossChallengeData[ID] = this;
}
}

View File

@@ -0,0 +1,17 @@
namespace MikuSB.Data.Excel;
[ResourceEntity("challenge/bosspvp/boss.json")]
public class BossPvpBossExcel : ExcelResource
{
public uint ID { get; set; }
public uint LevelID { get; set; }
public uint BossID { get; set; }
public List<List<int>> BossLevel { get; set; } = [];
public override uint GetId() => ID;
public override void Loaded()
{
GameData.BossPvpBossData[ID] = this;
}
}

View File

@@ -0,0 +1,15 @@
namespace MikuSB.Data.Excel;
[ResourceEntity("challenge/bosspvp/num.json")]
public class BossPvpNumExcel : ExcelResource
{
public uint Week { get; set; }
public uint Num { get; set; }
public override uint GetId() => Week;
public override void Loaded()
{
GameData.BossPvpNumData[Week] = this;
}
}

View File

@@ -0,0 +1,56 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace MikuSB.Data.Excel;
[ResourceEntity("challenge/climbtower/climb_tower_award.json")]
public class ClimbTowerAwardExcel : ExcelResource
{
[JsonProperty("ID")] public uint ID { get; set; }
[JsonProperty("Diff")] public JToken? DiffRaw { get; set; }
[JsonProperty("FirstAward")] public List<List<uint>> FirstAward { get; set; } = [];
[JsonProperty("StarCount1")] public int StarCount1 { get; set; }
[JsonProperty("StarAward1")] public List<List<uint>> StarAward1 { get; set; } = [];
[JsonProperty("StarCount2")] public int StarCount2 { get; set; }
[JsonProperty("StarAward2")] public List<List<uint>> StarAward2 { get; set; } = [];
[JsonProperty("StarCount3")] public int StarCount3 { get; set; }
[JsonProperty("StarAward3")] public List<List<uint>> StarAward3 { get; set; } = [];
[JsonIgnore]
public int Diff => DiffRaw?.Type switch
{
JTokenType.Integer => Math.Max(1, DiffRaw.Value<int>()),
JTokenType.String when int.TryParse(DiffRaw.Value<string>(), out var value) => Math.Max(1, value),
_ => 1
};
public override uint GetId() => (ID * 10u) + (uint)Diff;
public override void Loaded()
{
if (!GameData.ClimbTowerAwardData.TryGetValue(ID, out var diffMap))
{
diffMap = [];
GameData.ClimbTowerAwardData[ID] = diffMap;
}
diffMap[Diff] = this;
}
public int GetStarCount(int group) => group switch
{
1 => StarCount1,
2 => StarCount2,
3 => StarCount3,
_ => 0
};
public IReadOnlyList<IReadOnlyList<uint>> GetRewards(int group) => group switch
{
0 => FirstAward,
1 => StarAward1,
2 => StarAward2,
3 => StarAward3,
_ => []
};
}

View File

@@ -0,0 +1,18 @@
using Newtonsoft.Json;
namespace MikuSB.Data.Excel;
[ResourceEntity("challenge/climbtower/climb_tower_diff.json")]
public class ClimbTowerDiffExcel : ExcelResource
{
[JsonProperty("ID")] public uint ID { get; set; }
[JsonProperty("Level1")] public int Level1 { get; set; }
[JsonProperty("Level2")] public int Level2 { get; set; }
public override uint GetId() => ID;
public override void Loaded()
{
GameData.ClimbTowerDiffData[ID] = this;
}
}

View File

@@ -0,0 +1,17 @@
using Newtonsoft.Json;
namespace MikuSB.Data.Excel;
[ResourceEntity("challenge/climbtower/climb_tower_levelorder.json")]
public class ClimbTowerLevelOrderExcel : ExcelResource
{
[JsonProperty("ID")] public uint ID { get; set; }
[JsonProperty("LevelID")] public uint LevelID { get; set; }
public override uint GetId() => ID;
public override void Loaded()
{
GameData.ClimbTowerLevelOrderData[ID] = this;
}
}

View File

@@ -0,0 +1,57 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.Globalization;
namespace MikuSB.Data.Excel;
[ResourceEntity("challenge/climbtower/climb_tower_time.json")]
public class ClimbTowerTimeExcel : ExcelResource
{
[JsonProperty("ID")] public uint ID { get; set; }
[JsonProperty("StartTime")] public string StartTime { get; set; } = "";
[JsonProperty("EndTime")] public string EndTime { get; set; } = "";
[JsonProperty("Level1")] public List<List<uint>> Level1 { get; set; } = [];
[JsonProperty("Level2")] public JToken? Level2Raw { get; set; }
public override uint GetId() => ID;
public override void Loaded()
{
GameData.ClimbTowerTimeData[ID] = this;
}
public IReadOnlyList<IReadOnlyList<uint>> GetLevelGroups(int type)
{
if (type == 1)
return Level1;
if (Level2Raw == null)
return [];
if (Level2Raw.Type == JTokenType.Array)
{
return Level2Raw
.Children()
.OfType<JArray>()
.Select(x => (IReadOnlyList<uint>)x.Values<uint>().ToList())
.ToList();
}
if (Level2Raw.Type == JTokenType.Object)
{
return Level2Raw
.Children<JProperty>()
.Select(x => new
{
Key = uint.TryParse(x.Name, CultureInfo.InvariantCulture, out var key) ? key : 0u,
Value = x.Value.Type == JTokenType.Integer ? x.Value.Value<uint>() : 0u
})
.Where(x => x.Key > 0 && x.Value > 0)
.OrderBy(x => x.Key)
.Select(x => (IReadOnlyList<uint>)new List<uint> { x.Key, x.Value })
.ToList();
}
return [];
}
}

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,46 @@
using MikuSB.Util;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace MikuSB.Data.Excel;
[ResourceEntity("gacha/gacha.json")]
public class GachaExcel : ExcelResource
{
public uint ID { get; set; }
public List<string>? Pool { get; set; }
public uint Probability { get; set; }
public uint ProbabilityTen { get; set; }
public JToken? ProtectNum { get; set; }
public JToken? UpNum { get; set; }
public uint? ProtectTag { get; set; }
public uint? ProtectType { get; set; }
public JToken? ProtectCount { get; set; }
public uint? UpSelect { get; set; }
public override uint GetId() => ID;
public override void Loaded() => GameData.GachaData[ID] = this;
public override void AfterAllDone()
{
foreach (var poolName in Pool ?? [])
{
if (GameData.GachaPoolData.ContainsKey(poolName)) continue;
var path = ConfigManager.Config.Path.ResourcePath + "/gacha/pool/" + poolName + ".json";
if (!File.Exists(path)) continue;
var json = File.ReadAllText(path);
var items = JsonConvert.DeserializeObject<List<GachaPoolItem>>(json) ?? [];
GameData.GachaPoolData[poolName] = items;
}
}
}
public class GachaPoolItem
{
public int ID { get; set; }
public int Rarity { get; set; }
public List<uint> GDPL { get; set; } = [];
public int Weight { get; set; }
public int? UPTag { get; set; }
public int? UPSelectTag { get; set; }
}

View File

@@ -0,0 +1,18 @@
namespace MikuSB.Data.Excel;
[ResourceEntity("gacha/probability.json")]
public class GachaProbabilityExcel : ExcelResource
{
public uint ID { get; set; }
public int Rarity1 { get; set; }
public int Rarity2 { get; set; }
public int Rarity3 { get; set; }
public int Rarity4 { get; set; }
public int Rarity5 { get; set; }
public int Rarity6 { get; set; }
public int[] Weights => [Rarity1, Rarity2, Rarity3, Rarity4, Rarity5, Rarity6];
public override uint GetId() => ID;
public override void Loaded() => GameData.GachaProbabilityData[ID] = this;
}

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

@@ -0,0 +1,26 @@
using Newtonsoft.Json;
namespace MikuSB.Data.Excel;
[ResourceEntity("item/templates/monster_card.json")]
public class MonsterCardExcel : ExcelResource
{
[JsonProperty("Genre")] public uint Genre { get; set; }
[JsonProperty("Detail")] public uint Detail { get; set; }
[JsonProperty("Particular")] public uint Particular { get; set; }
[JsonProperty("Level")] public uint Level { get; set; }
[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);
public override uint GetId() => Particular;
public override void Loaded()
{
GameData.MonsterCardData[TemplateId] = this;
}
}

View File

@@ -0,0 +1,47 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace MikuSB.Data.Excel;
[ResourceEntity("item/templates/others.json")]
public class OtherItemExcel : ExcelResource
{
public uint Genre { get; set; }
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);
public override uint GetId() => (uint)GameResourceTemplateId.FromGdpl(Genre, Detail, Particular, Level);
public override void Loaded()
{
GameData.OtherItemData[GetId()] = this;
}
private static uint ReadUInt(JToken? token)
{
if (token == null)
return 0;
return 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 value) => value,
_ => 0
};
}
}

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

@@ -0,0 +1,14 @@
namespace MikuSB.Data.Excel;
[ResourceEntity("challenge/role/level.json")]
public class RoleLevelExcel : ExcelResource
{
public uint ID { get; set; }
public override uint GetId() => ID;
public override void Loaded()
{
GameData.RoleLevelData[ID] = this;
}
}

View File

@@ -0,0 +1,39 @@
using Newtonsoft.Json;
namespace MikuSB.Data.Excel;
[ResourceEntity("item/cardbreak/breaknew.json")]
public class SpecialBreakExcel : ExcelResource
{
[JsonProperty("ID")] public int Id { get; set; }
[JsonProperty("1Items1")] public List<List<int>> Items1 { get; set; } = [];
[JsonProperty("2Items1")] public List<List<int>> Items2 { get; set; } = [];
[JsonProperty("3Items1")] public List<List<int>> Items3 { get; set; } = [];
[JsonProperty("4Items1")] public List<List<int>> Items4 { get; set; } = [];
public List<List<int>> GetItems(uint breakLevel) => breakLevel switch
{
1 => Items1,
2 => Items2,
3 => Items3,
4 => Items4,
_ => []
};
public bool HasBreakLevel(uint breakLevel) => breakLevel switch
{
1 => Items1.Count > 0,
2 => Items2.Count > 0,
3 => Items3.Count > 0,
4 => Items4.Count > 0,
_ => false
};
public override uint GetId() => (uint)Id;
public override void Loaded()
{
GameData.SpecialBreakData[Id] = this;
}
}

View File

@@ -0,0 +1,27 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace MikuSB.Data.Excel;
[ResourceEntity("item/support/affix.json")]
public class SupportAffixExcel : ExcelResource
{
[JsonProperty("ID")] public int Id { get; set; }
[JsonExtensionData] public IDictionary<string, JToken> ExtraData { get; set; } = new Dictionary<string, JToken>();
public int TierCount =>
ExtraData
.Where(x => x.Key != "ID" && x.Key != "Sift" && x.Key != "Comment")
.Select(x => x.Value)
.OfType<JObject>()
.Select(x => x.Count)
.DefaultIfEmpty(0)
.Max();
public override uint GetId() => (uint)Id;
public override void Loaded()
{
GameData.SupportAffixData[Id] = this;
}
}

View File

@@ -0,0 +1,35 @@
using Newtonsoft.Json;
namespace MikuSB.Data.Excel;
[ResourceEntity("item/support/affix_pool.json")]
public class SupportAffixPoolExcel : ExcelResource
{
[JsonProperty("ID")] public int Id { get; set; }
public List<int> AffixGroup1 { get; set; } = [];
public int Weight1 { get; set; }
public List<int> AffixGroup2 { get; set; } = [];
public int Weight2 { get; set; }
public List<int> AffixGroup3 { get; set; } = [];
public int Weight3 { get; set; }
public List<int> AffixGroup4 { get; set; } = [];
public int Weight4 { get; set; }
public IEnumerable<(IReadOnlyList<int> Affixs, int Weight)> Groups
{
get
{
if (AffixGroup1.Count > 0 && Weight1 > 0) yield return (AffixGroup1, Weight1);
if (AffixGroup2.Count > 0 && Weight2 > 0) yield return (AffixGroup2, Weight2);
if (AffixGroup3.Count > 0 && Weight3 > 0) yield return (AffixGroup3, Weight3);
if (AffixGroup4.Count > 0 && Weight4 > 0) yield return (AffixGroup4, Weight4);
}
}
public override uint GetId() => (uint)Id;
public override void Loaded()
{
GameData.SupportAffixPoolData[Id] = this;
}
}

View File

@@ -1,4 +1,5 @@
using Newtonsoft.Json;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace MikuSB.Data.Excel;
@@ -11,7 +12,13 @@ public class SupportCardExcel : ExcelResource
public uint Level { get; set; }
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; }
[JsonProperty("InitialAffixCost")] public JToken? InitialAffixCostRaw { get; set; }
[JsonProperty("FixedAffixCost")] public JToken? FixedAffixCostRaw { get; set; }
public uint MaxLevel => LevelLimitId switch
{
@@ -21,6 +28,19 @@ public class SupportCardExcel : ExcelResource
_ => 10
};
public int InitialAffixCount => Color >= 5 ? 2 : 1;
public int TotalAffixCount => Color >= 5 ? 3 : 2;
[JsonIgnore]
public IReadOnlyList<uint> AffixCost => ParseFlatCost(AffixCostRaw);
[JsonIgnore]
public IReadOnlyList<IReadOnlyList<uint>> InitialAffixCost => ParseNestedCost(InitialAffixCostRaw);
[JsonIgnore]
public IReadOnlyList<uint> FixedAffixCost => ParseFlatCost(FixedAffixCostRaw);
public ulong TemplateId => GameResourceTemplateId.FromGdpl(Genre, Detail, Particular, Level);
public override uint GetId() => Icon;
@@ -29,4 +49,23 @@ public class SupportCardExcel : ExcelResource
{
GameData.SupportCardData.Add(this);
}
private static IReadOnlyList<uint> ParseFlatCost(JToken? token)
{
if (token is not JArray array)
return [];
return array.Select(x => x.Value<uint>()).ToArray();
}
private static IReadOnlyList<IReadOnlyList<uint>> ParseNestedCost(JToken? token)
{
if (token is not JArray outer)
return [];
var result = new List<IReadOnlyList<uint>>();
foreach (var entry in outer.OfType<JArray>())
result.Add(entry.Select(x => x.Value<uint>()).ToArray());
return result;
}
}

View File

@@ -0,0 +1,18 @@
using Newtonsoft.Json;
namespace MikuSB.Data.Excel;
[ResourceEntity("item/support/fixed.json")]
public class SupportFixedExcel : ExcelResource
{
[JsonProperty("ID")] public int Id { get; set; }
public int Num { get; set; }
public List<uint> Item { get; set; } = [];
public override uint GetId() => (uint)Id;
public override void Loaded()
{
GameData.SupportFixedData[Id] = this;
}
}

View File

@@ -0,0 +1,20 @@
using Newtonsoft.Json;
namespace MikuSB.Data.Excel;
[ResourceEntity("challenge/tower_event/level.json")]
public class TowerEventLevelExcel : ExcelResource
{
[JsonProperty("ID")] public uint ID { get; set; }
[JsonProperty("MapID")] public uint MapID { get; set; }
[JsonProperty("FightID")] public uint FightID { get; set; }
[JsonProperty("TaskPath")] public string TaskPath { get; set; } = "";
[JsonProperty("ConsumeVigor")] public List<int> ConsumeVigor { get; set; } = [];
public override uint GetId() => ID;
public override void Loaded()
{
GameData.TowerEventLevelData[ID] = this;
}
}

View File

@@ -0,0 +1,20 @@
using Newtonsoft.Json;
namespace MikuSB.Data.Excel;
[ResourceEntity("challenge/climbtower/level.json")]
public class TowerLevelExcel : ExcelResource
{
[JsonProperty("ID")] public uint ID { get; set; }
[JsonProperty("MapID")] public uint MapID { get; set; }
[JsonProperty("FightID")] public uint FightID { get; set; }
[JsonProperty("TaskPath")] public string TaskPath { get; set; } = "";
[JsonProperty("ConsumeVigor")] public List<int> ConsumeVigor { get; set; } = [];
public override uint GetId() => ID;
public override void Loaded()
{
GameData.TowerLevelData[ID] = this;
}
}

View File

@@ -0,0 +1,20 @@
using Newtonsoft.Json;
namespace MikuSB.Data.Excel;
[ResourceEntity("dlc/vircapture/captureregion.json")]
public class VirCaptureCaptureRegionExcel : ExcelResource
{
[JsonProperty("Id")] public uint Id { get; set; }
[JsonProperty("StartTime")] public string StartTime { get; set; } = "";
[JsonProperty("EndTime")] public string EndTime { get; set; } = "";
[JsonProperty("MapId")] public uint MapId { get; set; }
[JsonProperty("LevelRegionName")] public string LevelRegionName { get; set; } = "";
public override uint GetId() => Id;
public override void Loaded()
{
GameData.VirCaptureCaptureRegionData[Id] = this;
}
}

View File

@@ -0,0 +1,21 @@
using Newtonsoft.Json;
namespace MikuSB.Data.Excel;
[ResourceEntity("dlc/vircapture/levellist.json")]
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;
public override void Loaded()
{
GameData.VirCaptureLevelListData[Level] = this;
}
}

View File

@@ -0,0 +1,18 @@
using Newtonsoft.Json;
namespace MikuSB.Data.Excel;
[ResourceEntity("dlc/vircapture/season.json")]
public class VirCaptureSeasonExcel : ExcelResource
{
[JsonProperty("Id")] public uint Id { get; set; }
[JsonProperty("StartTime")] public string StartTime { get; set; } = "";
[JsonProperty("EndTime")] public string EndTime { get; set; } = "";
public override uint GetId() => Id;
public override void Loaded()
{
GameData.VirCaptureSeasonData[Id] = this;
}
}

View File

@@ -0,0 +1,20 @@
using Newtonsoft.Json;
namespace MikuSB.Data.Excel;
[ResourceEntity("dlc/vircapture/timelist.json")]
public class VirCaptureTimeExcel : ExcelResource
{
[JsonProperty("Id")] public uint Id { get; set; }
[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;
public override void Loaded()
{
GameData.VirCaptureTimeData[Id] = this;
}
}

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

@@ -0,0 +1,19 @@
using Newtonsoft.Json;
namespace MikuSB.Data.Excel;
[ResourceEntity("dlc/vircapture/trial_timelist.json")]
public class VirCaptureTrialTimeExcel : ExcelResource
{
[JsonProperty("Id")] public uint Id { get; set; }
[JsonProperty("StartTime")] public string StartTime { get; set; } = "";
[JsonProperty("EndTime")] public string EndTime { get; set; } = "";
[JsonProperty("AwardTime")] public string AwardTime { get; set; } = "";
public override uint GetId() => Id;
public override void Loaded()
{
GameData.VirCaptureTrialTimeData[Id] = this;
}
}

View File

@@ -13,6 +13,7 @@ public static class GameData
public static Dictionary<int, BreakLevelLimitExcel> BreakLevelLimitData { get; private set; } = [];
public static Dictionary<int, RecycleExcel> RecycleData { get; private set; } = [];
public static Dictionary<uint, ChapterLevelExcel> ChapterLevelData { get; private set; } = [];
public static Dictionary<uint, RoleLevelExcel> RoleLevelData { get; private set; } = [];
public static Dictionary<uint, ArItemExcel> ArItemData { get; private set; } = [];
public static Dictionary<uint, ManifestationExcel> ManifestationData { get; private set; } = [];
public static Dictionary<uint, Rogue3DDifficultExcel> Rogue3DDifficultData { get; private set; } = [];
@@ -20,11 +21,25 @@ public static class GameData
public static Dictionary<uint, Rogue3DTalentExcel> Rogue3DTalentData { get; private set; } = [];
public static Dictionary<uint, Rogue3DDailyBuffExcel> Rogue3DDailyBuffData { get; private set; } = [];
public static Dictionary<int, BreakExcel> BreakData { get; private set; } = [];
public static Dictionary<int, SpecialBreakExcel> SpecialBreakData { get; private set; } = [];
public static Dictionary<uint, SpineExcel> SpineData { get; private set; } = [];
public static Dictionary<uint, NodeConditionExcel> NodeConditionData { get; private set; } = [];
public static List<SupportCardExcel> SupportCardData { get; private set; } = [];
public static Dictionary<int, SupportAffixExcel> SupportAffixData { get; private set; } = [];
public static Dictionary<int, SupportAffixPoolExcel> SupportAffixPoolData { get; private set; } = [];
public static Dictionary<int, SupportFixedExcel> SupportFixedData { get; private set; } = [];
public static Dictionary<uint, WeaponSkinExcel> WeaponSkinData { get; private set; } = [];
public static Dictionary<uint, DailyLevelExcel> DailyLevelData { get; private set; } = [];
public static Dictionary<uint, BossPvpBossChallengeExcel> BossPvpBossChallengeData { get; private set; } = [];
public static Dictionary<uint, BossPvpBossExcel> BossPvpBossData { get; private set; } = [];
public static Dictionary<uint, BossPvpNumExcel> BossPvpNumData { get; private set; } = [];
public static Dictionary<uint, ClimbTowerTimeExcel> ClimbTowerTimeData { get; private set; } = [];
public static Dictionary<uint, ClimbTowerDiffExcel> ClimbTowerDiffData { get; private set; } = [];
public static Dictionary<uint, Dictionary<int, ClimbTowerAwardExcel>> ClimbTowerAwardData { get; private set; } = [];
public static Dictionary<uint, ClimbTowerLevelOrderExcel> ClimbTowerLevelOrderData { get; private set; } = [];
public static Dictionary<uint, TowerLevelExcel> TowerLevelData { get; private set; } = [];
public static Dictionary<uint, TowerEventLevelExcel> TowerEventLevelData { get; private set; } = [];
public static Dictionary<uint, OtherItemExcel> OtherItemData { get; private set; } = [];
public static Dictionary<uint, ProfileExcel> ProfileData { get; private set; } = [];
public static Dictionary<uint, CardSkinPartsExcel> CardSkinPartsData { get; private set; } = [];
public static Dictionary<uint, CallItemExcel> CallItemData { get; private set; } = [];
@@ -32,6 +47,21 @@ public static class GameData
public static Dictionary<uint, GuideExcel> GuideData { get; private set; } = [];
public static Dictionary<uint, DormGiftExcel> DormGiftData { get; private set; } = [];
public static Dictionary<uint, HouseFurniturePosExcel> HouseFurniturePosData { get; private set; } = [];
public static Dictionary<uint, GachaExcel> GachaData { get; private set; } = [];
public static Dictionary<uint, GachaProbabilityExcel> GachaProbabilityData { get; private set; } = [];
public static Dictionary<string, List<GachaPoolItem>> GachaPoolData { get; private set; } = [];
public static Dictionary<uint, VirCaptureTimeExcel> VirCaptureTimeData { get; private set; } = [];
public static Dictionary<uint, VirCaptureSeasonExcel> VirCaptureSeasonData { get; private set; } = [];
public static Dictionary<uint, VirCaptureTrialTimeExcel> VirCaptureTrialTimeData { get; private set; } = [];
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

@@ -1,4 +1,4 @@
using MikuSB.Enums.Item;
using MikuSB.Enums.Item;
using MikuSB.Proto;
using SqlSugar;
@@ -10,19 +10,19 @@ public class InventoryData : BaseDatabaseDataHelper
public uint NextUniqueUid { get; set; } = 100000;
[SugarColumn(IsJson = true)]
public Dictionary<uint, BaseGameItemInfo> Items { get; set; } = []; // Key: UniqueId
public Dictionary<uint, BaseGameItemInfo> Items { get; set; } = [];
[SugarColumn(IsJson = true)]
public Dictionary<uint, GameWeaponInfo> Weapons { get; set; } = []; // Key: UniqueId
public Dictionary<uint, GameWeaponInfo> Weapons { get; set; } = [];
[SugarColumn(IsJson = true)]
public Dictionary<uint, GameSkinInfo> Skins { get; set; } = []; // Key: UniqueId
public Dictionary<uint, GameSkinInfo> Skins { get; set; } = [];
[SugarColumn(IsJson = true)]
public Dictionary<uint, GameSupportCardInfo> SupportCards { get; set; } = []; // Key: UniqueId
public Dictionary<uint, GameSupportCardInfo> SupportCards { get; set; } = [];
[SugarColumn(IsJson = true)]
public Dictionary<uint, uint> SkinTypesBySkinId { get; set; } = []; // Key: nSkinId, Value: client nType
public Dictionary<uint, uint> SkinTypesBySkinId { get; set; } = [];
}
public class BaseGameItemInfo
@@ -63,6 +63,7 @@ public abstract class GrowableItemInfo : BaseGameItemInfo
public class GameWeaponInfo : GrowableItemInfo
{
[SugarColumn(IsJson = true)] public Dictionary<uint, ulong> PartSlots { get; set; } = [];
public override Item ToProto()
{
var proto = new Item
@@ -79,14 +80,17 @@ public class GameWeaponInfo : GrowableItemInfo
Evolue = Evolue
}
};
foreach (var (slot, uid) in PartSlots) proto.Slots[slot] = uid;
foreach (var (slot, uid) in PartSlots)
proto.Slots[slot] = uid;
return proto;
}
}
public class GameSkinInfo : BaseGameItemInfo
{
[SugarColumn(IsJson = true)] public Dictionary<uint, ulong> PartSlots { get; set; } = [];
public uint SkinType { get; set; }
public override Item ToProto()
{
var proto = new Item
@@ -97,15 +101,17 @@ public class GameSkinInfo : BaseGameItemInfo
Flag = (uint)Flag,
};
proto.Slots[(uint)ItemSkinSlotTypeEnum.SLOT_CARD_SKIL_TYPE] = Math.Min(SkinType, 1);
foreach (var (slot, uid) in PartSlots) proto.Slots[slot] = uid;
foreach (var (slot, uid) in PartSlots)
proto.Slots[slot] = uid;
return proto;
}
}
public class GameSupportCardInfo : BaseGameItemInfo
{
public uint AffixId { get; set; }
[SugarColumn(IsJson = true)] public List<uint> Affixs { get; set; } = [];
public override Item ToProto()
{
var proto = new Item
@@ -120,6 +126,7 @@ public class GameSupportCardInfo : BaseGameItemInfo
Exp = Exp
}
};
proto.Enhance.Affixs.AddRange(Affixs);
proto.Slots[(uint)ItemSupportCardSlotTypeEnum.SLOT_AFFIXINDEX] = AffixId;
return proto;
}

View File

@@ -0,0 +1,498 @@
using MikuSB.Data;
using MikuSB.Data.Excel;
using MikuSB.Database.Inventory;
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.Game.BossPvp;
internal static class BossPvpService
{
private const uint GroupId = 51;
private const uint ActivitySubId = 0;
private const uint ChallengeNumSid = 1;
private const uint DiffStartId = 10;
private const uint LevelStartSid = 100;
private const uint LevelStride = 10;
private const uint BossLineup1 = 15;
private const uint BossLineup2 = 16;
private static readonly JsonSerializerOptions JsonOptions = new()
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
public static async ValueTask<(object Response, NtfSyncPlayer Sync)> HandleGetOpenIdAsync(PlayerInstance player)
{
await EnsureBossLineupsAsync(player);
var sync = new NtfSyncPlayer();
var season = GetOpenSeason();
var seasonId = season?.ID ?? 1u;
SetStr(player, ActivitySubId, seasonId.ToString(CultureInfo.InvariantCulture), sync);
SetStr(player, ChallengeNumSid, GetDailyChallengeNum().ToString(CultureInfo.InvariantCulture), sync);
if (season != null)
{
for (var index = 0; index < season.BossIds.Count; index++)
{
var bossLevelId = season.BossIds[index];
EnsureStr(player, DiffStartId + (uint)(index + 1), "0", sync);
EnsureStr(player, GetBossSid(bossLevelId, 1), EmptySnapshotJson(), sync);
EnsureStr(player, GetBossSid(bossLevelId, 2), EmptySnapshotJson(), sync);
EnsureStr(player, GetBossSid(bossLevelId, 3), EmptySnapshotJson(), sync);
EnsureStr(player, GetBossSid(bossLevelId, 4), "0", sync);
EnsureStr(player, GetBossSid(bossLevelId, 5), "0", sync);
EnsureStr(player, GetBossSid(bossLevelId, 6), "0", sync);
EnsureStr(player, GetBossSid(bossLevelId, 7), "0", sync);
EnsureStr(player, GetBossSid(bossLevelId, 8), "0", sync);
}
}
var response = new
{
nID = seasonId,
tbTimeCfg = new[]
{
new
{
nStartTime = -1,
nEndTime = -1
}
}
};
return (response, sync);
}
public static object HandleEnterLevel(string? param)
{
var req = Deserialize<EnterLevelParam>(param);
return new
{
nSeed = Random.Shared.Next(1, int.MaxValue),
nID = req?.NId ?? 0
};
}
public static (object Response, NtfSyncPlayer Sync) HandleRecord(PlayerInstance player, string? param)
{
var req = Deserialize<RecordParam>(param);
if (req == null)
{
return (new { bRecord = false }, new NtfSyncPlayer());
}
var sync = new NtfSyncPlayer();
if (req.BRecord)
{
var historyScore = ReadInt(player, GetBossSid(req.NId, 4));
var currentScore = ComputeIntegral(req.NId, req.NDiff, req.ResidueTime);
if (currentScore >= historyScore)
{
WriteBestRun(player, req.NId, req.NTeamId, req.NTime, currentScore, sync);
}
}
return (new { bRecord = req.BRecord }, sync);
}
public static (JsonNode Response, NtfSyncPlayer Sync) HandleSettlement(PlayerInstance player, JsonNode? param)
{
var req = param?.Deserialize<SettlementParam>(JsonOptions);
var sync = new NtfSyncPlayer();
if (req == null)
{
return (new JsonObject(), sync);
}
var totalSid = GetBossSid(req.NId, 7);
var successSid = GetBossSid(req.NId, 6);
var diffSid = GetBossSid(req.NId, 8);
SetStr(player, totalSid, (ReadInt(player, totalSid) + 1).ToString(CultureInfo.InvariantCulture), sync);
SetStr(player, successSid, (ReadInt(player, successSid) + 1).ToString(CultureInfo.InvariantCulture), sync);
var clearedDiff = Math.Max(ReadInt(player, diffSid), req.NDiff);
SetStr(player, diffSid, clearedDiff.ToString(CultureInfo.InvariantCulture), sync);
var positionSid = TryGetPositionDiffSid(req.NId);
if (positionSid != null)
{
var newPositionDiff = Math.Max(ReadInt(player, positionSid.Value), req.NDiff);
SetStr(player, positionSid.Value, newPositionDiff.ToString(CultureInfo.InvariantCulture), sync);
}
var score = ComputeIntegral(req.NId, req.NDiff, req.ResidueTime);
if (score > ReadInt(player, GetBossSid(req.NId, 4)))
{
WriteBestRun(player, req.NId, req.NTeamId, req.NTime, score, sync);
}
return (new JsonObject(), sync);
}
public static (JsonNode Response, NtfSyncPlayer Sync) HandleFail(PlayerInstance player, JsonNode? param)
{
var req = param?.Deserialize<FailParam>(JsonOptions);
var sync = new NtfSyncPlayer();
if (req == null)
{
return (new JsonObject(), sync);
}
var totalSid = GetBossSid(req.NId, 7);
SetStr(player, totalSid, (ReadInt(player, totalSid) + 1).ToString(CultureInfo.InvariantCulture), sync);
return (new JsonObject(), sync);
}
public static (object Response, NtfSyncPlayer Sync) HandleMopup(PlayerInstance player, string? param)
{
var req = Deserialize<MopupParam>(param);
var sync = new NtfSyncPlayer();
if (req == null)
{
return (new { }, sync);
}
var totalSid = GetBossSid(req.NId, 7);
var successSid = GetBossSid(req.NId, 6);
var diffSid = GetBossSid(req.NId, 8);
SetStr(player, totalSid, (ReadInt(player, totalSid) + 1).ToString(CultureInfo.InvariantCulture), sync);
SetStr(player, successSid, (ReadInt(player, successSid) + 1).ToString(CultureInfo.InvariantCulture), sync);
var clearedDiff = Math.Max(ReadInt(player, diffSid), req.NDiff);
SetStr(player, diffSid, clearedDiff.ToString(CultureInfo.InvariantCulture), sync);
var positionSid = TryGetPositionDiffSid(req.NId);
if (positionSid != null)
{
var newPositionDiff = Math.Max(ReadInt(player, positionSid.Value), req.NDiff + 1);
SetStr(player, positionSid.Value, newPositionDiff.ToString(CultureInfo.InvariantCulture), sync);
}
var score = ComputeIntegral(req.NId, req.NDiff, 0);
if (score > ReadInt(player, GetBossSid(req.NId, 4)))
{
WriteBestRun(player, req.NId, 0, 0, score, sync);
}
return (new { }, sync);
}
public static object HandleGetReward(string? param)
{
_ = Deserialize<RewardParam>(param);
return new { tbAward = Array.Empty<object>() };
}
private static async ValueTask EnsureBossLineupsAsync(PlayerInstance player)
{
var lineups = player.LineupManager.LineupData.LineupInfo;
var baseLineup = lineups.GetValueOrDefault(1) ?? lineups.Values.FirstOrDefault();
if (baseLineup == null)
{
return;
}
if (!lineups.ContainsKey((int)BossLineup1))
{
await player.LineupManager.UpdateLineup((int)BossLineup1, baseLineup.Member1, baseLineup.Member2, baseLineup.Member3, true);
}
if (!lineups.ContainsKey((int)BossLineup2))
{
await player.LineupManager.UpdateLineup((int)BossLineup2, baseLineup.Member1, baseLineup.Member2, baseLineup.Member3, true);
}
}
private static void WriteBestRun(PlayerInstance player, uint bossLevelId, uint lineupId, double finishTime, int score, NtfSyncPlayer sync)
{
var snapshots = CaptureLineupSnapshots(player, lineupId);
SetStr(player, GetBossSid(bossLevelId, 1), System.Text.Json.JsonSerializer.Serialize(snapshots[0], JsonOptions), sync);
SetStr(player, GetBossSid(bossLevelId, 2), System.Text.Json.JsonSerializer.Serialize(snapshots[1], JsonOptions), sync);
SetStr(player, GetBossSid(bossLevelId, 3), System.Text.Json.JsonSerializer.Serialize(snapshots[2], JsonOptions), sync);
SetStr(player, GetBossSid(bossLevelId, 4), score.ToString(CultureInfo.InvariantCulture), sync);
SetStr(player, GetBossSid(bossLevelId, 5), Math.Max(0, (int)Math.Floor(finishTime)).ToString(CultureInfo.InvariantCulture), sync);
}
private static BossPvpRoleSnapshot[] CaptureLineupSnapshots(PlayerInstance player, uint lineupId)
{
var lineups = player.LineupManager.LineupData.LineupInfo;
var lineup = lineups.GetValueOrDefault((int)lineupId)
?? lineups.GetValueOrDefault((int)BossLineup1)
?? lineups.GetValueOrDefault(1)
?? lineups.Values.FirstOrDefault();
if (lineup == null)
{
return [new(), new(), new()];
}
return
[
CaptureRoleSnapshot(player, lineup.Member1),
CaptureRoleSnapshot(player, lineup.Member2),
CaptureRoleSnapshot(player, lineup.Member3)
];
}
private static BossPvpRoleSnapshot CaptureRoleSnapshot(PlayerInstance player, uint characterGuid)
{
if (characterGuid == 0)
{
return new BossPvpRoleSnapshot();
}
var character = player.CharacterManager.GetCharacterByGUID(characterGuid);
if (character == null)
{
return new BossPvpRoleSnapshot();
}
var snapshot = new BossPvpRoleSnapshot
{
Role = character.Guid,
Weapon = character.WeaponUniqueId
};
var weapon = player.InventoryManager.GetWeaponItem(character.WeaponUniqueId);
if (weapon != null)
{
snapshot.Wgdpl = BuildWeaponGdpl(weapon);
snapshot.Wslot = weapon.PartSlots;
}
var supports = character.SupportSlots
.OrderBy(x => x.Key)
.Select(x => x.Value)
.Where(x => x != 0)
.Take(3)
.ToArray();
if (supports.Length > 0)
{
snapshot.S1 = supports[0];
snapshot.Sgdpl1 = BuildSupportGdpl(player.InventoryManager.GetSupportCardItem(supports[0]));
}
if (supports.Length > 1)
{
snapshot.S2 = supports[1];
snapshot.Sgdpl2 = BuildSupportGdpl(player.InventoryManager.GetSupportCardItem(supports[1]));
}
if (supports.Length > 2)
{
snapshot.S3 = supports[2];
snapshot.Sgdpl3 = BuildSupportGdpl(player.InventoryManager.GetSupportCardItem(supports[2]));
}
return snapshot;
}
private static List<uint> BuildWeaponGdpl(GameWeaponInfo weapon)
{
var gdpl = DecodeGdpl(weapon.TemplateId);
gdpl.Add(weapon.Level);
gdpl.Add(weapon.Evolue);
return gdpl;
}
private static List<uint> BuildSupportGdpl(GameSupportCardInfo? support)
{
if (support == null)
{
return [];
}
var gdpl = DecodeGdpl(support.TemplateId);
gdpl.Add(support.Level);
gdpl.Add(0);
return gdpl;
}
private static List<uint> DecodeGdpl(ulong templateId)
{
return
[
(uint)(templateId & 0xFFFF),
(uint)((templateId >> 16) & 0xFFFF),
(uint)((templateId >> 32) & 0xFFFF),
(uint)((templateId >> 48) & 0xFFFF)
];
}
private static int ComputeIntegral(uint bossLevelId, int diff, int residueTime)
{
if (!GameData.BossPvpBossData.TryGetValue(bossLevelId, out var boss) || diff <= 0 || diff > boss.BossLevel.Count)
{
return 0;
}
var info = boss.BossLevel[diff - 1];
if (info.Count == 0)
{
return 0;
}
var multiplier = info.Count > 2 ? info[2] : 0;
var baseScore = info.Count > 3 ? info[3] : 0;
var residueScore = info.Count > 4 ? info[4] : 0;
var total = (baseScore + residueScore * Math.Max(0, residueTime)) * multiplier;
return (int)Math.Floor(total + 0.5);
}
private static uint? TryGetPositionDiffSid(uint bossLevelId)
{
var season = GetOpenSeason();
if (season == null)
{
return null;
}
var index = season.BossIds.FindIndex(x => x == bossLevelId);
return index >= 0 ? DiffStartId + (uint)(index + 1) : null;
}
private static BossPvpBossChallengeExcel? GetOpenSeason()
{
var now = DateTimeOffset.Now;
var current = GameData.BossPvpBossChallengeData.Values
.OrderBy(x => x.ID)
.FirstOrDefault(x =>
{
var startAt = ParseBossTime(x.StartTime);
var endAt = ParseBossTime(x.EndTime);
return startAt != null && endAt != null && now >= startAt && now <= endAt;
});
return current ?? GameData.BossPvpBossChallengeData.Values.OrderBy(x => x.ID).FirstOrDefault();
}
private static uint GetDailyChallengeNum()
{
var now = DateTime.Now;
if (now.Hour < 4)
{
now = now.AddHours(-4);
}
var week = now.DayOfWeek == DayOfWeek.Sunday ? 7 : (int)now.DayOfWeek;
return GameData.BossPvpNumData.TryGetValue((uint)week, out var count) ? count.Num : 8;
}
private static int ReadInt(PlayerInstance player, uint sid)
{
var attr = player.Data.StrAttrs.FirstOrDefault(x => x.Gid == GroupId && x.Sid == sid)?.Val;
return int.TryParse(attr, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value) ? value : 0;
}
private static void EnsureStr(PlayerInstance player, uint sid, string value, NtfSyncPlayer sync)
{
var attr = player.Data.StrAttrs.FirstOrDefault(x => x.Gid == GroupId && x.Sid == sid);
if (attr != null)
{
return;
}
SetStr(player, sid, value, sync);
}
private static void SetStr(PlayerInstance player, uint sid, string value, NtfSyncPlayer sync)
{
player.SetStrAttr(GroupId, sid, value);
sync.CustomStr[player.ToShiftedAttrKey(GroupId, sid)] = value;
}
private static uint GetBossSid(uint bossLevelId, uint offset) => (LevelStride * bossLevelId) + LevelStartSid + offset;
private static string EmptySnapshotJson() => System.Text.Json.JsonSerializer.Serialize(new BossPvpRoleSnapshot(), JsonOptions);
private static T? Deserialize<T>(string? param)
{
if (string.IsNullOrWhiteSpace(param))
{
return default;
}
return System.Text.Json.JsonSerializer.Deserialize<T>(param, JsonOptions);
}
private static DateTimeOffset? ParseBossTime(string? value)
{
if (string.IsNullOrWhiteSpace(value))
{
return null;
}
var raw = value.Trim().Trim('[', ']');
if (!DateTime.TryParseExact(raw, "yyyyMMddHHmm", CultureInfo.InvariantCulture, DateTimeStyles.None, out var localTime))
{
return null;
}
return new DateTimeOffset(localTime);
}
private sealed class EnterLevelParam
{
[JsonPropertyName("nID")] public uint NId { get; set; }
}
private sealed class RecordParam
{
[JsonPropertyName("nID")] public uint NId { get; set; }
[JsonPropertyName("nDiff")] public int NDiff { get; set; }
[JsonPropertyName("nTime")] public double NTime { get; set; }
[JsonPropertyName("ResidueTime")] public int ResidueTime { get; set; }
[JsonPropertyName("bRecord")] public bool BRecord { get; set; }
[JsonPropertyName("nTeamID")] public uint NTeamId { get; set; }
}
private sealed class SettlementParam
{
[JsonPropertyName("nID")] public uint NId { get; set; }
[JsonPropertyName("nDiff")] public int NDiff { get; set; }
[JsonPropertyName("nTime")] public double NTime { get; set; }
[JsonPropertyName("ResidueTime")] public int ResidueTime { get; set; }
[JsonPropertyName("nTeamID")] public uint NTeamId { get; set; }
}
private sealed class FailParam
{
[JsonPropertyName("nID")] public uint NId { get; set; }
}
private sealed class MopupParam
{
[JsonPropertyName("nID")] public uint NId { get; set; }
[JsonPropertyName("nDiff")] public int NDiff { get; set; }
}
private sealed class RewardParam
{
[JsonPropertyName("tbTaskID")] public List<uint> TaskIds { get; set; } = [];
}
private sealed class BossPvpRoleSnapshot
{
[JsonPropertyName("role")] public uint Role { get; set; }
[JsonPropertyName("weapon")] public uint Weapon { get; set; }
[JsonPropertyName("s1")] public uint S1 { get; set; }
[JsonPropertyName("s2")] public uint S2 { get; set; }
[JsonPropertyName("s3")] public uint S3 { get; set; }
[JsonPropertyName("wgdpl")] public List<uint> Wgdpl { get; set; } = [];
[JsonPropertyName("wslot")] public Dictionary<uint, ulong> Wslot { get; set; } = [];
[JsonPropertyName("sgdpl1")] public List<uint> Sgdpl1 { get; set; } = [];
[JsonPropertyName("sgdpl2")] public List<uint> Sgdpl2 { get; set; } = [];
[JsonPropertyName("sgdpl3")] public List<uint> Sgdpl3 { get; set; } = [];
}
}

View File

@@ -4,6 +4,7 @@ using MikuSB.Database;
using MikuSB.Database.Inventory;
using MikuSB.Enums.Item;
using MikuSB.GameServer.Game.Player;
using MikuSB.GameServer.Game.Support;
using MikuSB.GameServer.Server.Packet.Send.Misc;
namespace MikuSB.GameServer.Game.Inventory;
@@ -135,7 +136,17 @@ public class InventoryManager(PlayerInstance player) : BasePlayerManager(player)
ItemType = genre,
ItemCount = 1,
Level = cardLevel,
AffixId = 0,
};
var affixCount = cardLevel >= spCard.MaxLevel ? spCard.TotalAffixCount : spCard.InitialAffixCount;
for (int i = 0; i < affixCount && i < spCard.AffixPool.Count; i++)
{
var (affixId, tier) = SupportAffixService.GenerateRandomAffix(spCard.AffixPool[i]);
if (affixId == 0) continue;
SupportAffixStateService.SetAffix(info, i + 1, affixId, tier);
}
InventoryData.SupportCards[info.UniqueId] = info;
if (sendPacket) await Player.SendPacket(new PacketNtfCallScript([info]));
@@ -197,6 +208,27 @@ public class InventoryManager(PlayerInstance player) : BasePlayerManager(player)
return InventoryData.Items.Values.FirstOrDefault(x => x.TemplateId == templateId);
}
public async ValueTask<BaseGameItemInfo?> AddMonsterCardItem(uint detail, uint particular, uint level = 1, bool sendPacket = true)
{
const ItemTypeEnum genre = ItemTypeEnum.TYPE_MONSTER_CARD;
var templateId = GameResourceTemplateId.FromGdpl((uint)genre, detail, particular, level);
if (!GameData.MonsterCardData.ContainsKey(templateId))
return null;
var monsterInfo = new BaseGameItemInfo
{
TemplateId = templateId,
UniqueId = InventoryData.NextUniqueUid++,
ItemType = genre,
ItemCount = 1
};
InventoryData.Items[monsterInfo.UniqueId] = monsterInfo;
if (sendPacket) await Player.SendPacket(new PacketNtfCallScript([monsterInfo]));
return monsterInfo;
}
private static uint GetSuppliesMaxCount(SuppliesExcel suppliesData) =>
suppliesData.Genre == 5 && suppliesData.Detail == 4 ? 999999u : 99999u;

View File

@@ -196,7 +196,7 @@ public class PlayerInstance(PlayerGameData data)
return proto;
}
public Proto.Player ToPlayerProto()
public Proto.Player ToPlayerProto(bool includeSupportCards = true)
{
BuildPlayerAttr();
var displayName = PlayerGameData.NormalizeDisplayName(Data.Name);
@@ -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,
@@ -217,7 +219,10 @@ public class PlayerInstance(PlayerGameData data)
foreach (var item in InventoryManager.InventoryData.Items.Values) proto.Items.Add(item.ToProto());
foreach (var skin in InventoryManager.InventoryData.Skins.Values) proto.Items.Add(skin.ToProto());
foreach (var weapon in InventoryManager.InventoryData.Weapons.Values) proto.Items.Add(weapon.ToProto());
foreach (var card in InventoryManager.InventoryData.SupportCards.Values) proto.Items.Add(card.ToProto());
if (includeSupportCards)
{
foreach (var card in InventoryManager.InventoryData.SupportCards.Values) proto.Items.Add(card.ToProto());
}
foreach (var x in Data.Attrs)
{
uint gid = x.Gid;
@@ -239,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;
@@ -292,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();
@@ -422,6 +450,14 @@ public class PlayerInstance(PlayerGameData data)
yield return (22, levelId, 1_700_000_000);
}
// Role fragment chapters use Condition.PRE_LEVEL against Launch.GPASSID as well.
// Mark every role level as cleared so character-specific stages beyond the first one unlock.
foreach (var levelId in GameData.RoleLevelData.Keys)
{
yield return (21, levelId, 7);
yield return (22, levelId, 1_700_000_000);
}
foreach (var guide in GameData.GuideData.Values)
{
yield return (4, guide.ID, 999);
@@ -434,4 +470,4 @@ public class PlayerInstance(PlayerGameData data)
yield return (132, 1, 0);
}
#endregion
}
}

View File

@@ -0,0 +1,50 @@
using MikuSB.Data;
namespace MikuSB.GameServer.Game.Support;
public static class SupportAffixService
{
public static (uint AffixId, uint Tier) GenerateRandomAffix(int poolId, IEnumerable<uint>? excludedAffixIds = null)
{
if (!GameData.SupportAffixPoolData.TryGetValue(poolId, out var pool))
return (0, 0);
var groups = pool.Groups.ToList();
if (groups.Count == 0)
return (0, 0);
var totalWeight = groups.Sum(x => x.Weight);
var roll = Random.Shared.Next(totalWeight);
var cumulative = 0;
var selectedAffixs = groups[0].Affixs;
foreach (var (affixIds, weight) in groups)
{
cumulative += weight;
if (roll < cumulative)
{
selectedAffixs = affixIds;
break;
}
}
if (selectedAffixs.Count == 0)
return (0, 0);
var excluded = excludedAffixIds?.ToHashSet() ?? [];
var candidates = selectedAffixs.Where(x => !excluded.Contains((uint)x)).ToList();
if (candidates.Count == 0)
candidates = selectedAffixs.ToList();
var affixId = candidates[Random.Shared.Next(candidates.Count)];
var tierCount = GameData.SupportAffixData.GetValueOrDefault(affixId)?.TierCount ?? 5;
var tier = (uint)(Random.Shared.Next(tierCount) + 1);
return ((uint)affixId, tier);
}
public static uint GenerateTier(uint affixId)
{
var tierCount = GameData.SupportAffixData.GetValueOrDefault((int)affixId)?.TierCount ?? 5;
return (uint)(Random.Shared.Next(tierCount) + 1);
}
}

View File

@@ -0,0 +1,70 @@
using MikuSB.Database.Inventory;
namespace MikuSB.GameServer.Game.Support;
public static class SupportAffixStateService
{
public const int PairSize = 2;
public const int MaxLogicalSlots = 5;
public const int ActiveThirdAffixSlot = 3;
public const int PendingMaxAffixSlot = 4;
public const int PendingInitialAffixSlot = 5;
public static void EnsureCapacity(GameSupportCardInfo card, int logicalSlot = MaxLogicalSlots)
{
var minCount = Math.Clamp(logicalSlot, 1, MaxLogicalSlots) * PairSize;
while (card.Affixs.Count < minCount)
card.Affixs.Add(0);
}
public static (uint AffixId, uint Tier) GetAffix(GameSupportCardInfo card, int logicalSlot)
{
if (logicalSlot < 1 || logicalSlot > MaxLogicalSlots)
return (0, 0);
var index = (logicalSlot - 1) * PairSize;
if (card.Affixs.Count <= index + 1)
return (0, 0);
return (card.Affixs[index], card.Affixs[index + 1]);
}
public static bool HasAffix(GameSupportCardInfo card, int logicalSlot)
{
var (affixId, tier) = GetAffix(card, logicalSlot);
return affixId > 0 && tier > 0;
}
public static void SetAffix(GameSupportCardInfo card, int logicalSlot, uint affixId, uint tier)
{
if (logicalSlot < 1 || logicalSlot > MaxLogicalSlots)
return;
EnsureCapacity(card, logicalSlot);
var index = (logicalSlot - 1) * PairSize;
card.Affixs[index] = affixId;
card.Affixs[index + 1] = tier;
}
public static void ClearAffix(GameSupportCardInfo card, int logicalSlot)
{
SetAffix(card, logicalSlot, 0, 0);
}
public static void CopyAffix(GameSupportCardInfo card, int fromSlot, int toSlot)
{
var (affixId, tier) = GetAffix(card, fromSlot);
SetAffix(card, toSlot, affixId, tier);
}
public static uint GetVisibleInitialAffixIndex(GameSupportCardInfo card)
{
return HasAffix(card, PendingInitialAffixSlot) ? card.AffixId : 0;
}
public static void NormalizePendingState(GameSupportCardInfo card)
{
if (!HasAffix(card, PendingInitialAffixSlot))
card.AffixId = 0;
}
}

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

@@ -0,0 +1,13 @@
using MikuSB.GameServer.Game.BossPvp;
namespace MikuSB.GameServer.Server.CallGS.Handlers.BossPvp;
[CallGSApi("BossPvpLogic_EnterLevel")]
public class BossPvpLogic_EnterLevel : ICallGSHandler
{
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var response = BossPvpService.HandleEnterLevel(param);
await CallGSRouter.SendScript(connection, "BossPvpLogic_EnterLevel", System.Text.Json.JsonSerializer.Serialize(response));
}
}

View File

@@ -0,0 +1,13 @@
using MikuSB.GameServer.Game.BossPvp;
namespace MikuSB.GameServer.Server.CallGS.Handlers.BossPvp;
[CallGSApi("BossPvpLogic_GetOpenID")]
public class BossPvpLogic_GetOpenID : ICallGSHandler
{
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var (response, sync) = await BossPvpService.HandleGetOpenIdAsync(connection.Player!);
await CallGSRouter.SendScript(connection, "BossPvpLogic_GetOpenID", System.Text.Json.JsonSerializer.Serialize(response), sync);
}
}

View File

@@ -0,0 +1,13 @@
using MikuSB.GameServer.Game.BossPvp;
namespace MikuSB.GameServer.Server.CallGS.Handlers.BossPvp;
[CallGSApi("BossPvpLogic_GetReward")]
public class BossPvpLogic_GetReward : ICallGSHandler
{
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var response = BossPvpService.HandleGetReward(param);
await CallGSRouter.SendScript(connection, "BossPvpLogic_GetReward", System.Text.Json.JsonSerializer.Serialize(response));
}
}

View File

@@ -0,0 +1,14 @@
using MikuSB.GameServer.Game.BossPvp;
namespace MikuSB.GameServer.Server.CallGS.Handlers.BossPvp;
[CallGSApi("BossPvpLogic_LevelFail")]
public class BossPvpLogic_LevelFail : ICallGSHandler
{
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var node = System.Text.Json.Nodes.JsonNode.Parse(param);
var (response, sync) = BossPvpService.HandleFail(connection.Player!, node);
await CallGSRouter.SendScript(connection, "BossPvpLogic_LevelFail", response.ToJsonString(), sync);
}
}

View File

@@ -0,0 +1,13 @@
using MikuSB.GameServer.Game.BossPvp;
namespace MikuSB.GameServer.Server.CallGS.Handlers.BossPvp;
[CallGSApi("BossPvpLogic_LevelMopup")]
public class BossPvpLogic_LevelMopup : ICallGSHandler
{
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var (response, sync) = BossPvpService.HandleMopup(connection.Player!, param);
await CallGSRouter.SendScript(connection, "BossPvpLogic_LevelMopup", System.Text.Json.JsonSerializer.Serialize(response), sync);
}
}

View File

@@ -0,0 +1,14 @@
using MikuSB.GameServer.Game.BossPvp;
namespace MikuSB.GameServer.Server.CallGS.Handlers.BossPvp;
[CallGSApi("BossPvpLogic_LevelSettlement")]
public class BossPvpLogic_LevelSettlement : ICallGSHandler
{
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var node = System.Text.Json.Nodes.JsonNode.Parse(param);
var (response, sync) = BossPvpService.HandleSettlement(connection.Player!, node);
await CallGSRouter.SendScript(connection, "BossPvpLogic_LevelSettlement", response.ToJsonString(), sync);
}
}

View File

@@ -0,0 +1,13 @@
using MikuSB.GameServer.Game.BossPvp;
namespace MikuSB.GameServer.Server.CallGS.Handlers.BossPvp;
[CallGSApi("BossPvpLogic_Record")]
public class BossPvpLogic_Record : ICallGSHandler
{
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var (response, sync) = BossPvpService.HandleRecord(connection.Player!, param);
await CallGSRouter.SendScript(connection, "BossPvpLogic_Record", System.Text.Json.JsonSerializer.Serialize(response), sync);
}
}

View File

@@ -1,6 +1,11 @@
using System.Text.Json;
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;
@@ -10,17 +15,20 @@ public class Chapter_DealLevelSettlement : ICallGSHandler
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var req = JsonSerializer.Deserialize<DealLevelSettlementParam>(param);
NtfSyncPlayer? extraSync = null;
var response = new JsonObject
{
["sCmd"] = req?.SCmd ?? "Chapter_LevelSettlement",
["tbParam"] = BuildSettlementPayload(req?.SCmd, req?.TbParam)
["tbParam"] = BuildSettlementPayload(connection, req?.SCmd, req?.TbParam, out extraSync)
};
await CallGSRouter.SendScript(connection, "Chapter_DealLevelSettlement", response.ToJsonString());
await CallGSRouter.SendScript(connection, "Chapter_DealLevelSettlement", response.ToJsonString(), extraSync!);
}
private static JsonNode BuildSettlementPayload(string? sCmd, JsonNode? tbParam)
private static JsonNode BuildSettlementPayload(Connection connection, string? sCmd, JsonNode? tbParam, out NtfSyncPlayer? extraSync)
{
extraSync = null;
if (string.Equals(sCmd, "Chapter_LevelSettlement", StringComparison.Ordinal))
{
return new JsonArray();
@@ -37,8 +45,67 @@ public class Chapter_DealLevelSettlement : ICallGSHandler
return result;
}
if (string.Equals(sCmd, "BossPvpLogic_LevelSettlement", StringComparison.Ordinal))
{
var normalized = NormalizeBossPvpSettlement(tbParam);
var (response, sync) = BossPvpService.HandleSettlement(connection.Player!, normalized);
extraSync = sync;
return response;
}
if (string.Equals(sCmd, "BossPvpLogic_LevelFail", StringComparison.Ordinal))
{
var (response, sync) = BossPvpService.HandleFail(connection.Player!, tbParam);
extraSync = sync;
return response;
}
if (string.Equals(sCmd, "TowerLevel_LevelSettlement", StringComparison.Ordinal))
{
var (response, sync) = TowerLevel_LevelSettlement.HandleSettlement(connection.Player!, tbParam);
extraSync = sync;
return response;
}
if (string.Equals(sCmd, "TowerEventChapter_LevelSettlement", StringComparison.Ordinal))
{
var (response, sync) = TowerEventChapter_LevelSettlement.HandleSettlement(connection.Player!, tbParam);
extraSync = sync;
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();
}
private static JsonNode? NormalizeBossPvpSettlement(JsonNode? tbParam)
{
if (tbParam is not JsonObject obj)
return tbParam;
var clone = obj.DeepClone() as JsonObject ?? obj;
if (clone.TryGetPropertyValue("ResidueTime", out var residueNode) &&
residueNode is JsonValue residueValue &&
residueValue.TryGetValue<double>(out var residueTime))
{
clone["ResidueTime"] = (int)Math.Max(0, Math.Round(residueTime, MidpointRounding.AwayFromZero));
}
return clone;
}
}
internal sealed class DealLevelSettlementParam

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,552 @@
using MikuSB.Data;
using MikuSB.Data.Excel;
using MikuSB.Database;
using MikuSB.Database.Player;
using MikuSB.Enums.Item;
using MikuSB.GameServer.Game.Player;
using MikuSB.Proto;
using Newtonsoft.Json.Linq;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace MikuSB.GameServer.Server.CallGS.Handlers.Gacha;
[CallGSApi("Gacha_Launch")]
public class Gacha_Launch : ICallGSHandler
{
private const uint GachaGid = 5;
private const uint GachaSgid = 42;
private const uint SidTotalTime = 1;
private const uint SidDailyTotalTime = 2;
private const uint Interval = 10;
private const uint SidTimeInheritStart = 20000;
private const uint SidTimeNotInheritStart = 10;
private const uint SidAddTimeItem = 1;
private const uint SidAddTimeProb = 2;
private const uint SidAddProtectType = 3;
private const uint SidAddTotalTime = 7;
private const int UpSelectIndex = 0;
private const int UpSelectGetFlagIndex = 1;
private static readonly Random Rng = new();
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var player = connection.Player!;
var req = JsonSerializer.Deserialize<GachaLaunchParam>(param);
if (req == null || req.NId == 0 || req.NTime is not (1 or 10))
{
await CallGSRouter.SendScript(connection, "Gacha_Launch", "{\"sErr\":\"error.BadParam\"}");
return;
}
if (!GameData.GachaData.TryGetValue((uint)req.NId, out var gachaCfg))
{
await CallGSRouter.SendScript(connection, "Gacha_Launch", "{\"sErr\":\"error.BadParam\"}");
return;
}
var poolNames = (gachaCfg.Pool ?? [])
.Where(GameData.GachaPoolData.ContainsKey)
.ToList();
var allPoolItems = poolNames
.SelectMany(p => GameData.GachaPoolData[p])
.ToList();
if (allPoolItems.Count == 0 || !GameData.GachaProbabilityData.TryGetValue(gachaCfg.Probability, out var baseProbCfg))
{
await CallGSRouter.SendScript(connection, "Gacha_Launch", "{\"sErr\":\"error.BadParam\"}");
return;
}
var pityState = LoadPityState(player, gachaCfg);
var upSelectState = LoadUpSelectState(player, gachaCfg);
var config = BuildRuntimeConfig(gachaCfg, poolNames);
var awards = new List<List<uint>>();
var tbNew = new List<int>();
var tbTrigger = new List<bool>();
var syncItems = new List<Item>();
var sync = new NtfSyncPlayer();
for (int i = 0; i < req.NTime; i++)
{
var forceTopUp = config.UpTarget != null && pityState.ProtectType == 2;
var hitHardPity = config.ProtectThreshold > 0 && pityState.ItemCount + 1 >= config.ProtectThreshold;
var useTenGuarantee = gachaCfg.ProbabilityTen != 0
&& pityState.TenCount + 1 >= 10
&& !HasGuaranteedTenRarity(config, awards);
GachaProbabilityExcel probCfg = baseProbCfg;
if (useTenGuarantee && GameData.GachaProbabilityData.TryGetValue(gachaCfg.ProbabilityTen, out var tenProbCfg))
probCfg = tenProbCfg;
GachaPoolItem? item;
bool trigger = false;
if (hitHardPity)
{
item = PickGuaranteedItem(gachaCfg, config, preferUp: forceTopUp);
trigger = item != null;
}
else
{
var rarity = RollRarity(probCfg);
item = forceTopUp && config.UpTarget != null && rarity >= config.TopRarity
? PickGuaranteedItem(gachaCfg, config, preferUp: true)
: PickItem(allPoolItems, rarity);
trigger = forceTopUp && item != null && config.UpTarget != null && item.Rarity == config.UpTarget.Rarity;
}
if (item != null && upSelectState.SelectedItem != null && item.Rarity >= config.TopRarity)
{
bool forceSelected = upSelectState.GuaranteedNext;
bool shouldSelect = forceSelected || Rng.Next(100) < 50;
if (shouldSelect)
{
var selectedItem = FindExactItem(allPoolItems, upSelectState.SelectedItem);
if (selectedItem != null && selectedItem.Rarity >= config.TopRarity)
item = selectedItem;
}
}
if (item == null || item.GDPL.Count < 4)
{
tbTrigger.Add(false);
continue;
}
var g = item.GDPL[0];
var d = item.GDPL[1];
var p = item.GDPL[2];
var l = item.GDPL[3];
awards.Add([g, d, p, l]);
tbTrigger.Add(trigger);
UpdatePityState(pityState, config, item);
UpdateUpSelectState(upSelectState, config, item);
var itemType = (ItemTypeEnum)g;
switch (itemType)
{
case ItemTypeEnum.TYPE_CARD:
{
var alreadyOwned = player.CharacterManager.GetCharacterGDPL(itemType, (int)d, (int)p) != null;
if (!alreadyOwned)
{
var charInfo = await player.CharacterManager.AddCharacter(itemType, d, p, sendPacket: false);
if (charInfo != null)
{
syncItems.Add(charInfo.ToProto());
tbNew.Add(awards.Count);
}
}
break;
}
case ItemTypeEnum.TYPE_WEAPON:
{
var weaponInfo = await player.InventoryManager.AddWeaponItem(itemType, d, p, l, sendPacket: false);
if (weaponInfo != null) syncItems.Add(weaponInfo.ToProto());
break;
}
case ItemTypeEnum.TYPE_SUPPORT:
{
var cardInfo = await player.InventoryManager.AddSupportCardItem(d, p, l, sendPacket: false);
if (cardInfo != null) syncItems.Add(cardInfo.ToProto());
break;
}
}
}
if (awards.Count == 0)
{
await CallGSRouter.SendScript(connection, "Gacha_Launch", "{\"sErr\":\"error.BadParam\"}");
return;
}
SavePityState(player, gachaCfg, pityState, awards.Count, sync);
SaveUpSelectState(player, gachaCfg, upSelectState, sync);
DatabaseHelper.SaveDatabaseType(player.Data);
DatabaseHelper.SaveDatabaseType(player.InventoryManager.InventoryData);
DatabaseHelper.SaveDatabaseType(player.CharacterManager.CharacterData);
sync.Items.AddRange(syncItems);
var rsp = BuildResponse(req.NId, awards, tbNew, tbTrigger);
await CallGSRouter.SendScript(connection, "Gacha_Launch", rsp, sync);
}
private static bool HasGuaranteedTenRarity(GachaRuntimeConfig config, List<List<uint>> awards)
{
if (awards.Count == 0)
return false;
int windowStart = awards.Count >= 9 ? awards.Count - 9 : 0;
for (int i = windowStart; i < awards.Count; i++)
{
var award = awards[i];
if (award.Count < 4)
continue;
var template = FindPoolItemByGdpl(config.AllPoolItems, award);
if (template != null && template.Rarity >= config.TenGuaranteeRarity)
return true;
}
return false;
}
private static GachaPoolItem? FindPoolItemByGdpl(List<GachaPoolItem> pool, List<uint> gdpl) =>
pool.FirstOrDefault(x =>
x.GDPL.Count >= 4 &&
x.GDPL[0] == gdpl[0] &&
x.GDPL[1] == gdpl[1] &&
x.GDPL[2] == gdpl[2] &&
x.GDPL[3] == gdpl[3]);
private static GachaPoolItem? FindExactItem(List<GachaPoolItem> pool, uint[] gdpl) =>
pool.FirstOrDefault(x =>
x.GDPL.Count >= 4 &&
x.GDPL[0] == gdpl[0] &&
x.GDPL[1] == gdpl[1] &&
x.GDPL[2] == gdpl[2] &&
x.GDPL[3] == gdpl[3]);
private static GachaRuntimeConfig BuildRuntimeConfig(GachaExcel gachaCfg, List<string> poolNames)
{
var allPoolItems = poolNames.SelectMany(name => GameData.GachaPoolData[name]).ToList();
var protectPools = ParsePoolRarities(gachaCfg.ProtectNum);
var upTarget = ParseSinglePoolRarity(gachaCfg.UpNum);
var topRarity = new[] { upTarget?.Rarity ?? 0 }.Concat(protectPools.Select(x => x.Rarity)).Max();
if (topRarity <= 0)
topRarity = allPoolItems.Count == 0 ? 0 : allPoolItems.Max(x => x.Rarity);
return new GachaRuntimeConfig
{
AllPoolItems = allPoolItems,
ProtectThreshold = ParseThreshold(gachaCfg.ProtectNum),
ProtectPools = protectPools,
UpTarget = upTarget,
TopRarity = topRarity,
TenGuaranteeRarity = 4
};
}
private static int ParseThreshold(JToken? token)
{
if (token is not JArray arr || arr.Count == 0)
return 0;
return arr[0]?.Value<int>() ?? 0;
}
private static List<PoolRarityRef> ParsePoolRarities(JToken? token)
{
var result = new List<PoolRarityRef>();
if (token is not JArray arr || arr.Count < 2 || arr[1] is not JArray entries)
return result;
foreach (var entry in entries.OfType<JArray>())
{
if (entry.Count < 2)
continue;
var poolName = entry[0]?.Value<string>();
var rarity = entry[1]?.Value<int>() ?? 0;
if (string.IsNullOrWhiteSpace(poolName) || rarity <= 0)
continue;
result.Add(new PoolRarityRef(poolName, rarity));
}
return result;
}
private static PoolRarityRef? ParseSinglePoolRarity(JToken? token)
{
if (token is not JArray arr || arr.Count < 2 || arr[1] is not JArray entry || entry.Count < 2)
return null;
var poolName = entry[0]?.Value<string>();
var rarity = entry[1]?.Value<int>() ?? 0;
return string.IsNullOrWhiteSpace(poolName) || rarity <= 0 ? null : new PoolRarityRef(poolName, rarity);
}
private static GachaPityState LoadPityState(PlayerInstance player, GachaExcel gachaCfg)
{
var baseSid = GetBaseSid(gachaCfg);
return new GachaPityState
{
ItemCount = (int)GetAttr(player, GachaGid, baseSid + SidAddTimeItem),
TenCount = (int)GetAttr(player, GachaGid, baseSid + SidAddTimeProb),
ProtectType = Math.Max(1, (int)GetAttr(player, GachaGid, baseSid + SidAddProtectType)),
PoolTotalTime = (int)GetAttr(player, GachaGid, baseSid + SidAddTotalTime)
};
}
private static void SavePityState(PlayerInstance player, GachaExcel gachaCfg, GachaPityState state, int drawCount, NtfSyncPlayer sync)
{
var baseSid = GetBaseSid(gachaCfg);
SetAttr(player, sync, GachaGid, SidTotalTime, GetAttr(player, GachaGid, SidTotalTime) + (uint)drawCount);
SetAttr(player, sync, GachaGid, SidDailyTotalTime, GetAttr(player, GachaGid, SidDailyTotalTime) + (uint)drawCount);
SetAttr(player, sync, GachaGid, baseSid + SidAddTimeItem, (uint)state.ItemCount);
SetAttr(player, sync, GachaGid, baseSid + SidAddTimeProb, (uint)state.TenCount);
SetAttr(player, sync, GachaGid, baseSid + SidAddProtectType, (uint)Math.Max(1, state.ProtectType));
SetAttr(player, sync, GachaGid, baseSid + SidAddTotalTime, (uint)(state.PoolTotalTime + drawCount));
}
private static GachaUpSelectState LoadUpSelectState(PlayerInstance player, GachaExcel gachaCfg)
{
if (gachaCfg.UpSelect != 1)
return new GachaUpSelectState();
var raw = player.Data.StrAttrs.FirstOrDefault(x => x.Gid == GachaSgid && x.Sid == gachaCfg.ID)?.Val;
if (string.IsNullOrWhiteSpace(raw))
return new GachaUpSelectState();
try
{
var state = JArray.Parse(raw);
uint[]? selected = null;
if (state.Count > UpSelectIndex && state[UpSelectIndex] is JArray selectedArray && selectedArray.Count >= 4)
{
selected =
[
selectedArray[0]?.Value<uint>() ?? 0,
selectedArray[1]?.Value<uint>() ?? 0,
selectedArray[2]?.Value<uint>() ?? 0,
selectedArray[3]?.Value<uint>() ?? 0
];
}
return new GachaUpSelectState
{
SelectedItem = selected,
GuaranteedNext = state.Count > UpSelectGetFlagIndex && (state[UpSelectGetFlagIndex]?.Value<int>() ?? 0) == 1,
RawState = state
};
}
catch
{
return new GachaUpSelectState();
}
}
private static void SaveUpSelectState(PlayerInstance player, GachaExcel gachaCfg, GachaUpSelectState state, NtfSyncPlayer sync)
{
if (gachaCfg.UpSelect != 1 || state.RawState == null)
return;
EnsureArraySize(state.RawState, 2);
state.RawState[UpSelectGetFlagIndex] = state.GuaranteedNext ? 1 : 0;
var value = state.RawState.ToString(Newtonsoft.Json.Formatting.None);
player.SetStrAttr(GachaSgid, gachaCfg.ID, value);
sync.CustomStr[player.ToShiftedAttrKey(GachaSgid, gachaCfg.ID)] = value;
}
private static uint GetBaseSid(GachaExcel gachaCfg)
{
if (gachaCfg.ProtectTag.HasValue)
return SidTimeInheritStart + (gachaCfg.ProtectTag.Value * Interval);
return SidTimeNotInheritStart + (gachaCfg.ID * Interval);
}
private static uint GetAttr(PlayerInstance player, uint gid, uint sid) =>
player.Data.Attrs.FirstOrDefault(x => x.Gid == gid && x.Sid == sid)?.Val ?? 0;
private static void SetAttr(PlayerInstance player, NtfSyncPlayer sync, uint gid, uint sid, uint value)
{
var attr = player.Data.Attrs.FirstOrDefault(x => x.Gid == gid && x.Sid == sid);
if (attr == null)
{
attr = new PlayerAttr { Gid = gid, Sid = sid };
player.Data.Attrs.Add(attr);
}
attr.Val = value;
sync.Custom[player.ToPackedAttrKey(gid, sid)] = value;
sync.Custom[player.ToShiftedAttrKey(gid, sid)] = value;
}
private static void UpdatePityState(GachaPityState state, GachaRuntimeConfig config, GachaPoolItem item)
{
if (item.Rarity >= config.TenGuaranteeRarity)
state.TenCount = 0;
else
state.TenCount++;
if (item.Rarity >= config.TopRarity)
{
state.ItemCount = 0;
if (config.UpTarget != null)
state.ProtectType = IsFromPool(item, config.UpTarget) ? 1 : 2;
else
state.ProtectType = 1;
}
else
{
state.ItemCount++;
}
}
private static void UpdateUpSelectState(GachaUpSelectState state, GachaRuntimeConfig config, GachaPoolItem item)
{
if (state.SelectedItem == null || item.Rarity < config.TopRarity)
return;
state.GuaranteedNext = !MatchesGdpl(item, state.SelectedItem);
}
private static bool MatchesGdpl(GachaPoolItem item, uint[] gdpl) =>
item.GDPL.Count >= 4 &&
item.GDPL[0] == gdpl[0] &&
item.GDPL[1] == gdpl[1] &&
item.GDPL[2] == gdpl[2] &&
item.GDPL[3] == gdpl[3];
private static void EnsureArraySize(JArray state, int size)
{
while (state.Count < size)
state.Add(JValue.CreateNull());
}
private static bool IsFromPool(GachaPoolItem item, PoolRarityRef target) =>
item.Rarity == target.Rarity &&
GameData.GachaPoolData.TryGetValue(target.PoolName, out var pool) &&
pool.Any(x => x.ID == item.ID);
private static int RollRarity(GachaProbabilityExcel prob)
{
var weights = prob.Weights;
int total = weights.Sum();
int roll = Rng.Next(total);
int cumulative = 0;
for (int i = 0; i < weights.Length; i++)
{
cumulative += weights[i];
if (roll < cumulative)
return i + 1;
}
return 3;
}
private static GachaPoolItem? PickGuaranteedItem(GachaExcel gachaCfg, GachaRuntimeConfig config, bool preferUp)
{
if (preferUp && config.UpTarget != null)
{
var upItem = PickItemFromPool(config.UpTarget.PoolName, config.UpTarget.Rarity);
if (upItem != null)
return upItem;
}
foreach (var poolRef in config.ProtectPools)
{
var item = PickItemFromPool(poolRef.PoolName, poolRef.Rarity);
if (item != null)
return item;
}
return PickItem(config.AllPoolItems, config.TopRarity);
}
private static GachaPoolItem? PickItemFromPool(string poolName, int rarity)
{
if (!GameData.GachaPoolData.TryGetValue(poolName, out var pool))
return null;
return PickItem(pool, rarity);
}
private static GachaPoolItem? PickItem(List<GachaPoolItem> pool, int rarity)
{
var candidates = pool.Where(x => x.Rarity == rarity).ToList();
if (candidates.Count == 0)
{
candidates = pool.Where(x => x.Rarity == rarity - 1).ToList();
if (candidates.Count == 0)
return pool.FirstOrDefault();
}
int total = candidates.Sum(x => x.Weight);
if (total <= 0)
return candidates[Rng.Next(candidates.Count)];
int roll = Rng.Next(total);
int cumulative = 0;
foreach (var item in candidates)
{
cumulative += item.Weight;
if (roll < cumulative)
return item;
}
return candidates.Last();
}
private static string BuildResponse(int nId, List<List<uint>> awards, List<int> tbNew, List<bool> tbTrigger)
{
var sb = new StringBuilder();
sb.Append("{\"nId\":");
sb.Append(nId);
sb.Append(",\"tbAwards\":[");
for (int i = 0; i < awards.Count; i++)
{
if (i > 0)
sb.Append(',');
sb.Append('[');
sb.Append(string.Join(',', awards[i]));
sb.Append(']');
}
sb.Append("],\"nBoxCount\":0,\"tbNew\":[");
sb.Append(string.Join(',', tbNew));
sb.Append("],\"tbTrigger\":[");
sb.Append(string.Join(',', tbTrigger.Select(b => b ? "true" : "false")));
sb.Append("]}");
return sb.ToString();
}
}
internal sealed class GachaLaunchParam
{
[JsonPropertyName("nId")]
public int NId { get; set; }
[JsonPropertyName("bPickUp")]
public bool BPickUp { get; set; }
[JsonPropertyName("nTime")]
public int NTime { get; set; }
}
internal sealed class GachaPityState
{
public int ItemCount { get; set; }
public int TenCount { get; set; }
public int ProtectType { get; set; } = 1;
public int PoolTotalTime { get; set; }
}
internal sealed class GachaRuntimeConfig
{
public List<GachaPoolItem> AllPoolItems { get; set; } = [];
public int ProtectThreshold { get; set; }
public List<PoolRarityRef> ProtectPools { get; set; } = [];
public PoolRarityRef? UpTarget { get; set; }
public int TopRarity { get; set; }
public int TenGuaranteeRarity { get; set; }
}
internal sealed class GachaUpSelectState
{
public uint[]? SelectedItem { get; set; }
public bool GuaranteedNext { get; set; }
public JArray? RawState { get; set; } = new();
}
internal sealed record PoolRarityRef(string PoolName, int Rarity);

View File

@@ -0,0 +1,82 @@
using MikuSB.Data;
using MikuSB.Database;
using MikuSB.Proto;
using Newtonsoft.Json.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace MikuSB.GameServer.Server.CallGS.Handlers.Gacha;
[CallGSApi("Gacha_UpSelect")]
public class Gacha_UpSelect : ICallGSHandler
{
private const uint GachaStrGid = 42;
private const int UpSelectIndex = 0;
private const int UpSelectGetFlagIndex = 1;
private const int UpPickPoolIndex = 2;
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var req = JsonSerializer.Deserialize<GachaUpSelectParam>(param);
var player = connection.Player!;
if (req == null || req.NId == 0 || req.Gdpl == null || req.Gdpl.Count < 4)
{
await CallGSRouter.SendScript(connection, "Gacha_UpSelect", "{\"sErr\":\"error.BadParam\"}");
return;
}
if (!GameData.GachaData.TryGetValue((uint)req.NId, out var gachaCfg) || gachaCfg.UpSelect != 1)
{
await CallGSRouter.SendScript(connection, "Gacha_UpSelect", "{\"sErr\":\"error.BadParam\"}");
return;
}
var valid = (gachaCfg.Pool ?? [])
.Where(GameData.GachaPoolData.ContainsKey)
.SelectMany(name => GameData.GachaPoolData[name])
.Any(item =>
item.UPSelectTag == 1 &&
item.GDPL.Count >= 4 &&
item.GDPL[0] == req.Gdpl[0] &&
item.GDPL[1] == req.Gdpl[1] &&
item.GDPL[2] == req.Gdpl[2] &&
item.GDPL[3] == req.Gdpl[3]);
if (!valid)
{
await CallGSRouter.SendScript(connection, "Gacha_UpSelect", "{\"sErr\":\"error.BadParam\"}");
return;
}
var existing = player.Data.StrAttrs.FirstOrDefault(x => x.Gid == GachaStrGid && x.Sid == (uint)req.NId)?.Val;
var state = string.IsNullOrWhiteSpace(existing) ? new JArray() : JArray.Parse(existing);
EnsureArraySize(state, 3);
state[UpSelectIndex] = new JArray(req.Gdpl);
state[UpSelectGetFlagIndex] = 0;
if (state[UpPickPoolIndex] == null)
state[UpPickPoolIndex] = 0;
player.SetStrAttr(GachaStrGid, (uint)req.NId, state.ToString(Newtonsoft.Json.Formatting.None));
DatabaseHelper.SaveDatabaseType(player.Data);
var sync = new NtfSyncPlayer();
sync.CustomStr[player.ToShiftedAttrKey(GachaStrGid, (uint)req.NId)] = state.ToString(Newtonsoft.Json.Formatting.None);
await CallGSRouter.SendScript(connection, "Gacha_UpSelect", "{}", sync);
}
private static void EnsureArraySize(JArray state, int size)
{
while (state.Count < size)
state.Add(JValue.CreateNull());
}
}
internal sealed class GachaUpSelectParam
{
[JsonPropertyName("nId")]
public int NId { get; set; }
[JsonPropertyName("gdpl")]
public List<uint>? Gdpl { get; set; }
}

View File

@@ -0,0 +1,127 @@
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.Girl;
[CallGSApi("GirlCard_UpBySpecialBreak")]
public class GirlCard_UpBySpecialBreak : ICallGSHandler
{
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var player = connection.Player!;
var req = JsonSerializer.Deserialize<GirlCardUpBySpecialBreakParam>(param);
if (req == null || req.CardId == 0)
{
await CallGSRouter.SendScript(connection, "GirlCard_UpBySpecialBreak", "{\"sErr\":\"error.BadParam\"}");
return;
}
var card = player.CharacterManager.GetCharacterByGUID((uint)req.CardId);
if (card == null)
{
await CallGSRouter.SendScript(connection, "GirlCard_UpBySpecialBreak", "{\"sErr\":\"error.BadParam\"}");
return;
}
var cardTemplate = GameData.CardData.Values.FirstOrDefault(x =>
GameResourceTemplateId.FromGdpl(x.Genre, x.Detail, x.Particular, x.Level) == card.TemplateId);
if (cardTemplate == null)
{
await CallGSRouter.SendScript(connection, "GirlCard_UpBySpecialBreak", "{\"sErr\":\"error.BadParam\"}");
return;
}
if (cardTemplate.BreakMatID <= 10000 ||
!GameData.SpecialBreakData.TryGetValue(cardTemplate.BreakMatID, out var specialBreakExcel))
{
await CallGSRouter.SendScript(connection, "GirlCard_UpBySpecialBreak", "{\"sErr\":\"error.BadParam\"}");
return;
}
var nextBreak = card.Break + 1;
if (!specialBreakExcel.HasBreakLevel(nextBreak))
{
await CallGSRouter.SendScript(connection, "GirlCard_UpBySpecialBreak", "{\"sErr\":\"tip.already_max_break\"}");
return;
}
var requestedMaterials = new Dictionary<ulong, uint>();
foreach (var row in specialBreakExcel.GetItems(nextBreak))
{
if (row.Count < 5)
continue;
var templateId = GameResourceTemplateId.FromGdpl(
(uint)Math.Max(0, row[0]),
(uint)Math.Max(0, row[1]),
(uint)Math.Max(0, row[2]),
(uint)Math.Max(0, row[3]));
var count = (uint)Math.Max(0, row[4]);
if (templateId == 0 || count == 0)
continue;
requestedMaterials[templateId] = requestedMaterials.GetValueOrDefault(templateId) + count;
}
if (requestedMaterials.Count == 0)
{
await CallGSRouter.SendScript(connection, "GirlCard_UpBySpecialBreak", "{\"sErr\":\"tip.not_material_for_break\"}");
return;
}
foreach (var (templateId, count) in requestedMaterials)
{
var item = player.InventoryManager.InventoryData.Items.Values.FirstOrDefault(x => x.TemplateId == templateId);
if (item == null || item.ItemCount < count)
{
await CallGSRouter.SendScript(connection, "GirlCard_UpBySpecialBreak", "{\"sErr\":\"tip.not_material_for_break\"}");
return;
}
}
var syncItems = new List<Item>();
foreach (var (templateId, count) in requestedMaterials)
{
var item = player.InventoryManager.InventoryData.Items.Values.First(x => x.TemplateId == templateId);
item.ItemCount -= count;
if (item.ItemCount == 0)
{
player.InventoryManager.InventoryData.Items.Remove(item.UniqueId);
syncItems.Add(BuildRemovedProto(item));
}
else
{
syncItems.Add(item.ToProto());
}
}
card.Break = nextBreak;
syncItems.Add(card.ToProto());
DatabaseHelper.SaveDatabaseType(player.InventoryManager.InventoryData);
DatabaseHelper.SaveDatabaseType(player.CharacterManager.CharacterData);
var sync = new NtfSyncPlayer();
sync.Items.AddRange(syncItems);
await CallGSRouter.SendScript(connection, "GirlCard_UpBySpecialBreak", "{}", sync);
}
private static Item BuildRemovedProto(BaseGameItemInfo item)
{
var proto = item.ToProto();
proto.Count = 0;
return proto;
}
}
internal sealed class GirlCardUpBySpecialBreakParam
{
[JsonPropertyName("nCardId")]
public int CardId { 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,43 @@
using MikuSB.Database;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace MikuSB.GameServer.Server.CallGS.Handlers.Lineup;
[CallGSApi("Lineups_Update")]
public class Lineups_Update : ICallGSHandler
{
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var req = JsonSerializer.Deserialize<List<LineupUpdateBatchParam>>(param);
if (req == null)
{
await CallGSRouter.SendScript(connection, "UpdateLineup", "{}");
return;
}
foreach (var lineup in req)
{
if (lineup == null)
continue;
await connection.Player!.LineupManager.UpdateLineup(
lineup.Index,
lineup.Member1,
lineup.Member2,
lineup.Member3);
}
DatabaseHelper.SaveDatabaseType(connection.Player!.LineupManager.LineupData);
await CallGSRouter.SendScript(connection, "UpdateLineup", "{}");
}
}
internal sealed class LineupUpdateBatchParam
{
[JsonPropertyName("name")] public string Name { get; set; } = "";
[JsonPropertyName("index")] public int Index { get; set; }
[JsonPropertyName("member1")] public uint Member1 { get; set; }
[JsonPropertyName("member2")] public uint Member2 { get; set; }
[JsonPropertyName("member3")] public uint Member3 { 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,10 @@
namespace MikuSB.GameServer.Server.CallGS.Handlers.Misc;
[CallGSApi("ExtendFightDynamicLog")]
public class ExtendFightDynamicLog : ICallGSHandler
{
public Task Handle(Connection connection, string param, ushort seqNo)
{
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,10 @@
namespace MikuSB.GameServer.Server.CallGS.Handlers.Misc;
[CallGSApi("ExtendFightLog")]
public class ExtendFightLog : ICallGSHandler
{
public Task Handle(Connection connection, string param, ushort seqNo)
{
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,71 @@
using MikuSB.Data;
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.Rogue3D;
// Enters the Rogue3D season level. Returns a random seed used by the client for map generation.
// Persists SeasonGameplayId (sid=1006) and SeasonEnterFlag (sid=1008) as player attributes (GroupId=124).
// param: {"nDiffId", "nTeamID", "tbTeam", "tbBuffList", "tbLog"}
// Response: {"nSeed": int} on success, {"sErr": "key"} on failure
[CallGSApi("Rogue3D_EnterSeasonLevel")]
public class Rogue3D_EnterSeasonLevel : ICallGSHandler
{
private const uint GroupId = 124;
private const uint SeasonGameplayIdSid = 1006;
private const uint SeasonEnterFlagSid = 1008;
private static readonly Random Random = new();
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var req = JsonSerializer.Deserialize<EnterSeasonLevelParam>(param);
if (req == null)
{
await CallGSRouter.SendScript(connection, "Rogue3D_EnterSeasonLevel", "{\"nSeed\":0}");
return;
}
if (!GameData.Rogue3DDifficultData.TryGetValue(req.DiffId, out var cfg) || cfg.GameplayGroup.Count == 0)
{
await CallGSRouter.SendScript(connection, "Rogue3D_EnterSeasonLevel", "{\"sErr\":\"rogue3.massage_gameProcessError\"}");
return;
}
var player = connection.Player!;
var sync = new NtfSyncPlayer();
SetAttr(player, SeasonGameplayIdSid, cfg.GameplayGroup[0], sync);
SetAttr(player, SeasonEnterFlagSid, 1, sync);
var seed = Random.Next(1, 1_000_000_000);
await CallGSRouter.SendScript(connection, "Rogue3D_EnterSeasonLevel", $"{{\"nSeed\":{seed}}}", sync);
}
private static void SetAttr(PlayerInstance player, uint sid, uint val, NtfSyncPlayer sync)
{
var attr = player.Data.Attrs.FirstOrDefault(x => x.Gid == GroupId && x.Sid == sid);
if (attr == null)
{
attr = new PlayerAttr { Gid = GroupId, Sid = sid };
player.Data.Attrs.Add(attr);
}
if (attr.Val == val)
{
return;
}
attr.Val = val;
sync.Custom[player.ToPackedAttrKey(GroupId, sid)] = val;
sync.Custom[player.ToShiftedAttrKey(GroupId, sid)] = val;
}
}
internal sealed class EnterSeasonLevelParam
{
[JsonPropertyName("nDiffId")]
public uint DiffId { get; set; }
}

View File

@@ -0,0 +1,47 @@
using MikuSB.Database.Player;
using MikuSB.Proto;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace MikuSB.GameServer.Server.CallGS.Handlers.Rogue3D;
// Selects the Rogue3D season talent and persists it as player attribute (GroupId=124, TalentId=1007).
// param: {"nTalentId": int}
// Response: {} on success, {"sErr": "key"} on failure
[CallGSApi("Rogue3D_SelectSeasonTalent")]
public class Rogue3D_SelectSeasonTalent : ICallGSHandler
{
private const uint GroupId = 124;
private const uint SeasonTalentIdSid = 1007;
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var req = JsonSerializer.Deserialize<SelectSeasonTalentParam>(param);
if (req == null)
{
await CallGSRouter.SendScript(connection, "Rogue3D_SelectSeasonTalent", "{}");
return;
}
var player = connection.Player!;
var attr = player.Data.Attrs.FirstOrDefault(x => x.Gid == GroupId && x.Sid == SeasonTalentIdSid);
if (attr == null)
{
attr = new PlayerAttr { Gid = GroupId, Sid = SeasonTalentIdSid };
player.Data.Attrs.Add(attr);
}
attr.Val = req.TalentId;
var sync = new NtfSyncPlayer();
sync.Custom[player.ToPackedAttrKey(GroupId, SeasonTalentIdSid)] = attr.Val;
sync.Custom[player.ToShiftedAttrKey(GroupId, SeasonTalentIdSid)] = attr.Val;
await CallGSRouter.SendScript(connection, "Rogue3D_SelectSeasonTalent", "{}", sync);
}
}
internal sealed class SelectSeasonTalentParam
{
[JsonPropertyName("nTalentId")]
public uint TalentId { 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

@@ -0,0 +1,153 @@
using MikuSB.Data;
using MikuSB.Data.Excel;
using MikuSB.Database;
using MikuSB.Database.Inventory;
using MikuSB.Database.Player;
using MikuSB.GameServer.Game.Support;
using MikuSB.Proto;
using System.Text.Json.Serialization;
namespace MikuSB.GameServer.Server.CallGS.Handlers.SupporterCard;
internal static class SupporterCardAffixShared
{
public const uint BaseGid = 150;
public const uint FixedResetSid = 1;
public static SupportCardExcel? GetExcel(GameSupportCardInfo card)
{
return GameData.SupportCardData.FirstOrDefault(x => x.TemplateId == card.TemplateId);
}
public static async Task SendResetResponse(Connection connection, NtfSyncPlayer? sync = null)
{
await CallGSRouter.SendScript(connection, "SupporterCard_ResetAffix", "null", sync!);
}
public static async Task SendSelectResponse(Connection connection, NtfSyncPlayer? sync = null)
{
await CallGSRouter.SendScript(connection, "SupporterCard_SelectAffix", "null", sync!);
}
public static List<Item> ConsumeCostItems(Connection connection, IEnumerable<IReadOnlyList<uint>> costs)
{
var player = connection.Player!;
var syncItems = new List<Item>();
foreach (var cost in costs)
{
if (cost.Count < 5)
continue;
var templateId = GameResourceTemplateId.FromGdpl(cost);
var item = player.InventoryManager.InventoryData.Items.Values.FirstOrDefault(x => x.TemplateId == templateId);
if (item == null || item.ItemCount < cost[4])
throw new InvalidOperationException("support affix material not enough");
item.ItemCount -= cost[4];
var proto = item.ToProto();
if (item.ItemCount == 0)
{
player.InventoryManager.InventoryData.Items.Remove(item.UniqueId);
proto.Count = 0;
}
syncItems.Add(proto);
}
return syncItems;
}
public static bool HasEnoughItems(Connection connection, IEnumerable<IReadOnlyList<uint>> costs)
{
var items = connection.Player!.InventoryManager.InventoryData.Items.Values;
return costs.All(cost =>
{
if (cost.Count < 5)
return false;
var templateId = GameResourceTemplateId.FromGdpl(cost);
var item = items.FirstOrDefault(x => x.TemplateId == templateId);
return item != null && item.ItemCount >= cost[4];
});
}
public 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;
}
public static void SetAttr(Connection connection, NtfSyncPlayer sync, uint gid, uint sid, uint value)
{
var player = connection.Player!;
var attr = GetOrCreateAttr(player.Data, gid, sid);
attr.Val = value;
sync.Custom[player.ToPackedAttrKey(gid, sid)] = value;
sync.Custom[player.ToShiftedAttrKey(gid, sid)] = value;
}
public static IEnumerable<uint> GetActiveAffixIds(GameSupportCardInfo card, params int[] ignoreSlots)
{
var ignored = ignoreSlots.ToHashSet();
for (var slot = 1; slot <= SupportAffixStateService.ActiveThirdAffixSlot; slot++)
{
if (ignored.Contains(slot))
continue;
var (affixId, _) = SupportAffixStateService.GetAffix(card, slot);
if (affixId > 0)
yield return affixId;
}
}
public static void Save(Connection connection)
{
var player = connection.Player!;
DatabaseHelper.SaveDatabaseType(player.InventoryManager.InventoryData);
DatabaseHelper.SaveDatabaseType(player.Data);
}
}
internal sealed class SupporterCardIdParam
{
[JsonPropertyName("Id")]
public int SupportCardUid { get; set; }
}
internal sealed class SupporterCardSelectParam
{
[JsonPropertyName("Id")]
public int SupportCardUid { get; set; }
[JsonPropertyName("SelectNew")]
public bool SelectNew { get; set; }
}
internal sealed class SupporterCardResetInitialParam
{
[JsonPropertyName("Id")]
public int SupportCardUid { get; set; }
[JsonPropertyName("Index")]
public int Index { get; set; }
[JsonPropertyName("FixedId")]
public uint FixedId { get; set; }
}
internal sealed class SupporterCardSelectInitialParam
{
[JsonPropertyName("Id")]
public int SupportCardUid { get; set; }
[JsonPropertyName("Index")]
public int Index { get; set; }
[JsonPropertyName("SelectNew")]
public bool SelectNew { get; set; }
}

View File

@@ -0,0 +1,10 @@
namespace MikuSB.GameServer.Server.CallGS.Handlers.SupporterCard;
[CallGSApi("SupporterCard_FixedResetInitialAffix")]
public class SupporterCard_FixedResetInitialAffix : ICallGSHandler
{
public async Task Handle(Connection connection, string param, ushort seqNo)
{
await SupporterCard_ResetInitialAffix.Reset(connection, param, fixedMode: true);
}
}

View File

@@ -0,0 +1,57 @@
using MikuSB.Data;
using MikuSB.Proto;
using System.Text.Json.Nodes;
namespace MikuSB.GameServer.Server.CallGS.Handlers.SupporterCard;
[CallGSApi("SupporterCard_ReceiveFixedItem")]
public class SupporterCard_ReceiveFixedItem : ICallGSHandler
{
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var player = connection.Player!;
if (!GameData.SupportFixedData.TryGetValue(1, out var fixedCfg) || fixedCfg.Item.Count < 5 || fixedCfg.Num <= 0)
{
await CallGSRouter.SendScript(connection, "SupporterCard_ReceiveFixedItem", "{}");
return;
}
var attr = SupporterCardAffixShared.GetOrCreateAttr(player.Data, SupporterCardAffixShared.BaseGid, SupporterCardAffixShared.FixedResetSid);
var claimCount = attr.Val / (uint)fixedCfg.Num;
if (claimCount == 0)
{
await CallGSRouter.SendScript(connection, "SupporterCard_ReceiveFixedItem", "{}");
return;
}
attr.Val %= (uint)fixedCfg.Num;
var rewardTemplateId = (uint)GameResourceTemplateId.FromGdpl(fixedCfg.Item);
var rewardItem = GameData.SuppliesData.GetValueOrDefault(rewardTemplateId);
if (rewardItem == null)
{
await CallGSRouter.SendScript(connection, "SupporterCard_ReceiveFixedItem", "{}");
return;
}
var granted = await player.InventoryManager.AddSuppliesItem(rewardItem, claimCount * fixedCfg.Item[4], sendPacket: false);
var sync = new NtfSyncPlayer();
if (granted != null)
sync.Items.Add(granted.ToProto());
SupporterCardAffixShared.SetAttr(connection, sync, SupporterCardAffixShared.BaseGid, SupporterCardAffixShared.FixedResetSid, attr.Val);
SupporterCardAffixShared.Save(connection);
var arg = new JsonObject
{
["tbRewards"] = new JsonArray(
(int)fixedCfg.Item[0],
(int)fixedCfg.Item[1],
(int)fixedCfg.Item[2],
(int)fixedCfg.Item[3],
(int)(claimCount * fixedCfg.Item[4]))
}.ToJsonString();
await CallGSRouter.SendScript(connection, "SupporterCard_ReceiveFixedItem", arg, sync);
}
}

View File

@@ -0,0 +1,38 @@
using MikuSB.GameServer.Game.Support;
using MikuSB.Proto;
using System.Text.Json;
namespace MikuSB.GameServer.Server.CallGS.Handlers.SupporterCard;
[CallGSApi("SupporterCard_ResetAffix")]
public class SupporterCard_ResetAffix : ICallGSHandler
{
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var req = JsonSerializer.Deserialize<SupporterCardIdParam>(param);
var card = req == null ? null : connection.Player!.InventoryManager.GetSupportCardItem((uint)req.SupportCardUid);
var excel = card == null ? null : SupporterCardAffixShared.GetExcel(card);
if (card == null || excel == null || excel.AffixCost.Count < 5 || !SupportAffixStateService.HasAffix(card, SupportAffixStateService.ActiveThirdAffixSlot))
{
await SupporterCardAffixShared.SendResetResponse(connection);
return;
}
var costs = new[] { excel.AffixCost };
if (!SupporterCardAffixShared.HasEnoughItems(connection, costs))
{
await SupporterCardAffixShared.SendResetResponse(connection);
return;
}
var sync = new NtfSyncPlayer();
sync.Items.AddRange(SupporterCardAffixShared.ConsumeCostItems(connection, costs));
var excluded = SupporterCardAffixShared.GetActiveAffixIds(card, SupportAffixStateService.ActiveThirdAffixSlot);
var (affixId, tier) = SupportAffixService.GenerateRandomAffix(excel.AffixPool[SupportAffixStateService.ActiveThirdAffixSlot - 1], excluded);
SupportAffixStateService.SetAffix(card, SupportAffixStateService.PendingMaxAffixSlot, affixId, tier);
sync.Items.Add(card.ToProto());
SupporterCardAffixShared.Save(connection);
await SupporterCardAffixShared.SendResetResponse(connection, sync);
}
}

View File

@@ -0,0 +1,60 @@
using MikuSB.GameServer.Game.Support;
using MikuSB.Proto;
using System.Text.Json;
namespace MikuSB.GameServer.Server.CallGS.Handlers.SupporterCard;
[CallGSApi("SupporterCard_ResetInitialAffix")]
public class SupporterCard_ResetInitialAffix : ICallGSHandler
{
public async Task Handle(Connection connection, string param, ushort seqNo)
{
await Reset(connection, param, fixedMode: false);
}
internal static async Task Reset(Connection connection, string param, bool fixedMode)
{
var req = JsonSerializer.Deserialize<SupporterCardResetInitialParam>(param);
var card = req == null ? null : connection.Player!.InventoryManager.GetSupportCardItem((uint)req.SupportCardUid);
var excel = card == null ? null : SupporterCardAffixShared.GetExcel(card);
if (req == null || card == null || excel == null || req.Index is < 1 or > 2 || excel.AffixPool.Count < req.Index)
{
await SupporterCardAffixShared.SendResetResponse(connection);
return;
}
var costs = fixedMode ? new[] { excel.FixedAffixCost } : excel.InitialAffixCost;
if (!costs.Any() || !SupporterCardAffixShared.HasEnoughItems(connection, costs))
{
await SupporterCardAffixShared.SendResetResponse(connection);
return;
}
var sync = new NtfSyncPlayer();
sync.Items.AddRange(SupporterCardAffixShared.ConsumeCostItems(connection, costs));
uint affixId;
uint tier;
if (fixedMode && req.FixedId > 0)
{
affixId = req.FixedId;
tier = SupportAffixService.GenerateTier(affixId);
}
else
{
var excluded = SupporterCardAffixShared.GetActiveAffixIds(card, req.Index);
(affixId, tier) = SupportAffixService.GenerateRandomAffix(excel.AffixPool[req.Index - 1], excluded);
}
SupportAffixStateService.SetAffix(card, SupportAffixStateService.PendingInitialAffixSlot, affixId, tier);
card.AffixId = (uint)req.Index;
var attr = SupporterCardAffixShared.GetOrCreateAttr(connection.Player!.Data, SupporterCardAffixShared.BaseGid, SupporterCardAffixShared.FixedResetSid);
attr.Val += 1;
SupporterCardAffixShared.SetAttr(connection, sync, SupporterCardAffixShared.BaseGid, SupporterCardAffixShared.FixedResetSid, attr.Val);
sync.Items.Add(card.ToProto());
SupporterCardAffixShared.Save(connection);
await SupporterCardAffixShared.SendResetResponse(connection, sync);
}
}

View File

@@ -0,0 +1,30 @@
using MikuSB.GameServer.Game.Support;
using MikuSB.Proto;
using System.Text.Json;
namespace MikuSB.GameServer.Server.CallGS.Handlers.SupporterCard;
[CallGSApi("SupporterCard_SelectAffix")]
public class SupporterCard_SelectAffix : ICallGSHandler
{
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var req = JsonSerializer.Deserialize<SupporterCardSelectParam>(param);
var card = req == null ? null : connection.Player!.InventoryManager.GetSupportCardItem((uint)req.SupportCardUid);
if (card == null || !SupportAffixStateService.HasAffix(card, SupportAffixStateService.PendingMaxAffixSlot))
{
await SupporterCardAffixShared.SendSelectResponse(connection);
return;
}
if (req!.SelectNew)
SupportAffixStateService.CopyAffix(card, SupportAffixStateService.PendingMaxAffixSlot, SupportAffixStateService.ActiveThirdAffixSlot);
SupportAffixStateService.ClearAffix(card, SupportAffixStateService.PendingMaxAffixSlot);
var sync = new NtfSyncPlayer();
sync.Items.Add(card.ToProto());
SupporterCardAffixShared.Save(connection);
await SupporterCardAffixShared.SendSelectResponse(connection, sync);
}
}

View File

@@ -0,0 +1,31 @@
using MikuSB.GameServer.Game.Support;
using MikuSB.Proto;
using System.Text.Json;
namespace MikuSB.GameServer.Server.CallGS.Handlers.SupporterCard;
[CallGSApi("SupporterCard_SelectInitialAffix")]
public class SupporterCard_SelectInitialAffix : ICallGSHandler
{
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var req = JsonSerializer.Deserialize<SupporterCardSelectInitialParam>(param);
var card = req == null ? null : connection.Player!.InventoryManager.GetSupportCardItem((uint)req.SupportCardUid);
if (req == null || card == null || req.Index is < 1 or > 2 || card.AffixId != req.Index || !SupportAffixStateService.HasAffix(card, SupportAffixStateService.PendingInitialAffixSlot))
{
await SupporterCardAffixShared.SendSelectResponse(connection);
return;
}
if (req.SelectNew)
SupportAffixStateService.CopyAffix(card, SupportAffixStateService.PendingInitialAffixSlot, req.Index);
SupportAffixStateService.ClearAffix(card, SupportAffixStateService.PendingInitialAffixSlot);
card.AffixId = 0;
var sync = new NtfSyncPlayer();
sync.Items.Add(card.ToProto());
SupporterCardAffixShared.Save(connection);
await SupporterCardAffixShared.SendSelectResponse(connection, sync);
}
}

View File

@@ -1,5 +1,6 @@
using MikuSB.Data;
using MikuSB.Database;
using MikuSB.GameServer.Game.Support;
using MikuSB.Proto;
using System.Text.Json;
using System.Text.Json.Serialization;
@@ -19,7 +20,7 @@ public class SupporterCard_Upgrade : ICallGSHandler
return;
}
var supportCard = player.InventoryManager.InventoryData.Items.GetValueOrDefault((uint)req.SupportCardUid);
var supportCard = player.InventoryManager.GetSupportCardItem((uint)req.SupportCardUid);
if (supportCard == null)
{
await CallGSRouter.SendScript(connection, "Logistics_Upgrade", "{}");
@@ -68,10 +69,11 @@ public class SupporterCard_Upgrade : ICallGSHandler
}
// Apply exp and level up
if (supportCard.Level == 0) supportCard.Level = 1;
supportCard.Exp += gainedExp;
while (supportCard.Level < maxLevel)
{
var expNeeded = GetExpNeeded(supportCard.Level + 1);
var expNeeded = GetExpNeeded(supportCard.Level);
if (expNeeded == 0 || supportCard.Exp < expNeeded) break;
supportCard.Exp -= expNeeded;
supportCard.Level++;
@@ -80,6 +82,21 @@ public class SupporterCard_Upgrade : ICallGSHandler
{
supportCard.Exp = 0;
supportCard.Level = maxLevel;
// Unlock next affix slot when reaching max level for the first time
if (supportCardExcel != null)
{
var currentSlots = Enumerable.Range(1, SupportAffixStateService.ActiveThirdAffixSlot)
.Count(slot => SupportAffixStateService.HasAffix(supportCard, slot));
var totalSlots = supportCardExcel.TotalAffixCount;
if (currentSlots < totalSlots && currentSlots < supportCardExcel.AffixPool.Count)
{
var poolId = supportCardExcel.AffixPool[currentSlots];
var (affixId, tier) = SupportAffixService.GenerateRandomAffix(poolId);
if (affixId > 0)
SupportAffixStateService.SetAffix(supportCard, currentSlots + 1, affixId, tier);
}
}
}
syncItems.Add(supportCard.ToProto());

View File

@@ -0,0 +1,118 @@
using MikuSB.Data;
using MikuSB.Data.Excel;
using MikuSB.Database;
using MikuSB.Database.Player;
using MikuSB.GameServer.Game.Player;
using MikuSB.Proto;
namespace MikuSB.GameServer.Server.CallGS.Handlers.Tower;
[CallGSApi("ClimbTowerLogic_CheckCycleLevel")]
public class ClimbTowerLogic_CheckCycleLevel : ICallGSHandler
{
private const uint TowerGroupId = 3;
private const uint TimeSubId = 1;
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var player = connection.Player!;
var current = ResolveCurrentCycle(GameData.ClimbTowerTimeData.Values, DateTime.Now);
if (current == null)
{
await CallGSRouter.SendScript(connection, "ClimbTowerLogic_CheckCycleLevel", "{}");
return;
}
var currentTimeId = GetAttr(player.Data, TowerGroupId, TimeSubId);
var sync = new NtfSyncPlayer();
if (currentTimeId != current.ID)
{
ResetTowerAttrs(player, sync);
SetAttr(player.Data, TowerGroupId, TimeSubId, current.ID, sync, player);
DatabaseHelper.SaveDatabaseType(player.Data);
}
await CallGSRouter.SendScript(connection, "ClimbTowerLogic_CheckCycleLevel", $$"""{"timeID":{{current.ID}}}""", sync);
}
private static ClimbTowerTimeExcel? ResolveCurrentCycle(IEnumerable<ClimbTowerTimeExcel> 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);
if (latestStarted != null)
return latestStarted.Config;
return parsed.FirstOrDefault()?.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",
System.Globalization.CultureInfo.InvariantCulture,
System.Globalization.DateTimeStyles.None,
out var value)
? value
: null;
}
private static uint GetAttr(PlayerGameData data, uint gid, uint sid)
{
return data.Attrs.FirstOrDefault(x => x.Gid == gid && x.Sid == sid)?.Val ?? 0;
}
private static void ResetTowerAttrs(PlayerInstance player, NtfSyncPlayer sync)
{
var towerAttrs = player.Data.Attrs
.Where(x => x.Gid == TowerGroupId)
.ToList();
foreach (var attr in towerAttrs)
{
sync.Custom[player.ToPackedAttrKey(attr.Gid, attr.Sid)] = 0;
sync.Custom[player.ToShiftedAttrKey(attr.Gid, attr.Sid)] = 0;
}
player.Data.Attrs.RemoveAll(x => x.Gid == TowerGroupId);
}
private static void SetAttr(PlayerGameData data, uint gid, uint sid, uint value, NtfSyncPlayer sync, PlayerInstance player)
{
var attr = data.Attrs.FirstOrDefault(x => x.Gid == gid && x.Sid == sid);
if (attr == null)
{
attr = new PlayerAttr
{
Gid = gid,
Sid = sid
};
data.Attrs.Add(attr);
}
attr.Val = value;
sync.Custom[player.ToPackedAttrKey(gid, sid)] = value;
sync.Custom[player.ToShiftedAttrKey(gid, sid)] = value;
}
}

View File

@@ -0,0 +1,449 @@
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.Tower;
[CallGSApi("ClimbTowerLogic_GetReward")]
public class ClimbTowerLogic_GetReward : ICallGSHandler
{
private const uint TowerGroupId = 3;
private const uint RewardStateSidBase = 100;
private const uint TowerLevelStateSidBase = 10000;
private const uint LaunchPassGroupId = 22;
private const uint AdvancedDiffSid = 4;
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var player = connection.Player!;
var req = JsonSerializer.Deserialize<ClimbTowerGetRewardParam>(param);
if (req == null || req.Layer <= 0)
{
await CallGSRouter.SendScript(connection, "ClimbTowerLogic_GetReward", "{\"sErr\":\"error.BadParam\"}");
return;
}
var cycle = ResolveCurrentCycle(GameData.ClimbTowerTimeData.Values, DateTime.Now);
if (cycle == null)
{
await CallGSRouter.SendScript(connection, "ClimbTowerLogic_GetReward", "{\"sErr\":\"error.BadParam\"}");
return;
}
if (!TryResolveLayer(cycle, req.Layer, player.Data, out var towerIds, out var diff))
{
await CallGSRouter.SendScript(connection, "ClimbTowerLogic_GetReward", "{\"sErr\":\"error.BadParam\"}");
return;
}
if (!GameData.ClimbTowerAwardData.TryGetValue((uint)req.Layer, out var diffMap) ||
!diffMap.TryGetValue(diff, out var rewardCfg))
{
await CallGSRouter.SendScript(connection, "ClimbTowerLogic_GetReward", "{\"sErr\":\"error.BadParam\"}");
return;
}
var groups = ResolveRequestedGroups(req.Group);
if (groups.Count == 0)
{
await CallGSRouter.SendScript(connection, "ClimbTowerLogic_GetReward", "{\"sErr\":\"error.BadParam\"}");
return;
}
var claimableGroups = groups
.Where(group => CanClaimGroup(player.Data, rewardCfg, towerIds, req.Layer, group))
.Distinct()
.ToList();
if (claimableGroups.Count == 0)
{
await CallGSRouter.SendScript(connection, "ClimbTowerLogic_GetReward", "{\"sErr\":\"error.BadParam\"}");
return;
}
var sync = new NtfSyncPlayer();
var rewardStateAttr = GetOrCreateAttr(player.Data, TowerGroupId, RewardStateSidBase + (uint)req.Layer);
var responseRewards = new JsonArray();
foreach (var group in claimableGroups)
{
rewardStateAttr.Val |= 1u << GetFlagBitOffset(group);
foreach (var reward in rewardCfg.GetRewards(group))
{
if (reward.Count < 5)
continue;
await GrantRewardAsync(player, sync, reward);
responseRewards.Add(new JsonArray(
(int)reward[0],
(int)reward[1],
(int)reward[2],
(int)reward[3],
(int)reward[4]));
}
}
SyncAttr(sync, player, rewardStateAttr);
DatabaseHelper.SaveDatabaseType(player.Data);
DatabaseHelper.SaveDatabaseType(player.InventoryManager.InventoryData);
DatabaseHelper.SaveDatabaseType(player.CharacterManager.CharacterData);
var rsp = new JsonObject
{
["tbRewards"] = responseRewards
};
await CallGSRouter.SendScript(connection, "ClimbTowerLogic_GetReward", rsp.ToJsonString(), sync);
}
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;
}
private static bool CanClaimGroup(
PlayerGameData data,
ClimbTowerAwardExcel rewardCfg,
IReadOnlyList<uint> towerIds,
int layer,
int group)
{
if (group is < 0 or > 3 || IsRewardClaimed(data, layer, group))
return false;
if (group == 0)
return IsLayerPass(data, towerIds);
var requiredStar = rewardCfg.GetStarCount(group);
return requiredStar > 0 && GetLayerStar(data, towerIds) >= requiredStar;
}
private static bool IsLayerPass(PlayerGameData data, IReadOnlyList<uint> towerIds)
{
foreach (var towerId in towerIds)
{
if (!GameData.ClimbTowerLevelOrderData.TryGetValue(towerId, out var orderCfg))
return false;
var passAttr = data.Attrs.FirstOrDefault(x => x.Gid == LaunchPassGroupId && x.Sid == orderCfg.LevelID);
if (passAttr == null || passAttr.Val == 0)
return false;
}
return true;
}
private static int GetLayerStar(PlayerGameData data, IReadOnlyList<uint> towerIds)
{
var total = 0;
foreach (var towerId in towerIds)
{
var attr = data.Attrs.FirstOrDefault(x => x.Gid == TowerGroupId && x.Sid == TowerLevelStateSidBase + towerId);
var value = attr?.Val ?? 0;
for (var i = 0; i < 9; i++)
{
if (((value >> i) & 1u) != 0)
total++;
}
}
return total;
}
private static bool IsRewardClaimed(PlayerGameData data, int layer, int group)
{
var attr = data.Attrs.FirstOrDefault(x => x.Gid == TowerGroupId && x.Sid == RewardStateSidBase + (uint)layer);
if (attr == null)
return false;
var offset = GetFlagBitOffset(group);
return ((attr.Val >> offset) & 0xFu) > 0;
}
private static int GetFlagBitOffset(int group) => group switch
{
0 => 0,
1 => 4,
2 => 8,
3 => 12,
_ => 0
};
private static List<int> ResolveRequestedGroups(int? group)
{
if (!group.HasValue)
return [0, 1, 2, 3];
return group.Value is >= 0 and <= 3 ? [group.Value] : [];
}
private static bool TryResolveLayer(
ClimbTowerTimeExcel cycle,
int layer,
PlayerGameData data,
out IReadOnlyList<uint> towerIds,
out int diff)
{
var basicGroups = cycle.GetLevelGroups(1);
if (layer <= basicGroups.Count)
{
towerIds = basicGroups[layer - 1];
diff = 1;
return towerIds.Count > 0;
}
var advancedIndex = layer - basicGroups.Count;
var advancedGroups = cycle.GetLevelGroups(2);
if (advancedIndex <= 0 || advancedIndex > advancedGroups.Count)
{
towerIds = [];
diff = 0;
return false;
}
var diffAttr = data.Attrs.FirstOrDefault(x => x.Gid == TowerGroupId && x.Sid == AdvancedDiffSid);
diff = (int)(diffAttr?.Val ?? 0);
towerIds = advancedGroups[advancedIndex - 1];
return diff > 0 && towerIds.Count > 0;
}
private static ClimbTowerTimeExcel? ResolveCurrentCycle(IEnumerable<ClimbTowerTimeExcel> 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);
if (latestStarted != null)
return latestStarted.Config;
return parsed.FirstOrDefault()?.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 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 ClimbTowerGetRewardParam
{
[JsonPropertyName("nType")]
public int? Type { get; set; }
[JsonPropertyName("nLayer")]
public int Layer { get; set; }
[JsonPropertyName("nGroup")]
public int? Group { get; set; }
}

View File

@@ -0,0 +1,219 @@
using MikuSB.Data;
using MikuSB.Data.Excel;
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.Tower;
[CallGSApi("ClimbTowerLogic_RecordProgres")]
public class ClimbTowerLogic_RecordProgres : ICallGSHandler
{
private const uint TowerGroupId = 3;
private const uint BasicProgressSid = 2;
private const uint AdvancedProgressSid = 3;
private const uint LevelStateSidBase = 10000;
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var player = connection.Player!;
var req = JsonSerializer.Deserialize<ClimbTowerRecordProgressParam>(param);
if (req == null || req.LevelId == 0 || req.Area <= 0)
{
await CallGSRouter.SendScript(connection, "ClimbTowerLogic_RecordProgres", "{\"sErr\":\"error.BadParam\"}");
return;
}
var cycle = ResolveCurrentCycle(GameData.ClimbTowerTimeData.Values, DateTime.Now);
if (cycle == null)
{
await CallGSRouter.SendScript(connection, "ClimbTowerLogic_RecordProgres", "{\"sErr\":\"error.BadParam\"}");
return;
}
var towerType = ResolveTowerType(cycle, (uint)req.LevelId);
if (towerType == 0)
{
await CallGSRouter.SendScript(connection, "ClimbTowerLogic_RecordProgres", "{\"sErr\":\"error.BadParam\"}");
return;
}
var sync = new NtfSyncPlayer();
var levelStateSid = LevelStateSidBase + (uint)req.LevelId;
var levelState = GetOrCreateAttr(player.Data, TowerGroupId, levelStateSid);
levelState.Val = MergeAreaStars(levelState.Val, req.Area, req.StarMask);
SyncAttr(sync, player, levelState);
var progressSid = towerType == 1 ? BasicProgressSid : AdvancedProgressSid;
var progressAttr = GetOrCreateAttr(player.Data, TowerGroupId, progressSid);
progressAttr.Val = req.Area >= 3 ? 0u : PackProgress((uint)req.LevelId, (uint)(req.Area + 1));
SyncAttr(sync, player, progressAttr);
if (req.RoleHP.Count > 0 || req.TeamEnergy.HasValue)
{
SaveRoleState(player, sync, towerType, req.RoleHP, req.TeamEnergy.GetValueOrDefault());
}
DatabaseHelper.SaveDatabaseType(player.Data);
await CallGSRouter.SendScript(connection, "ClimbTowerLogic_RecordProgres", "{}", sync);
}
private static void SaveRoleState(
PlayerInstance player,
NtfSyncPlayer sync,
int towerType,
List<List<int>> roleHp,
int teamEnergy)
{
var slotStart = towerType == 2 ? 4u : 1u;
for (var slot = slotStart; slot < slotStart + 3; slot++)
{
var templateAttr = GetOrCreateAttr(player.Data, TowerGroupId, slot * 10);
var hpAttr = GetOrCreateAttr(player.Data, TowerGroupId, slot * 10 + 1);
templateAttr.Val = 0;
hpAttr.Val = 0;
SyncAttr(sync, player, templateAttr);
SyncAttr(sync, player, hpAttr);
}
for (var i = 0; i < Math.Min(roleHp.Count, 3); i++)
{
var row = roleHp[i];
if (row == null || row.Count < 2)
continue;
var slot = slotStart + (uint)i;
var templateAttr = GetOrCreateAttr(player.Data, TowerGroupId, slot * 10);
var hpAttr = GetOrCreateAttr(player.Data, TowerGroupId, slot * 10 + 1);
templateAttr.Val = (uint)Math.Max(0, row[0]);
hpAttr.Val = (uint)Math.Max(0, row[1]);
SyncAttr(sync, player, templateAttr);
SyncAttr(sync, player, hpAttr);
}
var energyAttr = GetOrCreateAttr(player.Data, TowerGroupId, slotStart * 10 + 2);
energyAttr.Val = (uint)Math.Max(0, teamEnergy);
SyncAttr(sync, player, energyAttr);
}
private static uint MergeAreaStars(uint currentValue, int area, int starMask)
{
var areaIndex = Math.Clamp(area, 1, 3) - 1;
var result = currentValue;
for (var i = 0; i < 3; i++)
{
if (((starMask >> i) & 1) == 0)
continue;
var bitIndex = areaIndex * 3 + i;
result |= 1u << bitIndex;
}
return result;
}
private static uint PackProgress(uint levelId, uint area) => (area << 24) | (levelId & 0x00FF_FFFF);
private static int ResolveTowerType(ClimbTowerTimeExcel cycle, uint levelId)
{
if (ContainsLevel(cycle.GetLevelGroups(1), levelId))
return 1;
if (ContainsLevel(cycle.GetLevelGroups(2), levelId))
return 2;
return 0;
}
private static bool ContainsLevel(IEnumerable<IReadOnlyList<uint>> groups, uint levelId)
{
return groups.Any(group => group.Any(id => id == levelId));
}
private static ClimbTowerTimeExcel? ResolveCurrentCycle(IEnumerable<ClimbTowerTimeExcel> 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);
if (latestStarted != null)
return latestStarted.Config;
return parsed.FirstOrDefault()?.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",
System.Globalization.CultureInfo.InvariantCulture,
System.Globalization.DateTimeStyles.None,
out var value)
? value
: null;
}
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 ClimbTowerRecordProgressParam
{
[JsonPropertyName("nID")]
public int LevelId { get; set; }
[JsonPropertyName("nArea")]
public int Area { get; set; }
[JsonPropertyName("nStar")]
public int StarMask { get; set; }
[JsonPropertyName("tbRoleHP")]
public List<List<int>> RoleHP { get; set; } = [];
[JsonPropertyName("nTeamEnergy")]
public int? TeamEnergy { get; set; }
}

View File

@@ -0,0 +1,76 @@
using MikuSB.Data;
using MikuSB.Database;
using MikuSB.Database.Player;
using MikuSB.Proto;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace MikuSB.GameServer.Server.CallGS.Handlers.Tower;
[CallGSApi("ClimbTowerLogic_SetLevelDiff")]
public class ClimbTowerLogic_SetLevelDiff : ICallGSHandler
{
private const uint TowerGroupId = 3;
private const uint DiffSid = 4;
private const uint HisDiffSid = 5;
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var player = connection.Player!;
var req = JsonSerializer.Deserialize<ClimbTowerSetLevelDiffParam>(param);
if (req == null || req.Diff <= 0)
{
await CallGSRouter.SendScript(connection, "ClimbTowerLogic_SetLevelDiff", "{\"sErr\":\"error.BadParam\"}");
return;
}
if (!GameData.ClimbTowerDiffData.ContainsKey((uint)req.Diff))
{
await CallGSRouter.SendScript(connection, "ClimbTowerLogic_SetLevelDiff", "{\"sErr\":\"error.BadParam\"}");
return;
}
var hisDiff = GetAttrValue(player.Data, TowerGroupId, HisDiffSid);
if (req.Diff > hisDiff + 1)
{
await CallGSRouter.SendScript(connection, "ClimbTowerLogic_SetLevelDiff", "{\"sErr\":\"error.BadParam\"}");
return;
}
var diffAttr = GetOrCreateAttr(player.Data, TowerGroupId, DiffSid);
diffAttr.Val = (uint)req.Diff;
var sync = new NtfSyncPlayer();
sync.Custom[player.ToPackedAttrKey(diffAttr.Gid, diffAttr.Sid)] = diffAttr.Val;
sync.Custom[player.ToShiftedAttrKey(diffAttr.Gid, diffAttr.Sid)] = diffAttr.Val;
DatabaseHelper.SaveDatabaseType(player.Data);
await CallGSRouter.SendScript(connection, "ClimbTowerLogic_SetLevelDiff", "{}", sync);
}
private static uint GetAttrValue(PlayerGameData data, uint gid, uint sid)
{
return data.Attrs.FirstOrDefault(x => x.Gid == gid && x.Sid == sid)?.Val ?? 0;
}
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;
}
}
internal sealed class ClimbTowerSetLevelDiffParam
{
[JsonPropertyName("nDiff")]
public int Diff { get; set; }
}

View File

@@ -0,0 +1,39 @@
using MikuSB.Data;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace MikuSB.GameServer.Server.CallGS.Handlers.Tower;
[CallGSApi("TowerEventChapter_EnterLevel")]
public class TowerEventChapter_EnterLevel : ICallGSHandler
{
private static readonly Random Random = new();
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var req = JsonSerializer.Deserialize<TowerEventEnterLevelParam>(param);
if (req == null || req.LevelId == 0 || req.TeamId <= 0)
{
await CallGSRouter.SendScript(connection, "TowerEventChapter_EnterLevel", "{\"sErr\":\"error.BadParam\"}");
return;
}
if (!GameData.TowerEventLevelData.ContainsKey((uint)req.LevelId))
{
await CallGSRouter.SendScript(connection, "TowerEventChapter_EnterLevel", "{\"sErr\":\"error.BadParam\"}");
return;
}
var rsp = $"{{\"nSeed\":{Random.Next(1, 1_000_000_000)}}}";
await CallGSRouter.SendScript(connection, "TowerEventChapter_EnterLevel", rsp);
}
}
internal sealed class TowerEventEnterLevelParam
{
[JsonPropertyName("nID")]
public int LevelId { get; set; }
[JsonPropertyName("nTeamID")]
public int TeamId { get; set; }
}

View File

@@ -0,0 +1,82 @@
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.Tower;
[CallGSApi("TowerEventChapter_LevelSettlement")]
public class TowerEventChapter_LevelSettlement : ICallGSHandler
{
private const uint LevelStateGroupId = 21;
private const uint LaunchPassGroupId = 22;
private const uint PassedFlagMask = (1u << 8) | 0b111u;
private static readonly Logger Logger = new("TowerEvent");
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var (response, sync) = HandleSettlement(connection.Player!, JsonNode.Parse(param));
await CallGSRouter.SendScript(connection, "TowerEventChapter_LevelSettlement", response.ToJsonString(), sync);
}
public static (JsonNode Response, NtfSyncPlayer Sync) HandleSettlement(PlayerInstance player, JsonNode? tbParam)
{
var req = tbParam?.Deserialize<TowerEventSettlementParam>();
if (req == null || req.LevelId == 0 || req.ChapterId == 0)
{
Logger.Error($"Invalid tower event settlement payload: {tbParam?.ToJsonString() ?? "null"}");
return (new JsonObject { ["sErr"] = "error.BadParam" }, new NtfSyncPlayer());
}
var sync = new NtfSyncPlayer();
var levelStateAttr = GetOrCreateAttr(player.Data, LevelStateGroupId, (uint)req.LevelId);
levelStateAttr.Val |= PassedFlagMask;
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(
$"TowerEvent settlement saved. uid={player.Uid} chapterId={req.ChapterId} levelId={req.LevelId} " +
$"levelStateVal={levelStateAttr.Val} passVal={passAttr.Val}");
DatabaseHelper.SaveDatabaseType(player.Data);
return (new JsonObject(), sync);
}
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 TowerEventSettlementParam
{
[JsonPropertyName("nID")]
public int LevelId { get; set; }
[JsonPropertyName("nChapterID")]
public int ChapterId { get; set; }
}

View File

@@ -0,0 +1,39 @@
using MikuSB.Data;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace MikuSB.GameServer.Server.CallGS.Handlers.Tower;
[CallGSApi("TowerLevel_EnterLevel")]
public class TowerLevel_EnterLevel : ICallGSHandler
{
private static readonly Random Random = new();
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var req = JsonSerializer.Deserialize<TowerLevelEnterLevelParam>(param);
if (req == null || req.LevelId == 0 || req.TeamId <= 0)
{
await CallGSRouter.SendScript(connection, "TowerLevel_EnterLevel", "{\"sErr\":\"error.BadParam\"}");
return;
}
if (!GameData.TowerLevelData.ContainsKey((uint)req.LevelId))
{
await CallGSRouter.SendScript(connection, "TowerLevel_EnterLevel", "{\"sErr\":\"error.BadParam\"}");
return;
}
var rsp = $"{{\"nSeed\":{Random.Next(1, 1_000_000_000)}}}";
await CallGSRouter.SendScript(connection, "TowerLevel_EnterLevel", rsp);
}
}
internal sealed class TowerLevelEnterLevelParam
{
[JsonPropertyName("nID")]
public int LevelId { get; set; }
[JsonPropertyName("nTeamID")]
public int TeamId { get; set; }
}

View File

@@ -0,0 +1,178 @@
using MikuSB.Data;
using MikuSB.Data.Excel;
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.Tower;
[CallGSApi("TowerLevel_LevelSettlement")]
public class TowerLevel_LevelSettlement : ICallGSHandler
{
private static readonly Logger Logger = new("Tower");
private const uint TowerGroupId = 3;
private const uint LaunchPassGroupId = 22;
private const uint BasicProgressSid = 2;
private const uint AdvancedProgressSid = 3;
private const uint LevelStateSidBase = 10000;
private const int FinalArea = 3;
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var (response, sync) = HandleSettlement(connection.Player!, JsonNode.Parse(param));
await CallGSRouter.SendScript(connection, "TowerLevel_LevelSettlement", response.ToJsonString(), sync);
}
public static (JsonNode Response, NtfSyncPlayer Sync) HandleSettlement(PlayerInstance player, JsonNode? tbParam)
{
var req = tbParam?.Deserialize<TowerLevelSettlementParam>();
if (req == null || req.TowerId == 0 || req.LevelId == 0)
{
Logger.Error($"Invalid tower settlement payload: {tbParam?.ToJsonString() ?? "null"}");
return (new JsonObject { ["sErr"] = "error.BadParam" }, new NtfSyncPlayer());
}
var cycle = ResolveCurrentCycle(GameData.ClimbTowerTimeData.Values, DateTime.Now);
if (cycle == null)
return (new JsonObject { ["sErr"] = "error.BadParam" }, new NtfSyncPlayer());
var towerType = ResolveTowerType(cycle, (uint)req.TowerId);
if (towerType == 0)
return (new JsonObject { ["sErr"] = "error.BadParam" }, new NtfSyncPlayer());
var sync = new NtfSyncPlayer();
var levelStateSid = LevelStateSidBase + (uint)req.TowerId;
var levelState = GetOrCreateAttr(player.Data, TowerGroupId, levelStateSid);
levelState.Val = MergeAreaStars(levelState.Val, FinalArea, req.StarMask);
SyncAttr(sync, player, levelState);
var progressSid = towerType == 1 ? BasicProgressSid : AdvancedProgressSid;
var progressAttr = GetOrCreateAttr(player.Data, TowerGroupId, progressSid);
progressAttr.Val = 0;
SyncAttr(sync, player, progressAttr);
var passAttr = GetOrCreateAttr(player.Data, LaunchPassGroupId, (uint)req.LevelId);
passAttr.Val = Math.Max(1u, passAttr.Val + 1);
SyncAttr(sync, player, passAttr);
Logger.Info(
$"Tower settlement saved. uid={player.Uid} towerId={req.TowerId} levelId={req.LevelId} starMask={req.StarMask} " +
$"towerStateSid={levelStateSid} towerStateVal={levelState.Val} progressSid={progressSid} passVal={passAttr.Val}");
DatabaseHelper.SaveDatabaseType(player.Data);
return (new JsonObject(), sync);
}
private static uint MergeAreaStars(uint currentValue, int area, int starMask)
{
var areaIndex = Math.Clamp(area, 1, 3) - 1;
var result = currentValue;
for (var i = 0; i < 3; i++)
{
if (((starMask >> i) & 1) == 0)
continue;
var bitIndex = areaIndex * 3 + i;
result |= 1u << bitIndex;
}
return result;
}
private static int ResolveTowerType(ClimbTowerTimeExcel cycle, uint levelId)
{
if (ContainsLevel(cycle.GetLevelGroups(1), levelId))
return 1;
if (ContainsLevel(cycle.GetLevelGroups(2), levelId))
return 2;
return 0;
}
private static bool ContainsLevel(IEnumerable<IReadOnlyList<uint>> groups, uint levelId)
{
return groups.Any(group => group.Any(id => id == levelId));
}
private static ClimbTowerTimeExcel? ResolveCurrentCycle(IEnumerable<ClimbTowerTimeExcel> 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);
if (latestStarted != null)
return latestStarted.Config;
return parsed.FirstOrDefault()?.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",
System.Globalization.CultureInfo.InvariantCulture,
System.Globalization.DateTimeStyles.None,
out var value)
? value
: null;
}
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(MikuSB.Proto.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 TowerLevelSettlementParam
{
[JsonPropertyName("nID")]
public int LevelId { get; set; }
[JsonPropertyName("nTowerID")]
public int TowerId { get; set; }
[JsonPropertyName("nStar")]
public int StarMask { get; set; }
}

View File

@@ -0,0 +1,134 @@
using MikuSB.Data.Excel;
using MikuSB.Util;
using Newtonsoft.Json.Linq;
namespace MikuSB.GameServer.Server.CallGS.Handlers.VirCapture;
internal static class VirCaptureCaptureRewardResolver
{
private static readonly Lock CacheLock = new();
private static readonly Dictionary<string, Dictionary<uint, VirCaptureLevelRegionReward>> RegionCache = [];
private static readonly Dictionary<string, Dictionary<uint, List<uint>>> BossCache = [];
public static List<uint>? ResolveGdpl(VirCaptureCaptureRegionExcel captureRegion, uint regionId)
{
if (string.IsNullOrWhiteSpace(captureRegion.LevelRegionName))
return null;
var regionMap = GetOrLoadRegionMap(captureRegion.LevelRegionName);
if (!regionMap.TryGetValue(regionId, out var regionReward))
return null;
if (regionReward.PalType == 2)
return GetOrLoadBossMap(captureRegion.LevelRegionName).GetValueOrDefault(regionId);
return regionReward.Rewards1;
}
private static Dictionary<uint, VirCaptureLevelRegionReward> GetOrLoadRegionMap(string mapName)
{
lock (CacheLock)
{
if (RegionCache.TryGetValue(mapName, out var cached))
return cached;
var loaded = new Dictionary<uint, VirCaptureLevelRegionReward>();
var path = Path.Combine(
ConfigManager.Config.Path.ResourcePath,
"dlc",
"vircapture",
mapName,
"region_info.json");
if (File.Exists(path))
{
var array = JArray.Parse(File.ReadAllText(path));
foreach (var token in array)
{
var id = ReadUInt(token["Id"]);
if (id == 0)
continue;
loaded[id] = new VirCaptureLevelRegionReward
{
PalType = ReadInt(token["PalType"]),
Rewards1 = token["Rewards1"]?.ToObject<List<uint>>() ?? []
};
}
}
RegionCache[mapName] = loaded;
return loaded;
}
}
private static Dictionary<uint, List<uint>> GetOrLoadBossMap(string mapName)
{
lock (CacheLock)
{
if (BossCache.TryGetValue(mapName, out var cached))
return cached;
var loaded = new Dictionary<uint, List<uint>>();
var path = Path.Combine(
ConfigManager.Config.Path.ResourcePath,
"dlc",
"vircapture",
mapName,
"boss.json");
if (File.Exists(path))
{
var array = JArray.Parse(File.ReadAllText(path));
foreach (var token in array)
{
var regionId = ReadUInt(token["RegionId"]);
var boss = token["Boss"]?.ToObject<List<uint>>();
if (regionId == 0 || boss == null || boss.Count < 4)
continue;
loaded.TryAdd(regionId, boss);
}
}
BossCache[mapName] = loaded;
return loaded;
}
}
private sealed class VirCaptureLevelRegionReward
{
public int PalType { get; init; }
public List<uint> Rewards1 { get; init; } = [];
}
private static uint ReadUInt(JToken? token)
{
if (token == null || token.Type == JTokenType.Null)
return 0;
return token.Type switch
{
JTokenType.Integer => token.Value<uint>(),
JTokenType.Float => Math.Max(0u, (uint)token.Value<double>()),
JTokenType.String when uint.TryParse(token.Value<string>(), out var value) => value,
JTokenType.String => 0,
_ => 0
};
}
private static int ReadInt(JToken? token)
{
if (token == null || token.Type == JTokenType.Null)
return 0;
return token.Type switch
{
JTokenType.Integer => token.Value<int>(),
JTokenType.Float => (int)token.Value<double>(),
JTokenType.String when int.TryParse(token.Value<string>(), out var value) => value,
JTokenType.String => 0,
_ => 0
};
}
}

View File

@@ -0,0 +1,40 @@
using MikuSB.Database;
using MikuSB.Proto;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace MikuSB.GameServer.Server.CallGS.Handlers.VirCapture;
[CallGSApi("VirCaptureLevel_ChangeFlag")]
public class VirCaptureLevel_ChangeFlag : ICallGSHandler
{
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var req = JsonSerializer.Deserialize<VirCaptureChangeFlagParam>(param);
if (req == null || req.LevelId == 0 || req.RegionId == 0)
{
await CallGSRouter.SendScript(connection, "VirCaptureLevel_ChangeFlag", "{\"sErr\":\"error.BadParam\"}");
return;
}
var player = connection.Player!;
var sync = new NtfSyncPlayer();
VirCaptureStateHelper.SetPointState(player, (uint)req.LevelId, (uint)req.RegionId, req.Clean ? 0u : 1u, sync);
DatabaseHelper.SaveDatabaseType(player.Data);
var rsp = $"{{\"nLevelID\":{req.LevelId},\"nRegionId\":{req.RegionId},\"bClean\":{req.Clean.ToString().ToLowerInvariant()}}}";
await CallGSRouter.SendScript(connection, "VirCaptureLevel_ChangeFlag", rsp, sync);
}
}
internal sealed class VirCaptureChangeFlagParam
{
[JsonPropertyName("nLevelID")]
public int LevelId { get; set; }
[JsonPropertyName("nRegionId")]
public int RegionId { get; set; }
[JsonPropertyName("bClean")]
public bool Clean { get; set; }
}

View File

@@ -0,0 +1,171 @@
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;
using System.Text.Json.Serialization;
namespace MikuSB.GameServer.Server.CallGS.Handlers.VirCapture;
[CallGSApi("VirCaptureLevel_EnterLevel")]
public class VirCaptureLevel_EnterLevel : ICallGSHandler
{
private const uint GroupId = 128;
private const uint MapDataStart = 10000;
private const uint MaxMapCount = 3;
private const uint MaxMapDataLen = 3000;
private const uint OffMapId = 1;
private const uint OffDayNight = 7;
private const uint OffMapLevel = 8;
private static readonly Random Random = new();
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var req = JsonSerializer.Deserialize<VirCaptureEnterLevelParam>(param);
if (req == null || req.LevelId == 0 || req.TeamId <= 0)
{
await CallGSRouter.SendScript(connection, "VirCaptureLevel_EnterLevel", "{\"sErr\":\"error.BadParam\"}");
return;
}
var now = DateTime.Now;
var act = ResolveCurrent(GameData.VirCaptureTimeData.Values, now);
if (act == null || !act.CaptureRegionId.Contains((uint)req.LevelId))
{
await CallGSRouter.SendScript(connection, "VirCaptureLevel_EnterLevel", "{\"sErr\":\"ui.TxtNotOpen\"}");
return;
}
if (!GameData.VirCaptureCaptureRegionData.TryGetValue((uint)req.LevelId, out var region))
{
await CallGSRouter.SendScript(connection, "VirCaptureLevel_EnterLevel", "{\"sErr\":\"error.BadParam\"}");
return;
}
var regionStart = ParseConfigTime(region.StartTime);
var regionEnd = ParseConfigTime(region.EndTime);
if (!regionStart.HasValue || !regionEnd.HasValue || now < regionStart.Value || now >= regionEnd.Value)
{
await CallGSRouter.SendScript(connection, "VirCaptureLevel_EnterLevel", "{\"sErr\":\"ui.TxtNotOpen\"}");
return;
}
var player = connection.Player!;
var sync = new NtfSyncPlayer();
EnsureMapState(player, (uint)req.LevelId, sync);
var rsp = $"{{\"nSeed\":{Random.Next(1, 1_000_000_000)}}}";
await CallGSRouter.SendScript(connection, "VirCaptureLevel_EnterLevel", rsp, sync);
}
private static void EnsureMapState(PlayerInstance player, uint levelId, NtfSyncPlayer sync)
{
var slotStart = FindOrAllocateMapSlot(player, levelId);
if (slotStart == 0)
return;
EnsureMapAttr(player, slotStart + OffMapId, levelId, sync);
EnsureMapAttr(player, slotStart + OffDayNight, 1, sync);
EnsureMapAttr(player, slotStart + OffMapLevel, 1, sync);
}
private static uint FindOrAllocateMapSlot(PlayerInstance player, uint levelId)
{
uint? emptySlot = null;
for (uint i = 0; i < MaxMapCount; i++)
{
var slotStart = MapDataStart + (i * MaxMapDataLen);
var mapIdAttr = player.Data.Attrs.FirstOrDefault(x => x.Gid == GroupId && x.Sid == slotStart + OffMapId);
if (mapIdAttr?.Val == levelId)
return slotStart;
if (emptySlot == null && (mapIdAttr == null || mapIdAttr.Val == 0))
emptySlot = slotStart;
}
return emptySlot ?? 0;
}
private static void EnsureMapAttr(PlayerInstance player, uint sid, uint minValue, NtfSyncPlayer sync)
{
var attr = player.Data.Attrs.FirstOrDefault(x => x.Gid == GroupId && x.Sid == sid);
if (attr == null)
{
attr = new PlayerAttr
{
Gid = GroupId,
Sid = sid,
Val = minValue
};
player.Data.Attrs.Add(attr);
SyncAttr(player, sync, sid, minValue);
return;
}
if (attr.Val < minValue)
{
attr.Val = minValue;
SyncAttr(player, sync, sid, attr.Val);
}
}
private static void SyncAttr(PlayerInstance player, NtfSyncPlayer sync, uint sid, uint value)
{
sync.Custom[player.ToPackedAttrKey(GroupId, sid)] = value;
sync.Custom[player.ToShiftedAttrKey(GroupId, sid)] = value;
}
private static VirCaptureTimeExcel? ResolveCurrent(IEnumerable<VirCaptureTimeExcel> 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);
if (latestStarted != null && latestStarted.End > latestStarted.Start)
return latestStarted.Config;
return null;
}
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 VirCaptureEnterLevelParam
{
[JsonPropertyName("nLevelID")]
public int LevelId { get; set; }
[JsonPropertyName("nTeamID")]
public int TeamId { get; set; }
}

View File

@@ -0,0 +1,222 @@
using MikuSB.Database;
using MikuSB.Data;
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.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);
if (req == null || req.LevelId == 0 || req.RegionId == 0)
{
await CallGSRouter.SendScript(connection, "VirCaptureLevel_SaveCapture", "{\"sErr\":\"error.BadParam\"}");
return;
}
var player = connection.Player!;
var sync = new NtfSyncPlayer();
VirCaptureStateHelper.SetPointState(player, (uint)req.LevelId, (uint)req.RegionId, 2u, sync);
if (!GameData.VirCaptureCaptureRegionData.TryGetValue((uint)req.LevelId, out var captureRegion))
{
await CallGSRouter.SendScript(connection, "VirCaptureLevel_SaveCapture", "{\"sErr\":\"error.BadParam\"}");
return;
}
var rewardGdpl = VirCaptureCaptureRewardResolver.ResolveGdpl(captureRegion, (uint)req.RegionId);
if (rewardGdpl == null || rewardGdpl.Count < 4 || rewardGdpl[0] != (uint)ItemTypeEnum.TYPE_MONSTER_CARD)
{
await CallGSRouter.SendScript(connection, "VirCaptureLevel_SaveCapture", "{\"sErr\":\"error.BadParam\"}", sync);
return;
}
var grantedItem = await player.InventoryManager.AddMonsterCardItem(
rewardGdpl[1],
rewardGdpl[2],
rewardGdpl[3],
sendPacket: false);
if (grantedItem == null)
{
await CallGSRouter.SendScript(connection, "VirCaptureLevel_SaveCapture", "{\"sErr\":\"error.BadParam\"}", sync);
return;
}
sync.Items.Add(grantedItem.ToProto());
SyncVirCaptureCounters(player, grantedItem.TemplateId, sync);
ApplyCaptureExp(player, grantedItem.TemplateId, sync);
DatabaseHelper.SaveDatabaseType(player.Data);
DatabaseHelper.SaveDatabaseType(player.InventoryManager.InventoryData);
var response = new JsonObject
{
["nLevelID"] = req.LevelId,
["nRegionId"] = req.RegionId,
["nAddItemId"] = grantedItem.UniqueId,
["tbGDPL"] = new JsonArray(rewardGdpl.Select(x => JsonValue.Create((int)x)).ToArray())
};
await CallGSRouter.SendScript(connection, "VirCaptureLevel_SaveCapture", response.ToJsonString(), sync);
}
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, BagNumSid, bagCount, sync);
if (!GameData.MonsterCardData.TryGetValue(templateId, out var monsterCard) || monsterCard.RikiId == 0)
return;
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 = RikiGroupId,
Sid = monsterCard.RikiId,
Val = 0
};
player.Data.Attrs.Add(rikiAttr);
}
rikiAttr.Val += 1;
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;
}
}
internal sealed class VirCaptureSaveCaptureParam
{
[JsonPropertyName("nLevelID")]
public int LevelId { get; set; }
[JsonPropertyName("nRegionId")]
public int RegionId { get; set; }
}

View File

@@ -0,0 +1,45 @@
using MikuSB.Database;
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("VirCaptureLevel_SaveFightData")]
public class VirCaptureLevel_SaveFightData : ICallGSHandler
{
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var req = JsonSerializer.Deserialize<VirCaptureSaveFightDataParam>(param);
if (req == null || req.LevelId == 0 || req.RegionId == 0)
{
await CallGSRouter.SendScript(connection, "VirCaptureLevel_SaveFightData", "{\"sErr\":\"error.BadParam\"}");
return;
}
var player = connection.Player!;
var sync = new NtfSyncPlayer();
VirCaptureStateHelper.SetPointState(player, (uint)req.LevelId, (uint)req.RegionId, 2u, sync);
DatabaseHelper.SaveDatabaseType(player.Data);
var response = new JsonObject
{
["nLevelID"] = req.LevelId,
["nRegionId"] = req.RegionId,
["tbRewards"] = new JsonArray()
};
await CallGSRouter.SendScript(connection, "VirCaptureLevel_SaveFightData", response.ToJsonString(), sync);
}
}
internal sealed class VirCaptureSaveFightDataParam
{
[JsonPropertyName("nLevelID")]
public int LevelId { get; set; }
[JsonPropertyName("nRegionId")]
public int RegionId { get; set; }
}

View File

@@ -0,0 +1,48 @@
using MikuSB.Database;
using MikuSB.Proto;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace MikuSB.GameServer.Server.CallGS.Handlers.VirCapture;
[CallGSApi("VirCaptureLevel_SavePos")]
public class VirCaptureLevel_SavePos : ICallGSHandler
{
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var req = JsonSerializer.Deserialize<VirCaptureSavePosParam>(param);
if (req == null || req.LevelId == 0)
{
await CallGSRouter.SendScript(connection, "VirCaptureLevel_SavePos", "{\"sErr\":\"error.BadParam\"}");
return;
}
var player = connection.Player!;
var sync = new NtfSyncPlayer();
VirCaptureStateHelper.SetSignedMapOffset(player, (uint)req.LevelId, VirCaptureStateHelper.OffPosX, req.PosX, sync);
VirCaptureStateHelper.SetSignedMapOffset(player, (uint)req.LevelId, VirCaptureStateHelper.OffPosY, req.PosY, sync);
VirCaptureStateHelper.SetSignedMapOffset(player, (uint)req.LevelId, VirCaptureStateHelper.OffPosZ, req.PosZ, sync);
VirCaptureStateHelper.SetSignedMapOffset(player, (uint)req.LevelId, VirCaptureStateHelper.OffToward, req.Toward, sync);
DatabaseHelper.SaveDatabaseType(player.Data);
await CallGSRouter.SendScript(connection, "VirCaptureLevel_SavePos", "{}", sync);
}
}
internal sealed class VirCaptureSavePosParam
{
[JsonPropertyName("nLevelID")]
public int LevelId { get; set; }
[JsonPropertyName("nPosX")]
public int PosX { get; set; }
[JsonPropertyName("nPosY")]
public int PosY { get; set; }
[JsonPropertyName("nPosZ")]
public int PosZ { get; set; }
[JsonPropertyName("nToward")]
public int Toward { get; set; }
}

View File

@@ -0,0 +1,157 @@
using MikuSB.Database.Player;
using MikuSB.GameServer.Game.Player;
using MikuSB.Proto;
namespace MikuSB.GameServer.Server.CallGS.Handlers.VirCapture;
internal static class VirCaptureStateHelper
{
public const uint GroupId = 128;
public const uint MapDataStart = 10000;
public const uint MapDataEnd = 19000;
public const uint MaxMapCount = 3;
public const uint MaxMapDataLen = 3000;
public const uint MaxPatrolPoint = 500;
public const uint MaxOtherPoint = 2500;
public const uint MinMaterialId = 50000;
public const uint MaxMaterialId = 51500;
public const uint OffMapId = 1;
public const uint OffTurnNum = 2;
public const uint OffPosX = 3;
public const uint OffPosY = 4;
public const uint OffPosZ = 5;
public const uint OffToward = 6;
public const uint OffDayNight = 7;
public const uint OffMapLevel = 8;
public const uint OffPatrolStart = 51;
public const uint OffPatrolEnd = 1000;
public const uint OffOtherStart = 1001;
public const uint OffOtherEnd = 1500;
public const uint OffMaterialStart = 1501;
public const uint OffMaterialEnd = 3000;
public static uint FindOrAllocateMapSlot(PlayerInstance player, uint levelId)
{
uint? emptySlot = null;
for (uint i = 0; i < MaxMapCount; i++)
{
var slotStart = MapDataStart + (i * MaxMapDataLen);
var mapIdAttr = player.Data.Attrs.FirstOrDefault(x => x.Gid == GroupId && x.Sid == slotStart + OffMapId);
if (mapIdAttr?.Val == levelId)
return slotStart;
if (emptySlot == null && (mapIdAttr == null || mapIdAttr.Val == 0))
emptySlot = slotStart;
}
return emptySlot ?? 0;
}
public static void EnsureBaseMapState(PlayerInstance player, uint levelId, NtfSyncPlayer sync)
{
var slotStart = FindOrAllocateMapSlot(player, levelId);
if (slotStart == 0)
return;
EnsureUnsignedAttr(player, slotStart + OffMapId, levelId, sync);
EnsureUnsignedAttr(player, slotStart + OffDayNight, 1, sync);
EnsureUnsignedAttr(player, slotStart + OffMapLevel, 1, sync);
}
public static void SetSignedMapOffset(PlayerInstance player, uint levelId, uint offset, int value, NtfSyncPlayer sync)
{
var slotStart = FindOrAllocateMapSlot(player, levelId);
if (slotStart == 0)
return;
EnsureBaseMapState(player, levelId, sync);
SetUnsignedAttr(player, slotStart + offset, unchecked((uint)value), sync);
}
public static void SetPointState(PlayerInstance player, uint levelId, uint pointId, uint value, NtfSyncPlayer sync)
{
var slotStart = FindOrAllocateMapSlot(player, levelId);
if (slotStart == 0 || pointId == 0)
return;
EnsureBaseMapState(player, levelId, sync);
if (pointId <= MaxPatrolPoint)
{
var sid = slotStart + (OffPatrolStart - 1) + pointId;
SetUnsignedAttr(player, sid, value, sync);
return;
}
if (pointId <= MaxOtherPoint)
{
var relative = pointId - MaxPatrolPoint;
var sid = slotStart + (uint)Math.Floor(relative / 30d) + OffOtherStart;
if (sid > slotStart + OffOtherEnd)
return;
var bit = (int)(relative % 30);
var attr = GetOrCreateAttr(player, sid);
var next = value > 0
? attr.Val | (1u << bit)
: attr.Val & ~(1u << bit);
if (next != attr.Val)
{
attr.Val = next;
SyncAttr(player, sync, sid, next);
}
return;
}
if (pointId > MinMaterialId && pointId <= MaxMaterialId)
{
var sid = slotStart + (OffMaterialStart - 1) + (pointId - MinMaterialId);
if (sid >= slotStart + OffMaterialEnd)
return;
SetUnsignedAttr(player, sid, value, sync);
}
}
public static void EnsureUnsignedAttr(PlayerInstance player, uint sid, uint minValue, NtfSyncPlayer sync)
{
var attr = GetOrCreateAttr(player, sid);
if (attr.Val < minValue)
{
attr.Val = minValue;
SyncAttr(player, sync, sid, attr.Val);
}
}
public static void SetUnsignedAttr(PlayerInstance player, uint sid, uint value, NtfSyncPlayer sync)
{
var attr = GetOrCreateAttr(player, sid);
if (attr.Val != value)
{
attr.Val = value;
SyncAttr(player, sync, 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;
}
private static void SyncAttr(PlayerInstance player, NtfSyncPlayer sync, uint sid, uint value)
{
sync.Custom[player.ToPackedAttrKey(GroupId, sid)] = value;
sync.Custom[player.ToShiftedAttrKey(GroupId, sid)] = value;
}
}

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,140 @@
using MikuSB.Data;
using MikuSB.Database;
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.VirCapture;
[CallGSApi("VirCapture_ChangeFormation")]
public class VirCapture_ChangeFormation : ICallGSHandler
{
private const uint StrGroupId = 57;
private const uint FormationSid = 1;
private const uint VirCaptureGroupId = 128;
private const uint CurLevelSid = 3;
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var req = JsonSerializer.Deserialize<VirCaptureChangeFormationParam>(param);
if (req == null)
{
await CallGSRouter.SendScript(connection, "VirCapture_ChangeFormation", "{\"sErr\":\"error.BadParam\"}");
return;
}
var player = connection.Player!;
var formation = ReadFormation(player);
var addId = (uint)Math.Max(0, req.Id);
var unloadId = (uint)Math.Max(0, req.UnloadId);
var unloadIndex = unloadId == 0 ? -1 : formation.FindIndex(x => x == unloadId);
if (unloadId > 0 && unloadIndex < 0)
{
await CallGSRouter.SendScript(connection, "VirCapture_ChangeFormation", "{\"sErr\":\"error.BadParam\"}");
return;
}
if (addId > 0)
{
if (formation.Contains(addId))
{
await CallGSRouter.SendScript(connection, "VirCapture_ChangeFormation", "{\"sErr\":\"error.BadParam\"}");
return;
}
var addItem = player.InventoryManager.GetNormalItem(addId);
if (addItem == null || addItem.ItemType != ItemTypeEnum.TYPE_MONSTER_CARD)
{
await CallGSRouter.SendScript(connection, "VirCapture_ChangeFormation", "{\"sErr\":\"error.BadParam\"}");
return;
}
}
if (unloadIndex >= 0)
formation.RemoveAt(unloadIndex);
if (addId > 0)
{
if (unloadIndex >= 0 && unloadIndex <= formation.Count)
formation.Insert(unloadIndex, addId);
else
formation.Add(addId);
}
if (!ValidateFormation(player, formation))
{
await CallGSRouter.SendScript(connection, "VirCapture_ChangeFormation", "{\"sErr\":\"error.BadParam\"}");
return;
}
var json = JsonSerializer.Serialize(formation);
player.SetStrAttr(StrGroupId, FormationSid, json);
DatabaseHelper.SaveDatabaseType(player.Data);
var sync = new NtfSyncPlayer();
sync.CustomStr[player.ToShiftedAttrKey(StrGroupId, FormationSid)] = json;
var response = new JsonObject
{
["nId"] = req.Id,
["nUnloadId"] = req.UnloadId,
["bAdd"] = addId > 0
};
await CallGSRouter.SendScript(connection, "VirCapture_ChangeFormation", response.ToJsonString(), sync);
}
private static List<uint> ReadFormation(MikuSB.GameServer.Game.Player.PlayerInstance player)
{
var raw = player.Data.StrAttrs.FirstOrDefault(x => x.Gid == StrGroupId && x.Sid == FormationSid)?.Val;
if (string.IsNullOrWhiteSpace(raw))
return [];
try
{
return JsonSerializer.Deserialize<List<uint>>(raw) ?? [];
}
catch
{
return [];
}
}
private static bool ValidateFormation(MikuSB.GameServer.Game.Player.PlayerInstance player, List<uint> formation)
{
var curLevel = player.Data.Attrs.FirstOrDefault(x => x.Gid == VirCaptureGroupId && x.Sid == CurLevelSid)?.Val ?? 1;
if (!GameData.VirCaptureLevelListData.TryGetValue(curLevel, out var levelCfg))
return formation.Count == 0;
if (formation.Count > levelCfg.Num)
return false;
uint totalCost = 0;
foreach (var itemId in formation)
{
var item = player.InventoryManager.GetNormalItem(itemId);
if (item == null || item.ItemType != ItemTypeEnum.TYPE_MONSTER_CARD)
return false;
if (!GameData.MonsterCardData.TryGetValue(item.TemplateId, out var monsterCfg))
return false;
totalCost += monsterCfg.CostValue;
}
return totalCost <= levelCfg.MaxCost;
}
}
internal sealed class VirCaptureChangeFormationParam
{
[JsonPropertyName("nId")]
public int Id { get; set; }
[JsonPropertyName("nUnloadId")]
public int UnloadId { get; set; }
}

View File

@@ -0,0 +1,164 @@
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.VirCapture;
[CallGSApi("VirCapture_CheckOpenAct")]
public class VirCapture_CheckOpenAct : ICallGSHandler
{
private const uint GroupId = 128;
private const uint ActIdSid = 1;
private const uint CurLevelSid = 3;
private const uint TrialActIdSid = 6;
private const uint SeasonActIdSid = 9;
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var now = DateTime.Now;
var act = ResolveCurrent(GameData.VirCaptureTimeData.Values, now);
if (act == null)
{
await CallGSRouter.SendScript(connection, "VirCapture_CheckOpenAct", "{\"bOpen\":false}");
return;
}
var player = connection.Player!;
var sync = new NtfSyncPlayer();
SetAttr(player, ActIdSid, act.Id, sync);
EnsureMinAttr(player, CurLevelSid, 1, sync);
var response = new JsonObject
{
["bOpen"] = true,
["nId"] = act.Id,
["nStartTime"] = ToUnixSeconds(ParseConfigTime(act.StartTime)),
["nEndTime"] = ToUnixSeconds(ParseConfigTime(act.EndTime))
};
var season = ResolveCurrent(GameData.VirCaptureSeasonData.Values, now);
if (season != null)
{
SetAttr(player, SeasonActIdSid, season.Id, sync);
response["tbSeason"] = new JsonObject
{
["nId"] = season.Id,
["nStartTime"] = ToUnixSeconds(ParseConfigTime(season.StartTime)),
["nEndTime"] = ToUnixSeconds(ParseConfigTime(season.EndTime))
};
}
else
{
SetAttr(player, SeasonActIdSid, 0, sync);
}
var trial = ResolveCurrent(GameData.VirCaptureTrialTimeData.Values, now);
SetAttr(player, TrialActIdSid, trial?.Id ?? 0, sync);
await CallGSRouter.SendScript(connection, "VirCapture_CheckOpenAct", response.ToJsonString(), sync);
}
private static T? ResolveCurrent<T>(IEnumerable<T> configs, DateTime now) where T : class
{
var parsed = configs
.Select(x => new
{
Config = x,
Start = ParseConfigTime(GetTimeValue(x, true)),
End = ParseConfigTime(GetTimeValue(x, false))
})
.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);
if (latestStarted != null && latestStarted.End > latestStarted.Start)
return latestStarted.Config;
return null;
}
private static string? GetTimeValue<T>(T value, bool start) where T : class
{
return value switch
{
VirCaptureTimeExcel time => start ? time.StartTime : time.EndTime,
VirCaptureSeasonExcel season => start ? season.StartTime : season.EndTime,
_ => null
};
}
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 EnsureMinAttr(PlayerInstance player, uint sid, uint minValue, NtfSyncPlayer sync)
{
var attr = GetOrCreateAttr(player, sid);
if (attr.Val < minValue)
{
attr.Val = minValue;
SyncAttr(player, sync, sid, attr.Val);
}
}
private static void SetAttr(PlayerInstance player, uint sid, uint value, NtfSyncPlayer sync)
{
var attr = GetOrCreateAttr(player, sid);
if (attr.Val != value)
{
attr.Val = value;
SyncAttr(player, sync, 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;
}
private static void SyncAttr(PlayerInstance player, NtfSyncPlayer sync, uint sid, uint value)
{
sync.Custom[player.ToPackedAttrKey(GroupId, sid)] = value;
sync.Custom[player.ToShiftedAttrKey(GroupId, sid)] = value;
}
}

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

@@ -21,6 +21,7 @@ namespace MikuSB.GameServer.Server.Packet.Recv.Login;
public class HandlerReqLogin : Handler
{
private static readonly Logger Logger = new("ReqLogin");
private const int SupportCardLoginSplitThreshold = 2000;
private static string? ExtractSdkAuthToken(string? token)
{
@@ -80,7 +81,10 @@ public class HandlerReqLogin : Handler
$"Debug-{DateTime.Now:yyyy-MM-dd HH-mm-ss}.log");
await connection.Player.OnEnterGame();
connection.Player.Connection = connection;
await connection.SendPacket(new PacketRspLogin(connection.Player!));
var splitSupportCards = connection.Player.InventoryManager.InventoryData.SupportCards.Count > SupportCardLoginSplitThreshold;
await connection.SendPacket(new PacketRspLogin(connection.Player!, !splitSupportCards));
if (splitSupportCards)
await SendSupportCardsOnLogin(connection);
await connection.SendPacket(new PacketNtfCallScript(connection.Player!));
await SendDebugLoginState(connection);
@@ -90,6 +94,22 @@ public class HandlerReqLogin : Handler
await SendGirlSkinTypeOnLogin(connection);
}
private static async Task SendSupportCardsOnLogin(Connection connection)
{
var player = connection.Player;
if (player == null)
return;
var supportCards = player.InventoryManager.InventoryData.SupportCards.Values.ToList();
Logger.Info($"Split support card sync on login: total={supportCards.Count}, chunkSize={SupportCardLoginSplitThreshold}");
foreach (var chunk in supportCards.Chunk(SupportCardLoginSplitThreshold))
{
var packet = new PacketNtfCallScript(chunk.ToList());
await connection.SendPacket(packet);
}
}
private static void ApplySavedGirlSkinTypes(PlayerInstance player)
{
var inventoryData = player.InventoryManager.InventoryData;

View File

@@ -10,18 +10,41 @@ public class PacketRspLogin : BasePacket
{
private static readonly Logger Logger = new("RspLogin");
public PacketRspLogin(PlayerInstance player) : base(CmdIds.RspLogin)
public PacketRspLogin(PlayerInstance player, bool includeSupportCards = true) : base(CmdIds.RspLogin)
{
var characterCount = player.CharacterManager.CharacterData.Characters.Count;
var itemCount = player.InventoryManager.InventoryData.Items.Count;
var skinCount = player.InventoryManager.InventoryData.Skins.Count;
var weaponCount = player.InventoryManager.InventoryData.Weapons.Count;
var supportCardCount = player.InventoryManager.InventoryData.SupportCards.Count;
var attrCount = player.Data.Attrs.Count;
var strAttrCount = player.Data.StrAttrs.Count;
var showItemCount = player.Data.ShowItems.Count;
var proto = new RspLogin
{
Timestamp = (uint)Extensions.GetUnixSec(),
WorldChannel = 1,
AreaId = 1,
Data = player.ToPlayerProto(),
Data = player.ToPlayerProto(includeSupportCards),
NeedRename = false
};
var bytes = Google.Protobuf.MessageExtensions.ToByteArray(proto);
Logger.Info(
"RspLogin content: " +
$"characters={characterCount}, " +
$"items={itemCount}, " +
$"skins={skinCount}, " +
$"weapons={weaponCount}, " +
$"supportCards={supportCardCount}, " +
$"supportCardsInRspLogin={(includeSupportCards ? supportCardCount : 0)}, " +
$"attrs={attrCount}, " +
$"strAttrs={strAttrCount}, " +
$"showItems={showItemCount}, " +
$"protoItems={proto.Data.Items.Count}, " +
$"protoAttrs={proto.Data.Attrs.Count}, " +
$"protoStrAttrs={proto.Data.StrAttrs.Count}");
Logger.Info($"RspLogin proto size: {bytes.Length} bytes");
SetData(bytes);

View File

@@ -1,6 +1,8 @@
using MikuSB.Database.Character;
using MikuSB.Database.Character;
using MikuSB.Database.Inventory;
using MikuSB.Enums.Item;
using MikuSB.GameServer.Game.Player;
using MikuSB.GameServer.Game.Support;
using MikuSB.Proto;
using MikuSB.TcpSharp;
@@ -10,14 +12,14 @@ public class PacketNtfCallScript : BasePacket
{
public PacketNtfCallScript(List<CharacterInfo> characters) : base(CmdIds.NtfScript)
{
var proto = new NtfCallScript
{
var proto = new NtfCallScript
{
Api = "",
Arg = "{}",
ExtraSync = new NtfSyncPlayer
{
Items = { characters.Select(x => x.ToProto()) }
}
}
};
SetData(proto);
@@ -61,7 +63,7 @@ public class PacketNtfCallScript : BasePacket
Arg = "{}",
ExtraSync = new NtfSyncPlayer
{
Items = { cards.Select(x => x.ToProto()) }
Items = { cards.Select(ToSupportCardProto) }
}
};
@@ -95,7 +97,7 @@ public class PacketNtfCallScript : BasePacket
foreach (var item in inventory.Items.Values) extraSync.Items.Add(item.ToProto());
foreach (var skin in inventory.Skins.Values) extraSync.Items.Add(skin.ToProto());
foreach (var weapon in inventory.Weapons.Values) extraSync.Items.Add(weapon.ToProto());
foreach (var supportCard in inventory.SupportCards.Values) extraSync.Items.Add(supportCard.ToProto());
foreach (var supportCard in inventory.SupportCards.Values) extraSync.Items.Add(ToSupportCardProto(supportCard));
proto.ExtraSync = extraSync;
SetData(proto);
}
@@ -124,8 +126,18 @@ 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);
}
private static Item ToSupportCardProto(GameSupportCardInfo card)
{
SupportAffixStateService.NormalizePendingState(card);
var proto = card.ToProto();
proto.Slots[(uint)ItemSupportCardSlotTypeEnum.SLOT_AFFIXINDEX] = SupportAffixStateService.GetVisibleInitialAffixIndex(card);
return 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.

Some files were not shown because too many files have changed in this diff Show More