implementation for the Favorite Item ,Character Counse and Harmony Cube systems (#51)

* 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 <mishakeys20@gmail.com>
This commit is contained in:
fxz2018
2025-09-10 07:05:12 +08:00
committed by GitHub
parent ee15420257
commit 2e4310edf1
40 changed files with 2356 additions and 111 deletions

View File

@@ -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<int, AttractiveLevelRewardRecord> AttractiveLevelReward = [];
[LoadRecord("AttractiveLevelTable.json", "id")]
public readonly Dictionary<int, AttractiveLevelRecord> AttractiveLevelTable = [];
[LoadRecord("SubQuestTable.json", "id")]
public readonly Dictionary<int, SubquestRecord> Subquests = [];
@@ -200,6 +203,32 @@ namespace EpinelPS.Data
[LoadRecord("RecycleResearchLevelTable.json", "id")]
public readonly Dictionary<int, RecycleResearchLevelRecord> RecycleResearchLevels = [];
// Harmony Cube Data Tables
[LoadRecord("ItemHarmonyCubeTable.json", "id")]
public readonly Dictionary<int, ItemHarmonyCubeRecord> ItemHarmonyCubeTable = [];
[LoadRecord("ItemHarmonyCubeLevelTable.json", "id")]
public readonly Dictionary<int, ItemHarmonyCubeLevelRecord> ItemHarmonyCubeLevelTable = [];
// Favorite Item Data Tables
[LoadRecord("FavoriteItemTable.json", "id")]
public readonly Dictionary<int, FavoriteItemRecord> FavoriteItemTable = [];
[LoadRecord("FavoriteItemExpTable.json", "id")]
public readonly Dictionary<int, FavoriteItemExpRecord> FavoriteItemExpTable = [];
[LoadRecord("FavoriteItemLevelTable.json", "id")]
public readonly Dictionary<int, FavoriteItemLevelRecord> FavoriteItemLevelTable = [];
[LoadRecord("FavoriteItemProbabilityTable.json", "id")]
public readonly Dictionary<int, FavoriteItemProbabilityRecord> FavoriteItemProbabilityTable = [];
[LoadRecord("FavoriteItemQuestTable.json", "id")]
public readonly Dictionary<int, FavoriteItemQuestRecord> FavoriteItemQuestTable = [];
[LoadRecord("FavoriteItemQuestStageTable.json", "id")]
public readonly Dictionary<int, FavoriteItemQuestStageRecord> FavoriteItemQuestStageTable = [];
static async Task<GameData> BuildAsync()
{
@@ -392,7 +421,9 @@ namespace EpinelPS.Data
}
else
{
DataTable<X> obj = await JsonSerializer.DeserializeAsync<DataTable<X>>(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<X> obj = JsonConvert.DeserializeObject<DataTable<X>>(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<int> 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;
}
}
}

View File

@@ -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<InfracoreFunction> 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<MessengerConditionTriggerList> 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,
@@ -889,4 +929,165 @@ namespace EpinelPS.Data
public List<StageSpawner> 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<HarmonyCubeSkillGroup> harmonycube_skill_group = [];
}
[MemoryPackable]
public partial class ItemHarmonyCubeLevelRecord
{
public int id;
public int level_enhance_id;
public int level;
public List<HarmonyCubeSkillLevel> skill_levels = [];
public int material_id;
public int material_value;
public int gold_value;
public int slot;
public List<HarmonyCubeStat> 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<FavoriteItemStatData> favoriteitem_stat_data = [];
public List<CollectionSkillLevelData> 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;
}
}

View File

@@ -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<CoreInfo>(File.ReadAllText(AppDomain.CurrentDomain.BaseDirectory + "/db.json"), IndentedJson);
var j = JsonConvert.DeserializeObject<CoreInfo>(File.ReadAllText(AppDomain.CurrentDomain.BaseDirectory + "/db.json"));
if (j != null)
{
Instance = j;
@@ -80,7 +77,7 @@ namespace EpinelPS.Database
Save();
}
var j = JsonSerializer.Deserialize<CoreInfo>(File.ReadAllText(AppDomain.CurrentDomain.BaseDirectory + "/db.json"));
var j = JsonConvert.DeserializeObject<CoreInfo>(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)

View File

