diff --git a/Common/Configuration/ConfigContainer.cs b/Common/Configuration/ConfigContainer.cs index 4b4dc03..4be7b60 100644 --- a/Common/Configuration/ConfigContainer.cs +++ b/Common/Configuration/ConfigContainer.cs @@ -60,6 +60,7 @@ public class ServerOption public string FallbackLanguage { get; set; } = "EN"; public string[] DefaultPermissions { get; set; } = ["Admin"]; public ServerProfile ServerProfile { get; set; } = new(); + public bool EnableGmMenu { get; set; } = true; public bool AutoCreateUser { get; set; } = true; public bool SavePersonalDebugFile { get; set; } = false; public bool AutoSendResponseWhenNoHandler { get; set; } = true; diff --git a/Common/Data/Excel/BreakExcel.cs b/Common/Data/Excel/BreakExcel.cs new file mode 100644 index 0000000..5abd201 --- /dev/null +++ b/Common/Data/Excel/BreakExcel.cs @@ -0,0 +1,34 @@ +using Newtonsoft.Json; + +namespace MikuSB.Data.Excel; + +[ResourceEntity("break.json")] +public class BreakExcel : ExcelResource +{ + [JsonProperty("ID")] public int Id { get; set; } + + [JsonProperty("Items1")] public List> Items1 { get; set; } = []; + [JsonProperty("Items2")] public List> Items2 { get; set; } = []; + [JsonProperty("Items3")] public List> Items3 { get; set; } = []; + [JsonProperty("Items4")] public List> Items4 { get; set; } = []; + [JsonProperty("Items5")] public List> Items5 { get; set; } = []; + [JsonProperty("Items6")] public List> Items6 { get; set; } = []; + + public List> GetItems(uint breakLevel) => breakLevel switch + { + 1 => Items1, + 2 => Items2, + 3 => Items3, + 4 => Items4, + 5 => Items5, + 6 => Items6, + _ => [] + }; + + public override uint GetId() => (uint)Id; + + public override void Loaded() + { + GameData.BreakData[Id] = this; + } +} diff --git a/Common/Data/Excel/CallItemExcel.cs b/Common/Data/Excel/CallItemExcel.cs new file mode 100644 index 0000000..2c1fb60 --- /dev/null +++ b/Common/Data/Excel/CallItemExcel.cs @@ -0,0 +1,24 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace MikuSB.Data.Excel; + +[ResourceEntity("call_item.json")] +public class CallItemExcel : ExcelResource +{ + public uint Genre { get; set; } + public uint Detail { get; set; } + public uint Particular { get; set; } + public uint Level { get; set; } + public string I18n { get; set; } = ""; + + public override uint GetId() + { + return (uint)I18n.GetHashCode(); + } + + public override void Loaded() + { + GameData.CallItemData.Add(GetId(), this); + } +} diff --git a/Common/Data/Excel/CardSkinPartsExcel.cs b/Common/Data/Excel/CardSkinPartsExcel.cs new file mode 100644 index 0000000..dd97684 --- /dev/null +++ b/Common/Data/Excel/CardSkinPartsExcel.cs @@ -0,0 +1,27 @@ +using Newtonsoft.Json; + +namespace MikuSB.Data.Excel; + +[ResourceEntity("card_skin_parts.json")] +public class CardSkinPartsExcel : ExcelResource +{ + public uint Genre { get; set; } + public uint Detail { get; set; } + public uint Particular { get; set; } + public uint Level { get; set; } + public uint Icon { get; set; } + public int AppearID { get; set; } + [JsonProperty("profile")] public List> CardSkinID { get; set; } = []; + public string I18n { get; set; } = ""; + [JsonIgnore] public ulong TemplateId { get; set; } + public override uint GetId() + { + return (uint)I18n.GetHashCode(); + } + + public override void Loaded() + { + TemplateId = GameResourceTemplateId.FromGdpl(Genre, Detail, Particular, Level); + GameData.CardSkinPartsData.Add(Icon, this); + } +} \ No newline at end of file diff --git a/Common/Data/Excel/DailyLevelExcel.cs b/Common/Data/Excel/DailyLevelExcel.cs new file mode 100644 index 0000000..12d0974 --- /dev/null +++ b/Common/Data/Excel/DailyLevelExcel.cs @@ -0,0 +1,14 @@ +namespace MikuSB.Data.Excel; + +[ResourceEntity("daily_level.json")] +public class DailyLevelExcel : ExcelResource +{ + public uint ID { get; set; } + + public override uint GetId() => ID; + + public override void Loaded() + { + GameData.DailyLevelData.Add(ID, this); + } +} diff --git a/Common/Data/Excel/ProfileExcel.cs b/Common/Data/Excel/ProfileExcel.cs new file mode 100644 index 0000000..8084e13 --- /dev/null +++ b/Common/Data/Excel/ProfileExcel.cs @@ -0,0 +1,22 @@ +namespace MikuSB.Data.Excel; + +[ResourceEntity("profile.json")] +public class ProfileExcel : ExcelResource +{ + public uint Genre { get; set; } + public uint Detail { get; set; } + public uint Particular { get; set; } + public uint Level { get; set; } + public string I18n { get; set; } = ""; + public string LuaType { get; set; } = ""; + + public override uint GetId() + { + return (uint)I18n.GetHashCode(); + } + + public override void Loaded() + { + GameData.ProfileData.Add(GetId(), this); + } +} diff --git a/Common/Data/Excel/WeaponSkinExcel.cs b/Common/Data/Excel/WeaponSkinExcel.cs new file mode 100644 index 0000000..a51d9c1 --- /dev/null +++ b/Common/Data/Excel/WeaponSkinExcel.cs @@ -0,0 +1,24 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace MikuSB.Data.Excel; + +[ResourceEntity("weapon_skin.json")] +public class WeaponSkinExcel : ExcelResource +{ + public uint Genre { get; set; } + public uint Detail { get; set; } + public uint Particular { get; set; } + public uint Level { get; set; } + public string I18n { get; set; } = ""; + + public override uint GetId() + { + return (uint)I18n.GetHashCode(); + } + + public override void Loaded() + { + GameData.WeaponSkinData.Add(GetId(), this); + } +} diff --git a/Common/Data/GameData.cs b/Common/Data/GameData.cs index 45cf34c..1a90c0c 100644 --- a/Common/Data/GameData.cs +++ b/Common/Data/GameData.cs @@ -16,9 +16,15 @@ public static class GameData public static Dictionary ArItemData { get; private set; } = []; public static Dictionary ManifestationData { get; private set; } = []; public static Dictionary Rogue3DDifficultData { get; private set; } = []; + public static Dictionary BreakData { get; private set; } = []; public static Dictionary SpineData { get; private set; } = []; public static Dictionary NodeConditionData { get; private set; } = []; public static List SupportCardData { get; private set; } = []; + public static Dictionary WeaponSkinData { get; private set; } = []; + public static Dictionary DailyLevelData { get; private set; } = []; + public static Dictionary ProfileData { get; private set; } = []; + public static Dictionary CardSkinPartsData { get; private set; } = []; + public static Dictionary CallItemData { get; private set; } = []; } public static class GameResourceTemplateId diff --git a/Common/Database/Character/CharacterData.cs b/Common/Database/Character/CharacterData.cs index 3cbb890..6eb42e0 100644 --- a/Common/Database/Character/CharacterData.cs +++ b/Common/Database/Character/CharacterData.cs @@ -23,13 +23,14 @@ public class CharacterInfo public int Trust { get; set; } public uint WeaponUniqueId { get; set; } public uint SkinId { get; set; } + public uint WeaponSkinId { get; set; } + public uint SupportTeamIndex { get; set; } = 1; public ItemFlagEnum Flag { get; set; } = ItemFlagEnum.FLAG_READED; public uint Expiration { get; set; } [SugarColumn(IsJson = true)] public List UnlockedSkin { get; set; } = []; [SugarColumn(IsJson = true)] public List Spines { get; set; } = []; [SugarColumn(IsJson = true)] public List Affixs { get; set; } = []; - // Key = EqSlot (= support card Detail), Value = support card UniqueId - [SugarColumn(IsJson = true)] public Dictionary SupportSlots { get; set; } = []; + [SugarColumn(IsJson = true)] public Dictionary SupportSlots { get; set; } = []; // Key = EqSlot (= support card Detail), Value = support card UniqueId public long Timestamp { get; set; } public uint Count { get; set; } = 1; @@ -55,8 +56,10 @@ public class CharacterInfo proto.Enhance.Spines.AddRange(Spines.Select(x => (ulong)x)); proto.Enhance.Affixs.AddRange(Affixs); - proto.Slots[4] = WeaponUniqueId; - proto.Slots[5] = SkinId; + proto.Slots[(uint)ItemCardSlotTypeEnum.SLOT_WEAPON] = WeaponUniqueId; + proto.Slots[(uint)ItemCardSlotTypeEnum.SLOT_SKIN] = SkinId; + proto.Slots[(uint)ItemCardSlotTypeEnum.SLOT_WEAPON_SKIN] = WeaponSkinId; + proto.Slots[(uint)ItemCardSlotTypeEnum.SLOT_SUPPORTERINDEX] = SupportTeamIndex; foreach (var (slot, uid) in SupportSlots) proto.Slots[slot] = uid; diff --git a/Common/Database/Inventory/InventoryData.cs b/Common/Database/Inventory/InventoryData.cs index 495e18d..3d1f89e 100644 --- a/Common/Database/Inventory/InventoryData.cs +++ b/Common/Database/Inventory/InventoryData.cs @@ -17,6 +17,12 @@ public class InventoryData : BaseDatabaseDataHelper [SugarColumn(IsJson = true)] public Dictionary Skins { get; set; } = []; // Key: UniqueId + + [SugarColumn(IsJson = true)] + public Dictionary SupportCards { get; set; } = []; // Key: UniqueId + + [SugarColumn(IsJson = true)] + public Dictionary SkinTypesBySkinId { get; set; } = []; // Key: nSkinId, Value: client nType } public class BaseGameItemInfo @@ -50,6 +56,7 @@ public abstract class GrowableItemInfo : BaseGameItemInfo public new uint Level { get; set; } public new uint Exp { get; set; } public uint Break { get; set; } + public uint Evolue { get; set; } public uint EquipAvatarId { get; set; } } @@ -67,13 +74,16 @@ public class GameWeaponInfo : GrowableItemInfo { Level = Level, Exp = Exp, - Break = Break + Break = Break, + Evolue = Evolue } }; return proto; } -}public class GameSkinInfo : BaseGameItemInfo +} +public class GameSkinInfo : BaseGameItemInfo { + [SugarColumn(IsJson = true)] public Dictionary PartSlots { get; set; } = []; public uint SkinType { get; set; } public override Item ToProto() { @@ -84,7 +94,31 @@ public class GameWeaponInfo : GrowableItemInfo Count = ItemCount, Flag = (uint)Flag, }; - proto.Slots[11] = SkinType; + proto.Slots[(uint)ItemSkinSlotTypeEnum.SLOT_CARD_SKIL_TYPE] = Math.Min(SkinType, 1); + foreach (var (slot, uid) in PartSlots) proto.Slots[slot] = uid; return proto; } -} \ No newline at end of file +} + + +public class GameSupportCardInfo : BaseGameItemInfo +{ + public uint AffixId { get; set; } + public override Item ToProto() + { + var proto = new Item + { + Id = UniqueId, + Template = TemplateId, + Count = ItemCount, + Flag = (uint)Flag, + Enhance = new Enhance + { + Level = Level, + Exp = Exp + } + }; + proto.Slots[(uint)ItemSupportCardSlotTypeEnum.SLOT_AFFIXINDEX] = AffixId; + return proto; + } +} diff --git a/Common/Enums/Item/ItemFlagEnum.cs b/Common/Enums/Item/ItemFlagEnum.cs deleted file mode 100644 index 8686b8d..0000000 --- a/Common/Enums/Item/ItemFlagEnum.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace MikuSB.Enums.Item; - -public enum ItemFlagEnum -{ - FLAG_USE = 1,// 使用中 - FLAG_LOCK = 2,// 锁定中 - FLAG_READED = 4,// 道具已查看 - FLAG_LEAVE = 8,// 角色大招后离场 - FLAG_WEAPON_DEFAULT = 16,// 武器显示原始样式 - FLAG_WEAPON_AUDIO = 32,// 武器消音器音效 - FLAG_ROLE_LIKE = 64,// 心选角色 -} - diff --git a/Common/Enums/Item/ItemTypeEnum.cs b/Common/Enums/Item/ItemTypeEnum.cs index 8e7be17..273cdc3 100644 --- a/Common/Enums/Item/ItemTypeEnum.cs +++ b/Common/Enums/Item/ItemTypeEnum.cs @@ -25,4 +25,56 @@ public enum ItemTypeEnum TYPE_MAIN_SCENE = 21, //主界面场景道具 TYPE_AR = 24, //AR道具 TYPE_CALL = 25, //电话陪伴道具 +} + +public enum ItemCardSlotTypeEnum +{ + SLOT_SUPPORTERCARD1 = 1, // 后勤卡 + SLOT_SUPPORTERCARD2 = 2, // 后勤卡 + SLOT_SUPPORTERCARD3 = 3, // 后勤卡 + SLOT_WEAPON = 4, // 武器 + SLOT_SKIN = 5, // 时装 + SLOT_WEAPON_SKIN = 6, // 武器时装 + SLOT_SUPPORTERINDEX = 7, // 当前使用的后勤组 + SLOT_SUPPORTERCARD4 = 8, // 后勤卡 + SLOT_SUPPORTERCARD5 = 9, // 后勤卡 + SLOT_SUPPORTERCARD6 = 10, // 后勤卡 + SLOT_SUPPORTERCARD7 = 11, // 后勤卡 + SLOT_SUPPORTERCARD8 = 12, // 后勤卡 + SLOT_SUPPORTERCARD9 = 13, // 后勤卡 +} + +public enum ItemSkinPartSlotTypeEnum +{ + SLOT_SkinPartSlot1 = 1, + SLOT_SkinPartSlot2 = 2, + SLOT_SkinPartSlot3 = 3, + SLOT_SkinPartSlot4 = 4, + SLOT_SkinPartSlot5 = 5, + SLOT_SkinPartSlot6 = 6, + SLOT_SkinPartSlot7 = 7, + SLOT_SkinPartSlot8 = 8, + SLOT_SkinPartSlot9 = 9, + SLOT_SkinPartSlot10 = 10, +} + +public enum ItemSkinSlotTypeEnum +{ + SLOT_CARD_SKIL_TYPE = 11 +} + +public enum ItemSupportCardSlotTypeEnum +{ + SLOT_AFFIXINDEX = 1 // 可洗练的初始词缀索引 +} + +public enum ItemFlagEnum +{ + FLAG_USE = 1, // 使用中 + FLAG_LOCK = 2, // 锁定中 + FLAG_READED = 4, // 道具已查看 + FLAG_LEAVE = 8, // 角色大招后离场 + FLAG_WEAPON_DEFAULT = 16, // 武器显示原始样式 + FLAG_WEAPON_AUDIO = 32, // 武器消音器音效 + FLAG_ROLE_LIKE = 64, // 心选角色 } \ No newline at end of file diff --git a/Common/Enums/Player/ProfileTypeEnum.cs b/Common/Enums/Player/ProfileTypeEnum.cs new file mode 100644 index 0000000..bed54fa --- /dev/null +++ b/Common/Enums/Player/ProfileTypeEnum.cs @@ -0,0 +1,24 @@ +namespace MikuSB.Enums.Player; + +public enum ProfileShowItemTypeEnum +{ + SHOWITEM_CARD1 = 1, //第1个展示卡 + SHOWITEM_CARD2 = 2, //第2个展示卡 + SHOWITEM_CARD3 = 3, //第3个展示卡 + SHOWITEM_GIRL = 4, //看板娘 + SHOWITEM_FACE = 5, //头像 + SHOWITEM_FRAME = 6, //头像框 + SHOWITEM_CARD4 = 7, //第4个展示卡 + SHOWITEM_CARD5 = 8, //第5个展示卡 + SHOWITEM_SKIN1 = 9, //第1个展示皮肤 + SHOWITEM_SKIN2 = 10, //第2个展示皮肤 + SHOWITEM_SKIN3 = 11, //第3个展示皮肤 + SHOWITEM_SKIN4 = 12, //第4个展示皮肤 + SHOWITEM_SKIN5 = 13, //第5个展示皮肤 + SHOWITEM_BADGE1 = 14, //第一个展示勋章 + SHOWITEM_BADGE2 = 15, //第二个展示勋章 + SHOWITEM_BADGE3 = 16, //第三个展示勋章 + SHOWITEM_COVER = 17, //展示封面 + SHOWITEM_NAMECARD = 18, //展示名片 + SHOWITEM_BUBBLE = 19, //聊天气泡 +} \ No newline at end of file diff --git a/Common/Internationalization/Message/LanguageCHS.cs b/Common/Internationalization/Message/LanguageCHS.cs index adf2c19..f9f5ac4 100644 --- a/Common/Internationalization/Message/LanguageCHS.cs +++ b/Common/Internationalization/Message/LanguageCHS.cs @@ -35,6 +35,12 @@ public class ServerTextCHS /// public class WordTextCHS { + public string CallItem => "召唤道具"; + public string SkinPart => "皮肤部件"; + public string Profile => "个人资料"; + public string WeaponSkin => "武器皮肤"; + public string SupportCard => "支援卡"; + public string Weapon => "武器"; public string Rank => "星魂"; public string Avatar => "角色"; public string Material => "材料"; @@ -121,6 +127,7 @@ public class CommandTextCHS public HelpTextCHS Help { get; } = new(); public GirlTextCHS Girl { get; } = new(); public GiveAllTextCHS GiveAll { get; } = new(); + public DebugTextCHS Debug { get; } = new(); } #endregion @@ -218,11 +225,13 @@ public class GirlTextCHS public string Usage => "用法: /girl add -p -l -s\n" + - "用法: /girl level "; + "用法: /girl level \n" + + "用法: /girl neuronic "; public string NotFound => "角色不存在!"; public string Added => "已为玩家添加 {0} 个角色!"; public string UpdateLevel => "已将 {1} 个角色等级设置为 {0}!"; + public string UpdateNeuronicLevel => "已将 {1} 个角色的神经元等级设置为 {0}!"; } /// @@ -230,11 +239,31 @@ public class GirlTextCHS /// public class GiveAllTextCHS { - public string Desc => "给玩家所有物品\n" + - "备注: -1 代表全部"; - public string Usage => "用法: /giveall weapon -p -l"; - public string WeaponNotFound => "找不到武器!"; - public string WeaponAdded => "已添加 {0} 把武器给玩家!"; + public string Desc => "给予玩家所有物品\n" + + "注意:-1 表示全部"; + public string Usage => "用法:/giveall weapon -p<特定> -l<等級>\n" + + "用法:/giveall weaponskin -p<特定>\n" + + "用法:/giveall card -p<特定> -l<等級>" + + "用法:/giveall profile -g<类型> -p<特定> -l<等级>" + + "用法:/giveall skinpart -g<類型> -p<特定> -l<等級>" + + "用法:/giveall call -g<類型> -p<特定> -l<等級>"; + public string NotFound => "未找到 {0}!"; + public string GiveAllItems => "已向玩家添加 {0} 个 {1}!"; +} + +/// +/// path: Game.Command.Debug +/// +public class DebugTextCHS +{ + public string Desc => "调试包输出开关"; + public string Usage => "用法: /debug [on|off|simple|detail|file]"; + public string Enabled => "已启用调试包输出。"; + public string Disabled => "已禁用调试包输出。"; + public string SimpleEnabled => "已启用简单调试包输出。"; + public string DetailEnabled => "已启用详细调试包输出。"; + public string FileEnabled => "个人调试文件输出已启用。"; + public string FileDisabled => "个人调试文件输出已禁用。"; } #endregion diff --git a/Common/Internationalization/Message/LanguageCHT.cs b/Common/Internationalization/Message/LanguageCHT.cs index 2851943..8ad0653 100644 --- a/Common/Internationalization/Message/LanguageCHT.cs +++ b/Common/Internationalization/Message/LanguageCHT.cs @@ -35,6 +35,12 @@ public class ServerTextCHT /// public class WordTextCHT { + public string CallItem => "召喚道具"; + public string SkinPart => "外觀部件"; + public string Profile => "個人資料"; + public string WeaponSkin => "武器外觀"; + public string SupportCard => "支援卡"; + public string Weapon => "武器"; public string Rank => "星魂"; public string Avatar => "角色"; public string Material => "材料"; @@ -121,6 +127,7 @@ public class CommandTextCHT public HelpTextCHT Help { get; } = new(); public GirlTextCHT Girl { get; } = new(); public GiveAllTextCHT GiveAll { get; } = new(); + public DebugTextCHT Debug { get; } = new(); } #endregion @@ -218,11 +225,13 @@ public class GirlTextCHT public string Usage => "用法: /girl add -p -l -s\n" + - "用法: /girl level "; + "用法: /girl level \n" + + "用法: /girl neuronic "; public string NotFound => "角色不存在!"; public string Added => "已為玩家新增 {0} 個角色!"; public string UpdateLevel => "已將 {1} 個角色等級設為 {0}!"; + public string UpdateNeuronicLevel => "已將 {1} 個角色的神經元等級設置為 {0}!"; } /// @@ -230,11 +239,31 @@ public class GirlTextCHT /// public class GiveAllTextCHT { - public string Desc => "給玩家所有物品\n" + - "備註: -1 代表全部"; - public string Usage => "用法: /giveall weapon -p -l"; - public string WeaponNotFound => "找不到武器!"; - public string WeaponAdded => "已添加 {0} 把武器給玩家!"; + public string Desc => "給予玩家所有物品\n" + + "注意:-1 表示全部"; + public string Usage => "用法:/giveall weapon -p<特定> -l<等級>\n" + + "用法:/giveall weaponskin -p<特定>\n" + + "用法:/giveall card -p<特定> -l<等級>" + + "用法:/giveall profile -g<類型> -p<特定> -l<等級>" + + "用法:/giveall skinpart -g<類型> -p<特定> -l<等級>" + + "用法:/giveall call -g<類型> -p<特定> -l<等級>"; + public string NotFound => "未找到 {0}!"; + public string GiveAllItems => "已向玩家添加 {0} 個 {1}!"; +} + +/// +/// path: Game.Command.Debug +/// +public class DebugTextCHT +{ + public string Desc => "切換調試封包輸出"; + public string Usage => "用法: /debug [on|off|simple|detail|file]"; + public string Enabled => "已啟用調試封包輸出。"; + public string Disabled => "已停用調試封包輸出。"; + public string SimpleEnabled => "已啟用簡易調試封包輸出。"; + public string DetailEnabled => "已啟用詳細調試封包輸出。"; + public string FileEnabled => "個人調試檔案輸出已啟用。"; + public string FileDisabled => "個人調試檔案輸出已停用。"; } #endregion diff --git a/Common/Internationalization/Message/LanguageEN.cs b/Common/Internationalization/Message/LanguageEN.cs index 8dcffd9..03d4f66 100644 --- a/Common/Internationalization/Message/LanguageEN.cs +++ b/Common/Internationalization/Message/LanguageEN.cs @@ -35,10 +35,13 @@ public class ServerTextEN /// public class WordTextEN { - public string Star => "Star"; + public string CallItem => "Call Item"; + public string SkinPart => "Skin Part"; + public string Profile => "Profile"; + public string WeaponSkin => "Weapon Skin"; public string Valk => "Valkyrie"; public string Material => "Material"; - public string Stigmata => "Stigmata"; + public string SupportCard => "Support Card"; public string Weapon => "Weapon"; public string Banner => "Gacha"; public string Activity => "Activity"; @@ -83,6 +86,7 @@ public class CommandTextEN public HelpTextEN Help { get; } = new(); public GirlTextEN Girl { get; } = new(); public GiveAllTextEN GiveAll { get; } = new(); + public DebugTextEN Debug { get; } = new(); } #endregion @@ -187,11 +191,13 @@ public class GirlTextEN public string Usage => "Usage: /girl add -p -l -s\n" + - "Usage: /girl level "; + "Usage: /girl level \n" + + "Usage: /girl neuronic "; public string NotFound => "Character not found!"; public string Added => "Granted {0} character(s) to player!"; public string UpdateLevel => "Set {1} character(s) to level {0}!"; + public string UpdateNeuronicLevel => "Set {1} character(s) Neuronic to level {0}!"; } /// @@ -201,9 +207,29 @@ public class GiveAllTextEN { public string Desc => "Give all items to player\n"+ "Note: -1 means all"; - public string Usage => "Usage: /giveall weapon -p -l"; - public string WeaponNotFound => "Weapon not found!"; - public string WeaponAdded => "Added {0} weapon(s) to player!"; + public string Usage => "Usage: /giveall weapon -p -l\n" + + "Usage: /giveall weaponskin -p\n" + + "Usage: /giveall card -p -l" + + "Usage: /giveall profile -g -p -l" + + "Usage: /giveall skinpart -g -p -l" + + "Usage: /giveall call -g -p -l"; + public string NotFound => "{0} not found!"; + public string GiveAllItems => "Added {0} {1} to player!"; +} + +/// +/// path: Game.Command.Debug +/// +public class DebugTextEN +{ + public string Desc => "Toggle debug packet output"; + public string Usage => "Usage: /debug [on|off|simple|detail|file]"; + public string Enabled => "Debug packet output enabled."; + public string Disabled => "Debug packet output disabled."; + public string SimpleEnabled => "Simple debug packet output enabled."; + public string DetailEnabled => "Detailed debug packet output enabled."; + public string FileEnabled => "Personal debug file output enabled."; + public string FileDisabled => "Personal debug file output disabled."; } #endregion diff --git a/GameServer/Command/CommandSender.cs b/GameServer/Command/CommandSender.cs index 0486421..e74e794 100644 --- a/GameServer/Command/CommandSender.cs +++ b/GameServer/Command/CommandSender.cs @@ -38,9 +38,9 @@ public class PlayerCommandSender(PlayerInstance player) : ICommandSender Type = ChatType.Friend, Sender = (uint)ConfigManager.Config.ServerOption.ServerProfile.Uid, Recver = (uint)Player.Uid, - Text = msg, + Text = ChatMessageHelper.NormalizeForClient(msg), Profile = Player.ToServerFriendProto(), - TimeStamp = (uint)Extensions.GetUnixMs() + TimeStamp = ChatMessageHelper.BuildClientTimestamp() }; await Player.SendPacket(CmdIds.NtfFriendChat, data); } diff --git a/GameServer/Command/Commands/CommandDebug.cs b/GameServer/Command/Commands/CommandDebug.cs new file mode 100644 index 0000000..1127dce --- /dev/null +++ b/GameServer/Command/Commands/CommandDebug.cs @@ -0,0 +1,69 @@ +using MikuSB.Configuration; +using MikuSB.Enums.Player; +using MikuSB.Util; +using MikuSB.Internationalization; + +namespace MikuSB.GameServer.Command.Commands; + +[CommandInfo("debug", "Game.Command.Debug.Desc", "Game.Command.Debug.Usage", ["dbg"], [PermEnum.Admin, PermEnum.Support])] +public class CommandDebug : ICommands +{ + private static readonly Logger Logger = new("CommandManager"); + + [CommandDefault] + public async ValueTask ToggleDebug(CommandArg arg) + { + var option = arg.Args.FirstOrDefault()?.ToLowerInvariant() ?? "on"; + var serverOption = ConfigManager.Config.ServerOption; + var message = option switch + { + "on" => EnableDebug(serverOption), + "off" => DisableDebug(serverOption), + "simple" => EnableSimpleDebug(serverOption), + "detail" => EnableDetailDebug(serverOption), + "file" => ToggleDebugFile(serverOption), + _ => I18NManager.Translate("Game.Command.Debug.Usage") + }; + + Logger.Info(message); + await arg.SendMsg(message); + } + + private static string EnableDebug(ServerOption serverOption) + { + serverOption.EnableDebug = true; + serverOption.DebugMessage = true; + serverOption.DebugDetailMessage = true; + return I18NManager.Translate("Game.Command.Debug.Enabled"); + } + + private static string DisableDebug(ServerOption serverOption) + { + serverOption.EnableDebug = false; + return I18NManager.Translate("Game.Command.Debug.Disabled"); + } + + private static string EnableSimpleDebug(ServerOption serverOption) + { + serverOption.EnableDebug = true; + serverOption.DebugMessage = true; + serverOption.DebugDetailMessage = false; + return I18NManager.Translate("Game.Command.Debug.SimpleEnabled"); + } + + private static string EnableDetailDebug(ServerOption serverOption) + { + serverOption.EnableDebug = true; + serverOption.DebugMessage = true; + serverOption.DebugDetailMessage = true; + return I18NManager.Translate("Game.Command.Debug.DetailEnabled"); + } + + private static string ToggleDebugFile(ServerOption serverOption) + { + serverOption.SavePersonalDebugFile = !serverOption.SavePersonalDebugFile; + return serverOption.SavePersonalDebugFile + ? I18NManager.Translate("Game.Command.Debug.FileEnabled") + : I18NManager.Translate("Game.Command.Debug.FileDisabled"); + } +} diff --git a/GameServer/Command/Commands/CommandGirl.cs b/GameServer/Command/Commands/CommandGirl.cs index 3341ecf..afeff25 100644 --- a/GameServer/Command/Commands/CommandGirl.cs +++ b/GameServer/Command/Commands/CommandGirl.cs @@ -84,4 +84,49 @@ public class CommandGirl : ICommands level.ToString(), girls.Count.ToString())); } + + [CommandMethod("neuronic")] + public async ValueTask UpdateNeuronicLevel(CommandArg arg) + { + if (!await arg.CheckOnlineTarget()) return; + if (!await arg.CheckArgCnt(2)) return; + + var guid = arg.GetInt(0); + var level = Math.Clamp(arg.GetInt(1), 0, 6); + var player = arg.Target!.Player!; + List girls = []; + + List spines = new List(); + for (int i = 0; i < 6; i++) + spines.Add(i < level ? 511u : 0u); + + uint proLevel = (uint)(spines.Count(x => x == 511) / 2); + + if (guid == -1) + { + foreach (var girl in player.CharacterManager.CharacterData.Characters) + { + girl.Spines = spines; + girl.ProLevel = proLevel; + girls.Add(girl); + } + } + else + { + var girl = player.CharacterManager.GetCharacterByGUID((uint)guid); + if (girl == null) + { + await arg.SendMsg(I18NManager.Translate("Game.Command.Girl.NotFound")); + return; + } + girl.Spines = spines; + girl.ProLevel = proLevel; + girls.Add(girl); + } + + if (girls.Count > 0) await player.SendPacket(new PacketNtfCallScript(girls)); + await arg.SendMsg(I18NManager.Translate("Game.Command.Girl.UpdateNeuronicLevel", + level.ToString(), + girls.Count.ToString())); + } } \ No newline at end of file diff --git a/GameServer/Command/Commands/CommandGiveAll.cs b/GameServer/Command/Commands/CommandGiveAll.cs index 50cb0fd..161fae9 100644 --- a/GameServer/Command/Commands/CommandGiveAll.cs +++ b/GameServer/Command/Commands/CommandGiveAll.cs @@ -18,7 +18,7 @@ public class CommandGiveAll : ICommands if (await arg.GetOption('l') is not int level) return; var detail = arg.GetInt(0); - level = Math.Clamp(level, 1, 80); + level = Math.Clamp(level, 1, 90); var player = arg.Target!.Player!; List weapons = []; if (detail == -1) @@ -36,12 +36,190 @@ public class CommandGiveAll : ICommands var weapon = await player.InventoryManager!.AddWeaponItem(ItemTypeEnum.TYPE_WEAPON, (uint)detail,(uint)particular,1,(uint)level,false); if (weapon == null) { - await arg.SendMsg(I18NManager.Translate("Game.Command.GiveAll.WeaponNotFound")); + await arg.SendMsg(I18NManager.Translate("Game.Command.GiveAll.NotFound", I18NManager.Translate("Word.Weapon"))); return; } weapons.Add(weapon); } if (weapons.Count > 0) await player.SendPacket(new PacketNtfCallScript(weapons)); - await arg.SendMsg(I18NManager.Translate("Game.Command.GiveAll.WeaponAdded", weapons.Count.ToString())); + await arg.SendMsg(I18NManager.Translate("Game.Command.GiveAll.GiveAllItems", + I18NManager.Translate("Word.Weapon"), weapons.Count.ToString())); + } + + [CommandMethod("card")] + public async ValueTask GiveAllSupportCard(CommandArg arg) + { + if (!await arg.CheckOnlineTarget()) return; + if (await arg.GetOption('p') is not int particular) return; + if (await arg.GetOption('l') is not int level) return; + + var detail = arg.GetInt(0); + var player = arg.Target!.Player!; + List supportCards = []; + if (detail == -1) + { + // add all + foreach (var config in GameData.SupportCardData) + { + var supportCard = await player.InventoryManager! + .AddSupportCardItem(config.Detail, config.Particular, config.Level, (uint)level, false); + if (supportCard != null) supportCards.Add(supportCard); + } + } + else + { + var supportCard = await player.InventoryManager!.AddSupportCardItem((uint)detail, (uint)particular, 1, (uint)level, false); + if (supportCard == null) + { + await arg.SendMsg(I18NManager.Translate("Game.Command.GiveAll.NotFound", I18NManager.Translate("Word.SupportCard"))); + return; + } + supportCards.Add(supportCard); + } + if (supportCards.Count > 0) await player.SendPacket(new PacketNtfCallScript(supportCards)); + await arg.SendMsg(I18NManager.Translate("Game.Command.GiveAll.GiveAllItems", + I18NManager.Translate("Word.SupportCard"), supportCards.Count.ToString())); + } + + [CommandMethod("weaponskin")] + public async ValueTask GiveAllWeaponSkin(CommandArg arg) + { + if (!await arg.CheckOnlineTarget()) return; + if (await arg.GetOption('p') is not int particular) return; + + var detail = arg.GetInt(0); + var player = arg.Target!.Player!; + List weaponSkins = []; + if (detail == -1) + { + // add all + foreach (var config in GameData.WeaponSkinData.Values) + { + var weaponSkin = await player.InventoryManager! + .AddWeaponSkinItem((ItemTypeEnum)config.Genre, config.Detail, config.Particular, config.Level, false); + if (weaponSkin != null) weaponSkins.Add(weaponSkin); + } + } + else + { + var weaponSkin = await player.InventoryManager!.AddWeaponSkinItem(ItemTypeEnum.TYPE_WEAPON, (uint)detail, (uint)particular, 1, false); + if (weaponSkin == null) + { + await arg.SendMsg(I18NManager.Translate("Game.Command.GiveAll.NotFound", I18NManager.Translate("Word.WeaponSkin"))); + return; + } + weaponSkins.Add(weaponSkin); + } + if (weaponSkins.Count > 0) await player.SendPacket(new PacketNtfCallScript(weaponSkins)); + await arg.SendMsg(I18NManager.Translate("Game.Command.GiveAll.GiveAllItems", + I18NManager.Translate("Word.WeaponSkin"), weaponSkins.Count.ToString())); + } + + [CommandMethod("profile")] + public async ValueTask GiveAllProfile(CommandArg arg) + { + if (!await arg.CheckOnlineTarget()) return; + if (await arg.GetOption('p') is not int particular) return; + if (await arg.GetOption('l') is not int level) return; + if (await arg.GetOption('g') is not int genre) return; + + var detail = arg.GetInt(0); + var player = arg.Target!.Player!; + List profileItems = []; + if (detail == -1) + { + // add all + foreach (var config in GameData.ProfileData.Values) + { + var profile = await player.InventoryManager! + .AddProfileItem((ItemTypeEnum)config.Genre, config.Detail, config.Particular, config.Level, false); + if (profile != null) profileItems.Add(profile); + } + } + else + { + var profile = await player.InventoryManager!.AddProfileItem((ItemTypeEnum)genre, (uint)detail, (uint)particular, (uint)level, false); + if (profile == null) + { + await arg.SendMsg(I18NManager.Translate("Game.Command.GiveAll.NotFound", I18NManager.Translate("Word.Profile"))); + return; + } + profileItems.Add(profile); + } + if (profileItems.Count > 0) await player.SendPacket(new PacketNtfCallScript(profileItems)); + await arg.SendMsg(I18NManager.Translate("Game.Command.GiveAll.GiveAllItems", + I18NManager.Translate("Word.Profile"), profileItems.Count.ToString())); + } + + [CommandMethod("skinpart")] + public async ValueTask GiveAllSkinPart(CommandArg arg) + { + if (!await arg.CheckOnlineTarget()) return; + if (await arg.GetOption('p') is not int particular) return; + if (await arg.GetOption('l') is not int level) return; + if (await arg.GetOption('g') is not int genre) return; + + var detail = arg.GetInt(0); + var player = arg.Target!.Player!; + List skinPartItems = []; + if (detail == -1) + { + // add all + foreach (var config in GameData.CardSkinPartsData.Values) + { + var skinPart = await player.InventoryManager! + .AddSkinPartItem((ItemTypeEnum)config.Genre, config.Detail, config.Particular, config.Level, false); + if (skinPart != null) skinPartItems.Add(skinPart); + } + } + else + { + var skinPart = await player.InventoryManager!.AddSkinPartItem((ItemTypeEnum)genre, (uint)detail, (uint)particular, (uint)level, false); + if (skinPart == null) + { + await arg.SendMsg(I18NManager.Translate("Game.Command.GiveAll.NotFound", I18NManager.Translate("Word.SkinPart"))); + return; + } + skinPartItems.Add(skinPart); + } + if (skinPartItems.Count > 0) await player.SendPacket(new PacketNtfCallScript(skinPartItems)); + await arg.SendMsg(I18NManager.Translate("Game.Command.GiveAll.GiveAllItems", + I18NManager.Translate("Word.SkinPart"), skinPartItems.Count.ToString())); + } + + [CommandMethod("call")] + public async ValueTask GiveAllCallItem(CommandArg arg) + { + if (!await arg.CheckOnlineTarget()) return; + if (await arg.GetOption('p') is not int particular) return; + if (await arg.GetOption('l') is not int level) return; + if (await arg.GetOption('g') is not int genre) return; + + var detail = arg.GetInt(0); + var player = arg.Target!.Player!; + List callItems = []; + if (detail == -1) + { + // add all + foreach (var config in GameData.CallItemData.Values) + { + var callItem = await player.InventoryManager! + .AddCallItem((ItemTypeEnum)config.Genre, config.Detail, config.Particular, config.Level, false); + if (callItem != null) callItems.Add(callItem); + } + } + else + { + var callItem = await player.InventoryManager!.AddCallItem((ItemTypeEnum)genre, (uint)detail, (uint)particular, (uint)level, false); + if (callItem == null) + { + await arg.SendMsg(I18NManager.Translate("Game.Command.GiveAll.NotFound", I18NManager.Translate("Word.CallItem"))); + return; + } + callItems.Add(callItem); + } + if (callItems.Count > 0) await player.SendPacket(new PacketNtfCallScript(callItems)); + await arg.SendMsg(I18NManager.Translate("Game.Command.GiveAll.GiveAllItems", + I18NManager.Translate("Word.CallItem"), callItems.Count.ToString())); } } \ No newline at end of file diff --git a/GameServer/Game/Inventory/InventoryManager.cs b/GameServer/Game/Inventory/InventoryManager.cs index 28d21c2..27a8915 100644 --- a/GameServer/Game/Inventory/InventoryManager.cs +++ b/GameServer/Game/Inventory/InventoryManager.cs @@ -24,8 +24,8 @@ public class InventoryManager(PlayerInstance player) : BasePlayerManager(player) TemplateId = templateId, UniqueId = InventoryData.NextUniqueUid++, Level = weaponLevel, - Break = weaponData.InitBreak, - ItemType = ItemTypeEnum.TYPE_WEAPON, + Break = GetWeaponBreak(weaponLevel), + ItemType = genre, ItemCount = 1 }; InventoryData.Weapons[weaponInfo.UniqueId] = weaponInfo; @@ -35,6 +35,17 @@ public class InventoryManager(PlayerInstance player) : BasePlayerManager(player) return weaponInfo; } + private static uint GetWeaponBreak(uint level) + { + if (level <= 20) return 1; + if (level <= 40) return 2; + if (level <= 60) return 3; + if (level <= 70) return 4; + if (level <= 80) return 5; + if (level <= 90) return 6; + return 7; + } + public GameWeaponInfo? GetWeaponItem(uint uniqueId) { return InventoryData.Weapons.GetValueOrDefault(uniqueId); @@ -56,13 +67,13 @@ public class InventoryManager(PlayerInstance player) : BasePlayerManager(player) if (genre != ItemTypeEnum.TYPE_CARD_SKIN) return null; var skinData = GameData.CardSkinData.Values.FirstOrDefault(x => x.Genre == (int)genre && x.Detail == detail && x.Particular == particular && x.Level == level); if (skinData == null) return null; - var templateId = GameResourceTemplateId.FromGdpl((uint)genre,detail,particular,level); + if (InventoryData.Items.Values.Any(x => x.TemplateId == templateId)) return null; var skinInfo = new GameSkinInfo { TemplateId = templateId, UniqueId = InventoryData.NextUniqueUid++, - ItemType = ItemTypeEnum.TYPE_CARD_SKIN, + ItemType = genre, ItemCount = 1 }; InventoryData.Skins[skinInfo.UniqueId] = skinInfo; @@ -100,7 +111,7 @@ public class InventoryManager(PlayerInstance player) : BasePlayerManager(player) { TemplateId = templateId, UniqueId = InventoryData.NextUniqueUid++, - ItemType = ItemTypeEnum.TYPE_AR, + ItemType = genre, ItemCount = 1 }; InventoryData.Items[arInfo.UniqueId] = arInfo; @@ -110,26 +121,44 @@ public class InventoryManager(PlayerInstance player) : BasePlayerManager(player) return arInfo; } - public async ValueTask AddSupportCardItem(uint detail, uint particular, uint level = 1, bool sendPacket = true) + public async ValueTask AddSupportCardItem(uint detail, uint particular, uint level = 1, uint cardLevel = 1, bool sendPacket = true) { const ItemTypeEnum genre = ItemTypeEnum.TYPE_SUPPORT; + var spCard = GameData.SupportCardData.FirstOrDefault(x => x.Genre == (int)genre && x.Detail == detail && x.Particular == particular && x.Level == level); + if (spCard == null) return null; var templateId = GameResourceTemplateId.FromGdpl((uint)genre, detail, particular, level); - if (InventoryData.Items.Values.Any(x => x.TemplateId == templateId)) return null; - - var info = new BaseGameItemInfo + cardLevel = Math.Clamp(cardLevel, 1, spCard.MaxLevel); + var info = new GameSupportCardInfo { TemplateId = templateId, UniqueId = InventoryData.NextUniqueUid++, ItemType = genre, - ItemCount = 1 + ItemCount = 1, + Level = cardLevel, }; - InventoryData.Items[info.UniqueId] = info; + InventoryData.SupportCards[info.UniqueId] = info; if (sendPacket) await Player.SendPacket(new PacketNtfCallScript([info])); return info; } + public GameSupportCardInfo? GetSupportCardItem(uint uniqueId) + { + return InventoryData.SupportCards.GetValueOrDefault(uniqueId); + } + + public GameSupportCardInfo? GetSupportCardByTemplateId(ulong templateId) + { + return InventoryData.SupportCards.Values.FirstOrDefault(x => x.TemplateId == templateId); + } + + public GameSupportCardInfo? GetSupportCardItemGDPL(ItemTypeEnum genre, uint detail, uint particular, uint level) + { + var templateId = GameResourceTemplateId.FromGdpl((uint)genre, detail, particular, level); + return InventoryData.SupportCards.Values.FirstOrDefault(x => x.TemplateId == templateId); + } + public async ValueTask AddManifestationItem(ItemTypeEnum genre, uint detail, uint particular, uint level = 1, bool sendPacket = true) { if (genre != ItemTypeEnum.TYPE_MANIFESTATION) return null; @@ -142,7 +171,7 @@ public class InventoryManager(PlayerInstance player) : BasePlayerManager(player) { TemplateId = templateId, UniqueId = InventoryData.NextUniqueUid++, - ItemType = ItemTypeEnum.TYPE_MANIFESTATION, + ItemType = genre, ItemCount = 1 }; InventoryData.Items[manifestInfo.UniqueId] = manifestInfo; @@ -198,4 +227,89 @@ public class InventoryManager(PlayerInstance player) : BasePlayerManager(player) return itemInfo; } + + public async ValueTask AddWeaponSkinItem(ItemTypeEnum genre, uint detail, uint particular, uint level = 1, bool sendPacket = true) + { + if (genre != ItemTypeEnum.TYPE_WEAPON_SKIN) return null; + var skinData = GameData.WeaponSkinData.Values.FirstOrDefault(x => x.Genre == (int)genre && x.Detail == detail && x.Particular == particular && x.Level == level); + if (skinData == null) return null; + + var templateId = GameResourceTemplateId.FromGdpl((uint)genre, detail, particular, level); + if (InventoryData.Items.Values.Any(x => x.TemplateId == templateId)) return null; + var skinInfo = new BaseGameItemInfo + { + TemplateId = templateId, + UniqueId = InventoryData.NextUniqueUid++, + ItemType = genre, + ItemCount = 1 + }; + InventoryData.Items[skinInfo.UniqueId] = skinInfo; + + if (sendPacket) await Player.SendPacket(new PacketNtfCallScript([skinInfo])); + + return skinInfo; + } + + public async ValueTask AddProfileItem(ItemTypeEnum genre, uint detail, uint particular, uint level = 1, bool sendPacket = true) + { + if (genre < ItemTypeEnum.TYPE_PROFILE || genre > ItemTypeEnum.TYPE_ANALYST) return null; + var profileData = GameData.ProfileData.Values.FirstOrDefault(x => x.Genre == (int)genre && x.Detail == detail && x.Particular == particular && x.Level == level); + if (profileData == null) return null; + var templateId = GameResourceTemplateId.FromGdpl((uint)genre, detail, particular, level); + if (InventoryData.Items.Values.Any(x => x.TemplateId == templateId)) return null; + var profileInfo = new BaseGameItemInfo + { + TemplateId = templateId, + UniqueId = InventoryData.NextUniqueUid++, + ItemType = genre, + ItemCount = 1 + }; + InventoryData.Items[profileInfo.UniqueId] = profileInfo; + + if (sendPacket) await Player.SendPacket(new PacketNtfCallScript([profileInfo])); + + return profileInfo; + } + + public async ValueTask AddSkinPartItem(ItemTypeEnum genre, uint detail, uint particular, uint level = 1, bool sendPacket = true) + { + if (genre != ItemTypeEnum.TYPE_CARD_SKIN_PART) return null; + var skinPartData = GameData.CardSkinPartsData.Values.FirstOrDefault(x => x.Genre == (int)genre && x.Detail == detail && x.Particular == particular && x.Level == level); + if (skinPartData == null) return null; + var templateId = GameResourceTemplateId.FromGdpl((uint)genre, detail, particular, level); + if (InventoryData.Items.Values.Any(x => x.TemplateId == templateId)) return null; + var skinPartInfo = new BaseGameItemInfo + { + TemplateId = templateId, + UniqueId = InventoryData.NextUniqueUid++, + ItemType = genre, + ItemCount = 1 + }; + InventoryData.Items[skinPartInfo.UniqueId] = skinPartInfo; + + if (sendPacket) await Player.SendPacket(new PacketNtfCallScript([skinPartInfo])); + + return skinPartInfo; + } + + public async ValueTask AddCallItem(ItemTypeEnum genre, uint detail, uint particular, uint level = 1, bool sendPacket = true) + { + if (genre != ItemTypeEnum.TYPE_CALL) return null; + var callData = GameData.CallItemData.Values.FirstOrDefault(x => x.Genre == (int)genre && x.Detail == detail && x.Particular == particular && x.Level == level); + if (callData == null) return null; + var templateId = GameResourceTemplateId.FromGdpl((uint)genre, detail, particular, level); + if (InventoryData.Items.Values.Any(x => x.TemplateId == templateId)) return null; + var callInfo = new BaseGameItemInfo + { + TemplateId = templateId, + UniqueId = InventoryData.NextUniqueUid++, + ItemType = genre, + ItemCount = 1 + }; + InventoryData.Items[callInfo.UniqueId] = callInfo; + + if (sendPacket) await Player.SendPacket(new PacketNtfCallScript([callInfo])); + + return callInfo; + } } \ No newline at end of file diff --git a/GameServer/Game/Player/ChatMessageHelper.cs b/GameServer/Game/Player/ChatMessageHelper.cs new file mode 100644 index 0000000..bebdf46 --- /dev/null +++ b/GameServer/Game/Player/ChatMessageHelper.cs @@ -0,0 +1,27 @@ +using System.Text.RegularExpressions; + +namespace MikuSB.GameServer.Game.Player; + +public static partial class ChatMessageHelper +{ + [GeneratedRegex(@"\s+")] + private static partial Regex MultiWhitespaceRegex(); + + public static uint BuildClientTimestamp() + { + return (uint)MikuSB.Util.Extensions.Extensions.GetUnixSec(); + } + + public static string NormalizeForClient(string? text) + { + if (string.IsNullOrWhiteSpace(text)) + return string.Empty; + + var normalized = text + .Replace("\r\n", " ") + .Replace('\r', ' ') + .Replace('\n', ' '); + + return MultiWhitespaceRegex().Replace(normalized, " ").Trim(); + } +} diff --git a/GameServer/Game/Player/PlayerInstance.cs b/GameServer/Game/Player/PlayerInstance.cs index 04995c8..1b77b98 100644 --- a/GameServer/Game/Player/PlayerInstance.cs +++ b/GameServer/Game/Player/PlayerInstance.cs @@ -66,10 +66,6 @@ public class PlayerInstance(PlayerGameData data) { await CharacterManager.AddCharacter((ItemTypeEnum)card.Genre, card.Detail, card.Particular, card.Level, sendPacket:false); } - foreach (var sc in GameData.SupportCardData) - { - await InventoryManager.AddSupportCardItem(sc.Detail, sc.Particular, sc.Level, sendPacket: false); - } foreach (var supplies in GameData.AllSuppliesData) { await InventoryManager.AddSuppliesItem(supplies, 90000, false); @@ -83,34 +79,6 @@ public class PlayerInstance(PlayerGameData data) await LineupManager.UpdateLineup(1, selected[0], selected[1], selected[2],false); - var bootstrapAttrs = BuildLobbyBootstrapAttrs(); - var existingAttrs = Data.Attrs - .ToDictionary(x => (x.Gid, x.Sid)); - var seenAttrs = new HashSet<(uint Gid, uint Sid)>(); - - foreach (var (gid, sid, value) in bootstrapAttrs) - { - if (!seenAttrs.Add((gid, sid))) - continue; - - if (existingAttrs.TryGetValue((gid, sid), out var attr)) - { - if (attr.Val < value) - attr.Val = value; - - continue; - } - - var newAttr = new PlayerAttr - { - Gid = gid, - Sid = sid, - Val = value - }; - - Data.Attrs.Add(newAttr); - existingAttrs[(gid, sid)] = newAttr; - } }); t.Wait(); @@ -192,9 +160,9 @@ public class PlayerInstance(PlayerGameData data) Sender = sendUid, Recver = recvUid, Emoji = emojiId ?? 0, - Text = message ?? "", + Text = ChatMessageHelper.NormalizeForClient(message), Profile = Data.ToProfileProto(), - TimeStamp = (uint)Extensions.GetUnixMs() + TimeStamp = ChatMessageHelper.BuildClientTimestamp() }; await SendPacket(CmdIds.NtfFriendChat, data); @@ -230,6 +198,7 @@ public class PlayerInstance(PlayerGameData data) public Proto.Player ToPlayerProto() { + BuildPlayerAttr(); var displayName = PlayerGameData.NormalizeDisplayName(Data.Name); var proto = new Proto.Player { @@ -241,6 +210,7 @@ public class PlayerInstance(PlayerGameData data) Sex = Data.Gender, Vigor = Data.Vigor, Solutions = { LineupManager.LineupData.LineupInfo.Values.Select(x => x.ToProto()) }, + Badges = { InventoryManager.InventoryData.Items.Values.Where(x => x.ItemType == ItemTypeEnum.TYPE_BADGE).Select(x => (ulong)x.UniqueId) } }; foreach (var chara in CharacterManager.CharacterData.Characters) proto.Items.Add(chara.ToProto()); @@ -297,6 +267,38 @@ public class PlayerInstance(PlayerGameData data) return (gid << 16) | sid; } + public void BuildPlayerAttr() + { + var bootstrapAttrs = BuildLobbyBootstrapAttrs(); + var existingAttrs = Data.Attrs + .ToDictionary(x => (x.Gid, x.Sid)); + var seenAttrs = new HashSet<(uint Gid, uint Sid)>(); + + foreach (var (gid, sid, value) in bootstrapAttrs) + { + if (!seenAttrs.Add((gid, sid))) + continue; + + if (existingAttrs.TryGetValue((gid, sid), out var attr)) + { + if (attr.Val < value) + attr.Val = value; + + continue; + } + + var newAttr = new PlayerAttr + { + Gid = gid, + Sid = sid, + Val = value + }; + + Data.Attrs.Add(newAttr); + existingAttrs[(gid, sid)] = newAttr; + } + } + private static IEnumerable<(uint Gid, uint Sid, uint Value)> BuildLobbyBootstrapAttrs() { // GuideLogic uses group 4. Value 999 is safely above every configured step count, @@ -341,6 +343,12 @@ public class PlayerInstance(PlayerGameData data) yield return (22, levelId, 1_700_000_000); } + foreach (var levelId in GameData.DailyLevelData.Keys) + { + yield return (21, levelId, 7); + yield return (22, levelId, 1_700_000_000); + } + // Main Scene 0 mean default scene yield return (132, 1, 0); } diff --git a/GameServer/Server/CallGS/Handlers/Daily/Daily_SetSelectSuit.cs b/GameServer/Server/CallGS/Handlers/Daily/Daily_SetSelectSuit.cs new file mode 100644 index 0000000..d233aba --- /dev/null +++ b/GameServer/Server/CallGS/Handlers/Daily/Daily_SetSelectSuit.cs @@ -0,0 +1,26 @@ +using System.Text.Json; + +namespace MikuSB.GameServer.Server.CallGS.Handlers.Daily; + +[CallGSApi("Daily_SetSelectSuit")] +public class Daily_SetSelectSuit : ICallGSHandler +{ + + public async Task Handle(Connection connection, string param, ushort seqNo) + { + var req = JsonSerializer.Deserialize(param); + if (req == null) + { + await CallGSRouter.SendScript(connection, "Daily_SetSelectSuit", "{}"); + return; + } + var rsp = $"{{\"SuitId\":{req.Suit}}}"; + await CallGSRouter.SendScript(connection, "Daily_SetSelectSuit", rsp); + } +} + +internal sealed class GirlWeaponSkinParam +{ + public uint Type { get; set; } + public uint Suit { get; set; } +} diff --git a/GameServer/Server/CallGS/Handlers/Girl/EnterGirlRoom.cs b/GameServer/Server/CallGS/Handlers/Girl/EnterGirlRoom.cs index d2101d5..5a476ef 100644 --- a/GameServer/Server/CallGS/Handlers/Girl/EnterGirlRoom.cs +++ b/GameServer/Server/CallGS/Handlers/Girl/EnterGirlRoom.cs @@ -12,11 +12,19 @@ public class EnterGirlRoom : ICallGSHandler var req = JsonSerializer.Deserialize(param); var response = new JsonObject { - ["nCardId"] = req?.CardId ?? 1, - ["nSkinId"] = req?.SkinId ?? 0, - ["bOpen"] = true + ["nCardId"] = 0, + ["nSkinId"] = 0, + ["bOpen"] = false }; + if (req == null) + { + await CallGSRouter.SendScript(connection, "EnterGirlRoom", response.ToJsonString()); + return; + } + response["nCardId"] = req.CardId; + response["nSkinId"] = req.SkinId; + response["bOpen"] = true; await CallGSRouter.SendScript(connection, "EnterGirlRoom", response.ToJsonString()); } } @@ -26,6 +34,6 @@ internal sealed class EnterGirlRoomParam [JsonPropertyName("nSkinId")] public int SkinId { get; set; } - [JsonPropertyName("nCardID")] + [JsonPropertyName("nCardId")] public uint CardId { get; set; } } \ No newline at end of file diff --git a/GameServer/Server/CallGS/Handlers/Girl/GirlSkinParts_Update.cs b/GameServer/Server/CallGS/Handlers/Girl/GirlSkinParts_Update.cs new file mode 100644 index 0000000..f4e7f43 --- /dev/null +++ b/GameServer/Server/CallGS/Handlers/Girl/GirlSkinParts_Update.cs @@ -0,0 +1,57 @@ +using Azure; +using MikuSB.Data; +using MikuSB.Database; +using MikuSB.Database.Inventory; +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.Girl; + +[CallGSApi("GirlSkinParts_Update")] +public class GirlSkinParts_Update : ICallGSHandler +{ + public async Task Handle(Connection connection, string param, ushort seqNo) + { + var req = JsonSerializer.Deserialize(param); + if (req == null) + { + await CallGSRouter.SendScript(connection, "GirlSkinParts_Update", "{\"sErr\":\"error.BadParam\"}"); + return; + } + var player = connection.Player!; + var data = new List(); + foreach(var partId in req.PartsId) + { + var partData = player.InventoryManager.GetNormalItem(partId); + if (partData == null) continue; + + var partExcel = GameData.CardSkinPartsData.Values.FirstOrDefault(x => x.TemplateId == partData.TemplateId); + if (partExcel == null) continue; + + var skinData = player.InventoryManager.GetSkinItem(req.SkinId); + if (skinData == null) continue; + + skinData.PartSlots[partExcel.Detail] = partData.UniqueId; + data.Add(skinData); + } + + var sync = new NtfSyncPlayer + { + Items = { data.Select(x => x.ToProto()) } + }; + await CallGSRouter.SendScript(connection, "GirlSkinParts_Update", "{}", sync); + } +} + +internal sealed class GirlSkinPartsUpdateParam +{ + [JsonPropertyName("tbPartsID")] + public List PartsId { get; set; } = []; + + [JsonPropertyName("nSkinId")] + public uint SkinId { get; set; } +} diff --git a/GameServer/Server/CallGS/Handlers/Girl/GirlSkin_ChangeSkinType.cs b/GameServer/Server/CallGS/Handlers/Girl/GirlSkin_ChangeSkinType.cs index 268f890..e1a992b 100644 --- a/GameServer/Server/CallGS/Handlers/Girl/GirlSkin_ChangeSkinType.cs +++ b/GameServer/Server/CallGS/Handlers/Girl/GirlSkin_ChangeSkinType.cs @@ -1,4 +1,9 @@ -using MikuSB.Proto; +using MikuSB.Data; +using MikuSB.Database; +using MikuSB.Database.Inventory; +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; @@ -11,9 +16,10 @@ public class GirlSkin_ChangeSkinType : ICallGSHandler public async Task Handle(Connection connection, string param, ushort seqNo) { var req = JsonSerializer.Deserialize(param); + var skinType = ClampClientSkinType(req?.Type ?? 0); var response = new JsonObject { - ["nType"] = req?.Type ?? 1, + ["nType"] = skinType, ["nSkinId"] = req?.SkinId }; if (req == null) @@ -21,16 +27,22 @@ public class GirlSkin_ChangeSkinType : ICallGSHandler await CallGSRouter.SendScript(connection, "GirlSkin_ChangeSkinType", response.ToJsonString()); return; } - + var player = connection.Player!; - var skinData = player.InventoryManager.GetSkinItem(req.SkinId); + var skinData = GetOrCreateSkinItem(player, req.SkinId); + if (skinData != null) + skinData.SkinType = skinType; + + player.InventoryManager.InventoryData.SkinTypesBySkinId ??= []; + player.InventoryManager.InventoryData.SkinTypesBySkinId[req.SkinId] = skinType; + DatabaseHelper.SaveDatabaseType(player.InventoryManager.InventoryData); + if (skinData == null) { await CallGSRouter.SendScript(connection, "GirlSkin_ChangeSkinType", response.ToJsonString()); return; } - skinData.SkinType = req.Type; var sync = new NtfSyncPlayer { Items = { skinData.ToProto() } @@ -38,6 +50,42 @@ public class GirlSkin_ChangeSkinType : ICallGSHandler await CallGSRouter.SendScript(connection, "GirlSkin_ChangeSkinType", response.ToJsonString(), sync); } + + internal static uint ClampClientSkinType(uint skinType) + { + return Math.Min(skinType, 1); + } + + internal static GameSkinInfo? GetOrCreateSkinItem(PlayerInstance player, uint skinId) + { + var inventoryData = player.InventoryManager.InventoryData; + if (inventoryData.Skins.TryGetValue(skinId, out var skinInfo)) + return skinInfo; + + if (!GameData.CardSkinData.TryGetValue(skinId, out var skinData)) + return null; + + var templateId = GameResourceTemplateId.FromGdpl(skinData.Genre, skinData.Detail, skinData.Particular, skinData.Level); + skinInfo = player.InventoryManager.GetSkinItemByTemplateId(templateId); + if (skinInfo != null) + { + inventoryData.Skins.Remove(skinInfo.UniqueId); + skinInfo.UniqueId = skinId; + } + else + { + skinInfo = new GameSkinInfo + { + UniqueId = skinId, + TemplateId = templateId, + ItemType = ItemTypeEnum.TYPE_CARD_SKIN, + ItemCount = 1 + }; + } + + inventoryData.Skins[skinId] = skinInfo; + return skinInfo; + } } internal sealed class ChangeSkinTypeParam diff --git a/GameServer/Server/CallGS/Handlers/Girl/GirlWeaponSkin_Change.cs b/GameServer/Server/CallGS/Handlers/Girl/GirlWeaponSkin_Change.cs new file mode 100644 index 0000000..14bac71 --- /dev/null +++ b/GameServer/Server/CallGS/Handlers/Girl/GirlWeaponSkin_Change.cs @@ -0,0 +1,47 @@ +using MikuSB.Proto; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; + +namespace MikuSB.GameServer.Server.CallGS.Handlers.Girl; + +[CallGSApi("GirlWeaponSkin_Change")] +public class GirlWeaponSkin_Change : ICallGSHandler +{ + public async Task Handle(Connection connection, string param, ushort seqNo) + { + var req = JsonSerializer.Deserialize(param); + if (req == null) + { + await CallGSRouter.SendScript(connection, "GirlWeaponSkin_Change", "{\"err\":\"error.BadParam\"}"); + return; + } + + var player = connection.Player!; + var cardData = player.CharacterManager.GetCharacterByGUID(req.CardId); + if (cardData == null) return; + var skinData = player.InventoryManager.GetNormalItem(req.SkinId); + if (skinData == null) + { + await CallGSRouter.SendScript(connection, "GirlWeaponSkin_Change", "{\"err\":\"error.BadParam\"}"); + return; + } + + cardData.WeaponSkinId = req.SkinId; + var sync = new NtfSyncPlayer + { + Items = { cardData.ToProto() } + }; + + await CallGSRouter.SendScript(connection, "GirlWeaponSkin_Change", "null", sync); + } +} + +internal sealed class GirlWeaponSkinParam +{ + [JsonPropertyName("nCardId")] + public uint CardId { get; set; } + + [JsonPropertyName("nSkinId")] + public uint SkinId { get; set; } +} diff --git a/GameServer/Server/CallGS/Handlers/Girl/RoleCard_SetSupporterTeamIndex.cs b/GameServer/Server/CallGS/Handlers/Girl/RoleCard_SetSupporterTeamIndex.cs new file mode 100644 index 0000000..e16bc28 --- /dev/null +++ b/GameServer/Server/CallGS/Handlers/Girl/RoleCard_SetSupporterTeamIndex.cs @@ -0,0 +1,37 @@ +using MikuSB.Proto; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; + +namespace MikuSB.GameServer.Server.CallGS.Handlers.Girl; + +[CallGSApi("RoleCard_SetSupporterTeamIndex")] +public class RoleCard_SetSupporterTeamIndex : ICallGSHandler +{ + public async Task Handle(Connection connection, string param, ushort seqNo) + { + var req = JsonSerializer.Deserialize(param); + if (req == null) + { + await CallGSRouter.SendScript(connection, "RoleCard_SetSupporterTeamIndex", "{\"err\":\"error.BadParam\"}"); + return; + } + var player = connection.Player!; + var cardData = player.CharacterManager.GetCharacterByGUID(req.CardId); + if (cardData == null) return; + + cardData.SupportTeamIndex = req.Index; + var sync = new NtfSyncPlayer + { + Items = { cardData.ToProto() } + }; + await CallGSRouter.SendScript(connection, "RoleCard_SetSupporterTeamIndex", "null", sync); + } +} + +internal sealed class SetSupporterTeamIndexParam +{ + [JsonPropertyName("Id")] + public uint CardId { get; set; } + public uint Index { get; set; } +} diff --git a/GameServer/Server/CallGS/Handlers/Misc/PlayerSetting_ChangeShowCard.cs b/GameServer/Server/CallGS/Handlers/Misc/PlayerSetting_ChangeShowCard.cs index 1c2957f..4347bbb 100644 --- a/GameServer/Server/CallGS/Handlers/Misc/PlayerSetting_ChangeShowCard.cs +++ b/GameServer/Server/CallGS/Handlers/Misc/PlayerSetting_ChangeShowCard.cs @@ -1,4 +1,4 @@ -using MikuSB.Database; +using MikuSB.Enums.Player; using MikuSB.Proto; using System.Text.Json; using System.Text.Json.Serialization; @@ -8,8 +8,6 @@ namespace MikuSB.GameServer.Server.CallGS.Handlers.Misc; [CallGSApi("PlayerSetting_ChangeShowCard")] public class PlayerSetting_ChangeShowCard : ICallGSHandler { - private const int ShowItemGirlIndex = 4; - public async Task Handle(Connection connection, string param, ushort seqNo) { var player = connection.Player!; @@ -23,13 +21,9 @@ public class PlayerSetting_ChangeShowCard : ICallGSHandler await CallGSRouter.SendScript(connection, "PlayerSetting_ChangeShowCard", "{}"); return; } - - player.SetShowItem(ShowItemGirlIndex, card.Guid); - DatabaseHelper.SaveDatabaseType(player.Data); - + player.SetShowItem((int)ProfileShowItemTypeEnum.SHOWITEM_GIRL, card.Guid); var sync = new NtfSyncPlayer(); sync.ShowItems.AddRange(player.Data.ShowItems); - await CallGSRouter.SendScript(connection, "PlayerSetting_ChangeShowCard", "{}", sync); } } diff --git a/GameServer/Server/CallGS/Handlers/Misc/PlayerSetting_SetProfileFace.cs b/GameServer/Server/CallGS/Handlers/Misc/PlayerSetting_SetProfileFace.cs new file mode 100644 index 0000000..ed50134 --- /dev/null +++ b/GameServer/Server/CallGS/Handlers/Misc/PlayerSetting_SetProfileFace.cs @@ -0,0 +1,49 @@ +using MikuSB.Enums.Player; +using MikuSB.Proto; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace MikuSB.GameServer.Server.CallGS.Handlers.Misc; + +[CallGSApi("PlayerSetting_SetProfileFace")] +public class PlayerSetting_SetProfileFace : ICallGSHandler +{ + public async Task Handle(Connection connection, string param, ushort seqNo) + { + var player = connection.Player!; + var req = JsonSerializer.Deserialize(param); + if (req == null) + return; + + if (req.HeadItemId > 0) + { + var item = player.InventoryManager.GetNormalItem(req.HeadItemId); + if (item == null) + { + await CallGSRouter.SendScript(connection, "PlayerSetting_SetProfileFace", "{\"err\":\"error.BadParam\"}"); + return; + } + player.SetShowItem((int)ProfileShowItemTypeEnum.SHOWITEM_FACE, item.UniqueId); + } + if (req.FrameItemId > 0) + { + var item = player.InventoryManager.GetNormalItem(req.FrameItemId); + if (item == null) + { + await CallGSRouter.SendScript(connection, "PlayerSetting_SetProfileFace", "{\"err\":\"error.BadParam\"}"); + return; + } + player.SetShowItem((int)ProfileShowItemTypeEnum.SHOWITEM_FRAME, item.UniqueId); + } + + var sync = new NtfSyncPlayer(); + sync.ShowItems.AddRange(player.Data.ShowItems); + await CallGSRouter.SendScript(connection, "PlayerSetting_SetProfileFace", "null", sync); + } +} + +internal sealed class SetProfileFaceParam +{ + [JsonPropertyName("nHeadItemID")] public uint HeadItemId { get; set; } + [JsonPropertyName("nFrameItemID")] public uint FrameItemId { get; set; } +} diff --git a/GameServer/Server/CallGS/Handlers/Misc/PlayerSetting_SetShowBadge.cs b/GameServer/Server/CallGS/Handlers/Misc/PlayerSetting_SetShowBadge.cs new file mode 100644 index 0000000..4dab8bb --- /dev/null +++ b/GameServer/Server/CallGS/Handlers/Misc/PlayerSetting_SetShowBadge.cs @@ -0,0 +1,43 @@ +using MikuSB.Enums.Player; +using MikuSB.Proto; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace MikuSB.GameServer.Server.CallGS.Handlers.Misc; + +[CallGSApi("PlayerSetting_SetShowBadge")] +public class PlayerSetting_SetShowBadge : ICallGSHandler +{ + public async Task Handle(Connection connection, string param, ushort seqNo) + { + var player = connection.Player!; + var req = JsonSerializer.Deserialize(param); + if (req == null) + { + await CallGSRouter.SendScript(connection, "PlayerSetting_SetShowBadge", "{\"err\":\"error.BadParam\"}"); + return; + } + + var slots = new[] + { + ProfileShowItemTypeEnum.SHOWITEM_BADGE1, + ProfileShowItemTypeEnum.SHOWITEM_BADGE2, + ProfileShowItemTypeEnum.SHOWITEM_BADGE3 + }; + for (int i = 0; i < slots.Length; i++) + { + var uniqueId = i < req.Badges.Count ? req.Badges[i] : 0; + player.SetShowItem((int)slots[i], uniqueId); + } + + var sync = new NtfSyncPlayer(); + sync.ShowItems.AddRange(player.Data.ShowItems); + await CallGSRouter.SendScript(connection, "PlayerSetting_SetShowBadge", "null", sync); + } +} + +internal sealed class SetShowBadgeParam +{ + [JsonPropertyName("tbBadge")] + public List Badges { get; set; } = []; +} diff --git a/GameServer/Server/CallGS/Handlers/Misc/PlayerSetting_SetShowBubble.cs b/GameServer/Server/CallGS/Handlers/Misc/PlayerSetting_SetShowBubble.cs new file mode 100644 index 0000000..5922f5c --- /dev/null +++ b/GameServer/Server/CallGS/Handlers/Misc/PlayerSetting_SetShowBubble.cs @@ -0,0 +1,36 @@ +using MikuSB.Enums.Player; +using MikuSB.Proto; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace MikuSB.GameServer.Server.CallGS.Handlers.Misc; + +[CallGSApi("PlayerSetting_SetShowBubble")] +public class PlayerSetting_SetShowBubble : ICallGSHandler +{ + public async Task Handle(Connection connection, string param, ushort seqNo) + { + var player = connection.Player!; + var req = JsonSerializer.Deserialize(param); + if (req == null) + return; + + var item = player.InventoryManager.GetNormalItem(req.Id); + if (item == null) + { + await CallGSRouter.SendScript(connection, "PlayerSetting_SetShowBubble", "{\"err\":\"error.BadParam\"}"); + return; + } + + player.SetShowItem((int)ProfileShowItemTypeEnum.SHOWITEM_BUBBLE, item.UniqueId); + var sync = new NtfSyncPlayer(); + sync.ShowItems.AddRange(player.Data.ShowItems); + await CallGSRouter.SendScript(connection, "PlayerSetting_SetShowBubble", "null", sync); + } +} + +internal sealed class SetShowBubbleParam +{ + [JsonPropertyName("nID")] + public uint Id { get; set; } +} diff --git a/GameServer/Server/CallGS/Handlers/Misc/PlayerSetting_SetShowCover.cs b/GameServer/Server/CallGS/Handlers/Misc/PlayerSetting_SetShowCover.cs new file mode 100644 index 0000000..c1b7315 --- /dev/null +++ b/GameServer/Server/CallGS/Handlers/Misc/PlayerSetting_SetShowCover.cs @@ -0,0 +1,36 @@ +using MikuSB.Enums.Player; +using MikuSB.Proto; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace MikuSB.GameServer.Server.CallGS.Handlers.Misc; + +[CallGSApi("PlayerSetting_SetShowCover")] +public class PlayerSetting_SetShowCover : ICallGSHandler +{ + public async Task Handle(Connection connection, string param, ushort seqNo) + { + var player = connection.Player!; + var req = JsonSerializer.Deserialize(param); + if (req == null) + return; + + var item = player.InventoryManager.GetNormalItem(req.Id); + if (item == null) + { + await CallGSRouter.SendScript(connection, "PlayerSetting_SetShowCover", "{\"err\":\"error.BadParam\"}"); + return; + } + + player.SetShowItem((int)ProfileShowItemTypeEnum.SHOWITEM_COVER, item.UniqueId); + var sync = new NtfSyncPlayer(); + sync.ShowItems.AddRange(player.Data.ShowItems); + await CallGSRouter.SendScript(connection, "PlayerSetting_SetShowCover", "null", sync); + } +} + +internal sealed class SetShowCoverParam +{ + [JsonPropertyName("nID")] + public uint Id { get; set; } +} diff --git a/GameServer/Server/CallGS/Handlers/Misc/PlayerSetting_SetShowNameCard.cs b/GameServer/Server/CallGS/Handlers/Misc/PlayerSetting_SetShowNameCard.cs new file mode 100644 index 0000000..e1846ff --- /dev/null +++ b/GameServer/Server/CallGS/Handlers/Misc/PlayerSetting_SetShowNameCard.cs @@ -0,0 +1,36 @@ +using MikuSB.Enums.Player; +using MikuSB.Proto; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace MikuSB.GameServer.Server.CallGS.Handlers.Misc; + +[CallGSApi("PlayerSetting_SetShowNameCard")] +public class PlayerSetting_SetShowNameCard : ICallGSHandler +{ + public async Task Handle(Connection connection, string param, ushort seqNo) + { + var player = connection.Player!; + var req = JsonSerializer.Deserialize(param); + if (req == null) + return; + + var item = player.InventoryManager.GetNormalItem(req.Id); + if (item == null) + { + await CallGSRouter.SendScript(connection, "PlayerSetting_SetShowNameCard", "{\"err\":\"error.BadParam\"}"); + return; + } + + player.SetShowItem((int)ProfileShowItemTypeEnum.SHOWITEM_NAMECARD, item.UniqueId); + var sync = new NtfSyncPlayer(); + sync.ShowItems.AddRange(player.Data.ShowItems); + await CallGSRouter.SendScript(connection, "PlayerSetting_SetShowNameCard", "null", sync); + } +} + +internal sealed class SetShowNameCardParam +{ + [JsonPropertyName("nID")] + public uint Id { get; set; } +} diff --git a/GameServer/Server/CallGS/Handlers/SupporterCard/SupporterCard_Equip.cs b/GameServer/Server/CallGS/Handlers/SupporterCard/SupporterCard_Equip.cs index 25277d1..30d0a00 100644 --- a/GameServer/Server/CallGS/Handlers/SupporterCard/SupporterCard_Equip.cs +++ b/GameServer/Server/CallGS/Handlers/SupporterCard/SupporterCard_Equip.cs @@ -25,7 +25,8 @@ public class SupporterCard_Equip : ICallGSHandler return; } - var slot = (uint)req.EqSlot; + var teamIndex = card.SupportTeamIndex; + var slot = GetTeamIndex((uint)req.EqSlot, teamIndex); // If an existing card is equipped in this slot and bForce is false, ask for confirmation if (!req.Force && req.CurrentEquippedUid != 0 && card.SupportSlots.TryGetValue(slot, out var existing) && existing != 0) @@ -46,6 +47,14 @@ public class SupporterCard_Equip : ICallGSHandler var responseApi = string.IsNullOrEmpty(req.Model) ? "Logistics_Change" : "Logistics_Equip"; await CallGSRouter.SendScript(connection, responseApi, "{}", sync); } + + private uint GetTeamIndex(uint slot, uint teamIndex) + { + if (teamIndex == 1) return slot; + if (teamIndex == 2) return slot + 7; + if (teamIndex == 3) return slot + 10; + return slot; + } } internal sealed class SupporterCardEquipParam diff --git a/GameServer/Server/CallGS/Handlers/Weapon/Weapon_Break.cs b/GameServer/Server/CallGS/Handlers/Weapon/Weapon_Break.cs new file mode 100644 index 0000000..96c2996 --- /dev/null +++ b/GameServer/Server/CallGS/Handlers/Weapon/Weapon_Break.cs @@ -0,0 +1,98 @@ +using MikuSB.Data; +using MikuSB.Database; +using MikuSB.Proto; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace MikuSB.GameServer.Server.CallGS.Handlers.Weapon; + +// s2c: function(sErr) — send "null" on success (json.decode("null") = nil = falsy in Lua) +[CallGSApi("Weapon_Break")] +public class Weapon_Break : ICallGSHandler +{ + private const uint MaxBreak = 6; + + public async Task Handle(Connection connection, string param, ushort seqNo) + { + var player = connection.Player!; + var req = JsonSerializer.Deserialize(param); + if (req == null || req.WeaponId == 0) + { + await CallGSRouter.SendScript(connection, "Weapon_Break", "\"error.BadParam\""); + return; + } + + var weapon = player.InventoryManager.InventoryData.Weapons.GetValueOrDefault((uint)req.WeaponId); + if (weapon == null) + { + await CallGSRouter.SendScript(connection, "Weapon_Break", "\"error.BadParam\""); + return; + } + + if (weapon.Break >= MaxBreak) + { + await CallGSRouter.SendScript(connection, "Weapon_Break", "\"tip.already_max_break\""); + return; + } + + var nextBreak = weapon.Break + 1; + + // Look up break cost from WeaponExcel → BreakExcel + var weaponExcel = GameData.WeaponData.Values.FirstOrDefault(x => + GameResourceTemplateId.FromGdpl(x.Genre, x.Detail, x.Particular, x.Level) == weapon.TemplateId); + + var requestedMaterials = new Dictionary(); + if (weaponExcel != null && GameData.BreakData.TryGetValue(weaponExcel.BreakMatID, out var breakExcel)) + { + foreach (var row in breakExcel.GetItems(nextBreak)) + { + if (row.Count < 5) continue; + var tid = GameResourceTemplateId.FromGdpl( + (uint)row[0], (uint)row[1], (uint)row[2], (uint)row[3]); + requestedMaterials[tid] = requestedMaterials.GetValueOrDefault(tid) + (uint)row[4]; + } + } + + // Validate materials + foreach (var (tid, count) in requestedMaterials) + { + var item = player.InventoryManager.InventoryData.Items.Values.FirstOrDefault(x => x.TemplateId == tid); + if (item == null || item.ItemCount < count) + { + await CallGSRouter.SendScript(connection, "Weapon_Break", "\"tip.not_material_for_break\""); + return; + } + } + + // Consume materials + var syncItems = new List(); + foreach (var (tid, count) in requestedMaterials) + { + var item = player.InventoryManager.InventoryData.Items.Values.First(x => x.TemplateId == tid); + item.ItemCount -= count; + var proto = item.ToProto(); + if (item.ItemCount == 0) + { + player.InventoryManager.InventoryData.Items.Remove(item.UniqueId); + proto.Count = 0; + } + syncItems.Add(proto); + } + + weapon.Break = nextBreak; + syncItems.Add(weapon.ToProto()); + + DatabaseHelper.SaveDatabaseType(player.InventoryManager.InventoryData); + + var sync = new NtfSyncPlayer(); + sync.Items.AddRange(syncItems); + + await CallGSRouter.SendScript(connection, "Weapon_Break", "null", sync); + } +} + +internal sealed class WeaponBreakParam +{ + [JsonPropertyName("Id")] + public int WeaponId { get; set; } +} diff --git a/GameServer/Server/CallGS/Handlers/Weapon/Weapon_Evolution.cs b/GameServer/Server/CallGS/Handlers/Weapon/Weapon_Evolution.cs new file mode 100644 index 0000000..bf8f9f1 --- /dev/null +++ b/GameServer/Server/CallGS/Handlers/Weapon/Weapon_Evolution.cs @@ -0,0 +1,77 @@ +using MikuSB.Database; +using MikuSB.Proto; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace MikuSB.GameServer.Server.CallGS.Handlers.Weapon; + +// s2c: function(sErr) — send "null" on success +// Id = target weapon UniqueId +// nItemId = material item UniqueId (weapon or supply item to consume) +[CallGSApi("Weapon_Evolution")] +public class Weapon_Evolution : ICallGSHandler +{ + public async Task Handle(Connection connection, string param, ushort seqNo) + { + var player = connection.Player!; + var req = JsonSerializer.Deserialize(param); + if (req == null || req.WeaponId == 0 || req.MaterialId == 0) + { + await CallGSRouter.SendScript(connection, "Weapon_Evolution", "\"error.BadParam\""); + return; + } + + var weapon = player.InventoryManager.InventoryData.Weapons.GetValueOrDefault((uint)req.WeaponId); + if (weapon == null) + { + await CallGSRouter.SendScript(connection, "Weapon_Evolution", "\"error.BadParam\""); + return; + } + + var syncItems = new List(); + + // Material can be a weapon or a regular item + if (player.InventoryManager.InventoryData.Weapons.TryGetValue((uint)req.MaterialId, out var matWeapon)) + { + player.InventoryManager.InventoryData.Weapons.Remove((uint)req.MaterialId); + var removed = matWeapon.ToProto(); + removed.Count = 0; + syncItems.Add(removed); + } + else if (player.InventoryManager.InventoryData.Items.TryGetValue((uint)req.MaterialId, out var matItem)) + { + matItem.ItemCount--; + var proto = matItem.ToProto(); + if (matItem.ItemCount == 0) + { + player.InventoryManager.InventoryData.Items.Remove(matItem.UniqueId); + proto.Count = 0; + } + syncItems.Add(proto); + } + else + { + await CallGSRouter.SendScript(connection, "Weapon_Evolution", "\"tip.not_material\""); + return; + } + + weapon.Evolue++; + syncItems.Add(weapon.ToProto()); + + DatabaseHelper.SaveDatabaseType(player.InventoryManager.InventoryData); + + var sync = new NtfSyncPlayer(); + sync.Items.AddRange(syncItems); + + await CallGSRouter.SendScript(connection, "Weapon_Evolution", "null", sync); + } +} + +internal sealed class WeaponEvolutionParam +{ + [JsonPropertyName("Id")] + public int WeaponId { get; set; } + + [JsonPropertyName("nItemId")] + public int MaterialId { get; set; } +} diff --git a/GameServer/Server/Packet/Recv/Login/HandlerReqLogin.cs b/GameServer/Server/Packet/Recv/Login/HandlerReqLogin.cs index 8efef28..a46bf09 100644 --- a/GameServer/Server/Packet/Recv/Login/HandlerReqLogin.cs +++ b/GameServer/Server/Packet/Recv/Login/HandlerReqLogin.cs @@ -4,12 +4,14 @@ using MikuSB.Database.Account; using MikuSB.Database.Player; using MikuSB.GameServer.Game.Player; using MikuSB.GameServer.Server.CallGS; +using MikuSB.GameServer.Server.CallGS.Handlers.Girl; using MikuSB.GameServer.Server.Packet.Send.Friend; using MikuSB.GameServer.Server.Packet.Send.Login; using MikuSB.GameServer.Server.Packet.Send.Misc; using MikuSB.Proto; using MikuSB.TcpSharp; using MikuSB.Util; +using System.Text.Json.Nodes; namespace MikuSB.GameServer.Server.Packet.Recv.Login; @@ -51,9 +53,80 @@ public class HandlerReqLogin : Handler await connection.Player.OnEnterGame(); connection.Player.Connection = connection; await connection.SendPacket(new PacketRspLogin(connection.Player!)); + await SendDebugLoginState(connection); await connection.Player.OnHeartBeat(); await connection.SendPacket(new PacketNtfUpdateFriend(connection.Player!)); + ApplySavedGirlSkinTypes(connection.Player!); await connection.SendPacket(new PacketNtfCallScript(connection.Player!.InventoryManager.InventoryData)); + await SendGirlSkinTypeOnLogin(connection); + } + + private static void ApplySavedGirlSkinTypes(PlayerInstance player) + { + var inventoryData = player.InventoryManager.InventoryData; + inventoryData.SkinTypesBySkinId ??= []; + var changed = false; + + foreach (var (skinId, skinType) in inventoryData.SkinTypesBySkinId.ToArray()) + { + var clamped = GirlSkin_ChangeSkinType.ClampClientSkinType(skinType); + if (clamped != skinType) + { + inventoryData.SkinTypesBySkinId[skinId] = clamped; + changed = true; + } + + var skinData = GirlSkin_ChangeSkinType.GetOrCreateSkinItem(player, skinId); + if (skinData != null && skinData.SkinType != clamped) + { + skinData.SkinType = clamped; + changed = true; + } + } + + if (changed) + DatabaseHelper.SaveDatabaseType(inventoryData); + } + + private static async Task SendGirlSkinTypeOnLogin(Connection connection) + { + var player = connection.Player; + if (player == null) + return; + + var inventoryData = player.InventoryManager.InventoryData; + inventoryData.SkinTypesBySkinId ??= []; + foreach (var (skinId, skinType) in inventoryData.SkinTypesBySkinId) + { + var clamped = GirlSkin_ChangeSkinType.ClampClientSkinType(skinType); + var skinData = GirlSkin_ChangeSkinType.GetOrCreateSkinItem(player, skinId); + var response = new JsonObject + { + ["nType"] = clamped, + ["nSkinId"] = skinId + }; + + if (skinData == null) + { + await CallGSRouter.SendScript(connection, "GirlSkin_ChangeSkinType", response.ToJsonString()); + continue; + } + + await CallGSRouter.SendScript(connection, "GirlSkin_ChangeSkinType", response.ToJsonString(), new NtfSyncPlayer + { + Items = { skinData.ToProto() } + }); + } + } + + private static async Task SendDebugLoginState(Connection connection) + { + var response = new JsonObject + { + ["IsDebug"] = ConfigManager.Config.ServerOption.EnableGmMenu + }; + + await CallGSRouter.SendScript(connection, "gm.notifylogin", response.ToJsonString()); } } diff --git a/GameServer/Server/Packet/Send/Misc/PacketNtfCallScript.cs b/GameServer/Server/Packet/Send/Misc/PacketNtfCallScript.cs index 3bc08f4..0fde531 100644 --- a/GameServer/Server/Packet/Send/Misc/PacketNtfCallScript.cs +++ b/GameServer/Server/Packet/Send/Misc/PacketNtfCallScript.cs @@ -1,6 +1,6 @@ using MikuSB.Database.Character; using MikuSB.Database.Inventory; -using MikuSB.GameServer.Game.Inventory; +using MikuSB.GameServer.Game.Player; using MikuSB.Proto; using MikuSB.TcpSharp; @@ -53,6 +53,21 @@ public class PacketNtfCallScript : BasePacket SetData(proto); } + public PacketNtfCallScript(List cards) : base(CmdIds.NtfScript) + { + var proto = new NtfCallScript + { + Api = "", + Arg = "{}", + ExtraSync = new NtfSyncPlayer + { + Items = { cards.Select(x => x.ToProto()) } + } + }; + + SetData(proto); + } + public PacketNtfCallScript(InventoryData inventory) : base(CmdIds.NtfScript) { var proto = new NtfCallScript @@ -65,7 +80,37 @@ 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()); proto.ExtraSync = extraSync; SetData(proto); } + + public PacketNtfCallScript(PlayerInstance Player) : base(CmdIds.NtfScript) + { + Player.BuildPlayerAttr(); + var proto = new NtfCallScript + { + Api = "", + Arg = "{}" + }; + var sync = new NtfSyncPlayer(); + foreach (var x in Player.Data.Attrs) + { + uint gid = x.Gid; + uint sid = x.Sid; + uint val = x.Val; + + if (gid == 0) + { + sync.Custom[sid] = val; + continue; + } + + sync.Custom[Player.ToPackedAttrKey(gid, sid)] = val; + sync.Custom[Player.ToShiftedAttrKey(gid, sid)] = val; + } + proto.ExtraSync = sync; + + SetData(proto); + } } diff --git a/version.txt b/version.txt index 3b8e8d5..e79e7ee 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -v=1.1 \ No newline at end of file +v=1.5 \ No newline at end of file