From 2e4310edf180e26bb85f9de16dbde7dc5cf0a2c0 Mon Sep 17 00:00:00 2001 From: fxz2018 <49226236+fxz2018@users.noreply.github.com> Date: Wed, 10 Sep 2025 07:05:12 +0800 Subject: [PATCH] =?UTF-8?q?=20implementation=20for=20the=20Favorite=20Item?= =?UTF-8?q?=20=EF=BC=8CCharacter=20Counse=20and=20Harmony=20Cube=20systems?= =?UTF-8?q?=20(#51)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Implement Favorite Item and Harmony Cube systems This commit introduces the core implementation for the Favorite Item and Harmony Cube systems. Features: - Added data structures and loading for Favorite Items, Harmony Cubes, and Attractive Levels. - Implemented lobby handlers for all related actions: - Favorite Items: equip, increase exp, quests, rewards, etc. - Harmony Cubes: get, clear, increase exp, level up, management, etc. - Character Counsel: check, present, quick counsel. - Updated user data models to store related progression. - Switched JSON deserialization for db.json to Newtonsoft.Json to handle protobuf models correctly. * fix InfraCoreExp * fix UserFavoriteItems and present count --------- Co-authored-by: Mikhail Tyukin --- EpinelPS/Data/GameData.cs | 68 ++++- EpinelPS/Data/JsonStaticData.cs | 207 ++++++++++++++- EpinelPS/Database/JsonDb.cs | 15 +- EpinelPS/EpinelPS.csproj | 3 +- .../Character/Counsel/CheckCounsel.cs | 18 ++ .../Character/Counsel/DoCounsel.cs | 119 ++++++--- .../LobbyServer/Character/Counsel/Present.cs | 114 +++++++++ .../Character/Counsel/QuickCounsel.cs | 99 +++++++ .../Character/GetCharacterAttractiveList.cs | 8 +- .../ClearFavoriteItemQuestStage.cs | 53 ++++ .../EnterFavoriteItemQuestStage.cs | 23 ++ .../FavoriteItem/EquipFavoriteItem.cs | 46 ++++ .../FavoriteItem/FinishFavoriteItemQuest.cs | 83 ++++++ .../FavoriteItem/GetFavoriteItemLibrary.cs | 9 + .../FavoriteItem/IncreaseExpFavoriteItem.cs | 214 ++++++++++++++++ .../FavoriteItem/ListFavoriteItem.cs | 6 + .../FavoriteItem/ListFavoriteItemQuests.cs | 17 +- .../ObtainFavoriteItemQuestReward.cs | 86 +++++++ .../FavoriteItem/StartFavoriteItemQuest.cs | 32 +++ .../FavoriteItem/UpgradeFavoriteItem.cs | 96 +++++++ .../LobbyServer/Inventory/ClearHarmonyCube.cs | 76 ++++++ .../LobbyServer/Inventory/GetHarmonyCube.cs | 49 ++++ .../Inventory/LevelUpHarmonyCube.cs | 101 ++++++++ .../Inventory/ManagementHarmonyCube.cs | 190 ++++++++++++++ .../LobbyServer/Inventory/WearHarmonyCube.cs | 241 ++++++++++++++++++ .../LobbyServer/LobbyUser/EnterLobbyServer.cs | 59 +++-- .../LobbyServer/Messenger/EnterMessenger.cs | 26 +- EpinelPS/LobbyServer/Messenger/GetMessages.cs | 69 +++++ EpinelPS/LobbyServer/Outpost/BuildBuilding.cs | 125 ++++++++- .../LobbyServer/Outpost/CheckInfracore.cs | 18 +- .../LobbyServer/Outpost/GetOutpostData.cs | 51 +++- .../Outpost/ObtainInfracoreReward.cs | 38 +++ EpinelPS/LobbyServer/Stage/ClearStage.cs | 5 +- EpinelPS/LobbyServer/Stage/FastClear.cs | 8 +- EpinelPS/Models/CoreInfoModel.cs | 2 +- EpinelPS/Models/DbModels.cs | 6 + EpinelPS/Models/UserModel.cs | 6 + EpinelPS/Utils/GameConfig.cs | 8 +- EpinelPS/Utils/NetUtils.cs | 13 + EpinelPS/Utils/RewardUtils.cs | 60 ++++- 40 files changed, 2356 insertions(+), 111 deletions(-) create mode 100644 EpinelPS/LobbyServer/Character/Counsel/CheckCounsel.cs create mode 100644 EpinelPS/LobbyServer/Character/Counsel/Present.cs create mode 100644 EpinelPS/LobbyServer/Character/Counsel/QuickCounsel.cs create mode 100644 EpinelPS/LobbyServer/FavoriteItem/ClearFavoriteItemQuestStage.cs create mode 100644 EpinelPS/LobbyServer/FavoriteItem/EnterFavoriteItemQuestStage.cs create mode 100644 EpinelPS/LobbyServer/FavoriteItem/EquipFavoriteItem.cs create mode 100644 EpinelPS/LobbyServer/FavoriteItem/FinishFavoriteItemQuest.cs create mode 100644 EpinelPS/LobbyServer/FavoriteItem/IncreaseExpFavoriteItem.cs create mode 100644 EpinelPS/LobbyServer/FavoriteItem/ObtainFavoriteItemQuestReward.cs create mode 100644 EpinelPS/LobbyServer/FavoriteItem/StartFavoriteItemQuest.cs create mode 100644 EpinelPS/LobbyServer/FavoriteItem/UpgradeFavoriteItem.cs create mode 100644 EpinelPS/LobbyServer/Inventory/ClearHarmonyCube.cs create mode 100644 EpinelPS/LobbyServer/Inventory/GetHarmonyCube.cs create mode 100644 EpinelPS/LobbyServer/Inventory/LevelUpHarmonyCube.cs create mode 100644 EpinelPS/LobbyServer/Inventory/ManagementHarmonyCube.cs create mode 100644 EpinelPS/LobbyServer/Inventory/WearHarmonyCube.cs create mode 100644 EpinelPS/LobbyServer/Outpost/ObtainInfracoreReward.cs diff --git a/EpinelPS/Data/GameData.cs b/EpinelPS/Data/GameData.cs index 3a8e7b3..66935c1 100644 --- a/EpinelPS/Data/GameData.cs +++ b/EpinelPS/Data/GameData.cs @@ -1,11 +1,11 @@ -using System.Data; +using System.Data; using System.Diagnostics; using System.Security.Cryptography; -using System.Text.Json; using EpinelPS.Database; using EpinelPS.Utils; using ICSharpCode.SharpZipLib.Zip; using MemoryPack; +using Newtonsoft.Json; namespace EpinelPS.Data { @@ -154,6 +154,9 @@ namespace EpinelPS.Data [LoadRecord("AttractiveLevelRewardTable.json", "id")] public readonly Dictionary AttractiveLevelReward = []; + [LoadRecord("AttractiveLevelTable.json", "id")] + public readonly Dictionary AttractiveLevelTable = []; + [LoadRecord("SubQuestTable.json", "id")] public readonly Dictionary Subquests = []; @@ -200,6 +203,32 @@ namespace EpinelPS.Data [LoadRecord("RecycleResearchLevelTable.json", "id")] public readonly Dictionary RecycleResearchLevels = []; + // Harmony Cube Data Tables + [LoadRecord("ItemHarmonyCubeTable.json", "id")] + public readonly Dictionary ItemHarmonyCubeTable = []; + + [LoadRecord("ItemHarmonyCubeLevelTable.json", "id")] + public readonly Dictionary ItemHarmonyCubeLevelTable = []; + + // Favorite Item Data Tables + [LoadRecord("FavoriteItemTable.json", "id")] + public readonly Dictionary FavoriteItemTable = []; + + [LoadRecord("FavoriteItemExpTable.json", "id")] + public readonly Dictionary FavoriteItemExpTable = []; + + [LoadRecord("FavoriteItemLevelTable.json", "id")] + public readonly Dictionary FavoriteItemLevelTable = []; + + [LoadRecord("FavoriteItemProbabilityTable.json", "id")] + public readonly Dictionary FavoriteItemProbabilityTable = []; + + [LoadRecord("FavoriteItemQuestTable.json", "id")] + public readonly Dictionary FavoriteItemQuestTable = []; + + [LoadRecord("FavoriteItemQuestStageTable.json", "id")] + public readonly Dictionary FavoriteItemQuestStageTable = []; + static async Task BuildAsync() { @@ -244,7 +273,7 @@ namespace EpinelPS.Data #region Data loading private static byte[] PresharedValue = [0xCB, 0xC2, 0x1C, 0x6F, 0xF3, 0xF5, 0x07, 0xF5, 0x05, 0xBA, 0xCA, 0xD4, 0x98, 0x28, 0x84, 0x1F, 0xF0, 0xD1, 0x38, 0xC7, 0x61, 0xDF, 0xD6, 0xE6, 0x64, 0x9A, 0x85, 0x13, 0x3E, 0x1A, 0x6A, 0x0C, 0x68, 0x0E, 0x2B, 0xC4, 0xDF, 0x72, 0xF8, 0xC6, 0x55, 0xE4, 0x7B, 0x14, 0x36, 0x18, 0x3B, 0xA7, 0xD1, 0x20, 0x81, 0x22, 0xD1, 0xA9, 0x18, 0x84, 0x65, 0x13, 0x0B, 0xED, 0xA3, 0x00, 0xE5, 0xD9]; - private static RSAParameters LoadParameters = new() + private static RSAParameters LoadParameters = new() { Exponent = [0x01, 0x00, 0x01], Modulus = [0x89, 0xD6, 0x66, 0x00, 0x7D, 0xFC, 0x7D, 0xCE, 0x83, 0xA6, 0x62, 0xE3, 0x1A, 0x5E, 0x9A, 0x53, 0xC7, 0x8A, 0x27, 0xF3, 0x67, 0xC1, 0xF3, 0xD4, 0x37, 0xFE, 0x50, 0x6D, 0x38, 0x45, 0xDF, 0x7E, 0x73, 0x5C, 0xF4, 0x9D, 0x40, 0x4C, 0x8C, 0x63, 0x21, 0x97, 0xDF, 0x46, 0xFF, 0xB2, 0x0D, 0x0E, 0xDB, 0xB2, 0x72, 0xB4, 0xA8, 0x42, 0xCD, 0xEE, 0x48, 0x06, 0x74, 0x4F, 0xE9, 0x56, 0x6E, 0x9A, 0xB1, 0x60, 0x18, 0xBC, 0x86, 0x0B, 0xB6, 0x32, 0xA7, 0x51, 0x00, 0x85, 0x7B, 0xC8, 0x72, 0xCE, 0x53, 0x71, 0x3F, 0x64, 0xC2, 0x25, 0x58, 0xEF, 0xB0, 0xC9, 0x1D, 0xE3, 0xB3, 0x8E, 0xFC, 0x55, 0xCF, 0x8B, 0x02, 0xA5, 0xC8, 0x1E, 0xA7, 0x0E, 0x26, 0x59, 0xA8, 0x33, 0xA5, 0xF1, 0x11, 0xDB, 0xCB, 0xD3, 0xA7, 0x1F, 0xB1, 0xC6, 0x10, 0x39, 0xC8, 0x31, 0x1D, 0x60, 0xDB, 0x0D, 0xA4, 0x13, 0x4B, 0x2B, 0x0E, 0xF3, 0x6F, 0x69, 0xCB, 0xA8, 0x62, 0x03, 0x69, 0xE6, 0x95, 0x6B, 0x8D, 0x11, 0xF6, 0xAF, 0xD9, 0xC2, 0x27, 0x3A, 0x32, 0x12, 0x05, 0xC3, 0xB1, 0xE2, 0x81, 0x4B, 0x40, 0xF8, 0x8B, 0x8D, 0xBA, 0x1F, 0x55, 0x60, 0x2C, 0x09, 0xC6, 0xED, 0x73, 0x96, 0x32, 0xAF, 0x5F, 0xEE, 0x8F, 0xEB, 0x5B, 0x93, 0xCF, 0x73, 0x13, 0x15, 0x6B, 0x92, 0x7B, 0x27, 0x0A, 0x13, 0xF0, 0x03, 0x4D, 0x6F, 0x5E, 0x40, 0x7B, 0x9B, 0xD5, 0xCE, 0xFC, 0x04, 0x97, 0x7E, 0xAA, 0xA3, 0x53, 0x2A, 0xCF, 0xD2, 0xD5, 0xCF, 0x52, 0xB2, 0x40, 0x61, 0x28, 0xB1, 0xA6, 0xF6, 0x78, 0xFB, 0x69, 0x9A, 0x85, 0xD6, 0xB9, 0x13, 0x14, 0x6D, 0xC4, 0x25, 0x36, 0x17, 0xDB, 0x54, 0x0C, 0xD8, 0x77, 0x80, 0x9A, 0x00, 0x62, 0x83, 0xDD, 0xB0, 0x06, 0x64, 0xD0, 0x81, 0x5B, 0x0D, 0x23, 0x9E, 0x88, 0xBD], @@ -392,7 +421,9 @@ namespace EpinelPS.Data } else { - DataTable obj = await JsonSerializer.DeserializeAsync>(MainZip.GetInputStream(fileEntry), JsonDb.IndentedJson) ?? throw new Exception("deserializeobject failed"); + using var streamReader = new System.IO.StreamReader(MainZip.GetInputStream(fileEntry)); + var json = await streamReader.ReadToEndAsync(); + DataTable obj = JsonConvert.DeserializeObject>(json) ?? throw new Exception("deserializeobject failed"); deserializedObject = [.. obj.records]; } @@ -560,7 +591,20 @@ namespace EpinelPS.Data public string? GetItemSubType(int itemType) { - return ItemEquipTable[itemType].item_sub_type; + // Check if it's an equipment item + if (ItemEquipTable.TryGetValue(itemType, out ItemEquipRecord? equipRecord)) + { + return equipRecord.item_sub_type; + } + + // Check if it's a harmony cube item + if (ItemHarmonyCubeTable.TryGetValue(itemType, out ItemHarmonyCubeRecord? harmonyCubeRecord)) + { + return harmonyCubeRecord.item_sub_type; + } + + // Return null if item type not found + return null; } internal IEnumerable GetStageIdsForChapter(int chapterNumber, bool normal) @@ -669,5 +713,17 @@ namespace EpinelPS.Data return results.FirstOrDefault().Value.reward_id; else return 0; } + + public FavoriteItemQuestRecord? GetFavoriteItemQuestTableData(int questId) + { + FavoriteItemQuestTable.TryGetValue(questId, out FavoriteItemQuestRecord?data); + return data; + } + + public FavoriteItemQuestStageRecord? GetFavoriteItemQuestStageData(int stageId) + { + FavoriteItemQuestStageTable.TryGetValue(stageId, out FavoriteItemQuestStageRecord? data); + return data; + } } -} +} \ No newline at end of file diff --git a/EpinelPS/Data/JsonStaticData.cs b/EpinelPS/Data/JsonStaticData.cs index c380cab..b0fa471 100644 --- a/EpinelPS/Data/JsonStaticData.cs +++ b/EpinelPS/Data/JsonStaticData.cs @@ -1,4 +1,4 @@ -using System.Data; +using System.Data; using MemoryPack; namespace EpinelPS.Data @@ -685,7 +685,7 @@ namespace EpinelPS.Data { public int id; public int grade; - public int reard_id; + public int reward_id; public int infra_core_exp; public List function_list = []; } @@ -706,6 +706,32 @@ namespace EpinelPS.Data public int attractive_level; public int costume; } + + [MemoryPackable] + public partial class AttractiveLevelRecord + { + public int id; + public int attractive_level; + public int attractive_point; + public int attacker_hp_rate; + public int attacker_attack_rate; + public int attacker_defence_rate; + public int attacker_energy_resist_rate; + public int attacker_metal_resist_rate; + public int attacker_bio_resist_rate; + public int defender_hp_rate; + public int defender_attack_rate; + public int defender_defence_rate; + public int defender_energy_resist_rate; + public int defender_metal_resist_rate; + public int defender_bio_resist_rate; + public int supporter_hp_rate; + public int supporter_attack_rate; + public int supporter_defence_rate; + public int supporter_energy_resist_rate; + public int supporter_metal_resist_rate; + public int supporter_bio_resist_rate; + } [MemoryPackable] public partial class SubquestRecord { @@ -731,9 +757,23 @@ namespace EpinelPS.Data public partial class MessengerMsgConditionRecord { public int id; + public List trigger_list = []; + public string message_type = ""; public string tid = ""; + public int resource_id; + public int stamina_value; public int reward_id; } + + [MemoryPackable] + public partial class MessengerConditionTriggerList + { + public string trigger = ""; + public int condition_id; + public int condition_value; + } + + public enum ScenarioRewardCondition { MainScenario, @@ -888,5 +928,166 @@ namespace EpinelPS.Data public List ItemSpawner { get; set; } = []; public List StageSpawner { get; set; } = []; } - + + // Harmony Cube Data Structures + [MemoryPackable] + public partial class ItemHarmonyCubeRecord + { + public int id; + public string name_localkey = ""; + public string description_localkey = ""; + public int location_id; + public string location_localkey = ""; + public int order; + public int resource_id; + public string bg = ""; + public string bg_color = ""; + public string item_type = ""; + public string item_sub_type = ""; + public string item_rare = ""; + public string @class = ""; + public int level_enhance_id; + public List harmonycube_skill_group = []; + } + + [MemoryPackable] + public partial class ItemHarmonyCubeLevelRecord + { + public int id; + public int level_enhance_id; + public int level; + public List skill_levels = []; + public int material_id; + public int material_value; + public int gold_value; + public int slot; + public List harmonycube_stats = []; + } + + public class HarmonyCubeSkillGroup + { + public int skill_group_id; + } + + public class HarmonyCubeSkillLevel + { + public int skill_level; + } + + public class HarmonyCubeStat + { + public string stat_type = ""; + public int stat_rate; + } + + [MemoryPackable] + public partial class FavoriteItemRecord + { + public int id; + public string name_localkey = ""; + public string description_localkey = ""; + public string icon_resource_id = ""; + public string img_resource_id = ""; + public string prop_resource_id = ""; + public int order; + public string favorite_rare = ""; + public string favorite_type = ""; + public string weapon_type = ""; + public int name_code; + public int max_level; + public int level_enhance_id; + public int probability_group; + public int albumcategory_id; + } + + [MemoryPackable] + public partial class FavoriteItemExpRecord + { + public int id; + public string favorite_rare = ""; + public int level; + public int need_exp; + } + + [MemoryPackable] + public partial class FavoriteItemLevelRecord + { + public int id; + public int level_enhance_id; + public int grade; + public int level; + public List favoriteitem_stat_data = []; + public List collection_skill_level_data = []; + } + + [MemoryPackable] + public partial class FavoriteItemProbabilityRecord + { + public int id; + public int probability_group; + public int level_min; + public int level_max; + public int need_item_id; + public int need_item_count; + public int exp; + public int great_success_rate; + public int great_success_level; + } + + [MemoryPackable] + public partial class FavoriteItemQuestRecord + { + public int id; + public int name_code; + public string condition_type = ""; + public int condition_value; + public string quest_thumbnail_resource_id = ""; + public string name_localkey = ""; + public string description_localkey = ""; + public int next_quest_id; + public string end_scenario_id = ""; + public int reward_id; + } + + public class FavoriteItemStatData + { + public string stat_type = ""; + public int stat_value; + } + + public class CollectionSkillLevelData + { + public int collection_skill_level; + } + + [MemoryPackable] + public partial class FavoriteItemQuestStageRecord + { + public int id; + public int group_id; + public int chapter_id; + public string chapter_mod = ""; + public int name_code; + public int spawn_condition_favoriteitem_quest_id; + public int spawn_condition_campaign_stage_id; + public int enter_condition_favoriteitem_quest_id; + public int enter_condition_campaign_stage_id; + public string name_localkey = ""; + public string stage_category = ""; + public bool spot_autocontrol; + public int monster_stage_lv; + public int dynamic_object_stage_lv; + public int standard_battle_power; + public int stage_stat_increase_group_id; + public bool is_use_quick_battle; + public int field_monster_id; + public int spot_id; + public int state_effect_function_id; + public int reward_id; + public string enter_scenario_type = ""; + public string enter_scenario = ""; + public int fixed_play_character_id; + public int spawn_condition_favoriteitem_quest_stage_id; + public int enter_condition_favoriteitem_quest_stage_id; + } } diff --git a/EpinelPS/Database/JsonDb.cs b/EpinelPS/Database/JsonDb.cs index dfe02af..c0dc378 100644 --- a/EpinelPS/Database/JsonDb.cs +++ b/EpinelPS/Database/JsonDb.cs @@ -1,9 +1,8 @@ -using System.Globalization; -using System.Text.Json; -using System.Text.Json.Serialization; +using System.Globalization; using EpinelPS.Data; using EpinelPS.Utils; using Google.Protobuf; +using Newtonsoft.Json; using Paseto; using Paseto.Builder; @@ -12,7 +11,6 @@ namespace EpinelPS.Database internal class JsonDb { public static CoreInfo Instance { get; internal set; } - public static readonly JsonSerializerOptions IndentedJson = new() { WriteIndented = true, IncludeFields = true }; // Note: change this in sodium public static byte[] ServerPrivateKey = Convert.FromBase64String("FSUY8Ohd942n5LWAfxn6slK3YGwc8OqmyJoJup9nNos="); @@ -20,7 +18,6 @@ namespace EpinelPS.Database static JsonDb() { - IndentedJson.Converters.Add(new JsonStringEnumConverter()); if (!File.Exists(AppDomain.CurrentDomain.BaseDirectory + "/db.json")) { Console.WriteLine("users: warning: configuration not found, writing default data"); @@ -28,7 +25,7 @@ namespace EpinelPS.Database Save(); } - var j = JsonSerializer.Deserialize(File.ReadAllText(AppDomain.CurrentDomain.BaseDirectory + "/db.json"), IndentedJson); + var j = JsonConvert.DeserializeObject(File.ReadAllText(AppDomain.CurrentDomain.BaseDirectory + "/db.json")); if (j != null) { Instance = j; @@ -80,7 +77,7 @@ namespace EpinelPS.Database Save(); } - var j = JsonSerializer.Deserialize(File.ReadAllText(AppDomain.CurrentDomain.BaseDirectory + "/db.json")); + var j = JsonConvert.DeserializeObject(File.ReadAllText(AppDomain.CurrentDomain.BaseDirectory + "/db.json")); if (j != null) { Instance = j; @@ -112,7 +109,7 @@ namespace EpinelPS.Database { if (Instance != null) { - File.WriteAllText(AppDomain.CurrentDomain.BaseDirectory + "/db.json", JsonSerializer.Serialize(Instance, IndentedJson)); + File.WriteAllText(AppDomain.CurrentDomain.BaseDirectory + "/db.json", JsonConvert.SerializeObject(Instance, Formatting.Indented)); } } public static int CurrentJukeboxBgm(int position) @@ -154,4 +151,4 @@ namespace EpinelPS.Database } } } -} +} \ No newline at end of file diff --git a/EpinelPS/EpinelPS.csproj b/EpinelPS/EpinelPS.csproj index 67a3739..60ae280 100644 --- a/EpinelPS/EpinelPS.csproj +++ b/EpinelPS/EpinelPS.csproj @@ -36,7 +36,8 @@ - + + diff --git a/EpinelPS/LobbyServer/Character/Counsel/CheckCounsel.cs b/EpinelPS/LobbyServer/Character/Counsel/CheckCounsel.cs new file mode 100644 index 0000000..0b05c1a --- /dev/null +++ b/EpinelPS/LobbyServer/Character/Counsel/CheckCounsel.cs @@ -0,0 +1,18 @@ +using EpinelPS.Utils; + +namespace EpinelPS.LobbyServer.Character.Counsel; + +[PacketPath("/character/counsel/check")] +public class CheckCounsel : LobbyMsgHandler +{ + protected override async Task HandleAsync() + { + ReqCounseledBefore req = await ReadData(); + + ResCounseledBefore response = new(); + + response.IsCounseledBefore = false; + + await WriteDataAsync(response); + } +} \ No newline at end of file diff --git a/EpinelPS/LobbyServer/Character/Counsel/DoCounsel.cs b/EpinelPS/LobbyServer/Character/Counsel/DoCounsel.cs index 6a6f2d7..bdbcb9a 100644 --- a/EpinelPS/LobbyServer/Character/Counsel/DoCounsel.cs +++ b/EpinelPS/LobbyServer/Character/Counsel/DoCounsel.cs @@ -2,49 +2,98 @@ using EpinelPS.Data; using EpinelPS.Database; using EpinelPS.Utils; -namespace EpinelPS.LobbyServer.Character.Counsel; - -[PacketPath("/character/attractive/counsel")] -public class DoCounsel : LobbyMsgHandler +namespace EpinelPS.LobbyServer.Character.Counsel { - protected override async Task HandleAsync() + [PacketPath("/character/attractive/counsel")] + public class DoCounsel : LobbyMsgHandler { - ReqCharacterCounsel req = await ReadData(); - User user = GetUser(); - - ResCharacterCounsel response = new(); - - foreach (KeyValuePair currency in user.Currency) + protected override async Task HandleAsync() { - response.Currencies.Add(new NetUserCurrencyData() { Type = (int)currency.Key, Value = currency.Value }); - } + ReqCharacterCounsel req = await ReadData(); + User user = GetUser(); - IEnumerable currentBondInfo = user.BondInfo.Where(x => x.NameCode == req.NameCode); + ResCharacterCounsel response = new(); - NetUserAttractiveData data; - - if (currentBondInfo.Any()) - { - data = currentBondInfo.First(); - - // TODO update - response.Attractive = data; - } - else - { - data = new() + foreach (KeyValuePair currency in user.Currency) { - NameCode = req.NameCode, - // TODO - }; + response.Currencies.Add(new NetUserCurrencyData() { Type = (int)currency.Key, Value = currency.Value }); + } - response.Attractive = data; - user.BondInfo.Add(data); + NetUserAttractiveData? currentBondInfo = user.BondInfo.FirstOrDefault(x => x.NameCode == req.NameCode); + + if (currentBondInfo != null) + { + int beforeLv = currentBondInfo.Lv; + int beforeExp = currentBondInfo.Exp; + + currentBondInfo.Exp += 100; + currentBondInfo.CounseledCount++; + currentBondInfo.CanCounselToday = true; // Always allow counseling + UpdateAttractiveLevel(currentBondInfo); + + response.Attractive = currentBondInfo; + response.Exp = new NetIncreaseExpData + { + NameCode = currentBondInfo.NameCode, + BeforeLv = beforeLv, + BeforeExp = beforeExp, + CurrentLv = currentBondInfo.Lv, + CurrentExp = currentBondInfo.Exp, + GainExp = 100 + }; + } + else + { + NetUserAttractiveData data = new NetUserAttractiveData() + { + NameCode = req.NameCode, + Exp = 100, + CounseledCount = 1, + IsFavorites = false, + CanCounselToday = true, + Lv = 1 + }; + UpdateAttractiveLevel(data); + user.BondInfo.Add(data); + response.Attractive = data; + response.Exp = new NetIncreaseExpData + { + NameCode = data.NameCode, + BeforeLv = 1, + BeforeExp = 0, + CurrentLv = 1, + CurrentExp = 100, + GainExp = 100 + }; + } + + JsonDb.Save(); + + await WriteDataAsync(response); } - JsonDb.Save(); + private void UpdateAttractiveLevel(NetUserAttractiveData attractiveData) + { + while (attractiveData.Lv < 40) + { + AttractiveLevelRecord? levelInfo = GameData.Instance.AttractiveLevelTable.FirstOrDefault(x => x.Value.attractive_level == attractiveData.Lv).Value; - // TODO: Validate response from real server and pull info from user info - await WriteDataAsync(response); + if (levelInfo == null) + { + // No more level data + break; + } + + if (attractiveData.Exp >= levelInfo.attractive_point) + { + attractiveData.Exp -= levelInfo.attractive_point; + attractiveData.Lv++; + } + else + { + break; + } + } + } } -} +} \ No newline at end of file diff --git a/EpinelPS/LobbyServer/Character/Counsel/Present.cs b/EpinelPS/LobbyServer/Character/Counsel/Present.cs new file mode 100644 index 0000000..e63405c --- /dev/null +++ b/EpinelPS/LobbyServer/Character/Counsel/Present.cs @@ -0,0 +1,114 @@ +using EpinelPS.Data; +using EpinelPS.Database; +using EpinelPS.Utils; + +namespace EpinelPS.LobbyServer.Character.Counsel +{ + [PacketPath("/character/attractive/present")] + public class Present : LobbyMsgHandler + { + protected override async Task HandleAsync() + { + ReqCharacterPresent req = await ReadData(); + User user = GetUser(); + + ResCharacterPresent response = new ResCharacterPresent(); + + NetUserAttractiveData? bondInfo = user.BondInfo.FirstOrDefault(x => x.NameCode == req.NameCode); + if (bondInfo == null) + { + return; + } + + int totalExpGained = 0; + CharacterRecord? characterRecord = GameData.Instance.CharacterTable.Values.FirstOrDefault(x => x.name_code == req.NameCode); + + foreach (NetItemData item in req.Items) + { + ItemMaterialRecord? materialInfo = GameData.Instance.itemMaterialTable.GetValueOrDefault(item.Tid); + if (materialInfo != null && materialInfo.item_sub_type == "AttractiveMaterial") + { + int expGained = materialInfo.item_value * (int)item.Count; + + if (characterRecord != null) + { + if (materialInfo.material_type == "Corporation") + { + string corporation = materialInfo.name_localkey.Split('_')[2]; + if (corporation.Equals(characterRecord.corporation, StringComparison.OrdinalIgnoreCase)) + { + expGained *= 5; + } + } + else if (materialInfo.material_type == "Squad") + { + string squad = materialInfo.name_localkey.Split('_')[2]; + if (squad.Equals(characterRecord.squad, StringComparison.OrdinalIgnoreCase)) + { + expGained *= 3; + } + } + } + + totalExpGained += expGained; + + ItemData? userItem = user.Items.FirstOrDefault(x => x.ItemType == item.Tid); + if (userItem != null) + { + userItem.Count -= (int)item.Count; + if (userItem.Count <= 0) + { + user.Items.Remove(userItem); + } + } + } + } + + int beforeLv = bondInfo.Lv; + int beforeExp = bondInfo.Exp; + + bondInfo.Exp += totalExpGained; + UpdateAttractiveLevel(bondInfo); + + response.Attractive = bondInfo; + response.Exp = new NetIncreaseExpData + { + NameCode = bondInfo.NameCode, + BeforeLv = beforeLv, + BeforeExp = beforeExp, + CurrentLv = bondInfo.Lv, + CurrentExp = bondInfo.Exp, + GainExp = totalExpGained + }; + + response.Items.AddRange(NetUtils.GetUserItems(user)); + + JsonDb.Save(); + + await WriteDataAsync(response); + } + + private void UpdateAttractiveLevel(NetUserAttractiveData attractiveData) + { + while (attractiveData.Lv < 40) + { + AttractiveLevelRecord? levelInfo = GameData.Instance.AttractiveLevelTable.Values.FirstOrDefault(x => x.attractive_level == attractiveData.Lv); + + if (levelInfo == null) + { + break; + } + + if (attractiveData.Exp >= levelInfo.attractive_point) + { + attractiveData.Exp -= levelInfo.attractive_point; + attractiveData.Lv++; + } + else + { + break; + } + } + } + } +} \ No newline at end of file diff --git a/EpinelPS/LobbyServer/Character/Counsel/QuickCounsel.cs b/EpinelPS/LobbyServer/Character/Counsel/QuickCounsel.cs new file mode 100644 index 0000000..6642226 --- /dev/null +++ b/EpinelPS/LobbyServer/Character/Counsel/QuickCounsel.cs @@ -0,0 +1,99 @@ +using EpinelPS.Data; +using EpinelPS.Database; +using EpinelPS.Utils; + +namespace EpinelPS.LobbyServer.Character.Counsel +{ + [PacketPath("/character/counsel/quick")] + public class QuickCounsel : LobbyMsgHandler + { + protected override async Task HandleAsync() + { + ReqCharacterQuickCounsel req = await ReadData(); + User user = GetUser(); + + ResCharacterQuickCounsel response = new ResCharacterQuickCounsel(); + + foreach (KeyValuePair currency in user.Currency) + { + response.Currencies.Add(new NetUserCurrencyData() { Type = (int)currency.Key, Value = currency.Value }); + } + + NetUserAttractiveData? bondInfo = user.BondInfo.FirstOrDefault(x => x.NameCode == req.NameCode); + + if (bondInfo != null) + { + int beforeLv = bondInfo.Lv; + int beforeExp = bondInfo.Exp; + + bondInfo.Exp += 100; + bondInfo.CounseledCount++; + bondInfo.CanCounselToday = true; // Always allow counseling + UpdateAttractiveLevel(bondInfo); + + response.Attractive = bondInfo; + response.Exp = new NetIncreaseExpData + { + NameCode = bondInfo.NameCode, + BeforeLv = beforeLv, + BeforeExp = beforeExp, + CurrentLv = bondInfo.Lv, + CurrentExp = bondInfo.Exp, + GainExp = 100 + }; + } + else + { + NetUserAttractiveData data = new NetUserAttractiveData() + { + NameCode = req.NameCode, + Exp = 100, + CounseledCount = 1, + IsFavorites = false, + CanCounselToday = true, + Lv = 1 + }; + UpdateAttractiveLevel(data); + user.BondInfo.Add(data); + response.Attractive = data; + response.Exp = new NetIncreaseExpData + { + NameCode = data.NameCode, + BeforeLv = 1, + BeforeExp = 0, + CurrentLv = 1, + CurrentExp = 100, + GainExp = 100 + }; + } + + JsonDb.Save(); + + await WriteDataAsync(response); + } + + private void UpdateAttractiveLevel(NetUserAttractiveData attractiveData) + { + while (attractiveData.Lv < 40) + { + AttractiveLevelRecord? levelInfo = GameData.Instance.AttractiveLevelTable.Values.FirstOrDefault(x => x.attractive_level == attractiveData.Lv); + + if (levelInfo == null) + { + // No more level data + break; + } + + if (attractiveData.Exp >= levelInfo.attractive_point) + { + attractiveData.Exp -= levelInfo.attractive_point; + attractiveData.Lv++; + } + else + { + break; + } + } + } + } +} \ No newline at end of file diff --git a/EpinelPS/LobbyServer/Character/GetCharacterAttractiveList.cs b/EpinelPS/LobbyServer/Character/GetCharacterAttractiveList.cs index 7789a99..dbecce8 100644 --- a/EpinelPS/LobbyServer/Character/GetCharacterAttractiveList.cs +++ b/EpinelPS/LobbyServer/Character/GetCharacterAttractiveList.cs @@ -1,4 +1,4 @@ -using EpinelPS.Utils; +using EpinelPS.Utils; namespace EpinelPS.LobbyServer.Character { @@ -19,13 +19,11 @@ namespace EpinelPS.LobbyServer.Character { response.Attractives.Add(item); item.CanCounselToday = true; - item.Exp = 9999; // TODO - item.Lv = 10; + } - // TODO: Validate response from real server and pull info from user info await WriteDataAsync(response); } } -} +} \ No newline at end of file diff --git a/EpinelPS/LobbyServer/FavoriteItem/ClearFavoriteItemQuestStage.cs b/EpinelPS/LobbyServer/FavoriteItem/ClearFavoriteItemQuestStage.cs new file mode 100644 index 0000000..bbff5ad --- /dev/null +++ b/EpinelPS/LobbyServer/FavoriteItem/ClearFavoriteItemQuestStage.cs @@ -0,0 +1,53 @@ +using EpinelPS.Database; +using EpinelPS.Utils; +using EpinelPS.Data; +using System.Linq; + +namespace EpinelPS.LobbyServer.FavoriteItem +{ + [PacketPath("/favoriteitem/quest/stage/clear")] + public class ClearFavoriteItemQuestStage : LobbyMsgHandler + { + protected override async Task HandleAsync() + { + ReqClearFavoriteItemQuestStage req = await ReadData(); + User user = GetUser(); + ResClearFavoriteItemQuestStage response = new(); + + FavoriteItemQuestStageRecord? stageData = GameData.Instance.GetFavoriteItemQuestStageData(req.StageId); + if (stageData == null) + { + await WriteDataAsync(response); + return; + } + + FavoriteItemQuestRecord? questData = GameData.Instance.GetFavoriteItemQuestTableData(req.FavoriteItemQuestId); + if (questData != null) + { + NetUserFavoriteItemQuestData? existingQuest = user.FavoriteItemQuests.FirstOrDefault(q => q.QuestId == req.FavoriteItemQuestId); + if (existingQuest != null) existingQuest.Clear = true; + else user.FavoriteItemQuests.Add(new NetUserFavoriteItemQuestData { QuestId = req.FavoriteItemQuestId, Clear = true }); + + if (questData.next_quest_id > 0 && user.FavoriteItemQuests.All(q => q.QuestId != questData.next_quest_id)) + { + user.FavoriteItemQuests.Add(new NetUserFavoriteItemQuestData { QuestId = questData.next_quest_id, Clear = false, Received = false }); + } + } + + string stageMapId = GameData.Instance.GetMapIdFromChapter(stageData.chapter_id, stageData.chapter_mod); + if (!user.FieldInfoNew.ContainsKey(stageMapId)) + { + user.FieldInfoNew.Add(stageMapId, new FieldInfoNew()); + } + if (!user.FieldInfoNew[stageMapId].CompletedStages.Contains(req.StageId)) + { + user.FieldInfoNew[stageMapId].CompletedStages.Add(req.StageId); + } + + JsonDb.Save(); + + await WriteDataAsync(response); + } + } +} + diff --git a/EpinelPS/LobbyServer/FavoriteItem/EnterFavoriteItemQuestStage.cs b/EpinelPS/LobbyServer/FavoriteItem/EnterFavoriteItemQuestStage.cs new file mode 100644 index 0000000..3bafd02 --- /dev/null +++ b/EpinelPS/LobbyServer/FavoriteItem/EnterFavoriteItemQuestStage.cs @@ -0,0 +1,23 @@ +using EpinelPS.Database; +using EpinelPS.Utils; +using EpinelPS.Data; + +namespace EpinelPS.LobbyServer.FavoriteItem +{ + [PacketPath("/favoriteitem/quest/stage/enter")] + public class EnterFavoriteItemQuestStage : LobbyMsgHandler + { + protected override async Task HandleAsync() + { + ReqEnterFavoriteItemQuestStage req = await ReadData(); + User user = GetUser(); + + user.AddTrigger(TriggerType.CampaignStart, 1, req.StageId); + + JsonDb.Save(); + + ResEnterFavoriteItemQuestStage response = new(); + await WriteDataAsync(response); + } + } +} diff --git a/EpinelPS/LobbyServer/FavoriteItem/EquipFavoriteItem.cs b/EpinelPS/LobbyServer/FavoriteItem/EquipFavoriteItem.cs new file mode 100644 index 0000000..353dc94 --- /dev/null +++ b/EpinelPS/LobbyServer/FavoriteItem/EquipFavoriteItem.cs @@ -0,0 +1,46 @@ +using EpinelPS.Database; +using EpinelPS.Utils; + +namespace EpinelPS.LobbyServer.FavoriteItem +{ + [PacketPath("/favoriteitem/equip")] + public class EquipFavoriteItem : LobbyMsgHandler + { + protected override async Task HandleAsync() + { + ReqEquipFavoriteItem req = await ReadData(); + User user = GetUser(); + + ResEquipFavoriteItem response = new(); + + NetUserFavoriteItemData? favoriteItemToEquip = user.FavoriteItems.FirstOrDefault(f => f.FavoriteItemId == req.FavoriteItemId); + if (favoriteItemToEquip == null) + { + throw new BadHttpRequestException($"FavoriteItem with ID {req.FavoriteItemId} not found", 404); + } + + CharacterModel? character = user.Characters.FirstOrDefault(c => c.Csn == req.Csn); + if (character == null) + { + throw new BadHttpRequestException($"Character with CSN {req.Csn} not found", 404); + } + + NetUserFavoriteItemData? existingEquippedItem = user.FavoriteItems.FirstOrDefault(f => f.Csn == req.Csn); + if (existingEquippedItem != null) + { + existingEquippedItem.Csn = 0; // Unequip + } + + favoriteItemToEquip.Csn = req.Csn; + + foreach (NetUserFavoriteItemData item in user.FavoriteItems) + { + response.FavoriteItems.Add(item); + } + + JsonDb.Save(); + + await WriteDataAsync(response); + } + } +} diff --git a/EpinelPS/LobbyServer/FavoriteItem/FinishFavoriteItemQuest.cs b/EpinelPS/LobbyServer/FavoriteItem/FinishFavoriteItemQuest.cs new file mode 100644 index 0000000..bb40536 --- /dev/null +++ b/EpinelPS/LobbyServer/FavoriteItem/FinishFavoriteItemQuest.cs @@ -0,0 +1,83 @@ +using EpinelPS.Database; +using EpinelPS.Utils; +using EpinelPS.Data; + +namespace EpinelPS.LobbyServer.FavoriteItem +{ + [PacketPath("/favoriteitem/quest/finish")] + public class FinishFavoriteItemQuest : LobbyMsgHandler + { + protected override async Task HandleAsync() + { + ReqFinishFavoriteItemQuest req = await ReadData(); + User user = GetUser(); + + FavoriteItemQuestRecord? questData = GetQuestDataFromGameData(req.FavoriteItemQuestId); + if (questData == null) + { + ResFinishFavoriteItemQuest errorResponse = new(); + await WriteDataAsync(errorResponse); + return; + } + + + NetUserFavoriteItemQuestData? existingQuest = user.FavoriteItemQuests.FirstOrDefault(q => q.QuestId == req.FavoriteItemQuestId); + + if (existingQuest != null) + { + if (existingQuest.Clear) + { + ResFinishFavoriteItemQuest errorResponse = new(); + await WriteDataAsync(errorResponse); + return; + } + + existingQuest.Clear = true; + } + else + { + NetUserFavoriteItemQuestData newQuest = new NetUserFavoriteItemQuestData + { + QuestId = req.FavoriteItemQuestId, + Clear = true, + Received = false + }; + user.FavoriteItemQuests.Add(newQuest); + } + + + if (questData.next_quest_id > 0) + { + NetUserFavoriteItemQuestData? nextQuest = user.FavoriteItemQuests.FirstOrDefault(q => q.QuestId == questData.next_quest_id); + if (nextQuest == null) + { + NetUserFavoriteItemQuestData newQuest = new NetUserFavoriteItemQuestData + { + QuestId = questData.next_quest_id, + Clear = false, + Received = false + }; + user.FavoriteItemQuests.Add(newQuest); + } + + } + + JsonDb.Save(); + + ResFinishFavoriteItemQuest response = new(); + await WriteDataAsync(response); + } + + private FavoriteItemQuestRecord? GetQuestDataFromGameData(int questId) + { + if (GameData.Instance.FavoriteItemQuestTable.TryGetValue(questId, out FavoriteItemQuestRecord? questRecord)) + { + return questRecord; + } + + return null; + + } + + } +} \ No newline at end of file diff --git a/EpinelPS/LobbyServer/FavoriteItem/GetFavoriteItemLibrary.cs b/EpinelPS/LobbyServer/FavoriteItem/GetFavoriteItemLibrary.cs index 91e0935..31b83ad 100644 --- a/EpinelPS/LobbyServer/FavoriteItem/GetFavoriteItemLibrary.cs +++ b/EpinelPS/LobbyServer/FavoriteItem/GetFavoriteItemLibrary.cs @@ -12,6 +12,15 @@ namespace EpinelPS.LobbyServer.FavoriteItem ResGetFavoriteItemLibrary response = new(); User user = GetUser(); + foreach (NetUserFavoriteItemData favoriteItem in user.FavoriteItems) + { + NetFavoriteItemLibraryElement libraryElement = new NetFavoriteItemLibraryElement + { + Tid = favoriteItem.Tid, + ReceivedAt = DateTime.UtcNow.Ticks // Use current time as received time + }; + response.FavoriteItemLibrary.Add(libraryElement); + } await WriteDataAsync(response); } diff --git a/EpinelPS/LobbyServer/FavoriteItem/IncreaseExpFavoriteItem.cs b/EpinelPS/LobbyServer/FavoriteItem/IncreaseExpFavoriteItem.cs new file mode 100644 index 0000000..b9935b1 --- /dev/null +++ b/EpinelPS/LobbyServer/FavoriteItem/IncreaseExpFavoriteItem.cs @@ -0,0 +1,214 @@ +using EpinelPS.Database; +using EpinelPS.Utils; +using EpinelPS.Data; + +namespace EpinelPS.LobbyServer.FavoriteItem +{ + [PacketPath("/favoriteitem/increaseexp")] + public class IncreaseExpFavoriteItem : LobbyMsgHandler + { + // Favorite item experience materials mapping + private static readonly Dictionary favoriteExpTable = new() + { + { 7150001, 20 }, // 20 exp + { 7150002, 50 }, // 50 exp + { 7150003, 100 }, // 100 exp + }; + + protected override async Task HandleAsync() + { + ReqIncreaseExpFavoriteItem req = await ReadData(); + User user = GetUser(); + + ResIncreaseExpFavoriteItem response = new(); + + NetUserFavoriteItemData? favoriteItem = user.FavoriteItems.FirstOrDefault(f => f.FavoriteItemId == req.FavoriteItemId); + if (favoriteItem == null) + { + throw new BadHttpRequestException($"FavoriteItem with ID {req.FavoriteItemId} not found", 404); + } + + if (req.ItemData == null) + { + throw new BadHttpRequestException($"No material item provided", 400); + } + + ItemData? userItem = user.Items.FirstOrDefault(x => x.Isn == req.ItemData.Isn); + if (userItem == null) + { + throw new BadHttpRequestException($"Material item with ISN {req.ItemData.Isn} not found", 404); + } + + int useCount = req.ItemData.Count * req.LoopCount; + if (userItem.Count < useCount) + { + throw new BadHttpRequestException($"Insufficient material. Required: {useCount}, Available: {userItem.Count}", 400); + } + + FavoriteItemProbabilityRecord? probabilityData = GetProbabilityData(favoriteItem.Lv, req.ItemData.Tid); + if (probabilityData == null) + { + throw new BadHttpRequestException($"Cannot upgrade at current level with this material", 400); + } + + int baseExp = probabilityData.exp * req.LoopCount; + bool isGreatSuccess = CheckGreatSuccess(probabilityData.great_success_rate); + + int totalExpGained = baseExp; + int targetLevel = favoriteItem.Lv; + + if (isGreatSuccess) + { + targetLevel = probabilityData.great_success_level; + } + + int goldCost = baseExp * 10; + + userItem.Count -= useCount; + + if (user.GetCurrencyVal(CurrencyType.Gold) < goldCost) + { + throw new BadHttpRequestException($"Insufficient gold. Required: {goldCost}, Available: {user.GetCurrencyVal(CurrencyType.Gold)}", 400); + } + + int originalLevel = favoriteItem.Lv; + int originalExp = favoriteItem.Exp; + + if (isGreatSuccess) + { + favoriteItem.Lv = targetLevel; + favoriteItem.Exp = 0; // Reset exp at target level + } + else + { + favoriteItem.Exp += totalExpGained; + ProcessLevelUp(favoriteItem); + } + + user.AddCurrency(CurrencyType.Gold, -goldCost); + + + response.FavoriteItem = favoriteItem; + response.Result = isGreatSuccess ? FavoriteItemGreatSuccessResult.GreatSuccess : FavoriteItemGreatSuccessResult.Success; + response.ItemData = NetUtils.ToNet(userItem); + response.LoopCount = req.LoopCount; + + + JsonDb.Save(); + + await WriteDataAsync(response); + } + + private FavoriteItemProbabilityRecord? GetProbabilityData(int currentLevel, int materialId) + { + foreach (var record in GameData.Instance.FavoriteItemProbabilityTable.Values) + { + if (record.need_item_id == materialId && + currentLevel >= record.level_min && + currentLevel <= record.level_max) + { + return record; + } + } + return null; + } + + private bool CheckGreatSuccess(int successRate) + { + Random random = new Random(); + int roll = random.Next(0, 10000); + return roll < successRate; + } + + private void ProcessLevelUp(NetUserFavoriteItemData favoriteItem) + { + + if (!GameData.Instance.FavoriteItemTable.TryGetValue(favoriteItem.Tid, out FavoriteItemRecord? favoriteRecord)) + { + var sampleTids = GameData.Instance.FavoriteItemTable.Keys.Take(5).ToList(); + return; + } + + + string itemRarity = favoriteRecord.favorite_rare; + if (string.IsNullOrEmpty(itemRarity)) + { + if (favoriteItem.Tid >= 100102 && favoriteItem.Tid <= 100602 && favoriteItem.Tid % 100 == 2) + { + itemRarity = "SR"; + } + else if (favoriteItem.Tid >= 100101 && favoriteItem.Tid <= 100601 && favoriteItem.Tid % 100 == 1) + { + itemRarity = "R"; + } + else if (favoriteItem.Tid >= 200101 && favoriteItem.Tid <= 201301 && favoriteItem.Tid % 100 == 1) + { + itemRarity = "SSR"; + } + + } + + + if (itemRarity == "SSR") + { + int ssrMaxLevel = 2; // SSR has levels 0, 1, 2 + + if (favoriteItem.Lv < ssrMaxLevel) + { + favoriteItem.Lv++; + favoriteItem.Exp = 0; // SSR items don't use exp system + } + + if (favoriteItem.Lv >= ssrMaxLevel) + { + favoriteItem.Exp = 0; + } + + return; + } + + var expRecords = GameData.Instance.FavoriteItemExpTable.Values + .Where(x => x.favorite_rare == itemRarity) + .OrderBy(x => x.level) + .ToList(); + + + if (!expRecords.Any()) + { + var allRarities = GameData.Instance.FavoriteItemExpTable.Values.Select(x => x.favorite_rare).Distinct(); + return; + } + + int maxLevel = 15; + + while (favoriteItem.Lv < maxLevel) + { + int expRequired = GetExpRequiredForLevel(favoriteItem.Lv + 1, expRecords); + + if (favoriteItem.Exp >= expRequired && expRequired > 0) + { + favoriteItem.Exp -= expRequired; + favoriteItem.Lv++; + } + else + { + break; + } + } + + // Cap excess exp if at max level + if (favoriteItem.Lv >= maxLevel) + { + favoriteItem.Exp = 0; + } + + } + + private int GetExpRequiredForLevel(int level, List expRecords) + { + var record = expRecords.FirstOrDefault(x => x.level == level); + return record?.need_exp ?? 0; + } + + } +} \ No newline at end of file diff --git a/EpinelPS/LobbyServer/FavoriteItem/ListFavoriteItem.cs b/EpinelPS/LobbyServer/FavoriteItem/ListFavoriteItem.cs index ac74cb9..93111cd 100644 --- a/EpinelPS/LobbyServer/FavoriteItem/ListFavoriteItem.cs +++ b/EpinelPS/LobbyServer/FavoriteItem/ListFavoriteItem.cs @@ -12,6 +12,12 @@ namespace EpinelPS.LobbyServer.FavoriteItem ResListFavoriteItem response = new(); + // Add all user's favorite items to the response + foreach (NetUserFavoriteItemData favoriteItem in user.FavoriteItems) + { + response.FavoriteItems.Add(favoriteItem); + } + await WriteDataAsync(response); } } diff --git a/EpinelPS/LobbyServer/FavoriteItem/ListFavoriteItemQuests.cs b/EpinelPS/LobbyServer/FavoriteItem/ListFavoriteItemQuests.cs index fe201dc..fb1651b 100644 --- a/EpinelPS/LobbyServer/FavoriteItem/ListFavoriteItemQuests.cs +++ b/EpinelPS/LobbyServer/FavoriteItem/ListFavoriteItemQuests.cs @@ -1,4 +1,5 @@ -using EpinelPS.Utils; +using EpinelPS.Utils; +using EpinelPS.Data; namespace EpinelPS.LobbyServer.FavoriteItem { @@ -9,10 +10,20 @@ namespace EpinelPS.LobbyServer.FavoriteItem { ReqListFavoriteItemQuest req = await ReadData(); User user = GetUser(); - + ResListFavoriteItemQuest response = new(); + + if (user.FavoriteItemQuests == null) + { + user.FavoriteItemQuests = new List(); + } + + foreach (NetUserFavoriteItemQuestData quest in user.FavoriteItemQuests) + { + response.FavoriteItemQuests.Add(quest); + } await WriteDataAsync(response); } } -} +} \ No newline at end of file diff --git a/EpinelPS/LobbyServer/FavoriteItem/ObtainFavoriteItemQuestReward.cs b/EpinelPS/LobbyServer/FavoriteItem/ObtainFavoriteItemQuestReward.cs new file mode 100644 index 0000000..5945457 --- /dev/null +++ b/EpinelPS/LobbyServer/FavoriteItem/ObtainFavoriteItemQuestReward.cs @@ -0,0 +1,86 @@ +using EpinelPS.Database; +using EpinelPS.Utils; +using EpinelPS.Data; + +namespace EpinelPS.LobbyServer.FavoriteItem +{ + [PacketPath("/favoriteitem/quest/obtain")] + public class ObtainFavoriteItemQuestReward : LobbyMsgHandler + { + protected override async Task HandleAsync() + { + ReqObtainFavoriteItemQuestReward req = await ReadData(); + User user = GetUser(); + + FavoriteItemQuestRecord? questData = GameData.Instance.GetFavoriteItemQuestTableData(req.QuestId); + if (questData == null) + { + throw new BadHttpRequestException("Quest not found"); + } + + NetUserFavoriteItemQuestData? userQuest = user.FavoriteItemQuests.FirstOrDefault(q => q.QuestId == req.QuestId); + if (userQuest == null || !userQuest.Clear || userQuest.Received) + { + throw new BadHttpRequestException("Quest not cleared or reward already received"); + } + + List characterRecords = GameData.Instance.CharacterTable.Values.Where(c => c.name_code == questData.name_code).ToList(); + if (!characterRecords.Any()) + { + throw new Exception($"Failed to find character record with name_code: {questData.name_code}"); + } + + HashSet characterTids = characterRecords.Select(c => c.id).ToHashSet(); + CharacterModel? character = user.Characters.FirstOrDefault(c => characterTids.Contains(c.Tid)); + + int characterCsn = 0; + if (character != null) + { + characterCsn = character.Csn; + } + + RewardTableRecord ? reward = GameData.Instance.GetRewardTableEntry(questData.reward_id); + if (reward?.rewards == null || reward.rewards.Length == 0 || reward.rewards[0].reward_type != "FavoriteItem") + { + if (questData.reward_id > 0 && reward != null) + { + NetRewardData rewardData = RewardUtils.RegisterRewardsForUser(user, reward); + ResObtainFavoriteItemQuestReward genericResponse = new ResObtainFavoriteItemQuestReward { UserReward = rewardData }; + userQuest.Received = true; + JsonDb.Save(); + await WriteDataAsync(genericResponse); + return; + } + throw new Exception("FavoriteItem reward data not found for quest"); + } + int newItemTid = reward.rewards[0].reward_id; + + if (character != null) + { + NetUserFavoriteItemData? existingEquippedItem = user.FavoriteItems.FirstOrDefault(f => f.Csn == characterCsn); + if (existingEquippedItem != null) + { + user.FavoriteItems.Remove(existingEquippedItem); + } + } + + NetRewardData finalRewardData = RewardUtils.RegisterRewardsForUser(user, reward); + + if (character != null && finalRewardData.UserFavoriteItems.Count > 0) + { + var newFavoriteItem = user.FavoriteItems.LastOrDefault(f => f.Tid == newItemTid); + if (newFavoriteItem != null) + { + newFavoriteItem.Csn = characterCsn; // Equip item by setting Csn + } + } + + userQuest.Received = true; + + ResObtainFavoriteItemQuestReward response = new ResObtainFavoriteItemQuestReward { UserReward = finalRewardData }; + + JsonDb.Save(); + await WriteDataAsync(response); + } + } +} \ No newline at end of file diff --git a/EpinelPS/LobbyServer/FavoriteItem/StartFavoriteItemQuest.cs b/EpinelPS/LobbyServer/FavoriteItem/StartFavoriteItemQuest.cs new file mode 100644 index 0000000..8ddbd94 --- /dev/null +++ b/EpinelPS/LobbyServer/FavoriteItem/StartFavoriteItemQuest.cs @@ -0,0 +1,32 @@ +using EpinelPS.Database; +using EpinelPS.Utils; +using EpinelPS.Data; + +namespace EpinelPS.LobbyServer.FavoriteItem +{ + [PacketPath("/favoriteitem/quest/start")] + public class StartFavoriteItemQuest : LobbyMsgHandler + { + protected override async Task HandleAsync() + { + ReqStartFavoriteItemQuest req = await ReadData(); + User user = GetUser(); + + var newQuest = new NetUserFavoriteItemQuestData + { + QuestId = req.FavoriteItemQuestId, + Clear = false, + Received = false + }; + + user.FavoriteItemQuests.Add(newQuest); + + JsonDb.Save(); + + ResStartFavoriteItemQuest response = new(); + await WriteDataAsync(response); + } + + + } +} \ No newline at end of file diff --git a/EpinelPS/LobbyServer/FavoriteItem/UpgradeFavoriteItem.cs b/EpinelPS/LobbyServer/FavoriteItem/UpgradeFavoriteItem.cs new file mode 100644 index 0000000..f8a689a --- /dev/null +++ b/EpinelPS/LobbyServer/FavoriteItem/UpgradeFavoriteItem.cs @@ -0,0 +1,96 @@ +using EpinelPS.Database; +using EpinelPS.Utils; +using EpinelPS.Data; + +namespace EpinelPS.LobbyServer.FavoriteItem +{ + [PacketPath("/favoriteitem/exchange")] + public class UpgradeFavoriteItem : LobbyMsgHandler + { + protected override async Task HandleAsync() + { + ReqIncreaseExpFavoriteItem req = await ReadData(); + User user = GetUser(); + + ResEquipFavoriteItem response = new(); + + NetUserFavoriteItemData? rFavoriteItem = user.FavoriteItems.FirstOrDefault(f => f.FavoriteItemId == req.FavoriteItemId); + if (rFavoriteItem == null) + { + throw new BadHttpRequestException("Favorite item not found", 400); + } + + int srItemTid = rFavoriteItem.Tid + 1; + + + NetUserFavoriteItemData? srInventoryItem = user.FavoriteItems.FirstOrDefault(f => f.Tid == srItemTid && f.Csn == 0); + if (srInventoryItem == null) + { + throw new BadHttpRequestException($"No SR-grade favorite item (TID: {srItemTid}) available in inventory for exchange", 400); + } + + (int NewLevel, int RemainingExp, double ConversionRate) expConversion = CalculateExpConversion(rFavoriteItem.Lv, rFavoriteItem.Exp); + + int exchangeCost = 100000; // 100k gold for exchange + long goldBefore = user.GetCurrencyVal(CurrencyType.Gold); + if (goldBefore < exchangeCost) + { + throw new BadHttpRequestException($"Insufficient gold for exchange. Required: {exchangeCost}, Available: {goldBefore}", 400); + } + + long equippedCharacterCsn = rFavoriteItem.Csn; + + NetUserFavoriteItemData newSrFavoriteItem = new NetUserFavoriteItemData + { + FavoriteItemId = user.GenerateUniqueItemId(), + Tid = srItemTid, + Csn = equippedCharacterCsn, // Maintain equipment status + Lv = expConversion.NewLevel, + Exp = expConversion.RemainingExp + }; + + + int itemCountBefore = user.FavoriteItems.Count; + user.FavoriteItems.Remove(rFavoriteItem); + user.FavoriteItems.Remove(srInventoryItem); + int itemCountAfter = user.FavoriteItems.Count; + user.FavoriteItems.Add(newSrFavoriteItem); + int finalItemCount = user.FavoriteItems.Count; + + user.AddCurrency(CurrencyType.Gold, -exchangeCost); + long goldAfter = user.GetCurrencyVal(CurrencyType.Gold); + + + foreach (NetUserFavoriteItemData item in user.FavoriteItems) + { + response.FavoriteItems.Add(item); + } + + JsonDb.Save(); + + + await WriteDataAsync(response); + } + + private static (int NewLevel, int RemainingExp, double ConversionRate) CalculateExpConversion(int rLevel, int rExp) + { + int totalRExp = (rLevel * 1000) + rExp; + + int totalSRExp = totalRExp; + + int srLevel = totalSRExp / 3000; // Each SR level needs 3000 exp + int remainingExp = totalSRExp % 3000; + + if (srLevel > 15) + { + srLevel = 15; + remainingExp = 0; + } + + double conversionRate = totalRExp > 0 ? (double)totalSRExp / totalRExp : 1.0; + + return (srLevel, remainingExp, conversionRate); + } + + } +} diff --git a/EpinelPS/LobbyServer/Inventory/ClearHarmonyCube.cs b/EpinelPS/LobbyServer/Inventory/ClearHarmonyCube.cs new file mode 100644 index 0000000..0a6f30f --- /dev/null +++ b/EpinelPS/LobbyServer/Inventory/ClearHarmonyCube.cs @@ -0,0 +1,76 @@ +using EpinelPS.Database; +using EpinelPS.Utils; +using EpinelPS.Data; + +namespace EpinelPS.LobbyServer.Inventory +{ + [PacketPath("/inventory/clearharmonycube")] + public class ClearHarmonyCube : LobbyMsgHandler + { + protected override async Task HandleAsync() + { + ReqClearHarmonyCube req = await ReadData(); + User user = GetUser(); + + + ResClearHarmonyCube response = new(); + + foreach (ItemData item in user.Items.ToArray()) + { + if (item.Isn == req.Isn) + { + if (req.Csn > 0) + { + if (item.CsnList.Contains(req.Csn)) + { + item.CsnList.Remove(req.Csn); + } + + if (item.CsnList.Count > 0) + { + item.Csn = item.CsnList[0]; + if (GameData.Instance.ItemHarmonyCubeTable.TryGetValue(item.ItemType, out var harmonyCubeData)) + { + item.Position = harmonyCubeData.location_id; + } + } + else + { + item.Csn = 0; + item.Position = 0; + } + } + else + { + item.CsnList.Clear(); + item.Csn = 0; + item.Position = 0; + } + + NetUserHarmonyCubeData netHarmonyCube = new() + { + Isn = item.Isn, + Tid = item.ItemType, + Lv = item.Level + }; + + foreach (long csn in item.CsnList) + { + netHarmonyCube.CsnList.Add(csn); + } + + if (item.Csn > 0 && !item.CsnList.Contains(item.Csn)) + { + netHarmonyCube.CsnList.Add(item.Csn); + } + + response.HarmonyCube = netHarmonyCube; + break; + } + } + + JsonDb.Save(); + await WriteDataAsync(response); + } + } +} diff --git a/EpinelPS/LobbyServer/Inventory/GetHarmonyCube.cs b/EpinelPS/LobbyServer/Inventory/GetHarmonyCube.cs new file mode 100644 index 0000000..a67c90e --- /dev/null +++ b/EpinelPS/LobbyServer/Inventory/GetHarmonyCube.cs @@ -0,0 +1,49 @@ +using EpinelPS.Database; +using EpinelPS.Utils; +using EpinelPS.Data; + +namespace EpinelPS.LobbyServer.Inventory +{ + [PacketPath("/inventory/getharmonycube")] + public class GetHarmonyCube : LobbyMsgHandler + { + protected override async Task HandleAsync() + { + ReqGetHarmonyCube req = await ReadData(); + User user = GetUser(); + + ResGetHarmonyCube response = new(); + + List harmonyCubes = user.Items.Where(item => + GameData.Instance.ItemHarmonyCubeTable.ContainsKey(item.ItemType)).ToList(); + + foreach (ItemData harmonyCube in harmonyCubes) + { + if (GameData.Instance.ItemHarmonyCubeTable.TryGetValue(harmonyCube.ItemType, out ItemHarmonyCubeRecord? harmonyCubeData)) + { + NetUserHarmonyCubeData netHarmonyCube = new() + { + Isn = harmonyCube.Isn, + Tid = harmonyCube.ItemType, + Lv = harmonyCube.Level + }; + + foreach (long csn in harmonyCube.CsnList) + { + netHarmonyCube.CsnList.Add(csn); + } + + if (harmonyCube.Csn > 0 && !harmonyCube.CsnList.Contains(harmonyCube.Csn)) + { + netHarmonyCube.CsnList.Add(harmonyCube.Csn); + } + + response.HarmonyCubes.Add(netHarmonyCube); + } + } + + + await WriteDataAsync(response); + } + } +} diff --git a/EpinelPS/LobbyServer/Inventory/LevelUpHarmonyCube.cs b/EpinelPS/LobbyServer/Inventory/LevelUpHarmonyCube.cs new file mode 100644 index 0000000..09ee90e --- /dev/null +++ b/EpinelPS/LobbyServer/Inventory/LevelUpHarmonyCube.cs @@ -0,0 +1,101 @@ +using EpinelPS.Database; +using EpinelPS.Data; +using EpinelPS.Utils; +using static EpinelPS.Data.TriggerType; + +namespace EpinelPS.LobbyServer.Inventory +{ + [PacketPath("/inventory/levelupharmonycube")] + public class LevelUpHarmonyCube : LobbyMsgHandler + { + protected override async Task HandleAsync() + { + ReqLevelUpHarmonyCube req = await ReadData(); + User user = GetUser(); + + ResLevelUpHarmonyCube response = new(); + + ItemData? harmonyCubeItem = user.Items.FirstOrDefault(x => x.Isn == req.Isn); + if (harmonyCubeItem == null) + { + throw new BadHttpRequestException("Harmony cube not found", 404); + } + + if (!GameData.Instance.ItemHarmonyCubeTable.TryGetValue(harmonyCubeItem.ItemType, out ItemHarmonyCubeRecord? harmonyCubeData)) + { + throw new BadHttpRequestException("Item is not a harmony cube", 400); + } + + List levelData = GameData.Instance.ItemHarmonyCubeLevelTable.Values + .Where(x => x.level_enhance_id == harmonyCubeData.level_enhance_id) + .OrderBy(x => x.level) + .ToList(); + + if (levelData.Count == 0) + { + throw new BadHttpRequestException("No level data found for this harmony cube", 400); + } + + ItemHarmonyCubeLevelRecord? currentLevelData = levelData.FirstOrDefault(x => x.level == harmonyCubeItem.Level); + if (currentLevelData == null) + { + throw new BadHttpRequestException("Current level data not found", 400); + } + + ItemHarmonyCubeLevelRecord? nextLevelData = levelData.FirstOrDefault(x => x.level == harmonyCubeItem.Level + 1); + if (nextLevelData == null) + { + throw new BadHttpRequestException("Harmony cube is already at max level", 400); + } + + int requiredMaterialCount = nextLevelData.material_value; + int requiredMaterialId = nextLevelData.material_id; + int requiredGold = nextLevelData.gold_value; + + ItemData? materialItem = user.Items.FirstOrDefault(x => x.ItemType == requiredMaterialId && x.Count >= requiredMaterialCount); + if (materialItem == null) + { + throw new BadHttpRequestException($"Not enough materials. Required: {requiredMaterialCount} of item {requiredMaterialId}", 400); + } + + if (user.GetCurrencyVal(CurrencyType.Gold) < requiredGold) + { + throw new BadHttpRequestException($"Not enough gold. Required: {requiredGold}", 400); + } + + materialItem.Count -= requiredMaterialCount; + if (materialItem.Count <= 0) + { + user.Items.Remove(materialItem); + } + else + { + response.Items.Add(NetUtils.ToNet(materialItem)); + } + + user.Currency[CurrencyType.Gold] -= requiredGold; + + int originalLevel = harmonyCubeItem.Level; + harmonyCubeItem.Level++; + harmonyCubeItem.Exp = 0; // Reset exp for the new level + + user.AddTrigger(TriggerType.HarmonyCubeLevel, harmonyCubeItem.Level, harmonyCubeItem.ItemType); + + if (harmonyCubeItem.Level >= levelData.Count) + { + user.AddTrigger(TriggerType.HarmonyCubeLevelMax, 1, harmonyCubeItem.ItemType); + } + + response.Items.Add(NetUtils.ToNet(harmonyCubeItem)); + + response.Currency = new NetUserCurrencyData + { + Type = (int)CurrencyType.Gold, + Value = user.GetCurrencyVal(CurrencyType.Gold) + }; + + JsonDb.Save(); + await WriteDataAsync(response); + } + } +} diff --git a/EpinelPS/LobbyServer/Inventory/ManagementHarmonyCube.cs b/EpinelPS/LobbyServer/Inventory/ManagementHarmonyCube.cs new file mode 100644 index 0000000..5e34968 --- /dev/null +++ b/EpinelPS/LobbyServer/Inventory/ManagementHarmonyCube.cs @@ -0,0 +1,190 @@ +using EpinelPS.Database; +using EpinelPS.Utils; +using EpinelPS.Data; + +namespace EpinelPS.LobbyServer.Inventory +{ + [PacketPath("/inventory/managementharmonycube")] + public class ManagementHarmonyCube : LobbyMsgHandler + { + protected override async Task HandleAsync() + { + ReqManagementHarmonyCube req = await ReadData(); + User user = GetUser(); + + ResManagementHarmonyCube response = new(); + + ItemData? harmonyCubeItem = user.Items.FirstOrDefault(x => x.Isn == req.Isn); + if (harmonyCubeItem == null) + { + throw new BadHttpRequestException("Harmony cube not found", 404); + } + + if (!GameData.Instance.ItemHarmonyCubeTable.TryGetValue(harmonyCubeItem.ItemType, out ItemHarmonyCubeRecord? harmonyCubeData)) + { + throw new BadHttpRequestException("Item is not a harmony cube", 400); + } + + ItemHarmonyCubeLevelRecord? currentLevelData = GetCurrentLevelData(harmonyCubeItem, harmonyCubeData); + int maxSlots = currentLevelData?.slot ?? 1; + + foreach (long clearCsn in req.Clears) + { + if (harmonyCubeItem.CsnList.Contains(clearCsn)) + { + harmonyCubeItem.CsnList.Remove(clearCsn); + } + + if (harmonyCubeItem.Csn == clearCsn) + { + harmonyCubeItem.Csn = 0; + harmonyCubeItem.Position = 0; + } + } + + foreach (NetWearHarmonyCubeData wearData in req.Wears) + { + long targetCsn = wearData.Csn; + long swapCsn = wearData.SwapCsn; + + if (swapCsn > 0) + { + if (harmonyCubeItem.CsnList.Contains(swapCsn)) + { + harmonyCubeItem.CsnList.Remove(swapCsn); + } + + if (harmonyCubeItem.Csn == swapCsn) + { + harmonyCubeItem.Csn = 0; + harmonyCubeItem.Position = 0; + } + } + + if (targetCsn > 0) + { + EquipHarmonyCubeToCharacter(user, harmonyCubeItem, harmonyCubeData, targetCsn, maxSlots); + } + } + + List allHarmonyCubes = user.Items.Where(item => + GameData.Instance.ItemHarmonyCubeTable.ContainsKey(item.ItemType)).ToList(); + + foreach (ItemData harmonyCube in allHarmonyCubes) + { + NetUserHarmonyCubeData netHarmonyCube = new() + { + Isn = harmonyCube.Isn, + Tid = harmonyCube.ItemType, + Lv = harmonyCube.Level + }; + + foreach (long csn in harmonyCube.CsnList) + { + netHarmonyCube.CsnList.Add(csn); + } + + if (harmonyCube.Csn > 0 && !harmonyCube.CsnList.Contains(harmonyCube.Csn)) + { + netHarmonyCube.CsnList.Add(harmonyCube.Csn); + } + + response.HarmonyCubes.Add(netHarmonyCube); + } + + JsonDb.Save(); + await WriteDataAsync(response); + } + + private void EquipHarmonyCubeToCharacter(User user, ItemData harmonyCubeItem, ItemHarmonyCubeRecord harmonyCubeData, long targetCsn, int maxSlots) + { + if (harmonyCubeItem.CsnList.Contains(targetCsn)) + { + return; // Already equipped, skip + } + + if (harmonyCubeItem.CsnList.Count >= maxSlots) + { + throw new BadHttpRequestException($"Harmony cube slot limit reached. Current level allows {maxSlots} characters.", 400); + } + + CharacterModel? character = user.GetCharacterBySerialNumber(targetCsn); + if (character == null) + { + throw new BadHttpRequestException($"Character {targetCsn} not found", 404); + } + + if (!IsClassCompatible(character, harmonyCubeData)) + { + throw new BadHttpRequestException($"Character class incompatible with harmony cube", 400); + } + + CleanupCharacterFromAllHarmonyCubes(user, targetCsn, harmonyCubeData.location_id, harmonyCubeItem.Isn); + + harmonyCubeItem.CsnList.Add(targetCsn); + + if (harmonyCubeItem.CsnList.Count == 1) + { + harmonyCubeItem.Csn = targetCsn; + harmonyCubeItem.Position = harmonyCubeData.location_id; + } + + } + + private void CleanupCharacterFromAllHarmonyCubes(User user, long targetCsn, int position, long excludeIsn) + { + foreach (ItemData item in user.Items.ToArray()) + { + if (!GameData.Instance.ItemHarmonyCubeTable.ContainsKey(item.ItemType) || + item.Isn == excludeIsn) + { + continue; + } + + if (!GameData.Instance.ItemHarmonyCubeTable.TryGetValue(item.ItemType, out ItemHarmonyCubeRecord? existingHarmonyCubeData)) + { + continue; + } + + bool wasInCsnList = item.CsnList.Contains(targetCsn); + bool wasInLegacyCsn = item.Csn == targetCsn; + + if (wasInCsnList || wasInLegacyCsn) + { + item.CsnList.Remove(targetCsn); + + if (item.CsnList.Count > 0) + { + item.Csn = item.CsnList[0]; + item.Position = existingHarmonyCubeData.location_id; + } + else + { + item.Csn = 0; + item.Position = 0; + } + + } + } + } + + private ItemHarmonyCubeLevelRecord? GetCurrentLevelData(ItemData harmonyCubeItem, ItemHarmonyCubeRecord harmonyCubeData) + { + List levelData = GameData.Instance.ItemHarmonyCubeLevelTable.Values + .Where(x => x.level_enhance_id == harmonyCubeData.level_enhance_id) + .OrderBy(x => x.level) + .ToList(); + + return levelData.FirstOrDefault(x => x.level == harmonyCubeItem.Level); + } + + private bool IsClassCompatible(CharacterModel character, ItemHarmonyCubeRecord harmonyCubeData) + { + if (GameData.Instance.CharacterTable.TryGetValue(character.Tid, out CharacterRecord? characterData)) + { + return harmonyCubeData.@class == "All" || harmonyCubeData.@class == characterData.character_class; + } + return false; + } + } +} diff --git a/EpinelPS/LobbyServer/Inventory/WearHarmonyCube.cs b/EpinelPS/LobbyServer/Inventory/WearHarmonyCube.cs new file mode 100644 index 0000000..f5cefc5 --- /dev/null +++ b/EpinelPS/LobbyServer/Inventory/WearHarmonyCube.cs @@ -0,0 +1,241 @@ +using EpinelPS.Database; +using EpinelPS.Utils; +using EpinelPS.Data; + +namespace EpinelPS.LobbyServer.Inventory +{ + [PacketPath("/inventory/wearharmonycube")] + public class WearHarmonyCube : LobbyMsgHandler + { + protected override async Task HandleAsync() + { + ReqWearHarmonyCube req = await ReadData(); + User user = GetUser(); + ResWearHarmonyCube response = new(); + + ItemData? harmonyCubeItem = user.Items.FirstOrDefault(x => x.Isn == req.Isn); + if (harmonyCubeItem == null) + { + throw new BadHttpRequestException("Harmony cube not found", 404); + } + + if (!GameData.Instance.ItemHarmonyCubeTable.TryGetValue(harmonyCubeItem.ItemType, out ItemHarmonyCubeRecord? harmonyCubeData)) + { + throw new BadHttpRequestException("Item is not a harmony cube", 400); + } + + if (req.Wear == null) + { + throw new BadHttpRequestException("Wear data is required", 400); + } + + long targetCsn = req.Wear.Csn; + long swapCsn = req.Wear.SwapCsn; + + ItemHarmonyCubeLevelRecord? currentLevelData = GetCurrentLevelData(harmonyCubeItem, harmonyCubeData); + int maxSlots = currentLevelData?.slot ?? 1; + + if (swapCsn > 0) + { + if (harmonyCubeItem.CsnList.Contains(swapCsn)) + { + harmonyCubeItem.CsnList.Remove(swapCsn); + } + + if (harmonyCubeItem.Csn == swapCsn) + { + harmonyCubeItem.Csn = 0; + harmonyCubeItem.Position = 0; + } + + } + + List modifiedItems = new(); + if (targetCsn > 0) + { + modifiedItems = EquipHarmonyCubeToCharacter(user, harmonyCubeItem, harmonyCubeData, targetCsn, maxSlots); + } + else if (targetCsn == 0) + { + harmonyCubeItem.CsnList.Clear(); + harmonyCubeItem.Csn = 0; + harmonyCubeItem.Position = 0; + } + else + { + throw new BadHttpRequestException("Invalid character CSN", 400); + } + + foreach (ItemData modifiedItem in modifiedItems) + { + NetUserHarmonyCubeData netModifiedHarmonyCube = new() + { + Isn = modifiedItem.Isn, + Tid = modifiedItem.ItemType, + Lv = modifiedItem.Level + }; + + foreach (long csn in modifiedItem.CsnList) + { + netModifiedHarmonyCube.CsnList.Add(csn); + } + + if (modifiedItem.Csn > 0 && !modifiedItem.CsnList.Contains(modifiedItem.Csn)) + { + netModifiedHarmonyCube.CsnList.Add(modifiedItem.Csn); + } + + response.HarmonyCubes.Add(netModifiedHarmonyCube); + } + + NetUserHarmonyCubeData netHarmonyCube = new() + { + Isn = harmonyCubeItem.Isn, + Tid = harmonyCubeItem.ItemType, + Lv = harmonyCubeItem.Level + }; + + foreach (long csn in harmonyCubeItem.CsnList) + { + netHarmonyCube.CsnList.Add(csn); + } + + if (harmonyCubeItem.Csn > 0 && !harmonyCubeItem.CsnList.Contains(harmonyCubeItem.Csn)) + { + netHarmonyCube.CsnList.Add(harmonyCubeItem.Csn); + } + + response.HarmonyCubes.Add(netHarmonyCube); + + JsonDb.Save(); + await WriteDataAsync(response); + } + + private List EquipHarmonyCubeToCharacter(User user, ItemData harmonyCubeItem, ItemHarmonyCubeRecord harmonyCubeData, long targetCsn, int maxSlots) + { + + // Check if already equipped to this character + if (harmonyCubeItem.CsnList.Contains(targetCsn)) + { + Console.WriteLine($"Harmony cube {harmonyCubeItem.ItemType} already equipped to character {targetCsn}"); + return new List(); // Already equipped, no need to do anything + } + + // Check slot limit + if (harmonyCubeItem.CsnList.Count >= maxSlots) + { + throw new BadHttpRequestException($"Harmony cube slot limit reached. Current level allows {maxSlots} characters.", 400); + } + + // Check if the character exists + CharacterModel? character = user.GetCharacterBySerialNumber(targetCsn); + if (character == null) + { + throw new BadHttpRequestException($"Character {targetCsn} not found", 404); + } + + // Check class compatibility + if (!IsClassCompatible(character, harmonyCubeData)) + { + throw new BadHttpRequestException($"Character class incompatible with harmony cube", 400); + } + + // CRITICAL: Remove this character from ALL harmony cubes at the same position + // This fixes any existing data inconsistency where a character might be in multiple CsnLists + List modifiedItems = CleanupCharacterFromAllHarmonyCubes(user, targetCsn, harmonyCubeData.location_id, harmonyCubeItem.Isn); + + // Add to CsnList + harmonyCubeItem.CsnList.Add(targetCsn); + + // For backward compatibility, also set legacy fields if this is the first character + if (harmonyCubeItem.CsnList.Count == 1) + { + harmonyCubeItem.Csn = targetCsn; + harmonyCubeItem.Position = harmonyCubeData.location_id; + } + + Console.WriteLine($"Equipped harmony cube {harmonyCubeItem.ItemType} to character {targetCsn} for user {user.Username} (slot {harmonyCubeItem.CsnList.Count}/{maxSlots})"); + + return modifiedItems; + } + + private List CleanupCharacterFromAllHarmonyCubes(User user, long targetCsn, int position, long excludeIsn) + { + // Remove this character from ALL harmony cubes (all positions) + // This ensures one character can only have one harmony cube equipped at any time + List modifiedItems = new(); + + foreach (ItemData item in user.Items.ToArray()) + { + // Skip if it's not a harmony cube or it's the item we're about to equip + if (!GameData.Instance.ItemHarmonyCubeTable.ContainsKey(item.ItemType) || + item.Isn == excludeIsn) + { + continue; + } + + // Get the harmony cube data + if (!GameData.Instance.ItemHarmonyCubeTable.TryGetValue(item.ItemType, out ItemHarmonyCubeRecord? existingHarmonyCubeData)) + { + continue; + } + + // Check ALL harmony cubes (not just same position) - ONE CHARACTER, ONE HARMONY CUBE RULE + // Check if this character is in the CsnList or legacy Csn field + bool wasInCsnList = item.CsnList.Contains(targetCsn); + bool wasInLegacyCsn = (item.Csn == targetCsn); + + if (wasInCsnList || wasInLegacyCsn) + { + // Remove from CsnList + item.CsnList.Remove(targetCsn); + + // Update legacy fields + if (item.CsnList.Count > 0) + { + // Set legacy fields to the first remaining character + item.Csn = item.CsnList[0]; + item.Position = existingHarmonyCubeData.location_id; + } + else + { + // No characters left, clear legacy fields + item.Csn = 0; + item.Position = 0; + } + + // Add to modified items list for response + modifiedItems.Add(item); + + Console.WriteLine($"[ONE HARMONY CUBE RULE] Removed character {targetCsn} from harmony cube {item.ItemType} (position {existingHarmonyCubeData.location_id}) - one character can only have one harmony cube"); + } + } + + return modifiedItems; + } + + private ItemHarmonyCubeLevelRecord? GetCurrentLevelData(ItemData harmonyCubeItem, ItemHarmonyCubeRecord harmonyCubeData) + { + // Get level data for this harmony cube + List levelData = GameData.Instance.ItemHarmonyCubeLevelTable.Values + .Where(x => x.level_enhance_id == harmonyCubeData.level_enhance_id) + .OrderBy(x => x.level) + .ToList(); + + // Find current level data + return levelData.FirstOrDefault(x => x.level == harmonyCubeItem.Level); + } + + private bool IsClassCompatible(CharacterModel character, ItemHarmonyCubeRecord harmonyCubeData) + { + // Get character data to check class + if (GameData.Instance.CharacterTable.TryGetValue(character.Tid, out CharacterRecord? characterData)) + { + // Check if harmony cube class restriction matches character class + return harmonyCubeData.@class == "All" || harmonyCubeData.@class == characterData.character_class; + } + return false; + } + + } +} diff --git a/EpinelPS/LobbyServer/LobbyUser/EnterLobbyServer.cs b/EpinelPS/LobbyServer/LobbyUser/EnterLobbyServer.cs index 9c98d9c..cb64fbf 100644 --- a/EpinelPS/LobbyServer/LobbyUser/EnterLobbyServer.cs +++ b/EpinelPS/LobbyServer/LobbyUser/EnterLobbyServer.cs @@ -1,4 +1,4 @@ -using EpinelPS.Data; +using EpinelPS.Data; using EpinelPS.Database; using EpinelPS.Utils; @@ -13,7 +13,7 @@ namespace EpinelPS.LobbyServer.LobbyUser User user = GetUser(); TimeSpan battleTime = DateTime.UtcNow - user.BattleTime; - long battleTimeMs = (long)(battleTime.TotalNanoseconds / 100); + long battleTimeMs = (long)(battleTime.TotalNanoseconds / 100); // NOTE: Keep this in sync with GetUser code @@ -67,18 +67,47 @@ namespace EpinelPS.LobbyServer.LobbyUser response.TypeTeams.Add(teamInfo.Value); } - // TODO: Save outpost data - response.Outposts.Add(new NetUserOutpostData() { SlotId = 1, BuildingId = 22401, IsDone = true, StartAt = 638549982076760660, CompleteAt = 638549982076760660 }); - response.Outposts.Add(new NetUserOutpostData() { SlotId = 4, BuildingId = 22701, IsDone = true, StartAt = 638549982076760660, CompleteAt = 638549982076760660 }); - response.Outposts.Add(new NetUserOutpostData() { SlotId = 5, BuildingId = 22801, IsDone = true, StartAt = 638549982076760660, CompleteAt = 638549982076760660 }); - response.Outposts.Add(new NetUserOutpostData() { SlotId = 6, BuildingId = 22901, IsDone = true, StartAt = 638549982076760660, CompleteAt = 638549982076760660 }); - response.Outposts.Add(new NetUserOutpostData() { SlotId = 7, BuildingId = 23001, IsDone = true, StartAt = 638549982076760660, CompleteAt = 638549982076760660 }); - response.Outposts.Add(new NetUserOutpostData() { SlotId = 3, BuildingId = 23101, IsDone = true, StartAt = 638549982076760660, CompleteAt = 638549982076760660 }); - response.Outposts.Add(new NetUserOutpostData() { SlotId = 2, BuildingId = 23201, IsDone = true, StartAt = 638549982076760660, CompleteAt = 638549982076760660 }); - response.Outposts.Add(new NetUserOutpostData() { SlotId = 9, BuildingId = 23301, IsDone = true, StartAt = 638549982076760660, CompleteAt = 638549982076760660 }); - response.Outposts.Add(new NetUserOutpostData() { SlotId = 8, BuildingId = 23401, IsDone = true, StartAt = 638549982076760660, CompleteAt = 638549982076760660 }); - response.Outposts.Add(new NetUserOutpostData() { SlotId = 10, BuildingId = 23501, IsDone = true, StartAt = 638549982076760660, CompleteAt = 638549982076760660 }); - response.Outposts.Add(new NetUserOutpostData() { SlotId = 38, BuildingId = 33601, IsDone = true, StartAt = 638549982076760660, CompleteAt = 638549982076760660 }); + if (user.OutpostBuildings != null && user.OutpostBuildings.Count > 0) + { + bool needsSave = false; + foreach (NetUserOutpostData building in user.OutpostBuildings) + { + + if (!building.IsDone && DateTime.UtcNow.Ticks >= building.CompleteAt) + { + building.IsDone = true; + needsSave = true; + } + } + + if (needsSave) + { + JsonDb.Save(); + } + + response.Outposts.AddRange(user.OutpostBuildings); + } + else + { + List defaultBuildings = new List + { + new() { SlotId = 1, BuildingId = 22401, IsDone = true, StartAt = 638549982076760660, CompleteAt = 638549982076760660 }, + new() { SlotId = 4, BuildingId = 22701, IsDone = true, StartAt = 638549982076760660, CompleteAt = 638549982076760660 }, + new() { SlotId = 5, BuildingId = 22801, IsDone = true, StartAt = 638549982076760660, CompleteAt = 638549982076760660 }, + new() { SlotId = 6, BuildingId = 22901, IsDone = true, StartAt = 638549982076760660, CompleteAt = 638549982076760660 }, + new() { SlotId = 7, BuildingId = 23001, IsDone = true, StartAt = 638549982076760660, CompleteAt = 638549982076760660 }, + new() { SlotId = 3, BuildingId = 23101, IsDone = true, StartAt = 638549982076760660, CompleteAt = 638549982076760660 }, + new() { SlotId = 2, BuildingId = 23201, IsDone = true, StartAt = 638549982076760660, CompleteAt = 638549982076760660 }, + new() { SlotId = 9, BuildingId = 23301, IsDone = true, StartAt = 638549982076760660, CompleteAt = 638549982076760660 }, + new() { SlotId = 8, BuildingId = 23401, IsDone = true, StartAt = 638549982076760660, CompleteAt = 638549982076760660 }, + new() { SlotId = 10, BuildingId = 23501, IsDone = true, StartAt = 638549982076760660, CompleteAt = 638549982076760660 }, + new() { SlotId = 38, BuildingId = 33601, IsDone = true, StartAt = 638549982076760660, CompleteAt = 638549982076760660 } + }; + + response.Outposts.AddRange(defaultBuildings); + user.OutpostBuildings = defaultBuildings; + JsonDb.Save(); + } response.LastClearedNormalMainStageId = user.LastNormalStageCleared; response.TimeRewardBuffs.AddRange(NetUtils.GetOutpostTimeReward(user)); @@ -90,4 +119,4 @@ namespace EpinelPS.LobbyServer.LobbyUser await WriteDataAsync(response); } } -} +} \ No newline at end of file diff --git a/EpinelPS/LobbyServer/Messenger/EnterMessenger.cs b/EpinelPS/LobbyServer/Messenger/EnterMessenger.cs index b6faa2b..031d546 100644 --- a/EpinelPS/LobbyServer/Messenger/EnterMessenger.cs +++ b/EpinelPS/LobbyServer/Messenger/EnterMessenger.cs @@ -14,16 +14,32 @@ namespace EpinelPS.LobbyServer.Messenger ResEnterMessengerDialog response = new(); - MessengerMsgConditionRecord opener = GameData.Instance.MessageConditions[req.Tid]; - KeyValuePair conversation = GameData.Instance.Messages.Where(x => x.Value.conversation_id == opener.tid && x.Value.is_opener).First(); - + if (!GameData.Instance.MessageConditions.TryGetValue(req.Tid, out MessengerMsgConditionRecord? opener)) + { + throw new BadHttpRequestException($"Message condition {req.Tid} not found", 404); + } + + KeyValuePair conversation = GameData.Instance.Messages.FirstOrDefault(x => + x.Value.conversation_id == opener.tid && x.Value.is_opener); + + if (conversation.Value == null) + { + conversation = GameData.Instance.Messages.FirstOrDefault(x => + x.Value.conversation_id == opener.tid); + + if (conversation.Value == null) + { + throw new BadHttpRequestException($"No conversation found for {opener.tid}", 404); + } + } + response.Message = user.CreateMessage(conversation.Value); user.AddTrigger(TriggerType.MessageClear, 1, req.Tid); // TODO check if this is correct - + JsonDb.Save(); await WriteDataAsync(response); } } -} +} \ No newline at end of file diff --git a/EpinelPS/LobbyServer/Messenger/GetMessages.cs b/EpinelPS/LobbyServer/Messenger/GetMessages.cs index af9fedc..edca06d 100644 --- a/EpinelPS/LobbyServer/Messenger/GetMessages.cs +++ b/EpinelPS/LobbyServer/Messenger/GetMessages.cs @@ -1,4 +1,5 @@ using EpinelPS.Utils; +using EpinelPS.Data; namespace EpinelPS.LobbyServer.Messenger { @@ -10,6 +11,8 @@ namespace EpinelPS.LobbyServer.Messenger ReqGetMessages req = await ReadData(); User user = GetUser(); + CheckAndCreateAvailableMessages(user); + ResGetMessages response = new(); IEnumerable newMessages = user.MessengerData.Where(x => x.Seq >= req.Seq); @@ -21,5 +24,71 @@ namespace EpinelPS.LobbyServer.Messenger await WriteDataAsync(response); } + + private static void CheckAndCreateAvailableMessages(User user) + { + foreach (KeyValuePair messageCondition in GameData.Instance.MessageConditions) + { + int conditionId = messageCondition.Key; + MessengerMsgConditionRecord msgCondition = messageCondition.Value; + + if (IsMessageConditionSatisfied(user, conditionId)) + { + bool messageExists = user.MessengerData.Any(m => m.ConversationId == msgCondition.tid); + if (!messageExists) + { + KeyValuePair conversation = GameData.Instance.Messages.FirstOrDefault(x => + x.Value.conversation_id == msgCondition.tid && x.Value.is_opener); + + if (conversation.Value != null) + { + user.CreateMessage(conversation.Value); + } + } + } + } + } + + private static bool IsMessageConditionSatisfied(User user, int conditionId) + { + if (!GameData.Instance.MessageConditions.TryGetValue(conditionId, out MessengerMsgConditionRecord? msgCondition)) + { + return false; + } + + foreach (MessengerConditionTriggerList trigger in msgCondition.trigger_list) + { + if (trigger.trigger == "None" || trigger.condition_id == 0) + continue; + + if (!CheckTriggerCondition(user, trigger)) + { + return false; // All conditions must be satisfied + } + } + + return true; + } + + private static bool CheckTriggerCondition(User user, MessengerConditionTriggerList trigger) + { + TriggerType triggerType = ParseTriggerType(trigger.trigger); + + return user.Triggers.Any(t => + t.Type == triggerType && + t.ConditionId == trigger.condition_id && + t.Value >= trigger.condition_value); + } + + private static TriggerType ParseTriggerType(string triggerString) + { + return triggerString switch + { + "ObtainCharacter" => TriggerType.ObtainCharacter, + "MainQuestClear" => TriggerType.MainQuestClear, + "MessageClear" => TriggerType.MessageClear, + _ => TriggerType.None + }; + } } } diff --git a/EpinelPS/LobbyServer/Outpost/BuildBuilding.cs b/EpinelPS/LobbyServer/Outpost/BuildBuilding.cs index d253381..5edf00e 100644 --- a/EpinelPS/LobbyServer/Outpost/BuildBuilding.cs +++ b/EpinelPS/LobbyServer/Outpost/BuildBuilding.cs @@ -1,4 +1,6 @@ -using EpinelPS.Utils; +using EpinelPS.Utils; +using EpinelPS.Database; +using EpinelPS.Data; using System; using System.Collections.Generic; using System.Linq; @@ -10,17 +12,132 @@ namespace EpinelPS.LobbyServer.Outpost [PacketPath("/outpost/building")] public class BuildBuilding : LobbyMsgHandler { + private static BuildingCost GetBuildingCost(int buildingId) + { + // TODO: get building cost from data + // test data + return buildingId switch + { + // bulidding (22xxx) + 22401 => new BuildingCost { Gold = 1000, BuildTimeMinutes = 5 }, + 22701 => new BuildingCost { Gold = 2000, BuildTimeMinutes = 10 }, + 22801 => new BuildingCost { Gold = 1500, BuildTimeMinutes = 8 }, + 22901 => new BuildingCost { Gold = 3000, BuildTimeMinutes = 15 }, + 23001 => new BuildingCost { Gold = 5000, BuildTimeMinutes = 20 }, + 23100 => new BuildingCost { Gold = 800, BuildTimeMinutes = 6 }, + 23200 => new BuildingCost { Gold = 1200, BuildTimeMinutes = 8 }, + 23300 => new BuildingCost { Gold = 2500, BuildTimeMinutes = 12 }, + 23400 => new BuildingCost { Gold = 2000, BuildTimeMinutes = 10 }, + 23500 => new BuildingCost { Gold = 1800, BuildTimeMinutes = 9 }, + + // bulidding (10xxx-12xxx) + 10100 => new BuildingCost { Gold = 500, BuildTimeMinutes = 3 }, + 10200 => new BuildingCost { Gold = 800, BuildTimeMinutes = 5 }, + 10300 => new BuildingCost { Gold = 1200, BuildTimeMinutes = 7 }, + 10400 => new BuildingCost { Gold = 900, BuildTimeMinutes = 6 }, + 10500 => new BuildingCost { Gold = 700, BuildTimeMinutes = 4 }, + 10600 => new BuildingCost { Gold = 1500, BuildTimeMinutes = 8 }, + 10700 => new BuildingCost { Gold = 1100, BuildTimeMinutes = 7 }, + 10800 => new BuildingCost { Gold = 1300, BuildTimeMinutes = 8 }, + 10900 => new BuildingCost { Gold = 1600, BuildTimeMinutes = 9 }, + 11000 => new BuildingCost { Gold = 1000, BuildTimeMinutes = 6 }, + 11100 => new BuildingCost { Gold = 1400, BuildTimeMinutes = 8 }, + 11200 => new BuildingCost { Gold = 1800, BuildTimeMinutes = 10 }, + 11300 => new BuildingCost { Gold = 800, BuildTimeMinutes = 5 }, + 11400 => new BuildingCost { Gold = 2000, BuildTimeMinutes = 11 }, + 11500 => new BuildingCost { Gold = 1200, BuildTimeMinutes = 7 }, + 11600 => new BuildingCost { Gold = 1700, BuildTimeMinutes = 9 }, + 11700 => new BuildingCost { Gold = 1300, BuildTimeMinutes = 8 }, + 11800 => new BuildingCost { Gold = 900, BuildTimeMinutes = 6 }, + 11900 => new BuildingCost { Gold = 2200, BuildTimeMinutes = 12 }, + 12000 => new BuildingCost { Gold = 1500, BuildTimeMinutes = 9 }, + 12100 => new BuildingCost { Gold = 3000, BuildTimeMinutes = 15 }, + 12200 => new BuildingCost { Gold = 2800, BuildTimeMinutes = 14 }, + 12300 => new BuildingCost { Gold = 1600, BuildTimeMinutes = 9 }, + + // default building cost + _ => new BuildingCost { Gold = 1000, BuildTimeMinutes = 5 } + }; + } + protected override async Task HandleAsync() { ReqBuilding req = await ReadData(); + User user = GetUser(); + + + BuildingCost cost = GetBuildingCost(req.BuildingId); + + if (!user.CanSubtractCurrency(CurrencyType.Gold, cost.Gold)) + { + + ResBuilding errorResponse = new() + { + StartAt = 0, + CompleteAt = 0 + }; + await WriteDataAsync(errorResponse); + return; + } + + bool goldDeducted = user.SubtractCurrency(CurrencyType.Gold, cost.Gold); + if (!goldDeducted) + { + ResBuilding errorResponse = new() + { + StartAt = 0, + CompleteAt = 0 + }; + await WriteDataAsync(errorResponse); + return; + } + + + DateTime startTime = DateTime.UtcNow; + DateTime completeTime = startTime.AddMinutes(cost.BuildTimeMinutes); + + NetUserOutpostData newBuilding = new NetUserOutpostData() + { + SlotId = req.PositionId, + BuildingId = req.BuildingId, + IsDone = false, + StartAt = startTime.Ticks, + CompleteAt = completeTime.Ticks + }; + + bool found = false; + for (int i = 0; i < user.OutpostBuildings.Count; i++) + { + if (user.OutpostBuildings[i].SlotId == req.PositionId) + { + user.OutpostBuildings[i] = newBuilding; + found = true; + break; + } + } + + if (!found) + { + user.OutpostBuildings.Add(newBuilding); + } + + JsonDb.Save(); ResBuilding response = new() { - StartAt = DateTime.UtcNow.Ticks, - CompleteAt = DateTime.UtcNow.AddDays(1).Ticks + StartAt = newBuilding.StartAt, + CompleteAt = newBuilding.CompleteAt }; - // TODO + await WriteDataAsync(response); } } + + public class BuildingCost + { + public long Gold { get; set; } = 0; + public int BuildTimeMinutes { get; set; } = 5; + // public long Materials { get; set; } = 0; + // public long SpecialCurrency { get; set; } = 0; + } } diff --git a/EpinelPS/LobbyServer/Outpost/CheckInfracore.cs b/EpinelPS/LobbyServer/Outpost/CheckInfracore.cs index f2f0384..aa2708e 100644 --- a/EpinelPS/LobbyServer/Outpost/CheckInfracore.cs +++ b/EpinelPS/LobbyServer/Outpost/CheckInfracore.cs @@ -1,4 +1,5 @@ using EpinelPS.Utils; +using EpinelPS.Data; namespace EpinelPS.LobbyServer.Outpost { @@ -10,7 +11,22 @@ namespace EpinelPS.LobbyServer.Outpost ReqCheckReceiveInfraCoreReward req = await ReadData(); ResCheckReceiveInfraCoreReward response = new(); - // TODO + User user = GetUser(); + + bool isReceived = false; + + int currentLevel = user.InfraCoreLvl; + + Dictionary gradeTable = GameData.Instance.InfracoreTable; + if (gradeTable.TryGetValue(currentLevel, out var gradeData)) + { + if (gradeData.reward_id > 0) + { + isReceived = user.InfraCoreRewardReceived.ContainsKey(currentLevel) && user.InfraCoreRewardReceived[currentLevel]; + } + } + + response.IsReceived = isReceived; await WriteDataAsync(response); } diff --git a/EpinelPS/LobbyServer/Outpost/GetOutpostData.cs b/EpinelPS/LobbyServer/Outpost/GetOutpostData.cs index 08715c8..0421e33 100644 --- a/EpinelPS/LobbyServer/Outpost/GetOutpostData.cs +++ b/EpinelPS/LobbyServer/Outpost/GetOutpostData.cs @@ -1,5 +1,6 @@ -using EpinelPS.Utils; +using EpinelPS.Utils; using EpinelPS.Data; +using EpinelPS.Database; namespace EpinelPS.LobbyServer.Outpost { [PacketPath("/outpost/getoutpostdata")] @@ -37,17 +38,41 @@ namespace EpinelPS.LobbyServer.Outpost response.OutpostBattleLevel = user.OutpostBattleLevel; response.OutpostBattleTime = new NetOutpostBattleTime() { MaxBattleTime = 864000000000, MaxOverBattleTime = 12096000000000, BattleTime = battleTimeMs, OverBattleTime = overBattleTime }; - response.Data.Add(new NetUserOutpostData() { SlotId = 1, BuildingId = 22401, IsDone = true, StartAt = 638549982076760660, CompleteAt = 638549982076760660 }); - response.Data.Add(new NetUserOutpostData() { SlotId = 4, BuildingId = 22701, IsDone = true, StartAt = 638549982076760660, CompleteAt = 638549982076760660 }); - response.Data.Add(new NetUserOutpostData() { SlotId = 5, BuildingId = 22801, IsDone = true, StartAt = 638549982076760660, CompleteAt = 638549982076760660 }); - response.Data.Add(new NetUserOutpostData() { SlotId = 6, BuildingId = 22901, IsDone = true, StartAt = 638549982076760660, CompleteAt = 638549982076760660 }); - response.Data.Add(new NetUserOutpostData() { SlotId = 7, BuildingId = 23001, IsDone = true, StartAt = 638549982076760660, CompleteAt = 638549982076760660 }); - response.Data.Add(new NetUserOutpostData() { SlotId = 3, BuildingId = 23101, IsDone = true, StartAt = 638549982076760660, CompleteAt = 638549982076760660 }); - response.Data.Add(new NetUserOutpostData() { SlotId = 2, BuildingId = 23201, IsDone = true, StartAt = 638549982076760660, CompleteAt = 638549982076760660 }); - response.Data.Add(new NetUserOutpostData() { SlotId = 9, BuildingId = 23301, IsDone = true, StartAt = 638549982076760660, CompleteAt = 638549982076760660 }); - response.Data.Add(new NetUserOutpostData() { SlotId = 8, BuildingId = 23401, IsDone = true, StartAt = 638549982076760660, CompleteAt = 638549982076760660 }); - response.Data.Add(new NetUserOutpostData() { SlotId = 10, BuildingId = 23501, IsDone = true, StartAt = 638549982076760660, CompleteAt = 638549982076760660 }); - response.Data.Add(new NetUserOutpostData() { SlotId = 38, BuildingId = 33601, IsDone = true, StartAt = 638549982076760660, CompleteAt = 638549982076760660 }); + // defult building + if (user.OutpostBuildings == null || user.OutpostBuildings.Count == 0) + { + var defaultBuildings = new List + { + new() { SlotId = 1, BuildingId = 22401, IsDone = true, StartAt = 638549982076760660, CompleteAt = 638549982076760660 }, + new() { SlotId = 4, BuildingId = 22701, IsDone = true, StartAt = 638549982076760660, CompleteAt = 638549982076760660 }, + new() { SlotId = 5, BuildingId = 22801, IsDone = true, StartAt = 638549982076760660, CompleteAt = 638549982076760660 }, + new() { SlotId = 6, BuildingId = 22901, IsDone = true, StartAt = 638549982076760660, CompleteAt = 638549982076760660 }, + new() { SlotId = 7, BuildingId = 23001, IsDone = true, StartAt = 638549982076760660, CompleteAt = 638549982076760660 }, + new() { SlotId = 3, BuildingId = 23101, IsDone = true, StartAt = 638549982076760660, CompleteAt = 638549982076760660 }, + new() { SlotId = 2, BuildingId = 23201, IsDone = true, StartAt = 638549982076760660, CompleteAt = 638549982076760660 }, + new() { SlotId = 9, BuildingId = 23301, IsDone = true, StartAt = 638549982076760660, CompleteAt = 638549982076760660 }, + new() { SlotId = 8, BuildingId = 23401, IsDone = true, StartAt = 638549982076760660, CompleteAt = 638549982076760660 }, + new() { SlotId = 10, BuildingId = 23501, IsDone = true, StartAt = 638549982076760660, CompleteAt = 638549982076760660 }, + new() { SlotId = 38, BuildingId = 33601, IsDone = true, StartAt = 638549982076760660, CompleteAt = 638549982076760660 } + }; + + response.Data.AddRange(defaultBuildings); + user.OutpostBuildings = defaultBuildings; + JsonDb.Save(); + } + else + { + foreach (var building in user.OutpostBuildings) + { + + if (!building.IsDone && DateTime.UtcNow.Ticks >= building.CompleteAt) + { + building.IsDone = true; + } + } + response.Data.AddRange(user.OutpostBuildings); + JsonDb.Save(); + } response.TimeRewardBuffs.AddRange(NetUtils.GetOutpostTimeReward(user)); @@ -55,4 +80,4 @@ namespace EpinelPS.LobbyServer.Outpost await WriteDataAsync(response); } } -} +} \ No newline at end of file diff --git a/EpinelPS/LobbyServer/Outpost/ObtainInfracoreReward.cs b/EpinelPS/LobbyServer/Outpost/ObtainInfracoreReward.cs new file mode 100644 index 0000000..cd35d83 --- /dev/null +++ b/EpinelPS/LobbyServer/Outpost/ObtainInfracoreReward.cs @@ -0,0 +1,38 @@ +using EpinelPS.Utils; +using EpinelPS.Data; + +namespace EpinelPS.LobbyServer.Outpost +{ + [PacketPath("/infracore/reward")] + public class ObtainInfracoreReward : LobbyMsgHandler + { + protected override async Task HandleAsync() + { + ReqObtainInfraCoreReward req = await ReadData(); + ResObtainInfraCoreReward response = new(); + + User user = GetUser(); + + int currentLevel = user.InfraCoreLvl; + + Dictionary gradeTable = GameData.Instance.InfracoreTable; + if (gradeTable.TryGetValue(currentLevel, out var gradeData)) + { + if (gradeData.reward_id > 0) + { + bool isReceived = user.InfraCoreRewardReceived.ContainsKey(currentLevel) && user.InfraCoreRewardReceived[currentLevel]; + + if (!isReceived) + { + user.InfraCoreRewardReceived[currentLevel] = true; + + var reward = RewardUtils.RegisterRewardsForUser(user, gradeData.reward_id); + response.Reward = reward; + } + } + } + + await WriteDataAsync(response); + } + } +} \ No newline at end of file diff --git a/EpinelPS/LobbyServer/Stage/ClearStage.cs b/EpinelPS/LobbyServer/Stage/ClearStage.cs index e5dc199..0283446 100644 --- a/EpinelPS/LobbyServer/Stage/ClearStage.cs +++ b/EpinelPS/LobbyServer/Stage/ClearStage.cs @@ -28,7 +28,10 @@ namespace EpinelPS.LobbyServer.Stage public static ResClearStage CompleteStage(User user, int StageId, bool forceCompleteScenarios = false) { - ResClearStage response = new(); + ResClearStage response = new() + { + OutpostTimeRewardBuff = new() + }; CampaignStageRecord clearedStage = GameData.Instance.GetStageData(StageId) ?? throw new Exception("cleared stage cannot be null"); string stageMapId = GameData.Instance.GetMapIdFromChapter(clearedStage.chapter_id, clearedStage.chapter_mod); diff --git a/EpinelPS/LobbyServer/Stage/FastClear.cs b/EpinelPS/LobbyServer/Stage/FastClear.cs index 913ce76..70726d7 100644 --- a/EpinelPS/LobbyServer/Stage/FastClear.cs +++ b/EpinelPS/LobbyServer/Stage/FastClear.cs @@ -21,10 +21,14 @@ namespace EpinelPS.LobbyServer.Stage OutpostBattle = rsp.OutpostBattle, OutpostBattleLevelReward = rsp.OutpostBattleLevelReward, StageClearReward = rsp.StageClearReward, - UserLevelUpReward = rsp.UserLevelUpReward + UserLevelUpReward = rsp.UserLevelUpReward, + OutpostTimeRewardBuff = new() }; - response.OutpostTimeRewardBuff.TimeRewardBuffs.AddRange(rsp.OutpostTimeRewardBuff.TimeRewardBuffs); + if (rsp.OutpostTimeRewardBuff != null) + { + response.OutpostTimeRewardBuff.TimeRewardBuffs.AddRange(rsp.OutpostTimeRewardBuff.TimeRewardBuffs); + } await WriteDataAsync(response); } diff --git a/EpinelPS/Models/CoreInfoModel.cs b/EpinelPS/Models/CoreInfoModel.cs index 02948f3..b397ef4 100644 --- a/EpinelPS/Models/CoreInfoModel.cs +++ b/EpinelPS/Models/CoreInfoModel.cs @@ -3,7 +3,7 @@ using EpinelPS.Utils; namespace EpinelPS.Models; public class CoreInfo { - public int DbVersion = 3; + public int DbVersion = 5; public List Users = []; public List LauncherAccessTokens = []; diff --git a/EpinelPS/Models/DbModels.cs b/EpinelPS/Models/DbModels.cs index 81a43a6..0854b15 100644 --- a/EpinelPS/Models/DbModels.cs +++ b/EpinelPS/Models/DbModels.cs @@ -61,6 +61,9 @@ namespace EpinelPS.Models public int Position; public int Corp; public long Isn; + + // For harmony cubes that can be equipped to multiple characters + public List CsnList = []; } public class EventData { @@ -106,6 +109,9 @@ namespace EpinelPS.Models public List CompletedDailyMissions = []; public int DailyMissionPoints; public SimroomData SimRoomData = new(); + + public bool UnlimitedCounseling = false; + public Dictionary DailyCounselCount = []; } public class WeeklyResetableData { diff --git a/EpinelPS/Models/UserModel.cs b/EpinelPS/Models/UserModel.cs index df51ad7..6376a23 100644 --- a/EpinelPS/Models/UserModel.cs +++ b/EpinelPS/Models/UserModel.cs @@ -48,6 +48,9 @@ public class User public long[] RepresentationTeamDataNew = []; public Dictionary ClearedTutorialData = []; + // Outpost buildings data + public List OutpostBuildings = []; + public NetWallpaperData[] WallpaperList = []; public NetWallpaperBackground[] WallpaperBackground = []; public NetWallpaperJukeboxFavorite[] WallpaperFavoriteList = []; @@ -61,6 +64,7 @@ public class User public Dictionary SubQuestData = []; public int InfraCoreExp = 0; public int InfraCoreLvl = 1; + public Dictionary InfraCoreRewardReceived = []; public UserPointData userPointData = new(); public DateTime LastLogin = DateTime.UtcNow; public DateTime BattleTime = DateTime.UtcNow; @@ -72,7 +76,9 @@ public class User public List Memorial = []; public List JukeboxBgm = []; + public List FavoriteItems = []; + public List FavoriteItemQuests = []; public Dictionary TowerProgress = []; public JukeBoxSetting LobbyMusic = new() { Location = NetJukeboxLocation.Lobby, TableId = 2, Type = NetJukeboxBgmType.JukeboxTableId }; diff --git a/EpinelPS/Utils/GameConfig.cs b/EpinelPS/Utils/GameConfig.cs index 9f08460..e8f3d8f 100644 --- a/EpinelPS/Utils/GameConfig.cs +++ b/EpinelPS/Utils/GameConfig.cs @@ -1,5 +1,5 @@ -using System.Text.Json; using EpinelPS.Database; +using Newtonsoft.Json; namespace EpinelPS.Utils { @@ -51,7 +51,7 @@ namespace EpinelPS.Utils Console.WriteLine("Loaded game config"); - _root = JsonSerializer.Deserialize(File.ReadAllText(AppDomain.CurrentDomain.BaseDirectory + "/gameconfig.json")); + _root = JsonConvert.DeserializeObject(File.ReadAllText(AppDomain.CurrentDomain.BaseDirectory + "/gameconfig.json")); if (_root == null) { @@ -67,8 +67,8 @@ namespace EpinelPS.Utils { if (Root != null) { - File.WriteAllText(AppDomain.CurrentDomain.BaseDirectory + "/gameconfig.json", JsonSerializer.Serialize(Root, JsonDb.IndentedJson)); + File.WriteAllText(AppDomain.CurrentDomain.BaseDirectory + "/gameconfig.json", JsonConvert.SerializeObject(Root, Formatting.Indented)); } } } -} +} \ No newline at end of file diff --git a/EpinelPS/Utils/NetUtils.cs b/EpinelPS/Utils/NetUtils.cs index 2073599..810ac63 100644 --- a/EpinelPS/Utils/NetUtils.cs +++ b/EpinelPS/Utils/NetUtils.cs @@ -104,6 +104,8 @@ namespace EpinelPS.Utils return 2; case "Module_D": return 3; + case "HarmonyCube": + return GetHarmonyCubePosition(item.ItemType); default: Console.WriteLine("Unknown item subtype: " + subType); break; @@ -114,6 +116,15 @@ namespace EpinelPS.Utils return 0; } + + public static int GetHarmonyCubePosition(int itemType) + { + if (GameData.Instance.ItemHarmonyCubeTable.TryGetValue(itemType, out ItemHarmonyCubeRecord? harmonyCube)) + { + return harmonyCube.location_id; + } + return 1; + } /// /// Takes multiple NetRewardData objects and merges it into one. Note that this function expects that rewards are already applied to user object. /// @@ -391,6 +402,8 @@ namespace EpinelPS.Utils { RandomItemRecord winningRecord = Rng.PickWeightedItem(probabilityEntries); + Logging.WriteLine($"LootBox {boxId}: Won item - Type: {winningRecord.reward_type}, ID: {winningRecord.reward_id}, Value: {winningRecord.reward_value_min}", LogType.Info); + if (winningRecord.reward_value_min != winningRecord.reward_value_max) { Logging.WriteLine("TODO: reward_value_max", LogType.Warning); diff --git a/EpinelPS/Utils/RewardUtils.cs b/EpinelPS/Utils/RewardUtils.cs index 368e1a3..b7e681c 100644 --- a/EpinelPS/Utils/RewardUtils.cs +++ b/EpinelPS/Utils/RewardUtils.cs @@ -191,11 +191,39 @@ namespace EpinelPS.Utils } else if (rewardType == "InfraCoreExp") { + int beforeLv = user.InfraCoreLvl; + int beforeExp = user.InfraCoreExp; + + user.InfraCoreExp += rewardCount; + + // Check for level ups + Dictionary gradeTable = GameData.Instance.InfracoreTable; + int newLevel = user.InfraCoreLvl; + + foreach (InfracoreRecord grade in gradeTable.Values.OrderBy(g => g.grade)) + { + if (user.InfraCoreExp >= grade.infra_core_exp) + { + newLevel = grade.grade + 1; + } + else + { + break; + } + } + + if (newLevel > user.InfraCoreLvl) + { + user.InfraCoreLvl = newLevel; + } + ret.InfraCoreExp = new NetIncreaseExpData() { - BeforeLv = user.InfraCoreLvl, - BeforeExp = user.InfraCoreExp, - // TODO + BeforeLv = beforeLv, + BeforeExp = beforeExp, + CurrentLv = user.InfraCoreLvl, + CurrentExp = user.InfraCoreExp, + GainExp = rewardCount }; } else if (rewardType == "ItemRandomBox") @@ -221,6 +249,32 @@ namespace EpinelPS.Utils user.Items.Add(new ItemData() { Count = rewardCount, Isn = itm.Isn, ItemType = itm.Tid }); } } + else if (rewardType == "FavoriteItem") + { + + NetUserFavoriteItemData newFavoriteItem = new NetUserFavoriteItemData + { + FavoriteItemId = user.GenerateUniqueItemId(), + Tid = rewardId, + Csn = 0, + Lv = 0, + Exp = 0 + }; + user.FavoriteItems.Add(newFavoriteItem); + + ret.UserFavoriteItems.Add(newFavoriteItem); + + NetFavoriteItemData favoriteItemData = new NetFavoriteItemData + { + FavoriteItemId = newFavoriteItem.FavoriteItemId, + Tid = newFavoriteItem.Tid, + Csn = newFavoriteItem.Csn, + Lv = newFavoriteItem.Lv, + Exp = newFavoriteItem.Exp + }; + ret.FavoriteItems.Add(favoriteItemData); + + } else { Logging.WriteLine("TODO: Reward type " + rewardType, LogType.Warning);