@@ -36,7 +36,8 @@
<PackageReference Include="PeterO.Cbor" Version="4.5.5" />
<PackageReference Include="SharpZipLib" Version="1.4.2" />
<PackageReference Include="Sodium.Core" Version="1.4.0" />
<PackageReference Include="System.Text.Json" Version="9.0.8" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
<ItemGroup>

View File

@@ -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<ReqCounseledBefore>();
ResCounseledBefore response = new();
response.IsCounseledBefore = false;
await WriteDataAsync(response);
}
}

View File

@@ -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<ReqCharacterCounsel>();
User user = GetUser();
ResCharacterCounsel response = new();
foreach (KeyValuePair<CurrencyType, long> 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<ReqCharacterCounsel>();
User user = GetUser();
IEnumerable<NetUserAttractiveData> 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<CurrencyType, long> 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;
}
}
}
}
}

View File

@@ -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<ReqCharacterPresent>();
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;
}
}
}
}
}

View File

@@ -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<ReqCharacterQuickCounsel>();
User user = GetUser();
ResCharacterQuickCounsel response = new ResCharacterQuickCounsel();
foreach (KeyValuePair<CurrencyType, long> 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;
}
}
}
}
}

View File

@@ -1,4 +1,4 @@
using EpinelPS.Utils;
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Character
{
@@ -19,10 +19,8 @@ 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);

View File

@@ -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<ReqClearFavoriteItemQuestStage>();
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);
}
}
}

View File

@@ -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<ReqEnterFavoriteItemQuestStage>();
User user = GetUser();
user.AddTrigger(TriggerType.CampaignStart, 1, req.StageId);
JsonDb.Save();
ResEnterFavoriteItemQuestStage response = new();
await WriteDataAsync(response);
}
}
}

View File

@@ -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<ReqEquipFavoriteItem>();
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);
}
}
}

View File

@@ -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<ReqFinishFavoriteItemQuest>();
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;
}
}
}

View File

@@ -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);
}

View File

@@ -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<int, int> favoriteExpTable = new()
{
{ 7150001, 20 }, // 20 exp
{ 7150002, 50 }, // 50 exp
{ 7150003, 100 }, // 100 exp
};
protected override async Task HandleAsync()
{
ReqIncreaseExpFavoriteItem req = await ReadData<ReqIncreaseExpFavoriteItem>();
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<FavoriteItemExpRecord> expRecords)
{
var record = expRecords.FirstOrDefault(x => x.level == level);
return record?.need_exp ?? 0;
}
}
}

View File

@@ -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);
}
}

View File

@@ -1,4 +1,5 @@
using EpinelPS.Utils;
using EpinelPS.Utils;
using EpinelPS.Data;
namespace EpinelPS.LobbyServer.FavoriteItem
{
@@ -12,6 +13,16 @@ namespace EpinelPS.LobbyServer.FavoriteItem
ResListFavoriteItemQuest response = new();
if (user.FavoriteItemQuests == null)
{
user.FavoriteItemQuests = new List<NetUserFavoriteItemQuestData>();
}
foreach (NetUserFavoriteItemQuestData quest in user.FavoriteItemQuests)
{
response.FavoriteItemQuests.Add(quest);
}
await WriteDataAsync(response);
}
}

View File

@@ -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<ReqObtainFavoriteItemQuestReward>();
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<CharacterRecord> 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<int> 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);
}
}
}

View File

@@ -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<ReqStartFavoriteItemQuest>();
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);
}
}
}

View File

@@ -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<ReqIncreaseExpFavoriteItem>();
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);
}
}
}

View File

@@ -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<ReqClearHarmonyCube>();
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);
}
}
}

View File

@@ -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<ReqGetHarmonyCube>();
User user = GetUser();
ResGetHarmonyCube response = new();
List<ItemData> 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);
}
}
}

View File

@@ -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<ReqLevelUpHarmonyCube>();
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<ItemHarmonyCubeLevelRecord> 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);
}
}
}

View File

@@ -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<ReqManagementHarmonyCube>();
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<ItemData> 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<ItemHarmonyCubeLevelRecord> 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;
}
}
}

View File

@@ -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<ReqWearHarmonyCube>();
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<ItemData> 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<ItemData> 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<ItemData>(); // 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<ItemData> 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<ItemData> 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<ItemData> 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<ItemHarmonyCubeLevelRecord> 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;
}
}
}

View File

@@ -1,4 +1,4 @@
using EpinelPS.Data;
using EpinelPS.Data;
using EpinelPS.Database;
using EpinelPS.Utils;
@@ -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<NetUserOutpostData> defaultBuildings = new List<NetUserOutpostData>
{
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));

View File

@@ -14,8 +14,24 @@ namespace EpinelPS.LobbyServer.Messenger
ResEnterMessengerDialog response = new();
MessengerMsgConditionRecord opener = GameData.Instance.MessageConditions[req.Tid];
KeyValuePair<string, MessengerDialogRecord> 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<string, MessengerDialogRecord> 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);

View File

@@ -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<ReqGetMessages>();
User user = GetUser();
CheckAndCreateAvailableMessages(user);
ResGetMessages response = new();
IEnumerable<NetMessage> 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<int, MessengerMsgConditionRecord> 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<string, MessengerDialogRecord> 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
};
}
}
}

View File

@@ -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<ReqBuilding>();
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;
}
}

View File

@@ -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<ReqCheckReceiveInfraCoreReward>();
ResCheckReceiveInfraCoreReward response = new();
// TODO
User user = GetUser();
bool isReceived = false;
int currentLevel = user.InfraCoreLvl;
Dictionary<int, InfracoreRecord> 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);
}

View File

@@ -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<NetUserOutpostData>
{
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));

View File

@@ -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<ReqObtainInfraCoreReward>();
ResObtainInfraCoreReward response = new();
User user = GetUser();
int currentLevel = user.InfraCoreLvl;
Dictionary<int, InfracoreRecord> 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);
}
}
}

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -3,7 +3,7 @@ using EpinelPS.Utils;
namespace EpinelPS.Models;
public class CoreInfo
{
public int DbVersion = 3;
public int DbVersion = 5;
public List<User> Users = [];
public List<AccessToken> LauncherAccessTokens = [];

View File

@@ -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<long> CsnList = [];
}
public class EventData
{
@@ -106,6 +109,9 @@ namespace EpinelPS.Models
public List<int> CompletedDailyMissions = [];
public int DailyMissionPoints;
public SimroomData SimRoomData = new();
public bool UnlimitedCounseling = false;
public Dictionary<int, int> DailyCounselCount = [];
}
public class WeeklyResetableData
{

View File

@@ -48,6 +48,9 @@ public class User
public long[] RepresentationTeamDataNew = [];
public Dictionary<int, ClearedTutorialData> ClearedTutorialData = [];
// Outpost buildings data
public List<NetUserOutpostData> OutpostBuildings = [];
public NetWallpaperData[] WallpaperList = [];
public NetWallpaperBackground[] WallpaperBackground = [];
public NetWallpaperJukeboxFavorite[] WallpaperFavoriteList = [];
@@ -61,6 +64,7 @@ public class User
public Dictionary<int, bool> SubQuestData = [];
public int InfraCoreExp = 0;
public int InfraCoreLvl = 1;
public Dictionary<int, bool> InfraCoreRewardReceived = [];
public UserPointData userPointData = new();
public DateTime LastLogin = DateTime.UtcNow;
public DateTime BattleTime = DateTime.UtcNow;
@@ -72,7 +76,9 @@ public class User
public List<int> Memorial = [];
public List<int> JukeboxBgm = [];
public List<NetUserFavoriteItemData> FavoriteItems = [];
public List<NetUserFavoriteItemQuestData> FavoriteItemQuests = [];
public Dictionary<int, int> TowerProgress = [];
public JukeBoxSetting LobbyMusic = new() { Location = NetJukeboxLocation.Lobby, TableId = 2, Type = NetJukeboxBgmType.JukeboxTableId };

View File

@@ -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<GameConfigRoot>(File.ReadAllText(AppDomain.CurrentDomain.BaseDirectory + "/gameconfig.json"));
_root = JsonConvert.DeserializeObject<GameConfigRoot>(File.ReadAllText(AppDomain.CurrentDomain.BaseDirectory + "/gameconfig.json"));
if (_root == null)
{
@@ -67,7 +67,7 @@ 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));
}
}
}

View File

@@ -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;
}
/// <summary>
/// Takes multiple NetRewardData objects and merges it into one. Note that this function expects that rewards are already applied to user object.
/// </summary>
@@ -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);

View File

@@ -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<int, InfracoreRecord> 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);